// crovi-scene2.jsx — Scene 2: continues from scene 1's zoomed-in end-frame,
// pulls the camera back, collapses the prompt to a seed, then fans out.

// 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;

// triangular bell: 0 outside [center-w, center+w], peaks at 1 at center
const bell = (t, center, w) => Math.max(0, 1 - Math.abs(t - center) / w);

// Match scene 1's final camera state exactly so the cut is invisible.
const S1_END_ZOOM = 1.42;
const S1_END_CAMX = -260;
const S1_END_CAMY = -40;

function Scene2FanOut() {
  const { localTime: t } = useSprite();

  // Beat 0: camera HOLDS scene 1's zoom while the bar collapses — no premature pull-back.
  // The bar shrinks/blurs into the seed inside the zoomed frame, then the camera resets
  // to neutral (cut) once the bar is gone, ready for the fan-out's wider composition.
  const collapseT = clamp(t / 1.0, 0, 1);
  const collapseE = Easing.easeInCubic(collapseT);

  // While the bar is visible, camera stays exactly at scene 1's end state.
  const introZoom = S1_END_ZOOM;
  const introCamX = S1_END_CAMX;
  const introCamY = S1_END_CAMY;

  // Beat B: fan-out begins at 1.0s (right after collapse completes)
  const fanT = clamp((t - 1.0) / 4.0, 0, 1);

  // Bridge: last 1.0s of scene 2 — zoom IN on the sample-leaf cluster (biobanks + AMCs),
  // fade out everything else (agent, platform branch, axis/cat chrome, non-keepers).
  // Scene 3 then opens with the sample leaves still in view, ready for the audit card.
  const bridgeT = clamp((t - 6.0) / 1.0, 0, 1);   // 0 → 1 over last second
  const bridgeE = Easing.easeInOutCubic(bridgeT);

  // Search bar shrinks + fades into a seed dot
  const barOpacity = 1 - collapseE;
  const barScale = 1 - 0.85 * collapseE;
  const barW = 1400;

  // NOTED chip — inherited from scene 1 end; fades as collapse begins
  const notedOp = clamp(1 - collapseT * 1.6, 0, 1);

  // Seed (the agent dot) appears once the bar has fully faded — clean handoff,
  // no overlap between zoomed-bar and unzoomed-seed compositions.
  const seedOpacity = collapseT >= 1 ? clamp((t - 1.0) / 0.3, 0, 1) : 0;
  const seedX = 280;
  // Seed sits at world (seedX, cy) and is rendered INSIDE the camera so it
  // stays glued to the L0→L1 connector origins (which are also at world
  // (cx, cy) inside HTree). With the seed outside the camera, panning made the
  // dot drift away from the lines emanating from it, which read as "ball not
  // at the centre of the tree".
  const seedY = STAGE_H / 2;

  // Live pulse
  const phase = (t % 2.2) / 2.2;
  const wave = Math.sin(phase * Math.PI * 2) * 0.5 + 0.5;
  const seedGlow = 14 + wave * 14;

  return (
    <div style={{
      position: 'absolute', inset: 0, background: C.ink, overflow: 'hidden',
      fontFamily: C.body,
    }}>
      {/* faint background wordmark */}
      <div style={{
        position: 'absolute', left: 0, right: 0, bottom: -120,
        textAlign: 'center', fontFamily: C.display, fontStyle: 'italic',
        fontSize: 480, color: C.ink2, opacity: 0.45,
        letterSpacing: '-0.04em', lineHeight: 1, pointerEvents: 'none',
      }}>crovi</div>

      {/* Collapsing search bar — inherits scene 1's camera at t=0, eases back to neutral */}
      {barOpacity > 0.01 && (
        <div style={{
          position: 'absolute', inset: 0,
          transform: `translate(${introCamX}px, ${introCamY}px) scale(${introZoom})`,
          transformOrigin: 'center center',
          willChange: 'transform',
          pointerEvents: 'none',
        }}>
          {/* ambient brand glow — inherited from scene 1, fades with collapse */}
          <div style={{
            position: 'absolute',
            left: '50%', top: '50%',
            width: 1400, height: 600,
            marginLeft: -700, marginTop: -300,
            background: `radial-gradient(ellipse at center, ${C.brandGlow}, transparent 65%)`,
            opacity: 0.7 * (1 - collapseT * 0.5),
            filter: 'blur(20px)',
            pointerEvents: 'none',
          }}/>

          <div style={{
            position: 'absolute', left: '50%', top: '50%',
            transform: `translate(-50%, -50%) scale(${barScale})`,
            opacity: barOpacity,
            width: barW,
            background: tt('oklch(0.09 0.012 250 / 0.92)'),
            border: `1px solid ${C.ink3}`,
            borderRadius: 22,
            padding: '40px 44px',
            backdropFilter: 'blur(14px)',
            boxShadow: tt(`0 30px 80px oklch(0 0 0 / 0.5), 0 0 0 1px oklch(0.74 0.14 195 / 0.2) inset, 0 0 ${60 * collapseE}px oklch(0.74 0.14 195 / ${0.40 * collapseE})`),
            boxSizing: 'border-box',
          }}>
            {/* The full prompt — IDENTICAL to scene 1's last frame (PromptKey at progress=1)
                so the cut between scenes is invisible. Blur ramps in as collapseE grows. */}
            <div style={{
              fontFamily: C.display,
              fontSize: 50,
              lineHeight: 1.28,
              letterSpacing: '-0.015em',
              color: C.inkText,
              filter: `blur(${collapseE * 8}px)`,
            }}>
              {PROMPT_PARTS.map((p, i) => {
                if (!p.key) {
                  return <span key={i} style={{ color: C.inkText2 }}>{p.t}</span>;
                }
                return (
                  <PromptKey key={i} text={p.t} progress={1} special={p.special}/>
                );
              })}
            </div>
          </div>

          {/* "→ Source" toggle inherited from scene 1's end frame; fades as collapse begins. */}
          {notedOp > 0.01 && (
            <div style={{
              position: 'absolute',
              left: '50%', top: 'calc(50% + 30px)',
              transform: `translate(420px, 30px)`,
              opacity: notedOp,
              display: 'flex', alignItems: 'center', gap: 8,
              padding: '8px 14px',
              background: tt(`oklch(0.74 0.14 195 / 0.18)`),
              border: `1px solid ${C.brandHi}`,
              borderRadius: 999,
              color: C.brandHi,
              fontFamily: C.mono,
              fontSize: 12,
              letterSpacing: '0.14em',
              textTransform: 'uppercase',
              boxShadow: `0 0 24px ${C.brandGlow}`,
            }}>
              Source
              <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
                <path d="M2 7 H11 M7 3 L11 7 L7 11"
                  stroke={C.brandHi} strokeWidth="1.6"
                  strokeLinecap="round" strokeLinejoin="round"/>
              </svg>
            </div>
          )}
        </div>
      )}

      {/* Camera zoom + tree spread combined: tree grows outward AND camera follows.
          Seed (agent dot) and its label are rendered INSIDE the camera so they
          travel with the tree and remain anchored to the connector origins. */}
      <CameraZoom t={t} fanT={fanT} cx={seedX} cy={seedY} bridgeE={bridgeE}>
        {/* Seed (search agent) */}
        {seedOpacity > 0 && (
          <div style={{
            position: 'absolute',
            left: seedX, top: seedY,
            transform: 'translate(-50%, -50%)',
            opacity: seedOpacity * (1 - bridgeE),
          }}>
            {/* outer glow */}
            <div style={{
              position: 'absolute', left: '50%', top: '50%',
              transform: 'translate(-50%, -50%)',
              width: 120, height: 120, borderRadius: '50%',
              background: tt(`radial-gradient(circle, oklch(0.74 0.14 195 / 0.4), transparent 70%)`),
              filter: `blur(${seedGlow}px)`,
            }}/>
            {/* core dot */}
            <div style={{
              position: 'relative',
              width: 28, height: 28, borderRadius: '50%',
              background: C.brandHi,
              boxShadow: `0 0 ${seedGlow}px ${C.brandHi}`,
            }}/>
          </div>
        )}

        {/* Search agent label (appears after collapse, fades when fan begins) */}
        {seedOpacity > 0 && fanT < 0.4 && (
          <div style={{
            position: 'absolute',
            left: seedX, top: seedY + 32,
            transform: `translate(-50%, ${(1 - clamp((collapseT - 0.7)/0.3, 0, 1)) * 8}px)`,
            opacity: seedOpacity * (1 - clamp((fanT - 0.2)/0.2, 0, 1)),
            textAlign: 'center',
          }}>
            <div style={{ fontFamily: C.display, fontSize: 24, color: C.inkText, letterSpacing: '-0.01em' }}>
              Search agent
            </div>
            <div style={{ marginTop: 6 }}>
              <Mono size={10} color={C.brandHi}>GOING WIDE</Mono>
            </div>
          </div>
        )}

        <FanOut t={t} fanT={fanT} cx={seedX} cy={seedY} bridgeE={bridgeE}/>
      </CameraZoom>

    </div>
  );
}

