// scene.jsx — Signal Moving cinematic animation
// One continuous timeline. The signal is the anchor; the system around it
// transforms slowly through five phases and loops back.

const W = 1600;
const H = 900;
const DUR = 26; // seconds total

// Phase timing (seconds)
const PHASES = {
  line:      [0,    4.5],   // simple A->B line
  network:   [4.5,  10.5],  // nodes/branches/filters appear
  orbital:   [10.5, 16.5],  // curves into orbital trajectories
  drift:     [16.5, 21.5],  // grid + misalignment
  dissolve:  [21.5, 26],    // back to simple line
};

// Color tokens — light mode
const COL = {
  bg:       '#f3f0e8',
  bgInner:  '#ece8df',
  line:     'rgba(28,32,44,0.55)',
  lineFaint:'rgba(28,32,44,0.22)',
  lineDim:  'rgba(28,32,44,0.10)',
  text:     'rgba(28,32,44,0.72)',
  textDim:  'rgba(28,32,44,0.40)',
  signal:   '#c8741a',
  signalCore:'#e89436',
  signalGlow:'rgba(232, 148, 54, 0.55)',
  warn:     'oklch(58% 0.14 45)',
};

// ── Helpers ─────────────────────────────────────────────────────────────────
const lerp = (a, b, t) => a + (b - a) * t;
const smooth = (t) => t < 0 ? 0 : t > 1 ? 1 : t * t * (3 - 2 * t);
// Smooth ramp: 0 outside [a,d], 1 between [b,c], cosine in/out on edges.
function ramp(t, a, b, c, d) {
  if (t <= a || t >= d) return 0;
  if (t >= b && t <= c) return 1;
  if (t < b) return smooth((t - a) / (b - a));
  return smooth(1 - (t - c) / (d - c));
}
// Step: 0 before a, 1 after b, smooth in between
function step(t, a, b) {
  if (t <= a) return 0;
  if (t >= b) return 1;
  return smooth((t - a) / (b - a));
}

// Sample a point along a quadratic/cubic by parameter using an SVG path.
function ptOnPath(path, t) {
  if (!path) return { x: 0, y: 0 };
  const len = path.getTotalLength();
  return path.getPointAtLength(len * t);
}

