/* Project card artwork — one animated diagram per project.
 *
 * Every animation here moves only `transform` and `opacity`. Those are the
 * two properties the compositor can run on the GPU without a style repaint,
 * so eleven of these can loop at once without touching the main thread per
 * frame. The keyframes live in styles.css under "Project card artwork".
 *
 * No animation library, deliberately:
 *   - a per-card WebGL context is not an option (Chrome keeps only 16 live
 *     contexts per page and silently drops the oldest), which rules out
 *     three.js / OGL / Pixi / Rive for an 11-card grid;
 *   - the JS animation libraries drive rAF on the main thread, so they buy
 *     nothing over CSS keyframes for declarative loops like these, and cost
 *     70-140KB over the wire on a site with no build step.
 *
 * Geometry is computed once at load from a seeded PRNG so it is identical on
 * every visit. Positioning uses static SVG `transform` attributes on an outer
 * <g>; CSS animates an inner element, because a CSS transform would otherwise
 * overwrite the presentation attribute.
 */

const ART_VB = "0 0 580 220";

function artRng(seed) {
  let s = seed >>> 0;
  return () => ((s = (s * 1664525 + 1013904223) >>> 0) / 4294967296);
}
const r1 = (v) => Math.round(v * 10) / 10;

function polyline(n, fn) {
  const out = [];
  for (let i = 0; i <= n; i++) {
    const p = fn(i / n, i);
    out.push(r1(p[0]) + "," + r1(p[1]));
  }
  return out.join(" ");
}

/* A soft horizontal sweep band, reused by several cards. */
function Sweep({ id, cls = "pa-sweep" }) {
  return (
    <g className={cls}>
      <rect x="-70" y="0" width="140" height="220" fill={`url(#${id})`} />
    </g>
  );
}
function SweepGradient({ id, peak = 0.5 }) {
  return (
    <linearGradient id={id} x1="0" x2="1" y1="0" y2="0">
      <stop offset="0" stopColor="#fff" stopOpacity="0" />
      <stop offset="0.5" stopColor="#fff" stopOpacity={peak} />
      <stop offset="1" stopColor="#fff" stopOpacity="0" />
    </linearGradient>
  );
}

/* ------------------------------------------------------------------ *
 * 1. Quantum-ECC — surface-code lattice, pulsing syndrome defects,
 *    a matching path, and a decoder sweep.
 * ------------------------------------------------------------------ */
const ECC = (() => {
  const cols = 9, rows = 5, dx = 52, dy = 38;
  const x0 = (580 - (cols - 1) * dx) / 2;
  const y0 = (220 - (rows - 1) * dy) / 2;
  const at = (c, r) => [x0 + c * dx, y0 + r * dy];
  const nodes = [];
  for (let r = 0; r < rows; r++)
    for (let c = 0; c < cols; c++) nodes.push({ c, r, p: at(c, r) });
  const plaquettes = [];
  for (let r = 0; r < rows - 1; r++)
    for (let c = 0; c < cols - 1; c++)
      if ((r + c) % 2 === 0) plaquettes.push(at(c, r));
  const defects = [[1, 1], [3, 3], [5, 1], [7, 2], [4, 0]].map((d) => at(d[0], d[1]));
  const chain = [[1, 1], [2, 1], [2, 2], [3, 2], [3, 3]].map((d) => at(d[0], d[1]));
  const chain2 = [[5, 1], [6, 1], [6, 2], [7, 2]].map((d) => at(d[0], d[1]));
  const asPts = (a) => a.map((p) => r1(p[0]) + "," + r1(p[1])).join(" ");
  return { cols, rows, dx, dy, nodes, plaquettes, defects, chain: asPts(chain), chain2: asPts(chain2), at };
})();

