// scene.jsx — UCAT Decision Making Venn animation
// Walks through the "neither A nor S" question step-by-step.

const T = {
  // [start, end] for each phase (seconds)
  intro:    [0.4, 2.5],     // question fades in, title visible
  neither:  [3.0, 5.5],     // highlight "neither" word, sticky note
  catLabel: [6.0, 8.8],     // F, A, S annotations appear next to categories
  drawF:    [9.4, 10.9],    // draw F circle
  drawA:    [10.9, 12.4],
  drawS:    [12.4, 13.9],
  centre:   [14.2, 17.5],   // line 1 → 10 in centre
  fa:       [17.7, 22.2],   // line 2 → 18-10=8
  as:       [22.4, 26.9],   // line 3 → 23-10=13
  fs:       [27.1, 31.6],   // line 4 → 11-10=1
  fonly:    [31.8, 37.5],   // line 5 → 36-19=17
  aonly:    [37.7, 43.4],   // line 6 → 52-31=21
  sonly:    [43.6, 49.3],   // line 7 → 39-24=15
  outside:  [49.5, 55.5],   // 150 - 85 = 65 outside
  reread:   [55.7, 59.5],   // re-highlight "neither" + dim A,S
  combine:  [59.7, 64.0],   // 17 + 65 = 82
  answer:   [64.2, 70.0],   // big D. 82 reveal, hold
  // total = 70 + 1.5 buffer
};

const COLOURS = {
  bg:       '#f8f8f5',
  paper:    '#fbfaf5',
  ink:      '#1A1A1A',
  inkSoft:  '#4a4a48',
  inkMute:  '#8a8a86',
  rule:     '#e8e6df',
  spice:    '#cf1f15',
  pencil:   '#2b2a26',  // graphite-ish for handwritten working
  highlightYellow: 'rgba(245, 215, 90, 0.42)',
  highlightSpice:  'rgba(207, 31, 21, 0.12)',
};

// ── Helpers ──────────────────────────────────────────────────

function inWindow(t, [a, b]) { return t >= a && t <= b; }
function afterWindow(t, [, b]) { return t > b; }

// 0 → 1 ramp over [start, end] then hold at 1
function rampIn(t, start, dur = 0.4, ease = Easing.easeOutCubic) {
  return ease(clamp((t - start) / dur, 0, 1));
}
function rampOut(t, start, dur = 0.4, ease = Easing.easeInCubic) {
  return 1 - ease(clamp((t - start) / dur, 0, 1));
}
// pulse: rises to 1, holds, falls back
function pulse(t, [a, b], rise = 0.4, fall = 0.6) {
  if (t < a) return 0;
  if (t > b) return 0;
  const r = clamp((t - a) / rise, 0, 1);
  const f = 1 - clamp((t - (b - fall)) / fall, 0, 1);
  return Math.min(r, f);
}

// ── Geometry for the Venn ────────────────────────────────────

const VENN = {
  cx: 1340, cy: 540, r: 195,
  // Three circle centres
  F: { cx: 1340, cy: 410 },
  A: { cx: 1230, cy: 605 },
  S: { cx: 1450, cy: 605 },
};

// Region label positions (rough centroids of each region)
const REGION = {
  centre: { x: 1340, y: 540 },          // F∩A∩S
  fa:     { x: 1235, y: 478 },           // F∩A only (excl S)
  fs:     { x: 1445, y: 478 },           // F∩S only (excl A)
  as:     { x: 1340, y: 645 },           // A∩S only (excl F)
  fOnly:  { x: 1340, y: 290 },          // F crescent
  aOnly:  { x: 1100, y: 690 },          // A crescent
  sOnly:  { x: 1582, y: 690 },          // S crescent
  outside:{ x: 1740, y: 870 },          // outside everything
};

// ── Header ───────────────────────────────────────────────────