// ── Master signal path: the spine the signal always rides ───────────────────
// It morphs with phases by interpolating control points.
function signalPath(time) {
  // Anchors A and B (slightly off-center)
  const A = { x: 220, y: H / 2 };
  const B = { x: W - 220, y: H / 2 };

  // Phase mixers
  const pLine     = ramp(time, -1, 0, PHASES.line[1] - 0.5, PHASES.network[0] + 0.4);
  const pNetwork  = ramp(time, PHASES.line[1] - 0.4, PHASES.network[0] + 0.6, PHASES.network[1] - 0.4, PHASES.orbital[0] + 0.6);
  const pOrbital  = ramp(time, PHASES.network[1] - 0.4, PHASES.orbital[0] + 0.6, PHASES.orbital[1] - 0.4, PHASES.drift[0] + 0.6);
  const pDrift    = ramp(time, PHASES.orbital[1] - 0.4, PHASES.drift[0] + 0.6, PHASES.drift[1] - 0.4, PHASES.dissolve[0] + 0.6);
  const pDissolve = step(time, PHASES.dissolve[0] + 0.4, PHASES.dissolve[1]);

  // We compute waypoints W1..W3 across A and B by mixing different shapes.
  // Line shape: straight line, even spacing
  const lineW = [
    { x: lerp(A.x, B.x, 0.33), y: H / 2 },
    { x: lerp(A.x, B.x, 0.50), y: H / 2 },
    { x: lerp(A.x, B.x, 0.67), y: H / 2 },
  ];

  // Network shape: a few subtle bends (route through filter nodes)
  const netW = [
    { x: lerp(A.x, B.x, 0.28), y: H / 2 - 70 },
    { x: lerp(A.x, B.x, 0.52), y: H / 2 + 50 },
    { x: lerp(A.x, B.x, 0.74), y: H / 2 - 30 },
  ];

  // Orbital shape: deep S-curve that arcs above center (atmospheric arc)
  const orbW = [
    { x: lerp(A.x, B.x, 0.30), y: H / 2 - 160 },
    { x: lerp(A.x, B.x, 0.50), y: H / 2 - 220 },
    { x: lerp(A.x, B.x, 0.72), y: H / 2 - 140 },
  ];

  // Drift shape: misaligned, slightly warped
  const drW = [
    { x: lerp(A.x, B.x, 0.26), y: H / 2 - 120 },
    { x: lerp(A.x, B.x, 0.48) + 30, y: H / 2 + 90 },
    { x: lerp(A.x, B.x, 0.70) - 20, y: H / 2 - 60 },
  ];

  const mix = (k) =>
    lineW[k].x     * (pLine + pDissolve) / Math.max(0.0001, pLine + pNetwork + pOrbital + pDrift + pDissolve) +
    netW[k].x     * pNetwork / Math.max(0.0001, pLine + pNetwork + pOrbital + pDrift + pDissolve) +
    orbW[k].x     * pOrbital / Math.max(0.0001, pLine + pNetwork + pOrbital + pDrift + pDissolve) +
    drW[k].x      * pDrift / Math.max(0.0001, pLine + pNetwork + pOrbital + pDrift + pDissolve);
  // simpler: weighted blend on x and y separately
  const blend = (key) => {
    const wSum = pLine + pNetwork + pOrbital + pDrift + pDissolve;
    const w = wSum < 0.0001 ? 1 : wSum;
    return (
      lineW[key][0]    // never used; placeholder
    );
  };

  const w = (pLine + pNetwork + pOrbital + pDrift + pDissolve) || 1;
  const blendPt = (k) => ({
    x: (lineW[k].x*(pLine+pDissolve) + netW[k].x*pNetwork + orbW[k].x*pOrbital + drW[k].x*pDrift) / w,
    y: (lineW[k].y*(pLine+pDissolve) + netW[k].y*pNetwork + orbW[k].y*pOrbital + drW[k].y*pDrift) / w,
  });

  const w1 = blendPt(0);
  const w2 = blendPt(1);
  const w3 = blendPt(2);

  // Build a smooth path using cubic-like through points
  const d = `M ${A.x} ${A.y} C ${A.x + 80} ${A.y}, ${w1.x - 60} ${w1.y}, ${w1.x} ${w1.y} S ${w2.x} ${w2.y}, ${w2.x} ${w2.y} S ${w3.x} ${w3.y}, ${w3.x} ${w3.y} S ${B.x - 80} ${B.y}, ${B.x} ${B.y}`;
  return { d, A, B, w1, w2, w3 };
}

// ── Background grid (fades in for orbital/drift phases) ─────────────────────
function BgGrid() {
  const time = useTime();
  const op = ramp(time, PHASES.network[1] - 0.5, PHASES.orbital[0] + 0.8, PHASES.drift[1] - 0.6, PHASES.dissolve[0] + 1.0) * 0.5
           + ramp(time, PHASES.drift[0] - 0.2, PHASES.drift[0] + 1.2, PHASES.drift[1] - 0.6, PHASES.dissolve[0] + 0.8) * 0.4;
  if (op < 0.01) return null;
  const cells = [];
  const n = 14, m = 8;
  for (let i = 0; i <= n; i++) {
    cells.push(<line key={`v${i}`} x1={(i/n)*W} y1={0} x2={(i/n)*W} y2={H} stroke="rgba(28,32,44,0.05)" strokeWidth="1"/>);
  }
  for (let j = 0; j <= m; j++) {
    cells.push(<line key={`h${j}`} x1={0} y1={(j/m)*H} x2={W} y2={(j/m)*H} stroke="rgba(28,32,44,0.05)" strokeWidth="1"/>);
  }
  // perspective-y subtle warp: small horizon dots
  return <g style={{ opacity: Math.min(1, op) }}>{cells}</g>;
}