function ArtQuantumEcc() {
  return (
    <svg className="pa pa-ecc" viewBox={ART_VB} preserveAspectRatio="xMidYMid meet">
      <defs><SweepGradient id="sw-ecc" peak="0.16" /></defs>
      <g className="pa-ink">
        {ECC.plaquettes.map((p, i) => (
          <rect key={i} x={p[0]} y={p[1]} width={ECC.dx} height={ECC.dy} className="pa-plaq" />
        ))}
        {Array.from({ length: ECC.rows }, (_, r) => (
          <line key={"h" + r} x1={ECC.at(0, r)[0]} y1={ECC.at(0, r)[1]}
            x2={ECC.at(ECC.cols - 1, r)[0]} y2={ECC.at(0, r)[1]} className="pa-edge" />
        ))}
        {Array.from({ length: ECC.cols }, (_, c) => (
          <line key={"v" + c} x1={ECC.at(c, 0)[0]} y1={ECC.at(0, 0)[1]}
            x2={ECC.at(c, 0)[0]} y2={ECC.at(0, ECC.rows - 1)[1]} className="pa-edge" />
        ))}
        <polyline points={ECC.chain} className="pa-chain" />
        <polyline points={ECC.chain2} className="pa-chain d2" />
        {ECC.nodes.map((n, i) => (
          <circle key={i} cx={n.p[0]} cy={n.p[1]} r="1.5" className="pa-node pa-dot" />
        ))}
        {ECC.defects.map((p, i) => (
          <g key={i} transform={`translate(${r1(p[0])} ${r1(p[1])})`}>
            <circle r="3.2" className="pa-defect pa-dot" style={{ animationDelay: `${i * 0.42}s` }} />
            <circle r="7" className="pa-halo" style={{ animationDelay: `${i * 0.42}s` }} />
          </g>
        ))}
      </g>
      <Sweep id="sw-ecc" />
    </svg>
  );
}

/* ------------------------------------------------------------------ *
 * 2. Merton — asset-value walk against a default barrier, with the
 *    breach-probability tail breathing under the barrier.
 * ------------------------------------------------------------------ */
const MERTON = (() => {
  const rnd = artRng(7);
  const n = 64, walk = [];
  let v = 122;
  for (let i = 0; i <= n; i++) {
    const t = i / n;
    v = Math.max(96, Math.min(150, v + (rnd() - 0.5) * 11));
    walk.push([40 + t * 386, v - 42 * Math.sin(Math.PI * t * 0.92)]);
  }
  /* Terminal asset distribution, drawn sideways at the horizon. Its mass
     below the barrier is the default probability — that is the hatched part,
     so the barrier sits well inside the distribution to give it real area. */
  const BX = 430, BARRIER = 146, MU = 102, SD = 31, TAIL_H = 54;
  const dens = (y) => Math.exp(-Math.pow(y - MU, 2) / (2 * SD * SD)) * 98;
  const bell = polyline(64, (t) => {
    const y = 28 + t * 176;
    return [BX + dens(y), y];
  });
  const tail = polyline(28, (t) => {
    const y = BARRIER + t * TAIL_H;
    return [BX + dens(y), y];
  });
  return {
    line: walk.map((p) => r1(p[0]) + "," + r1(p[1])).join(" "),
    bell,
    tailArea: `${BX},${BARRIER} ${tail} ${BX},${BARRIER + TAIL_H}`,
    barrier: BARRIER,
    bx: BX,
    end: walk[walk.length - 1],
  };
})();