// Camera that zooms gradually as fanT progresses, revealing levels in turn.
// 0.0 → 0.18 : framed on agent (L0/L1)
// 0.18 → 0.4 : pulls to show categories (L2)
// 0.4 → 0.85 : pulls again to show leaves (L3)
// 0.85 → 1.0 : settled wide
function CameraZoom({ t, fanT, cx, cy, bridgeE = 0, children }) {
  // Camera waypoints — keep agent (cx,cy) on-screen throughout, never shift it offscreen.
  // Focus moves rightward as levels reveal but at smaller scales so agent stays visible.
  const frames = [
    { fx: cx + 80,    fy: cy,         s: 2.2,  start: 0.00 },  // close on agent + axis
    { fx: cx + 220,   fy: cy + 30,    s: 1.7,  start: 0.18 },  // axis + categories
    { fx: cx + 380,   fy: cy + 40,    s: 1.25, start: 0.40 },  // categories + leaves
    { fx: cx + 520,   fy: cy + 40,    s: 1.0,  start: 0.85 },  // settle: full tree, agent stays in frame
  ];
  // Find current segment
  let from = frames[0], to = frames[0], local = 0;
  for (let i = 0; i < frames.length - 1; i++) {
    if (fanT >= frames[i].start && fanT <= frames[i+1].start) {
      from = frames[i]; to = frames[i+1];
      local = (fanT - from.start) / (to.start - from.start);
      break;
    }
    if (fanT > frames[i+1].start) { from = to = frames[i+1]; local = 0; }
  }
  const e = Easing.easeInOutCubic(clamp(local, 0, 1));
  let fx = from.fx + (to.fx - from.fx) * e;
  let fy = from.fy + (to.fy - from.fy) * e;
  let s  = from.s  + (to.s  - from.s)  * e;
  // Bridge: pan + zoom toward the sample-leaf cluster (above agent, around AMCs y).
  // Final target frames the 4 sample leaves on the right side of the stage so scene 3's
  // audit card (also on the right) can slide in from there.
  if (bridgeE > 0) {
    const COL_LEAF_END = cx + 640;             // matches HTree's final COL_LEAF
    // Midpoint of the four keeper leaves (CHTN, NDRI, MD Anderson, Karolinska)
    // with biobanks_y = cy - 220*vSpread and amcs_y = cy, spacing 80.
    const SAMPLE_MID_Y = cy - 203;
    const tgtFx = COL_LEAF_END + 130;
    const tgtFy = SAMPLE_MID_Y;
    const tgtS  = 1.55;
    fx = fx + (tgtFx - fx) * bridgeE;
    fy = fy + (tgtFy - fy) * bridgeE;
    s  = s  + (tgtS  - s ) * bridgeE;
  }
  // Translate so the focus point sits at viewport center, then scale.
  const tx = STAGE_W / 2 - fx;
  const ty = STAGE_H / 2 - fy;
  return (
    <div style={{
      position: 'absolute', inset: 0,
      transform: `translate(${tx}px, ${ty}px) scale(${s})`,
      transformOrigin: `${fx}px ${fy}px`,
      transition: 'none',
      willChange: 'transform',
    }}>
      {children}
    </div>
  );
}

