// crovi-scene4.jsx — picks up from scene 3.

// Identity fallback for the palette transformer; bare `T` is stripped by
// Babel's TS preset because it looks like a type parameter.
const tt = (typeof window !== 'undefined' && typeof window.T === 'function')
  ? window.T
  : (s) => s;

//
// Continuity: scene 3 ends with the audit card centered at screen (1280, 540)
// at 820×540 with all checks done + summary. Scene 4 inherits THAT EXACT card
// and collapses it IN PLACE — no horizontal translation as it shrinks to a
// 240×80 pill. To preserve "in place" continuity, the entire scene-4 pipeline
// is anchored at screen X=1280 (instead of the usual stage center 960). Every
// focused pill therefore appears at X=1280; the camera pans rightward to
// follow the pipeline as quote → bundle → file ready come into view.
//
// Camera-follow horizontal pipeline:
//  A (0.0–1.4s): Audit card collapses in place → "Audit agent ✓" pill at X=1280
//  B (1.4–3.4s): Camera pans to "Negotiating a quote" (also lands at X=1280)
//  C (3.4–6.0s): Camera pans to "Validate bundle"
//  D (6.0–9.5s): LONG transit segment to "File ready"
//  E (9.5–11s):  "File ready" notification toast slides in with download

const COLLAPSE_AT = 1.4;

// Screen anchor: where focused pills sit horizontally. Set to the center of
// scene 3's audit card so the 3→4 seam aligns and the audit pill stays put.
const SCREEN_ANCHOR_X = 1920 / 2 + 320;   // 1280
const TRANSIT_START = 6.5;        // bundle done sooner — transit begins
const TRANSIT_END   = 8.5;        // accelerated transit (was 12.0)
const TOAST_AT      = 8.5;
const QUOTE_CARD_AT = 3.6;        // quote card materializes — appears with the pill
const BUNDLE_CLICK  = 5.6;        // ~2s of card hold (down from 3s)
const BUNDLE_DONE   = 6.4;        // line + check completes faster

const STEPS = [
  { id: 'audit',    label: 'Audit agent',
    appearAt: 0.0,  spinAt: null,  doneAt: 0.0,  thread: null  },
  { id: 'quote',    label: 'Negotiating a quote',
    appearAt: 1.5,  spinAt: 1.7,  doneAt: 2.7,  thread: 'fast' },
  { id: 'bundle',   label: 'Validate bundle',
    appearAt: 3.3,  spinAt: BUNDLE_CLICK, doneAt: BUNDLE_DONE,
    needsClick: true, clickAt: BUNDLE_CLICK, thread: null },
  { id: 'contract', label: 'File ready',
    appearAt: TOAST_AT,  spinAt: null, doneAt: TOAST_AT,
    thread: null, isDoc: true },
];

// Status words that rotate during the transit segment.
const TRANSIT_WORDS = ['Orchestrating', 'Logging', 'Assessing', 'Validating', 'Communicating'];