function ArtMerton() {
  return (
    <svg className="pa pa-merton" viewBox={ART_VB} preserveAspectRatio="xMidYMid meet">
      <defs>
        <pattern id="hatch-merton" width="5" height="5" patternTransform="rotate(45)"
          patternUnits="userSpaceOnUse">
          <line x1="0" y1="0" x2="0" y2="5" stroke="#fff" strokeOpacity="0.62" strokeWidth="0.8" />
        </pattern>
      </defs>
      <g className="pa-ink">
        <polyline points={MERTON.line} className="pa-walk" />
        <line x1="30" y1={MERTON.barrier} x2="550" y2={MERTON.barrier} className="pa-barrier" />
        <polyline points={MERTON.bell} className="pa-bell" />
        <g className="pa-tail">
          <polygon points={MERTON.tailArea} className="pa-hatch" fill="url(#hatch-merton)" />
        </g>
        <line x1={MERTON.bx} y1="40" x2={MERTON.bx} y2="200" className="pa-horizon" />
        <g transform={`translate(${r1(MERTON.end[0])} 0)`}>
          <g className="pa-dd">
            <line x1="0" y1={r1(MERTON.end[1])} x2="0" y2={MERTON.barrier} className="pa-ddline" />
            <line x1="-4" y1={r1(MERTON.end[1])} x2="4" y2={r1(MERTON.end[1])} className="pa-ddcap" />
            <line x1="-4" y1={MERTON.barrier} x2="4" y2={MERTON.barrier} className="pa-ddcap" />
          </g>
        </g>
        <circle cx={r1(MERTON.end[0])} cy={r1(MERTON.end[1])} r="2.6" className="pa-tip pa-dot" />
      </g>
    </svg>
  );
}

/* ------------------------------------------------------------------ *
 * 3. SABR Volatility Insights — wireframe smile surface, with the
 *    highlighted expiry slice scrubbing across it.
 * ------------------------------------------------------------------ */
const SABR = (() => {
  const nx = 13, ny = 7;
  const smile = (u) => 1 - 0.82 * Math.exp(-Math.pow(u - 0.5, 2) / 0.055);
  const proj = (u, v) => {
    const h = smile(u) * (38 + v * 21);
    return [70 + u * 380 + v * 92, 168 - v * 44 - h];
  };
  /* A rib is one expiry's smile across strike — that is the slice the
     workbench highlights, so ribs are what light up in sequence. */
  const ribs = [];
  for (let j = 0; j < ny; j++)
    ribs.push(polyline(nx - 1, (t) => proj(t, j / (ny - 1))));
  const spars = [];
  for (let i = 0; i < nx; i++)
    spars.push(polyline(ny - 1, (t) => proj(i / (nx - 1), t)));
  return { ribs, spars, ny, proj };
})();

function ArtSabr() {
  return (
    <svg className="pa pa-sabr" viewBox={ART_VB} preserveAspectRatio="xMidYMid meet">
      <g className="pa-orbit">
        <g className="pa-ink">
          {SABR.spars.map((d, i) => <polyline key={"s" + i} points={d} className="pa-mesh faint" />)}
          {SABR.ribs.map((d, i) => (
            <polyline key={"r" + i} points={d} className="pa-mesh pa-smile"
              style={{ animationDelay: `${r1((i * 8.4) / SABR.ny)}s` }} />
          ))}
          <line x1="66" y1="174" x2="470" y2="174" className="pa-axis" />
          <line x1="66" y1="174" x2="66" y2="98" className="pa-axis" />
        </g>
      </g>
    </svg>
  );
}

/* ------------------------------------------------------------------ *
 * 4. Quantum-ML — a spin-flip wave travelling down the chain, under a
 *    twinkling network and the order parameter with its BKT kink.
 * ------------------------------------------------------------------ */
