/* ============================================================
   THE CASE FILM — a film, not a diagram (Mo, 16.09., third pass:
   "make a black background … it should be really a video … think from
   a user perspective … storytelling to the point, like to children as
   to management").

   It plays on a dark stage, opens and closes on the real GenData logo
   with the subject written above it, and tells one story:

     0  OPEN      what this is about, and who is telling it
     1  ONE VOICE one plainly written review, taken apart
     2  SCALE     the same reading, over every review we have
     3  LEVERS    what means the same becomes one named lever
     4  CAUSAL AI which of those relationships are real — then only that one
     5  YOUR DATA everything except the reviews comes from the customer:
                  what they changed, their own numbers, their results
     6  IMPACT    the chain runs through, step 1 → 4, and the € lights up
     7  CLOSE     the logo again — and a replay button, no auto-loop

   Two lanes say where everything comes from: reviews from above, the
   company's own systems from below (Mo, 16.09.: "not only the context
   data … the intervention, the outcome and context data — this is from
   the OEM"). Lever names are the real ones from lever_catalog_v8.json:
   "Evidence before authorisation" (86), "Phone & email reachability"
   (55), "Diagnosis-to-fix turnaround" (38).

   No euro figure is shown: the € stands for the question, not for a
   number we have published.
   ============================================================ */

const { useEffect: cfEffect, useRef: cfRef, useState: cfState } = React;

/* the stage is dark, so the palette is its own — the page tokens are
   built for paper and would disappear here */
const CF_BG = '#0A0E14';
const CF_TXT = '#EEF2F7';
const CF_DIM = 'rgba(238,242,247,0.58)';
const CF_FAINT = 'rgba(238,242,247,0.30)';
const CF_LINE = 'rgba(238,242,247,0.15)';
const CF_CARD = 'rgba(238,242,247,0.05)';
const CF_BLUE = '#6AA6FF';
const CF_CYAN = '#22D3EE';

const cfEase = (t) => (t <= 0 ? 0 : t >= 1 ? 1 : (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2));
const cfR01 = (v, a, b) => (b <= a ? 0 : Math.max(0, Math.min(1, (v - a) / (b - a))));
const cfLerp = (a, b, t) => a + (b - a) * t;
const cfP = (v, a, b) => cfEase(cfR01(v, a, b));

/* A sentence anybody could have written — the earlier one used words no
   customer uses (Mo, 16.09.: "who would write that?"). */
const CF_TEXT = 'Showed me a video of the problem before the repair … loved that … I’ll be back for sure.';
const CF_CAUSE_P = 'video of the problem before the repair';
const CF_EFFECT_P = 'I’ll be back for sure';

const CF_SNIPPETS = [
  'showed me the problem', 'nobody called back', 'price was clear', 'car not ready',
  'kept me posted', 'no surprise on the bill', 'had to call twice', 'fixed first time',
];

const CF_CAUSES = ['Evidence before authorisation', 'Phone & email reachability', 'Diagnosis-to-fix turnaround'];
const CF_EFFECTS = ['Loyalty', 'Recommendation'];
const CF_KEEP_C = 0, CF_KEEP_E = 0;

/* end second · narrator line (empty where the picture speaks for itself) */
const CF_CHAPTERS = [
  [3.6, ''],
  [11, 'One customer writes what happened.'],
  [17, 'The same reading, over every review.'],
  [23.5, 'What means the same thing gets one name.'],
  [31, 'Causal AI finds which relationships are real.'],
  [39, 'The rest we work out together, from your data.'],
  [46, 'Now one measure can be followed all the way.'],
  [56, 'And this is what a measure like that is worth.'],
  [60.5, ''],
];

/* the beat between a line appearing and the picture moving */
const CF_RD = 1.1;

/* Four corners, out or in. */
function CfFsIcon({ exit }) {
  return (
    <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor"
      strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
      {exit ? (
        <><path d="M4 1v3H1"/><path d="M6 1v3h3"/><path d="M4 9V6H1"/><path d="M6 9V6h3"/></>
      ) : (
        <><path d="M1 4V1h3"/><path d="M9 4V1H6"/><path d="M1 6v3h3"/><path d="M9 6v3H6"/></>
      )}
    </svg>
  );
}

