// origin-film-scenes.jsx — the seven chapters of the GrytLabs origin film.
const { Sprite, useSprite, useTime, Easing, clamp } = window;
const { FILM, useFilmConfig, Appear, ChapterShell, Hi, Em, Body, ChapterHead,
        Quote, Beats, GlassMoment, HexMark, LogoMark } = window;

// ── 0 · Title (0–12) ────────────────────────────────────────────────────────
// Ground zero: the logo appears, a box is drawn + folded shut over it, then the
// box UNFOLDS to release the title — "how GrytLabs unfolded" made literal.
function TitleScene() {
  const { opening } = useFilmConfig();
  return (
    <ChapterShell grow={0.02} origin="50% 46%">
      <Sprite start={0} end={12} keepMounted={false}>
        <Appear at={4.5} x={960} y={234} align="center">
          <div style={{ fontFamily: FILM.mono, fontSize: 21, letterSpacing: '0.42em',
            textTransform: 'uppercase', color: FILM.gold, whiteSpace: 'nowrap' }}>
            GrytLabs&nbsp;&nbsp;·&nbsp;&nbsp;Origin Record</div>
        </Appear>
        <Appear at={3.95} x={960} y={282} align="center" w={1600} dur={1.3} from={38}>
          <div style={{ fontFamily: FILM.serif, fontWeight: 600, fontSize: 116,
            lineHeight: 1.02, color: FILM.ink, letterSpacing: '-0.012em',
            textAlign: 'center' }}>How GrytLabs <span style={{ fontStyle: 'italic' }}>unfolded</span></div>
        </Appear>
        <TitleLogo />
        <Appear at={5.2} x={960} y={588} align="center" w={1160}>
          <div style={{ fontFamily: FILM.sans, fontSize: 30, lineHeight: 1.55,
            color: FILM.sec, textAlign: 'center', textWrap: 'pretty' }}>
            One year. From a disrupted consulting practice to a frontier AI
            research and development firm.</div>
        </Appear>
        <Appear at={6.9} x={960} y={726} align="center">
          <div style={{ fontFamily: FILM.mono, fontSize: 19, letterSpacing: '0.34em',
            textTransform: 'uppercase', color: FILM.ink, whiteSpace: 'nowrap' }}>
            June 2025 — June 2026</div>
        </Appear>
        {opening === 'mark' ? <MarkDraw /> : <GroundZero />}
      </Sprite>
    </ChapterShell>
  );
}

// The cold open. localTime beats:
//   0.15–0.9  logo fades/scales in, centered
//   1.05–1.85 a square is drawn around it
//   1.9–2.75  four box flaps fold in and shade it shut; logo fades under the lid
//   2.75–3.15 held closed, sealed  ·  the story, boxed up
//   3.15–4.45 the box UNFOLDS — flaps swing open, releasing the title
function GroundZero() {
  const { localTime: lt } = useSprite();
  if (lt > 4.9) return null;

  const S = 300, x0 = 810, y0 = 330, cx = 960, cy = 480;

  const orbIn = Easing.easeOutCubic(clamp((lt - 0.15) / 0.7, 0, 1));
  const orbO = orbIn * (1 - Easing.easeInOutCubic(clamp((lt - 2.05) / 0.5, 0, 1)));
  const orbPulse = Math.sin(lt * 5.0);                       // gentle breathing
  const orbScale = (0.84 + 0.16 * orbIn) * (1 + 0.035 * orbPulse);
  const orbGlowScale = 1 + 0.11 * orbPulse;                  // halo pulses more
  const orbGlowO = 0.8 + 0.2 * (0.5 + 0.5 * orbPulse);

  const drawP = Easing.easeInOutCubic(clamp((lt - 1.05) / 0.8, 0, 1));
  const outlineO = (drawP > 0 ? 1 : 0) * (1 - Easing.easeInOutCubic(clamp((lt - 3.15) / 0.85, 0, 1)));

  // flap angle: open(1) → fold shut(0) → hold → unfold(1)
  let openAmt;
  if (lt < 1.9) openAmt = 1;
  else if (lt < 2.75) openAmt = 1 - Easing.easeInOutCubic((lt - 1.9) / 0.85);
  else if (lt < 3.15) openAmt = 0;
  else openAmt = Easing.easeInOutCubic(clamp((lt - 3.15) / 1.45, 0, 1));
  const ang = clamp(openAmt, 0, 1) * 118;
  const flapO = clamp((92 - Math.abs(ang)) / 22, 0, 1);

  const sealO = Easing.easeOutCubic(clamp((lt - 2.5) / 0.32, 0, 1)) *
                (1 - Easing.easeInCubic(clamp((lt - 3.1) / 0.28, 0, 1)));
  const glowO = Easing.easeInOutCubic(clamp((lt - 1.9) / 0.8, 0, 1)) *
                (1 - Easing.easeInOutCubic(clamp((lt - 3.3) / 1.0, 0, 1)));

  const flaps = [
    { clip: 'polygon(0% 0%, 100% 0%, 50% 50%)',   o: '50% 0%',   tf: `rotateX(${-ang}deg)`,
      bg: 'linear-gradient(160deg, rgba(214,178,104,0.52) 0%, rgba(154,122,62,0.52) 100%)' },
    { clip: 'polygon(0% 100%, 100% 100%, 50% 50%)', o: '50% 100%', tf: `rotateX(${ang}deg)`,
      bg: 'linear-gradient(20deg, rgba(196,162,89,0.48) 0%, rgba(139,109,54,0.48) 100%)' },
    { clip: 'polygon(0% 0%, 0% 100%, 50% 50%)',    o: '0% 50%',   tf: `rotateY(${ang}deg)`,
      bg: 'linear-gradient(75deg, rgba(206,171,97,0.49) 0%, rgba(147,115,58,0.49) 100%)' },
    { clip: 'polygon(100% 0%, 100% 100%, 50% 50%)', o: '100% 50%', tf: `rotateY(${-ang}deg)`,
      bg: 'linear-gradient(285deg, rgba(201,164,92,0.49) 0%, rgba(143,113,56,0.49) 100%)' },
  ];

  return (
    <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
      {/* gold seat glow under the box */}
      <div style={{ position: 'absolute', left: cx - 360, top: cy - 360, width: 720, height: 720,
        opacity: glowO, background:
        'radial-gradient(circle, rgba(201,165,90,0.16), rgba(201,165,90,0) 66%)' }}></div>

      {/* the gold orb — the prior form, boxed up and transformed */}
      {orbO > 0.001 ? (
        <div style={{ position: 'absolute', left: cx, top: cy, opacity: orbO,
          transform: `translate(-50%,-50%) scale(${orbScale})`, transformOrigin: 'center' }}>
          <div style={{ position: 'absolute', left: '50%', top: '50%', width: 240, height: 240,
            marginLeft: -120, marginTop: -120, opacity: orbGlowO,
            transform: `scale(${orbGlowScale})`, transformOrigin: 'center',
            background: 'radial-gradient(circle, rgba(232,196,120,0.34), rgba(232,196,120,0) 64%)' }}></div>
          <div style={{ position: 'absolute', left: '50%', top: '50%', width: 108, height: 108,
            marginLeft: -54, marginTop: -54, borderRadius: '50%',
            background: 'radial-gradient(circle at 36% 32%, #fff6da 0%, #eecb84 34%, #c9a55a 66%, #8c6c36 100%)',
            boxShadow: '0 0 40px rgba(232,196,120,0.42), inset -6px -8px 18px rgba(84,60,18,0.55)' }}></div>
        </div>
      ) : null}

      {/* the square, drawn */}
      {outlineO > 0.001 ? (
        <svg width={S + 8} height={S + 8} viewBox={`0 0 ${S + 8} ${S + 8}`}
          style={{ position: 'absolute', left: x0 - 4, top: y0 - 4, opacity: outlineO }}>
          <rect x="4" y="4" width={S} height={S} rx="3" fill="none" stroke={FILM.gold}
            strokeWidth="2" pathLength="1" strokeDasharray="1" strokeDashoffset={1 - drawP}></rect>
        </svg>
      ) : null}

      {/* box flaps + seal */}
      <div style={{ position: 'absolute', left: x0, top: y0, width: S, height: S,
        perspective: '1150px' }}>
        {flaps.map((f, i) => (
          <div key={i} style={{ position: 'absolute', inset: 0, background: f.bg,
            clipPath: f.clip, transformOrigin: f.o, transform: f.tf, opacity: flapO,
            backfaceVisibility: 'hidden',
            boxShadow: 'inset 0 0 40px rgba(58,38,10,0.2)' }}></div>
        ))}
        <div style={{ position: 'absolute', left: '50%', top: '50%', width: 30, height: 30,
          marginLeft: -15, marginTop: -15, transform: 'rotate(45deg)', opacity: sealO,
          background: 'linear-gradient(135deg, #f2da9d, #c9a55a)', borderRadius: 2,
          boxShadow: '0 0 0 1px rgba(58,38,10,0.45), inset 0 0 9px rgba(120,86,26,0.55)' }}></div>
      </div>
    </div>
  );
}