const QML = (() => {
  const n = 15;
  const spins = Array.from({ length: n }, (_, i) => ({
    x: r1(58 + (i * 464) / (n - 1)),
    up: i % 3 !== 1,
    delay: r1(i * 0.11),
  }));
  /* Order parameter across the anisotropy scan: flat through the gapless XY
     phase, then a hard corner at the BKT point and a fast climb into the
     Ising ferromagnet. The corner is the whole point, so keep it sharp. */
  const order = polyline(160, (t) => {
    const y = t < 0.5
      ? 74 - 5 * Math.pow(t / 0.5, 2)
      : 69 - 38 * Math.pow((t - 0.5) / 0.5, 0.55);
    return [66 + t * 448, y];
  });
  const net = [];
  const layers = [5, 3, 1];
  layers.forEach((count, li) => {
    for (let i = 0; i < count; i++) {
      net.push({
        x: r1(290 - ((count - 1) * 30) / 2 + i * 30),
        y: 132 - li * 22,
        delay: r1((li * 5 + i) * 0.17),
      });
    }
  });
  const links = [];
  for (let li = 0; li < layers.length - 1; li++) {
    const a = net.filter((_, k) => k >= layers.slice(0, li).reduce((s, v) => s + v, 0) && k < layers.slice(0, li + 1).reduce((s, v) => s + v, 0));
    const b = net.filter((_, k) => k >= layers.slice(0, li + 1).reduce((s, v) => s + v, 0) && k < layers.slice(0, li + 2).reduce((s, v) => s + v, 0));
    a.forEach((p) => b.forEach((q) => links.push([p, q])));
  }
  return { spins, order, net, links };
})();

function ArtQuantumMl() {
  return (
    <svg className="pa pa-qml" viewBox={ART_VB} preserveAspectRatio="xMidYMid meet">
      <g className="pa-ink">
        <polyline points={QML.order} className="pa-order" />
        {QML.links.map((l, i) => (
          <line key={i} x1={l[0].x} y1={l[0].y} x2={l[1].x} y2={l[1].y} className="pa-link" />
        ))}
        {QML.net.map((p, i) => (
          <circle key={i} cx={p.x} cy={p.y} r="2.4" className="pa-neuron pa-dot"
            style={{ animationDelay: `${p.delay}s` }} />
        ))}
        {QML.spins.map((s, i) => (
          <g key={i} transform={`translate(${s.x} 182)`}>
            <g className="pa-spin" style={{ animationDelay: `${s.delay}s` }}>
              <g transform={s.up ? "" : "rotate(180)"}>
                <line x1="0" y1="9" x2="0" y2="-9" className="pa-arrow" />
                <polyline points="-3,-5 0,-9.5 3,-5" className="pa-arrow" />
              </g>
            </g>
          </g>
        ))}
      </g>
    </svg>
  );
}

/* ------------------------------------------------------------------ *
 * 5. Energy Demand Prediction — load curve with a forecast that peels
 *    away, and a scan line walking the horizon.
 * ------------------------------------------------------------------ */
const ENERGY = (() => {
  const shape = (t) => 126 - 42 * Math.pow(Math.sin(2 * Math.PI * 6 * t), 3) + 5 * Math.sin(2 * Math.PI * 2.3 * t);
  const load = polyline(220, (t) => [40 + t * 500, shape(t)]);
  const fc = polyline(220, (t) => {
    const div = Math.pow(Math.max(0, (t - 0.64) / 0.36), 2);
    return [40 + t * 500, shape(t) - div * 19];
  });
  return { load, fc };
})();

function ArtEnergy() {
  return (
    <svg className="pa pa-energy" viewBox={ART_VB} preserveAspectRatio="xMidYMid meet">
      <defs><SweepGradient id="sw-energy" peak="0.13" /></defs>
      <g className="pa-ink">
        <line x1="34" y1="182" x2="546" y2="182" className="pa-axis" />
        {Array.from({ length: 13 }, (_, i) => (
          <line key={i} x1={r1(40 + (i * 500) / 12)} y1="182"
            x2={r1(40 + (i * 500) / 12)} y2="188" className="pa-tick" />
        ))}
        <polyline points={ENERGY.load} className="pa-load" />
        <polyline points={ENERGY.fc} className="pa-fc" />
      </g>
      <g className="pa-scan">
        <rect x="-70" y="0" width="140" height="220" fill="url(#sw-energy)" />
        <line x1="0" y1="24" x2="0" y2="192" className="pa-scanline" />
      </g>
    </svg>
  );
}