function Scene4Quote() {
  const { localTime: t } = useSprite();
  const cy = STAGE_H / 2;

  // Pill geometry — bigger gap to FEEL like advancing.
  const pillH = 80;
  const pillGap = (id) => ({
    'audit-quote':    260,    // small gap
    'quote-bundle':   320,    // medium
    'bundle-contract': 1100,  // LONG — the process transit
  }[id] || 280);

  const pillW = (id) => ({
    audit: 240,
    quote: 320,
    bundle: 280,
    contract: 320,
  }[id] || 260);

  // Compute X positions left-to-right.
  const widths = STEPS.map(s => pillW(s.id));
  const xs = [];
  {
    let acc = 0;
    for (let i = 0; i < STEPS.length; i++) {
      xs.push(acc);
      if (i < STEPS.length - 1) {
        acc += widths[i] + pillGap(STEPS[i].id + '-' + STEPS[i+1].id);
      }
    }
  }
  const totalW = xs[xs.length - 1] + widths[widths.length - 1];

  // Center the WHOLE strip at stage start (so audit is left-of-center, contract far right).
  // The camera will translate this strip leftward as we advance.
  const stripStartX = 200;     // left padding before audit pill on the world

  // ── Camera: which step is the focus, and where does it sit on the stage ──
  // The focus point of each step is its horizontal center.
  // We want the focus at stage center horizontally (or for transit, slowly traveling).
  const focusXForStep = (i) => stripStartX + xs[i] + widths[i] / 2;

  // Decide camera focus by time.
  // Targets:
  //   t < 1.4:    audit
  //   1.4–3.4:    quote
  //   3.4–TRANSIT_START:  bundle (slow hold for quote card + click)
  //   TRANSIT_START–TRANSIT_END: travel along transit line
  //   TRANSIT_END+: contract
  let cameraX;
  let cameraScale = 1;
  if (t < 1.4) {
    cameraX = focusXForStep(0);
  } else if (t < 3.0) {
    const k = Easing.easeInOutCubic(clamp((t - 1.4) / 0.9, 0, 1));
    cameraX = focusXForStep(0) + (focusXForStep(1) - focusXForStep(0)) * k;
  } else if (t < TRANSIT_START) {
    // Pan from quote → bundle, AND zoom in.
    const k = Easing.easeInOutCubic(clamp((t - 3.0) / 0.7, 0, 1));
    cameraX = focusXForStep(1) + (focusXForStep(2) - focusXForStep(1)) * k;
    cameraScale = 1 + 0.20 * k;             // zoom in to 1.20×
  } else if (t < TRANSIT_END) {
    // Stay zoomed; pan along transit at accelerating ease (slow in, fast out).
    const raw = clamp((t - TRANSIT_START) / (TRANSIT_END - TRANSIT_START), 0, 1);
    const k = raw * raw;                     // accelerating ease (easeInQuad)
    cameraX = focusXForStep(2) + (focusXForStep(3) - focusXForStep(2)) * k;
    cameraScale = 1.20;
  } else {
    cameraX = focusXForStep(3);
    // ease zoom back to 1 over 0.4s
    const k = Easing.easeOutCubic(clamp((t - TRANSIT_END) / 0.4, 0, 1));
    cameraScale = 1.20 - 0.20 * k;
  }

  // World offset: shift world so cameraX → SCREEN_ANCHOR_X (not stage center).
  // The 320px rightward offset keeps focused pills aligned with where scene 3's
  // audit card sat, so the 3→4 seam reads as "the same card collapsing in place"
  // rather than a translation across the stage.
  const worldDx = SCREEN_ANCHOR_X - cameraX;

  // Step phase
  const stepState = (s) => {
    const visible = t >= s.appearAt - 0.05;
    const enterP  = Easing.easeOutCubic(clamp((t - s.appearAt) / 0.45, 0, 1));
    let phase = 'queued';
    if (s.spinAt == null && s.doneAt != null && t >= s.doneAt) phase = 'done';
    else if (s.spinAt != null && t >= s.spinAt) {
      phase = (s.doneAt != null && t >= s.doneAt) ? 'done' : 'running';
    }
    if (s.needsClick && t < (s.clickAt ?? s.spinAt)) phase = 'awaiting';
    return { visible, enterP, phase };
  };

  return (
    <div style={{ position: 'absolute', inset: 0, background: C.ink, overflow: 'hidden', fontFamily: C.body }}>

      {/* faint bg wordmark — fixed in viewport */}
      <div style={{
        position: 'absolute', left: 0, right: 0, bottom: -160,
        textAlign: 'center', fontFamily: C.display, fontStyle: 'italic',
        fontSize: 480, color: C.ink2, opacity: 0.30,
        letterSpacing: '-0.04em', lineHeight: 1, pointerEvents: 'none',
      }}>crovi</div>

      {/* WORLD — translated by camera */}
      <div style={{
        position: 'absolute', inset: 0,
        transform: `translateX(${worldDx}px) scale(${cameraScale})`,
        transformOrigin: `${cameraX}px ${cy}px`,
        transition: 'transform 80ms linear',
      }}>

        {/* connectors / process line between pills */}
        <svg style={{ position: 'absolute', left: 0, top: 0, width: stripStartX + totalW + 400, height: STAGE_H, pointerEvents: 'none', overflow: 'visible' }}>
          {STEPS.slice(0, -1).map((s, i) => {
            const next = STEPS[i + 1];
            const x1 = stripStartX + xs[i] + widths[i];
            const isTransit = (s.id === 'bundle' && next.id === 'contract');
            // Transit dash terminates at the completion check position, not at the file pill.
            const x2 = isTransit
              ? (stripStartX + xs[i + 1] - 90)
              : (stripStartX + xs[i + 1]);
            const y = cy;
            // standard connector draw
            if (!isTransit) {
              const drawP = Easing.easeOutCubic(clamp((t - next.appearAt + 0.2) / 0.4, 0, 1));
              if (drawP <= 0) return null;
              return (
                <line key={i}
                  x1={x1} y1={y}
                  x2={x1 + (x2 - x1) * drawP} y2={y}
                  stroke={C.brandHi} strokeWidth={1.4}
                  strokeDasharray="5 5" opacity={0.55}/>
              );
            }
            // transit line: solid base + traveling glow
            return (
              <g key={i}>
                <line x1={x1} y1={y} x2={x2} y2={y}
                  stroke={C.ink3} strokeWidth={1.4}/>
                <line x1={x1} y1={y} x2={x2} y2={y}
                  stroke={C.brandHi} strokeWidth={1.4}
                  strokeDasharray="6 6" opacity={0.45}/>
                {/* travelling pulse */}
                {t >= TRANSIT_START && t <= TRANSIT_END && (() => {
                  const k = clamp((t - TRANSIT_START) / (TRANSIT_END - TRANSIT_START), 0, 1);
                  const px = x1 + (x2 - x1) * Easing.easeInOutCubic(k);
                  return (
                    <g>
                      <circle cx={px} cy={y} r={6}
                        fill={C.brandHi}
                        style={{ filter: `drop-shadow(0 0 12px ${C.brandHi})` }}/>
                      <circle cx={px} cy={y} r={14}
                        fill="none" stroke={C.brandHi} strokeWidth={1} opacity={0.5}/>
                    </g>
                  );
                })()}
              </g>
            );
          })}
        </svg>

        {/* completion check — lands at end of transit, just before the file toast */}
        <CompletionCheck
          t={t}
          appearAt={TRANSIT_END - 0.6}
          x={stripStartX + xs[3] - 90}
          y={cy}
        />

        {/* step pills */}
        {STEPS.map((s, i) => {
          const { visible, enterP, phase } = stepState(s);
          if (!visible) return null;
          if (s.id === 'audit' && t < COLLAPSE_AT) return null;
          if (s.isDoc) return null;  // contract rendered as toast separately
          const w = widths[i];
          const x = stripStartX + xs[i];
          const y = cy - pillH / 2;
          return (
            <React.Fragment key={s.id}>
              <StepPill
                x={x} y={y} w={w} h={pillH}
                label={s.label}
                phase={phase}
                enterP={enterP}
                t={t}
              />
              {s.thread && phase === 'running' && (
                <CommThread
                  x={x} y={y + pillH + 14} w={w}
                  t={t}
                  startT={s.spinAt}
                  dur={(s.doneAt ?? (s.spinAt + 1.5)) - s.spinAt}
                  kind={s.thread}
                />
              )}
              {/* quote card: sits below bundle pill while awaiting */}
              {s.id === 'bundle' && (
                <QuoteCard
                  t={t}
                  appearAt={QUOTE_CARD_AT}
                  clickAt={BUNDLE_CLICK}
                  doneAt={BUNDLE_DONE}
                  pillX={x} pillY={y} pillW={w} pillH={pillH}
                />
              )}
            </React.Fragment>
          );
        })}

        {/* fake cursor on bundle */}
        {(() => {
          const i = STEPS.findIndex(s => s.id === 'bundle');
          const s = STEPS[i];
          const cx = stripStartX + xs[i] + widths[i] / 2;
          return (
            <FakeCursor t={t}
              startAt={s.clickAt - 0.9}
              clickAt={s.clickAt}
              targetX={cx}
              targetY={cy}
              fromX={cx + 240}
              fromY={cy + 200}
            />
          );
        })()}

        {/* file-ready toast notification — slides in from right at TOAST_AT */}
        <FileReadyToast
          t={t} startAt={TOAST_AT}
          x={stripStartX + xs[3]}
          y={cy - pillH / 2}
        />
      </div>

      {/* Faded overlay of scene-3 leaves — keeps the LEFT side of the stage
          from going empty at the 3→4 seam. Fades out over 0.4..1.4s so the
          pipeline has the stage to itself by the time the camera starts panning. */}
      <Scene3LeavesOverlay t={t} fadeStart={0.4} fadeEnd={1.4}/>

      {/* audit card → pill morph — rendered OUTSIDE the world so it can use
          screen-absolute coords. It starts at scene 3's exact card geometry
          (center 1280, 540, 820×540 with full content) and collapses IN PLACE
          to the pill (center 1280, 540, 240×80) where the actual StepPill
          (rendered inside the world, also anchored to screen X=1280) takes
          over. No horizontal translation during the morph. */}
      <AuditMorph
        t={t} endAt={COLLAPSE_AT}
        anchorCx={SCREEN_ANCHOR_X} anchorCy={cy}
        pillW={widths[0]} pillH={pillH}
      />

    </div>
  );
}