// ── Endpoint markers (Point A / Point B) ────────────────────────────────────
function Endpoint({ x, y, label, time, side='left', visible = 1 }) {
  // tiny crosshair + label
  const tx = side === 'left' ? x - 18 : x + 18;
  const align = side === 'left' ? 'end' : 'start';
  return (
    <g style={{ opacity: visible }}>
      <circle cx={x} cy={y} r="3.5" fill={COL.signal} />
      <circle cx={x} cy={y} r="11" fill="none" stroke="rgba(28,32,44,0.40)" strokeWidth="1"/>
      <circle cx={x} cy={y} r="22" fill="none" stroke="rgba(28,32,44,0.16)" strokeWidth="1" strokeDasharray="2 4"/>
      <text
        x={tx} y={y + 4}
        textAnchor={align}
        fill={COL.text}
        fontFamily="JetBrains Mono, ui-monospace, monospace"
        fontSize="12"
        letterSpacing="0.18em"
      >
        {label}
      </text>
    </g>
  );
}

// ── Network elements (nodes, filters, branches) ─────────────────────────────
// These appear during the network phase, evolve into orbital, then fade.
const NETWORK_NODES = [
  // x_pct, y_offset, kind, phaseStart
  { id: 'n1', x: 0.18, y: -180, kind: 'node',   t: 4.6 },
  { id: 'n2', x: 0.30, y: +160, kind: 'filter', t: 4.9 },
  { id: 'n3', x: 0.42, y: -100, kind: 'node',   t: 5.2 },
  { id: 'n4', x: 0.50, y: +220, kind: 'node',   t: 5.5 },
  { id: 'n5', x: 0.58, y: -240, kind: 'filter', t: 5.8 },
  { id: 'n6', x: 0.66, y: +120, kind: 'node',   t: 6.1 },
  { id: 'n7', x: 0.78, y: -160, kind: 'node',   t: 6.4 },
  { id: 'n8', x: 0.86, y: +200, kind: 'filter', t: 6.7 },
  { id: 'n9', x: 0.36, y: +60,  kind: 'node',   t: 7.1 },
  { id: 'n10',x: 0.62, y: -40,  kind: 'node',   t: 7.4 },
];

function Node({ cx, cy, kind, op }) {
  if (kind === 'filter') {
    return (
      <g style={{ opacity: op }}>
        <rect x={cx-9} y={cy-9} width="18" height="18" fill="none" stroke={COL.line} strokeWidth="1" transform={`rotate(45 ${cx} ${cy})`}/>
        <circle cx={cx} cy={cy} r="2" fill={COL.line}/>
      </g>
    );
  }
  return (
    <g style={{ opacity: op }}>
      <circle cx={cx} cy={cy} r="4.5" fill="none" stroke={COL.line} strokeWidth="1"/>
      <circle cx={cx} cy={cy} r="1.8" fill={COL.line}/>
    </g>
  );
}

function Network() {
  const time = useTime();
  // Nodes appear during network, persist into orbital, then fade by drift end
  const baseOp = ramp(time, PHASES.line[1] - 0.2, PHASES.network[0] + 0.8, PHASES.drift[0] + 1.5, PHASES.dissolve[0] + 0.6);
  if (baseOp < 0.01) return null;

  const Ax = 220, Bx = W - 220, midY = H / 2;

  return (
    <g>
      {/* Branch lines connecting nodes — drawn first, faint */}
      {NETWORK_NODES.map((n, i) => {
        const cx = lerp(Ax, Bx, n.x);
        const cy = midY + n.y;
        const appear = step(time, n.t, n.t + 0.7);
        const fade = 1 - step(time, PHASES.drift[0] + 0.5, PHASES.dissolve[0] + 0.4);
        const op = baseOp * appear * fade;
        // connect to previous node-ish
        const prev = i > 0 ? NETWORK_NODES[i-1] : null;
        if (!prev) return null;
        const px = lerp(Ax, Bx, prev.x);
        const py = midY + prev.y;
        return (
          <line key={`branch-${n.id}`}
            x1={px} y1={py} x2={cx} y2={cy}
            stroke={COL.lineFaint}
            strokeWidth="1"
            style={{ opacity: op * 0.7 }}
          />
        );
      })}

      {/* Connector stubs from spine to each node */}
      {NETWORK_NODES.map((n) => {
        const cx = lerp(Ax, Bx, n.x);
        const cy = midY + n.y;
        const appear = step(time, n.t, n.t + 0.8);
        const fade = 1 - step(time, PHASES.drift[0] + 0.5, PHASES.dissolve[0] + 0.4);
        const op = baseOp * appear * fade;
        return (
          <line key={`stub-${n.id}`}
            x1={cx} y1={midY} x2={cx} y2={cy}
            stroke={COL.lineFaint}
            strokeWidth="1"
            strokeDasharray="2 3"
            style={{ opacity: op * 0.55 }}
          />
        );
      })}

      {/* Nodes */}
      {NETWORK_NODES.map((n) => {
        const cx = lerp(Ax, Bx, n.x);
        const cy = midY + n.y;
        const appear = step(time, n.t, n.t + 0.7);
        const fade = 1 - step(time, PHASES.drift[0] + 0.5, PHASES.dissolve[0] + 0.4);
        const op = baseOp * appear * fade;
        return <Node key={n.id} cx={cx} cy={cy} kind={n.kind} op={op}/>;
      })}
    </g>
  );
}

