import './bootstrap';

import Lenis from 'lenis';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

// ── Lenis smooth scroll ──────────────────────────────────────
const lenis = new Lenis({ lerp: 0.055, smoothWheel: true });
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);

// ── Navbar scroll effect ─────────────────────────────────────
const navbar = document.getElementById('navbar');
window.addEventListener('scroll', () => {
  if (window.scrollY > 60) navbar.classList.add('scrolled');
  else navbar.classList.remove('scrolled');
}, { passive: true });

// ── Mobile menu ──────────────────────────────────────────────
const menuToggle = document.getElementById('menu-toggle');
const mobileMenu = document.getElementById('mobile-menu');
const iconHamburger = document.getElementById('icon-hamburger');
const iconClose = document.getElementById('icon-close');
let menuOpen = false;
menuToggle?.addEventListener('click', () => {
  menuOpen = !menuOpen;
  mobileMenu.classList.toggle('open', menuOpen);
  iconHamburger.classList.toggle('hidden', menuOpen);
  iconClose.classList.toggle('hidden', !menuOpen);
});
mobileMenu?.querySelectorAll('a').forEach(a => {
  a.addEventListener('click', () => {
    menuOpen = false;
    mobileMenu.classList.remove('open');
    iconHamburger.classList.remove('hidden');
    iconClose.classList.add('hidden');
  });
});

// ── Hero entrance: zoom-in on load ───────────────────────────
// Globe zooms from larger scale → natural
gsap.from('#hero-visual', {
  scale: 1.18,
  opacity: 0,
  duration: 2.2,
  ease: 'power2.out',
});

// Hero content zooms in slightly
gsap.from('#hero-content', {
  scale: 0.94,
  duration: 1.6,
  ease: 'power3.out',
});

// Hero headline line-by-line reveal
const heroTl = gsap.timeline({ defaults: { ease: 'power3.out' } });
const lines = document.querySelectorAll('#hero-headline .clip-word-inner');
heroTl
  .to('#hero-tag', { opacity: 1, y: 0, duration: 0.7, ease: 'power2.out' }, 0.3)
  .to(lines, { y: '0%', duration: 1.05, stagger: 0.12, ease: 'power4.out' }, 0.45)
  .to('#hero-sub', { opacity: 1, y: 0, duration: 0.75, ease: 'power2.out' }, 0.85)
  .to('#hero-ctas', { opacity: 1, y: 0, duration: 0.65, ease: 'power2.out' }, 1.0)
  .to('#hero-stats', { opacity: 1, y: 0, duration: 0.65, ease: 'power2.out' }, 1.15);

// ── Hero parallax zoom on scroll: globe zooms out, content fades ──
gsap.to('#hero-visual', {
  scale: 1.14,
  ease: 'none',
  scrollTrigger: {
    trigger: '#hero',
    start: 'top top',
    end: 'bottom top',
    scrub: 1.8,
  },
});
gsap.to('#hero-content', {
  y: -60,
  opacity: 0.5,
  ease: 'none',
  scrollTrigger: {
    trigger: '#hero',
    start: 'top top',
    end: '80% top',
    scrub: 1.2,
  },
});

// Hero globe slow rotate
gsap.to('#hero-visual', {
  rotation: 4,
  duration: 22,
  repeat: -1,
  yoyo: true,
  ease: 'sine.inOut',
  transformOrigin: 'center center',
});

// Hero stat chips float
document.querySelectorAll('#hero-stats > div').forEach((el, i) => {
  gsap.to(el, {
    y: -10,
    duration: 3.5 + i * 0.6,
    repeat: -1,
    yoyo: true,
    ease: 'sine.inOut',
    delay: i * 0.4,
  });
});

// ── Hero counter (immediate, no scroll) ─────────────────────
document.querySelectorAll('.counter-display').forEach(el => {
  const target = parseInt(el.dataset.target, 10);
  const suffix = el.dataset.suffix || '';
  const duration = 1800;
  const start = Date.now();
  const update = () => {
    const elapsed = Date.now() - start;
    const progress = Math.min(elapsed / duration, 1);
    const eased = 1 - Math.pow(1 - progress, 3);
    const current = Math.floor(eased * target);
    el.textContent = current.toLocaleString() + suffix;
    if (progress < 1) requestAnimationFrame(update);
  };
  setTimeout(() => requestAnimationFrame(update), 900);
});

// ── Zoom reveal for section headers and key elements ─────────
document.querySelectorAll('.zoom-reveal').forEach(el => {
  gsap.fromTo(el,
    { opacity: 0, scale: 0.93, y: 32 },
    {
      opacity: 1, scale: 1, y: 0,
      duration: 1.0,
      ease: 'power3.out',
      scrollTrigger: {
        trigger: el,
        start: 'top 88%',
        toggleActions: 'play none none none',
      },
    }
  );
});