function Header() {
  return (
    <div style={{
      position: 'absolute', left: 0, right: 0, top: 0, height: 78,
      borderBottom: `1px solid ${COLOURS.rule}`,
      display: 'flex', alignItems: 'center',
      padding: '0 56px',
      background: COLOURS.bg,
    }}>
      <div style={{
        fontFamily: 'Fraunces, serif', fontWeight: 500, fontSize: 26,
        color: COLOURS.ink, letterSpacing: '-0.01em',
      }}>
        theMSAG<span style={{ color: COLOURS.spice }}>.</span>
      </div>
      <div style={{
        marginLeft: 28, paddingLeft: 28,
        borderLeft: `1px solid ${COLOURS.rule}`,
        fontFamily: 'Plus Jakarta Sans, sans-serif',
        fontSize: 13, fontWeight: 600,
        textTransform: 'uppercase', letterSpacing: '0.18em',
        color: COLOURS.spice,
      }}>
        UCAT · Decision Making · Venn diagrams
      </div>
      <div style={{ flex: 1 }} />
      <div style={{
        fontFamily: 'Plus Jakarta Sans, sans-serif',
        fontSize: 13, fontWeight: 500,
        color: COLOURS.inkMute,
      }}>
        Worked example · 150 patients · 3 symptoms
      </div>
    </div>
  );
}

// ── Question panel (left side) ──────────────────────────────

function QLine({ children, activeRange, processedAfter, style = {} }) {
  const t = useTime();
  let state = 'idle';
  if (activeRange && inWindow(t, activeRange)) state = 'active';
  else if (processedAfter != null && t > processedAfter) state = 'processed';

  const opacity = state === 'active' ? 1 : state === 'processed' ? 0.42 : 0.78;
  const bg = state === 'active' ? COLOURS.highlightYellow : 'transparent';

  return (
    <div style={{
      opacity,
      background: bg,
      padding: '6px 10px',
      margin: '2px -10px',
      borderRadius: 4,
      transition: 'opacity 220ms ease, background 220ms ease',
      ...style,
    }}>
      {children}
    </div>
  );
}

// Annotation chip: little spice-coloured letter that pops in next to a category mention.
function Ann({ letter, appearAt }) {
  const t = useTime();
  const p = rampIn(t, appearAt, 0.35, Easing.easeOutBack);
  if (p <= 0) return null;
  return (
    <span style={{
      display: 'inline-block',
      marginLeft: 6, marginRight: 2,
      padding: '0 6px',
      fontFamily: 'Caveat, cursive',
      fontWeight: 500,
      fontSize: 28,
      color: COLOURS.spice,
      lineHeight: 1,
      transform: `scale(${0.6 + 0.4 * p}) rotate(${-6 + 4 * (1 - p)}deg)`,
      opacity: p,
      transformOrigin: 'left center',
      verticalAlign: 'middle',
    }}>
      {letter}
    </span>
  );
}

// Wavy red underline appearing under a span ("neither" / "nor")
function ScribbleUnderline({ at, dur = 0.55, width = '100%' }) {
  const t = useTime();
  const p = rampIn(t, at, dur, Easing.easeOutCubic);
  if (p <= 0) return null;
  return (
    <svg
      style={{
        position: 'absolute',
        left: 0, right: 0, bottom: -10,
        width: width, height: 14,
        pointerEvents: 'none',
        overflow: 'visible',
      }}
      viewBox="0 0 100 14" preserveAspectRatio="none"
    >
      <path
        d="M 1 7 Q 8 2, 16 7 T 32 7 T 48 7 T 64 7 T 80 7 T 99 7"
        fill="none"
        stroke={COLOURS.spice}
        strokeWidth="1.4"
        strokeLinecap="round"
        pathLength="1"
        strokeDasharray="1"
        strokeDashoffset={1 - p}
        vectorEffect="non-scaling-stroke"
      />
    </svg>
  );
}