function CaseFilm() {
  const ENDS = CF_CHAPTERS.map(c => c[0]);
  const TOTAL = ENDS[ENDS.length - 1];
  const s0 = ENDS[0], s1 = ENDS[1], s2 = ENDS[2], s3 = ENDS[3], s4 = ENDS[4], s5 = ENDS[5],
    s6 = ENDS[6], s7 = ENDS[7];

  const [t, setT] = cfState(0);
  const [done, setDone] = cfState(false);
  const [run, setRun] = cfState(0);            // bumped by the replay button
  const [hoverPause, setHoverPause] = cfState(false);
  const [keyPause, setKeyPause] = cfState(false);
  /* Full screen (Mo, 18.09.): the Fullscreen API where the browser has
     it, a fixed overlay where it does not (iPhone Safari). In full
     screen the whole screen is the stage, so hovering can no longer
     mean "pause" — there a click does what Space does. */
  const figRef = cfRef(null);
  const [nativeFs, setNativeFs] = cfState(false);
  const [pseudoFs, setPseudoFs] = cfState(false);
  const isFs = nativeFs || pseudoFs;
  const paused = (hoverPause && !isFs) || keyPause;
  const raf = cfRef(0);
  const tRef = cfRef(0);
  tRef.current = t;
  const reduced = typeof window !== 'undefined' && window.matchMedia
    && window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  /* The clock carries on from wherever it was stopped, so hovering to
     re-read a frame costs nothing (Mo, 16.09.). */
  cfEffect(() => {
    if (reduced) { setT(TOTAL); setDone(true); return; }
    if (paused) return;
    let on = true;
    const base = tRef.current >= TOTAL ? 0 : tRef.current;
    const t0 = performance.now();
    if (base === 0) setDone(false);
    const tick = (now) => {
      if (!on) return;
      const e = base + (now - t0) / 1000;
      if (e >= TOTAL) { setT(TOTAL); setDone(true); return; }   // no auto-loop
      setT(e);
      raf.current = requestAnimationFrame(tick);
    };
    raf.current = requestAnimationFrame(tick);
    return () => { on = false; cancelAnimationFrame(raf.current); };
  }, [reduced, run, paused, TOTAL]);

  /* Space stops and starts it, unless the reader is typing somewhere. */
  cfEffect(() => {
    const onKey = (e) => {
      if (e.key === 'Escape') { setPseudoFs(false); return; }
      if (e.code !== 'Space' && e.key !== ' ') return;
      const el = e.target;
      const tag = el && el.tagName ? el.tagName.toLowerCase() : '';
      if (tag === 'input' || tag === 'textarea' || tag === 'button' || (el && el.isContentEditable)) return;
      e.preventDefault();
      setKeyPause(v => !v);
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, []);

  /* Follow the browser: Esc, the system gesture and our own button all
     end in the same event. */
  cfEffect(() => {
    const onChange = () => {
      const el = document.fullscreenElement || document.webkitFullscreenElement;
      setNativeFs(!!el && el === figRef.current);
    };
    document.addEventListener('fullscreenchange', onChange);
    document.addEventListener('webkitfullscreenchange', onChange);
    return () => {
      document.removeEventListener('fullscreenchange', onChange);
      document.removeEventListener('webkitfullscreenchange', onChange);
    };
  }, []);

  const toggleFs = (e) => {
    if (e && e.currentTarget && e.currentTarget.blur) e.currentTarget.blur();
    setHoverPause(false);
    if (pseudoFs) { setPseudoFs(false); return; }
    const active = document.fullscreenElement || document.webkitFullscreenElement;
    if (active) {
      const exit = document.exitFullscreen || document.webkitExitFullscreen;
      if (exit) exit.call(document);
      return;
    }
    const el = figRef.current;
    const req = el && (el.requestFullscreen || el.webkitRequestFullscreen);
    if (!req) { setPseudoFs(true); return; }
    try {
      const p = req.call(el);
      if (p && p.catch) p.catch(() => setPseudoFs(true));
    } catch (err) { setPseudoFs(true); }
  };

  const tt = t;

  /* ---- stage progress ------------------------------------------------ */
  const R = CF_RD;
  const openP = cfP(tt, 0.3, 1.3) * (1 - cfP(tt, s0 - 0.5, s0 + 0.1));
  const sentIn = cfP(tt, s0 + R, s0 + R + 0.8);
  const hiP = cfP(tt, s0 + R + 1.1, s0 + R + 1.7);
  const travC = cfP(tt, s0 + R + 1.9, s0 + R + 2.9);
  const travE = cfP(tt, s0 + R + 3.2, s0 + R + 4.2);
  const bubbleIn = [cfP(tt, s0 + R + 2.7, s0 + R + 3.3), cfP(tt, s0 + R + 4.0, s0 + R + 4.6)];
  const sentOut = cfP(tt, s1 - 1.2, s1 - 0.3);

  const swarmP = cfP(tt, s1 + R - 0.3, s1 + R + 1.3);
  const clusterP = cfP(tt, s2 + R, s2 + R + 2.0);
  const linkP = cfP(tt, s3 + R, s3 + R + 2.0);
  const aiP = cfP(tt, s3 + R + 0.4, s3 + R + 1.2) * (1 - cfP(tt, s4 - 1.0, s4 - 0.3));
  const focusP = cfP(tt, s3 + R + 3.0, s3 + R + 4.6);
  const joinP = cfP(tt, s4 + R, s4 + R + 1.8);
  const ctxP = cfP(tt, s4 + R + 2.2, s4 + R + 3.4);
  const stepP = cfP(tt, s4 + R + 3.8, s4 + R + 4.8);
  const chainP = cfP(tt, s5 + R, s5 + R + 1.2);
  const euroP = cfP(tt, s5 + R + 1.6, s5 + R + 2.8);
  const outP = cfP(tt, s6 - 0.4, s6 + 0.5);
  const claimP = cfP(tt, s6 + 0.6, s6 + 1.5) * (1 - cfP(tt, s7 - 0.6, s7 + 0.2));
  const closeP = cfP(tt, s7 + 0.1, s7 + 0.7);

  /* ---- one grid, symmetric by construction --------------------------
     Two equal bands while we read reviews, three bands once the company's
     own side joins. Boxes are centred in their band, the bands are centred
     on the canvas, and nothing is placed by eye (Mo, 17.09.). */
  const W = 1200, H = 770;
  const HEAD_Y = 152, RULE_Y = 174, BAND_T = 190, BAND_H = 460, FLOOR_Y = 700;
  const BAND_C = BAND_T + BAND_H / 2;       // 420 — everything hangs off this

  /* phase A — two bands of the same width, same margin left and right */
  const A = [{ x: 100, w: 460 }, { x: 640, w: 460 }];
  const A_C = A[0].x + A[0].w / 2;          // 330
  const A_E = A[1].x + A[1].w / 2;          // 870

  /* phase B — intervention · what customers experience · outcome */
  const B = [{ x: 15, w: 230 }, { x: 260, w: 640 }, { x: 915, w: 270 }];
  const IV_X = B[0].x + B[0].w / 2;         // 130
  const OUT_X = B[2].x + B[2].w / 2;        // 1050
  const B_C = 420, B_E = 765;               // the pair, centred inside B[1]

  const PW = 290, EW = 240, CW = 190, PH = 52, CH = 72;
  const CHAIN_Y = BAND_C, CHIP_Y = BAND_C + 118, EURO_Y = BAND_C + 150;

  /* the two pills glide inward as the outer bands appear */
  const CAUSE_X = cfLerp(A_C, B_C, joinP);
  const EFFECT_X = cfLerp(A_E, B_E, joinP);

  const causeY = [BAND_C - 106, BAND_C, BAND_C + 106];
  const effectY = [BAND_C - 68, BAND_C + 68];
  const bubbleC = { x: A_C, y: BAND_C, r: 64 };
  const bubbleE = { x: A_E, y: BAND_C, r: 64 };
  const sentenceY = 80;
  const LANE_B = FLOOR_Y;

  const found = ENDS.findIndex(e => tt < e);
  const stage = found === -1 ? ENDS.length - 1 : found;
  const body = 1 - outP;
  const card = Math.max(openP, closeP);

  /* ---- the sentence, cut into two highlighted phrases ----------------- */
  const CHAR_W = 10.8, FS = 22;
  const sentLeft = W / 2 - (CF_TEXT.length * CHAR_W) / 2;
  const segs = (() => {
    const marks = [{ p: CF_CAUSE_P, kind: 'cause' }, { p: CF_EFFECT_P, kind: 'effect' }]
      .map(m => ({ p: m.p, kind: m.kind, i: CF_TEXT.indexOf(m.p) }))
      .filter(m => m.i >= 0).sort((a, b) => a.i - b.i);
    const out = []; let cur = 0;
    marks.forEach(m => {
      if (m.i > cur) out.push({ txt: CF_TEXT.slice(cur, m.i) });
      out.push({ txt: m.p, kind: m.kind });
      cur = m.i + m.p.length;
    });
    if (cur < CF_TEXT.length) out.push({ txt: CF_TEXT.slice(cur) });
    return out;
  })();
  const phraseX = (p) => sentLeft + (CF_TEXT.indexOf(p) + p.length / 2) * CHAR_W;

  /* ---- the other voices, absorbed into the levers --------------------- */
  const swarm = (() => {
    let s = 20260916;
    const rnd = () => { s = (s * 1664525 + 1013904223) % 4294967296; return s / 4294967296; };
    const list = [];
    const inBand = (band) => band.x + 34 + rnd() * (band.w - 68);
    const inRows = () => BAND_T + 34 + rnd() * (BAND_H - 68);
    for (let i = 0; i < 20; i++) {
      list.push({ k: 'c', x: inBand(A[0]), y: inRows(),
        r: 5 + rnd() * 7, to: Math.floor(rnd() * 3), d: rnd() * 0.45 });
    }
    for (let i = 0; i < 14; i++) {
      list.push({ k: 'e', x: inBand(A[1]), y: inRows(),
        r: 5 + rnd() * 7, to: Math.floor(rnd() * 2), d: rnd() * 0.45 });
    }
    return list;
  })();

  /* The kept lever carries the bubble's place into the cluster and the
     cluster's place into the chain, so nothing ever jumps. */
  const causePos = (i) => ({
    y: i === CF_KEEP_C ? cfLerp(cfLerp(bubbleC.y, causeY[i], clusterP), CHAIN_Y, focusP) : causeY[i],
    o: i === CF_KEEP_C ? 1 : 1 - focusP,
  });
  const effectPos = (i) => ({
    y: i === CF_KEEP_E ? cfLerp(cfLerp(bubbleE.y, effectY[i], clusterP), CHAIN_Y, focusP) : effectY[i],
    o: i === CF_KEEP_E ? 1 : 1 - focusP,
  });

  const chainPath = 'M ' + (IV_X + CW / 2) + ' ' + CHAIN_Y + ' L ' + CAUSE_X + ' ' + CHAIN_Y
    + ' L ' + EFFECT_X + ' ' + CHAIN_Y + ' L ' + OUT_X + ' ' + CHAIN_Y + ' L ' + OUT_X + ' ' + EURO_Y;

  /* the four steps of the finished chain, numbered so it reads in order */
  const steps = [
    { n: 1, x: (IV_X + CW / 2 + B_C - PW / 2) / 2, y: CHAIN_Y - 32, col: CF_TXT },
    { n: 2, x: (B_C + PW / 2 + B_E - EW / 2) / 2, y: CHAIN_Y - 32, col: CF_CYAN },
    { n: 3, x: (B_E + EW / 2 + OUT_X - CW / 2) / 2, y: CHAIN_Y - 32, col: CF_CYAN },
    { n: 4, x: OUT_X + 26, y: (CHAIN_Y + CH / 2 + EURO_Y - 36) / 2, col: CF_CYAN },
  ];

  const mono = 'JetBrains Mono, ui-monospace';
  const sans = 'Plus Jakarta Sans, system-ui';
  const ctl = isFs ? 'text-[11px] px-3 py-1.5' : 'text-[9px] px-2.5 py-1';

  return (
    <figure ref={figRef}
      className={(pseudoFs ? 'fixed inset-0 z-[100] ' : 'relative h-full rounded-[24px] ')
        + 'flex flex-col overflow-hidden'}
      style={{ background: CF_BG }}>

      <div className={'mono absolute z-10 rounded-full '
          + (isFs ? 'top-6 left-6 text-[11px] px-3 py-1.5' : 'top-4 left-4 text-[9.5px] px-2.5 py-1')}
        style={{ background: 'rgba(34,211,238,0.14)', color: CF_CYAN, letterSpacing: '0.16em' }}>
        HOW
      </div>
      <div className={'absolute z-10 flex items-center gap-2 ' + (isFs ? 'top-6 right-6' : 'top-4 right-4')}>
        {paused && (
          <span className={'mono rounded-full ' + ctl}
            style={{ background: 'rgba(34,211,238,0.14)', color: CF_CYAN }}>
            ‖ PAUSED · {isFs ? 'CLICK OR SPACE' : 'SPACE'}
          </span>
        )}
        <button type="button"
          onClick={(e) => { e.currentTarget.blur(); setT(0); setKeyPause(false); setHoverPause(false); setRun(r => r + 1); }}
          className={'mono rounded-full border transition-colors ' + ctl}
          style={{ borderColor: done ? CF_CYAN : CF_LINE, color: done ? CF_CYAN : CF_FAINT }}>
          ↻ REPLAY
        </button>
        <button type="button" onClick={toggleFs}
          className={'mono rounded-full border transition-colors inline-flex items-center gap-1.5 ' + ctl}
          style={{ borderColor: CF_LINE, color: CF_FAINT }}
          aria-label={isFs ? 'Exit full screen' : 'Show the film full screen'}>
          <CfFsIcon exit={isFs}/>{isFs ? 'EXIT' : 'FULL SCREEN'}
        </button>
      </div>

      <div className={'relative flex-1 min-h-0 pb-0 ' + (isFs ? 'px-10 pt-10' : 'px-3 sm:px-6 pt-6')}
        onMouseEnter={() => setHoverPause(true)} onMouseLeave={() => setHoverPause(false)}
        onClick={() => { if (isFs) setKeyPause(v => !v); }}>
        {/* the opening and closing frame: the subject, and the real logo */}
        <div className="absolute inset-0 grid place-items-center px-6"
          style={{ opacity: card, pointerEvents: 'none' }}>
          <div className="text-center">
            <div className={'mono ' + (isFs ? 'text-[14px]' : 'text-[10.5px]')}
              style={{ color: CF_DIM, letterSpacing: '0.26em' }}>
              AUTOMOTIVE AFTER-SALES STEERING
            </div>
            <div className={isFs ? 'mt-9' : 'mt-6'} style={{ '--c-ink': '238 242 247' }}>
              <Logo size={isFs ? 84 : 54}/>
            </div>
            <div className={isFs ? 'mt-9 text-[20px]' : 'mt-6 text-[14px]'} style={{ color: CF_DIM }}>
              A case study — from one customer sentence to what a measure is worth.
            </div>
          </div>
        </div>

        <svg viewBox={'0 0 ' + W + ' ' + H} className="w-full h-full" preserveAspectRatio="xMidYMid meet" role="img"
          aria-label="An after-sales case study. A customer review is taken apart into a cause and an effect; the same reading runs over every review; what means the same thing gets one name; causal AI tests which of the relationships between those causes and effects are real, and one is kept; together with the company we add what it changed, its context data and its results; the finished chain is then followed in four steps to what the measure was worth.">
          <defs>
            {[['cf-a-cyan', CF_CYAN], ['cf-a-dim', CF_FAINT], ['cf-a-txt', CF_TXT]].map(pr => (
              <marker key={pr[0]} id={pr[0]} viewBox="0 0 10 10" refX="9" refY="5"
                markerWidth="4" markerHeight="4" orient="auto">
                <path d="M 0 0 L 10 5 L 0 10 z" fill={pr[1]}/>
              </marker>
            ))}
          </defs>

          <g opacity={body}>
            {/* ── lane below: everything else comes from the customer ── */}
            {joinP > 0.02 && (
              <g opacity={joinP}>
                {[[IV_X, CHAIN_Y + CH / 2 + 10], [B[1].x + B[1].w / 2, CHIP_Y + 30], [OUT_X, CHAIN_Y + CH / 2 + 10]].map((pt, i) => (
                  <line key={i} x1={pt[0]} y1={LANE_B - 6} x2={pt[0]} y2={pt[1]}
                    stroke={CF_FAINT} strokeWidth="1.5" strokeDasharray="5 5" markerEnd="url(#cf-a-dim)"/>
                ))}
                <line x1="40" y1={LANE_B - 2} x2={W - 40} y2={LANE_B - 2}
                  stroke={CF_LINE} strokeWidth="1"/>
              </g>
            )}

            {/* ── the columns ARE the guidance (Mo, 17.09.): a surface per
                 column and the word over it, big enough to lead the eye.
                 Two while we are reading reviews, three once your own side
                 joins — the middle one is simply the two earlier columns
                 taken together. ── */}
            {(() => {
              const two = cfP(tt, s0 + R + 1.5, s0 + R + 2.3) * (1 - joinP);
              const three = joinP;
              const bands = [];
              if (two > 0.02) bands.push(
                { k: 'c', ...A[0], t: 'Causes', n: '3 of 61', c: CF_BLUE, o: two },
                { k: 'e', ...A[1], t: 'Effects', n: '2 of 4', c: CF_CYAN, o: two },
              );
              if (three > 0.02) bands.push(
                { k: 'i', ...B[0], t: 'Intervention', c: CF_TXT, o: three },
                { k: 'm', ...B[1], t: 'What customers experience', c: CF_TXT, o: three },
                { k: 'o', ...B[2], t: 'Outcome', c: CF_TXT, o: three },
              );
              if (!bands.length) return null;
              return bands.map(b => (
                <g key={b.k} opacity={b.o}>
                  <rect x={b.x} y={BAND_T} width={b.w} height={BAND_H} rx="18"
                    fill="rgba(238,242,247,0.032)"/>
                  <text x={b.x + b.w / 2} y={HEAD_Y} textAnchor="middle" fontSize="24" fontWeight="600"
                    fontFamily={sans} fill={b.c}>
                    {b.t}
                    {b.n && (
                      <tspan fontSize="12.5" fontWeight="500" fontFamily={mono} fill={CF_DIM}>
                        {'  \u00b7 ' + b.n}
                      </tspan>
                    )}
                  </text>
                  <line x1={b.x + 18} y1={RULE_Y} x2={b.x + b.w - 18} y2={RULE_Y}
                    stroke={CF_LINE} strokeWidth="1"/>
                </g>
              ));
            })()}

            {/* ── the one review ── */}
            {sentIn > 0.01 && sentOut < 0.99 && (
              <foreignObject x="40" y={sentenceY - 36} width={W - 80} height="76"
                opacity={sentIn * (1 - sentOut)}>
                <div xmlns="http://www.w3.org/1999/xhtml" style={{
                  fontFamily: sans, fontSize: FS + 'px', fontWeight: 500, color: CF_TXT,
                  textAlign: 'center', letterSpacing: '-0.01em', lineHeight: 1.5,
                }}>
                  {segs.map((sg, i) => {
                    if (!sg.kind) return <span key={i} style={{ whiteSpace: 'pre' }}>{sg.txt}</span>;
                    const bg = sg.kind === 'effect'
                      ? 'rgba(34,211,238,' + (0.28 * hiP) + ')' : 'rgba(106,166,255,' + (0.26 * hiP) + ')';
                    return (
                      <span key={i} style={{
                        background: bg, borderRadius: 3, boxShadow: '0 0 0 3px ' + bg, whiteSpace: 'pre',
                      }}>{sg.txt}</span>
                    );
                  })}
                </div>
              </foreignObject>
            )}

            {/* ── the two phrases fly to their side ── */}
            {[{ p: CF_CAUSE_P, prog: travC, dst: bubbleC, col: CF_BLUE, w: 322 },
              { p: CF_EFFECT_P, prog: travE, dst: bubbleE, col: CF_CYAN, w: 190 }].map(tr => {
              if (tr.prog <= 0 || tr.prog >= 1) return null;
              const e = cfEase(tr.prog);
              const x = cfLerp(phraseX(tr.p), tr.dst.x, e), y = cfLerp(sentenceY, tr.dst.y, e);
              const fade = Math.min(1, tr.prog / 0.12) * Math.min(1, (1 - tr.prog) / 0.18);
              return (
                <g key={tr.p} opacity={fade}>
                  <rect x={x - tr.w / 2} y={y - 16} width={tr.w} height="32" rx="16" fill={tr.col}/>
                  <text x={x} y={y + 5} textAnchor="middle" fontSize="14.5" fontWeight="600"
                    fontFamily={sans} fill={CF_BG}>{tr.p}</text>
                </g>
              );
            })}

            {/* ── the same reading, on every other review ── */}
            {tt > s1 + R - 0.2 && tt < s2 && CF_SNIPPETS.map((sn, i) => {
              const span = Math.max(0.6, (s2 - 0.7) - (s1 + R));
              const spawn = (s1 + R) + (i / CF_SNIPPETS.length) * span;
              const local = tt - spawn;
              if (local < 0 || local > 1.1) return null;
              const p = cfEase(local / 1.1);
              const isE = i % 3 === 2;
              const tgt = isE ? bubbleE : bubbleC;
              const jx = ((i * 151) % 340) - 170;
              const x = cfLerp(tgt.x + jx, tgt.x, p), y = cfLerp(-24, tgt.y, p);
              const a = Math.min(1, p / 0.12) * Math.min(1, (1 - p) / 0.3) * 0.9;
              return (
                <text key={sn} x={x} y={y} textAnchor="middle" fontSize="12" fontWeight="500"
                  fontFamily={mono} fill={isE ? CF_CYAN : CF_BLUE} opacity={a}
                  style={{ letterSpacing: '0.03em' }}>{sn}</text>
              );
            })}

            {/* ── the other voices, collapsing into the levers ── */}
            {swarmP > 0.01 && swarm.map((b, i) => {
              const appear = cfP(tt, s1 + R - 0.3 + b.d * 1.6, s1 + R + 0.5 + b.d * 1.6);
              const cp = cfP(tt, s2 + R + 0.1 + b.d * 1.2, s2 + R + 1.5 + b.d * 1.2);
              const tx = b.k === 'c' ? CAUSE_X : EFFECT_X;
              const ty = b.k === 'c' ? causeY[b.to] : effectY[b.to];
              const op = appear * (1 - cp) * 0.55;
              if (op < 0.02) return null;
              return (
                <circle key={'s' + i} cx={cfLerp(b.x, tx, cp)} cy={cfLerp(b.y, ty, cp)}
                  r={b.r * cfLerp(1, 0.5, cp)} fill="none" strokeWidth="1.6"
                  stroke={b.k === 'c' ? CF_BLUE : CF_CYAN} opacity={op}/>
              );
            })}

            {/* ── where causal AI is applied: every one of these arrows ── */}
            {linkP > 0.01 && [[0, 0, true], [1, 0, true], [2, 0, true], [0, 1, true], [2, 1, false]].map((l, i) => {
              const p = cfEase(Math.max(0, Math.min(1, (linkP - i * 0.12) / 0.5)));
              if (p <= 0) return null;
              const keep = l[0] === CF_KEEP_C && l[1] === CF_KEEP_E;
              const o = (keep ? 1 : 1 - focusP) * (l[2] ? 0.9 : 0.4);
              if (o < 0.02) return null;
              const c = causePos(l[0]), e = effectPos(l[1]);
              const x1 = CAUSE_X + PW / 2, x2 = EFFECT_X - EW / 2;
              const d = 'M ' + x1 + ' ' + c.y + ' C ' + (x1 + 40) + ' ' + c.y + ' '
                + (x2 - 40) + ' ' + e.y + ' ' + x2 + ' ' + e.y;
              return (
                <path key={'l' + i} d={d} fill="none" stroke={l[2] ? CF_CYAN : CF_FAINT}
                  strokeWidth={keep ? cfLerp(2, 3.4, focusP) : 1.8}
                  strokeDasharray={l[2] ? '1' : '5 5'}
                  pathLength={l[2] ? 1 : undefined} strokeDashoffset={l[2] ? 1 - p : undefined}
                  opacity={o} markerEnd={l[2] && p > 0.9 ? 'url(#cf-a-cyan)' : undefined}/>
              );
            })}

            {/* ── and it says so ── */}
            {aiP > 0.02 && (
              <g opacity={aiP}>
                <circle cx={W / 2 - 62} cy="112" r="5" fill={CF_CYAN}/>
                <text x={W / 2 - 46} y="118" fontSize="18" fontWeight="600" fontFamily={sans} fill={CF_CYAN}>
                  Causal AI
                </text>
              </g>
            )}

            {/* ── the levers, named ── */}
            {CF_CAUSES.map((c, i) => {
              const pos = causePos(i);
              const op = clusterP * pos.o;
              if (op < 0.02) return null;
              const keep = i === CF_KEEP_C;
              const x = keep ? cfLerp(bubbleC.x, CAUSE_X, clusterP) : CAUSE_X;
              return (
                <g key={c} opacity={op}>
                  <rect x={x - PW / 2} y={pos.y - PH / 2} width={PW} height={PH} rx={PH / 2}
                    fill={CF_CARD} stroke={CF_BLUE} strokeWidth={keep ? cfLerp(1.6, 2.6, focusP) : 1.6}/>
                  <text x={x} y={pos.y + 6} textAnchor="middle" fontSize="16.5" fontWeight="600"
                    fontFamily={sans} fill={CF_BLUE}>{c}</text>
                </g>
              );
            })}
            {CF_EFFECTS.map((c, i) => {
              const pos = effectPos(i);
              const op = clusterP * pos.o;
              if (op < 0.02) return null;
              const keep = i === CF_KEEP_E;
              const x = keep ? cfLerp(bubbleE.x, EFFECT_X, clusterP) : EFFECT_X;
              return (
                <g key={c} opacity={op}>
                  <rect x={x - EW / 2} y={pos.y - PH / 2} width={EW} height={PH} rx={PH / 2}
                    fill={CF_CARD} stroke={CF_CYAN} strokeWidth={keep ? cfLerp(1.6, 2.6, focusP) : 1.6}/>
                  <text x={x} y={pos.y + 6} textAnchor="middle" fontSize="16.5" fontWeight="600"
                    fontFamily={sans} fill={CF_CYAN}>{c}</text>
                </g>
              );
            })}

            {/* ── the first two bubbles, before they become levers ── */}
            {[{ b: bubbleC, col: CF_BLUE, p: bubbleIn[0] }, { b: bubbleE, col: CF_CYAN, p: bubbleIn[1] }].map((o, i) => {
              const op = o.p * (1 - clusterP);
              if (op < 0.02) return null;
              return (
                <circle key={'b' + i} cx={o.b.x} cy={o.b.y} r={o.b.r * cfLerp(1, 0.4, clusterP)}
                  fill={CF_CARD} stroke={o.col} strokeWidth="2" opacity={op}/>
              );
            })}

            {/* ── what you changed ── */}
            {joinP > 0.02 && (() => {
              const x = cfLerp(IV_X - 260, IV_X, joinP);
              return (
                <g opacity={joinP}>
                  <rect x={x - CW / 2} y={CHAIN_Y - CH / 2} width={CW} height={CH} rx="14"
                    fill={CF_CARD} stroke={CF_TXT} strokeWidth="1.6"/>
                  <g transform={'translate(' + (x - 16) + ' ' + (CHAIN_Y - 26) + ')'}>
                    <rect width="32" height="26" rx="7" fill="none" stroke={CF_TXT} strokeWidth="1.5"/>
                    <path d="M13 8 L22 13 L13 18 Z" fill={CF_TXT}/>
                  </g>
                  <text x={x} y={CHAIN_Y + 24} textAnchor="middle" fontSize="15" fontWeight="600"
                    fontFamily={sans} fill={CF_TXT}>Video of the repair</text>
                  <line x1={x + CW / 2 + 8} y1={CHAIN_Y} x2={CAUSE_X - PW / 2 - 10} y2={CHAIN_Y}
                    stroke={CF_TXT} strokeWidth="1.8" markerEnd="url(#cf-a-txt)"/>
                </g>
              );
            })()}

            {/* ── what came of it ── */}
            {joinP > 0.02 && (() => {
              const x = cfLerp(OUT_X + 260, OUT_X, joinP);
              return (
                <g opacity={joinP}>
                  <line x1={EFFECT_X + EW / 2 + 8} y1={effectPos(CF_KEEP_E).y} x2={x - CW / 2 - 10} y2={CHAIN_Y}
                    stroke={CF_CYAN} strokeWidth="1.8" markerEnd="url(#cf-a-cyan)"/>
                  <rect x={x - CW / 2} y={CHAIN_Y - CH / 2} width={CW} height={CH} rx="14"
                    fill={CF_CARD} stroke={CF_CYAN} strokeWidth="1.6"/>
                  <g transform={'translate(' + (x - 15) + ' ' + (CHAIN_Y - 28) + ')'} fill="none"
                    stroke={CF_CYAN} strokeWidth="1.7" strokeLinecap="round">
                    <path d="M4 16 A 11 11 0 1 0 8 6"/>
                    <path d="M3 1 L8 6 L3 11"/>
                  </g>
                  <text x={x} y={CHAIN_Y + 24} textAnchor="middle" fontSize="15" fontWeight="600"
                    fontFamily={sans} fill={CF_TXT}>Customers return</text>
                </g>
              );
            })()}

            {/* ── your own numbers, into the middle ── */}
            {ctxP > 0.02 && (
              <g opacity={ctxP}>
                <rect x={B[1].x + B[1].w / 2 - 140} y={CHIP_Y - 21} width="280" height="42" rx="14"
                  fill={CF_CARD} stroke={CF_LINE} strokeWidth="1.2"/>
                <text x={B[1].x + B[1].w / 2} y={CHIP_Y + 5} textAnchor="middle" fontSize="13.5"
                  fill={CF_TXT}>
                  turnaround · wait time · capacity
                </text>
                <line x1={B[1].x + B[1].w / 2} y1={CHIP_Y - 25} x2={B[1].x + B[1].w / 2}
                  y2={CHAIN_Y + 34} stroke={CF_FAINT} strokeWidth="1.5"
                  strokeDasharray="5 5" markerEnd="url(#cf-a-dim)"/>
              </g>
            )}

            {/* ── the four steps, in order ── */}
            {stepP > 0.02 && steps.map((st, i) => {
              const p = cfEase(Math.max(0, Math.min(1, (stepP - i * 0.12) / 0.6)));
              if (p <= 0.02) return null;
              if (i === 3 && euroP < 0.05) return null;
              return (
                <g key={st.n} opacity={p}>
                  <circle cx={st.x} cy={st.y} r="11" fill={CF_BG} stroke={st.col} strokeWidth="1.5"/>
                  <text x={st.x} y={st.y + 4} textAnchor="middle" fontSize="12" fontWeight="600"
                    fontFamily={mono} fill={st.col}>{st.n}</text>
                </g>
              );
            })}

            {/* ── the chain runs through ── */}
            {chainP > 0.02 && (
              <circle r="8" fill={CF_CYAN} opacity={chainP}>
                <animateMotion dur="3.2s" repeatCount="indefinite" path={chainPath}/>
                <animate attributeName="opacity" values="0;1;1;0" keyTimes="0;0.08;0.85;1"
                  dur="3.2s" repeatCount="indefinite"/>
              </circle>
            )}

            {/* ── and what it was worth ── */}
            {euroP > 0.02 && (
              <g opacity={euroP}>
                <line x1={OUT_X} y1={CHAIN_Y + CH / 2 + 6} x2={OUT_X} y2={EURO_Y - 42}
                  stroke={CF_CYAN} strokeWidth="1.8" markerEnd="url(#cf-a-cyan)"/>
                <circle cx={OUT_X} cy={EURO_Y} r={36 * cfLerp(0.6, 1, euroP)} fill={CF_CYAN}/>
                <circle cx={OUT_X} cy={EURO_Y} r="36" fill="none" stroke={CF_CYAN} strokeWidth="2" opacity="0.5">
                  <animate attributeName="r" values="36;52;36" dur="2.6s" repeatCount="indefinite"/>
                  <animate attributeName="opacity" values="0.5;0;0.5" dur="2.6s" repeatCount="indefinite"/>
                </circle>
                <text x={OUT_X} y={EURO_Y + 13} textAnchor="middle" fontSize="36" fontWeight="700"
                  fill={CF_BG}>€</text>
                <text x={OUT_X} y={EURO_Y + 66} textAnchor="middle" fontSize="13" fill={CF_DIM}>
                  business impact
                </text>
              </g>
            )}
          </g>

          {/* ── what a measure like this is worth ──
               Three steps, in the order the chain runs them: what customers
               say, what customers do, what it is worth. Modelled — the
               header says so — but stated the way a result is stated, not
               the way an apology is (Mo, 17.09.). */}
          {claimP > 0.02 && (() => {
            const smp = (window.GD && window.GD.CASE_AUTO && window.GD.CASE_AUTO.sample) || null;
            if (!smp || !smp.steps) return null;
            const step = (i) => cfP(tt, s6 + 1.6 + i * 0.9, s6 + 2.4 + i * 0.9);
            const CW2 = 340, GAP = 30, X0 = (W - (3 * CW2 + 2 * GAP)) / 2;
            const cx = (i) => X0 + i * (CW2 + GAP) + CW2 / 2;
            const TOP = 280, HGT = 200;
            return (
              <g opacity={claimP}>
                <text x={W / 2} y="190" textAnchor="middle" fontSize="22" fontWeight="700"
                  fontFamily={sans} fill={CF_CYAN} letterSpacing="0.16em">{smp.head}</text>
                <text x={W / 2} y="238" textAnchor="middle" fontSize="20" fill={CF_TXT}>
                  {smp.lead}
                </text>

                {smp.steps.map((st, i) => (
                  <g key={st.t} opacity={step(i)}>
                    <rect x={cx(i) - CW2 / 2} y={TOP} width={CW2} height={HGT} rx="20"
                      fill={st.money ? 'rgba(34,211,238,0.12)' : CF_CARD}
                      stroke={st.money ? CF_CYAN : CF_LINE} strokeWidth={st.money ? 2 : 1.2}/>
                    <path
                      d={st.dir === 'up'
                        ? 'M ' + (cx(i) - 15) + ' ' + (TOP + 62) + ' L ' + cx(i) + ' ' + (TOP + 34)
                          + ' L ' + (cx(i) + 15) + ' ' + (TOP + 62) + ' Z'
                        : 'M ' + (cx(i) - 15) + ' ' + (TOP + 34) + ' L ' + cx(i) + ' ' + (TOP + 62)
                          + ' L ' + (cx(i) + 15) + ' ' + (TOP + 34) + ' Z'}
                      fill={CF_CYAN}/>
                    <text x={cx(i)} y={TOP + 132} textAnchor="middle" fontSize="46" fontWeight="700"
                      fontFamily={sans} fill={st.money ? CF_CYAN : CF_TXT}
                      letterSpacing="-0.03em">{st.v}</text>
                    <text x={cx(i)} y={TOP + 168} textAnchor="middle" fontSize="14" fill={CF_DIM}>
                      {st.t}
                    </text>
                  </g>
                ))}

                {[0, 1].map(i => (
                  <path key={i} opacity={step(i + 1)}
                    d={'M ' + (cx(i) + CW2 / 2 + 6) + ' ' + (TOP + HGT / 2 - 9) + ' l 11 9 l -11 9'}
                    fill="none" stroke={CF_CYAN} strokeWidth="2.4" strokeLinecap="round"
                    strokeLinejoin="round"/>
                ))}

                <g opacity={step(smp.steps.length)}>
                  {smp.foot.map((f, i) => (
                    <text key={i} x={W / 2} y={540 + i * 21} textAnchor="middle" fontSize="13"
                      fill={CF_FAINT}>{f}</text>
                  ))}
                </g>
              </g>
            );
          })()}
        </svg>
      </div>

      {/* the subtitle: one line, right under what it describes */}
      <div className={'relative px-8 pt-2 flex items-start justify-center '
          + (isFs ? 'pb-10 min-h-[136px]' : 'pb-6 min-h-[92px]')}>
        <h2 className="text-center font-bold tracking-[-0.025em] leading-[1.15] max-w-[40ch]"
          style={{ color: CF_TXT, fontSize: isFs ? 'clamp(26px, 2.8vw, 50px)' : 'clamp(22px, 2.5vw, 34px)' }}>
          {CF_CHAPTERS[stage][1]}
        </h2>
      </div>

      {/* the scrubber, along the bottom edge */}
      <div className="flex gap-[2px] shrink-0" aria-hidden>
        {CF_CHAPTERS.map((c, i) => (
          <span key={i} className="h-[3px] flex-1 overflow-hidden" style={{ background: CF_LINE }}>
            <span className="block h-full" style={{
              background: CF_CYAN,
              width: i < stage ? '100%' : i === stage
                ? Math.round(cfR01(tt, i === 0 ? 0 : ENDS[i - 1], ENDS[i]) * 100) + '%' : '0%',
            }}/>
          </span>
        ))}
      </div>
    </figure>
  );
}

window.CaseFilm = CaseFilm;