// ── Step pill ──────────────────────────────────────────────────────────────
function StepPill({ x, y, w, h, label, phase, enterP, t }) {
  const running = phase === 'running';
  const done    = phase === 'done';
  const awaiting= phase === 'awaiting';
  const pulse = awaiting ? (Math.sin(t * 4.0) * 0.5 + 0.5) : 0;

  const border = running ? C.brandHi
              : done    ? C.brandHi
              : awaiting ? tt(`oklch(${0.62 + 0.10 * pulse} 0.10 200)`)
              : C.ink3;
  const bg = running ? tt('oklch(0.18 0.025 195 / 0.45)')
           : done    ? tt('oklch(0.13 0.013 250 / 0.95)')
           : awaiting ? tt(`oklch(0.18 0.04 200 / ${0.30 + 0.20 * pulse})`)
           : tt('oklch(0.10 0.01 250 / 0.85)');
  const glow = running ? `0 0 32px ${C.brandGlow}`
             : awaiting ? tt(`0 0 ${10 + 14 * pulse}px oklch(0.78 0.13 200 / ${0.25 + 0.30 * pulse})`)
             : 'none';

  return (
    <div style={{
      position: 'absolute', left: x, top: y, width: w, height: h,
      opacity: enterP,
      transform: `translateY(${(1 - enterP) * 10}px) scale(${0.96 + 0.04 * enterP})`,
      background: bg,
      border: `1px solid ${border}`,
      borderRadius: 14,
      padding: '0 20px',
      boxSizing: 'border-box',
      display: 'flex', alignItems: 'center', gap: 14,
      boxShadow: glow,
      backdropFilter: 'blur(8px)',
      transition: 'background 200ms, border-color 200ms, box-shadow 200ms',
    }}>
      <div style={{
        width: 36, height: 36, borderRadius: '50%',
        background: done ? tt('oklch(0.74 0.14 195 / 0.20)')
                  : awaiting ? tt(`oklch(0.30 0.10 200 / ${0.40 + 0.30 * pulse})`)
                  : tt('oklch(0.10 0.01 250)'),
        border: `1px solid ${done || running ? C.brandHi : awaiting ? C.brandHi : C.ink3}`,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        flexShrink: 0,
      }}>
        {done    ? Icons.check(C.brandHi, 16)
        : running ? <Spinner4 t={t} size={16}/>
        : awaiting? Icons.alert(C.brandHi, 16)
        : <span style={{ width: 6, height: 6, borderRadius: '50%', background: C.inkText3 }}/>}
      </div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{
          fontFamily: C.display, fontSize: 22,
          color: (done || running || awaiting) ? C.inkText : C.inkText2,
          letterSpacing: '-0.005em', lineHeight: 1.1,
          whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
        }}>{label}</div>
        <div style={{ marginTop: 5 }}>
          <Mono size={10} color={done ? C.brandHi : (running ? C.brandHi : awaiting ? C.brandHi : C.inkText3)}>
            {done ? 'DONE' : running ? 'RUNNING' : awaiting ? 'AWAITING APPROVAL' : 'QUEUED'}
          </Mono>
        </div>
      </div>
    </div>
  );
}

function Spinner4({ t, size = 14 }) {
  const angle = (t * 360) % 360;
  return (
    <svg width={size} height={size} viewBox="0 0 14 14" style={{ transform: `rotate(${angle}deg)` }}>
      <circle cx="7" cy="7" r="5" stroke={C.ink3} strokeWidth="1.5" fill="none"/>
      <path d="M 7 2 A 5 5 0 0 1 12 7" stroke={C.brandHi} strokeWidth="1.5" fill="none" strokeLinecap="round"/>
    </svg>
  );
}

// ── Fake cursor ────────────────────────────────────────────────────────────
function FakeCursor({ t, startAt, clickAt, targetX, targetY, fromX, fromY }) {
  if (t < startAt - 0.05) return null;
  const moveDur = clickAt - startAt;
  const moveP = Easing.easeOutCubic(clamp((t - startAt) / moveDur, 0, 1));
  const x = fromX + (targetX - fromX) * moveP;
  const y = fromY + (targetY - fromY) * moveP;
  const clickP = clamp((t - clickAt) / 0.18, 0, 1);
  const fadeOut = clamp((t - (clickAt + 0.55)) / 0.4, 0, 1);
  const opacity = (1 - fadeOut) * Easing.easeOutCubic(clamp((t - startAt) / 0.18, 0, 1));
  if (opacity <= 0) return null;
  return (
    <div style={{
      position: 'absolute', left: x, top: y,
      transform: 'translate(-4px,-4px)', pointerEvents: 'none', opacity,
    }}>
      {clickP > 0 && clickP < 1 && (
        <div style={{
          position: 'absolute', left: -2, top: -2,
          width: 28, height: 28, borderRadius: '50%',
          border: `1.5px solid ${C.brandHi}`,
          opacity: 1 - clickP,
          transform: `scale(${0.4 + clickP * 1.4})`,
        }}/>
      )}
      <svg width="22" height="22" viewBox="0 0 22 22" style={{
        transform: `scale(${1 - clickP * 0.15})`,
        filter: `drop-shadow(0 2px 4px rgba(0,0,0,0.6))`,
      }}>
        <path d="M3 2 L3 16 L7 12 L10 18 L13 17 L10 11 L16 11 Z"
          fill={C.inkText} stroke={C.ink} strokeWidth="0.8" strokeLinejoin="round"/>
      </svg>
    </div>
  );
}

