// whiteboard-scene.jsx
// Living whiteboard: ideas, connections, structure — looping.

const LOOP = 18; // seconds

// ── Palette ──────────────────────────────────────────────────────────────
const C = {
  board:   '#f4f1ea',   // warm off-white whiteboard
  boardEdge:'#e8e3d6',
  ink:     '#2b2a26',   // graphite
  inkSoft: 'rgba(43,42,38,0.55)',
  inkFaint:'rgba(43,42,38,0.22)',
  red:     '#c14e3a',
  blue:    '#3a6ea8',
  green:   '#5b8a4a',
  yellow:  '#f3d56b',
  peach:   '#f0b48c',
  mint:    '#bfd8b8',
  lilac:   '#c9b8d6',
  paper:   '#fdf9ee',
};

// Cursor colors per collaborator
const COLLABS = [
  { name: 'Maya',  color: '#3a6ea8' },
  { name: 'Theo',  color: '#c14e3a' },
  { name: 'Iris',  color: '#5b8a4a' },
  { name: 'Jun',   color: '#9764b8' },
];

// ── Helpers ──────────────────────────────────────────────────────────────
const lerp = (a, b, t) => a + (b - a) * t;
const seeded = (i) => {
  // deterministic pseudo-random
  const x = Math.sin(i * 9301 + 49297) * 233280;
  return x - Math.floor(x);
};
const mod = (a, n) => ((a % n) + n) % n;

// Cycle 0..1 across the loop
function loopProgress(t) { return mod(t, LOOP) / LOOP; }

// Smooth fade window: returns 0..1..0 across [start..end] with ramp at edges
function lifeWindow(t, start, fadeIn, hold, fadeOut) {
  const dt = t - start;
  if (dt < 0) return 0;
  if (dt < fadeIn) return Easing.easeOutCubic(dt / fadeIn);
  if (dt < fadeIn + hold) return 1;
  if (dt < fadeIn + hold + fadeOut) {
    const u = (dt - fadeIn - hold) / fadeOut;
    return 1 - Easing.easeInCubic(u);
  }
  return 0;
}

// Looping life window — wraps: appears at `start` (mod LOOP), lives `life` seconds,
// returns fade 0..1
function loopLife(t, start, fadeIn, hold, fadeOut) {
  const total = fadeIn + hold + fadeOut;
  const tt = mod(t - start, LOOP);
  if (tt > total) return 0;
  if (tt < fadeIn) return Easing.easeOutCubic(tt / fadeIn);
  if (tt < fadeIn + hold) return 1;
  const u = (tt - fadeIn - hold) / fadeOut;
  return 1 - Easing.easeInCubic(u);
}

// Path-draw progress: 0..1 stroke reveal, then 0..1 erase from start
function loopDraw(t, start, drawDur, hold, eraseDur) {
  const tt = mod(t - start, LOOP);
  const total = drawDur + hold + eraseDur;
  if (tt < 0 || tt > total) return { dash: 0, opacity: 0 };
  if (tt < drawDur) {
    return { dash: Easing.easeInOutCubic(tt / drawDur), opacity: 1 };
  }
  if (tt < drawDur + hold) return { dash: 1, opacity: 1 };
  const u = (tt - drawDur - hold) / eraseDur;
  return { dash: 1, opacity: 1 - Easing.easeInCubic(u) };
}

// ── Hand-drawn rough filter (used on most strokes) ───────────────────────
function RoughDefs() {
  return (
    <defs>
      <filter id="rough" x="-5%" y="-5%" width="110%" height="110%">
        <feTurbulence type="fractalNoise" baseFrequency="0.022" numOctaves="2" seed="3" />
        <feDisplacementMap in="SourceGraphic" scale="1.6" />
      </filter>
      <filter id="roughHard" x="-5%" y="-5%" width="110%" height="110%">
        <feTurbulence type="fractalNoise" baseFrequency="0.04" numOctaves="2" seed="7" />
        <feDisplacementMap in="SourceGraphic" scale="2.4" />
      </filter>
      <filter id="paperShadow" x="-20%" y="-20%" width="140%" height="140%">
        <feGaussianBlur in="SourceAlpha" stdDeviation="2" />
        <feOffset dx="1" dy="2" result="off" />
        <feComponentTransfer><feFuncA type="linear" slope="0.18"/></feComponentTransfer>
        <feMerge>
          <feMergeNode/>
          <feMergeNode in="SourceGraphic"/>
        </feMerge>
      </filter>
      <filter id="grain">
        <feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" stitchTiles="stitch"/>
        <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0   0 0 0 0.06 0"/>
      </filter>
      <pattern id="dotgrid" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
        <circle cx="1" cy="1" r="0.7" fill="rgba(43,42,38,0.08)"/>
      </pattern>
    </defs>
  );
}