// The GrytLabs logo, released by the unfolding box — rises into place between
// the headline and the subtitle, and stays with the title card.
function TitleLogo() {
  const { localTime: lt } = useSprite();
  const t = Easing.easeOutCubic(clamp((lt - 3.5) / 1.0, 0, 1));
  if (t <= 0.001) return null;
  const scale = 0.78 + 0.22 * t;
  return (
    <div style={{ position: 'absolute', left: 960, top: 480, opacity: t,
      transform: `translate(-50%,-50%) scale(${scale})`, transformOrigin: 'center' }}>
      <div style={{ position: 'absolute', left: '50%', top: '50%', width: 320, height: 320,
        marginLeft: -160, marginTop: -160, opacity: 0.6 * t, pointerEvents: 'none',
        background: 'radial-gradient(circle, rgba(201,165,90,0.20), rgba(201,165,90,0) 60%)' }}></div>
      <LogoMark size={110} />
    </div>
  );
}

// Alternate opening (Tweaks → Opening → mark): the original self-drawing mark.
function MarkDraw() {
  const { localTime: lt } = useSprite();
  const p = Easing.easeInOutCubic(clamp((lt - 0.3) / 2.4, 0, 1));
  const o = Easing.easeOutCubic(clamp(lt / 0.8, 0, 1)) *
            (1 - Easing.easeInOutCubic(clamp((lt - 3.2) / 1.2, 0, 1)));
  if (o <= 0.001) return null;
  return (
    <div style={{ position: 'absolute', left: 960, top: 452, opacity: o,
      transform: 'translate(-50%,-50%)' }}>
      <HexMark x={-98} y={-98} size={196} progress={p} align="left" />
    </div>
  );
}

// ── 1 · The disruption (12–38) ──────────────────────────────────────────────
function DisruptionScene() {
  return (
    <ChapterShell>
      <ChapterHead date="June 2025" kicker="First contact" title="The disruption" />
      <Appear at={2.6} out={10.2} x={200} y={396} w={950}>
        <Body>Widespread changes to federal funding hit <Hi>DC Smith Advisory</Hi> and
          opened a sudden gap.</Body>
      </Appear>
      <Appear at={5.8} out={10.2} x={200} y={556} w={950}>
        <Body>She opened a large language model for the first time. Not to build
          anything — to steady the moment: a résumé, cover letters, a job search.</Body>
      </Appear>
      <Appear at={11} x={200} y={396} w={950}>
        <Body size={40} color={FILM.ink}>The first result stopped her.</Body>
      </Appear>
      <Quote at={12.8} y={500} w={960}
        quote="“Wow — it actually did a decent job on the first try. I wonder if it can help with the company website?”"
        who="Cam · June 2025" />
      <Appear at={17.5} x={200} y={766} w={950}>
        <Body>A tool grabbed to survive a setback had just compressed a week of
          contractor work into an afternoon.</Body>
      </Appear>
      <Beats at={15.5} label="First outputs" items={[
        ['Résumé & cover letters', 'the entry point'],
        ['DCSA website, revamped', 'professional-grade — no contractor invoice'],
      ]} />
      <Appear at={20.5} x={1240} y={640} w={520}>
        <div style={{ fontFamily: FILM.serif, fontStyle: 'italic', fontSize: 30,
          lineHeight: 1.5, color: FILM.sec }}>Quietly, the job hunt started to
          become something else.</div>
      </Appear>
    </ChapterShell>
  );
}

// ── 2 · The drift (38–66) ───────────────────────────────────────────────────
function DriftTitle() {
  const { progress } = useSprite();
  return (
    <span style={{ letterSpacing: `${-0.01 + 0.07 * progress}em`,
      transition: 'letter-spacing 0.1s linear' }}>The drift</span>
  );
}

function DriftScene() {
  return (
    <ChapterShell>
      <ChapterHead date="Summer — October 2025" kicker="Exploration" title={<DriftTitle />} />
      <Appear at={2.6} out={9.2} x={200} y={396} w={960}>
        <Body>She drove the tools deeper into the practice itself — and the deeper
          she went, the more she saw the same flaw: the models <Hi>drifted</Hi>.
          Lost the thread. Contradicted themselves. Wandered from facts sitting
          right there in the corpus.</Body>
      </Appear>
      <Quote at={10} y={400} w={1000} size={44}
        quote="“Why is it still drifting when the foundational knowledge is readily accessible in the corpus?”"
        who="Cam · summer 2025"
        tag="The question that became everything" />
      <Appear at={17} x={200} y={760} w={960}>
        <Body>Not raw capability — something <Hi>structural</Hi>, in how these
          systems held, or failed to hold, what they were given.</Body>
      </Appear>
      <Beats at={12} label="Work product" items={[
        ['AI-accelerated DCSA workflows', 'the tools, applied at depth'],
        ['GrantsProQR product spec', 'AI for regulatory compliance'],
        ['Phased AI Transformation Plan', 'structure for the shift'],
        ['Cam’s AI Studio article', 'Oct 2025 · thinking, published to inspect'],
      ]} />
    </ChapterShell>
  );
}

// ── 3 · The weaving (66–96) ─────────────────────────────────────────────────
function WeaveVisual() {
  const { localTime: lt } = useSprite();
  const r = 134, cx = 290, cy = 292;
  const crashEnd = 14, settleEnd = 19.5;
  const oscAt = (x) => 320 - 215 * Math.abs(Math.sin(x * 0.7));
  let sep;
  if (lt < crashEnd) sep = oscAt(lt);
  else if (lt < settleEnd) {
    const k = Easing.easeInOutCubic((lt - crashEnd) / (settleEnd - crashEnd));
    sep = oscAt(crashEnd) + (148 - oscAt(crashEnd)) * k;
  } else sep = 148;
  const overlap = Math.max(0, 200 - sep) / 200;
  const shake = lt < crashEnd ? overlap * Math.sin(lt * 42) * 5 : 0;
  const settled = clamp((lt - settleEnd) / 2, 0, 1);
  const rot = lt > settleEnd ? (lt - settleEnd) * 7 : 0;
  const enter = Easing.easeOutCubic(clamp((lt - 1.2) / 1.4, 0, 1));
  const labelO = 1 - Easing.easeInOutCubic(clamp((lt - crashEnd) / 4, 0, 1));
  if (enter <= 0.001) return null;
  return (
    <div style={{ position: 'absolute', left: 1230, top: 200, width: 580, height: 680, opacity: enter }}>
      <svg width="580" height="600" viewBox="0 0 580 600">
        <defs>
          <radialGradient id="weaveGlow">
            <stop offset="0%" stopColor="rgba(224,193,132,0.5)"></stop>
            <stop offset="100%" stopColor="rgba(224,193,132,0)"></stop>
          </radialGradient>
        </defs>
        <circle cx={cx} cy={cy} r="248" fill="none" stroke={FILM.line}
          strokeDasharray="3 11" opacity="0.7"></circle>
        <g transform={`rotate(${rot} ${cx} ${cy}) translate(${shake} 0)`}>
          {settled > 0 ? <circle cx={cx} cy={cy} r={56 + 26 * settled}
            fill="url(#weaveGlow)" opacity={settled * 0.95}></circle> : null}
          <circle cx={cx - sep / 2} cy={cy} r={r} fill="none" stroke={FILM.gold}
            strokeWidth="2.5" opacity="0.9"></circle>
          <circle cx={cx + sep / 2} cy={cy} r={r} fill="none" stroke={FILM.mut}
            strokeWidth="2.5" opacity="0.85"></circle>
        </g>
      </svg>
      <div style={{ position: 'absolute', top: 556, left: 40, width: 220, textAlign: 'center',
        fontFamily: FILM.mono, fontSize: 15, letterSpacing: '0.2em', textTransform: 'uppercase',
        color: FILM.gold, opacity: labelO * 0.9 }}>World-model loop</div>
      <div style={{ position: 'absolute', top: 556, left: 320, width: 220, textAlign: 'center',
        fontFamily: FILM.mono, fontSize: 15, letterSpacing: '0.2em', textTransform: 'uppercase',
        color: FILM.mut, opacity: labelO * 0.9 }}>Enterprise loop</div>
      <div style={{ position: 'absolute', top: 600, left: 0, width: 580, textAlign: 'center',
        fontFamily: FILM.serif, fontStyle: 'italic', fontSize: 28, color: FILM.goldHi,
        opacity: settled }}>two loops, one frame</div>
    </div>
  );
}

function WeaveScene() {
  return (
    <ChapterShell origin="38% 42%">
      <ChapterHead date="December 2025 — January 2026" kicker="Convergence" title="The weaving" />
      <WeaveVisual />
      <Appear at={2.6} out={13.6} x={200} y={396} w={900}>
        <Body size={31}>Two loops had been crashing into each other for months:
          LeCun’s <Hi>world-model loop</Hi>, and the <Hi>enterprise loop</Hi> she’d
          lived as a <Hi>performance auditor</Hi> — plan, execute, learn, plan.</Body>
      </Appear>
      <Quote at={8.5} out={13.6} y={640} w={900} size={38}
        quote="“His formula and the enterprise formula kept crashing in my brain.”"
        who="Cam · the suspended thought" />
      <Quote at={14.8} y={612} w={1000} size={37}
        quote="“It wasn’t crashing anymore. It was weaving together. This might actually work!”"
        who="Cam · winter 2025–26" />
      <Appear at={16.5} x={200} y={392} w={980}>
        <Body size={31}><Hi>Claude Opus 4.5</Hi> meets the <Hi>DCSA Unified Data
          Architecture</Hi> in December — the <Hi>GrantsProQR</Hi> prototype, an
          AI-native grant consulting service, ushers in the new year.</Body>
      </Appear>
    </ChapterShell>
  );
}