// ── Services: pin + scroll-driven card transitions (all 5) ─────
// Guarded on #services existing — this pinned stage is homepage-only;
// inner pages don't have a #services element so this whole block no-ops there.
if (document.getElementById('services')) {
const serviceCards = document.querySelectorAll('.service-card');
const SVC_N   = serviceCards.length; // 5
const SEG_DUR = 220; // timeline units per card — larger value = more hold time between transitions (doubled from 110)

// Lift cards out of the nested max-w wrapper (whose inner .relative div collapses to
// 0px height when all children are absolute) and into a dedicated full-viewport stage
// that is a direct child of #services, which has an explicit height: 100vh.
const svcStage = document.createElement('div');
svcStage.style.cssText = 'position:absolute;inset:0;z-index:20;';
document.getElementById('services').appendChild(svcStage);
serviceCards.forEach(card => svcStage.appendChild(card));

// Progress indicator — appended to svcStage so it lives above the cards (z-index:10 > z-index:1)
const svcProgress = document.createElement('div');
svcProgress.id = 'svc-progress';
svcProgress.innerHTML =
  `<div class="svc-prog-num"><span class="svc-prog-current">01</span> / ${String(SVC_N).padStart(2,'0')}</div>` +
  `<div class="svc-prog-dots">${Array.from({length:SVC_N},(_,i)=>`<span class="svc-prog-dot${i===0?' active':''}"></span>`).join('')}</div>`;
svcStage.appendChild(svcProgress);
const progCurrent = svcProgress.querySelector('.svc-prog-current');
const progDots    = svcProgress.querySelectorAll('.svc-prog-dot');

serviceCards.forEach(card => card.classList.add('svc-card-fixed'));
gsap.set(serviceCards, { xPercent: -50, yPercent: -50, opacity: 0, scale: 0.82 });
serviceCards.forEach(card => {
  gsap.set(card.querySelectorAll('h3, p, .svc-tags, a'), { opacity: 0, y: 18 });
});

const servicesTl = gsap.timeline();

serviceCards.forEach((card, i) => {
  const seg   = i * SEG_DUR;
  const img   = card.querySelector('.svc-img');
  const texts = card.querySelectorAll('h3, p, .svc-tags, a');

  // zoom in — entrance, card holds, then exit. Doubled again (was 36/40/18/+18,
  // exit 36, SEG_DUR 110) — everything scaled 2x together (including SEG_DUR)
  // so the hold/gap proportions stay identical, just twice as long. Exit's
  // offset still scales with its duration so it lands exactly at the next
  // segment's start (seg + SEG_DUR).
  servicesTl
    .fromTo(card,  { scale: 0.88, opacity: 0 }, { scale: 1, opacity: 1, duration: 72, ease: 'power2.inOut' }, seg)
    .fromTo(img,   { scale: 1.1              }, { scale: 1,              duration: 80, ease: 'power2.inOut' }, seg)
    .fromTo(texts, { opacity: 0,  y: 14      }, { opacity: 1, y: 0, duration: 36, ease: 'power2.inOut', stagger: 1.0 }, seg + 36);

  // zoom out — gradual exit, all except last card
  if (i < SVC_N - 1) {
    servicesTl.fromTo(card,
      { scale: 1,    opacity: 1 },
      { scale: 1.06, opacity: 0, duration: 72, ease: 'power2.inOut' },
      seg + SEG_DUR - 88);
  }
});

// Hold the last card at scale:1, opacity:1 until scroll progress = 1
const svcTarget = SVC_N * SEG_DUR;
const svcPad = svcTarget - servicesTl.duration();
if (svcPad > 0) {
  servicesTl.to(serviceCards[SVC_N - 1], { scale: 1, opacity: 1, duration: svcPad, ease: 'none' });
}

ScrollTrigger.create({
  trigger: '#services',
  start: 'top top',
  end: '+=4200vh', // increased from 2800vh — still too fast
  pin: true,
  scrub: 8.4, // increased from 5.6 — still too fast
  animation: servicesTl,
  onUpdate: (self) => {
    const idx = Math.min(Math.floor(self.progress * SVC_N), SVC_N - 1);
    progCurrent.textContent = String(idx + 1).padStart(2, '0');
    progDots.forEach((dot, i) => dot.classList.toggle('active', i === idx));
  },
});
}

// ── Why Choose Us: stat cards travel diagonally (bottom-right → top-left) as
// the section scrolls, scrubbed to scroll position (no autoplay, reverses on
// scroll-up, linear — no springy easing since it's tied 1:1 to scroll). Card 1
// leads; 2, 3, 4 follow slightly behind on the same path, and all 4 stay put
// on the diagonal once they've arrived — no grid-settle phase (tried that,
// didn't want it — cards should stay diagonal, not rearrange into a grid).
// The block is PINNED (see ScrollTrigger.create below) for the animation's
// scroll range — without a pin, this container scrolls normally like any
// other content, and since it's taller than a typical viewport minus the
// fixed navbar, card4 (top of the diagonal) could scroll up behind the navbar
// before card1's entrance even finished. Pinning holds the whole block still
// on screen for the duration, same fix as the How It Works progress bar.
// Only active at lg (1024px) — matches the cards' own `hidden lg:block`. Below
// that (all of mobile/tablet) the plain stacked/2-col fallback grid (`lg:hidden`,
// already in the markup) is shown instead, with no scroll animation attached.
// NOTE on card width: cards are 240px (kept wide per request rather than the
// narrower 200px used briefly before). Left steps are fixed at 0 / 260 / 520px
// + card1 via right:0. At ~1013px usable width (a ~1093px window) that's a
// ~13-20px clear gap between every card — verified by hand. Right at the lg
// floor itself (1024px viewport, ~944px usable), 4×240px cards mathematically
// can't all have a positive gap (960px alone exceeds the 944px available) —
// that narrow band (~1024-1090px) is a known, accepted tightness tradeoff for
// keeping the cards this wide; it clears again above ~1090px.
const statCardsDesktop = document.getElementById('why-stat-cards-desktop');
if (statCardsDesktop) {
  ScrollTrigger.matchMedia({
    '(min-width: 1024px)': () => {
      const cards = [1, 2, 3, 4]
        .map((n) => document.getElementById(`stat-card-${n}`))
        .filter(Boolean);
      if (!cards.length) return;

      const ENTRY_DURATION = 32; // how long each card's own entry takes (increased from 24 — was still too fast)
      const ENTRY_OVERLAP = 0.5; // how much the next card's entry overlaps the previous one's tail (lowered from 0.25 — more separation/stagger between cards, less piling-in)

      // All 4 cards must start from roughly the SAME hidden spot near the
      // bottom-right corner, not each shifted by the same flat offset from
      // its own resting position — a flat offset barely moves card3/card4
      // (whose targets already sit near the container's top-left origin),
      // leaving them visible on screen well before their turn while card1/2
      // (already at the far corner) get pushed properly off-screen. Measuring
      // each card's actual rest position and computing a shared entry point
      // fixes that: every card is genuinely hidden until it's its turn.
      const restRects = cards.map((c) => c.getBoundingClientRect());
      const entryPoint = {
        x: restRects[0].left + 260, // card1 (index 0) is the bottom-right anchor; push further out
        y: restRects[0].top + 260,
      };
      const entryOffsets = cards.map((c, i) => ({
        dx: entryPoint.x - restRects[i].left,
        dy: entryPoint.y - restRects[i].top,
      }));

      gsap.set(cards, { x: (i) => entryOffsets[i].dx, y: (i) => entryOffsets[i].dy });

      const tl = gsap.timeline();
      // Each card's entry starts only after the previous one is mostly done
      // (small overlap for fluidity, not simultaneity) — so cards visibly
      // arrive one at a time in a flow, instead of piling in together. They
      // hold their diagonal position for the rest of the pinned scroll once
      // arrived — nothing rearranges them afterward.
      cards.forEach((card, i) => {
        const startPos = i === 0 ? 0 : `-=${ENTRY_OVERLAP}`;
        tl.fromTo(card,
          { x: entryOffsets[i].dx, y: entryOffsets[i].dy },
          { x: 0, y: 0, ease: 'none', duration: ENTRY_DURATION },
          startPos
        );
      });

      ScrollTrigger.create({
        trigger: statCardsDesktop,
        // 'top 100px' (not 'top top') — when pinned, the container's top freezes at
        // whatever y this resolves to for the whole pin duration. 'top top' froze it
        // at y=0, flush under the fixed/transparent navbar (~82px tall), so card1
        // slid in partly hidden behind the navbar. Leaving a 100px gap here clears it.
        start: 'top 100px',
        end: '+=1200vh', // increased from 840vh — was still too fast
        pin: true,
        scrub: 8, // increased from 5.4 — higher scrub also smooths out the motion
        animation: tl,
      });
    },
  });
}

