/* ============================================================ 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'); 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(); })();