/* ------------------------------------------------------------------ *
 * 6. Arbitrage-Free SABR — the Hagan-2014 negative-density region
 *    breathing open and closed, beside a finite-difference stencil.
 * ------------------------------------------------------------------ */
const AFSABR = (() => {
  const f = (t) =>
    150 - (Math.exp(-Math.pow(t - 0.56, 2) / (2 * 0.0225)) -
      0.3 * Math.exp(-Math.pow(t - 0.145, 2) / (2 * 0.0027))) * 84;
  const dens = polyline(140, (t) => [150 + t * 380, f(t)]);
  /* Hatch only where the density is genuinely negative, i.e. where the curve
     sits below the zero line. Sampling a fixed t-window leaves a stray sliver
     poking above it once the curve has already crossed back. */
  const below = [];
  for (let i = 0; i <= 220; i++) {
    const u = i / 220;
    if (f(u) > 150.15) below.push([150 + u * 380, f(u)]);
  }
  const dipArea = below.length
    ? `${r1(below[0][0])},150 ` +
      below.map((p) => r1(p[0]) + "," + r1(p[1])).join(" ") +
      ` ${r1(below[below.length - 1][0])},150`
    : "";
  return { dens, dipArea };
})();

function ArtAfSabr() {
  return (
    <svg className="pa pa-afsabr" viewBox={ART_VB} preserveAspectRatio="xMidYMid meet">
      <defs>
        <pattern id="hatch-af" width="5" height="5" patternTransform="rotate(45)"
          patternUnits="userSpaceOnUse">
          <line x1="0" y1="0" x2="0" y2="5" stroke="#fff" strokeOpacity="0.6" strokeWidth="0.8" />
        </pattern>
      </defs>
      <g className="pa-ink">
        <line x1="120" y1="150" x2="556" y2="150" className="pa-zero" />
        <g className="pa-dip">
          <polygon points={AFSABR.dipArea} className="pa-hatch" fill="url(#hatch-af)" />
        </g>
        <polyline points={AFSABR.dens} className="pa-dens" />
        <g transform="translate(64 116)">
          {[[0, 0], [0, -20], [0, 20], [-20, 0], [20, 0]].map((p, i) => (
            <g key={i}>
              {i > 0 && <line x1="0" y1="0" x2={p[0]} y2={p[1]} className="pa-stencil-arm" />}
              <circle cx={p[0]} cy={p[1]} r="2.2" className="pa-stencil pa-dot"
                style={{ animationDelay: `${i * 0.26}s` }} />
            </g>
          ))}
          {[-1, 0, 1].map((i) =>
            [-1, 0, 1].map((j) => (
              <rect key={i + ":" + j} x={i * 20 - 10} y={j * 20 - 10} width="20" height="20"
                className="pa-stencil-cell" />
            ))
          )}
        </g>
      </g>
    </svg>
  );
}

/* ------------------------------------------------------------------ *
 * 7. QuantumFolio — candidate portfolios twinkling below the frontier
 *    while the solver steps through a circuit.
 * ------------------------------------------------------------------ */
const QFOLIO = (() => {
  const frontier = polyline(64, (t) => [76 + t * 300, 176 - 96 * Math.sqrt((t + 0.03) / 1.03)]);
  const rnd = artRng(3);
  const cloud = [];
  for (let i = 0; i < 22; i++) {
    const t = rnd();
    cloud.push({
      x: r1(78 + t * 292),
      y: r1(176 - 92 * Math.sqrt((t + 0.03) / 1.03) + 8 + rnd() * 26),
      delay: r1(rnd() * 3.4),
    });
  }
  const best = [r1(76 + 0.62 * 300), r1(176 - 96 * Math.sqrt(0.65 / 1.03))];
  const gates = [0, 1, 2, 3, 4, 5].map((i) => ({
    x: 424 + i * 22, wire: [0, 2, 1, 3, 0, 2][i], delay: r1(i * 0.26),
  }));
  return { frontier, cloud, best, gates };
})();