// ── Why Choose Us: scroll-driven showcase (label / icon / detail crossfade) ──
const whyShowcase = document.querySelector('.why-showcase');
if (whyShowcase) {
  const whyLabels  = whyShowcase.querySelectorAll('.why-label-item');
  const whyIcons   = whyShowcase.querySelectorAll('.why-icon-item');
  const whyDetails = whyShowcase.querySelectorAll('.why-detail-item');
  const whyCounter = whyShowcase.querySelector('.why-counter-current');
  const WHY_N = whyLabels.length;

  let whyActiveIdx = 0;
  const applyWhyActive = (idx) => {
    whyActiveIdx = idx;
    whyLabels.forEach((el, i)  => el.classList.toggle('active', i === idx));
    whyIcons.forEach((el, i)   => el.classList.toggle('active', i === idx));
    whyDetails.forEach((el, i) => el.classList.toggle('active', i === idx));
    whyCounter.textContent = idx + 1;
  };

  // A fast/flung scroll can jump the underlying progress across several items'
  // worth of distance in one update — applying that idx straight away made
  // 2-3 items skip past at once instead of crossfading through each one.
  // Decoupling the visible step from raw scroll speed fixes it: onUpdate only
  // records the target, and a paced queue advances whyActiveIdx one step at a
  // time (WHY_STEP_MS apart) toward it, so every item is always shown even on
  // a big scroll jump.
  const WHY_STEP_MS = 140; // reduced from 160 — still too slow
  let whyTargetIdx = 0;
  let whyStepTimer = null;
  let whyFirstSync = true; // on load/reload, jump straight to the correct item instead of pacing up to it
  const advanceWhy = () => {
    whyStepTimer = null;
    if (whyActiveIdx === whyTargetIdx) return;
    applyWhyActive(whyActiveIdx + (whyActiveIdx < whyTargetIdx ? 1 : -1));
    whyStepTimer = setTimeout(advanceWhy, WHY_STEP_MS);
  };
  const queueWhyActive = (idx) => {
    whyTargetIdx = idx;
    if (whyFirstSync) {
      whyFirstSync = false;
      applyWhyActive(idx);
      return;
    }
    if (whyStepTimer === null) advanceWhy();
  };

  // Scroll-pinned crossfade only applies at md (768px) and up — below that, the
  // mobile stacked list (.why-mobile-list) is shown instead with no scroll interactivity.
  ScrollTrigger.matchMedia({
    '(min-width: 768px)': () => {
      ScrollTrigger.create({
        trigger: whyShowcase,
        start: 'center center',
        end: '+=' + (WHY_N * 500) + 'vh', // reduced from 800vh/item — still too slow; the anti-skip stepping logic above already prevents skipping regardless of distance, so this can be much shorter
        pin: true,
        scrub: 1.5, // reduced from 2
        onUpdate: (self) => {
          // Clamp so the final item settles at progress 1 instead of overshooting/freezing.
          const idx = Math.min(Math.floor(self.progress * WHY_N), WHY_N - 1);
          queueWhyActive(idx);
        },
      });
    },
  });
}

// ── Scroll counter triggers ──────────────────────────────────
function animateCounter(el) {
  if (el.dataset.counted) return;
  el.dataset.counted = '1';
  const target = parseInt(el.dataset.target, 10);
  const suffix = el.dataset.suffix || '';
  const duration = 2000;
  const start = Date.now();
  const update = () => {
    const elapsed = Date.now() - start;
    const progress = Math.min(elapsed / duration, 1);
    const eased = 1 - Math.pow(1 - progress, 4);
    const val = Math.floor(eased * target);
    el.textContent = val.toLocaleString() + suffix;
    if (progress < 1) requestAnimationFrame(update);
  };
  requestAnimationFrame(update);
}
// #reach's counters (map bubbles + stats row) get a separate reversible
// version below, so they're excluded here — this one stays one-shot/play-once
// for every other counter-trigger on the site. The service-detail "By The
// Numbers" stats are also excluded — their count-up is driven by the
// cargo-manifest block below instead, staggered per block as it individually
// enters view rather than independently at "top 85%".
document.querySelectorAll('.counter-trigger').forEach(el => {
  if (el.closest('#reach') || el.closest('.stats-manifest-section')) return;
  ScrollTrigger.create({
    trigger: el,
    start: 'top 85%',
    onEnter: () => animateCounter(el),
  });
});

// ── Credential tiles (Clients page): top accent line fills in on scroll ──
document.querySelectorAll('.credential-accent-line').forEach(el => {
  gsap.fromTo(el,
    { width: '0%' },
    {
      width: '100%',
      duration: 0.9,
      ease: 'power2.out',
      scrollTrigger: {
        trigger: el.closest('.credential-tile'),
        start: 'top 85%',
        toggleActions: 'play none none none',
      },
    }
  );
});