function FanOut({ t, fanT, cx, cy, bridgeE = 0 }) {
  return <HTree t={t} fanT={fanT} cx={cx} cy={cy} bridgeE={bridgeE}/>;
}

function HTree({ t, fanT, cx, cy, bridgeE = 0 }) {
  // Tree GROWS outward as levels reveal, instead of camera zooming.
  // 0..0.18 : only L0/L1 visible, axes very close to agent
  // 0.18..0.4 : L2 categories appear, tree spreads to mid
  // 0.4..1.0 : L3 leaves appear, tree fully extended
  // Spread factor: how far out levels sit relative to baseline.
  const spreadCat   = clamp((fanT - 0.05) / 0.30, 0, 1);  // 0→1 over L1→L2
  const spreadLeaf  = clamp((fanT - 0.30) / 0.35, 0, 1);  // 0→1 over L2→L3
  const eC = Easing.easeOutCubic(spreadCat);
  const eL = Easing.easeOutCubic(spreadLeaf);

  // Column X positions interpolate from start (close) to final (far)
  const COL_AXIS = cx + 60   + (160 - 60)   * eC;     // 60 → 160
  const COL_CAT  = cx + 180  + (380 - 180)  * eC;     // 180 → 380
  const COL_LEAF = cx + 380  + (640 - 380)  * eL;     // 380 → 640

  // Vertical span scales with reveal too
  const vSpread = 0.45 + 0.85 * eL;   // 0.45 at start, up to 1.30 fully open

  // Sample side — Y positions multiplied by vSpread so tree grows outward.
  // Biobanks pushed up to 220*vSpread (was 180) to make room for the expanded
  // chip height once details (email + region/TAT) fade in at end of fan,
  // so biobanks-bottom and AMCs-top don't collide.
  const SAMPLE_CATS = [
    { id: 'sk',  label: 'Biobanks',  y: cy - 220 * vSpread, leaves: [
      { name: 'CHTN · Tissue', price: '€38k', region: 'US',  tat: '8w', n: 120, keep: true },
      { name: 'NDRI',          price: '€32k', region: 'US',  tat: '7w', n: 90, keep: true  },
      { name: 'CTSU Oxford',   price: '€41k', region: 'EU',  tat: '9w', n: 140 },
    ]},
    { id: 'sa',  label: 'AMCs',      y: cy + 0,             leaves: [
      { name: 'MD Anderson',   price: '€36k', region: 'US',  tat: '10w', n: 320, pub: true, keep: true },
      { name: 'Karolinska',    price: '€34k', region: 'EU',  tat: '11w', n: 280, pub: true, keep: true },
      { name: 'Mayo · GI',     price: '€39k', region: 'US',  tat: '9w',  n: 210, pub: true },
      { name: 'MGH · path',    price: '€37k', region: 'US',  tat: '10w', n: 240, pub: true },
    ]},
  ];
  const PLATFORM_CATS = [
    { id: 'pw', label: 'WGS / WES',     y: cy + 200 * vSpread, leaves: [
      { name: 'Illumina · NovaSeq', price: '€72k', region: 'US', tat: '5w', tech: 'short-read', keep: true },
      { name: 'PacBio · Revio',     price: '€88k', region: 'EU', tat: '7w', tech: 'long-read'  },
    ]},
    { id: 'pm', label: 'Methylation',   y: cy + 360 * vSpread, leaves: [
      { name: 'EPIC v2',           price: '€54k', region: 'EU', tat: '6w', tech: 'array', keep: true  },
      { name: 'EM-seq',            price: '€61k', region: 'US', tat: '8w', tech: 'NGS'    },
    ]},
  ];
  const ALL_CATS = [...SAMPLE_CATS, ...PLATFORM_CATS];

  // Stagger: axes 0.0, cats 0.15..0.35, leaves 0.4..0.85
  const axisP = Easing.easeOutCubic(clamp(fanT / 0.18, 0, 1));
  const catP  = (i) => Easing.easeOutCubic(clamp((fanT - 0.18 - i * 0.04) / 0.18, 0, 1));
  const leafP = (i, j) => Easing.easeOutCubic(clamp((fanT - 0.36 - i * 0.04 - j * 0.05) / 0.22, 0, 1));

  // Active-stage signal per level: lights up when its level is in flight.
  // Axis stays lit once on (it's structural — brand identity of the search).
  const axisHeat = clamp((fanT - 0.02) / 0.15, 0, 1);   // ramps in, stays at 1
  const catHeat  = clamp((fanT - 0.18) / 0.15, 0, 1);   // ramps in, stays at 1
  const leafHeat = 0; // leaves stay neutral — sourcing agent will color them in next scene

  // Filter pass: after leaves appear, fade-grey then drop most.
  // 0.62..0.74 — reject candidates fade to grey
  // 0.74..0.85 — rejects shrink + fade out, keepers remain
  // Filter pass disabled in scene 2 — sourcing agent (next scene) surfaces relevant leaves.
  // All candidates remain fully visible at the end of this scene.
  const filterT = 0;
  const dropT   = 0;

  // Bridge fade: in the last second of scene 2, fade out everything except the
  // sample keepers — chrome (axis/cat chips), platform branch entirely, non-keeper
  // sample leaves, and connectors leading to them.
  const chromeOp = 1 - bridgeE;
  const sampleKeeperMul = 1;
  const nonKeeperMul = 1 - bridgeE;

  // Branch attach points — split from agent (Y scales with vSpread). The sample
  // axis sits at biobanks_y for visual alignment with that category's chip rail.
  const sampleAxis = { x: COL_AXIS, y: cy - 220 * vSpread };
  const platformAxis = { x: COL_AXIS, y: cy + 280 * vSpread };

  return (
    <>
      {/* SVG connectors */}
      <svg style={{ position: 'absolute', inset: 0, pointerEvents: 'none', overflow: 'visible' }}>
        {/* L0 → L1 axis lines */}
        <Connector p={axisP} heat={axisHeat} x1={cx} y1={cy} x2={sampleAxis.x} y2={sampleAxis.y} opacityMul={chromeOp}/>
        <Connector p={axisP} heat={axisHeat} x1={cx} y1={cy} x2={platformAxis.x} y2={platformAxis.y} opacityMul={chromeOp}/>
        {/* L1 → L2 cat lines */}
        {SAMPLE_CATS.map((c, i) => (
          <Connector key={c.id} p={catP(i)} heat={catHeat} x1={sampleAxis.x} y1={sampleAxis.y} x2={COL_CAT} y2={c.y} opacityMul={chromeOp}/>
        ))}
        {PLATFORM_CATS.map((c, i) => (
          <Connector key={c.id} p={catP(i + SAMPLE_CATS.length)} heat={catHeat} x1={platformAxis.x} y1={platformAxis.y} x2={COL_CAT} y2={c.y} opacityMul={chromeOp}/>
        ))}
        {/* L2 → L3 leaf lines — for sample side, keepers stay (fade into scene 3);
            for platform side and sample non-keepers, fade with bridge. */}
        {ALL_CATS.map((c, i) => (
          c.leaves.map((lf, j) => {
            const ly = c.y - (c.leaves.length - 1) * 40 + j * 80;
            const isPlatform = i >= SAMPLE_CATS.length;
            // bridge fade: keepers in sample stay (mul=1); everything else fades out
            const bridgeMul = isPlatform ? chromeOp : (lf.keep ? 1 : nonKeeperMul);
            const fadeOut = ((!lf.keep) ? (1 - filterT * 0.85) : 1) * bridgeMul;
            return (
              <Connector key={c.id + j} p={leafP(i, j)} heat={leafHeat * (lf.keep ? 1 : 1 - filterT)}
                x1={COL_CAT + 90} y1={c.y}
                x2={COL_LEAF} y2={ly}
                accent={lf.pub && lf.keep}
                opacityMul={fadeOut}/>
            );
          })
        ))}
      </svg>

      {/* Axis labels (group brackets) — fade in bridge */}
      <AxisChip x={sampleAxis.x} y={sampleAxis.y} label="SAMPLE"   opacity={axisP * chromeOp} heat={axisHeat}/>
      <AxisChip x={platformAxis.x} y={platformAxis.y} label="PLATFORM" opacity={axisP * chromeOp} heat={axisHeat}/>

      {/* Category rails — fade in bridge */}
      {ALL_CATS.map((c, i) => (
        <CatChip key={c.id} x={COL_CAT} y={c.y} label={c.label} count={c.leaves.length} opacity={catP(i) * chromeOp} heat={catHeat}/>
      ))}

      {/* Leaf chips */}
      {ALL_CATS.map((c, i) => (
        c.leaves.map((lf, j) => {
          const p = leafP(i, j);
          if (p <= 0) return null;
          const ly = c.y - (c.leaves.length - 1) * 40 + j * 80;
          const isPlatform = i >= SAMPLE_CATS.length;
          // Bridge fade: sample keepers stay; everything else fades out.
          const bridgeMul = isPlatform ? chromeOp : (lf.keep ? 1 : nonKeeperMul);
          // brief mail/phone flicker — fade out during the bridge so the
          // chips match scene 3's clean state at the seam.
          const fStart = 0.36 + i * 0.04 + j * 0.05 + 0.35;
          const fLocal = fanT - fStart;
          const flickerOp = (fLocal > 0 && fLocal < 0.45)
            ? Math.sin((fLocal / 0.45) * Math.PI) * (1 - bridgeE)
            : 0;
          return (
            <LeafChip key={c.id + '-' + j}
              x={COL_LEAF} y={ly}
              p={p * bridgeMul} name={lf.name} price={lf.price}
              region={lf.region} tat={lf.tat}
              pub={lf.pub} heat={leafHeat}
              keep={lf.keep} filterT={filterT}
              fanT={fanT}
              shimmerStart={0.36 + i * 0.04 + j * 0.05 + 0.10}
              flicker={flickerOp} flickerKind={(i+j)%2 ? 'phone' : 'mail'}/>
          );
        })
      ))}
    </>
  );
}