function ArtQuantumFolio() {
  return (
    <svg className="pa pa-qfolio" viewBox={ART_VB} preserveAspectRatio="xMidYMid meet">
      <g className="pa-ink">
        <line x1="70" y1="180" x2="392" y2="180" className="pa-axis" />
        <line x1="70" y1="180" x2="70" y2="66" className="pa-axis" />
        <polyline points={QFOLIO.frontier} className="pa-frontier" />
        {QFOLIO.cloud.map((p, i) => (
          <circle key={i} cx={p.x} cy={p.y} r="1.9" className="pa-cand pa-dot"
            style={{ animationDelay: `${p.delay}s` }} />
        ))}
        <g transform={`translate(${QFOLIO.best[0]} ${QFOLIO.best[1]})`}>
          <circle r="2.8" className="pa-best pa-dot" />
          <circle r="8" className="pa-best-halo" />
        </g>
        <g>
          {[0, 1, 2, 3].map((w) => (
            <line key={w} x1="410" y1={82 + w * 20} x2="548" y2={82 + w * 20} className="pa-wire" />
          ))}
          {QFOLIO.gates.map((g, i) => (
            <g key={i}>
              {i % 2 === 0 && (
                <g>
                  <line x1={g.x} y1={82 + g.wire * 20} x2={g.x} y2={82 + (g.wire + 1) * 20}
                    className="pa-gate-link" style={{ animationDelay: `${g.delay}s` }} />
                  <circle cx={g.x} cy={82 + (g.wire + 1) * 20} r="2" className="pa-gate-link pa-dot"
                    style={{ animationDelay: `${g.delay}s` }} />
                </g>
              )}
              <rect x={g.x - 4.5} y={82 + g.wire * 20 - 4.5} width="9" height="9"
                className="pa-gate" style={{ animationDelay: `${g.delay}s` }} />
            </g>
          ))}
        </g>
      </g>
    </svg>
  );
}

/* ------------------------------------------------------------------ *
 * 8. Review Summarizer — prose flowing through the pipeline and
 *    landing as structured rows.
 * ------------------------------------------------------------------ */
const SUMM = (() => {
  const rnd = artRng(11);
  const docs = [0, 1, 2, 3].map((i) => ({
    y: 52 + i * 38,
    lines: [0, 1, 2, 3].map(() => r1(52 + rnd() * 74)),
    delay: r1(i * 1.15),
  }));
  const rows = [0, 1, 2].map((i) => ({ y: 74 + i * 36, delay: r1(1.2 + i * 0.55) }));
  return { docs, rows };
})();

function ArtReviewSummarizer() {
  return (
    <svg className="pa pa-summ" viewBox={ART_VB} preserveAspectRatio="xMidYMid meet">
      <g className="pa-ink">
        <line x1="46" y1="110" x2="534" y2="110" className="pa-bus" />
        {SUMM.docs.map((d, i) => (
          <g key={i} className="pa-doc" style={{ animationDelay: `${d.delay}s` }}>
            {d.lines.map((w, j) => (
              <line key={j} x1="44" y1={d.y + j * 8} x2={44 + w} y2={d.y + j * 8} className="pa-prose" />
            ))}
          </g>
        ))}
        <g transform="translate(276 110)">
          <rect x="-28" y="-22" width="56" height="44" rx="9" className="pa-funnel" />
          <circle r="2.6" className="pa-funnel-dot pa-dot" />
        </g>
        {SUMM.rows.map((r, i) => (
          <g key={i} className="pa-rec" style={{ animationDelay: `${r.delay}s` }}>
            <circle cx="376" cy={r.y} r="3" className="pa-rec-dot pa-dot" />
            <line x1="388" y1={r.y} x2="492" y2={r.y} className="pa-rec-bar" />
            <line x1="388" y1={r.y + 8} x2="444" y2={r.y + 8} className="pa-rec-bar dim" />
            <line x1="452" y1={r.y + 8} x2="478" y2={r.y + 8} className="pa-rec-bar dim" />
          </g>
        ))}
      </g>
    </svg>
  );
}