// ── Process steps: fade + rise in as each enters viewport, staggered ──
// (dim/highlight only applies md+ — below that all steps rest at full opacity)
document.querySelectorAll('.process-step-v').forEach((el, i) => {
  const dimsOnThisViewport = window.matchMedia('(min-width: 768px)').matches;
  const restOpacity = (!dimsOnThisViewport || el.classList.contains('active')) ? 1 : 0.6;
  gsap.fromTo(el,
    { opacity: 0, y: 28 },
    {
      opacity: restOpacity, y: 0,
      duration: 1.5,
      ease: 'power3.out',
      delay: i * 0.28,
      clearProps: 'opacity,transform',
      scrollTrigger: {
        trigger: el,
        start: 'top 85%',
        toggleActions: 'play none none none',
      },
    }
  );
});

// ── How It Works: pinned scroll-driven progress bar ──────────
// Pin + floating object only run at md (768px) and up — below that, the 4 steps
// are shown as a plain static stacked list with no line and no animation.
const processSection = document.getElementById('process');
const processPinTarget = document.getElementById('process-pin-target');
if (processSection && processPinTarget) {
  console.log('[process] init: section found, registering matchMedia');
  ScrollTrigger.matchMedia({
    '(min-width: 768px)': () => {
      console.log('[process] matchMedia callback fired (viewport >= 768px)');
      try {
        const processSteps = processSection.querySelectorAll('.process-step-v');
        const processFill = document.getElementById('process-progress-fill');
        const processDots = processSection.querySelectorAll('.process-progress-dot');
        const processPuck = document.getElementById('process-progress-puck');
        const processPuckIcons = processPuck.querySelectorAll('.process-puck-icon');
        const processRow = document.getElementById('process-row');
        const STEP_N = processSteps.length;
        const ROW_DRIFT_PX = 40; // max horizontal travel of the cards row, each direction

        console.log('[process] elements resolved:', {
          steps: processSteps.length,
          fill: !!processFill,
          dots: processDots.length,
          puck: !!processPuck,
          icons: processPuckIcons.length,
          row: !!processRow,
        });

        // Pin only the progress-bar + steps block (not the header above it) —
        // that block is compact enough to fit one viewport, so nothing sits
        // below the fold while pinned. The header scrolls normally beforehand.
        const st = ScrollTrigger.create({
          trigger: processPinTarget,
          start: 'top top',
          end: '+=1350vh', // increased from 900vh — was too fast
          pin: true,
          scrub: 4.2, // increased from 2.8 — was too fast
          // NOTE: no built-in GSAP `snap` here — it drives the scroller's native
          // scrollTo, which fights Lenis's own smoothing loop and can jam the
          // whole scrub. Snapping is done manually below via lenis.scrollTo().
          onUpdate: (self) => {
            const progress = self.progress;
            processFill.style.width = (progress * 100) + '%';
            processPuck.style.left = (progress * 100) + '%';

            // Cards row drifts horizontally in sync with the puck below it — a
            // sine curve so it starts and ends at 0 (no jump on pin enter/exit)
            // and peaks mid-scroll, rather than a linear slide that would leave
            // a residual offset once the pin releases.
            processRow.style.transform = `translateX(${Math.sin(progress * Math.PI) * ROW_DRIFT_PX}px)`;

            // Round to nearest step so each step owns an even window centered
            // on its snap point (0, 1/3, 2/3, 1 for 4 steps).
            const idx = Math.min(Math.max(Math.round(progress * (STEP_N - 1)), 0), STEP_N - 1);
            processDots.forEach((dot, i) => dot.classList.toggle('active', i <= idx));
            processSteps.forEach((el, i) => el.classList.toggle('active', i <= idx));
            processPuckIcons.forEach((icon, i) => icon.classList.toggle('active', i === idx));

            console.log(`[process] progress=${progress.toFixed(3)} activeStep=${idx + 1}/${STEP_N}`);
          },
        });

        // Manual snap-to-nearest-step once scrolling settles, driven through
        // Lenis's own scrollTo (not GSAP's native-scrollTo snap) so it doesn't
        // fight Lenis's smoothing.
        let snapTimer = null;
        lenis.on('scroll', () => {
          if (!st.isActive) return;
          clearTimeout(snapTimer);
          snapTimer = setTimeout(() => {
            const stepFrac = 1 / (STEP_N - 1);
            const nearestStep = Math.round(st.progress / stepFrac);
            const targetProgress = nearestStep * stepFrac;
            const targetScroll = st.start + (st.end - st.start) * targetProgress;
            lenis.scrollTo(targetScroll, { duration: 0.5 });
          }, 120);
        });

        console.log('[process] ScrollTrigger created:', st, 'start=', st.start, 'end=', st.end);
      } catch (err) {
        console.error('[process] setup failed:', err);
      }
    },
  });
}

// ── Why Choose Us: editorial hover-list (about page only — no-op if
// #why-list isn't on the page). Desktop: hovering a row dims its siblings
// and floats that row's data-img thumbnail near the cursor. Touch devices
// get neither — CSS gates the dim/float visuals behind (hover:hover), and
// this listener only ever runs the float positioning, which stays inert
// (opacity never toggles) without that CSS also flipping .hovering on.
const whyList = document.getElementById('why-list');
const whyFloatImg = document.getElementById('why-float-img');
if (whyList && whyFloatImg) {
  const whyRows = whyList.querySelectorAll('.why-row');
  const FLOAT_OFFSET_X = 24;
  const FLOAT_OFFSET_Y = -140; // places the image up-right of the cursor

  whyRows.forEach((row) => {
    row.addEventListener('mouseenter', () => {
      whyList.classList.add('hovering');
      row.classList.add('why-row-active');
      whyFloatImg.style.backgroundImage = `url('${row.dataset.img}')`;
      whyFloatImg.classList.add('visible');
    });
    row.addEventListener('mouseleave', () => {
      whyList.classList.remove('hovering');
      row.classList.remove('why-row-active');
      whyFloatImg.classList.remove('visible');
    });
  });

  whyList.addEventListener('mousemove', (e) => {
    whyFloatImg.style.transform = `translate(${e.clientX + FLOAT_OFFSET_X}px, ${e.clientY + FLOAT_OFFSET_Y}px)`;
  });
}

// ── Service-detail hero: background Ken Burns zoom, heading line-mask
// reveal, floating trust chip, and scroll cue (service-detail template only
// — these classes don't exist elsewhere, so every block here no-ops on
// every other page). Homepage's own hero (#hero-visual etc.) is untouched.
const heroBgZoom = document.querySelector('.hero-bg-zoom');
if (heroBgZoom) {
  // One slow continuous zoom-in over the page's dwell time, not a repeating
  // breathing loop — subtle "always still creeping in" rather than pulsing.
  gsap.to(heroBgZoom, { scale: 1.08, duration: 20, ease: 'none' });
}

