/* ============================================================ HESU — homepage interactions (vanilla JS, no dependencies) ============================================================ */ (function () { 'use strict'; /* ---------- header scrolled state ---------- */ var header = document.getElementById('siteHeader'); function onScroll() { if (!header) return; if (window.scrollY > 40) header.classList.add('scrolled'); else header.classList.remove('scrolled'); } window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); /* ---------- mobile drawer ---------- */ var toggle = document.getElementById('menuToggle'); var nav = document.getElementById('mainNav'); // backdrop var backdrop = document.createElement('div'); backdrop.className = 'nav-backdrop'; document.body.appendChild(backdrop); function closeNav() { if (!nav || !toggle) return; nav.classList.remove('open'); toggle.classList.remove('open'); toggle.setAttribute('aria-expanded', 'false'); backdrop.classList.remove('show'); document.body.style.overflow = ''; } function openNav() { nav.classList.add('open'); toggle.classList.add('open'); toggle.setAttribute('aria-expanded', 'true'); backdrop.classList.add('show'); document.body.style.overflow = 'hidden'; } if (toggle && nav) { toggle.addEventListener('click', function () { nav.classList.contains('open') ? closeNav() : openNav(); }); } backdrop.addEventListener('click', closeNav); // close on link click (mobile) if (nav) { nav.querySelectorAll('a').forEach(function (a) { a.addEventListener('click', function () { if (window.innerWidth <= 860) { // allow mega parent toggle to work first on mobile if (a.parentElement.classList.contains('has-mega')) return; closeNav(); } }); }); /* ---------- mobile dropdown collapsibles ---------- */ nav.querySelectorAll('.nav-item.has-mega > a').forEach(function (trigger) { trigger.addEventListener('click', function (e) { if (window.innerWidth > 860) return; e.preventDefault(); var li = trigger.parentElement; // close other open items nav.querySelectorAll('.nav-item.has-mega.open').forEach(function (other) { if (other !== li) other.classList.remove('open'); }); li.classList.toggle('open'); }); }); } /* ---------- Full-screen mega menus (desktop hover; mobile uses drawer collapsible) ---------- */ (function () { var items = Array.prototype.slice.call(document.querySelectorAll('.nav-item.has-mega')); if (!items.length) return; var openTimer, closeTimer; function closeAll() { document.querySelectorAll('.mega.open').forEach(function (m) { m.classList.remove('open'); }); items.forEach(function (it) { it.classList.remove('mega-on'); }); } function open(mega, item) { closeAll(); mega.classList.add('open'); item.classList.add('mega-on'); } items.forEach(function (item) { var mega = item.querySelector('.mega'); if (!mega) return; item.addEventListener('mouseenter', function () { if (window.innerWidth <= 860) return; clearTimeout(closeTimer); openTimer = setTimeout(function () { open(mega, item); }, 120); }); item.addEventListener('mouseleave', function () { if (window.innerWidth <= 860) return; clearTimeout(openTimer); closeTimer = setTimeout(closeAll, 200); }); mega.querySelectorAll('.mega-card').forEach(function (c) { c.addEventListener('click', function () { closeAll(); }); }); }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeAll(); }); })(); /* ---------- partner carousel ---------- */ var carousel = document.getElementById('partnerCarousel'); var dotsWrap = document.getElementById('partnerDots'); if (carousel && dotsWrap) { var slides = Array.prototype.slice.call(carousel.querySelectorAll('.slide')); var idx = 0; var timer = null; slides.forEach(function (_, i) { var b = document.createElement('button'); b.setAttribute('role', 'tab'); b.setAttribute('aria-label', 'Slide ' + (i + 1)); b.addEventListener('click', function () { goTo(i, true); }); dotsWrap.appendChild(b); }); var dots = Array.prototype.slice.call(dotsWrap.children); function render() { slides.forEach(function (s, i) { s.classList.toggle('is-active', i === idx); }); dots.forEach(function (d, i) { d.classList.toggle('active', i === idx); }); } function goTo(i, user) { idx = (i + slides.length) % slides.length; render(); if (user) restart(); } function next() { goTo(idx + 1); } function stop() { clearInterval(timer); timer = null; } function start() { stop(); timer = setInterval(next, 5000); } function restart() { start(); } render(); start(); carousel.addEventListener('mouseenter', stop); carousel.addEventListener('mouseleave', start); } /* ---------- scroll reveal ---------- */ var reveals = document.querySelectorAll('[data-reveal], .reveal'); /* unified reveal — page scripts removed */ if ('IntersectionObserver' in window) { var io = new IntersectionObserver(function (entries) { entries.forEach(function (en) { /* reveal-repeat: replay the stagger every time the element enters the viewport (reset while fully out) — used by the bento business cells */ if (en.target.classList.contains('reveal-repeat')) { en.target.classList.toggle('in', en.isIntersecting); return; } if (en.isIntersecting) { en.target.classList.add('in'); io.unobserve(en.target); } }); }, { threshold: 0.12 }); reveals.forEach(function (el) { io.observe(el); }); } else { reveals.forEach(function (el) { el.classList.add('in'); }); } /* ---------- stats count-up ---------- */ function animateCount(el) { var target = parseInt(el.getAttribute('data-count'), 10); if (isNaN(target)) return; var plus = el.querySelector('.plus'); var dur = 1400, start = null; function step(ts) { if (start === null) start = ts; var p = Math.min((ts - start) / dur, 1); var eased = 1 - Math.pow(1 - p, 3); // easeOutCubic var val = Math.floor(eased * target); el.firstChild.nodeValue = val.toLocaleString(); if (p < 1) requestAnimationFrame(step); else el.firstChild.nodeValue = target.toLocaleString(); } requestAnimationFrame(step); } var stats = document.querySelectorAll('.stat-num'); if ('IntersectionObserver' in window) { var so = new IntersectionObserver(function (entries) { entries.forEach(function (en) { if (en.isIntersecting) { animateCount(en.target); so.unobserve(en.target); } }); }, { threshold: 0.4 }); stats.forEach(function (el) { so.observe(el); }); } /* ---------- Network peek carousel — drag to slide, seamless infinite loop (same drag system on desktop & mobile) ---------- */ (function () { var stage = document.querySelector('.network-stage'); if (!stage) return; var strip = stage.querySelector('.network-strip'); var track = stage.querySelector('.network-track'); if (!track) return; var realCards = Array.prototype.slice.call(track.querySelectorAll('.network-card')); var n = realCards.length; if (!n) return; var GAP = 18, VISIBLE = 3.6, PAIRED = true; // desktop defaults; mobile re-tunes in applyMode var pos = n; // continuous position (left card of centered pair) var cardW = 0, cardStep = 1, stripW = 0, centerBase = 0; var cloned = false, enabled = false; // build [clone][real][clone] so both edges always have cards (no blank when looping) function setClones(on) { if (on && !cloned) { var pre = document.createDocumentFragment(); var post = document.createDocumentFragment(); realCards.forEach(function (c) { var a = c.cloneNode(true); a.setAttribute('data-clone', ''); pre.appendChild(a); var b = c.cloneNode(true); b.setAttribute('data-clone', ''); post.appendChild(b); }); track.insertBefore(pre, track.firstChild); track.appendChild(post); cloned = true; } else if (!on && cloned) { Array.prototype.slice.call(track.querySelectorAll('.network-card[data-clone]')).forEach(function (c) { c.remove(); }); cloned = false; } } function measure() { stripW = strip.getBoundingClientRect().width; cardW = stripW / VISIBLE; cardStep = cardW + GAP; centerBase = PAIRED ? (stripW - 2 * cardW - GAP) / 2 : (stripW - cardW) / 2; var all = track.querySelectorAll('.network-card'); for (var i = 0; i < all.length; i++) all[i].style.flexBasis = cardW + 'px'; } function transformFor(p) { return 'translateX(' + (centerBase - p * cardStep) + 'px)'; } function render(animate) { track.style.transition = animate ? '' : 'none'; track.style.transform = transformFor(pos); } function normalize() { pos = n + (((pos - n) % n) + n) % n; } // wrap into real band [n, 2n) // after a slide settles, silently fold position back into the real band (seamless) track.addEventListener('transitionend', function (e) { if (enabled && e.propertyName === 'transform') { normalize(); render(false); } }); // drag to slide (pointer = mouse + touch) var dragging = false, startX = 0, startP = 0, delta = 0, moved = false; function onDown(e) { if (!enabled) return; dragging = true; moved = false; delta = 0; startX = e.clientX; startP = pos; track.classList.add('dragging'); } function onMove(e) { if (!dragging) return; delta = e.clientX - startX; if (Math.abs(delta) > 6) moved = true; track.style.transform = transformFor(startP - delta / cardStep); } function onUp() { if (!dragging) return; dragging = false; track.classList.remove('dragging'); pos = Math.round(startP - delta / cardStep); // snap to nearest card render(true); // animate; transitionend folds it back } strip.addEventListener('pointerdown', onDown); window.addEventListener('pointermove', onMove); window.addEventListener('pointerup', onUp); window.addEventListener('pointercancel', onUp); // suppress link navigation right after a real drag track.addEventListener('click', function (e) { if (moved) { e.preventDefault(); e.stopPropagation(); } }, true); function applyMode() { var mobile = window.matchMedia('(max-width: 860px)').matches; PAIRED = !mobile; // desktop centers a card pair; mobile centers one card GAP = mobile ? 12 : 18; VISIBLE = mobile ? 1.32 : 3.6; // mobile: centered card + neighbour peeks enabled = true; setClones(true); measure(); if (!track.dataset.init) { pos = n; track.dataset.init = '1'; } if (pos < 1 || pos >= 2 * n) pos = n; render(false); } var rT; window.addEventListener('resize', function () { clearTimeout(rT); rT = setTimeout(applyMode, 150); }); applyMode(); })(); /* ---------- presence map — bloom all, then rotating-window highlights ---------- Entering the viewport blooms ALL locations at once; the cycle then runs the sliding window straight from its steady state: every tick the next location replays its full entrance while the oldest visible one exits (wrapping around, the last location returns exactly as the first one leaves). Four stay visible. Leaving the viewport gracefully exits everything; coming back blooms again. */ (function () { var overlay = document.querySelector('.map-overlay'); if (!overlay) return; var groups = Array.prototype.slice.call(overlay.querySelectorAll('.location')); if (!groups.length) return; function show(i) { groups.forEach(function (g, j) { g.classList.toggle('active', j === i); }); } // drop the DUBAI active baked into the HTML (no-JS keeps it as a static fallback) show(-1); var tick = 0, timer = null, run = 0; // stride-2 visit order (0,2,4,1,3) so the point entering and the point // exiting each step are far apart on the map instead of neighbours var order = []; var stride = groups.length % 2 === 1 ? 2 : (groups.length > 3 ? 3 : 1); for (var i = 0; i < groups.length; i++) order.push((i * stride) % groups.length); // deactivate with the reverse (.out) choreography — class is lifted when it ends function exit(el) { el.classList.remove('active'); if (el.classList.contains('out')) return; // already on its way out el.classList.add('out'); setTimeout(function () { el.classList.remove('out'); }, 650); } function step() { var on = order[tick % order.length]; var off = order[(tick + 1) % order.length]; groups.forEach(function (g, i) { if (i === on) { // remove → flush → re-add so the entrance sequence replays g.classList.remove('active'); g.getBoundingClientRect(); g.classList.remove('out'); g.classList.add('active'); } else if (i === off) { exit(g); } }); tick++; } function start() { if (timer) return; var myRun = ++run; // let one frame of the invisible base state paint first, then bloom everything requestAnimationFrame(function () { requestAnimationFrame(function () { if (run !== myRun) return; tick = 0; groups.forEach(function (g) { g.classList.remove('out'); g.classList.add('active'); }); timer = setInterval(step, 2000); }); }); timer = 1; // mark as starting (replaced by the real interval above) } function stop() { if (timer) { clearInterval(timer); timer = null; } run++; // invalidate pending callbacks tick = 0; groups.forEach(function (g) { if (g.classList.contains('active')) exit(g); }); } var wrap = document.querySelector('.map-wrap'); // Paint the page's exact background onto the wrap: same image, stretched // to .page-content's full size, offset by the wrap's position inside it — // a pixel-perfect local copy. With .map-wrap { isolation: isolate } the // blended img mixes against THIS instead of the page backdrop, so Chrome // never has to capture a cross-element backdrop when the map scrolls into // view (that capture lags a frame and flashes the map's intrinsic colors). var pc = document.querySelector('.page-content'); if (wrap && pc) { var syncBackdrop = function () { var wr = wrap.getBoundingClientRect(); var pr = pc.getBoundingClientRect(); wrap.style.backgroundImage = 'url(image/home-bgs.png)'; wrap.style.backgroundSize = pr.width + 'px ' + pc.offsetHeight + 'px'; wrap.style.backgroundPosition = (pr.left - wr.left) + 'px ' + (pr.top - wr.top) + 'px'; wrap.style.backgroundRepeat = 'no-repeat'; }; syncBackdrop(); var rT2 = null; window.addEventListener('resize', function () { clearTimeout(rT2); rT2 = setTimeout(syncBackdrop, 150); }); } // Fade the map image in only after the page has fully loaded: the image // color-dodges against the (locally copied) page background image, and // over an unloaded near-white backdrop the map washes out — showing it // early would flash a colorless map before the glow pops in. var mapImg = wrap ? wrap.querySelector('.map-bg') : null; if (mapImg) { // Reveal needs BOTH: the lazy image decoded (it paints a frame or two // before its mix-blend-mode composite kicks in) AND the page fully // loaded (color-dodge over an unloaded white backdrop washes out). // The fade is JS-driven because CSS animations/transitions lose to the // !important reveal rule in the cascade — this way the un-blended // first frames stay at near-zero opacity no matter what. var imgLoaded = mapImg.complete && mapImg.naturalWidth > 0; var pageLoaded = document.readyState === 'complete'; var tryReveal = function () { if (!imgLoaded || !pageLoaded) return; requestAnimationFrame(function () { requestAnimationFrame(function () { var t0 = null; var step = function (t) { if (t0 === null) t0 = t; var p = Math.min((t - t0) / 700, 1); mapImg.style.opacity = p.toFixed(3); if (p < 1) requestAnimationFrame(step); else mapImg.classList.add('ready'); // !important locks the final state }; requestAnimationFrame(step); }); }); }; if (!imgLoaded) mapImg.addEventListener('load', function () { imgLoaded = true; tryReveal(); }, { once: true }); if (!pageLoaded) window.addEventListener('load', function () { pageLoaded = true; tryReveal(); }, { once: true }); tryReveal(); } if ('IntersectionObserver' in window && wrap) { var mo = new IntersectionObserver(function (entries) { entries[0].isIntersecting ? start() : stop(); }, { threshold: 0.12 }); mo.observe(wrap); } else { start(); } })(); /* ---------- ae page map — bloom all, then rotating-window highlights ---------- Same logic as the homepage map: enter blooms everything, then the sliding window runs from its steady state (next marker replays in, oldest exits). */ (function () { var overlay = document.querySelector('.ae-map__overlay'); if (!overlay) return; var markers = Array.prototype.slice.call(overlay.querySelectorAll('.marker')); if (!markers.length) return; function show(i) { markers.forEach(function (m, j) { m.classList.toggle('active', j === i); }); } // drop the Belarus active baked into the HTML (no-JS keeps it as a static fallback) show(-1); var tick = 0, timer = null, run = 0; // stride-2 visit order so the marker entering and the marker exiting each // step are far apart on the map instead of neighbours var order = []; var stride = markers.length % 2 === 1 ? 2 : (markers.length > 3 ? 3 : 1); for (var i = 0; i < markers.length; i++) order.push((i * stride) % markers.length); // deactivate with the reverse (.out) choreography — class is lifted when it ends function exit(el) { el.classList.remove('active'); if (el.classList.contains('out')) return; // already on its way out el.classList.add('out'); setTimeout(function () { el.classList.remove('out'); }, 650); } function step() { var on = order[tick % order.length]; var off = order[(tick + 1) % order.length]; markers.forEach(function (m, i) { if (i === on) { // remove → flush → re-add so the entrance sequence replays m.classList.remove('active'); m.getBoundingClientRect(); m.classList.remove('out'); m.classList.add('active'); } else if (i === off) { exit(m); } }); tick++; } function start() { if (timer) return; var myRun = ++run; requestAnimationFrame(function () { requestAnimationFrame(function () { if (run !== myRun) return; tick = 0; markers.forEach(function (m) { m.classList.remove('out'); m.classList.add('active'); }); timer = setInterval(step, 1800); }); }); timer = 1; } function stop() { if (timer) { clearInterval(timer); timer = null; } run++; // invalidate pending callbacks tick = 0; markers.forEach(function (m) { if (m.classList.contains('active')) exit(m); }); } var wrap = overlay.closest('.ae-map'); if ('IntersectionObserver' in window && wrap) { var mo = new IntersectionObserver(function (entries) { entries[0].isIntersecting ? start() : stop(); }, { threshold: 0.12 }); mo.observe(wrap); } else { start(); } })(); /* ---------- footer year ---------- */ var y = document.getElementById('year'); if (y) y.textContent = new Date().getFullYear(); })();