function QuestionPanel() {
  const t = useTime();
  const intro = rampIn(t, T.intro[0], 0.7, Easing.easeOutCubic);
  const ty = (1 - intro) * 12;

  return (
    <div style={{
      position: 'absolute',
      left: 56, top: 110,
      width: 660, height: 940,
      background: COLOURS.bg,
      opacity: intro,
      transform: `translateY(${ty}px)`,
    }}>
      {/* Eyebrow */}
      <div style={{
        fontFamily: 'Plus Jakarta Sans, sans-serif',
        fontWeight: 600, fontSize: 12, letterSpacing: '0.18em',
        textTransform: 'uppercase', color: COLOURS.spice,
        marginBottom: 14,
      }}>
        Question 14 of 29
      </div>

      {/* Title */}
      <div style={{
        fontFamily: 'Fraunces, serif', fontWeight: 500,
        fontSize: 36, lineHeight: 1.15,
        color: COLOURS.ink, letterSpacing: '-0.015em',
        marginBottom: 22,
      }}>
        Stroke symptoms across <em>150&nbsp;patients</em>
      </div>

      {/* Stem */}
      <div style={{
        fontFamily: 'Plus Jakarta Sans, sans-serif',
        fontSize: 19, lineHeight: 1.55, color: COLOURS.inkSoft,
        marginBottom: 14,
      }}>
        A study reviewed <b style={{ color: COLOURS.ink }}>150</b> patients to record three symptoms of stroke:{' '}
        <span style={{ position: 'relative', whiteSpace: 'nowrap' }}>
          <em style={{ color: COLOURS.ink, fontStyle: 'normal', fontWeight: 600 }}>face drooping</em>
          <Ann letter="F" appearAt={T.catLabel[0]} />
        </span>
        ,{' '}
        <span style={{ position: 'relative', whiteSpace: 'nowrap' }}>
          <em style={{ color: COLOURS.ink, fontStyle: 'normal', fontWeight: 600 }}>arm weakness</em>
          <Ann letter="A" appearAt={T.catLabel[0] + 0.6} />
        </span>
        , and{' '}
        <span style={{ position: 'relative', whiteSpace: 'nowrap' }}>
          <em style={{ color: COLOURS.ink, fontStyle: 'normal', fontWeight: 600 }}>speech difficulties</em>
          <Ann letter="S" appearAt={T.catLabel[0] + 1.2} />
        </span>
        . The researchers found:
      </div>

      {/* Facts list */}
      <div style={{
        fontFamily: 'Plus Jakarta Sans, sans-serif',
        fontSize: 18, lineHeight: 1.45, color: COLOURS.ink,
        marginBottom: 16,
      }}>
        <QLine activeRange={T.centre} processedAfter={T.centre[1]}>
          <b>10</b> patients had face drooping <strong style={{ color: COLOURS.spice }}>AND</strong> arm weakness <strong style={{ color: COLOURS.spice }}>AND</strong> speech difficulties.
        </QLine>
        <QLine activeRange={T.fa} processedAfter={T.fa[1]}>
          <b>18</b> patients had face drooping and arm weakness.
        </QLine>
        <QLine activeRange={T.as} processedAfter={T.as[1]}>
          <b>23</b> patients had arm weakness and speech difficulties.
        </QLine>
        <QLine activeRange={T.fs} processedAfter={T.fs[1]}>
          <b>11</b> patients had face drooping and speech difficulties.
        </QLine>
        <QLine activeRange={T.fonly} processedAfter={T.fonly[1]}>
          <b>36</b> patients had face drooping in total.
        </QLine>
        <QLine activeRange={T.aonly} processedAfter={T.aonly[1]}>
          <b>52</b> patients had arm weakness in total.
        </QLine>
        <QLine activeRange={T.sonly} processedAfter={T.sonly[1]}>
          <b>39</b> patients had speech difficulties in total.
        </QLine>
        <QLine activeRange={T.outside} processedAfter={T.outside[1]}>
          <b>150</b> patients in total.
        </QLine>
      </div>

      {/* The actual question */}
      <QClosingQuestion />

      {/* Options */}
      <Options />
    </div>
  );
}