// ── Comm thread under a running pill ──────────────────────────────────────
function CommThread({ x, y, w, t, startT, dur, kind = 'fast' }) {
  const local = t - startT;
  if (local < -0.1) return null;
  const phaseT = clamp(local / dur, 0, 1);
  const SCHEDULES = {
    fast: [
      { side: 'out', at: 0.02, icon: 'phone' },
      { side: 'in',  at: 0.16, icon: 'mail'  },
      { side: 'out', at: 0.30, icon: 'mail'  },
      { side: 'in',  at: 0.44, icon: 'phone' },
      { side: 'out', at: 0.58, icon: 'phone' },
      { side: 'in',  at: 0.72, icon: 'mail'  },
      { side: 'out', at: 0.86, icon: 'mail'  },
    ],
  };
  const BUBBLES = SCHEDULES[kind] || SCHEDULES.fast;
  return (
    <div style={{
      position: 'absolute', left: x, top: y, width: w, height: 30,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      gap: 5, pointerEvents: 'none',
    }}>
      {BUBBLES.map((b, i) => {
        const bp = Easing.easeOutCubic(clamp((phaseT - b.at) / 0.12, 0, 1));
        if (bp <= 0) return null;
        const accent = b.side === 'out' || b.icon === 'phone';
        return (
          <div key={i} style={{
            opacity: bp,
            transform: `translateY(${(1 - bp) * 6}px) scale(${0.7 + 0.3 * bp})`,
            width: 30, height: 24, borderRadius: 6,
            background: accent ? tt('oklch(0.74 0.14 195 / 0.22)') : tt('oklch(0.20 0.018 250 / 0.95)'),
            border: tt(`1px solid ${accent ? C.brandHi : 'oklch(0.42 0.04 250)'}`),
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            boxShadow: accent ? tt(`0 0 6px oklch(0.74 0.14 195 / 0.35)`) : 'none',
            flexShrink: 0,
          }}>{Icons[b.icon](accent ? C.brandHi : tt('oklch(0.62 0.02 250)'), 12)}</div>
        );
      })}
    </div>
  );
}