// ── Orbital arcs — curving trajectories that hint at satellite paths ────────
function Orbitals() {
  const time = useTime();
  const op = ramp(time, PHASES.network[1] - 1.0, PHASES.orbital[0] + 0.8, PHASES.drift[1] - 0.6, PHASES.dissolve[0] + 0.6);
  if (op < 0.01) return null;

  const cx = W / 2;
  const cy = H / 2 + 80; // orbits arc above
  const arcs = [
    { rx: 540, ry: 280, rot: -8, dash: '0', sw: 1.0 },
    { rx: 700, ry: 360, rot: -12, dash: '4 6', sw: 1.0 },
    { rx: 900, ry: 480, rot: -4, dash: '2 8', sw: 1.0 },
    { rx: 460, ry: 220, rot: 12, dash: '0', sw: 1.0 },
  ];

  return (
    <g style={{ opacity: op }}>
      {arcs.map((a, i) => {
        const appear = step(time, PHASES.orbital[0] + 0.3 + i*0.25, PHASES.orbital[0] + 1.2 + i*0.25);
        // Draw upper half-ellipse
        const x1 = cx - a.rx, x2 = cx + a.rx;
        const d = `M ${x1} ${cy} A ${a.rx} ${a.ry} ${a.rot} 0 1 ${x2} ${cy}`;
        return (
          <g key={i}>
            <path d={d}
              fill="none"
              stroke={COL.lineFaint}
              strokeWidth={a.sw}
              strokeDasharray={a.dash}
              style={{
                opacity: appear * 0.9,
                transform: `rotate(${a.rot * 0.0}deg)`,
                transformOrigin: `${cx}px ${cy}px`,
              }}
              strokeDashoffset={-time * 12}
            />
            {/* satellite tick */}
            <SatelliteTick d={d} t={(time * 0.07 + i * 0.27) % 1} op={appear} />
          </g>
        );
      })}
    </g>
  );
}

function SatelliteTick({ d, t, op }) {
  const ref = React.useRef(null);
  const [pt, setPt] = React.useState({ x: 0, y: 0 });
  React.useEffect(() => {
    if (!ref.current) return;
    const len = ref.current.getTotalLength();
    const p = ref.current.getPointAtLength(len * t);
    setPt({ x: p.x, y: p.y });
  }, [t, d]);
  return (
    <g>
      <path ref={ref} d={d} fill="none" stroke="none"/>
      <g style={{ opacity: op * 0.85 }}>
        <circle cx={pt.x} cy={pt.y} r="2" fill={COL.line}/>
        <circle cx={pt.x} cy={pt.y} r="6" fill="none" stroke={COL.lineFaint} strokeWidth="1"/>
      </g>
    </g>
  );
}