function Connector({ p, x1, y1, x2, y2, accent, heat = 0, opacityMul = 1 }) {
  if (p <= 0) return null;
  // Right-angle (orthogonal) routing: x1,y1 → midX,y1 → midX,y2 → x2,y2
  const midX = (x1 + x2) / 2;
  const len = Math.abs(midX - x1) + Math.abs(y2 - y1) + Math.abs(x2 - midX);
  // Heat blends from cool muted to brand-cyan; AMC accent stays brand throughout.
  const heatStroke = tt(`oklch(${0.45 + 0.29*heat} ${0.02 + 0.12*heat} ${250 - 55*heat})`);
  const stroke = accent ? tt("oklch(0.74 0.14 195)") : heatStroke;
  const op = (accent ? 0.85 : (0.45 + 0.45 * heat)) * opacityMul;
  return (
    <path
      d={`M ${x1} ${y1} L ${midX} ${y1} L ${midX} ${y2} L ${x2} ${y2}`}
      stroke={stroke} strokeWidth={accent || heat > 0.5 ? 1.4 : 1} fill="none"
      strokeDasharray={`${len}`}
      strokeDashoffset={(1 - p) * len}
      opacity={op}
      style={(accent || heat > 0.4) && opacityMul > 0.5 ? { filter: tt(`drop-shadow(0 0 ${3 + 4*heat}px oklch(0.74 0.14 195 / ${0.4 + 0.3*heat}))`) } : {}}
    />
  );
}