// ── Audit card → pill morph ────────────────────────────────────────────────
// Starts as scene 3's exact final card (center at SCREEN_ANCHOR_X, 820×540,
// header + 3 done checks + summary). Collapses IN PLACE — no translation —
// to the audit pill (240×80, same center). Rendered outside the world
// transform so it can use screen-absolute coords directly.
function AuditMorph({ t, endAt, anchorCx, anchorCy, pillW, pillH }) {
  if (t > endAt + 0.05) return null;

  // Scene 3 card size (must match crovi-scene3.jsx auditW/auditH).
  const startW  = 820;
  const startH  = 540;

  // Hold scene-3 card briefly, then collapse over the remaining window.
  const HOLD = 0.3;
  const mp = Easing.easeInOutCubic(clamp((t - HOLD) / (endAt - HOLD), 0, 1));

  // Geometry interpolation: center stays put; only width/height shrink.
  const cx = anchorCx;
  const cy = anchorCy;
  const w  = startW + (pillW - startW) * mp;
  const h  = startH + (pillH - startH) * mp;

  // Content fades out as the card shrinks
  const innerOp   = 1 - clamp(mp / 0.55, 0, 1);   // checks + divider + summary
  const eyebrowMp = clamp((mp - 0.45) / 0.25, 0, 1);  // VALIDATING FIT → DONE
  const iconMp    = clamp((mp - 0.55) / 0.30, 0, 1);  // sparkle → check

  // Padding shrinks as we morph
  const padX = 32 + (20 - 32) * mp;
  const padTop = 24 + (0 - 24) * mp;
  const padBottom = 28 + (0 - 28) * mp;

  // Header sizes
  const iconSize = 44 + (36 - 44) * mp;
  const iconRadius = 12 + (4 - 12) * mp;
  const titleSize = 32 + (22 - 32) * mp;

  return (
    <div style={{
      position: 'absolute',
      left: cx - w / 2, top: cy - h / 2,
      width: w, height: h,
      background: tt('oklch(0.13 0.013 250 / 0.96)'),
      border: `1px solid ${C.brandHi}`,
      borderRadius: 18 + (14 - 18) * mp,
      boxShadow: `0 0 ${40 - 16 * mp}px ${C.brandGlow}, 0 ${16 - 16 * mp}px ${48 - 48 * mp}px rgba(0,0,0,${0.45 - 0.45 * mp})`,
      backdropFilter: 'blur(10px)',
      padding: `${padTop}px ${padX}px ${padBottom}px`,
      boxSizing: 'border-box',
      overflow: 'hidden',
      display: 'flex',
      flexDirection: 'column',
      // When the inner content has faded, justify-content: center vertically
      // centers the lone header (matching the pill layout). While inner content
      // is visible, the flex-grow on the checks list absorbs all free space so
      // justify-content has no effect.
      justifyContent: 'center',
    }}>
      {/* header row — same as scene 3's: sparkle box + "Audit agent" + status mono + live dot */}
      <div style={{
        display: 'flex', alignItems: 'center',
        gap: 14,
        flexShrink: 0,
        width: '100%',
        minWidth: 0,
      }}>
        <div style={{
          width: iconSize, height: iconSize, borderRadius: iconRadius,
          background: tt('oklch(0.74 0.14 195 / 0.18)'),
          border: `1px solid ${C.brandHi}`,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          flexShrink: 0,
        }}>
          {iconMp < 0.5 ? <Sparkle size={22 - 6 * mp}/> : Icons.check(C.brandHi, 16)}
        </div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{
            fontFamily: C.display, fontSize: titleSize, color: C.inkText,
            letterSpacing: '-0.01em', lineHeight: 1.1,
            whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
          }}>Audit agent</div>
          <div style={{ marginTop: 4 + (1 - 4) * mp }}>
            <Mono size={11 - mp} color={C.brandHi}>
              {eyebrowMp < 0.5 ? 'VALIDATING FIT' : 'DONE'}
            </Mono>
          </div>
        </div>
        {innerOp > 0.02 && (
          <div style={{ opacity: innerOp, flexShrink: 0 }}>
            <LiveDot size={8} time={t}/>
          </div>
        )}
      </div>

      {/* divider — fades + collapses */}
      {innerOp > 0.02 && (
        <div style={{
          height: 1,
          background: C.ink3,
          margin: '20px 0 16px',
          opacity: innerOp,
        }}/>
      )}

      {/* checks list (all done) — fades out as card collapses */}
      {innerOp > 0.02 && (
        <div style={{
          display: 'flex', flexDirection: 'column', gap: 14,
          opacity: innerOp,
          flex: '1 1 auto',
          minHeight: 0,
        }}>
          {[
            { label: 'Checking collection protocol',  kind: 'email' },
            { label: 'Verifying storage conditions',  kind: 'chat'  },
            { label: 'Confirming metadata endpoints', kind: 'loop'  },
          ].map((c, i) => (
            <div key={i} style={{
              display: 'flex', alignItems: 'center', gap: 14,
              padding: '14px 16px',
              background: tt('oklch(0.10 0.01 250 / 0.7)'),
              border: tt(`1px solid oklch(0.30 0.06 195 / 0.8)`),
              borderRadius: 12,
            }}>
              <div style={{
                width: 28, height: 28, borderRadius: '50%',
                background: tt('oklch(0.74 0.14 195 / 0.20)'),
                border: `1px solid ${C.brandHi}`,
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                flexShrink: 0,
              }}>{Icons.check(C.brandHi, 14)}</div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{
                  fontFamily: C.body, fontSize: 19, color: C.inkText,
                  letterSpacing: '-0.005em',
                }}>{c.label}</div>
                <div style={{
                  marginTop: 8, height: 3, borderRadius: 2,
                  background: tt('oklch(0.18 0.018 250)'), overflow: 'hidden',
                }}>
                  <div style={{ width: '100%', height: '100%', background: C.brandHi }}/>
                </div>
              </div>
              {/* Mirror scene 3's CheckThread done-state so the seam frame is identical */}
              <DoneCheckThread kind={c.kind}/>
              <div style={{ minWidth: 70, textAlign: 'right' }}>
                <Mono size={10} color={C.brandHi}>PASS</Mono>
              </div>
            </div>
          ))}
        </div>
      )}

      {/* bottom summary — fades out */}
      {innerOp > 0.02 && (
        <div style={{
          marginTop: 20,
          fontFamily: C.body, fontSize: 16, color: C.inkText2,
          opacity: innerOp,
          display: 'flex', alignItems: 'center', gap: 12,
          flexShrink: 0,
        }}>
          <Mono size={11} color={C.brandHi}>RESULT</Mono>
          <span>2 of 4 sample routes cleared all checks</span>
        </div>
      )}
    </div>
  );
}

// ── Process word: shimmering rotating verb above the transit line ─────────
function ProcessWord({ t, startAt, endAt, x1, x2, y }) {
  if (t < startAt - 0.05 || t > endAt + 0.4) return null;
  const local = clamp(t - startAt, 0, endAt - startAt);
  const totalDur = endAt - startAt;
  const wordDur = totalDur / TRANSIT_WORDS.length;
  const idx = Math.min(TRANSIT_WORDS.length - 1, Math.floor(local / wordDur));
  const wordLocal = local - idx * wordDur;
  const fadeIn  = Easing.easeOutCubic(clamp(wordLocal / 0.28, 0, 1));
  const fadeOut = 1 - Easing.easeInCubic(clamp((wordLocal - (wordDur - 0.28)) / 0.28, 0, 1));
  const wordOp = fadeIn * fadeOut;

  const k = Easing.easeInOutCubic(clamp(local / totalDur, 0, 1));
  const px = x1 + (x2 - x1) * k;

  // shimmer sweep position (0..1) — moves left→right
  const shim = ((t * 0.6) % 1);
  const word = TRANSIT_WORDS[idx];

  return (
    <div style={{
      position: 'absolute', left: px, top: y,
      transform: 'translate(-50%, -50%)',
      pointerEvents: 'none',
      opacity: wordOp,
      whiteSpace: 'nowrap',
      textAlign: 'center',
    }}>
      <div style={{
        fontFamily: C.display, fontSize: 46,
        letterSpacing: '-0.01em',
        backgroundImage: tt(`linear-gradient(90deg,
          oklch(0.50 0.05 195) 0%,
          oklch(0.50 0.05 195) ${Math.max(0, shim * 100 - 22)}%,
          oklch(0.92 0.06 195) ${shim * 100}%,
          oklch(0.50 0.05 195) ${Math.min(100, shim * 100 + 22)}%,
          oklch(0.50 0.05 195) 100%)`),
        WebkitBackgroundClip: 'text',
        backgroundClip: 'text',
        WebkitTextFillColor: 'transparent',
        color: 'transparent',
        textShadow: `0 0 18px ${C.brandGlow}`,
      }}>{word}…</div>
      <div style={{ marginTop: 8 }}>
        <Mono size={11} color={C.inkText3} tracking="0.20em">
          PROCESSING · STEP {idx + 1} OF {TRANSIT_WORDS.length}
        </Mono>
      </div>
    </div>
  );
}