// ── Spatial layered grid (for drift phase) ──────────────────────────────────
function SpatialGrid() {
  const time = useTime();
  const op = ramp(time, PHASES.orbital[1] - 0.8, PHASES.drift[0] + 1.0, PHASES.drift[1] - 0.4, PHASES.dissolve[0] + 0.6);
  if (op < 0.01) return null;
  const cx = W/2, cy = H/2 + 280;
  // perspective-style grid lines converging toward a horizon, slightly skewed
  const lines = [];
  const horizonY = H/2 + 30;
  const skew = Math.sin(time * 0.4) * 6 + 4; // subtle misalignment
  for (let i = -8; i <= 8; i++) {
    const x = cx + i * 80 + skew * (i*0.1);
    lines.push(
      <line key={`pv${i}`} x1={x} y1={H} x2={cx + i*8} y2={horizonY}
        stroke={COL.lineDim} strokeWidth="1"/>
    );
  }
  for (let j = 1; j <= 6; j++) {
    const y = horizonY + Math.pow(j/6, 1.6) * (H - horizonY);
    lines.push(
      <line key={`ph${j}`} x1={0} y1={y} x2={W} y2={y}
        stroke={COL.lineDim} strokeWidth="1"/>
    );
  }
  return <g style={{ opacity: op }}>{lines}</g>;
}

// ── Misalignment artifacts: slight ghost duplicates of the spine ────────────
function Ghosts({ pathD }) {
  const time = useTime();
  const op = ramp(time, PHASES.orbital[1] - 0.4, PHASES.drift[0] + 1.0, PHASES.drift[1] - 0.2, PHASES.dissolve[0] + 0.5);
  if (op < 0.02) return null;
  const offsets = [
    { dx: 0, dy: -8, op: 0.5 },
    { dx: 0, dy: 6, op: 0.35 },
    { dx: 4, dy: 0, op: 0.25 },
  ];
  return (
    <g style={{ opacity: op }}>
      {offsets.map((o, i) => (
        <path key={i} d={pathD}
          fill="none"
          stroke="rgba(28,32,44,0.28)"
          strokeWidth="1"
          transform={`translate(${o.dx},${o.dy})`}
          style={{ opacity: o.op, mixBlendMode: 'multiply' }}
        />
      ))}
    </g>
  );
}

// ── Phase label (mono caption fading at top) ────────────────────────────────
function PhaseLabel() {
  const time = useTime();
  const labels = [
    { t0: 0.4, t1: 4.0, primary: 'TRANSMIT', secondary: 'point a → point b' },
    { t0: 5.0, t1: 9.5, primary: 'BUILD · ANALYZE', secondary: 'nodes · filters · branches' },
    { t0: 11.0, t1: 15.5, primary: 'PROTOTYPE', secondary: 'distributed · orbital relay' },
    { t0: 17.0, t1: 20.8, primary: 'REFINE', secondary: 'capacity · latency · cost' },
    { t0: 22.2, t1: 25.5, primary: 'RESET', secondary: 'return to source' },
  ];
  return (
    <g>
      {labels.map((l, i) => {
        const op = ramp(time, l.t0 - 0.3, l.t0 + 0.5, l.t1 - 0.5, l.t1 + 0.3);
        if (op < 0.01) return null;
        return (
          <g key={i} style={{ opacity: op }}>
            <text x={W/2} y={120} textAnchor="middle"
              fill={COL.text}
              fontFamily="JetBrains Mono, ui-monospace, monospace"
              fontSize="13" letterSpacing="0.32em">
              {l.primary}
            </text>
            <text x={W/2} y={142} textAnchor="middle"
              fill={COL.textDim}
              fontFamily="JetBrains Mono, ui-monospace, monospace"
              fontSize="11" letterSpacing="0.18em">
              {l.secondary}
            </text>
          </g>
        );
      })}
    </g>
  );
}