// ── Sticky note ──────────────────────────────────────────────────────────
function StickyNote({ x, y, w = 150, h = 110, rot = 0, color = C.yellow, text, sub, opacity = 1, scale = 1, fontSize = 18, font = 'handwriting' }) {
  const fontFamily = font === 'handwriting'
    ? '"Caveat", "Bradley Hand", cursive'
    : '"Helvetica Neue", Helvetica, Arial, sans-serif';
  return (
    <g transform={`translate(${x} ${y}) rotate(${rot}) scale(${scale})`} opacity={opacity} style={{ transformOrigin: 'center', transformBox: 'fill-box' }}>
      {/* Subtle drop shadow */}
      <rect x={-w/2 + 1} y={-h/2 + 3} width={w} height={h} fill="rgba(0,0,0,0.12)" rx="2" filter="url(#rough)" />
      {/* Note body */}
      <rect x={-w/2} y={-h/2} width={w} height={h} fill={color} rx="1" filter="url(#rough)" />
      {/* Tape strip */}
      <rect x={-22} y={-h/2 - 6} width={44} height={14} fill="rgba(255,255,255,0.5)" stroke="rgba(0,0,0,0.06)" strokeWidth="0.5" filter="url(#rough)" />
      {/* Text */}
      <text x="0" y={sub ? -6 : 6} fontFamily={fontFamily} fontSize={fontSize} fill={C.ink} textAnchor="middle" style={{ fontWeight: font === 'handwriting' ? 600 : 500 }}>
        {text}
      </text>
      {sub && (
        <text x="0" y={18} fontFamily={fontFamily} fontSize={fontSize - 5} fill={C.inkSoft} textAnchor="middle">
          {sub}
        </text>
      )}
    </g>
  );
}

// ── Ink node (circle/oval with handwritten label) ───────────────────────
function InkNode({ x, y, r = 32, label, color = C.ink, fill = 'transparent', opacity = 1, drawProgress = 1, fontSize = 14 }) {
  const circumference = 2 * Math.PI * r;
  return (
    <g transform={`translate(${x} ${y})`} opacity={opacity}>
      {fill !== 'transparent' && (
        <ellipse cx="0" cy="0" rx={r} ry={r * 0.85} fill={fill} opacity={drawProgress} filter="url(#rough)" />
      )}
      <ellipse
        cx="0" cy="0" rx={r} ry={r * 0.85}
        fill="none" stroke={color} strokeWidth="1.6"
        strokeDasharray={circumference}
        strokeDashoffset={circumference * (1 - drawProgress)}
        filter="url(#rough)"
        strokeLinecap="round"
      />
      {label && drawProgress > 0.7 && (
        <text
          x="0" y={4}
          fontFamily='"Caveat", "Bradley Hand", cursive'
          fontSize={fontSize}
          fill={C.ink}
          textAnchor="middle"
          opacity={(drawProgress - 0.7) / 0.3}
          fontWeight="600"
        >
          {label}
        </text>
      )}
    </g>
  );
}