// ── File-ready toast notification ──────────────────────────────────────────
function FileReadyToast({ t, startAt, x, y }) {
  const local = t - startAt;
  if (local < -0.05) return null;
  const slideP = Easing.easeOutCubic(clamp(local / 0.55, 0, 1));
  const offX = (1 - slideP) * 100;
  const overshoot = Math.sin(clamp(local / 0.55, 0, 1) * Math.PI) * 6;
  // download arrow bouncing
  const armed = local > 0.7;
  const cyc = armed ? ((local - 0.7) % 1.2) / 1.2 : 0;
  const arrowY = armed ? (-6 + cyc * 14) : -6;
  const arrowOp = armed ? Math.sin(cyc * Math.PI) : 0;

  return (
    <div style={{
      position: 'absolute', left: x + offX + overshoot, top: y - 20,
      width: 360, height: 120,
      background: tt('oklch(0.16 0.02 195 / 0.96)'),
      border: `1.5px solid ${C.brandHi}`,
      borderRadius: 14,
      boxShadow: `0 0 36px ${C.brandGlow}, 0 16px 32px rgba(0,0,0,0.5)`,
      padding: '16px 20px',
      boxSizing: 'border-box',
      display: 'flex', alignItems: 'center', gap: 16,
      opacity: slideP,
      backdropFilter: 'blur(10px)',
    }}>
      {/* doc icon */}
      <div style={{
        width: 56, height: 70,
        background: tt('oklch(0.20 0.04 195 / 0.95)'),
        border: `1.5px solid ${C.brandHi}`,
        borderRadius: 8,
        position: 'relative',
        flexShrink: 0,
        display: 'flex', flexDirection: 'column',
        padding: '10px 8px', gap: 4,
      }}>
        <div style={{
          position: 'absolute', top: 0, right: 0,
          width: 14, height: 14,
          background: tt(`linear-gradient(225deg, oklch(0.10 0.01 250) 50%, transparent 50%)`),
          borderLeft: `1px solid ${C.brandHi}`,
          borderBottom: `1px solid ${C.brandHi}`,
        }}/>
        <div style={{ height: 2, background: C.brandHi, opacity: 0.85, width: '70%', borderRadius: 1 }}/>
        <div style={{ height: 1.5, background: C.inkText3, width: '95%', borderRadius: 1 }}/>
        <div style={{ height: 1.5, background: C.inkText3, width: '85%', borderRadius: 1 }}/>
        <div style={{ height: 1.5, background: C.inkText3, width: '90%', borderRadius: 1 }}/>
        <div style={{ flex: 1 }}/>
        <svg width="32" height="10" viewBox="0 0 32 10">
          <path d="M2 7 C 5 1, 9 9, 14 4 S 24 1, 30 6"
            stroke={C.brandHi} strokeWidth="1.2" fill="none" strokeLinecap="round"/>
        </svg>
      </div>
      {/* text */}
      <div style={{ flex: 1, minWidth: 0 }}>
        <Mono size={10} color={C.brandHi} tracking="0.18em">NOTIFICATION</Mono>
        <div style={{
          fontFamily: C.display, fontSize: 22, color: C.inkText,
          letterSpacing: '-0.005em', lineHeight: 1.15, marginTop: 4,
        }}>File ready</div>
        <div style={{ fontFamily: C.body, fontSize: 13, color: C.inkText3, marginTop: 3 }}>
          contract_KAU-2104.pdf · 2.4 MB
        </div>
      </div>
      {/* download icon w/ bouncing arrow */}
      <div style={{ position: 'relative', width: 36, height: 36, flexShrink: 0 }}>
        <div style={{
          position: 'absolute', inset: 0, borderRadius: '50%',
          background: tt('oklch(0.74 0.14 195 / 0.20)'),
          border: `1px solid ${C.brandHi}`,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
        }}>
          <svg width="16" height="16" viewBox="0 0 22 22" fill="none">
            <path d="M11 3 V14 M5 9 L11 15 L17 9 M4 19 H18" stroke={C.brandHi} strokeWidth="2"
              fill="none" strokeLinecap="round" strokeLinejoin="round"/>
          </svg>
        </div>
        {armed && (
          <div style={{
            position: 'absolute', left: '50%', top: arrowY - 18,
            transform: 'translateX(-50%)', opacity: arrowOp,
          }}>
            <svg width="14" height="14" viewBox="0 0 22 22" fill="none">
              <path d="M11 3 V15 M5 10 L11 16 L17 10" stroke={C.brandHi} strokeWidth="2"
                fill="none" strokeLinecap="round" strokeLinejoin="round"/>
            </svg>
          </div>
        )}
      </div>
    </div>
  );
}