// The catalyst's visual: a line-drawn rocket lifting off a launch pad.
function RocketVisual() {
  const { localTime: lt } = useSprite();
  const enter = Easing.easeOutCubic(clamp((lt - 1.0) / 1.4, 0, 1));
  if (enter <= 0.001) return null;
  const ignite = Easing.easeOutCubic(clamp((lt - 3.6) / 1.4, 0, 1));
  const lift = Easing.easeInCubic(clamp((lt - 5.5) / 10, 0, 1));
  const riseY = -lift * 170;
  const flick = 1 + 0.2 * Math.sin(lt * 24);
  const flame = ignite * (28 + 78 * lift) * flick;
  const smoke = clamp((lt - 5) / 3.5, 0, 1);
  const gold = FILM.gold, blue = FILM.mut;

  return (
    <div style={{ position: 'absolute', left: 1250, top: 150, width: 540, height: 760, opacity: enter }}>
      <svg width="540" height="760" viewBox="0 0 540 760">
        <line x1="150" y1="300" x2="150" y2="586" stroke={FILM.line} strokeWidth="1.5" opacity="0.7"></line>
        <line x1="150" y1="340" x2="240" y2="340" stroke={FILM.line} strokeWidth="1" opacity="0.5"></line>
        <line x1="150" y1="410" x2="240" y2="410" stroke={FILM.line} strokeWidth="1" opacity="0.5"></line>
        <line x1="150" y1="480" x2="240" y2="480" stroke={FILM.line} strokeWidth="1" opacity="0.5"></line>
        <line x1="190" y1="586" x2="430" y2="586" stroke={FILM.line} strokeWidth="2"></line>
        <line x1="232" y1="586" x2="210" y2="636" stroke={FILM.line} strokeWidth="1.5"></line>
        <line x1="388" y1="586" x2="410" y2="636" stroke={FILM.line} strokeWidth="1.5"></line>

        {smoke > 0 ? [0, 1, 2, 3, 4].map((i) => {
          const s = smoke * (1 - i * 0.1);
          const dx = (i - 2) * 52 * smoke;
          return <circle key={i} cx={310 + dx} cy={582 - i * 4} r={16 + 30 * s}
            fill="none" stroke={gold} strokeWidth="1" opacity={0.3 * s}></circle>;
        }) : null}

        <g transform={`translate(0 ${riseY})`}>
          {flame > 2 ? (
            <React.Fragment>
              <path d={`M292 556 Q310 ${556 + flame} 328 556`} fill="none" stroke={gold} strokeWidth="2.5" opacity="0.9"></path>
              <path d={`M300 556 Q310 ${556 + flame * 0.62} 320 556`} fill="none" stroke={FILM.goldHi} strokeWidth="1.5" opacity="0.85"></path>
            </React.Fragment>
          ) : null}
          <path d="M310 292 C286 316 280 352 280 398 L280 516 C280 542 292 556 310 556 C328 556 340 542 340 516 L340 398 C340 352 334 316 310 292 Z"
            fill="none" stroke={gold} strokeWidth="2.5"></path>
          <path d="M280 502 L252 556 L280 538 Z" fill="none" stroke={blue} strokeWidth="2"></path>
          <path d="M340 502 L368 556 L340 538 Z" fill="none" stroke={blue} strokeWidth="2"></path>
          <circle cx="310" cy="376" r="15" fill="none" stroke={blue} strokeWidth="2"></circle>
          <circle cx="310" cy="376" r="6" fill="none" stroke={gold} strokeWidth="1.5" opacity="0.8"></circle>
          <line x1="284" y1="470" x2="336" y2="470" stroke={gold} strokeWidth="1.5" opacity="0.55"></line>
        </g>
      </svg>
    </div>
  );
}

function CatalystScene() {
  return (
    <ChapterShell origin="42% 42%">
      <ChapterHead date="January 2026" kicker="Convergence" title="The catalyst" />
      <RocketVisual />
      <Appear at={2.4} x={200} y={402} w={940}>
        <Body size={32}>In January, her sister’s <Hi>six words</Hi> changed
          everything — <Em>“I want to start a business!”</Em></Body>
      </Appear>
      <Quote at={6.0} y={556} w={960} size={40}
        quote="“I’ll just extract the plumbing and ship an app she can use to build an AI-native company herself!”"
        who="Cam · January 2026" />
      <Appear at={11} x={200} y={802} w={1000}>
        <Body size={30}>The answer had a shape: <Hi>GR🚀T</Hi>, an app for founders —
          and beneath it, the <Hi>Decision Lineage Protocol (DLP)</Hi>.</Body>
      </Appear>
    </ChapterShell>
  );
}

// ── 4 · The transition (96–124) ─────────────────────────────────────────────
function ChipRow({ at = 0, y = 916, items }) {
  const { localTime: lt } = useSprite();
  return (
    <div style={{ position: 'absolute', left: 0, right: 0, top: y, display: 'flex',
      justifyContent: 'center', gap: 18 }}>
      {items.map((c, i) => {
        const o = Easing.easeOutCubic(clamp((lt - at - i * 0.55) / 0.7, 0, 1));
        return (
          <div key={i} style={{ opacity: o, transform: `translateY(${(1 - o) * 18}px)`,
            display: 'inline-flex', alignItems: 'baseline', gap: 13,
            border: `1px solid ${FILM.lineStrong}`, borderRadius: 999,
            padding: '15px 26px', background: 'rgba(10,20,36,0.6)' }}>
            <span style={{ width: 9, height: 9, background: FILM.gold,
              transform: 'rotate(45deg)', borderRadius: 1.5, flex: '0 0 auto' }}></span>
            <span style={{ fontFamily: FILM.sans, fontSize: 21, fontWeight: 600,
              color: FILM.ink, whiteSpace: 'nowrap' }}>{c[0]}</span>
            <span style={{ fontFamily: FILM.mono, fontSize: 15, color: FILM.dim,
              whiteSpace: 'nowrap' }}>{c[1]}</span>
          </div>
        );
      })}
    </div>
  );
}

function TransitionScene() {
  return (
    <ChapterShell origin="50% 44%">
      <ChapterHead date="February — March 2026" kicker="The transition" size={84}
        title="To what end, and for whom" />
      <Appear at={2.8} x={200} y={350} w={1020}>
        <Body size={31}>In February she watched the World Model Workshop at <Hi>Mila</Hi>.</Body>
      </Appear>
      <Appear at={6.0} x={200} y={440} w={1020}>
        <Body size={31}>The field’s most serious minds circled one question — <Em>what is
          the right level of abstraction?</Em> Inconclusive.</Body>
      </Appear>
      <GlassMoment at={9.5} y={628} who="Cam · February 2026">
        “I kept waiting for a hook that never came — to what end is this work being
        done? What value are we aiming for, and for whom?”
      </GlassMoment>
    </ChapterShell>
  );
}

function SpecDemoScene() {
  return (
    <ChapterShell origin="46% 42%">
      <ChapterHead date="March 2026" kicker="The transition" size={84}
        title="The spec-faithful demo" />
      <Appear at={2.6} x={200} y={372} w={1460}>
        <Body size={32}>She watched the workshop panel again and again — pausing, taking
          notes, researching the notes, returning. Evidence mounting.</Body>
      </Appear>
      <Appear at={5.0} x={200} y={500} w={1460}>
        <Body size={32}>Confidence and capability high, Cam and <Hi>Claude Opus 4.6</Hi> turn
          the <Hi>DLP specification</Hi> into a <Hi>DLP-SDK</Hi> prototype. After that,
          things moved fast.</Body>
      </Appear>
      <ChipRow at={7.0} y={760} items={[
        ['Mila Workshop', 'Feb 2026'],
        ['World Model Initiative', 'the thesis'],
        ['GrytLabs Research Institute', 'Mar 2026'],
        ['GrytLabs Dynamics Inc.', 'May 8 · Delaware'],
      ]} />
    </ChapterShell>
  );
}

// ── 5 · The institute (124–147) ─────────────────────────────────────────────
function ClusterCard({ x, y, at, title, items, w = 412 }) {
  const { localTime: lt } = useSprite();
  const o = Easing.easeOutCubic(clamp((lt - at) / 0.8, 0, 1));
  if (o <= 0.001) return null;
  return (
    <div style={{ position: 'absolute', left: x, top: y, width: w, opacity: o,
      boxSizing: 'border-box',
      transform: `translateY(${(1 - o) * 22}px)`, background: 'rgba(19,30,54,0.5)',
      border: `1px solid ${FILM.line}`, borderRadius: 14, padding: '20px 24px' }}>
      <div style={{ fontFamily: FILM.mono, fontSize: 14, letterSpacing: '0.22em',
        textTransform: 'uppercase', color: FILM.gold, marginBottom: 12 }}>{title}</div>
      {items.map((it, i) => {
        const io = Easing.easeOutCubic(clamp((lt - at - 0.5 - i * 0.35) / 0.6, 0, 1));
        return (
          <div key={i} style={{ display: 'flex', gap: 12, alignItems: 'baseline',
            opacity: io, padding: '5px 0' }}>
            <span style={{ width: 8, height: 8, flex: '0 0 auto', background: FILM.gold,
              transform: 'rotate(45deg)', borderRadius: 1.5, opacity: 0.85 }}></span>
            <span style={{ fontFamily: FILM.sans, fontSize: 19, color: FILM.ink2 }}>{it}</span>
          </div>
        );
      })}
    </div>
  );
}

