/* STATUS: SHIPPED prototype — SEGS_T/COPY still geo-cut-v2 era, NOT the Beeby master.
   Beeby truth (anchors, segments, world-stop copy): tools/remotion-studio/WARGAME-flythrough.md.
   Newer draft: landing/scroll-world/. Lanes map: REPO-MAP.md § Hero flythrough.

   Cornell AI Network — Home hero: scroll-scrubbed flight (Ithaca → Cornell Tech).
   The hero pins (CSS sticky inside a tall track) while scroll scrubs the single
   geo-cut-v2 flight (5× scrub encode, keyframe every 0.5s — see
   landing/QA-v2-decision.md §7). GSAP ScrollTrigger (scroller = the site's own
   scroll div) is the single progress driver; the video core (blob loading,
   coalesced seeks, poster-hold, iOS priming) follows the scroll-world scrub
   engine. Scroll dwells at the doc's three stop anchors (Ezra / Baker /
   Cornell Tech). Mascot is the 3D-rendered loop set (VP9 alpha webm, 24fps)
   flying as a DOM layer for the whole journey — nothing is baked into the film.
   No build step — attaches window.CAIN_HeroFlight like the sibling files. */

(function () {
  const ASSET = (n) => `public/scroll/${n}`;

  /* Progress → video-time map. Times are seconds in flight.mp4 (46.1s,
     geo-cut-v2 at 5×; stop anchors from QA-v2-decision.md ÷ 5).
     w = scroll weight; t0===t1 is a dwell (camera holds, copy peaks).
     The Cornell Tech finale is a slow orbit, so it scrubs slowly instead
     of freezing. */
  const SEGS_T = [
    { t0: 0.0,  t1: 7.9,  w: 1.0 },  // launch: Sage → Arts Quad
    { t0: 7.9,  t1: 7.9,  w: 0.5 },  // ★ STOP 1 — Ezra Cornell
    { t0: 7.9,  t1: 25.6, w: 2.0 },  // gorge run
    { t0: 25.6, t1: 25.6, w: 0.5 },  // ★ STOP 2 — Baker Lab
    { t0: 25.6, t1: 44.3, w: 2.1 },  // exit Ithaca → Cayuga → NYC
    { t0: 44.3, t1: 46.0, w: 0.9 },  // ★ STOP 3 — Cornell Tech slow orbit
  ];
  const SEGS = (() => {
    const total = SEGS_T.reduce((a, s) => a + s.w, 0);
    let off = 0;
    return SEGS_T.map((s) => {
      const o = { p0: off / total, p1: (off + s.w) / total, t0: s.t0, t1: s.t1 };
      off += s.w;
      return o;
    });
  })();
  const T_END = 46.0;

  /* Copy beat bands in overall progress. Seg boundaries: fly1 0–.143,
     Ezra .143–.214, gorge .214–.5, Baker .5–.571, exit .571–.871, CT .871–1. */
  const BEATS = [
    { s: 0.0,  e: 0.14, mode: "first" },
    { s: 0.14, e: 0.30, mode: "mid" },   // Ezra dwell + into the gorge
    { s: 0.49, e: 0.62, mode: "mid" },   // Baker dwell
    { s: 0.64, e: 0.85, mode: "mid" },   // Cayuga → NYC
    { s: 0.86, e: 1.0,  mode: "last" },  // Cornell Tech CTA
  ];

  /* Mascot flight path in overall hero progress (x:vw, y:vh). The mascot is
     on screen the whole ride: waves beside the hint, ignites on first scroll,
     escorts the camera (right side mostly, two L→R traverses), and lands
     beside the CTA to wave again. */
  const KF = [
    { p: 0.000, x: 56, y: 83, rot: 0,   scale: 1.05, op: 0.95 }, // standing wave beside hint
    { p: 0.006, x: 56, y: 82, rot: -4,  scale: 1.06, op: 0.95 }, // ignition
    { p: 0.035, x: 68, y: 58, rot: -16, scale: 1.10, op: 0.95 }, // fast climb
    { p: 0.090, x: 80, y: 30, rot: -8,  scale: 0.95, op: 0.90 },
    { p: 0.150, x: 78, y: 36, rot: -2,  scale: 0.95, op: 0.90 }, // Ezra dwell — hover by the statue
    { p: 0.210, x: 74, y: 40, rot: 2,   scale: 0.95, op: 0.90 },
    { p: 0.300, x: 24, y: 24, rot: 10,  scale: 0.85, op: 0.85 }, // traverse the gorge
    { p: 0.400, x: 40, y: 18, rot: -4,  scale: 0.80, op: 0.80 },
    { p: 0.500, x: 76, y: 30, rot: -8,  scale: 0.90, op: 0.90 }, // arrive Baker
    { p: 0.560, x: 78, y: 34, rot: 0,   scale: 0.92, op: 0.90 }, // Baker dwell
    { p: 0.660, x: 60, y: 16, rot: -10, scale: 0.80, op: 0.85 }, // exit high
    { p: 0.750, x: 20, y: 22, rot: 8,   scale: 0.80, op: 0.80 }, // over the lake
    { p: 0.840, x: 70, y: 28, rot: -8,  scale: 0.90, op: 0.90 }, // NYC arrival
    { p: 0.900, x: 76, y: 48, rot: -3,  scale: 1.00, op: 0.95 }, // descent
    { p: 0.945, x: 76, y: 62, rot: 0,   scale: 1.05, op: 0.95 }, // touchdown
    { p: 1.000, x: 76, y: 63, rot: 0,   scale: 1.05, op: 0.95 }, // waving at the CTA
  ];
  const POSE_STOPS = [
    { p: 0.000, pose: "wave" },
    { p: 0.012, pose: "hover" },
    { p: 0.930, pose: "wave" }, // landed — wave at the CTA
  ];
  const POSE_FADE = 0.02;
  const GLOW_BAND = { start: 0.004, peak: 0.03, end: 0.09 }; // lights up the moment scroll starts

  const COPY = [
    {
      title: "Join the AI Heartbeat of Cornell University",
      body: "The go-to network for Cornell University AI enthusiasts, students, alumni, researchers, founders, investors, and operators — for exclusive events, learning, and networking opportunities.",
    },
    {
      title: "What you'll find here",
      features: ["Learn from AI Leaders", "Hire & Get Hired", "Discover AI Companies", "Connect with the Community"],
    },
    {
      title: "Onward",
      body: "From the Hill, the city calls.",
    },
    {
      title: "From the Hill to the City",
      body: "Wherever Cornell AI people gather, we're there.",
      events: ["Fri 23 Feb · Ithaca — Cornell AI Network-ing", "Wed 13 Mar · Cornell — AI Skill-up workshop"],
    },
    {
      title: "Land where AI careers take off",
      body: "Exclusive events, real introductions, and knowledge worth your time.",
      cta: true,
    },
  ];

  const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
  const smoothstep = (t) => { t = clamp(t, 0, 1); return t * t * (3 - 2 * t); };

  function timeAt(p) {
    p = clamp(p, 0, 1);
    for (let i = 0; i < SEGS.length; i++) {
      const s = SEGS[i];
      if (p <= s.p1 || i === SEGS.length - 1) {
        const local = smoothstep((p - s.p0) / Math.max(0.0001, s.p1 - s.p0));
        return s.t0 + (s.t1 - s.t0) * local;
      }
    }
    return T_END;
  }

  function mascotAt(p) {
    p = clamp(p, 0, 1);
    let i = 0;
    while (i < KF.length - 2 && KF[i + 1].p <= p) i++;
    const a = KF[i], b = KF[i + 1];
    const t = smoothstep((p - a.p) / Math.max(0.0001, b.p - a.p));
    const L = (k) => a[k] + (b[k] - a[k]) * t;
    return { x: L("x"), y: L("y"), rot: L("rot"), scale: L("scale"), op: L("op"), flip: (b.x - a.x) < -0.01 };
  }
  function poseIndexAt(p) {
    let j = 0;
    while (j < POSE_STOPS.length - 1 && POSE_STOPS[j + 1].p <= p) j++;
    return j;
  }

  function injectCSS() {
    if (document.getElementById("cain-hf-css")) return;
    const css = `
    .cain-hf-track{position:relative;height:650vh;background:var(--cain-ink);}
    .cain-hf-stage{position:sticky;top:0;height:100vh;overflow:hidden;}
    .cain-hf-scene{position:absolute;inset:0;}
    .cain-hf-still,.cain-hf-video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center 42%;}
    .cain-hf-scene.has-video .cain-hf-still{opacity:0;}
    .cain-hf-scrim{position:absolute;inset:0;z-index:2;pointer-events:none;
      background:linear-gradient(90deg,rgba(20,16,14,.62) 0%,rgba(20,16,14,.32) 42%,rgba(20,16,14,.04) 72%);}
    /* Brand mark, top-right. Doubles as the fix for the baked "MT" watermark on ezra2.mp4
       and bakerlab-entry-reversed-muted.mp4 — measured at x1775-1890 y15-80 of 1920x1080,
       i.e. the top ~7% of the right ~8%. The robot cannot cover a corner mark and a
       lower-third at once (measured 0% coverage), so this does it for zero footage cost. */
    .cain-hf-mark{position:absolute;z-index:22;top:0;right:0;pointer-events:none;
      min-width:16%;min-height:10%;padding:18px 26px;display:flex;align-items:flex-start;
      justify-content:flex-end;
      background:linear-gradient(205deg,rgba(20,16,14,.62) 0%,rgba(20,16,14,.3) 58%,rgba(20,16,14,0) 100%);}
    .cain-hf-mark span{font-family:var(--font-display);font-size:clamp(12px,1.05vw,16px);
      letter-spacing:.15em;text-transform:uppercase;color:#fff;white-space:nowrap;
      text-shadow:0 1px 10px rgba(20,16,14,.7);}
    .cain-hf-copy{position:absolute;z-index:20;left:0;right:0;top:0;bottom:0;pointer-events:none;}
    .cain-hf-beat{position:absolute;left:var(--gutter,48px);top:50%;transform:translateY(-50%);
      max-width:540px;color:#fff;opacity:0;will-change:opacity,transform;}
    .cain-hf-beat h1,.cain-hf-beat h2{font-family:var(--font-display);color:#fff;line-height:1.05;margin:0 0 18px;
      font-size:clamp(2.4rem,4.6vw,4.2rem);text-shadow:0 2px 24px rgba(20,16,14,.5);text-wrap:balance;}
    .cain-hf-beat p{font-family:var(--font-body);font-size:17px;line-height:1.6;opacity:.94;margin:0 0 24px;max-width:460px;
      text-shadow:0 1px 14px rgba(20,16,14,.6);}
    .cain-hf-feat{list-style:none;margin:0;padding:0;font-family:var(--font-display);}
    .cain-hf-feat li{font-size:clamp(1.3rem,2vw,1.8rem);line-height:1.5;color:rgba(255,255,255,.38);transition:color .3s;}
    .cain-hf-feat li.is-on{color:#fff;}
    .cain-hf-events{list-style:none;margin:0;padding:0;font-family:var(--font-body);font-size:15.5px;line-height:1.9;opacity:.94;}
    .cain-hf-cta{display:flex;gap:12px;pointer-events:auto;}
    .cain-hf-mascot-wrap{position:absolute;z-index:30;left:0;top:0;pointer-events:none;will-change:transform;}
    .cain-hf-mascot{position:absolute;left:0;top:0;width:clamp(90px,12vw,170px);max-width:none;transform:translate(-50%,-50%);
      animation:cain-hf-bob 3.2s ease-in-out infinite;filter:drop-shadow(0 10px 24px rgba(20,16,14,.35));}
    .cain-hf-glow{position:absolute;left:0;top:0;width:clamp(120px,15vw,220px);height:clamp(120px,15vw,220px);
      transform:translate(-50%,-42%);border-radius:50%;opacity:0;pointer-events:none;
      background:radial-gradient(circle,rgba(255,196,120,.55) 0%,rgba(255,140,66,.25) 40%,transparent 70%);}
    @keyframes cain-hf-bob{0%,100%{margin-top:0}50%{margin-top:-7px}}
    .cain-hf-hint{position:absolute;z-index:25;left:50%;bottom:calc(24px + env(safe-area-inset-bottom,0px));transform:translateX(-50%);
      color:rgba(255,255,255,.85);font-family:var(--font-body);font-size:12px;letter-spacing:.14em;text-transform:uppercase;
      display:flex;flex-direction:column;align-items:center;gap:8px;transition:opacity .3s;pointer-events:none;}
    .cain-hf-hint i{width:22px;height:34px;border-radius:12px;border:1px solid rgba(255,255,255,.5);position:relative;}
    .cain-hf-hint i::after{content:"";position:absolute;left:50%;top:7px;width:3px;height:7px;border-radius:2px;
      background:#fff;transform:translateX(-50%);animation:cain-hf-wheel 1.7s ease-in-out infinite;}
    @keyframes cain-hf-wheel{0%{opacity:0;top:6px}40%{opacity:1}100%{opacity:0;top:17px}}
    @media (max-width:860px){
      .cain-hf-scrim{background:linear-gradient(0deg,rgba(20,16,14,.72) 0%,rgba(20,16,14,.3) 42%,rgba(20,16,14,.02) 70%);}
      .cain-hf-beat{left:20px;right:20px;top:auto;transform:none;max-width:none;
        bottom:calc(clamp(64px,14dvh,120px) + env(safe-area-inset-bottom,0px));}
      .cain-hf-beat h1,.cain-hf-beat h2{font-size:clamp(1.9rem,7.5vw,2.7rem);}
      .cain-hf-beat p{font-size:15px;}
      .cain-hf-feat li{font-size:clamp(1.1rem,5vw,1.4rem);}
      .cain-hf-mark{min-width:26%;padding:12px 16px;}
      .cain-hf-still,.cain-hf-video{object-position:center 44%;}
      .cain-hf-mascot{width:clamp(64px,16vw,110px);}
    }
    @media (max-width:480px){ .cain-hf-mascot-wrap{display:none;} }
    `;
    const style = document.createElement("style");
    style.id = "cain-hf-css";
    style.textContent = css;
    document.head.appendChild(style);
  }

  function StaticHero({ onNavigate, Button }) {
    return (
      <section style={{ position: "relative", minHeight: "560px", display: "flex", alignItems: "center", overflow: "hidden" }}>
        <img src="../design-system/assets/photos/campus-aerial.jpg" alt="Cornell campus" style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover" }} />
        <div style={{ position: "absolute", inset: 0, background: "linear-gradient(90deg, rgba(20,16,14,0.66) 0%, rgba(20,16,14,0.34) 42%, rgba(20,16,14,0.05) 75%)" }} />
        <div style={{ position: "relative", maxWidth: "var(--container)", margin: "0 auto", padding: "0 var(--gutter)", width: "100%" }}>
          <div style={{ maxWidth: "560px", color: "#fff", paddingBlock: "80px" }}>
            <h1 style={{ fontFamily: "var(--font-display)", fontSize: "clamp(2.8rem,5vw,4.6rem)", lineHeight: 1.04, margin: "0 0 22px", color: "#fff" }}>
              Join the AI Heartbeat of Cornell University
            </h1>
            <p style={{ fontFamily: "var(--font-body)", fontSize: "17px", lineHeight: 1.6, opacity: 0.94, margin: "0 0 28px", maxWidth: "460px" }}>
              The go-to network for Cornell University AI enthusiasts, students, alumni, researchers, founders, investors, and operators — for exclusive events, learning, and networking opportunities.
            </p>
            <div style={{ display: "flex", gap: "12px" }}>
              <Button variant="solid" onClick={() => onNavigate("events")}>Events</Button>
              <Button variant="light" onClick={() => onNavigate("what")}>Learn more</Button>
            </div>
          </div>
        </div>
      </section>
    );
  }

  /* Safari decodes VP9 but ignores the alpha plane (black matte) — fall back
     to the first-frame PNGs there; everywhere else the loops play. */
  const SAFARI = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);

  function HeroFlight({ onNavigate }) {
    const DS = window.CornellAINetworkDesignSystem_392896 || {};
    const Button = DS.Button || ((p) => <button onClick={p.onClick}>{p.children}</button>);

    const [reduced, setReduced] = React.useState(() =>
      window.matchMedia("(prefers-reduced-motion: reduce)").matches);
    const gsapOK = typeof window.gsap !== "undefined" && typeof window.ScrollTrigger !== "undefined";

    const trackRef = React.useRef(null);
    const sceneRef = React.useRef(null);
    const beatRefs = React.useRef([]);
    const featRefs = React.useRef([]);
    const mascotWrapRef = React.useRef(null);
    const poseFromRef = React.useRef(null);
    const poseToRef = React.useRef(null);
    const glowRef = React.useRef(null);
    const hintRef = React.useRef(null);
    const poseIndexRef = React.useRef(0);
    const [poseIdx, setPoseIdx] = React.useState(0);
    const updateRef = React.useRef(null);
    const lastPRef = React.useRef(0);

    /* A pose change re-renders the layers with fresh opacity:0 — re-apply the
       last progress after commit, or the mascot vanishes until the next scroll. */
    React.useEffect(() => {
      if (updateRef.current) updateRef.current(lastPRef.current);
    }, [poseIdx]);

    React.useEffect(() => {
      const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
      const onChange = () => setReduced(mq.matches);
      if (mq.addEventListener) mq.addEventListener("change", onChange);
      else mq.addListener(onChange);
      return () => { if (mq.removeEventListener) mq.removeEventListener("change", onChange); else mq.removeListener(onChange); };
    }, []);

    React.useEffect(() => {
      if (reduced || !gsapOK) return;
      const track = trackRef.current;
      if (!track) return;
      injectCSS();

      const gsap = window.gsap, ScrollTrigger = window.ScrollTrigger;
      gsap.registerPlugin(ScrollTrigger);
      ScrollTrigger.config({ ignoreMobileResize: true });

      // The site scrolls inside an inner div (see index.html Site()) — find it.
      let scroller = track.parentElement;
      while (scroller && scroller !== document.body) {
        const s = getComputedStyle(scroller).overflowY;
        if (s === "auto" || s === "scroll") break;
        scroller = scroller.parentElement;
      }
      if (!scroller || scroller === document.body) scroller = window;

      const coarse = window.matchMedia("(hover: none) and (pointer: coarse)").matches;
      const smallMQ = window.matchMedia("(max-width: 860px)");
      const isMobile = () => coarse || smallMQ.matches;

      const bg = { loading: false, ready: false, hasVideo: false, failed: false,
                   video: null, cur: 0, target: 0, lastSeekAt: 0 };
      const blobUrls = [];

      /* Non-negotiable: play from memory. Blob → object URL guarantees
         seekability regardless of the host's byte-range support. */
      function loadVideo() {
        if (bg.loading || bg.hasVideo || bg.failed) return;
        bg.loading = true;
        const url = ASSET(isMobile() ? "flight-m.mp4" : "flight.mp4");
        fetch(url)
          .then((r) => (r.ok ? r.blob() : Promise.reject(new Error("404"))))
          .then((blob) => {
            const objUrl = URL.createObjectURL(blob);
            blobUrls.push(objUrl);
            const v = document.createElement("video");
            v.muted = true; v.playsInline = true; v.preload = "auto";
            v.setAttribute("muted", ""); v.setAttribute("playsinline", "");
            v.className = "cain-hf-video";
            v.src = objUrl;
            v.addEventListener("loadedmetadata", () => {
              bg.ready = true;
              if (v.seekable && v.seekable.length && v.seekable.end(0) > 0) { /* blob seekable — expected */ }
              else console.warn("cain-hf: flight video not seekable");
            });
            // Poster holds until a real frame paints (iOS blank-video fix).
            v.addEventListener("seeked", function onSeeked() {
              if (sceneRef.current) sceneRef.current.classList.add("has-video");
              v.removeEventListener("seeked", onSeeked);
            });
            v.addEventListener("loadeddata", () => { try { v.pause(); } catch (e) {} if (userReady) prime(v); });
            const el = sceneRef.current;
            if (el) el.insertBefore(v, el.querySelector(".cain-hf-scrim"));
            bg.video = v; bg.hasVideo = true; bg.loading = false;
          })
          .catch(() => { bg.loading = false; bg.failed = true; });
      }

      // iOS: a muted video that never played won't paint a seeked frame — prime on first touch.
      let userReady = false;
      const prime = (v) => {
        if (!isMobile() || !v) return;
        try { const p = v.play(); if (p && p.then) p.then(() => { try { v.pause(); } catch (e) {} }).catch(() => {}); } catch (e) {}
      };
      const onFirstGesture = () => {
        if (userReady) return;
        userReady = true;
        prime(bg.video);
      };
      window.addEventListener("pointerdown", onFirstGesture, { once: true, passive: true });
      window.addEventListener("touchstart", onFirstGesture, { once: true, passive: true });

      const setMascot = gsap.quickSetter(mascotWrapRef.current, "css");

      function update(p) {
        lastPRef.current = p;
        bg.target = timeAt(p) / T_END;

        // copy beats: first greets on landing, last holds the CTA, middles bell
        for (let i = 0; i < BEATS.length; i++) {
          const el = beatRefs.current[i];
          if (!el) continue;
          const b = BEATS[i];
          const pr = clamp((p - b.s) / (b.e - b.s), 0, 1);
          const before = p < b.s, after = p > b.e;
          let op;
          if (b.mode === "first") op = after ? 0 : smoothstep(1 - pr / 0.62);
          else if (b.mode === "last") op = before ? 0 : smoothstep(pr / 0.4);
          else op = (before || after) ? 0 : smoothstep(1 - Math.abs(pr - 0.5) / 0.5);
          el.style.opacity = String(op);
          el.style.transform = `translateY(calc(-50% + ${(0.5 - pr) * 4}vh))`;
          el.style.pointerEvents = op > 0.5 ? "auto" : "none";
          if (i === 1 && !before && !after) {
            const active = Math.min(3, Math.floor(pr * 4));
            featRefs.current.forEach((li, k) => { if (li) li.classList.toggle("is-on", k === active); });
          }
        }

        // mascot
        const m = mascotAt(p);
        setMascot({ x: m.x + "vw", y: m.y + "vh", rotation: m.rot, scaleX: (m.flip ? -1 : 1) * m.scale, scaleY: m.scale });
        const j = poseIndexAt(p);
        if (j !== poseIndexRef.current) { poseIndexRef.current = j; setPoseIdx(j); }
        const mix = j === 0 ? 1 : clamp((p - POSE_STOPS[j].p) / POSE_FADE, 0, 1);
        if (poseFromRef.current) poseFromRef.current.style.opacity = String(m.op * (1 - mix));
        if (poseToRef.current) poseToRef.current.style.opacity = String(m.op * mix);
        if (glowRef.current) {
          const g = p < GLOW_BAND.peak
            ? clamp((p - GLOW_BAND.start) / (GLOW_BAND.peak - GLOW_BAND.start), 0, 1)
            : clamp(1 - (p - GLOW_BAND.peak) / (GLOW_BAND.end - GLOW_BAND.peak), 0, 1);
          glowRef.current.style.opacity = String(g * 0.9);
        }
        if (hintRef.current) hintRef.current.style.opacity = String(clamp(1 - p / 0.03, 0, 1));
      }

      const st = ScrollTrigger.create({
        trigger: track,
        scroller: scroller,
        start: "top top",
        end: "bottom bottom",
        onUpdate: (self) => update(self.progress),
        onRefresh: (self) => update(self.progress),
      });

      /* Permanent rAF: eases video time toward target with coalesced seeks —
         never issue a new currentTime while the decoder is still seeking,
         and throttle to one seek per ~80ms so phone flicks can't pile up. */
      let rafId = null;
      function raf() {
        const eps = isMobile() ? 0.02 : 0.008;
        if (bg.hasVideo && bg.ready && bg.video && !bg.video.seeking) {
          bg.cur += (bg.target - bg.cur) * 0.22;
          const dur = bg.video.duration;
          if (dur && dur > 0.1) {
            const now = Date.now();
            if (now - bg.lastSeekAt >= 80) {
              const t = clamp(bg.cur, 0, 0.999) * Math.max(0.01, dur - 0.05);
              if (Math.abs(bg.video.currentTime - t) > eps) {
                try { bg.video.currentTime = t; bg.lastSeekAt = now; } catch (e) { /* seek race */ }
              }
            }
          }
        }
        rafId = requestAnimationFrame(raf);
      }
      rafId = requestAnimationFrame(raf);
      loadVideo();
      updateRef.current = update;
      update(st.progress || 0);

      return () => {
        updateRef.current = null;
        st.kill();
        if (rafId != null) cancelAnimationFrame(rafId);
        window.removeEventListener("pointerdown", onFirstGesture);
        window.removeEventListener("touchstart", onFirstGesture);
        blobUrls.forEach((u) => { try { URL.revokeObjectURL(u); } catch (e) {} });
        if (bg.video) { try { bg.video.pause(); bg.video.src = ""; } catch (e) {} }
      };
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [reduced, gsapOK]);

    if (reduced || !gsapOK) return <StaticHero onNavigate={onNavigate} Button={Button} />;

    injectCSS();
    const narrow = window.innerWidth <= 860;
    const poster = ASSET(narrow ? "flight-poster-m.jpg" : "flight-poster.jpg");
    const fromPose = POSE_STOPS[Math.max(0, poseIdx - 1)].pose;
    const toPose = POSE_STOPS[poseIdx].pose;
    const PoseLayer = ({ pose, layerRef }) => SAFARI
      ? <img className="cain-hf-mascot" ref={layerRef} src={ASSET(`mascot-anim-${pose}.png`)} alt="" style={{ opacity: 0 }} />
      : <video className="cain-hf-mascot" ref={layerRef} src={ASSET(`mascot-anim-${pose}.webm`)}
               loop muted playsInline autoPlay style={{ opacity: 0 }} />;

    return (
      <section className="cain-hf-track" ref={trackRef}>
        <div className="cain-hf-stage">
          <div className="cain-hf-scene" ref={sceneRef}>
            <img className="cain-hf-still" src={poster} alt="" decoding="async" loading="eager" />
            <div className="cain-hf-scrim" />
          </div>

          <div className="cain-hf-mark" aria-hidden="true">
            <span>Cornell AI Network</span>
          </div>

          <div className="cain-hf-mascot-wrap" ref={mascotWrapRef} aria-hidden="true">
            <div className="cain-hf-glow" ref={glowRef} />
            <PoseLayer pose={fromPose} layerRef={poseFromRef} />
            <PoseLayer pose={toPose} layerRef={poseToRef} />
          </div>

          <div className="cain-hf-copy">
            {COPY.map((c, i) => (
              <div key={i} className="cain-hf-beat" ref={(el) => (beatRefs.current[i] = el)}>
                {i === 0 ? <h1>{c.title}</h1> : <h2>{c.title}</h2>}
                {c.body && <p>{c.body}</p>}
                {c.features && (
                  <ul className="cain-hf-feat">
                    {c.features.map((f, k) => (
                      <li key={f} ref={(el) => (featRefs.current[k] = el)}>{f}</li>
                    ))}
                  </ul>
                )}
                {c.events && (
                  <ul className="cain-hf-events">
                    {c.events.map((e) => <li key={e}>{e}</li>)}
                  </ul>
                )}
                {c.cta && (
                  <div className="cain-hf-cta">
                    <Button variant="solid" onClick={() => onNavigate("events")}>Events</Button>
                    <Button variant="light" onClick={() => onNavigate("what")}>Learn more</Button>
                  </div>
                )}
              </div>
            ))}
          </div>

          <div className="cain-hf-hint" ref={hintRef}><span>Scroll to fly</span><i /></div>
        </div>
      </section>
    );
  }

  Object.assign(window, { CAIN_HeroFlight: HeroFlight });
})();