// ── Quote card: materializes below bundle pill during awaiting ─────────────
// On click, contents fade out, the pill+card visually merge into a long line
// that races to the right, ending in a check.
function QuoteCard({ t, appearAt, clickAt, doneAt, pillX, pillY, pillW, pillH }) {
  if (t < appearAt - 0.05) return null;

  // Phase A: appear (appearAt → appearAt+0.5)
  // Phase B: hold (read) until clickAt
  // Phase C: dismiss content + line acceleration (clickAt → doneAt - 0.4)
  // Phase D: check (doneAt - 0.4 → doneAt)

  const appearP = Easing.easeOutCubic(clamp((t - appearAt) / 0.55, 0, 1));
  const dismissP = Easing.easeInCubic(clamp((t - clickAt) / 0.35, 0, 1));
  const lineP = Easing.easeInOutCubic(clamp((t - clickAt - 0.15) / (doneAt - clickAt - 0.5), 0, 1));
  const checkP = Easing.easeOutCubic(clamp((t - (doneAt - 0.4)) / 0.4, 0, 1));

  const cardW = pillW + 60;       // a touch wider than the pill, anchored to its left
  const cardH = 200;
  const cardX = pillX + pillW / 2 - cardW / 2;
  const cardY = pillY + pillH + 22;   // sits below the pill with a small gap

  // After click: card visually collapses upward (height shrinks, opacity fades)
  const collapseH = cardH * (1 - dismissP);
  const cardOpacity = appearP * (1 - dismissP);

  // Line extension to the right of the bundle pill
  const lineStartX = pillX + pillW;
  // extend toward the start of the transit segment (right of the bundle world)
  const lineEndX = lineStartX + 160 + lineP * 0;   // small fixed extension; transit line takes over
  const lineDrawWidth = lineP * (lineEndX - lineStartX);

  return (
    <React.Fragment>
      {/* connector tick from pill bottom to card top */}
      {appearP > 0.05 && dismissP < 0.95 && (
        <div style={{
          position: 'absolute',
          left: pillX + pillW / 2 - 0.5,
          top: pillY + pillH,
          width: 1, height: 22 * (1 - dismissP),
          background: C.brandHi,
          opacity: appearP * 0.6 * (1 - dismissP),
        }}/>
      )}

      {/* Card itself — flat, no heavy shadow to keep depth low */}
      {cardOpacity > 0.02 && (
        <div style={{
          position: 'absolute',
          left: cardX, top: cardY,
          width: cardW, height: collapseH,
          background: tt('oklch(0.13 0.013 250 / 0.95)'),
          border: tt(`1px solid oklch(0.30 0.05 195 / 0.7)`),
          borderRadius: 12,
          boxSizing: 'border-box',
          padding: '16px 20px',
          display: 'flex', flexDirection: 'column', gap: 10,
          opacity: cardOpacity,
          transform: `translateY(${(1 - appearP) * -8}px)`,
          overflow: 'hidden',
        }}>
          {/* eyebrow row: QUOTE + sand-clock */}
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
            <Mono size={10} color={C.brandHi} tracking="0.20em">QUOTE</Mono>
            <SandClock t={t} size={16}/>
          </div>

          {/* two-column key facts */}
          <div style={{
            display: 'grid', gridTemplateColumns: '1fr 1fr',
            columnGap: 18, rowGap: 8, marginTop: 4,
          }}>
            <KV label="Sample provider" value="Biobank"/>
            <KV label="Platform"        value="Isospec"/>
            <KV label="Total"           value="$ 50,000 USD" emphasize/>
            <KV label="Turnaround"      value="3 weeks"/>
          </div>
        </div>
      )}

      {/* Long line + check: only after click */}
      {/* (line removed — transit dashed path takes over) */}
    </React.Fragment>
  );
}

// ── CompletionCheck: big animated check landing on the line just before the file toast.
function CompletionCheck({ t, appearAt, x, y }) {
  if (t < appearAt - 0.05) return null;
  const p = Easing.easeOutCubic(clamp((t - appearAt) / 0.5, 0, 1));
  // pop slightly bigger then settle
  const popScale = p < 0.7 ? 0.4 + p * (1 / 0.7) * 0.7 : 1.1 - (p - 0.7) / 0.3 * 0.1;
  return (
    <div style={{
      position: 'absolute',
      left: x - 32, top: y - 32,
      width: 64, height: 64, borderRadius: '50%',
      background: tt('oklch(0.74 0.14 195 / 0.18)'),
      border: `1.6px solid ${C.brandHi}`,
      boxShadow: `0 0 ${24 * p}px ${C.brandGlow}`,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      transform: `scale(${popScale})`,
      opacity: p,
    }}>
      <svg width="32" height="32" viewBox="0 0 28 28">
        <path d="M6 14 L12 20 L22 8"
          stroke={C.brandHi} strokeWidth="3" fill="none"
          strokeLinecap="round" strokeLinejoin="round"
          strokeDasharray="40"
          strokeDashoffset={40 * (1 - p)}/>
      </svg>
    </div>
  );
}

function KV({ label, value, emphasize }) {
  return (
    <div>
      <div style={{ fontFamily: C.body, fontSize: 11, color: C.inkText3, letterSpacing: '0.02em' }}>{label}</div>
      <div style={{
        fontFamily: emphasize ? C.display : C.body,
        fontSize: emphasize ? 20 : 15,
        color: emphasize ? C.brandHi : C.inkText,
        marginTop: 2, lineHeight: 1.1,
        whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
      }}>{value}</div>
    </div>
  );
}

function SandClock({ t, size = 16 }) {
  // Subtle rotation every 1.6s
  const cyc = (t % 1.6) / 1.6;
  const rot = cyc < 0.7 ? 0 : ((cyc - 0.7) / 0.3) * 180;
  // sand drains: top fills 1→0, bottom 0→1
  const drain = clamp(cyc / 0.7, 0, 1);
  return (
    <svg width={size} height={size} viewBox="0 0 16 16" style={{ transform: `rotate(${rot}deg)`, transition: 'transform 60ms linear' }}>
      <path d="M3 2 H13 L9 8 L13 14 H3 L7 8 Z"
        fill="none" stroke={C.brandHi} strokeWidth="1.2" strokeLinejoin="round"/>
      {/* top sand */}
      <path d={`M${3 + drain * 2} 2 H${13 - drain * 2} L${8 + (1-drain) * 1} ${2 + (1-drain) * 5} Z`}
        fill={C.brandHi} opacity="0.45"/>
      {/* bottom sand */}
      <path d={`M${3 + (1-drain) * 2} 14 H${13 - (1-drain) * 2} L${8} ${14 - drain * 5} Z`}
        fill={C.brandHi} opacity="0.65"/>
    </svg>
  );
}