// The institute as a tree unfolding from the GrytLabs Dynamics logo: a WMI /
// yellow-pentagon branch and a GEM / grey-pentagon branch.
function DynamicsTree() {
  const { localTime: lt } = useSprite();
  const rev = (t0, d = 0.6) => Easing.easeOutCubic(clamp((lt - t0) / d, 0, 1));
  const logoO = rev(0.4, 1.1);
  const lx = 316, rx = 1120, busY = 404, headY = 452, r0 = 524, step = 66;
  const gold = FILM.gold, grey = '#9aa3b2';

  const wmi = ['Published DLP Specification', 'DLP-SDK', 'World Model Studio',
    'Governance Framework (WMGF)', 'AI Risk Assessment (AIRA-WM)', 'WMI Series Publication'];
  const gem = ['GEM Publication Series', 'ProtoLex', 'Loom', 'World Model Federation Exchange'];

  const vline = (x, y, h, c, o) => (<div style={{ position: 'absolute', left: x, top: y, width: 2, height: h, background: c, opacity: o }}></div>);
  const hline = (x, y, w, c, o) => (<div style={{ position: 'absolute', left: x, top: y, width: w, height: 2, background: c, opacity: o }}></div>);
  const penta = (x, y, s, c, o) => (<div style={{ position: 'absolute', left: x, top: y, width: s, height: s, background: c, opacity: o, clipPath: 'polygon(50% 0%,100% 38%,82% 100%,18% 100%,0% 38%)' }}></div>);

  const row = (spineX, i, label, color, baseAt) => {
    const y = r0 + i * step;
    const o = rev(baseAt + i * 0.22);
    return (
      <React.Fragment key={label}>
        {hline(spineX + 1, y, 26, color, 0.6 * o)}
        {penta(spineX + 28, y - 12, 24, color, o)}
        <div style={{ position: 'absolute', left: spineX + 66, top: y - 15, opacity: o,
          transform: `translateX(${(1 - o) * -12}px)`,
          fontFamily: FILM.sans, fontSize: 18, color: FILM.ink2, whiteSpace: 'nowrap' }}>{label}</div>
      </React.Fragment>
    );
  };

  const head = (spineX, y, label, color, o) => (
    <React.Fragment>
      {penta(spineX - 13, y - 15, 30, color, o)}
      <div style={{ position: 'absolute', left: spineX + 32, top: y - 17, opacity: o,
        fontFamily: FILM.serif, fontWeight: 600, fontSize: 24, color: FILM.ink, whiteSpace: 'nowrap' }}>{label}</div>
    </React.Fragment>
  );

  return (
    <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
      <div style={{ position: 'absolute', left: 960, top: 288, opacity: logoO,
        transform: `translate(-50%,-50%) scale(${0.85 + 0.15 * logoO})` }}>
        <LogoMark size={100} />
      </div>
      {vline(959, 340, 64, FILM.lineStrong, rev(1.2))}
      {hline(lx, busY, rx - lx, FILM.lineStrong, rev(1.4, 0.9))}
      {vline(lx, busY, headY - busY, gold, rev(1.7))}
      {vline(rx, busY, headY - busY, grey, rev(1.7))}

      {head(lx, headY, 'World Model Initiative (WMI)', gold, rev(2.0))}
      {vline(lx, 478, 376, gold, rev(2.4) * 0.6)}
      {wmi.map((l, i) => row(lx, i, l, gold, 2.6))}

      {head(rx, headY, 'Generated Enterprise Manifest (GEM)', gold, rev(3.6))}
      {vline(rx, 478, 244, grey, rev(4.0) * 0.6)}
      {gem.map((l, i) => row(rx, i, l, grey, 4.2))}

      <Appear at={7.4} x={rx - 13} y={796} w={700}>
        <div style={{ fontFamily: FILM.serif, fontStyle: 'italic', fontSize: 29,
          lineHeight: 1.45, color: FILM.goldHi, textWrap: 'pretty' }}>
          The cycle turns again — GEM is the new thesis; the work begins one altitude higher.</div>
      </Appear>
    </div>
  );
}

function InstituteScene() {
  return (
    <ChapterShell origin="50% 42%">
      <Appear at={0.3} x={200} y={168}>
        <div style={{ fontFamily: FILM.mono, fontSize: 22, letterSpacing: '0.3em',
          textTransform: 'uppercase', color: FILM.gold, whiteSpace: 'nowrap' }}>
          April — June 2026<span style={{ color: FILM.dim }}>&nbsp;&nbsp;·&nbsp;&nbsp;GrytLabs Dynamics Inc</span></div>
      </Appear>
      <DynamicsTree />
    </ChapterShell>
  );
}

// ── 6 · Coda (147–160) ──────────────────────────────────────────────────────
// The "for-whom", made literal — a slow carousel of the practitioners the work
// serves. Small translucent glass cards, each carrying a gold practitioner
// glyph, drift past a masked window with the centre held in focus.
const FORWHOM = [
  ['attorney', 'Attorney'],
  ['auditor', 'Auditor'],
  ['clinician', 'Clinician'],
  ['educator', 'Educator'],
  ['planner', 'Planner'],
  ['public-servant', 'Public servant'],
  ['household-manager', 'Household manager'],
  ['small-business-operator', 'Small business'],
];

function ForWhomCard({ slug, label, x, y, w, h, scale, opacity, focus }) {
  return (
    <div style={{ position: 'absolute', left: x, top: y, width: w, height: h,
      opacity, transform: `scale(${scale})`, transformOrigin: 'center center',
      boxSizing: 'border-box', display: 'flex', flexDirection: 'column',
      alignItems: 'center', gap: 11, padding: '18px 12px 15px',
      background: 'linear-gradient(158deg, rgba(28,43,72,0.62), rgba(10,20,36,0.40))',
      border: `1px solid rgba(201,165,90,${(0.14 + 0.24 * focus).toFixed(3)})`,
      borderRadius: 16, willChange: 'transform, opacity',
      boxShadow: `inset 0 1px 0 rgba(255,255,255,0.05), 0 14px 34px rgba(0,0,0,${(0.26 + 0.16 * focus).toFixed(3)})` }}>
      <img src={`uploads/${slug}.svg`} width={54} height={54} alt={label}
        style={{ display: 'block', filter: 'drop-shadow(0 5px 9px rgba(0,0,0,0.34))' }} />
      <div style={{ fontFamily: FILM.mono, fontSize: 12, letterSpacing: '0.14em',
        textTransform: 'uppercase', color: FILM.sec, textAlign: 'center',
        lineHeight: 1.32, minHeight: 30, textWrap: 'balance' }}>{label}</div>
    </div>
  );
}

function ForWhomCarousel({ at = 5.7, top = 616 }) {
  const { localTime: lt } = useSprite();
  const appear = Easing.easeOutCubic(clamp((lt - at) / 1.2, 0, 1));
  if (appear <= 0.001) return null;

  const CARD = 152, GAP = 30, PITCH = CARD + GAP;      // 182
  const N = FORWHOM.length, setW = N * PITCH;          // 1456
  const WIN = 1380, winLeft = 960 - WIN / 2;           // 270
  const CARD_H = 130, WIN_H = 200;
  const winTop = top - (WIN_H - CARD_H) / 2;           // 581
  const cardTop = (WIN_H - CARD_H) / 2;                // 35

  const scroll = Math.max(0, lt - at - 0.5) * 34;      // px/s, dignified drift
  const tx = -(scroll % setW);

  const cards = [];
  for (let g = 0; g < 2; g++) {
    for (let i = 0; i < N; i++) {
      const localX = g * setW + i * PITCH + tx;
      if (localX + CARD < -24 || localX > WIN + 24) continue;
      const screenCx = winLeft + localX + CARD / 2;
      const focus = clamp(1 - Math.abs(screenCx - 960) / 820, 0, 1);
      const edge = clamp(Math.min(localX + CARD, WIN - localX) / 170, 0, 1);
      const scale = (0.9 + 0.13 * focus) * (0.93 + 0.07 * appear);
      const cardO = (0.32 + 0.68 * focus) * edge * appear;
      const bob = Math.sin(lt * 0.7 + i * 1.4) * 4;
      if (cardO <= 0.002) continue;
      cards.push(
        <ForWhomCard key={g + '-' + i} slug={FORWHOM[i][0]} label={FORWHOM[i][1]}
          x={localX} y={cardTop + bob + (1 - appear) * 28} w={CARD} h={CARD_H}
          scale={scale} opacity={cardO} focus={focus} />
      );
    }
  }

  return (
    <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
      <div style={{ position: 'absolute', left: 960, top: top + CARD_H / 2, width: 1200, height: 300,
        transform: 'translate(-50%,-50%)', opacity: 0.7 * appear,
        background: 'radial-gradient(ellipse 50% 50% at 50% 50%, rgba(201,165,90,0.11), rgba(201,165,90,0) 70%)' }}></div>
      <div style={{ position: 'absolute', left: winLeft, top: winTop, width: WIN, height: WIN_H,
        overflow: 'hidden',
        WebkitMaskImage: 'linear-gradient(90deg, transparent 0, #000 15%, #000 85%, transparent 100%)',
        maskImage: 'linear-gradient(90deg, transparent 0, #000 15%, #000 85%, transparent 100%)' }}>
        {cards}
      </div>
    </div>
  );
}