// ── Hand-drawn arrow / line ──────────────────────────────────────────────
// Uses a slight curve and stroke-dash reveal
function InkLine({ x1, y1, x2, y2, curve = 0, color = C.ink, width = 1.6, opacity = 1, dash = 1, arrow = false, dashed = false, length: lengthOverride }) {
  // Control point perpendicular to midline for slight curve
  const mx = (x1 + x2) / 2;
  const my = (y1 + y2) / 2;
  const dx = x2 - x1;
  const dy = y2 - y1;
  const len = lengthOverride || Math.hypot(dx, dy);
  const nx = -dy / (len || 1);
  const ny = dx / (len || 1);
  const cx = mx + nx * curve;
  const cy = my + ny * curve;
  // Approx path length for dash
  const pathLen = len * 1.05 + Math.abs(curve) * 0.5;

  const arrowSize = 9;
  const ang = Math.atan2(y2 - cy, x2 - cx);
  const ax1 = x2 - Math.cos(ang - 0.5) * arrowSize;
  const ay1 = y2 - Math.sin(ang - 0.5) * arrowSize;
  const ax2 = x2 - Math.cos(ang + 0.5) * arrowSize;
  const ay2 = y2 - Math.sin(ang + 0.5) * arrowSize;

  return (
    <g opacity={opacity}>
      <path
        d={`M ${x1} ${y1} Q ${cx} ${cy} ${x2} ${y2}`}
        fill="none"
        stroke={color}
        strokeWidth={width}
        strokeLinecap="round"
        strokeDasharray={dashed ? '5 4' : pathLen}
        strokeDashoffset={dashed ? 0 : pathLen * (1 - dash)}
        filter="url(#rough)"
      />
      {arrow && dash > 0.92 && (
        <path
          d={`M ${ax1} ${ay1} L ${x2} ${y2} L ${ax2} ${ay2}`}
          fill="none"
          stroke={color}
          strokeWidth={width}
          strokeLinecap="round"
          strokeLinejoin="round"
          opacity={(dash - 0.92) / 0.08}
          filter="url(#rough)"
        />
      )}
    </g>
  );
}