/* ------------------------------------------------------------------ *
 * 9. Delta-Vega Hedging — unhedged vega residual against a tight
 *    delta-vega residual, scrolling seamlessly.
 * ------------------------------------------------------------------ */
const DV = (() => {
  const N = 116;
  /* delta-only leaves the vega exposure unhedged, so its residual swings
     wide; delta-vega stays inside a narrow band. The contrast is the point,
     so keep the amplitudes far apart. */
  const wild = (i) => {
    const t = i / N;
    return 46 * Math.sin(2 * Math.PI * 1.7 * t) * Math.sin(2 * Math.PI * 0.5 * t) +
      17 * Math.sin(2 * Math.PI * 5.3 * t + 1.1);
  };
  const tight = (i) => {
    const t = i / N;
    return 3.6 * Math.sin(2 * Math.PI * 3.1 * t + 0.4) + 1.8 * Math.sin(2 * Math.PI * 8.7 * t);
  };
  const tile = (f) => {
    const out = [];
    for (let i = 0; i <= N * 2; i++) out.push(r1((i * 580) / N) + "," + r1(110 + f(i % N)));
    return out.join(" ");
  };
  return { wild: tile(wild), tight: tile(tight) };
})();

function ArtDeltaVega() {
  return (
    <svg className="pa pa-dv" viewBox={ART_VB} preserveAspectRatio="xMidYMid meet">
      <g className="pa-ink">
        <line x1="0" y1="110" x2="580" y2="110" className="pa-mid" />
        <g className="pa-drift">
          <polyline points={DV.wild} className="pa-resid" />
        </g>
        <g className="pa-drift slow">
          <polyline points={DV.tight} className="pa-resid tight" />
        </g>
        {Array.from({ length: 17 }, (_, i) => (
          <line key={i} x1={r1(30 + (i * 520) / 16)} y1="192"
            x2={r1(30 + (i * 520) / 16)} y2="200" className="pa-tick" />
        ))}
        <g transform="translate(556 0)">
          <g className="pa-env">
            <line x1="0" y1="62" x2="0" y2="158" className="pa-ddline" />
            <line x1="-4" y1="62" x2="4" y2="62" className="pa-ddcap" />
            <line x1="-4" y1="158" x2="4" y2="158" className="pa-ddcap" />
          </g>
        </g>
      </g>
    </svg>
  );
}

/* ------------------------------------------------------------------ *
 * 10. Clean Option Data — a Greeks matrix with holes being backfilled
 *     as the pipeline sweeps through.
 * ------------------------------------------------------------------ */
const ODATA = (() => {
  const cols = 15, rows = 5, cw = 32, ch = 26;
  const x0 = (580 - cols * cw) / 2, y0 = (220 - rows * ch) / 2 - 8;
  const rnd = artRng(23);
  const cells = [];
  for (let r = 0; r < rows; r++)
    for (let c = 0; c < cols; c++) {
      const gap = rnd() < 0.17;
      cells.push({
        x: r1(x0 + c * cw), y: r1(y0 + r * ch), gap,
        delay: r1((c / cols) * 5.2),
      });
    }
  return { cols, rows, cw, ch, x0, y0, cells };
})();