const heroHeadingMask = document.querySelector('.hero-heading-mask');
if (heroHeadingMask) {
  const heroHeading = heroHeadingMask.querySelector('.hero-heading');
  gsap.fromTo(heroHeading,
    { yPercent: 100 },
    { yPercent: 0, duration: 0.9, ease: 'power4.out', delay: 0.2 }
  );
}

const heroFloatChip = document.querySelector('.hero-float-chip');
if (heroFloatChip) {
  gsap.fromTo(heroFloatChip,
    { opacity: 0, scale: 0.9 },
    { opacity: 1, scale: 1, duration: 0.7, ease: 'power2.out', delay: 0.6 }
  );
  // Continuous gentle float, oscillating either side of rest (-8 <-> 8),
  // independent of the entrance tween above (GSAP composites the two
  // transform properties — opacity/scale here, y on the float — cleanly).
  gsap.fromTo(heroFloatChip,
    { y: -8 },
    { y: 8, duration: 3, ease: 'sine.inOut', yoyo: true, repeat: -1, delay: 0.6 }
  );
}

const heroScrollDot = document.querySelector('.hero-scroll-dot');
if (heroScrollDot) {
  gsap.fromTo(heroScrollDot,
    { y: 0 },
    { y: 20, duration: 1.5, ease: 'power1.inOut', yoyo: true, repeat: -1 }
  );
}

// ── Service overview: choreographed scroll-reveal (service-detail template
// only — no-op if .overview-section isn't on the page, e.g. About/Contact) ──
const overviewSection = document.querySelector('.overview-section');
if (overviewSection) {
  const overviewHeading = overviewSection.querySelector('.overview-heading');
  const overviewLeft = overviewSection.querySelector('.overview-left');
  const overviewTags = overviewSection.querySelectorAll('.overview-highlight-tag');
  const overviewStickyCard = overviewSection.querySelector('.overview-sticky-card');
  const overviewImage = overviewSection.querySelector('.overview-image img');

  if (overviewHeading) {
    gsap.fromTo(overviewHeading,
      { opacity: 0, y: 32 },
      {
        opacity: 1, y: 0, duration: 0.9, ease: 'power3.out',
        scrollTrigger: { trigger: overviewHeading, start: 'top 88%', toggleActions: 'play none none none' },
      }
    );
  }
  if (overviewLeft) {
    gsap.fromTo(overviewLeft,
      { opacity: 0, y: 32 },
      {
        opacity: 1, y: 0, duration: 0.9, delay: 0.15, ease: 'power3.out',
        scrollTrigger: { trigger: overviewLeft, start: 'top 88%', toggleActions: 'play none none none' },
      }
    );
  }
  if (overviewTags.length) {
    gsap.fromTo(overviewTags,
      { opacity: 0, scale: 0.7 },
      {
        opacity: 1, scale: 1, duration: 0.55, stagger: 0.08, ease: 'back.out(1.7)',
        scrollTrigger: { trigger: overviewTags[0], start: 'top 92%', toggleActions: 'play none none none' },
      }
    );
  }
  if (overviewStickyCard) {
    gsap.fromTo(overviewStickyCard,
      { opacity: 0, y: 32 },
      {
        opacity: 1, y: 0, duration: 0.9, ease: 'power3.out',
        scrollTrigger: { trigger: overviewStickyCard, start: 'top 88%', toggleActions: 'play none none none' },
      }
    );
  }
  if (overviewImage) {
    gsap.fromTo(overviewImage,
      { scale: 1.12 },
      {
        scale: 1, duration: 1.1, ease: 'power2.out',
        scrollTrigger: { trigger: overviewImage, start: 'top 85%', toggleActions: 'play none none none' },
      }
    );
  }
}

// ── Features: pinned split-scroll (service-detail template only — no-op if
// .feature-scroll-section isn't on the page). Desktop/lg only — the sticky
// left image stack is CSS-only (lg:sticky, hidden below lg), so the mobile
// stacked layout (own inline image per step) needs none of this JS at all.
const featureScrollSection = document.querySelector('.feature-scroll-section');
if (featureScrollSection) {
  ScrollTrigger.matchMedia({
    '(min-width: 1024px)': () => {
      const stackImages = featureScrollSection.querySelectorAll('.feature-stack-img');
      const steps = featureScrollSection.querySelectorAll('.feature-step');

      steps.forEach((step) => {
        const stepIndex = step.dataset.stepTrigger; // matches the left image's data-stack-step
        const targetImg = featureScrollSection.querySelector(`.feature-stack-img[data-stack-step="${stepIndex}"]`);
        const stepInner = step.querySelector('.feature-step-inner');

        // Crossfade the left stack to this step's image, and give the step's
        // own number/title/description a quick settle-in pulse (scale 1.03 -> 1).
        const activate = () => {
          stackImages.forEach((img) => img.classList.toggle('active', img === targetImg));
          if (stepInner) {
            gsap.fromTo(stepInner, { scale: 1.03 }, { scale: 1, duration: 0.4, ease: 'power2.out' });
          }
        };

        ScrollTrigger.create({
          trigger: step,
          start: 'top 60%',
          end: 'bottom 40%',
          onEnter: activate,
          onEnterBack: activate,
        });
      });

      // Cleanup returned to matchMedia: nothing extra to tear down — GSAP's
      // own ScrollTrigger revert (triggered on breakpoint cross) removes
      // these triggers automatically.
    },
  });
}