function CodaScene() {
  return (
    <ChapterShell grow={0.018} origin="50% 45%">
      <Appear at={0.9} x={960} y={360} align="center" w={1400} dur={1.2}>
        <div style={{ fontFamily: FILM.serif, fontSize: 52, lineHeight: 1.28,
          color: FILM.ink, textAlign: 'center' }}>She had opened a chat window
          to fix a résumé.</div>
      </Appear>
      <Appear at={4.2} x={960} y={450} align="center" w={1340} dur={1.2}>
        <div style={{ fontFamily: FILM.serif, fontSize: 52, lineHeight: 1.28,
          color: FILM.ink, textAlign: 'center', textWrap: 'pretty' }}>A year later,
          the work had what the panel never named: <Em>an end, and a for-whom.</Em></div>
      </Appear>
      <ForWhomCarousel at={5.7} top={616} />
      <Appear at={8.2} x={960} y={812} align="center" w={1100}>
        <div style={{ fontFamily: FILM.serif, fontStyle: 'italic', fontSize: 37,
          color: FILM.goldHi, textAlign: 'center' }}>All you need is knowledge — and Gr🚀t.</div>
      </Appear>
    </ChapterShell>
  );
}

function CodaMark() {
  const { localTime } = useSprite();
  const p = Easing.easeInOutCubic(clamp((localTime - 9.6) / 1.8, 0, 1));
  if (p <= 0.001) return null;
  return (
    <div style={{ position: 'absolute', left: 960, top: 806, opacity: 0.95 * p,
      transform: `translate(-50%,-50%) scale(${0.8 + 0.2 * p})` }}>
      <LogoMark size={72} />
    </div>
  );
}

// ── P1 · Twenty years, one job (12–31) ──────────────────────────────────────
function EraCard({ x, y, w = 700, at, roman, years, kicker, title, roles }) {
  const { localTime: lt } = useSprite();
  const o = Easing.easeOutCubic(clamp((lt - at) / 0.8, 0, 1));
  if (o <= 0.001) return null;
  return (
    <div style={{ position: 'absolute', left: x, top: y, width: w, opacity: o,
      transform: `translateY(${(1 - o) * 20}px)`, borderLeft: `2px solid ${FILM.gold}`,
      paddingLeft: 26 }}>
      <div style={{ fontFamily: FILM.mono, fontSize: 15, letterSpacing: '0.2em',
        textTransform: 'uppercase', color: FILM.gold, marginBottom: 7 }}>
        {roman}<span style={{ color: FILM.dim }}>&nbsp;&nbsp;·&nbsp;&nbsp;{years}&nbsp;&nbsp;·&nbsp;&nbsp;{kicker}</span>
      </div>
      <div style={{ fontFamily: FILM.serif, fontWeight: 600, fontSize: 29, color: FILM.ink,
        lineHeight: 1.12, marginBottom: 9 }}>{title}</div>
      {roles.map((r, i) => (
        <div key={i} style={{ fontFamily: FILM.sans, fontSize: 18.5, color: FILM.mut,
          lineHeight: 1.4, marginTop: 3 }}>{r}</div>
      ))}
    </div>
  );
}

function PrologueFounderScene() {
  return (
    <ChapterShell origin="42% 42%">
      <ChapterHead date="2005 — 2025" kicker="Prologue · the professional" size={78}
        title="Twenty years, one job" />
      <Appear at={2.4} x={200} y={402} w={790}>
        <Body size={30}>Before the chat window that would change everything, there were
          two decades under a single title — <Hi>performance auditor</Hi>. Making
          high-stakes public money accountable, and usable by the people who run it.</Body>
      </Appear>
      <EraCard at={4.2} x={1046} y={356} roman="I" years="2005 – 2019" kicker="the audit decade"
        title="Accountability over public money"
        roles={['Federal Grants Intern — FDOT',
                'Senior Performance Auditor — FDOT-OIG',
                'Senior Auditor — City of Tallahassee']} />
      <EraCard at={6.0} x={1046} y={544} roman="II" years="2019 – 2023" kicker="the grants pivot"
        title="From auditing it to running it"
        roles={['City Grant Manager — the $46.2M ARPA program',
                'Advisory Manager, Cherry Bekaert — Grants SME']} />
      <EraCard at={7.8} x={1046} y={732} roman="III" years="2023 – 2025" kicker="the founder turn"
        title="Teaching it, then building it"
        roles={['DC Smith Advisory — federal grants SME & instructor',
                'GrytLabs — the accountability layer, generalized to AI']} />
      <Appear at={11} x={200} y={620} w={800}>
        <div style={{ fontFamily: FILM.mono, fontSize: 16, letterSpacing: '0.1em',
          color: FILM.sec, lineHeight: 1.95 }}>
          Certified Internal Auditor (CIA)&nbsp;&nbsp;·&nbsp;&nbsp;Certified Government Auditing Professional (CGAP)&nbsp;&nbsp;·&nbsp;&nbsp;FSU B.S. Accounting &amp; Finance
        </div>
      </Appear>
      <Appear at={13.5} x={200} y={724} w={800}>
        <div style={{ fontFamily: FILM.serif, fontStyle: 'italic', fontSize: 32,
          lineHeight: 1.4, color: FILM.goldHi, textWrap: 'pretty' }}>
          Every title was the same job — generalized, one day, to the AI era.</div>
      </Appear>
    </ChapterShell>
  );
}

// ── P2 · The ARPA work (31–50) ──────────────────────────────────────────────
function InstrumentCard({ x, y, w = 660, at, tag, title, role, points }) {
  const { localTime: lt } = useSprite();
  const o = Easing.easeOutCubic(clamp((lt - at) / 0.8, 0, 1));
  if (o <= 0.001) return null;
  return (
    <div style={{ position: 'absolute', left: x, top: y, width: w, opacity: o,
      boxSizing: 'border-box', transform: `translateY(${(1 - o) * 20}px)`,
      background: 'rgba(19,30,54,0.5)', border: `1px solid ${FILM.line}`,
      borderRadius: 13, padding: '17px 24px' }}>
      <div style={{ fontFamily: FILM.mono, fontSize: 13, letterSpacing: '0.2em',
        textTransform: 'uppercase', color: FILM.gold, marginBottom: 6 }}>{tag}</div>
      <div style={{ fontFamily: FILM.serif, fontWeight: 600, fontSize: 22, color: FILM.ink,
        lineHeight: 1.12 }}>{title}</div>
      <div style={{ fontFamily: FILM.sans, fontSize: 15, color: FILM.sec, marginTop: 4,
        marginBottom: 11 }}>{role}</div>
      {points.map((p, i) => {
        const io = Easing.easeOutCubic(clamp((lt - at - 0.5 - i * 0.3) / 0.6, 0, 1));
        return (
          <div key={i} style={{ display: 'flex', gap: 11, alignItems: 'baseline',
            opacity: io, padding: '3px 0' }}>
            <span style={{ width: 7, height: 7, flex: '0 0 auto', background: FILM.gold,
              transform: 'rotate(45deg)', borderRadius: 1.5, opacity: 0.85 }}></span>
            <span style={{ fontFamily: FILM.sans, fontSize: 16, color: FILM.ink2,
              lineHeight: 1.35 }}>{p}</span>
          </div>
        );
      })}
    </div>
  );
}