function QClosingQuestion() {
  const t = useTime();
  // Highlight "neither ... nor" in two windows: T.neither and T.reread
  const neitherActive = inWindow(t, T.neither) || inWindow(t, T.reread);

  return (
    <div style={{
      marginTop: 18,
      paddingTop: 18,
      borderTop: `1px solid ${COLOURS.rule}`,
      fontFamily: 'Fraunces, serif', fontWeight: 500,
      fontSize: 26, lineHeight: 1.3,
      color: COLOURS.ink,
      letterSpacing: '-0.01em',
      position: 'relative',
    }}>
      How many patients had{' '}
      <span style={{ position: 'relative', display: 'inline-block' }}>
        <span style={{
          color: neitherActive ? COLOURS.spice : COLOURS.ink,
          fontWeight: 500,
          fontStyle: 'italic',
          transition: 'color 220ms ease',
        }}>neither</span>
        <ScribbleUnderline at={T.neither[0] + 0.4} />
      </span>{' '}
      arm weakness{' '}
      <span style={{ position: 'relative', display: 'inline-block' }}>
        <span style={{
          color: neitherActive ? COLOURS.spice : COLOURS.ink,
          fontWeight: 500,
          fontStyle: 'italic',
          transition: 'color 220ms ease',
        }}>nor</span>
        <ScribbleUnderline at={T.neither[0] + 0.95} />
      </span>{' '}
      speech difficulties?

      {/* Sticky-note margin call-out, appears in T.neither, lingers, fades for reread */}
      <StickyNote />
    </div>
  );
}

function StickyNote() {
  const t = useTime();
  const inA = rampIn(t, T.neither[0] + 0.6, 0.45, Easing.easeOutBack);
  const outA = rampOut(t, T.catLabel[0] - 0.4, 0.4, Easing.easeInCubic);
  const opa = Math.max(0, Math.min(inA, outA));
  if (opa <= 0.001) return null;
  return (
    <div style={{
      position: 'absolute',
      right: -28, top: -4,
      transform: `rotate(3deg) scale(${0.85 + 0.15 * inA})`,
      opacity: opa,
      fontFamily: 'Caveat, cursive', fontWeight: 500,
      fontSize: 22, lineHeight: 1.15,
      color: COLOURS.spice,
      maxWidth: 170, textAlign: 'left',
      pointerEvents: 'none',
    }}>
      ↑ hold onto<br />this word
    </div>
  );
}