// ── DoneCheckThread ───────────────────────────────────────────────────────
// Static rendering of scene 3's CheckThread in its DONE state (phaseT >= 1)
// so the AuditMorph's check rows look identical to scene 3's final card.
function DoneCheckThread({ kind }) {
  if (kind === 'loop') {
    return (
      <div style={{
        width: 240, height: 38,
        display: 'flex', alignItems: 'center', justifyContent: 'flex-end',
        gap: 10,
      }}>
        <Mono size={10} color={C.brandHi} tracking="0.10em">18 REQ</Mono>
        <div style={{
          width: 32, height: 32, borderRadius: 8,
          background: tt('oklch(0.74 0.14 195 / 0.18)'),
          border: `1px solid ${C.brandHi}`,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
        }}>
          {Icons.check(C.brandHi, 16)}
        </div>
      </div>
    );
  }

  const SCHEDULES = {
    email: [
      { side: 'out', icon: 'mail' },
      { side: 'in',  icon: 'mail' },
      { side: 'out', icon: 'mail' },
    ],
    chat: [
      { side: 'out', icon: 'chat' },
      { side: 'in',  icon: 'chat' },
      { side: 'out', icon: 'phone' },
      { side: 'in',  icon: 'chat' },
    ],
  };
  const BUBBLES = SCHEDULES[kind] || SCHEDULES.email;

  return (
    <div style={{
      width: 240, height: 38,
      display: 'flex', alignItems: 'center', justifyContent: 'flex-end',
      gap: 5,
    }}>
      {BUBBLES.map((b, i) => {
        const accent = b.side === 'out' || b.icon === 'phone';
        return (
          <div key={i} style={{
            width: 36, height: 26, borderRadius: 7,
            background: accent ? tt('oklch(0.74 0.14 195 / 0.22)') : tt('oklch(0.20 0.018 250 / 0.95)'),
            border: tt(`1px solid ${accent ? C.brandHi : 'oklch(0.42 0.04 250)'}`),
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            boxShadow: accent ? tt(`0 0 6px oklch(0.74 0.14 195 / 0.3)`) : 'none',
            flexShrink: 0,
          }}>
            {Icons[b.icon](accent ? C.brandHi : tt('oklch(0.62 0.02 250)'), 13)}
          </div>
        );
      })}
    </div>
  );
}

// ── Scene-3 leaves overlay ────────────────────────────────────────────────
// Renders a frozen snapshot of scene 3's final leaf column on the LEFT of
// the stage so the seam frame matches scene 3 exactly. Fades to 0 over
// fadeStart..fadeEnd so the pipeline view is uncluttered before the camera
// starts panning.
function Scene3LeavesOverlay({ t, fadeStart, fadeEnd }) {
  const opacity = 1 - Easing.easeInOutCubic(clamp((t - fadeStart) / (fadeEnd - fadeStart), 0, 1));
  if (opacity <= 0.01) return null;

  // Geometry must mirror crovi-scene3.jsx (S3_LEAF_W/H/GAP, S3_COL_X).
  const LEAF_W = 260, LEAF_H = 88, LEAF_GAP = 24;
  const COL_X  = 380;
  const cy = STAGE_H / 2;
  const colH = 4 * LEAF_H + 3 * LEAF_GAP;
  const startY = cy - colH / 2;

  // Same data + final pass/fail state scene 3 ended on.
  const LEAVES = [
    { name: 'CHTN · Tissue', price: '€38k', region: 'US', tat: '8w',  pub: false, pass: false },
    { name: 'NDRI',          price: '€32k', region: 'US', tat: '7w',  pub: false, pass: false },
    { name: 'MD Anderson',   price: '€36k', region: 'US', tat: '10w', pub: true,  pass: true  },
    { name: 'Karolinska',    price: '€34k', region: 'EU', tat: '11w', pub: true,  pass: true  },
  ];

  return (
    <div style={{
      position: 'absolute', inset: 0,
      opacity, pointerEvents: 'none',
    }}>
      {LEAVES.map((s, i) => {
        const top = startY + i * (LEAF_H + LEAF_GAP);
        const handle = s.name.toLowerCase()
          .replace(/\s*·\s*/g, '.').replace(/\s+/g, '.').replace(/[^a-z.]/g, '');
        const email = `partnerships@${handle}.org`;
        const failed = !s.pass;
        const passed = s.pass;
        const borderColor = passed ? C.brandHi : tt(`oklch(0.30 0.02 250)`);
        return (
          <div key={i} style={{
            position: 'absolute', left: COL_X, top,
            width: LEAF_W, height: LEAF_H,
            opacity: failed ? 0.15 : 1,
          }}>
            <div style={{
              position: 'relative',
              width: '100%', height: '100%',
              background: tt('oklch(0.13 0.013 250 / 0.95)'),
              border: `1px solid ${borderColor}`,
              borderRadius: 10,
              padding: '10px 14px',
              boxSizing: 'border-box',
              boxShadow: passed ? `0 0 16px ${C.brandGlow}` : 'none',
              display: 'flex', flexDirection: 'column', justifyContent: 'center',
              gap: 4,
              filter: failed ? `grayscale(1)` : 'none',
              overflow: 'hidden',
            }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                <div style={{
                  flex: 1, minWidth: 0,
                  fontFamily: C.display, fontSize: 17, color: C.inkText,
                  letterSpacing: '-0.005em', lineHeight: 1.1,
                  whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                  textDecoration: failed ? 'line-through' : 'none',
                }}>{s.name}</div>
                <div style={{
                  fontFamily: C.mono, fontSize: 11, color: C.inkText,
                  opacity: 0.85, flexShrink: 0,
                }}>{s.price}</div>
                {s.pub && (
                  <div style={{
                    width: 18, height: 18, borderRadius: 4,
                    border: tt(`1px solid oklch(0.45 0.015 250)`),
                    background: tt('oklch(0.20 0.012 250 / 0.5)'),
                    display: 'flex', alignItems: 'center', justifyContent: 'center',
                    flexShrink: 0,
                  }}>{Icons.doc(tt('oklch(0.62 0.02 250)'))}</div>
                )}
              </div>
              <div style={{
                fontFamily: C.mono, fontSize: 10, color: C.inkText3,
                whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
              }}>{email}</div>
              <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                <div style={{
                  fontFamily: C.mono, fontSize: 9, color: C.inkText3,
                  padding: '1px 5px', borderRadius: 3,
                  border: tt(`1px solid oklch(0.30 0.02 250)`),
                  letterSpacing: '0.04em',
                }}>{s.region}</div>
                <div style={{
                  fontFamily: C.mono, fontSize: 9, color: C.inkText3,
                  padding: '1px 5px', borderRadius: 3,
                  border: tt(`1px solid oklch(0.30 0.02 250)`),
                  letterSpacing: '0.04em',
                }}>{s.tat}</div>
                <div style={{ flex: 1 }}/>
                <div style={{ flexShrink: 0 }}>
                  {failed ? Icons.cross(C.inkText3, 12) : Icons.check(C.brandHi, 14)}
                </div>
              </div>
            </div>
          </div>
        );
      })}
    </div>
  );
}

window.Scene4Quote = Scene4Quote;