// Right-side panel: the private-sector arc as a vertical mini-timeline.
function PrivateSectorPanel({ at = 7.0 }) {
  const { localTime: lt } = useSprite();
  const panelO = Easing.easeOutCubic(clamp((lt - at) / 0.8, 0, 1));
  if (panelO <= 0.001) return null;
  const rev = (t0, d = 0.6) => Easing.easeOutCubic(clamp((lt - t0) / d, 0, 1));
  const spineX = 86;

  const node = (y, o) => (
    <div style={{ position: 'absolute', left: spineX - 6, top: y - 6, width: 12, height: 12,
      transform: 'rotate(45deg)', borderRadius: 2, opacity: o,
      background: 'linear-gradient(135deg, #f2da9d, #c9a55a)',
      boxShadow: '0 0 0 5px rgba(14,24,44,0.95)' }}></div>
  );
  const chip = (y, o, date, company) => (
    <div style={{ position: 'absolute', left: spineX + 28, top: y - 15, opacity: o,
      display: 'flex', alignItems: 'center', gap: 14 }}>
      <span style={{ fontFamily: FILM.mono, fontSize: 12, letterSpacing: '0.16em',
        textTransform: 'uppercase', color: FILM.gold, border: `1px solid ${FILM.lineStrong}`,
        borderRadius: 999, padding: '5px 12px', whiteSpace: 'nowrap' }}>{date}</span>
      <span style={{ fontFamily: FILM.serif, fontWeight: 600, fontSize: 22, color: FILM.ink,
        whiteSpace: 'nowrap' }}>{company}</span>
    </div>
  );
  const stat = (v, label, strike, o) => (
    <div style={{ opacity: o }}>
      <div style={{ fontFamily: FILM.serif, fontSize: 33, lineHeight: 1,
        color: strike ? FILM.gold : FILM.goldHi,
        textDecoration: strike ? 'line-through' : 'none',
        textDecorationColor: 'rgba(224,193,132,0.6)', textDecorationThickness: '1.5px' }}>{v}</div>
      <div style={{ fontFamily: FILM.mono, fontSize: 11, letterSpacing: '0.12em',
        textTransform: 'uppercase', color: FILM.dim, marginTop: 8, whiteSpace: 'nowrap' }}>{label}</div>
    </div>
  );

  const items = ['Municipal Accounting Optimization', 'IHE Single Audit Readiness',
                 'Local Government Controls Assessment', 'Advisory Services Management'];

  return (
    <div style={{ position: 'absolute', left: 990, top: 320, width: 820, height: 408,
      opacity: panelO, transform: `translateY(${(1 - panelO) * 16}px)`, boxSizing: 'border-box',
      background: 'rgba(16,26,48,0.42)', border: `1px solid ${FILM.line}`, borderRadius: 16 }}>
      <div style={{ position: 'absolute', left: spineX - 1, top: 56, height: 200, width: 2,
        opacity: 0.55 * rev(at + 0.3),
        background: `linear-gradient(${FILM.gold}, rgba(201,165,90,0.2))` }}></div>

      {node(56, rev(at + 0.4))}
      {chip(56, rev(at + 0.4), 'Sept 2022', 'Cherry Bekaert')}

      {items.map((label, i) => {
        const y = 104 + i * 32;
        const o = rev(at + 0.9 + i * 0.22);
        return (
          <React.Fragment key={i}>
            <div style={{ position: 'absolute', left: spineX + 6, top: y - 1, width: 15, height: 2,
              background: FILM.gold, opacity: 0.6 * o }}></div>
            <div style={{ position: 'absolute', left: spineX + 32, top: y - 12, opacity: o,
              fontFamily: FILM.sans, fontSize: 16.5, color: FILM.ink2, whiteSpace: 'nowrap' }}>{label}</div>
          </React.Fragment>
        );
      })}

      {node(256, rev(at + 2.0))}
      {chip(256, rev(at + 2.0), 'March 2023', 'DC Smith Advisory')}

      <div style={{ position: 'absolute', left: spineX + 28, top: 300, opacity: rev(at + 2.4) }}>
        <div style={{ fontFamily: FILM.mono, fontSize: 11.5, letterSpacing: '0.2em',
          textTransform: 'uppercase', color: FILM.dim, marginBottom: 15 }}>Revenue trajectory</div>
        <div style={{ display: 'flex', gap: 42, alignItems: 'flex-end' }}>
          {stat('233%', 'Y2 · actual', false, rev(at + 2.6))}
          {stat('525%', 'Y3 · projected', true, rev(at + 2.8))}
          {stat('356%', 'CAGR · proj.', true, rev(at + 3.0))}
        </div>
      </div>
    </div>
  );
}

function PrologueArpaScene() {
  return (
    <ChapterShell origin="50% 42%">
      <Appear at={0.3} x={140} y={206}>
        <div style={{ fontFamily: FILM.mono, fontSize: 22, letterSpacing: '0.3em',
          textTransform: 'uppercase', color: FILM.gold, whiteSpace: 'nowrap' }}>
          2021 — 2023<span style={{ color: FILM.dim }}>&nbsp;&nbsp;·&nbsp;&nbsp;Prologue · translation leadership</span></div>
      </Appear>

      {/* column divider */}
      <Appear at={2.0} x={958} y={292} from={0} dur={1.2}>
        <div style={{ width: 1, height: 606, background:
          `linear-gradient(${FILM.line}, rgba(40,64,106,0.12))` }}></div>
      </Appear>

      {/* LEFT · Public Service */}
      <Appear at={2.2} x={140} y={292}>
        <div style={{ fontFamily: FILM.mono, fontSize: 16, letterSpacing: '0.28em',
          textTransform: 'uppercase', color: FILM.gold }}>Public Service</div>
      </Appear>
      <Appear at={2.6} x={140} y={338} w={770}>
        <Body size={26}>March 2021 — a brand-new <Hi>$46.2M</Hi> federal relief program
          landed on the City of Tallahassee while Treasury was still writing the rules.
          She turned it into something a government could execute.</Body>
      </Appear>
      <InstrumentCard at={4.2} x={140} y={480} w={770} tag="Instrument 1 · policy"
        title="Implementation Guidance" role="The City’s key control for SLFRF compliance"
        points={['Governance — allocations, planning, risk management',
                 'Program administration & performance monitoring',
                 'Reporting & records retention']} />
      <InstrumentCard at={5.6} x={140} y={692} w={770} tag="Instrument 2 · operations"
        title="Project Planning Guide" role="An 8-sheet workbook operationalizing the framework"
        points={['Project intake + statutory eligible uses',
                 '105-row expenditure-category matrix',
                 'Subaward compliance & Davis-Bacon']} />

      {/* RIGHT · Private Sector */}
      <Appear at={6.6} x={1010} y={292}>
        <div style={{ fontFamily: FILM.mono, fontSize: 16, letterSpacing: '0.28em',
          textTransform: 'uppercase', color: FILM.gold }}>Private Sector</div>
      </Appear>
      <PrivateSectorPanel at={7.0} />
      <Appear at={11.2} x={1010} y={780} w={780}>
        <div style={{ fontFamily: FILM.serif, fontStyle: 'italic', fontSize: 27,
          lineHeight: 1.42, color: FILM.goldHi, textWrap: 'pretty' }}>
          “You never know how strong you are until being strong is your only choice.”</div>
        <div style={{ fontFamily: FILM.mono, fontSize: 14, letterSpacing: '0.18em',
          textTransform: 'uppercase', color: FILM.dim, marginTop: 14 }}>~ Bob Marley</div>
      </Appear>
    </ChapterShell>
  );
}

// ── Chapter map ─────────────────────────────────────────────────────────────
// ── Logo sting — the closing brand beat. Ported from the production sting
//    engine (sting-engine.js): strata draw → spine + nodes → converge → flash
//    → the matrix GLOBE LOADER compiles on (gold binary, top→bottom) → whirl
//    rings spiral in and lock → wordmark. All phases are driven off the film
//    timeline (localTime) so it scrubs and exports deterministically.