function ArtOptionData() {
  return (
    <svg className="pa pa-odata" viewBox={ART_VB} preserveAspectRatio="xMidYMid meet">
      <defs><SweepGradient id="sw-odata" peak="0.12" /></defs>
      <g className="pa-ink">
        {ODATA.cells.map((c, i) => (
          <g key={i} transform={`translate(${c.x} ${c.y})`}>
            <rect width={ODATA.cw} height={ODATA.ch}
              className={"pa-cell" + (c.gap ? " gap" : "")} />
            {c.gap ? (
              <circle cx={ODATA.cw / 2} cy={ODATA.ch / 2} r="1.8" className="pa-fill pa-dot"
                style={{ animationDelay: `${c.delay}s` }} />
            ) : (
              <circle cx={ODATA.cw / 2} cy={ODATA.ch / 2} r="1.4" className="pa-datum pa-dot" />
            )}
          </g>
        ))}
        <line x1={ODATA.x0} y1="188" x2={r1(ODATA.x0 + ODATA.cols * ODATA.cw)} y2="188"
          className="pa-axis" />
        {Array.from({ length: ODATA.cols + 1 }, (_, i) => (
          <line key={i} x1={r1(ODATA.x0 + i * ODATA.cw)} y1="188"
            x2={r1(ODATA.x0 + i * ODATA.cw)} y2="193" className="pa-tick" />
        ))}
      </g>
      <g className="pa-fillsweep">
        <rect x="-70" y="0" width="140" height="220" fill="url(#sw-odata)" />
      </g>
    </svg>
  );
}

/* ------------------------------------------------------------------ *
 * 11. CDS Pricing for FX — time value bleeding out of the premium
 *     curve until it settles onto intrinsic payoff.
 * ------------------------------------------------------------------ */
const CDS = (() => {
  const k = 0.44, x0 = 76, w = 414, base = 168;
  const payoff = `${x0},${base} ${r1(x0 + k * w)},${base} ${r1(x0 + w)},${r1(base - (1 - k) * 150)}`;
  const premium = polyline(96, (t) => {
    const intr = Math.max(0, t - k) * 150;
    const tv = 30 * Math.exp(-Math.pow(t - k, 2) / (2 * 0.04));
    return [x0 + t * w, base - intr - tv];
  });
  return { payoff, premium, strike: r1(x0 + k * w), base };
})();

function ArtCdsFx() {
  return (
    <svg className="pa pa-cds" viewBox={ART_VB} preserveAspectRatio="xMidYMid meet">
      <g className="pa-ink">
        {Array.from({ length: 5 }, (_, r) =>
          Array.from({ length: 7 }, (_, c) => (
            <rect key={r + ":" + c} x={92 + c * 58} y={44 + r * 26} width="58" height="26"
              className="pa-sheet" />
          ))
        )}
        {/* axis sits below the payoff base so the zero-value leg stays readable */}
        <line x1="66" y1="184" x2="520" y2="184" className="pa-axis" />
        <line x1="66" y1="184" x2="66" y2="40" className="pa-axis" />
        <polyline points={CDS.payoff} className="pa-payoff" />
        <g className="pa-theta">
          <polyline points={CDS.premium} className="pa-premium" />
        </g>
        <g transform={`translate(${CDS.strike} 0)`}>
          <line x1="0" y1={CDS.base} x2="0" y2={CDS.base - 10} className="pa-strike" />
          <circle cy={CDS.base} r="2.4" className="pa-strike-dot pa-dot" />
        </g>
      </g>
    </svg>
  );
}

/* ------------------------------------------------------------------ */

const PROJECT_ART = {
  "quantum-ecc": ArtQuantumEcc,
  merton: ArtMerton,
  sabr: ArtSabr,
  "quantum-ml": ArtQuantumMl,
  energy: ArtEnergy,
  "sabr-py": ArtAfSabr,
  quantumfolio: ArtQuantumFolio,
  "review-summarizer": ArtReviewSummarizer,
  "delta-vega": ArtDeltaVega,
  "option-data": ArtOptionData,
  "cds-fx": ArtCdsFx,
};

function ProjectArt({ id }) {
  const Art = PROJECT_ART[id];
  return Art ? <Art /> : null;
}

Object.assign(window, { PROJECT_ART, ProjectArt });