// ── Stats: "Cargo Manifest Strip" (service-detail "By The Numbers" only —
// no-op if .stats-manifest-section isn't on the page). The whole strip
// fades/rises in; then each block's dashed divider(s) "draw" via scaleY
// (vertical) or scaleX (horizontal) from their top/left edge, staggered;
// each number's count-up starts as its own block comes into view, staggered
// ~0.1s apart on top of that.
const statsManifestSection = document.querySelector('.stats-manifest-section');
if (statsManifestSection) {
  const manifestStrip = statsManifestSection.querySelector('.stats-manifest-strip');
  const statBlocks = statsManifestSection.querySelectorAll('.stat-manifest-block');

  if (manifestStrip) {
    gsap.fromTo(manifestStrip,
      { opacity: 0, y: 30 },
      {
        opacity: 1, y: 0, duration: 0.8, ease: 'power2.out',
        scrollTrigger: { trigger: manifestStrip, start: 'top 85%', toggleActions: 'play none none none' },
      }
    );
  }

  statBlocks.forEach((block, i) => {
    const dividers = block.querySelectorAll('.stat-manifest-divider-v, .stat-manifest-divider-h');
    const numEl = block.querySelector('.stat-manifest-num');

    ScrollTrigger.create({
      trigger: block,
      start: 'top 85%',
      onEnter: () => {
        dividers.forEach((divider) => {
          const isVertical = divider.classList.contains('stat-manifest-divider-v');
          const prop = isVertical ? 'scaleY' : 'scaleX';
          gsap.fromTo(divider,
            { [prop]: 0 },
            {
              [prop]: 1,
              transformOrigin: isVertical ? 'top' : 'left',
              duration: 0.6,
              ease: 'power2.out',
              delay: 0.3 + i * 0.1,
            }
          );
        });
        if (numEl) setTimeout(() => animateCounter(numEl), i * 100);
      },
    });
  });
}

// ── Related Services: staggered fade-up reveal (service-detail template only
// — no-op if .related-list isn't on the page) ──
const relatedList = document.querySelector('.related-list');
if (relatedList) {
  const relatedRows = relatedList.querySelectorAll('.related-row');
  if (relatedRows.length) {
    gsap.fromTo(relatedRows,
      { opacity: 0, y: 26 },
      {
        opacity: 1, y: 0, duration: 0.7, stagger: 0.07, ease: 'power2.out',
        scrollTrigger: { trigger: relatedList, start: 'top 85%', toggleActions: 'play none none none' },
      }
    );
  }
}

// ── Related Services: cursor-follow image preview (service-detail template
// only — no-op if .related-list isn't on the page). Desktop/pointer devices
// only (hover:hover + pointer:fine) — touch devices get no listeners at all.
// GSAP quickTo smoothly lerps the preview toward the cursor instead of
// snapping to it; switching rows before fully leaving the list crossfades
// the image between two stacked <img> layers instead of hard-swapping src.
const relatedPreview = document.getElementById('related-hover-preview');
const relatedPreviewRows = document.querySelectorAll('.related-list [data-preview-image]');
if (relatedPreview && relatedPreviewRows.length && window.matchMedia('(hover: hover) and (pointer: fine)').matches) {
  const previewLayers = relatedPreview.querySelectorAll('.related-hover-preview-img');
  const PREVIEW_W = 220, PREVIEW_H = 150;
  const OFFSET_X = 20, OFFSET_Y = -PREVIEW_H / 2; // floats up-right of the cursor
  const EDGE_MARGIN = 16;
  let activeLayer = 0;
  let hoverToken = 0;
  let isVisible = false;

  const quickToX = gsap.quickTo(relatedPreview, 'x', { duration: 0.45, ease: 'power3' });
  const quickToY = gsap.quickTo(relatedPreview, 'y', { duration: 0.45, ease: 'power3' });
  gsap.set(relatedPreview, { x: -9999, y: -9999 });

  function setPreviewImage(src) {
    const current = previewLayers[activeLayer];
    if (!src || current.getAttribute('src') === src) return;
    const next = previewLayers[1 - activeLayer];
    next.src = src;
    next.classList.add('active');
    current.classList.remove('active');
    activeLayer = 1 - activeLayer;
  }

  function positionPreview(e) {
    let x = e.clientX + OFFSET_X;
    let y = e.clientY + OFFSET_Y;
    // Flip to the cursor's left if the preview would overflow the right edge;
    // clamp top/bottom so it never clips off the top or bottom of the viewport.
    if (x + PREVIEW_W > window.innerWidth - EDGE_MARGIN) x = e.clientX - PREVIEW_W - OFFSET_X;
    if (x < EDGE_MARGIN) x = EDGE_MARGIN;
    if (y < EDGE_MARGIN) y = EDGE_MARGIN;
    if (y + PREVIEW_H > window.innerHeight - EDGE_MARGIN) y = window.innerHeight - PREVIEW_H - EDGE_MARGIN;
    quickToX(x);
    quickToY(y);
  }

  relatedPreviewRows.forEach((row) => {
    row.addEventListener('mouseenter', (e) => {
      hoverToken++;
      setPreviewImage(row.dataset.previewImage);
      positionPreview(e);
      if (!isVisible) {
        isVisible = true;
        gsap.to(relatedPreview, { opacity: 1, scale: 1, duration: 0.3, ease: 'power2.out' });
      }
    });
    row.addEventListener('mousemove', positionPreview);
    row.addEventListener('mouseleave', () => {
      // Deferred check: if a mouseenter on the next row fires first (moving
      // directly from one row to another), hoverToken will have advanced by
      // the time this runs, so the preview stays visible and just crossfades.
      const token = hoverToken;
      setTimeout(() => {
        if (hoverToken === token) {
          isVisible = false;
          gsap.to(relatedPreview, { opacity: 0, scale: 0.9, duration: 0.3, ease: 'power2.out' });
        }
      }, 0);
    });
  });
}

// ── Global Footprint map: bubble size proportional to data-value (sqrt scale,
// same logic the old hand-drawn markers used for radius) — reads data-value,
// writes a responsive clamp() to --bubble-size, no hardcoded per-marker sizes.
const reachMarkers = document.querySelectorAll('.reach-marker');
if (reachMarkers.length) {
  const values = Array.from(reachMarkers).map((el) => parseFloat(el.dataset.value));
  const minV = Math.min(...values);
  const maxV = Math.max(...values);
  const MIN_PX = 34, MAX_PX = 62; // smallest/largest bubble's preferred size
  const REF_WIDTH = 1320; // ~ the map panel's width at the content column's max-w (1400px minus px-10 padding)
  reachMarkers.forEach((el) => {
    const v = parseFloat(el.dataset.value);
    const t = maxV === minV ? 1 : (Math.sqrt(v) - Math.sqrt(minV)) / (Math.sqrt(maxV) - Math.sqrt(minV));
    const pref = MIN_PX + t * (MAX_PX - MIN_PX);
    const vw = (pref / REF_WIDTH * 100).toFixed(2); // vw that reproduces `pref` at REF_WIDTH, scaling down on narrower viewports
    const floor = Math.round(pref - 8);
    const ceil = Math.round(pref + 8);
    el.querySelector('.reach-bubble').style.setProperty('--bubble-size', `clamp(${floor}px, ${vw}vw, ${ceil}px)`);
  });
}