function AxisChip({ x, y, label, opacity, heat = 0 }) {
  const borderL = 0.30 + 0.44 * heat;
  const borderC = 0.02 + 0.12 * heat;
  const borderH = 250 - 55 * heat;
  const glow = 0.05 + 0.45 * heat;
  return (
    <div style={{
      position: 'absolute', left: x, top: y,
      transform: 'translate(-50%, -50%)',
      opacity,
      padding: '8px 14px',
      background: tt('oklch(0.13 0.013 250 / 0.95)'),
      border: tt(`1px solid oklch(${borderL} ${borderC} ${borderH})`),
      borderRadius: 999,
      whiteSpace: 'nowrap',
      boxShadow: tt(`0 0 ${8 + 16*heat}px oklch(0.74 0.14 195 / ${glow})`),
    }}>
      <Mono size={12} color={tt(`oklch(${borderL} ${borderC} ${borderH})`)}>{label}</Mono>
    </div>
  );
}

function CatChip({ x, y, label, count, opacity, heat = 0 }) {
  const borderL = 0.30 + 0.30 * heat;
  const borderC = 0.02 + 0.10 * heat;
  const borderH = 250 - 55 * heat;
  return (
    <div style={{
      position: 'absolute', left: x, top: y,
      transform: `translate(0, -50%)`,
      opacity,
      width: 200, padding: '8px 14px',
      background: tt('oklch(0.11 0.012 250 / 0.95)'),
      border: tt(`1px solid oklch(${borderL} ${borderC} ${borderH})`),
      borderRadius: 8,
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      boxShadow: heat > 0.2 ? tt(`0 0 ${10*heat}px oklch(0.74 0.14 195 / ${0.25*heat})`) : 'none',
    }}>
      <div style={{ fontFamily: C.display, fontSize: 17, color: C.inkText, letterSpacing: '-0.005em' }}>{label}</div>
      <Mono size={10} color={C.inkText3}>{count}</Mono>
    </div>
  );
}