// ── Scribble (a small loopy doodle) ──────────────────────────────────────
function Scribble({ x, y, size = 30, opacity = 1, drawProgress = 1, color = C.inkSoft, seed = 0 }) {
  // Build a wandering polyline
  const pts = [];
  for (let i = 0; i < 24; i++) {
    const a = i / 23;
    const r = (Math.sin(seed + a * 12) * 0.4 + 0.6) * size;
    const theta = a * Math.PI * 3 + seed;
    pts.push([Math.cos(theta) * r * 0.9, Math.sin(theta) * r * 0.6]);
  }
  const d = pts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p[0]} ${p[1]}`).join(' ');
  const totalLen = size * 8;
  return (
    <g transform={`translate(${x} ${y})`} opacity={opacity}>
      <path
        d={d}
        fill="none"
        stroke={color}
        strokeWidth="1.2"
        strokeLinecap="round"
        strokeDasharray={totalLen}
        strokeDashoffset={totalLen * (1 - drawProgress)}
        filter="url(#rough)"
      />
    </g>
  );
}

// ── Cursor (collaborator pointer) ────────────────────────────────────────
function Cursor({ x, y, color, name, opacity = 1 }) {
  return (
    <g transform={`translate(${x} ${y})`} opacity={opacity} style={{ pointerEvents: 'none' }}>
      <path
        d="M 0 0 L 0 16 L 4.5 12.5 L 7 18 L 9.5 17 L 7 11.5 L 12 11 Z"
        fill={color} stroke="#fff" strokeWidth="1" strokeLinejoin="round"
      />
      <g transform="translate(14 16)">
        <rect x="-2" y="-10" width={name.length * 6.5 + 8} height="14" rx="2" fill={color} />
        <text x="2" y="0" fontSize="10" fill="#fff" fontFamily='"Helvetica Neue", Helvetica, sans-serif' fontWeight="600">
          {name}
        </text>
      </g>
    </g>
  );
}

// ─────────────────────────────────────────────────────────────────────────
// SCENE — choreographed timeline across LOOP seconds
// Phases:
//   0–4   scatter: notes pop on, single ideas
//   4–8   connect: lines draw, cursors active
//   8–12  reorganize: notes drift to cluster, lines re-route
//   12–16 structure: clean tree, central concept
//   16–18 dissolve: fade to faint traces, restart
// ─────────────────────────────────────────────────────────────────────────

// Notes — each has scatter pos (start) and structure pos (resolved)
const NOTES = [
  // Idea cluster: PROBLEM
  { id: 'p1', text: 'onboarding',   sub: 'too long',     color: C.peach,  s: [200, 160], r: [430, 195], rotS: -6, rotR: -3, w: 140, h: 90, born: 0.2 },
  { id: 'p2', text: 'unclear value', sub: '',            color: C.peach,  s: [410, 110], r: [690, 195], rotS: 4,  rotR: 2,  w: 150, h: 80, born: 0.7 },
  { id: 'p3', text: 'churn',        sub: 'week 2',       color: C.peach,  s: [120, 320], r: [560, 195], rotS: -3, rotR: 0,  w: 120, h: 80, born: 1.3 },

  // Idea cluster: USERS — cluster at (270,360), notes around it
  { id: 'u1', text: 'new user',     sub: '',             color: C.mint,   s: [780, 200], r: [160, 470], rotS: 5,  rotR: -2, w: 130, h: 80, born: 1.8 },
  { id: 'u2', text: 'power user',   sub: '',             color: C.mint,   s: [930, 360], r: [310, 470], rotS: -4, rotR: 1,  w: 140, h: 80, born: 2.3 },
  { id: 'u3', text: 'team admin',   sub: '',             color: C.mint,   s: [870, 540], r: [160, 570], rotS: 3,  rotR: -1, w: 140, h: 80, born: 2.9 },

  // Idea cluster: SOLUTIONS — cluster lives at (1080,200), notes flank below
  { id: 's1', text: 'guided tour',  sub: 'interactive',  color: C.yellow, s: [140, 540], r: [950, 350],  rotS: -5, rotR: 2,  w: 150, h: 90, born: 3.2 },
  { id: 's2', text: 'templates',    sub: '',             color: C.yellow, s: [310, 620], r: [1110, 360], rotS: 6,  rotR: -2, w: 130, h: 80, born: 3.7 },
  { id: 's3', text: 'AI assist',    sub: 'in-context',   color: C.yellow, s: [620, 600], r: [950, 480],  rotS: -2, rotR: 1,  w: 140, h: 90, born: 4.2 },
  { id: 's4', text: 'progress',     sub: 'milestones',   color: C.yellow, s: [780, 690], r: [1110, 490], rotS: 4,  rotR: -1, w: 140, h: 80, born: 4.6 },
];

// Cluster-label nodes (inked circles) that emerge during structure phase
const CLUSTERS = [
  { id: 'cP', label: 'PROBLEM',  x: 560, y: 100,  color: C.red,   r: 50, born: 8.5 },
  { id: 'cU', label: 'USERS',    x: 270, y: 360,  color: C.blue,  r: 50, born: 9.0 },
  { id: 'cS', label: 'SOLUTION', x: 1080, y: 200, color: C.green, r: 55, born: 9.5 },
  { id: 'cC', label: '',         x: 600, y: 400,  color: C.ink,   r: 80, born: 10.5 },
];

// Early-phase scattered connections (ephemeral, replaced)
const EARLY_LINKS = [
  { from: 'p1', to: 'u1', born: 4.2, life: 3.5, curve: -30 },
  { from: 'p2', to: 'p3', born: 4.6, life: 3.0, curve: 25  },
  { from: 'u2', to: 's1', born: 5.0, life: 3.2, curve: -40 },
  { from: 'p3', to: 's2', born: 5.4, life: 2.8, curve: 30  },
  { from: 'u3', to: 's3', born: 5.8, life: 3.0, curve: -20 },
  { from: 'u1', to: 'p2', born: 6.2, life: 2.5, curve: 35  },
  { from: 's4', to: 'u2', born: 6.6, life: 2.2, curve: -25 },
];

// Structure-phase connections (cluster-centric, persist longer)
const STRUCT_LINKS = [
  { from: 'cP', toNote: 'p1', born: 9.8, curve: -20, color: C.red },
  { from: 'cP', toNote: 'p2', born: 10.0, curve: 15, color: C.red },
  { from: 'cP', toNote: 'p3', born: 10.2, curve: -10, color: C.red },
  { from: 'cU', toNote: 'u1', born: 10.4, curve: 18, color: C.blue },
  { from: 'cU', toNote: 'u2', born: 10.6, curve: -12, color: C.blue },
  { from: 'cU', toNote: 'u3', born: 10.8, curve: 8, color: C.blue },
  { from: 'cS', toNote: 's1', born: 11.0, curve: -10, color: C.green },
  { from: 'cS', toNote: 's2', born: 11.2, curve: 12, color: C.green },
  { from: 'cS', toNote: 's3', born: 11.4, curve: -8, color: C.green },
  { from: 'cS', toNote: 's4', born: 11.6, curve: 10, color: C.green },
  // Cluster-to-core arrows — endpoints stop at the edge of the central oval (drawn in the JSX)
  { fromCluster: 'cP', toCluster: 'cC', born: 12.6, curve: 18, color: C.ink, arrow: true, width: 2 },
  { fromCluster: 'cU', toCluster: 'cC', born: 12.9, curve: -16, color: C.ink, arrow: true, width: 2 },
  { fromCluster: 'cS', toCluster: 'cC', born: 13.2, curve: 16, color: C.ink, arrow: true, width: 2 },
];

// Note positions are interpolated: scatter (0–4) → still scattered (4–8) →
// drift to resolved (8–11) → settled (11–17) → fade out (17–18)
function noteState(note, t) {
  const driftStart = 8 + seeded(note.id.charCodeAt(0) + note.id.charCodeAt(1)) * 0.8;
  const driftEnd = driftStart + 2.0;
  let p; // position interp 0..1 from scatter to resolved
  if (t < driftStart) p = 0;
  else if (t < driftEnd) p = Easing.easeInOutCubic((t - driftStart) / (driftEnd - driftStart));
  else p = 1;

  const x = lerp(note.s[0], note.r[0], p);
  const y = lerp(note.s[1], note.r[1], p);
  const rot = lerp(note.rotS, note.rotR, p);

  // Birth fade: born → born+0.6
  let opacity = 0;
  if (t >= note.born) {
    const a = Math.min(1, (t - note.born) / 0.6);
    opacity = Easing.easeOutCubic(a);
  }
  // Dissolve at end of loop: 16.8 → 17.8
  if (t > 16.8) {
    const f = (t - 16.8) / 1.0;
    opacity *= 1 - Easing.easeInCubic(Math.min(1, f));
  }

  // Subtle breathing wobble during structure phase
  const wob = p > 0.5 ? Math.sin(t * 1.2 + seeded(note.id.length) * 6) * 0.4 : 0;

  // Pop scale on entry
  let scale = 1;
  const since = t - note.born;
  if (since >= 0 && since < 0.6) {
    scale = 0.6 + 0.4 * Easing.easeOutBack(since / 0.6);
  }

  return { x, y, rot: rot + wob, opacity, scale, resolved: p };
}

// Get current point of a note (for drawing lines TO it)
function noteAnchor(note, t) {
  const s = noteState(note, t);
  return { x: s.x, y: s.y, opacity: s.opacity };
}

const NOTE_BY_ID = Object.fromEntries(NOTES.map(n => [n.id, n]));
const CLUSTER_BY_ID = Object.fromEntries(CLUSTERS.map(c => [c.id, c]));

// ── Cursor paths — collaborators wander, point, leave ─────────────────────
// Each path is a list of [time, x, y] keyframes; cursors loop their own little routes.
const CURSOR_PATHS = [
  // Maya (blue) — leads the connections in phase 2
  { idx: 0, keyframes: [
    [0.0, 100, 800], [0.6, 200, 160], [1.5, 410, 110],
    [2.5, 120, 320], [3.8, 780, 200], [5.0, 200, 200],
    [6.0, 600, 250], [7.5, 950, 290], [9.5, 600, 380],
    [11.0, 880, 380], [13.0, 600, 380], [14.5, 100, 800],
    [17.0, 100, 800], [18.0, 100, 800],
  ]},
  // Theo (red) — sticks ideas in phase 1
  { idx: 1, keyframes: [
    [0.0, 1300, 800], [0.3, 410, 110], [1.1, 930, 360],
    [2.0, 870, 540], [3.0, 310, 620], [4.5, 1100, 700],
    [6.5, 720, 220], [8.5, 270, 360], [10.5, 270, 360],
    [13.0, 950, 290], [15.0, 1300, 800], [18.0, 1300, 800],
  ]},
  // Iris (green) — contributes solutions
  { idx: 2, keyframes: [
    [0.0, 800, 900], [1.0, 800, 900], [3.0, 140, 540],
    [4.0, 620, 600], [5.5, 780, 690], [7.5, 880, 530],
    [9.5, 1020, 530], [11.5, 950, 290], [13.5, 600, 380],
    [16.0, 800, 900], [18.0, 800, 900],
  ]},
];

function cursorPos(path, t) {
  const kf = path.keyframes;
  for (let i = 0; i < kf.length - 1; i++) {
    if (t >= kf[i][0] && t <= kf[i + 1][0]) {
      const span = kf[i + 1][0] - kf[i][0];
      const local = span === 0 ? 0 : (t - kf[i][0]) / span;
      const e = Easing.easeInOutCubic(local);
      return {
        x: lerp(kf[i][1], kf[i + 1][1], e),
        y: lerp(kf[i][2], kf[i + 1][2], e),
      };
    }
  }
  return { x: kf[kf.length - 1][1], y: kf[kf.length - 1][2] };
}

// Visibility for cursor: hide when offstage (x<60 or x>1200, y>820)
function cursorVisible(p) {
  if (p.y > 820 || p.y < 50) return 0;
  if (p.x < 60 || p.x > 1200) return 0;
  return 1;
}

// ── Background marks: faint dots, marker stains, prior-meeting traces ────
function BoardBackground({ t }) {
  // Header lettering & frame
  return (
    <g>
      {/* Subtle dot grid */}
      <rect x="0" y="0" width="1280" height="800" fill="url(#dotgrid)" />
      {/* Faint old marker traces (always present, very low opacity) */}
      <g opacity="0.18">
        <path d="M 60 60 Q 200 50 360 60" stroke={C.inkFaint} strokeWidth="1" fill="none" filter="url(#rough)" />
        <path d="M 1100 80 L 1200 80" stroke={C.inkFaint} strokeWidth="1" fill="none" filter="url(#rough)" />
        <circle cx="1150" cy="730" r="22" stroke={C.inkFaint} strokeWidth="1" fill="none" filter="url(#rough)" />
      </g>
      {/* Top-left header: meeting name, hand-written */}
      <text x="60" y="50" fontFamily='"Caveat", "Bradley Hand", cursive' fontSize="26" fill={C.inkSoft} fontWeight="600">
        product workshop · may 3
      </text>
      <line x1="60" y1="58" x2="320" y2="58" stroke={C.inkFaint} strokeWidth="1" filter="url(#rough)" />

      {/* Bottom-right legend, pencil */}
      <g transform="translate(1080 740)" opacity="0.55">
        <text fontFamily='"Caveat", cursive' fontSize="16" fill={C.inkSoft}>4 collaborators</text>
        <circle cx="-12" cy="-5" r="4" fill={C.blue} opacity="0.7" />
        <circle cx="-22" cy="-5" r="4" fill={C.red} opacity="0.7" />
        <circle cx="-32" cy="-5" r="4" fill={C.green} opacity="0.7" />
        <circle cx="-42" cy="-5" r="4" fill={C.lilac} opacity="0.7" />
      </g>
    </g>
  );
}

// ── Random small annotations (arrows, asterisks, question marks) ─────────
const ANNOTATIONS = [
  { type: 'q',     x: 480, y: 95,  born: 1.5, life: 4.0 },
  { type: 'star',  x: 920, y: 290, born: 2.5, life: 4.5 },
  { type: 'excl',  x: 220, y: 280, born: 3.4, life: 5.0 },
  { type: 'arrow', x: 380, y: 580, born: 5.5, life: 3.5, dir: [0, -1] },
  { type: 'q',     x: 1080, y: 480, born: 6.5, life: 3.8 },
  { type: 'star',  x: 510, y: 400, born: 12.5, life: 4.0 }, // emphasizes core idea (offset from oval center)
  { type: 'excl',  x: 950, y: 230, born: 13.0, life: 3.5 },
];

function Annotation({ a, t }) {
  const dt = t - a.born;
  if (dt < 0 || dt > a.life + 0.6) return null;
  const fade = lifeWindow(t, a.born, 0.4, a.life - 0.8, 0.6);
  if (fade < 0.01) return null;
  const draw = Math.min(1, dt / 0.5);
  const common = { stroke: C.inkSoft, strokeWidth: 1.6, fill: 'none', strokeLinecap: 'round', filter: 'url(#rough)' };
  let content;
  if (a.type === 'q') {
    content = (
      <text fontFamily='"Caveat", cursive' fontSize="32" fill={C.red} opacity={fade} fontWeight="700">?</text>
    );
  } else if (a.type === 'excl') {
    content = (
      <text fontFamily='"Caveat", cursive' fontSize="30" fill={C.red} opacity={fade} fontWeight="700">!</text>
    );
  } else if (a.type === 'star') {
    const r = 12;
    content = (
      <g opacity={fade}>
        <path d={`M 0 ${-r} L ${r*0.3} ${-r*0.3} L ${r} 0 L ${r*0.3} ${r*0.3} L 0 ${r} L ${-r*0.3} ${r*0.3} L ${-r} 0 L ${-r*0.3} ${-r*0.3} Z`} {...common} stroke={C.red} />
      </g>
    );
  } else if (a.type === 'arrow') {
    const dx = (a.dir?.[0] || 1) * 36;
    const dy = (a.dir?.[1] || 0) * 36;
    content = (
      <g opacity={fade}>
        <path d={`M 0 0 L ${dx} ${dy}`} {...common} />
        <path d={`M ${dx} ${dy} L ${dx - dx*0.3 - dy*0.2} ${dy - dy*0.3 + dx*0.2} M ${dx} ${dy} L ${dx - dx*0.3 + dy*0.2} ${dy - dy*0.3 - dx*0.2}`} {...common} />
      </g>
    );
  }
  return <g transform={`translate(${a.x} ${a.y})`}>{content}</g>;
}

// ── Floating "thinking" dots (collaborator typing) ────────────────────────
function ThinkingDots({ t }) {
  // little sparkles at random locations during early phases
  const dots = [];
  for (let i = 0; i < 12; i++) {
    const phase = (i * 1.4) % LOOP;
    const dt = mod(t - phase, LOOP);
    if (dt < 1.2) {
      const x = 200 + seeded(i * 3 + 1) * 880;
      const y = 120 + seeded(i * 3 + 2) * 560;
      const a = dt < 0.5 ? dt / 0.5 : 1 - (dt - 0.5) / 0.7;
      dots.push(
        <circle key={i} cx={x} cy={y} r={2 + seeded(i) * 1.5} fill={C.inkFaint} opacity={Math.max(0, a) * 0.6} />
      );
    }
  }
  return <g>{dots}</g>;
}

// ── Final "core insight" callout that appears in structure phase ──────────
function CoreCallout({ t }) {
  // Always visible after the central node draws — sits inside the oval as the resolved insight.
  if (t < 11.4 || t > 17.5) return null;
  const fade = lifeWindow(t, 11.4, 0.7, 16.8 - 11.4 - 0.7 - 0.7, 0.7);
  if (fade < 0.01) return null;
  return (
    <g transform="translate(600 400)" opacity={fade}>
      <text fontFamily='"Caveat", cursive' fontSize="24" fontWeight="700" fill={C.ink} textAnchor="middle" y="-4">
        reduce
      </text>
      <text fontFamily='"Caveat", cursive' fontSize="24" fontWeight="700" fill={C.ink} textAnchor="middle" y="22">
        time-to-value
      </text>
    </g>
  );
}

// ─────────────────────────────────────────────────────────────────────────
// MAIN SCENE
// ─────────────────────────────────────────────────────────────────────────
function WhiteboardScene() {
  const time = useTime();
  const t = mod(time, LOOP);

  return (
    <svg viewBox="0 0 1280 800" width="1280" height="800" style={{ display: 'block', background: C.board }}>
      <RoughDefs />

      {/* board paper texture */}
      <rect width="1280" height="800" fill={C.board} />
      <rect width="1280" height="800" fill="url(#grain)" opacity="0.8" />

      <BoardBackground t={t} />

      <ThinkingDots t={t} />

      {/* Annotations layer (under notes) */}
      {ANNOTATIONS.map((a, i) => <Annotation key={i} a={a} t={t} />)}

      {/* Early scattered links — drawn between notes, then erased before structure */}
      {EARLY_LINKS.map((l, i) => {
        const a = noteAnchor(NOTE_BY_ID[l.from], t);
        const b = noteAnchor(NOTE_BY_ID[l.to], t);
        const draw = loopDraw(time, l.born, 0.7, l.life - 1.4, 0.7);
        if (draw.opacity < 0.01) return null;
        const op = draw.opacity * Math.min(a.opacity, b.opacity) * 0.7;
        return (
          <InkLine key={i}
            x1={a.x} y1={a.y} x2={b.x} y2={b.y}
            curve={l.curve}
            color={C.inkSoft}
            width={1.4}
            dash={draw.dash}
            opacity={op}
            dashed={false}
          />
        );
      })}

      {/* Structure links — cluster-centric, drawn 9.8–14, hold, fade with everything.
          Endpoints are pulled back to the perimeter of source/target circles so we don't
          stab into the cluster ovals. */}
      {STRUCT_LINKS.map((l, i) => {
        const draw = loopDraw(time, l.born, 0.8, 16.8 - l.born - 0.8, 0.8);
        if (draw.opacity < 0.01) return null;
        let aPt, bPt, aR = 0, bR = 0;
        if (l.toNote) {
          const c = CLUSTER_BY_ID[l.from];
          aPt = { x: c.x, y: c.y };
          aR = c.r + 4;
          bPt = noteAnchor(NOTE_BY_ID[l.toNote], t);
          bR = 0; // touch note edge naturally
        } else {
          const ca = CLUSTER_BY_ID[l.fromCluster];
          const cb = CLUSTER_BY_ID[l.toCluster];
          aPt = { x: ca.x, y: ca.y }; aR = ca.r + 4;
          bPt = { x: cb.x, y: cb.y }; bR = cb.r + 6;
        }
        // pull back along direction
        const dx = bPt.x - aPt.x, dy = bPt.y - aPt.y;
        const d = Math.hypot(dx, dy) || 1;
        const ux = dx / d, uy = dy / d;
        const x1 = aPt.x + ux * aR;
        const y1 = aPt.y + uy * aR;
        const x2 = bPt.x - ux * bR;
        const y2 = bPt.y - uy * bR;
        return (
          <InkLine key={i}
            x1={x1} y1={y1} x2={x2} y2={y2}
            curve={l.curve || 0}
            color={l.color}
            width={l.width || 1.4}
            dash={draw.dash}
            opacity={draw.opacity * 0.85}
            arrow={l.arrow}
          />
        );
      })}

      {/* Cluster nodes (drawn during structure phase). Central node `cC` has no label
          of its own — the CoreCallout component renders the resolved insight inside it. */}
      {CLUSTERS.map((c) => {
        const dt = t - c.born;
        if (dt < -0.1 || t > 17.5) return null;
        const draw = Math.max(0, Math.min(1, dt / 0.9));
        const fade = t < 16.8 ? 1 : Math.max(0, 1 - (t - 16.8) / 1.0);
        return (
          <InkNode key={c.id}
            x={c.x} y={c.y}
            r={c.r}
            label={c.label}
            color={c.color}
            fill={c.id === 'cC' ? 'rgba(243,213,107,0.30)' : 'transparent'}
            opacity={fade}
            drawProgress={Easing.easeOutCubic(draw)}
            fontSize={14}
          />
        );
      })}

      {/* Notes — render last so they sit on top of background lines but UNDER structural link arrowheads */}
      {NOTES.map((n) => {
        const s = noteState(n, t);
        if (s.opacity < 0.01) return null;
        return (
          <StickyNote key={n.id}
            x={s.x} y={s.y}
            w={n.w} h={n.h}
            rot={s.rot}
            color={n.color}
            text={n.text}
            sub={n.sub}
            opacity={s.opacity}
            scale={s.scale}
            fontSize={n.text.length > 11 ? 16 : 18}
          />
        );
      })}

      {/* Cluster labels emphasis dot (a small drawn highlight) */}
      <CoreCallout t={t} />

      {/* Cursors on top */}
      {CURSOR_PATHS.map((p) => {
        const pos = cursorPos(p, t);
        const v = cursorVisible(pos);
        if (v < 0.01) return null;
        const collab = COLLABS[p.idx];
        return (
          <Cursor key={p.idx} x={pos.x} y={pos.y} color={collab.color} name={collab.name} opacity={v} />
        );
      })}

      {/* very subtle paper-edge vignette */}
      <rect x="0" y="0" width="1280" height="800" fill="url(#vignette)" pointerEvents="none" />
      <defs>
        <radialGradient id="vignette" cx="50%" cy="50%" r="70%">
          <stop offset="60%" stopColor="rgba(0,0,0,0)" />
          <stop offset="100%" stopColor="rgba(60,50,30,0.10)" />
        </radialGradient>
      </defs>
    </svg>
  );
}

window.WhiteboardScene = WhiteboardScene;
window.WHITEBOARD_LOOP = LOOP;