// The canvas globe: a glossy navy sphere skinned in gold binary that "compiles
// on" from the top pole down. Ported 1:1 from sting-engine.js renderGlobe.
const ST_TINT = { dim: [201, 165, 90], hot: [232, 196, 120], white: [255, 245, 212] };
function stIhash(a, b, c) {
  let h = (a | 0) * 73856093 ^ (b | 0) * 19349663 ^ (c | 0) * 83492791;
  h = Math.imul(h ^ h >>> 13, 1274126177);
  return (h ^ h >>> 16) >>> 0;
}
function drawStingGlobe(ctx, SIZE, absTime, rotLambda, revealFrac) {
  const CX = SIZE / 2, CY = SIZE / 2, R = SIZE * 0.32, TILT = -22, k = R / 230;
  const proj = (lon, lat) => {
    const l0 = rotLambda * Math.PI / 180, f0 = TILT * Math.PI / 180;
    const lr = lon * Math.PI / 180 - l0, fr = lat * Math.PI / 180;
    const cosc = Math.sin(f0) * Math.sin(fr) + Math.cos(f0) * Math.cos(fr) * Math.cos(lr);
    if (cosc < 0) return null;
    const x = R * Math.cos(fr) * Math.sin(lr);
    const y = R * (Math.cos(f0) * Math.sin(fr) - Math.sin(f0) * Math.cos(fr) * Math.cos(lr));
    return [CX + x, CY - y, cosc];
  };
  ctx.clearRect(0, 0, SIZE, SIZE);

  let g = ctx.createRadialGradient(CX, CY, R * 0.99, CX, CY, R * 1.17);
  g.addColorStop(0, 'rgba(232,196,120,0.28)'); g.addColorStop(0.45, 'rgba(201,165,90,0.08)'); g.addColorStop(1, 'rgba(201,165,90,0)');
  ctx.fillStyle = g; ctx.beginPath(); ctx.arc(CX, CY, R * 1.17, 0, Math.PI * 2); ctx.fill();

  g = ctx.createRadialGradient(CX - R * 0.38, CY - R * 0.45, R * 0.04, CX + R * 0.10, CY + R * 0.14, R * 1.05);
  g.addColorStop(0, '#3a4a6c'); g.addColorStop(0.10, '#22304a'); g.addColorStop(0.30, '#121a2c');
  g.addColorStop(0.62, '#070b18'); g.addColorStop(0.88, '#03050d'); g.addColorStop(1, '#010205');
  ctx.beginPath(); ctx.arc(CX, CY, R, 0, Math.PI * 2); ctx.fillStyle = g; ctx.fill();

  ctx.save(); ctx.beginPath(); ctx.arc(CX, CY, R - 1, 0, Math.PI * 2); ctx.clip();

  ctx.beginPath(); let first = true;
  for (let lon = -180; lon <= 180; lon += 4) {
    const p = proj(lon, 0); if (!p) { first = true; continue; }
    if (first) { ctx.moveTo(p[0], p[1]); first = false; } else ctx.lineTo(p[0], p[1]);
  }
  ctx.lineWidth = 1.0 * k; ctx.strokeStyle = 'rgba(232,196,120,0.20)'; ctx.stroke();

  const revealLat = 95 - 190 * revealFrac;
  if (revealFrac > 0) {
    ctx.font = `600 ${(14 * k).toFixed(1)}px "Geist Mono","JetBrains Mono",ui-monospace,monospace`;
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    const latStep = 5, baseLonStep = 5;
    const bitBucket = Math.floor(absTime * 1.6), heatBucket = Math.floor(absTime * 3.0);
    for (let lat = 78; lat >= -78; lat -= latStep) {
      if (lat < revealLat) break;
      const latRad = lat * Math.PI / 180;
      const lonStep = Math.min(40, baseLonStep / Math.max(0.18, Math.cos(latRad)));
      const rowOffset = (Math.round(lat / latStep) & 1) ? lonStep * 0.5 : 0;
      const rowFresh = clamp(1 - (lat - revealLat) / 14, 0, 1);
      for (let lon = -180 + rowOffset; lon < 180; lon += lonStep) {
        const p = proj(lon, lat); if (!p) continue;
        const dr = Math.hypot(p[0] - CX, p[1] - CY) / R; if (dr > 0.985) continue;
        const limbFade = 1 - Math.pow(dr, 3.2); if (limbFade <= 0.02) continue;
        const cellId = (Math.round(lat * 11) | 0) * 1009 + Math.round(lon * 11);
        const h = stIhash(Math.round(lat * 11), Math.round(lon * 11), bitBucket);
        const bit = (h & 1) ? '1' : '0';
        const heatH = stIhash(cellId, 17, heatBucket) / 0xffffffff;
        let rC, gC, bC, aC; const boosted = rowFresh > 0.4 && heatH > 0.6;
        if (boosted || heatH > 0.985) { [rC, gC, bC] = ST_TINT.white; aC = (0.85 + 0.10 * rowFresh) * limbFade; }
        else if (heatH > 0.90) {
          const kk = (heatH - 0.90) / 0.085;
          rC = ST_TINT.dim[0] + (ST_TINT.hot[0] - ST_TINT.dim[0]) * kk;
          gC = ST_TINT.dim[1] + (ST_TINT.hot[1] - ST_TINT.dim[1]) * kk;
          bC = ST_TINT.dim[2] + (ST_TINT.hot[2] - ST_TINT.dim[2]) * kk;
          aC = (0.55 + 0.35 * kk) * limbFade;
        } else { [rC, gC, bC] = ST_TINT.dim; aC = 0.16 + 0.42 * limbFade; }
        ctx.fillStyle = `rgba(${rC | 0},${gC | 0},${bC | 0},${aC})`;
        ctx.fillText(bit, p[0], p[1]);
      }
    }
  }

  let wash = ctx.createRadialGradient(CX - R * 0.40, CY - R * 0.55, 0, CX - R * 0.40, CY - R * 0.55, R * 0.85);
  wash.addColorStop(0, 'rgba(255,250,235,0.32)'); wash.addColorStop(0.4, 'rgba(220,235,255,0.08)'); wash.addColorStop(1, 'rgba(220,235,255,0)');
  ctx.fillStyle = wash; ctx.fillRect(0, 0, SIZE, SIZE);

  ctx.save(); ctx.translate(CX - R * 0.42, CY - R * 0.52); ctx.rotate(-0.45); ctx.scale(1.4, 0.75);
  let gl = ctx.createRadialGradient(0, 0, 0, 0, 0, R * 0.18);
  gl.addColorStop(0, 'rgba(255,255,250,0.85)'); gl.addColorStop(0.4, 'rgba(255,255,250,0.25)'); gl.addColorStop(1, 'rgba(255,255,250,0)');
  ctx.fillStyle = gl; ctx.beginPath(); ctx.arc(0, 0, R * 0.22, 0, Math.PI * 2); ctx.fill(); ctx.restore();

  let limb = ctx.createRadialGradient(CX, CY, R * 0.5, CX, CY, R);
  limb.addColorStop(0, 'rgba(0,0,0,0)'); limb.addColorStop(0.7, 'rgba(0,0,0,0.22)'); limb.addColorStop(0.92, 'rgba(0,0,0,0.62)'); limb.addColorStop(1, 'rgba(0,0,0,0.92)');
  ctx.fillStyle = limb; ctx.fillRect(0, 0, SIZE, SIZE);
  ctx.restore();

  ctx.beginPath(); ctx.arc(CX, CY, R, 0, Math.PI * 2); ctx.lineWidth = 1.6 * k; ctx.strokeStyle = 'rgba(232,196,120,0.55)'; ctx.stroke();
  ctx.beginPath(); ctx.arc(CX, CY, R + 3 * k, 0, Math.PI * 2); ctx.lineWidth = 0.6 * k; ctx.strokeStyle = 'rgba(201,165,90,0.22)'; ctx.stroke();
}

// Globe loader: scales in + compiles on over P7 (localTime `at`..`at+dur`), then
// holds and rotates. Draws on every frame from the timeline value `t`.
function GlobeLoader({ t, at = 4.35, dur = 0.85, size = 440 }) {
  const ref = React.useRef(null);
  const stateRef = React.useRef(null);
  const dpr = Math.min(window.devicePixelRatio || 1, 2);

  const p7 = clamp((t - at) / dur, 0, 1);
  const opacity = Easing.easeOutCubic(clamp(p7 * 1.4, 0, 1));
  const scale = 0.1 + 0.9 * Easing.easeOutCubic(p7);
  const reveal = Easing.easeInOutCubic(clamp((p7 - 0.15) / 0.85, 0, 1));
  const rot = Math.max(0, t - at) * 14;

  React.useLayoutEffect(() => {
    const canvas = ref.current; if (!canvas) return;
    let st = stateRef.current;
    if (!st) {
      canvas.width = size * dpr; canvas.height = size * dpr;
      const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr);
      st = stateRef.current = { ctx };
    }
    if (opacity <= 0.002) { st.ctx.clearRect(0, 0, size, size); return; }
    drawStingGlobe(st.ctx, size, t, rot, reveal);
  });

  return (
    <div style={{ position: 'absolute', left: 960, top: 372, width: size, height: size,
      transform: `translate(-50%,-50%) scale(${scale.toFixed(3)})`, transformOrigin: 'center',
      opacity, pointerEvents: 'none' }}>
      <canvas ref={ref} style={{ width: size, height: size, display: 'block' }}></canvas>
    </div>
  );
}

// Whirl rings that spiral in (scale 2→1) and lock over P8. Ring spins derive
// from localTime so they're deterministic.
function stArc(cx, cy, r, a, b) {
  const p2c = (deg) => { const rad = (deg - 90) * Math.PI / 180; return [cx + r * Math.cos(rad), cy + r * Math.sin(rad)]; };
  const [sx, sy] = p2c(b), [ex, ey] = p2c(a);
  return `M ${sx} ${sy} A ${r} ${r} 0 ${b - a <= 180 ? 0 : 1} 0 ${ex} ${ey}`;
}
function StingWhirl({ t, at = 5.3, dur = 0.8, size = 440 }) {
  const p8 = clamp((t - at) / dur, 0, 1);
  if (p8 <= 0.001) return null;
  const op = Easing.easeOutCubic(p8);
  const scale = 2.0 - 1.0 * Easing.easeOutQuart(p8);
  const w = (rate) => (t * 360 / rate) % 360;
  const ticks = [];
  for (let i = 0; i < 72; i++) {
    const major = i % 6 === 0;
    ticks.push(<line key={i} x1="50" y1={50 - 47} x2="50" y2={50 - 47 + (major ? 1.8 : 0.9)}
      stroke="#c9a55a" strokeOpacity={major ? 0.7 : 0.25} strokeWidth="0.15" transform={`rotate(${i * 5} 50 50)`}></line>);
  }
  const streakSet = (base, color, o, width, len) => [0, 90, 180, 270].map((a) => (
    <path key={a} d={stArc(50, 50, base, a, a + len)} stroke={color} strokeOpacity={o}
      strokeWidth={width} fill="none" strokeLinecap="round" filter="url(#stWhirlSoft)"></path>
  ));
  const cardinals = [0, 90, 180, 270].map((a) => {
    const rad = a * Math.PI / 180, x = 50 + Math.cos(rad) * 48.8, y = 50 + Math.sin(rad) * 48.8;
    return (<g key={a} transform={`translate(${x} ${y})`}>
      <circle r="0.55" fill="#e8c478" filter="url(#stWhirlGlow)"></circle>
      <circle r="0.25" fill="#fff5d6"></circle></g>);
  });
  return (
    <div style={{ position: 'absolute', left: 960, top: 372, width: size, height: size,
      transform: `translate(-50%,-50%) scale(${scale.toFixed(3)})`, transformOrigin: 'center',
      opacity: op, pointerEvents: 'none' }}>
      <svg viewBox="0 0 100 100" width={size} height={size} style={{ overflow: 'visible', display: 'block' }}>
        <defs>
          <radialGradient id="stGoldFade" cx="50%" cy="50%" r="50%">
            <stop offset="0%" stopColor="#e8c479" stopOpacity="0"></stop>
            <stop offset="72%" stopColor="#d4a851" stopOpacity="0.5"></stop>
            <stop offset="100%" stopColor="#d4a851" stopOpacity="0"></stop>
          </radialGradient>
          <filter id="stWhirlSoft" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="0.18"></feGaussianBlur></filter>
          <filter id="stWhirlGlow" x="-200%" y="-200%" width="500%" height="500%"><feGaussianBlur stdDeviation="0.5"></feGaussianBlur></filter>
        </defs>
        <g transform={`rotate(${(-w(120)).toFixed(2)} 50 50)`}>{ticks}</g>
        <g transform={`rotate(${w(52).toFixed(2)} 50 50)`}>
          <circle cx="50" cy="50" r="48" fill="none" stroke="#c9a55a" strokeOpacity="0.35" strokeWidth="0.18" strokeDasharray="0.6 2.4"></circle>
        </g>
        <g transform={`rotate(${(-w(36)).toFixed(2)} 50 50)`}>
          <circle cx="50" cy="50" r="44.5" fill="none" stroke="#e8c478" strokeOpacity="0.45" strokeWidth="0.12" strokeDasharray="3 1.6 0.4 1.6"></circle>
          <circle cx="50" cy="50" r="43.6" fill="none" stroke="#a08545" strokeOpacity="0.7" strokeWidth="0.08"></circle>
        </g>
        <g transform={`rotate(${w(14).toFixed(2)} 50 50)`}>
          <circle cx="50" cy="50" r="40.8" fill="none" stroke="#c9a55a" strokeOpacity="0.55" strokeWidth="0.12" strokeDasharray="0.3 1.2 1.8 0.6"></circle>
        </g>
        <g transform={`rotate(${w(9).toFixed(2)} 50 50)`}>{streakSet(41.5, 'url(#stGoldFade)', 0.85, 0.9, 38)}</g>
        <g transform={`rotate(${w(13).toFixed(2)} 50 50)`}>{streakSet(39.5, '#e8c478', 0.40, 0.55, 24)}</g>
        <g transform={`rotate(${w(17).toFixed(2)} 50 50)`}>{streakSet(46.0, '#c9a55a', 0.30, 0.35, 18)}</g>
        <g>{cardinals}</g>
      </svg>
    </div>
  );
}