// ── Bottom telemetry — small mono numbers that hint at tradeoffs ────────────
function Telemetry() {
  const time = useTime();
  const op = ramp(time, PHASES.network[0] - 0.2, PHASES.network[0] + 1.5, PHASES.dissolve[0] - 0.2, PHASES.dissolve[0] + 1.0);
  if (op < 0.02) return null;
  // Three slowly-changing readouts
  const t = time;
  const lat = (8 + Math.sin(t * 0.7) * 1.4 + (t > 14 ? 2.2 : 0)).toFixed(2);
  const cap = (94 + Math.cos(t * 0.5) * 3 - (t > 16 ? 4 : 0)).toFixed(1);
  const cost= (1.0 + (t > 10 ? (t - 10) * 0.04 : 0) + Math.sin(t)*0.02).toFixed(2);

  const items = [
    { k: 'LAT', v: `${lat}ms` },
    { k: 'CAP', v: `${cap}%` },
    { k: 'COST', v: `${cost}×` },
  ];
  return (
    <g style={{ opacity: op }}>
      {items.map((it, i) => {
        const x = 220 + i * 200;
        return (
          <g key={i}>
            <text x={x} y={H - 72}
              fill={COL.textDim}
              fontFamily="JetBrains Mono, ui-monospace, monospace"
              fontSize="10" letterSpacing="0.28em">
              {it.k}
            </text>
            <text x={x} y={H - 50}
              fill={COL.text}
              fontFamily="JetBrains Mono, ui-monospace, monospace"
              fontSize="14" letterSpacing="0.06em"
              fontVariantNumeric="tabular-nums">
              {it.v}
            </text>
          </g>
        );
      })}
      {/* right side — a phase tag */}
      <text x={W - 220} y={H - 72} textAnchor="end"
        fill={COL.textDim}
        fontFamily="JetBrains Mono, ui-monospace, monospace"
        fontSize="10" letterSpacing="0.28em">
        SYS
      </text>
      <text x={W - 220} y={H - 50} textAnchor="end"
        fill={COL.text}
        fontFamily="JetBrains Mono, ui-monospace, monospace"
        fontSize="14" letterSpacing="0.06em"
        fontVariantNumeric="tabular-nums">
        {`v${(0.1 + time * 0.04).toFixed(2)}`}
      </text>
    </g>
  );
}

// ── The signal: the always-present anchor ───────────────────────────────────
// It travels along the morphing path. Each "lap" takes ~5.2s, with subtle
// pacing variation per phase. We sample the live path with a hidden ref.
function Signal({ pathD }) {
  const time = useTime();
  const pathRef = React.useRef(null);
  const [pos, setPos] = React.useState({ x: 220, y: H/2 });
  const [trail, setTrail] = React.useState([]);

  // Lap pacing — slightly slower in drift phase (heavier feel)
  const lapDur = time < PHASES.network[1] ? 5.2
    : time < PHASES.orbital[1] ? 5.6
    : time < PHASES.drift[1] ? 6.0
    : 4.8;
  const lapT = ((time / lapDur) % 1);
  // Ease the lap so the signal accelerates and decelerates per pass
  const eased = lapT < 0.5
    ? 2 * lapT * lapT
    : 1 - Math.pow(-2 * lapT + 2, 2) / 2;

  React.useLayoutEffect(() => {
    if (!pathRef.current) return;
    const len = pathRef.current.getTotalLength();
    const p = pathRef.current.getPointAtLength(len * eased);
    setPos({ x: p.x, y: p.y });
    // sample trail behind
    const N = 18;
    const pts = [];
    for (let i = 1; i <= N; i++) {
      const back = eased - i * 0.012;
      if (back <= 0) break;
      const q = pathRef.current.getPointAtLength(len * back);
      pts.push({ x: q.x, y: q.y, a: 1 - i / N });
    }
    setTrail(pts);
  }, [eased, pathD]);

  // Pulsing core radius
  const pulse = 1 + Math.sin(time * 6) * 0.08;
  const coreR = 4.2 * pulse;

  return (
    <g>
      {/* hidden path used only for sampling */}
      <path ref={pathRef} d={pathD} fill="none" stroke="none"/>

      {/* Trail */}
      <g style={{ mixBlendMode: 'multiply' }}>
        {trail.map((p, i) => (
          <circle key={i} cx={p.x} cy={p.y}
            r={3.2 * (1 - i / trail.length)}
            fill={COL.signalGlow}
            opacity={p.a * 0.55}
          />
        ))}
      </g>

      {/* Outer halo */}
      <circle cx={pos.x} cy={pos.y} r="24" fill="url(#signalHalo)" style={{ mixBlendMode: 'multiply' }}/>
      <circle cx={pos.x} cy={pos.y} r="12" fill="url(#signalHalo)" style={{ mixBlendMode: 'multiply' }}/>

      {/* Core */}
      <circle cx={pos.x} cy={pos.y} r={coreR + 1} fill={COL.signal}/>
      <circle cx={pos.x} cy={pos.y} r={coreR} fill={COL.signalCore}/>
      <circle cx={pos.x} cy={pos.y} r={coreR * 0.45} fill="#fff7e6"/>
    </g>
  );
}