// ── Global Footprint map: bubbles scale-in, staggered, reversible ──
// toggleActions 'play reverse play reverse' = onEnter/onLeave/onEnterBack/
// onLeaveBack: scrolling down in plays it forward, scrolling back up past
// the top reverses it (bubbles shrink back out), scrolling down again
// replays it forward — same for exiting past the bottom.
document.querySelectorAll('.reach-marker-inner').forEach((el, i) => {
  gsap.fromTo(el,
    { opacity: 0, scale: 0 },
    {
      opacity: 1, scale: 1,
      duration: 0.8,
      ease: 'back.out(1.7)',
      delay: i * 0.15,
      scrollTrigger: {
        trigger: el,
        start: 'top 88%',
        // Explicit end: without one, GSAP defaults to roughly the element's
        // own (small) height, so scrolling even a bit further down crosses
        // it and fires onLeave (reverse) again almost immediately — the
        // bubble would flicker back out while still mid-scroll into the
        // section. 'bottom top' keeps it "played" for as long as any part
        // of the element is still on screen, only reversing once it's
        // scrolled fully above the viewport.
        end: 'bottom top',
        toggleActions: 'play reverse play reverse',
      },
    }
  );
});

// ── Global Footprint: reversible count-up (map labels + stats row) ──
// A real GSAP tween (not the rAF-based animateCounter above) so ScrollTrigger
// can drive it with toggleActions — reverse animates the number back to 0
// instead of just resetting instantly.
document.querySelectorAll('#reach .counter-trigger').forEach((el) => {
  const target = parseInt(el.dataset.target, 10);
  const suffix = el.dataset.suffix || '';
  const proxy = { val: 0 };
  gsap.to(proxy, {
    val: target,
    duration: 1.4,
    ease: 'power2.out',
    onUpdate: () => { el.textContent = Math.floor(proxy.val).toLocaleString() + suffix; },
    scrollTrigger: {
      trigger: el,
      start: 'top 85%',
      end: 'bottom top', // see comment on the bubble scale-in trigger above — same reason
      toggleActions: 'play reverse play reverse',
    },
  });
});

// ── Global Footprint: stats row fade-in, staggered, reversible ──
document.querySelectorAll('.reach-stat').forEach((el, i) => {
  gsap.fromTo(el,
    { opacity: 0, y: 24 },
    {
      opacity: 1, y: 0,
      duration: 0.7,
      ease: 'power3.out',
      delay: i * 0.1,
      scrollTrigger: {
        trigger: el,
        start: 'top 90%',
        end: 'bottom top',
        toggleActions: 'play reverse play reverse',
      },
    }
  );
});

// ── Testimonials carousel — 2 per view on desktop, 1 on mobile ──
const carouselTrack = document.getElementById('carousel-track');
const carouselDots = document.querySelectorAll('.carousel-dot');
const prevBtn = document.getElementById('carousel-prev');
const nextBtn = document.getElementById('carousel-next');
const carouselSlides = document.querySelectorAll('.carousel-slide');
const totalSlides = carouselSlides.length;
let currentSlide = 0;
let autoTimer;

function perView() { return window.innerWidth >= 1024 ? 2 : 1; }
function maxSlide() { return totalSlides - perView(); }

function getOffset(index) {
  if (!carouselSlides[0]) return 0;
  const slideW = carouselSlides[0].offsetWidth;
  const gap = parseFloat(getComputedStyle(carouselTrack).gap) || 0;
  return index * (slideW + gap);
}

function updateDots(index) {
  const ms = maxSlide();
  carouselDots.forEach((dot, i) => {
    dot.style.display = i <= ms ? '' : 'none';
    dot.classList.toggle('active', i === index);
  });
}

function goToSlide(index) {
  const ms = maxSlide();
  currentSlide = Math.max(0, Math.min(index, ms));
  gsap.to(carouselTrack, { x: -getOffset(currentSlide), duration: 0.65, ease: 'power3.inOut' });
  updateDots(currentSlide);
}

function startAuto() {
  clearInterval(autoTimer);
  autoTimer = setInterval(() => {
    goToSlide(currentSlide < maxSlide() ? currentSlide + 1 : 0);
  }, 5500);
}

prevBtn?.addEventListener('click', () => { goToSlide(currentSlide - 1); startAuto(); });
nextBtn?.addEventListener('click', () => { goToSlide(currentSlide + 1); startAuto(); });
carouselDots.forEach(dot => {
  dot.addEventListener('click', () => { goToSlide(parseInt(dot.dataset.index, 10)); startAuto(); });
});

window.addEventListener('resize', () => { goToSlide(0); startAuto(); }, { passive: true });

// Scroll reveal for testimonials
gsap.fromTo('#testimonials > div',
  { opacity: 0, y: 36 },
  {
    opacity: 1, y: 0,
    duration: 0.85,
    ease: 'power3.out',
    scrollTrigger: { trigger: '#testimonials', start: 'top 82%', toggleActions: 'play none none none' },
  }
);

if (carouselTrack) {
  goToSlide(0);
  startAuto();
}

// ── CTA parallax: heading floats up on scroll ────────────────
gsap.to('.cta-bg h2', {
  yPercent: -10,
  ease: 'none',
  scrollTrigger: {
    trigger: '.cta-bg',
    start: 'top bottom',
    end: 'bottom top',
    scrub: 1.5,
  },
});

// ── Contact input focus styling ──────────────────────────────
document.querySelectorAll('.contact-input').forEach(input => {
  input.addEventListener('focus', () => {
    input.style.borderColor = '#1a2d6e';
    input.style.boxShadow = '0 0 0 3px rgba(26,45,110,.08)';
  });
  input.addEventListener('blur', () => {
    input.style.borderColor = 'rgba(26,45,110,.14)';
    input.style.boxShadow = 'none';
  });
});