function StingScene() {
  const { localTime: t } = useSprite();
  const E = Easing, cl = clamp;
  const scan = cl((t - 0.2) / 0.9, 0, 1);
  const barW = (i) => 80 * E.easeOutCubic(cl((t - 0.2 - i * 0.14) / 0.6, 0, 1));
  const barFade = 1 - E.easeInCubic(cl((t - 3.0) / 0.6, 0, 1));
  const spineDraw = E.easeInOutCubic(cl((t - 1.1) / 0.8, 0, 1));
  const spineFade = 1 - 0.85 * E.easeInOutCubic(cl((t - 3.6) / 0.8, 0, 1));
  const nodePop = (i) => E.easeOutBack(cl((t - 1.7 - i * 0.08) / 0.5, 0, 1));
  const converge = E.easeInOutCubic(cl((t - 3.6) / 0.8, 0, 1));
  const nodeHide = 1 - E.easeInCubic(cl((t - 4.4) / 0.3, 0, 1));
  const flash = cl((t - 4.4) / 0.25, 0, 1) * (1 - cl((t - 4.65) / 0.5, 0, 1));
  const word = E.easeOutCubic(cl((t - 6.2) / 0.9, 0, 1));
  const uline = E.easeInOutCubic(cl((t - 6.5) / 0.9, 0, 1));

  const navy = ['#8ab4e0', '#5a8bc2', '#3b6398', '#28406a', '#1a2844'];
  const nodes = [{ cy: 0, r: 3.8 }, { cy: 10, r: 2.8 }, { cy: 28, r: 2.2 },
                 { cy: 46, r: 2.2 }, { cy: 64, r: 2.2 }, { cy: 82, r: 2.8 }];

  return (
    <ChapterShell grow={0.01} origin="50% 42%">
      {/* P7 · the globe loader compiles on where the flash lands */}
      <GlobeLoader t={t} at={4.35} dur={0.85} size={440} />

      {/* P1–P6 · strata → spine → nodes → converge → flash (fades out into the globe) */}
      <div style={{ position: 'absolute', left: 960, top: 372, transform: 'translate(-50%,-50%)' }}>
        <svg width="420" height="420" viewBox="0 0 160 160" fill="none" style={{ overflow: 'visible' }}>
          <defs>
            <linearGradient id="stSpine" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0%" stopColor="#fff5d4"></stop><stop offset="35%" stopColor="#e8c478"></stop>
              <stop offset="65%" stopColor="#c9a55a"></stop><stop offset="100%" stopColor="#a08545"></stop>
            </linearGradient>
            <radialGradient id="stNode" cx="35%" cy="35%" r="65%">
              <stop offset="0%" stopColor="#fff5d4"></stop><stop offset="45%" stopColor="#e8c478"></stop><stop offset="100%" stopColor="#a08545"></stop>
            </radialGradient>
            <radialGradient id="stFlash" cx="50%" cy="50%" r="50%">
              <stop offset="0%" stopColor="#fff5d4" stopOpacity="1"></stop><stop offset="45%" stopColor="#e8c478" stopOpacity="0.6"></stop><stop offset="100%" stopColor="#c9a55a" stopOpacity="0"></stop>
            </radialGradient>
            <filter id="stGlow" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="1.3"></feGaussianBlur></filter>
          </defs>
          <g transform="translate(40,35)">
            {navy.map((c, i) => (
              <rect key={i} x="0" y={8.5 + i * 18} width={barW(i)} height="3" rx="1.5" fill={c} opacity={barFade}></rect>
            ))}
            <line x1="-10" y1="10" x2="-10" y2="82" stroke="#3b6398" strokeWidth="1.2" opacity={0.6 * barFade}></line>
            <line x1="90" y1="10" x2="90" y2="82" stroke="#3b6398" strokeWidth="1.2" opacity={0.6 * barFade}></line>
            <line x1={-10 + 100 * scan} y1="4" x2={-10 + 100 * scan} y2="88" stroke="#fff5d4" strokeWidth="0.8"
              opacity={scan > 0 && scan < 1 ? 0.85 : 0} filter="url(#stGlow)"></line>
            <line x1="40" y1="0" x2="40" y2="90" stroke="url(#stSpine)" strokeWidth="2.4" strokeLinecap="round"
              pathLength="100" strokeDasharray="100" strokeDashoffset={100 - 100 * spineDraw}
              opacity={spineFade} filter="url(#stGlow)"></line>
            {nodes.map((n, i) => {
              const cy = n.cy + (45 - n.cy) * converge;
              const o = cl(nodePop(i) * nodeHide, 0, 1);
              if (o <= 0.001) return null;
              return <circle key={i} cx="40" cy={cy} r={n.r * (0.6 + 0.4 * Math.min(nodePop(i), 1))}
                fill="url(#stNode)" opacity={o} filter="url(#stGlow)"></circle>;
            })}
            {flash > 0.001 ? <circle cx="40" cy="45" r={6 + 30 * flash} fill="url(#stFlash)" opacity={flash}></circle> : null}
          </g>
        </svg>
      </div>

      {/* P8 · whirl rings spiral in and lock around the globe */}
      <StingWhirl t={t} at={5.3} dur={0.8} size={440} />

      {/* P9 · wordmark */}
      <div style={{ position: 'absolute', left: 960, top: 648, transform: 'translate(-50%,-50%)',
        opacity: word, textAlign: 'center' }}>
        <div style={{ fontFamily: FILM.sans, fontSize: 46, fontWeight: 600, letterSpacing: '-0.02em', color: FILM.ink }}>
          GrytLabs<span style={{ color: FILM.mut, fontWeight: 400, marginLeft: 10 }}>Dynamics</span></div>
        <div style={{ height: 1, margin: '18px auto 0', width: `${uline * 320}px`,
          background: 'linear-gradient(90deg, transparent, #e8c478, transparent)',
          boxShadow: '0 0 8px rgba(232,196,120,0.6)' }}></div>
      </div>
    </ChapterShell>
  );
}

const FILM_DURATION = 240;
const CHAPTERS = [
  { start: 0,   end: 12,  C: TitleScene,           name: 'title' },
  { start: 12,  end: 31,  C: PrologueFounderScene, name: 'prologue · founder' },
  { start: 31,  end: 50,  C: PrologueArpaScene,    name: 'prologue · arpa' },
  { start: 50,  end: 76,  C: DisruptionScene,      name: 'the disruption' },
  { start: 76,  end: 104, C: DriftScene,           name: 'the drift' },
  { start: 104, end: 128, C: WeaveScene,           name: 'the weaving' },
  { start: 128, end: 150, C: CatalystScene,        name: 'the catalyst' },
  { start: 150, end: 170, C: TransitionScene,      name: 'the transition' },
  { start: 170, end: 190, C: SpecDemoScene,        name: 'the spec-faithful demo' },
  { start: 190, end: 213, C: InstituteScene,       name: 'the institute' },
  { start: 213, end: 229, C: CodaScene,            name: 'coda' },
  { start: 229, end: 240, C: StingScene,           name: 'sting' },
];
const PHASE_MARKS = [
  { t: 12,  label: 'PROLOGUE' },
  { t: 50,  label: 'ORIGIN' },
  { t: 76,  label: 'EXPLORATION' },
  { t: 104, label: 'CONVERGENCE' },
  { t: 150, label: 'TRANSITION' },
  { t: 190, label: 'DYNAMICS' },
];

Object.assign(window, { CHAPTERS, FILM_DURATION, PHASE_MARKS });