function LeafChip({ x, y, p, name, price, region, tat, pub, heat = 0, keep = false, filterT = 0, fanT = 0, shimmerStart = 0, flicker, flickerKind }) {
  const tx = (1 - p) * -10;
  // Shimmer + details driven by absolute fanT (slow, scene-wide), not p.
  // Each leaf shimmers over ~1.2 of fanT (i.e. ~3.6s of real time at 3s fanT span).
  const shimmerT  = clamp((fanT - shimmerStart) / 0.45, 0, 1);   // slow sweep
  const detailsT  = clamp((fanT - shimmerStart - 0.40) / 0.20, 0, 1);   // details after shimmer
  // Synthetic contact email derived from name
  const handle = name.toLowerCase()
    .replace(/\s*·\s*/g, '.').replace(/\s+/g, '.').replace(/[^a-z.]/g, '');
  const email = `partnerships@${handle}.org`;
  const eliminated = !keep;
  const fade = eliminated ? (1 - filterT * 0.78) : 1;
  const desat = eliminated ? clamp(filterT * 1.4, 0, 1) : 0;
  const winnerGlow = keep ? clamp(filterT * 1.2, 0, 1) : 0;
  const borderL = 0.30 + 0.30 * heat + 0.10 * winnerGlow;
  const borderC = 0.02 + 0.10 * heat + 0.06 * winnerGlow;
  const borderH = 250 - 55 * heat - 5 * winnerGlow;
  return (
    <div style={{
      position: 'absolute', left: x, top: y,
      transform: `translate(0, -50%) translateX(${tx}px)`,
      opacity: p * fade,
      filter: desat > 0 ? `grayscale(${desat}) brightness(${1 - desat * 0.35})` : 'none',
      width: 260,
      padding: '8px 12px',
      background: tt('oklch(0.13 0.013 250 / 0.95)'),
      border: tt(`1px solid oklch(${borderL} ${borderC} ${borderH})`),
      borderRadius: 8,
      boxShadow: (winnerGlow > 0.1
            ? tt(`0 0 ${10*winnerGlow}px oklch(0.74 0.14 195 / ${0.3*winnerGlow})`)
            : (heat > 0.2 ? tt(`0 0 ${8*heat}px oklch(0.74 0.14 195 / ${0.18*heat})`) : 'none')),
      transition: 'none',
    }}>
      {/* slow gentle shimmer sweeps once when leaf appears */}
      {shimmerT > 0 && shimmerT < 1 && (
        <div style={{
          position: 'absolute', inset: 0,
          borderRadius: 8,
          overflow: 'hidden',
          pointerEvents: 'none',
        }}>
          <div style={{
            position: 'absolute', top: 0, bottom: 0,
            left: `${-60 + shimmerT * 180}%`,
            width: '60%',
            background: tt('linear-gradient(90deg, transparent 0%, oklch(0.74 0.14 195 / 0.10) 35%, oklch(0.74 0.14 195 / 0.22) 50%, oklch(0.74 0.14 195 / 0.10) 65%, transparent 100%)'),
            transform: 'skewX(-12deg)',
            opacity: Math.sin(shimmerT * Math.PI), // ease in then out
          }}/>
        </div>
      )}
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
            <div style={{
              fontFamily: C.display, fontSize: 15, color: C.inkText,
              letterSpacing: '-0.005em', lineHeight: 1.25,
              whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
              flex: 1, minWidth: 0,
              textDecoration: eliminated && filterT > 0.5 ? 'line-through' : 'none',
              textDecorationColor: tt('oklch(0.5 0.02 250 / 0.6)'),
            }}>{name}</div>
            <div style={{
              fontFamily: C.mono, fontSize: 11, color: C.inkText,
              opacity: 0.85 * detailsT, flexShrink: 0,
              transform: `translateX(${(1 - detailsT) * 6}px)`,
            }}>{price}</div>
          </div>
          {/* email contact — fades in after shimmer */}
          {detailsT > 0 && (
            <div style={{
              fontFamily: C.mono, fontSize: 10, color: C.inkText3,
              marginTop: 4, opacity: detailsT,
              whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
              transform: `translateY(${(1 - detailsT) * 4}px)`,
            }}>{email}</div>
          )}
          {/* meta row: region · TAT — fades in with details */}
          {(region || tat) && detailsT > 0 && (
            <div style={{
              display: 'flex', gap: 6, marginTop: 4,
              opacity: detailsT,
              transform: `translateY(${(1 - detailsT) * 4}px)`,
            }}>
              {region && <MetaTag label={region}/>}
              {tat && <MetaTag label={tat}/>}
            </div>
          )}
        </div>
        {pub && (
          <div title="indexed via publications" style={{
            width: 20, height: 20, 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>
      {/* shortlist check appears on survivors as filter completes */}
      {keep && winnerGlow > 0.3 && (
        <div style={{
          position: 'absolute', top: -8, right: -8,
          width: 22, height: 22, borderRadius: '50%',
          background: tt('oklch(0.74 0.14 195 / 0.95)'),
          color: tt('oklch(0.13 0.013 250)'),
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          fontSize: 14, fontFamily: C.mono, fontWeight: 600,
          opacity: winnerGlow,
          transform: `scale(${0.6 + 0.4 * winnerGlow})`,
          boxShadow: tt(`0 0 8px oklch(0.74 0.14 195 / 0.6)`),
        }}>✓</div>
      )}
      {flicker > 0.02 && !eliminated && (
        <div style={{
          position: 'absolute', top: -8, right: -8,
          width: 20, height: 20, borderRadius: '50%',
          background: tt('oklch(0.13 0.013 250 / 0.98)'),
          border: tt(`1px solid oklch(0.74 0.14 195)`),
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          opacity: flicker * (1 - winnerGlow),
          transform: `scale(${0.85 + 0.15 * flicker})`,
        }}>
          {flickerKind === 'mail' ? Icons.mail(tt('oklch(0.74 0.14 195)'), 10) : Icons.phone(tt('oklch(0.74 0.14 195)'), 10)}
        </div>
      )}
    </div>
  );
}

function MetaTag({ label }) {
  return (
    <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',
    }}>{label}</div>
  );
}

function _OldFanOut({ t, fanT, cx, cy }) {
  // Upper branch — Sample providers (above center): biobank + AMC (with publication icon)
  const SAMPLE = [
    { label: 'Biobank',   sub: 'CHTN · Tissue',     icon: 'archive',  x: cx - 460, y: cy - 360, delay: 0.0,  price: '~€48k' },
    { label: 'AMC',       sub: 'via publications',  icon: 'building', x: cx + 240, y: cy - 360, delay: 0.10, amc: true, pub: true, price: '~€36k' },
  ];
  // Lower branch — Platform providers (below center): two CROs
  const PLATFORM = [
    { label: 'CRO',       sub: 'WGS · Illumina',    icon: 'flask', x: cx - 460, y: cy + 200, delay: 0.0,  price: '~€72k' },
    { label: 'CRO',       sub: 'Methylation EPIC',  icon: 'flask', x: cx + 240, y: cy + 200, delay: 0.10, price: '~€54k' },
  ];
  const ALL = [...SAMPLE, ...PLATFORM];

  // Each node: appearance progress 0..1
  const nodeProg = (delay) => {
    const local = fanT - delay * 0.7; // stagger
    if (local <= 0) return 0;
    return Easing.easeOutCubic(clamp(local / 0.35, 0, 1));
  };

  // Axis labels
  const axisLabelOp = clamp((fanT - 0.15) / 0.3, 0, 1);

  return (
    <>
      {/* Connectors (drawn behind nodes) */}
      <svg style={{ position: 'absolute', inset: 0, pointerEvents: 'none', overflow: 'visible' }}>
        {ALL.map((n, i) => {
          const p = nodeProg(n.delay);
          if (p <= 0) return null;
          // simple cubic from center to node center
          const nx = n.x + 110;
          const ny = n.y + 42;
          const dx = (nx - cx) * 0.55;
          const path = `M ${cx} ${cy} C ${cx + dx} ${cy}, ${nx - dx} ${ny}, ${nx} ${ny}`;
          return (
            <path key={i} d={path}
              stroke={n.amc ? C.brandHi : C.ink3}
              strokeWidth={n.amc ? 1.2 : 1}
              fill="none"
              strokeDasharray="4 4"
              opacity={n.amc ? 0.7 * p : 0.55 * p}
              style={n.amc ? { filter: `drop-shadow(0 0 4px ${C.brandGlow})` } : {}}
            />
          );
        })}
      </svg>

      {/* Branch labels — group brackets above each branch */}
      <BranchLabel
        x1={cx - 460} x2={cx + 460}
        y={cy - 410}
        label="SAMPLE PROVIDERS"
        opacity={axisLabelOp}
      />
      <BranchLabel
        x1={cx - 460} x2={cx + 460}
        y={cy + 320}
        label="PLATFORM PROVIDERS"
        opacity={axisLabelOp}
      />

      {/* Bridge — faint connector drawn between branches in the settle phase */}
      <BridgeLine fanT={fanT} cx={cx} cy={cy}/>

      {/* Nodes */}
      {ALL.map((n, i) => {
        const p = nodeProg(n.delay);
        if (p <= 0) return null;
        const ty = (1 - p) * 12;
        // outreach icon flicker: a brief mail/phone burst once node has settled
        const flickerStart = (n.delay * 0.7) + 0.5;
        const flickerLocal = fanT - flickerStart;
        const flickerOp = flickerLocal > 0 && flickerLocal < 0.6
          ? Math.sin((flickerLocal / 0.6) * Math.PI)
          : 0;
        const flickerKind = i % 2 === 0 ? 'mail' : 'phone';
        return (
          <div key={i} style={{
            position: 'absolute', left: n.x, top: n.y,
            width: 220, height: 84,
            opacity: p,
            transform: `translateY(${ty}px)`,
          }}>
            <div style={{
              position: 'relative',
              width: '100%', height: '100%',
              background: tt('oklch(0.13 0.013 250 / 0.95)'),
              border: `1px solid ${n.amc ? C.brandHi : C.ink3}`,
              borderRadius: 12,
              padding: '10px 12px',
              boxSizing: 'border-box',
              boxShadow: n.amc ? tt(`0 0 16px oklch(0.74 0.14 195 / 0.25)`) : 'none',
              backdropFilter: 'blur(8px)',
            }}>
              {/* header row */}
              <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                <span style={{ flexShrink: 0 }}>{Icons[n.icon](n.amc ? C.brandHi : C.inkText2)}</span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{
                    fontFamily: C.display, fontSize: 16, color: C.inkText,
                    letterSpacing: '-0.005em', lineHeight: 1.1,
                  }}>{n.label}</div>
                  <div style={{
                    fontFamily: C.body, fontSize: 11, color: C.inkText3,
                    marginTop: 2,
                  }}>{n.sub}</div>
                </div>
                {/* publication icon for AMCs */}
                {n.pub && (
                  <div title="indexed via publications" style={{
                    width: 22, height: 22, borderRadius: 4,
                    border: `1px solid ${C.brandHi}`,
                    display: 'flex', alignItems: 'center', justifyContent: 'center',
                    background: tt('oklch(0.74 0.14 195 / 0.10)'),
                  }}>
                    {Icons.doc(C.brandHi)}
                  </div>
                )}
              </div>
              {/* muted indicative price tag */}
              <div style={{
                marginTop: 10,
                fontFamily: C.mono, fontSize: 10,
                color: C.inkText3, letterSpacing: '0.04em',
                opacity: 0.7,
              }}>
                {n.price} · indicative
              </div>
              {/* outreach flicker icon */}
              {flickerOp > 0.02 && (
                <div style={{
                  position: 'absolute', top: -10, right: -10,
                  width: 22, height: 22, borderRadius: '50%',
                  background: tt('oklch(0.13 0.013 250 / 0.98)'),
                  border: `1px solid ${C.brandHi}`,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  opacity: flickerOp,
                  transform: `scale(${0.8 + 0.2 * flickerOp})`,
                  boxShadow: tt(`0 0 8px oklch(0.74 0.14 195 / ${0.5 * flickerOp})`),
                }}>
                  {flickerKind === 'mail' ? Icons.mail(C.brandHi, 11) : Icons.phone(C.brandHi, 11)}
                </div>
              )}
            </div>
          </div>
        );
      })}
    </>
  );
}

// Branch label with bracket lines on either side
function BranchLabel({ x1, x2, y, label, opacity }) {
  const lineColor = tt(`oklch(0.74 0.14 195 / 0.4)`);
  return (
    <div style={{
      position: 'absolute',
      left: x1, top: y,
      width: x2 - x1,
      display: 'flex', alignItems: 'center', gap: 14,
      opacity,
    }}>
      <div style={{ height: 1, flex: 1, background: lineColor }}/>
      <Mono size={11} color={C.brandHi}>{label}</Mono>
      <div style={{ height: 1, flex: 1, background: lineColor }}/>
    </div>
  );
}

// Bridge line: faint vertical connector drawn during settle phase (>4s)
function BridgeLine({ fanT, cx, cy }) {
  // bridge starts drawing at fanT > 0.55 (i.e. after both branches have settled)
  const bp = clamp((fanT - 0.65) / 0.4, 0, 1);
  if (bp <= 0) return null;
  const y1 = cy - 250;
  const y2 = cy + 200;
  return (
    <svg style={{ position: 'absolute', inset: 0, pointerEvents: 'none', overflow: 'visible' }}>
      <line
        x1={cx} y1={y1}
        x2={cx} y2={y1 + (y2 - y1) * bp}
        stroke={C.brandHi}
        strokeWidth={1}
        strokeDasharray="3 4"
        opacity={0.45 * bp}
      />
    </svg>
  );
}

window.Scene2FanOut = Scene2FanOut;