// ── The spine path itself, rendered visibly ────────────────────────────────
function Spine({ pathD }) {
  const time = useTime();
  // Width, opacity, and dash all evolve subtly through phases
  const baseOp = 1; // always visible
  const sw = time < PHASES.network[0] ? 1.4
           : time < PHASES.orbital[0] ? 1.2
           : time < PHASES.drift[0] ? 1.1
           : time < PHASES.dissolve[0] ? 1.0
           : 1.4;
  // Dash drift gives a feeling of "current"
  const dashOffset = -time * 30;
  return (
    <g>
      {/* very faint glow under spine */}
      <path d={pathD} fill="none" stroke={COL.signalGlow} strokeWidth="6"
        style={{ opacity: 0.22, filter: 'blur(6px)', mixBlendMode: 'multiply' }}/>
      <path d={pathD} fill="none" stroke={COL.line} strokeWidth={sw}
        strokeDasharray="1 3" strokeDashoffset={dashOffset}
        style={{ opacity: 0.85 }}/>
      <path d={pathD} fill="none" stroke={COL.lineFaint} strokeWidth="1"/>
    </g>
  );
}

// ── Subtle vignette + film grain ────────────────────────────────────────────
function Vignette() {
  return (
    <>
      <rect x="0" y="0" width={W} height={H} fill="url(#vignette)" style={{ pointerEvents: 'none' }}/>
    </>
  );
}

// ── Main scene ──────────────────────────────────────────────────────────────
function SignalScene() {
  const time = useTime();
  const { d, A, B } = signalPath(time);

  // Endpoints fade slightly during drift, return for dissolve
  const endpointOp = 1 - 0.35 * ramp(time, PHASES.drift[0], PHASES.drift[0] + 1.5, PHASES.drift[1] - 0.4, PHASES.dissolve[0] + 0.8);

  return (
    <svg viewBox={`0 0 ${W} ${H}`} width={W} height={H}
      style={{ position: 'absolute', inset: 0, display: 'block' }}>
      <defs>
        <radialGradient id="signalHalo" cx="50%" cy="50%" r="50%">
          <stop offset="0%" stopColor="#e89436" stopOpacity="0.75"/>
          <stop offset="40%" stopColor="#c8741a" stopOpacity="0.35"/>
          <stop offset="100%" stopColor="#c8741a" stopOpacity="0"/>
        </radialGradient>
        <radialGradient id="vignette" cx="50%" cy="50%" r="75%">
          <stop offset="60%" stopColor="rgba(64,52,32,0)"/>
          <stop offset="100%" stopColor="rgba(64,52,32,0.22)"/>
        </radialGradient>
        <radialGradient id="bgGlow" cx="50%" cy="50%" r="55%">
          <stop offset="0%" stopColor="#f7f4ec" stopOpacity="1"/>
          <stop offset="100%" stopColor="#ece8de" stopOpacity="1"/>
        </radialGradient>
      </defs>

      {/* Background */}
      <rect x="0" y="0" width={W} height={H} fill="url(#bgGlow)"/>

      {/* Layered systems, behind the spine */}
      <BgGrid/>
      <SpatialGrid/>
      <Orbitals/>
      <Network/>

      {/* Misalignment ghosts (drift phase) */}
      <Ghosts pathD={d}/>

      {/* The spine (path A→B, morphing) */}
      <Spine pathD={d}/>

      {/* Endpoints */}
      <g style={{ opacity: endpointOp }}>
        <Endpoint x={A.x} y={A.y} label="POINT A" side="left"/>
        <Endpoint x={B.x} y={B.y} label="POINT B" side="right"/>
      </g>

      {/* The signal — always last so it's on top */}
      <Signal pathD={d}/>

      {/* Captions */}
      <PhaseLabel/>
      <Telemetry/>

      <Vignette/>
    </svg>
  );
}

Object.assign(window, { SignalScene, DUR, W, H });