// ── Magnetic CTA button effect ───────────────────────────────
document.querySelectorAll('.btn-primary').forEach(btn => {
  btn.addEventListener('mousemove', (e) => {
    const rect = btn.getBoundingClientRect();
    const x = e.clientX - rect.left - rect.width / 2;
    const y = e.clientY - rect.top - rect.height / 2;
    gsap.to(btn, { x: x * 0.16, y: y * 0.16, duration: 0.35, ease: 'power2.out' });
  });
  btn.addEventListener('mouseleave', () => {
    gsap.to(btn, { x: 0, y: 0, duration: 0.5, ease: 'elastic.out(1, 0.4)' });
  });
});

// ── Smooth anchor scroll via Lenis ──────────────────────────
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
  anchor.addEventListener('click', (e) => {
    const href = anchor.getAttribute('href');
    if (href === '#') return;
    const target = document.querySelector(href);
    if (target) {
      e.preventDefault();
      lenis.scrollTo(target, { offset: -80, duration: 1.4 });
    }
  });
});

// ── Recalculate all ScrollTrigger positions once everything (webfonts,
// images) has actually loaded — late layout shifts can leave pinned
// sections measured against stale heights.
window.addEventListener('load', () => ScrollTrigger.refresh());

// ── Services nav dropdown (inner pages only — no-op on homepage, which has
// no [data-dropdown] markup) ──────────────────────────────────
document.querySelectorAll('[data-dropdown]').forEach((dd) => {
  const trigger = dd.querySelector('[data-dropdown-trigger]');
  const panel = dd.querySelector('[data-dropdown-panel]');
  if (!trigger || !panel) return;
  let open = false;
  const setOpen = (v) => {
    open = v;
    panel.classList.toggle('open', open);
    trigger.classList.toggle('open', open);
  };
  trigger.addEventListener('click', (e) => {
    e.stopPropagation();
    setOpen(!open);
  });
  document.addEventListener('click', (e) => {
    if (open && !dd.contains(e.target)) setOpen(false);
  });
});

document.querySelectorAll('[data-mobile-dropdown]').forEach((dd) => {
  const trigger = dd.querySelector('[data-mobile-dropdown-trigger]');
  const panel = dd.querySelector('[data-mobile-dropdown-panel]');
  if (!trigger || !panel) return;
  trigger.addEventListener('click', () => {
    trigger.classList.toggle('open');
    panel.classList.toggle('open');
  });
});

// ── Services page: pinned scroll-driven card stack (services.blade.php only) ──
// Guarded on #services-stack-stage — a different id from the homepage's own
// #services pinned stage, so this never collides with (or gets picked up by)
// that unrelated block above. Desktop/tablet (lg 1024px+) only, matching the
// same ScrollTrigger.matchMedia gate pattern used elsewhere on the site; below
// that breakpoint the Blade view renders a plain stacked list with the site's
// existing generic `.zoom-reveal` fade-in instead (no JS needed for that path).
const servicesStackStage = document.getElementById('services-stack-stage');
if (servicesStackStage) {
  ScrollTrigger.matchMedia({
    '(min-width: 1024px)': () => {
      const cards = Array.from(servicesStackStage.querySelectorAll('.stack-card'));
      const N = cards.length;
      if (!N) return;
      const SEG_DUR = 120; // timeline units per card (relative only — real scroll distance set by ScrollTrigger `end` below)

      const progress = document.createElement('div');
      progress.id = 'stack-progress';
      progress.innerHTML =
        `<div class="stack-progress-num"><span class="stack-progress-current">01</span> / ${String(N).padStart(2, '0')}</div>` +
        `<div class="stack-progress-dots">${cards.map((_, i) => `<span class="stack-progress-dot${i === 0 ? ' active' : ''}"></span>`).join('')}</div>`;
      servicesStackStage.appendChild(progress);
      const progCurrent = progress.querySelector('.stack-progress-current');
      const progDots = progress.querySelectorAll('.stack-progress-dot');

      // Cards are stacked via CSS (position:absolute;inset:0) in DOM order, so
      // later cards paint over earlier ones with no z-index bookkeeping needed.
      gsap.set(cards, { opacity: 0, scale: 1.04, filter: 'blur(0px)' });
      gsap.set(cards[0], { opacity: 1, scale: 1 });

      const tl = gsap.timeline();
      cards.forEach((card, i) => {
        const seg = i * SEG_DUR;
        // Incoming card: fades/scales up into full focus.
        if (i > 0) {
          tl.fromTo(card,
            { opacity: 0, scale: 1.04, filter: 'blur(0px)' },
            { opacity: 1, scale: 1, filter: 'blur(0px)', duration: 45, ease: 'power2.inOut' },
            seg
          );
        }
        // Outgoing card: scales down + blurs + dims into the background as the
        // next card arrives on top of it — the depth/stack effect.
        if (i < N - 1) {
          tl.to(card, {
            scale: 0.94,
            opacity: 0.35,
            filter: 'blur(6px)',
            duration: 45,
            ease: 'power2.inOut',
          }, seg + SEG_DUR - 45);
        }
      });

      // Hold the last card in full focus until scroll progress = 1.
      const target = N * SEG_DUR;
      const pad = target - tl.duration();
      if (pad > 0) {
        tl.to(cards[N - 1], { scale: 1, opacity: 1, filter: 'blur(0px)', duration: pad, ease: 'none' });
      }

      ScrollTrigger.create({
        trigger: servicesStackStage,
        start: 'top top',
        // NOTE: GSAP's `+=Nvh` relative end-offset syntax does NOT scale by
        // viewport height the way plain `Nvh` does elsewhere — it silently
        // treats the number as raw pixels (confirmed empirically). A function
        // returning an explicit px value (re-evaluated on ScrollTrigger.refresh,
        // e.g. on resize) is the reliable way to get real viewport-relative
        // scroll distance: ~120vh of scroll per card.
        end: () => '+=' + Math.round(window.innerHeight * 1.2 * N),
        pin: true,
        scrub: 1.2,
        animation: tl,
        onUpdate: (self) => {
          const idx = Math.min(Math.floor(self.progress * N), N - 1);
          progCurrent.textContent = String(idx + 1).padStart(2, '0');
          progDots.forEach((dot, i) => dot.classList.toggle('active', i === idx));
        },
      });
    },
  });
}