function Options() {
  const t = useTime();
  const reveal = rampIn(t, T.answer[0], 0.5, Easing.easeOutCubic);
  const opts = [
    { letter: 'A', value: '17' },
    { letter: 'B', value: '65' },
    { letter: 'C', value: '80' },
    { letter: 'D', value: '82' },
  ];
  return (
    <div style={{
      marginTop: 28,
      display: 'grid',
      gridTemplateColumns: '1fr 1fr',
      gap: 12,
    }}>
      {opts.map(o => {
        const isAns = o.letter === 'D';
        const isHi = isAns && reveal > 0;
        return (
          <div key={o.letter} style={{
            display: 'flex', alignItems: 'center', gap: 14,
            padding: '14px 18px',
            border: `1.5px solid ${isHi ? COLOURS.spice : COLOURS.rule}`,
            borderRadius: 8,
            background: isHi ? 'rgba(207,31,21,0.06)' : COLOURS.bg,
            transition: 'all 320ms ease',
            transform: isHi ? `scale(${1 + 0.02 * reveal})` : 'scale(1)',
          }}>
            <div style={{
              width: 30, height: 30, borderRadius: '50%',
              background: isHi ? COLOURS.spice : COLOURS.ink,
              color: '#fff',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              fontFamily: 'Plus Jakarta Sans, sans-serif',
              fontWeight: 700, fontSize: 14,
              transition: 'background 320ms ease',
            }}>{o.letter}</div>
            <div style={{
              fontFamily: 'Fraunces, serif', fontWeight: 500,
              fontSize: 24,
              color: isHi ? COLOURS.spice : COLOURS.ink,
              transition: 'color 320ms ease',
            }}>{o.value}</div>
            {isHi && (
              <div style={{
                marginLeft: 'auto',
                fontFamily: 'Caveat, cursive', fontSize: 26,
                color: COLOURS.spice,
                opacity: reveal,
              }}>
                ✓
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}

// ── Paper panel (right side) ─────────────────────────────────

function PaperPanel() {
  const t = useTime();
  return (
    <div style={{
      position: 'absolute',
      left: 740, top: 110, width: 1124, height: 940,
      background: COLOURS.paper,
      borderRadius: 4,
      boxShadow: '0 1px 1px rgba(0,0,0,.05), 0 14px 32px -16px rgba(0,0,0,.18)',
      overflow: 'hidden',
    }}>
      {/* faint horizontal rule lines for "paper" feel */}
      <PaperLines />
      <PaperHeader />
      <Venn />
      <RegionNumbers />
      <WorkingNotes />
      <FinalSum />
    </div>
  );
}

function PaperLines() {
  const lines = [];
  for (let y = 80; y < 940; y += 38) {
    lines.push(
      <div key={y} style={{
        position: 'absolute',
        left: 32, right: 32, top: y, height: 1,
        background: 'rgba(36, 92, 138, 0.06)',
      }} />
    );
  }
  return <>{lines}</>;
}

function PaperHeader() {
  return (
    <div style={{
      position: 'absolute', left: 36, top: 26,
      fontFamily: 'Caveat, cursive', fontWeight: 500,
      fontSize: 32, color: COLOURS.pencil,
      letterSpacing: '0.01em',
    }}>
      Working out
      <div style={{
        marginTop: 4, height: 1.5, width: 170,
        background: COLOURS.pencil,
      }} />
    </div>
  );
}

const PANEL_LEFT = 740;
const PANEL_TOP  = 110;

function L(p) {
  return { x: p.cx - PANEL_LEFT, y: p.cy - PANEL_TOP };
}
function LR(p) { // for region anchors which use {x, y}
  return { x: p.x - PANEL_LEFT, y: p.y - PANEL_TOP };
}

function Venn() {
  const t = useTime();
  const r = VENN.r;
  const C = 2 * Math.PI * r;

  const dp = (range) => clamp((t - range[0]) / (range[1] - range[0]), 0, 1);
  const pF = Easing.easeOutCubic(dp(T.drawF));
  const pA = Easing.easeOutCubic(dp(T.drawA));
  const pS = Easing.easeOutCubic(dp(T.drawS));

  const F = L(VENN.F);
  const A = L(VENN.A);
  const S = L(VENN.S);

  // Dim A & S after reread starts; brighten F-only highlight + outside highlight
  const dimAS = clamp((t - (T.reread[0] + 0.3)) / 0.9, 0, 1);
  // Highlight final regions (F-only crescent + outside) during combine/answer
  const hi = clamp((t - T.combine[0]) / 0.7, 0, 1);

  return (
    <svg
      style={{
        position: 'absolute', left: 0, top: 0,
        width: 1124, height: 940,
        pointerEvents: 'none', overflow: 'visible',
      }}
      viewBox="0 0 1124 940"
    >
      <defs>
        {/* Mask for F-only crescent: F circle minus A and S */}
        <mask id="m-fonly" maskUnits="userSpaceOnUse">
          <rect x={0} y={0} width={1124} height={940} fill="black" />
          <circle cx={F.x} cy={F.y} r={r} fill="white" />
          <circle cx={A.x} cy={A.y} r={r} fill="black" />
          <circle cx={S.x} cy={S.y} r={r} fill="black" />
        </mask>
        {/* Mask for outside region: everything except F∪A∪S */}
        <mask id="m-outside" maskUnits="userSpaceOnUse">
          <rect x={0} y={0} width={1124} height={940} fill="white" />
          <circle cx={F.x} cy={F.y} r={r} fill="black" />
          <circle cx={A.x} cy={A.y} r={r} fill="black" />
          <circle cx={S.x} cy={S.y} r={r} fill="black" />
        </mask>
      </defs>

      {/* Final highlights — drawn under outlines */}
      <rect x={0} y={0} width={1124} height={940}
        fill={COLOURS.spice}
        opacity={0.10 * hi}
        mask="url(#m-outside)"
      />
      <circle cx={F.x} cy={F.y} r={r}
        fill={COLOURS.spice}
        opacity={0.14 * hi}
        mask="url(#m-fonly)"
      />

      {/* F circle */}
      <circle cx={F.x} cy={F.y} r={r}
        fill="none"
        stroke={COLOURS.ink}
        strokeWidth={2.4}
        strokeLinecap="round"
        strokeDasharray={C}
        strokeDashoffset={(1 - pF) * C}
        opacity={1}
      />
      {/* A circle */}
      <circle cx={A.x} cy={A.y} r={r}
        fill="none"
        stroke={COLOURS.ink}
        strokeWidth={2.4}
        strokeLinecap="round"
        strokeDasharray={C}
        strokeDashoffset={(1 - pA) * C}
        opacity={1 - 0.55 * dimAS}
      />
      {/* S circle */}
      <circle cx={S.x} cy={S.y} r={r}
        fill="none"
        stroke={COLOURS.ink}
        strokeWidth={2.4}
        strokeLinecap="round"
        strokeDasharray={C}
        strokeDashoffset={(1 - pS) * C}
        opacity={1 - 0.55 * dimAS}
      />

      {/* Cross-out marks on A and S during reread */}
      {dimAS > 0 && (
        <g opacity={dimAS}>
          <CrossOut center={A} r={r * 0.82} />
          <CrossOut center={S} r={r * 0.82} />
        </g>
      )}

      {/* Circle labels (F, A, S) — appear shortly after each circle */}
      <CircleLabel
        text="F"
        at={{ x: F.x, y: F.y - r - 20 }}
        appearAt={T.drawF[1] - 0.2}
        dim={false}
      />
      <CircleLabel
        text="A"
        at={{ x: A.x - r - 22, y: A.y + 8 }}
        appearAt={T.drawA[1] - 0.2}
        dim={dimAS}
      />
      <CircleLabel
        text="S"
        at={{ x: S.x + r + 22, y: S.y + 8 }}
        appearAt={T.drawS[1] - 0.2}
        dim={dimAS}
      />
    </svg>
  );
}

function CrossOut({ center, r }) {
  // Two diagonal strokes through the circle area
  const x1 = center.x - r * 0.6, y1 = center.y - r * 0.6;
  const x2 = center.x + r * 0.6, y2 = center.y + r * 0.6;
  return (
    <>
      <line x1={x1} y1={y1} x2={x2} y2={y2}
        stroke={COLOURS.spice} strokeWidth={3} strokeLinecap="round" opacity={0.55} />
      <line x1={x1} y1={y2} x2={x2} y2={y1}
        stroke={COLOURS.spice} strokeWidth={3} strokeLinecap="round" opacity={0.55} />
    </>
  );
}

function CircleLabel({ text, at, appearAt, dim }) {
  const t = useTime();
  const p = rampIn(t, appearAt, 0.4, Easing.easeOutBack);
  if (p <= 0) return null;
  return (
    <text
      x={at.x} y={at.y}
      textAnchor="middle"
      dominantBaseline="middle"
      fontFamily="Caveat, cursive"
      fontWeight="500"
      fontSize={42}
      fill={COLOURS.pencil}
      opacity={p * (1 - 0.5 * (dim || 0))}
    >
      {text}
    </text>
  );
}

// Numbers written into each region of the Venn.
function RegionNumbers() {
  return (
    <div style={{
      position: 'absolute', left: 0, top: 0,
      width: 1124, height: 940,
      pointerEvents: 'none',
    }}>
      <RegionNum value="10" at={LR(REGION.centre)}  appearAt={T.centre[0] + 1.4} highlight={false} />
      <RegionNum value="8"  at={LR(REGION.fa)}       appearAt={T.fa[0] + 2.2} />
      <RegionNum value="13" at={LR(REGION.as)}       appearAt={T.as[0] + 2.2} />
      <RegionNum value="1"  at={LR(REGION.fs)}       appearAt={T.fs[0] + 2.2} />
      <RegionNum value="17" at={LR(REGION.fOnly)}    appearAt={T.fonly[0] + 3.0} keyHi />
      <RegionNum value="21" at={LR(REGION.aOnly)}    appearAt={T.aonly[0] + 3.0} dimAfter={T.reread[0]} />
      <RegionNum value="15" at={LR(REGION.sOnly)}    appearAt={T.sonly[0] + 3.0} dimAfter={T.reread[0]} />
      <RegionNum value="65" at={LR(REGION.outside)}  appearAt={T.outside[0] + 3.0} keyHi />
    </div>
  );
}

function RegionNum({ value, at, appearAt, keyHi = false, dimAfter = null }) {
  const t = useTime();
  const p = rampIn(t, appearAt, 0.45, Easing.easeOutBack);
  if (p <= 0) return null;

  // dim post-reread for non-key numbers
  let dim = 0;
  if (dimAfter != null) dim = clamp((t - dimAfter) / 0.8, 0, 1);

  // key numbers (17 and 65) get a slight spice glow during combine/answer
  const glow = keyHi ? clamp((t - T.combine[0]) / 0.6, 0, 1) : 0;

  return (
    <div style={{
      position: 'absolute',
      left: at.x, top: at.y,
      transform: `translate(-50%, -50%) scale(${0.6 + 0.4 * p})`,
      fontFamily: 'Caveat, cursive', fontWeight: 500,
      fontSize: 44, lineHeight: 1,
      color: glow > 0 ? COLOURS.spice : COLOURS.pencil,
      opacity: p * (1 - 0.55 * dim),
      transition: 'color 320ms ease',
      textShadow: glow > 0 ? `0 0 ${10 * glow}px rgba(207,31,21,${0.3 * glow})` : 'none',
    }}>
      {value}
    </div>
  );
}

// Working notes — short Caveat snippets that appear next to the active step,
// then fade as the next step takes over.
function WorkingNotes() {
  const notes = [
    // each: { text, at:{x,y}, in, out, anchor }
    { id: 'all3', text: 'all three →\ncentre', at: { x: 880, y: 360 },
      inAt: T.centre[0] + 0.2, outAt: T.fa[0] + 0.4 },
    { id: 'fa',  text: '"and" → could include S\n18 − 10 = 8',
      at: { x: 880, y: 460 },
      inAt: T.fa[0] + 0.4, outAt: T.as[0] + 0.4 },
    { id: 'as',  text: '23 − 10 = 13',
      at: { x: 880, y: 720 },
      inAt: T.as[0] + 0.4, outAt: T.fs[0] + 0.4 },
    { id: 'fs',  text: '11 − 10 = 1',
      at: { x: 1670, y: 470 },
      inAt: T.fs[0] + 0.4, outAt: T.fonly[0] + 0.4 },
    { id: 'fonly', text: 'F total = 36\n8 + 1 + 10 = 19\n36 − 19 = 17',
      at: { x: 880, y: 280 },
      inAt: T.fonly[0] + 0.4, outAt: T.aonly[0] + 0.4 },
    { id: 'aonly', text: 'A total = 52\n8 + 13 + 10 = 31\n52 − 31 = 21',
      at: { x: 870, y: 760 },
      inAt: T.aonly[0] + 0.4, outAt: T.sonly[0] + 0.4 },
    { id: 'sonly', text: 'S total = 39\n13 + 1 + 10 = 24\n39 − 24 = 15',
      at: { x: 1660, y: 750 },
      inAt: T.sonly[0] + 0.4, outAt: T.outside[0] + 0.4 },
    { id: 'outside', text: 'inside = 10+8+13+1\n+17+21+15 = 85\nouters: 150 − 85 = 65',
      at: { x: 1670, y: 870 },
      inAt: T.outside[0] + 0.4, outAt: T.reread[0] + 0.3 },
    { id: 'reread', text: 'neither A nor S →\nignore A and S\nlook at what\'s left',
      at: { x: 880, y: 360 },
      inAt: T.reread[0] + 0.4, outAt: T.combine[0] + 0.3 },
  ];
  return (
    <>
      {notes.map(n => <Note key={n.id} {...n} />)}
    </>
  );
}

function Note({ text, at, inAt, outAt }) {
  const t = useTime();
  const inP = rampIn(t, inAt, 0.45, Easing.easeOutCubic);
  const outP = rampOut(t, outAt, 0.4, Easing.easeInCubic);
  const opa = Math.max(0, Math.min(inP, outP));
  if (opa <= 0.001) return null;

  // Convert from absolute stage coords to panel-local
  const lx = at.x - PANEL_LEFT;
  const ly = at.y - PANEL_TOP;

  return (
    <div style={{
      position: 'absolute',
      left: lx, top: ly,
      transform: `translate(-50%, -50%) translateY(${(1 - inP) * 8}px)`,
      opacity: opa,
      fontFamily: 'Caveat, cursive', fontWeight: 500,
      fontSize: 26, lineHeight: 1.18,
      color: COLOURS.pencil,
      whiteSpace: 'pre',
      textAlign: 'center',
      pointerEvents: 'none',
      maxWidth: 320,
    }}>
      {text}
    </div>
  );
}

function FinalSum() {
  const t = useTime();
  const inP = rampIn(t, T.combine[0] + 0.6, 0.6, Easing.easeOutBack);
  if (inP <= 0) return null;

  // Position: below the Venn, centered in panel
  const x = VENN.cx - PANEL_LEFT;
  const y = 870;

  // Show in two "stages": first "17 + 65", then "= 82"
  const showEq = rampIn(t, T.combine[0] + 1.6, 0.55, Easing.easeOutBack);

  return (
    <div style={{
      position: 'absolute', left: x, top: y,
      transform: `translate(-50%, -50%) scale(${0.85 + 0.15 * inP})`,
      opacity: inP,
      display: 'flex', alignItems: 'center', gap: 16,
      fontFamily: 'Fraunces, serif', fontWeight: 500,
      fontSize: 64, lineHeight: 1,
      color: COLOURS.ink,
      letterSpacing: '-0.02em',
      whiteSpace: 'nowrap',
    }}>
      <span style={{ color: COLOURS.spice }}>17</span>
      <span style={{ color: COLOURS.inkSoft, fontSize: 48 }}>+</span>
      <span style={{ color: COLOURS.spice }}>65</span>
      <span style={{ color: COLOURS.inkSoft, opacity: showEq, fontSize: 48 }}>=</span>
      <span style={{
        color: COLOURS.spice,
        opacity: showEq,
        fontWeight: 500,
        fontSize: 84,
        fontStyle: 'italic',
        transform: `scale(${0.7 + 0.3 * showEq})`,
        display: 'inline-block',
        transformOrigin: 'left center',
      }}>
        82
      </span>
    </div>
  );
}

// ── Root scene ──────────────────────────────────────────────

function VennScene() {
  return (
    <>
      <Header />
      <QuestionPanel />
      <PaperPanel />
      {/* Centre arrow connecting "10 patients had F AND A AND S" line to centre of Venn */}
      <CentreArrow />
    </>
  );
}

// Arrow drawn from the active "10" line to the Venn centre, only during T.centre
function CentreArrow() {
  const t = useTime();
  if (!inWindow(t, [T.centre[0] + 0.3, T.centre[1]])) return null;
  const p = rampIn(t, T.centre[0] + 0.3, 0.6, Easing.easeOutCubic);
  // From (around 700, 530) — right edge of question panel, near line 1 — to (1340, 540) Venn centre
  return (
    <svg style={{
      position: 'absolute', left: 0, top: 0,
      width: 1920, height: 1080,
      pointerEvents: 'none', overflow: 'visible',
    }}>
      <defs>
        <marker id="arrowhead" viewBox="0 0 10 10" refX="8" refY="5"
          markerWidth="8" markerHeight="8" orient="auto-start-reverse">
          <path d="M 0 0 L 10 5 L 0 10 z" fill={COLOURS.spice} />
        </marker>
      </defs>
      <path
        d={`M 700 472 Q 950 430, 1300 530`}
        fill="none"
        stroke={COLOURS.spice}
        strokeWidth={2}
        strokeLinecap="round"
        pathLength="1"
        strokeDasharray="1"
        strokeDashoffset={1 - p}
        markerEnd="url(#arrowhead)"
        opacity={0.7}
      />
    </svg>
  );
}

window.VennScene = VennScene;
window.SCENE_DURATION = 71.5;
