/* osc-compare.jsx — The Oscillator bench: a discrete two-transistor astable
   and an NE555 astable, wired to the SAME R·C timing, blinking in lockstep so
   you can see they are the same machine. A shared oscilloscope reads any of the
   eight 555 pins and overlays the matching node on the discrete board, proving
   the two circuits carry the same signals.

   Self-contained (own LED glyph, own clock) so it has no load-order deps.
   All names OC_/oc prefixed to stay unique in the shared Babel scope. */

const OC_LN2 = Math.log(2);
const { useState: ocUseState, useEffect: ocUseEffect, useRef: ocUseRef } = React;

/* ── shared real-time clock ──────────────────────────────────────────────
   One phase 0..1 drives BOTH boards and the scope, so they are always in
   lockstep. The on-screen period is clamped to a watchable 1.2–6 s window;
   the TRUE frequency from R·C is reported separately. */
function ocUsePhase(periodSec, running) {
  const [phase, setPhase] = ocUseState(0);
  const pRef = ocUseRef(0);
  const perRef = ocUseRef(periodSec);
  perRef.current = Math.max(1.2, Math.min(6, periodSec));
  ocUseEffect(() => {
    if (!running) return;
    let raf, last = performance.now();
    const tick = (now) => {
      const dt = (now - last) / 1000; last = now;
      pRef.current = (pRef.current + dt / perRef.current) % 1;
      setPhase(pRef.current);
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [running]);
  return phase;
}

/* ── the canonical 555-astable pin model ─────────────────────────────────
   Given the phase and duty, returns every pin's voltage as a fraction of Vcc.
   This is the textbook astable: TRIG(2) & THR(6) tie to the cap; the cap ramps
   ⅓→⅔ while OUT(3) is HIGH, then ⅔→⅓ while OUT is LOW and DIS(7) sinks it. */
function ocPinModel(phase, duty) {
  const charging = phase < duty;
  const u = charging ? phase / duty : (phase - duty) / (1 - duty);
  // cap voltage, normalized to Vcc, bouncing between ⅓ and ⅔
  const vcap = charging ? 1 - (2 / 3) * Math.exp(-OC_LN2 * u)
                        : (2 / 3) * Math.exp(-OC_LN2 * u);
  const out = charging ? 1 : 0;
  return {
    charging, vcap, out,
    pins: {
      1: { v: 0,            label: "GND",     name: "Ground" },
      2: { v: vcap,         label: "TRIG",    name: "Trigger" },
      3: { v: out,          label: "OUT",     name: "Output" },
      4: { v: 1,            label: "RST",     name: "Reset" },
      5: { v: 2 / 3,        label: "CTRL",    name: "Control" },
      6: { v: vcap,         label: "THR",     name: "Threshold" },
      7: { v: charging ? vcap : 0, label: "DIS", name: "Discharge" },
      8: { v: 1,            label: "VCC",     name: "Supply" },
    },
  };
}

/* For the discrete two-transistor astable, the comparable nodes:
   OUT  ↔ Q1 collector square wave (in phase with the 555 output)
   THR/TRIG ↔ a base node ramp (cap charging through R — same shape as the 555
   cap node). Pins with no discrete counterpart return null. */
function ocDiscreteNode(pinNo, phase) {
  // discrete astable: 50% duty, symmetric
  const m = ocPinModel(phase, 0.5);
  switch (pinNo) {
    case 3: return m.out;                       // collector square
    case 2: case 6: return m.vcap;              // base/cap ramp
    case 1: return 0;                           // ground shared
    case 8: return 1;                           // supply shared
    default: return null;                       // RST/CTRL/DIS are 555-internal
  }
}

/* ── shared LED glyph ─────────────────────────────────────────────────── */
function OcLed({ cx, cy, on, r = 13 }) {
  return (
    <g style={{ pointerEvents: "none" }}>
      {on && <circle cx={cx} cy={cy} r={r + 17} fill="var(--current)" opacity="0.16" />}
      {on && <circle cx={cx} cy={cy} r={r + 7} fill="var(--current)" opacity="0.3" />}
      <circle cx={cx} cy={cy} r={r} fill={on ? "var(--current)" : "var(--bg-card)"}
              stroke={on ? "var(--current-deep)" : "var(--current-soft)"} strokeWidth="2"
              style={{ transition: "fill 90ms linear" }} />
      {on && <circle cx={cx - 4} cy={cy - 4} r={r * 0.26} fill="var(--current-soft)" opacity="0.9" />}
    </g>
  );
}

const ocWire = (d, c, w, dash) =>
  <path d={d} fill="none" stroke={c} strokeWidth={w || 2.4} strokeLinecap="round"
        strokeLinejoin="round" strokeDasharray={dash || "none"} />;
const ocRes = (x, y, vertical, label, c) => (
  <g>
    {vertical
      ? <rect x={x - 7} y={y - 15} width="14" height="30" rx="2.5" fill="var(--bg-card)" stroke={c || "var(--ink)"} strokeWidth="1.8" />
      : <rect x={x - 15} y={y - 7} width="30" height="14" rx="2.5" fill="var(--bg-card)" stroke={c || "var(--ink)"} strokeWidth="1.8" />}
    {label && <text x={vertical ? x + 13 : x} y={vertical ? y + 4 : y - 12} textAnchor={vertical ? "start" : "middle"}
                    fontFamily="IBM Plex Mono, monospace" fontSize="13" fill={c || "var(--ink-soft)"}>{label}</text>}
  </g>
);

/* ─────────────────────────────────────────────────────────────────────────
   The DISCRETE board: 2 transistors, 2 caps, 2 collector R, 2 base R, 2 LEDs.
   Q1 lit while charging (OUT high); Q2 antiphase.
   ───────────────────────────────────────────────────────────────────────── */
function OcDiscreteBoard({ phase, scopedPin, kids }) {
  const W = 440, H = 340;
  const railP = 44, railN = 300;
  const xL = 150, xR = 310;
  const ledY = 104, qY = 232;
  const m = ocPinModel(phase, 0.5);
  const q1On = m.charging;          // Q1 conducts (its LED + collector LOW) — show LED lit while ON
  // OUT node we compare = Q1 collector; lit LED tracks q1On
  const scopeHitsOut = scopedPin === 3;
  const scopeHitsCap = scopedPin === 2 || scopedPin === 6;

  const branch = (x, on, tag, baseSide) => (
    <g>
      {ocWire(`M${x} ${railP} L${x} ${ledY - 32}`, "var(--ink-soft)", 2.2)}
      {ocRes(x, ledY - 32, false, "R", scopeHitsOut ? "var(--current-deep)" : "var(--ink-soft)")}
      {ocWire(`M${x} ${ledY - 18} L${x} ${ledY - 13}`, "var(--ink-soft)", 2.2)}
      <OcLed cx={x} cy={ledY} on={on} r={12} />
      {ocWire(`M${x} ${ledY + 13} L${x} ${qY - 24}`, on ? "var(--current)" : "var(--ink-faint)", on ? 3 : 2.2)}
      {/* transistor */}
      <g transform={`translate(${x} ${qY})`}>
        <circle r="20" fill="var(--bg-card)" stroke={on ? "var(--current)" : "var(--ink-soft)"} strokeWidth="2.3" />
        <line x1="-8" y1="-10" x2="-8" y2="10" stroke={on ? "var(--current)" : "var(--ink-soft)"} strokeWidth="3" />
        <line x1="-8" y1="-3" x2="8" y2="-12" stroke={on ? "var(--current)" : "var(--ink-soft)"} strokeWidth="2.3" />
        <line x1="-8" y1="3" x2="8" y2="12" stroke={on ? "var(--current)" : "var(--ink-soft)"} strokeWidth="2.3" />
        <text x="0" y="34" textAnchor="middle" fontFamily="IBM Plex Mono, monospace" fontSize="15"
              fill={on ? "var(--current)" : "var(--ink-faint)"}>{tag}</text>
      </g>
      {ocWire(`M${x} ${qY + 20} L${x} ${railN}`, on ? "var(--current)" : "var(--ink-faint)", on ? 3 : 2.2)}
      {/* base resistor up to + rail */}
      {ocWire(`M${x + baseSide} ${railP} L${x + baseSide} ${qY} L${x - 20} ${qY}`,
        scopeHitsCap ? "var(--water)" : "var(--ink-faint)", scopeHitsCap ? 2.4 : 1.8)}
    </g>
  );

  return (
    <svg viewBox={`0 0 ${W} ${H}`} width="100%" height="auto"
         preserveAspectRatio="xMidYMid meet" style={{ display: "block" }}>
      {ocWire(`M28 ${railP} L${W - 28} ${railP}`, "var(--current)", 3)}
      {ocWire(`M28 ${railN} L${W - 28} ${railN}`, "var(--water)", 3)}
      <text x="30" y={railP - 10} fontFamily="IBM Plex Mono, monospace" fontSize="15" fill="var(--current)">+9V</text>
      <text x="30" y={railN + 22} fontFamily="IBM Plex Mono, monospace" fontSize="15" fill="var(--water)">GND</text>

      {/* the two cross-coupling caps */}
      {[{ from: xL, to: xR, charging: !q1On }, { from: xR, to: xL, charging: q1On }].map((c, i) => {
        const my = 168 + i * 22;
        return (
          <g key={i}>
            {ocWire(`M${c.from} ${ledY + 86} C ${(c.from + c.to) / 2} ${my}, ${(c.from + c.to) / 2} ${my}, ${c.to} ${qY - 4}`,
              c.charging ? (scopeHitsCap ? "var(--water)" : "var(--water-soft)") : "var(--rule-strong)",
              c.charging ? 2.6 : 1.8, c.charging ? "none" : "4 5")}
          </g>
        );
      })}

      {branch(xL, q1On, "Q1", -64)}
      {branch(xR, !q1On, "Q2", 64)}

      <text x={(xL + xR) / 2} y={188} textAnchor="middle" fontFamily="IBM Plex Mono, monospace"
            fontSize="13" fill="var(--water)">{kids ? "buckets fill, then flip" : "C charges → tips the other side"}</text>
      <text x={xL} y={ledY + 2} fontFamily="IBM Plex Mono, monospace" fontSize="11" fill="var(--ink-faint)"
            textAnchor="middle" transform={`translate(${-26} 0)`}>{scopeHitsOut ? "◄ OUT" : ""}</text>
    </svg>
  );
}

/* ─────────────────────────────────────────────────────────────────────────
   The 555 board: an 8-pin DIP wired as an astable (RA, RB, C, LED), with
   every pin numbered + labelled and a live voltage tag. The scoped pin glows.
   ───────────────────────────────────────────────────────────────────────── */
function Oc555Board({ phase, duty, scopedPin, onPickPin, kids }) {
  const W = 440, H = 340;
  const m = ocPinModel(phase, duty);
  const railP = 44, railN = 300;
  // chip body
  const bx = 150, bw = 140, by = 120, bh = 150;
  // pin geometry: 1–4 down the left, 5–8 down the right (DIP order, 8 top-right)
  const pinY = (row) => by + 28 + row * ((bh - 56) / 3);  // row 0..3
  const leftPins  = [1, 2, 3, 4];
  const rightPins = [8, 7, 6, 5];
  const pinPos = {};
  leftPins.forEach((p, r) => pinPos[p] = { x: bx, y: pinY(r), side: -1 });
  rightPins.forEach((p, r) => pinPos[p] = { x: bx + bw, y: pinY(r), side: +1 });

  const Pin = ({ n }) => {
    const pos = pinPos[n], info = m.pins[n];
    const sel = scopedPin === n;
    const stub = 18;
    const ex = pos.x + pos.side * stub;
    const lit = info.v > 0.55;
    return (
      <g style={{ cursor: "pointer" }} onClick={() => onPickPin(n)}>
        {ocWire(`M${pos.x} ${pos.y} L${ex} ${pos.y}`, sel ? "var(--current)" : (lit ? "var(--current-soft)" : "var(--ink-faint)"), sel ? 3.2 : 2.2)}
        <circle cx={ex} cy={pos.y} r={sel ? 6 : 4.5}
                fill={sel ? "var(--current)" : "var(--bg-card)"}
                stroke={sel ? "var(--current-deep)" : "var(--ink-soft)"} strokeWidth="1.8" />
        <text x={pos.x + pos.side * 4} y={pos.y - 7} textAnchor={pos.side < 0 ? "start" : "end"}
              fontFamily="IBM Plex Mono, monospace" fontSize="10.5"
              fill={sel ? "var(--current-deep)" : "var(--ink-soft)"}>{n} {info.label}</text>
      </g>
    );
  };

  return (
    <svg viewBox={`0 0 ${W} ${H}`} width="100%" height="auto"
         preserveAspectRatio="xMidYMid meet" style={{ display: "block" }}>
      {ocWire(`M28 ${railP} L${W - 28} ${railP}`, "var(--current)", 3)}
      {ocWire(`M28 ${railN} L${W - 28} ${railN}`, "var(--water)", 3)}
      <text x="30" y={railP - 10} fontFamily="IBM Plex Mono, monospace" fontSize="15" fill="var(--current)">+9V</text>
      <text x="30" y={railN + 22} fontFamily="IBM Plex Mono, monospace" fontSize="15" fill="var(--water)">GND</text>

      {/* VCC(8) + RST(4) to + rail ; GND(1) to − rail */}
      {ocWire(`M${pinPos[8].x + 18} ${pinPos[8].y} L${bx + bw + 30} ${pinPos[8].y} L${bx + bw + 30} ${railP}`, "var(--current-soft)", 2)}
      {ocWire(`M${pinPos[4].x - 18} ${pinPos[4].y} L${bx - 34} ${pinPos[4].y} L${bx - 34} ${railP}`, "var(--current-soft)", 2)}
      {ocWire(`M${pinPos[1].x - 18} ${pinPos[1].y} L${bx - 22} ${pinPos[1].y} L${bx - 22} ${railN}`, "var(--water-soft)", 2)}

      {/* RA: +rail → DIS(7) ; RB: DIS(7) → THR(6) ; C: THR(6) → GND */}
      {ocWire(`M${pinPos[7].x + 18} ${pinPos[7].y} L${bx + bw + 52} ${pinPos[7].y} L${bx + bw + 52} ${railP + 20}`, "var(--ink-soft)", 2)}
      {ocRes(bx + bw + 52, (railP + 20 + pinPos[7].y) / 2, true, "RA", "var(--ink-soft)")}
      {ocWire(`M${pinPos[7].x + 18} ${pinPos[7].y} L${bx + bw + 76} ${pinPos[7].y} L${bx + bw + 76} ${pinPos[6].y} L${pinPos[6].x + 18} ${pinPos[6].y}`, "var(--ink-soft)", 2)}
      {ocRes(bx + bw + 76, (pinPos[7].y + pinPos[6].y) / 2, true, "RB", "var(--ink-soft)")}
      {/* cap from THR(6) node to ground */}
      {ocWire(`M${pinPos[6].x + 18} ${pinPos[6].y} L${bx + bw + 100} ${pinPos[6].y} L${bx + bw + 100} ${railN - 26}`,
        (scopedPin === 6 || scopedPin === 2) ? "var(--water)" : "var(--ink-soft)", 2)}
      <g>
        <line x1={bx + bw + 92} y1={railN - 26} x2={bx + bw + 108} y2={railN - 26} stroke="var(--water-deep)" strokeWidth="2.4" />
        <line x1={bx + bw + 92} y1={railN - 20} x2={bx + bw + 108} y2={railN - 20} stroke="var(--water-deep)" strokeWidth="2.4" />
        <text x={bx + bw + 112} y={railN - 20} fontFamily="IBM Plex Mono, monospace" fontSize="13" fill="var(--water)">C</text>
      </g>
      {ocWire(`M${bx + bw + 100} ${railN - 18} L${bx + bw + 100} ${railN}`, "var(--water-soft)", 2)}

      {/* TRIG(2) tied to THR(6) */}
      {ocWire(`M${pinPos[2].x - 18} ${pinPos[2].y} L${bx - 50} ${pinPos[2].y} L${bx - 50} ${pinPos[2].y + 64} L${bx + bw + 88} ${pinPos[2].y + 64} L${bx + bw + 88} ${pinPos[6].y} `,
        "var(--ink-faint)", 1.6, "5 4")}

      {/* OUT(3) → LED → GND */}
      {ocWire(`M${pinPos[3].x - 18} ${pinPos[3].y} L${bx - 66} ${pinPos[3].y}`, m.out ? "var(--current)" : "var(--ink-faint)", m.out ? 3 : 2)}
      <OcLed cx={bx - 66} cy={pinPos[3].y + 30} on={!!m.out} r={12} />
      {ocWire(`M${bx - 66} ${pinPos[3].y} L${bx - 66} ${pinPos[3].y + 17}`, m.out ? "var(--current)" : "var(--ink-faint)", m.out ? 3 : 2)}
      {ocWire(`M${bx - 66} ${pinPos[3].y + 43} L${bx - 66} ${railN}`, m.out ? "var(--current)" : "var(--ink-faint)", m.out ? 3 : 2)}

      {/* chip body */}
      <rect x={bx} y={by} width={bw} height={bh} rx="9" fill="var(--bg-deeper)" stroke="var(--ink)" strokeWidth="2" />
      <path d={`M${bx + bw / 2 - 11} ${by} a11 11 0 0 0 22 0`} fill="none" stroke="var(--ink)" strokeWidth="2" />
      <text x={bx + bw / 2} y={by + bh / 2 + 6} textAnchor="middle" fontFamily="IBM Plex Mono, monospace"
            fontSize="18" fill="var(--ink)" letterSpacing="0.08em">NE555</text>
      <text x={bx + bw / 2} y={by + bh / 2 + 26} textAnchor="middle" fontFamily="IBM Plex Mono, monospace"
            fontSize="11" fill="var(--ink-faint)">{kids ? "tap a pin to scope it" : "tap any pin → scope"}</text>

      {[1, 2, 3, 4, 5, 6, 7, 8].map(n => <Pin key={n} n={n} />)}
    </svg>
  );
}

/* ─────────────────────────────────────────────────────────────────────────
   The shared OSCILLOSCOPE: pick a 555 pin; it plots that pin's waveform and,
   when the discrete board has a matching node, overlays it (dashed) so you can
   confirm they coincide.
   ───────────────────────────────────────────────────────────────────────── */
function OcScope({ scopedPin, duty, fTrue, dutyPct, running, kids }) {
  const W = 900, H = 250, x0 = 150, x1 = W - 24, y0 = 30, y1 = H - 54;
  const histRef = ocUseRef([]);
  const phRef = ocUseRef(0);
  const [, force] = ocUseState(0);
  // own sampling clock, synced to the same watchable period
  const perRef = ocUseRef(1);
  perRef.current = Math.max(1.2, Math.min(6, 1 / fTrue));
  ocUseEffect(() => {
    if (!running) return;
    let raf, last = performance.now(), acc = 0;
    const tick = (now) => {
      const dt = (now - last) / 1000; last = now;
      phRef.current = (phRef.current + dt / perRef.current) % 1;
      acc += dt;
      if (acc > 0.03) {
        acc = 0;
        const p = phRef.current;
        const fiveFifty = ocPinModel(p, duty);
        histRef.current.push({ p });
        if (histRef.current.length > 260) histRef.current.shift();
        force(n => n + 1);
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [running, duty]);

  const info = ocPinModel(0, duty).pins[scopedPin];
  const hist = histRef.current;
  const n = hist.length;
  const X = (i) => x0 + (i / 259) * (x1 - x0);
  const Yv = (v) => y1 - v * (y1 - y0);

  const v555 = (p) => ocPinModel(p, duty).pins[scopedPin].v;
  const vDisc = (p) => ocDiscreteNode(scopedPin, p);
  const has = vDisc(0) != null;

  const pts555 = hist.map((h, i) => `${X(i)},${Yv(v555(h.p))}`).join(" ");
  const ptsDisc = has ? hist.map((h, i) => `${X(i)},${Yv(vDisc(h.p))}`).join(" ") : "";

  return (
    <svg viewBox={`0 0 ${W} ${H}`} width="100%" height="auto"
         preserveAspectRatio="xMidYMid meet" style={{ display: "block" }}>
      <rect x="2" y="2" width={W - 4} height={H - 4} rx="10" fill="var(--bg-deeper)" stroke="var(--rule)" strokeWidth="1" />
      {/* reference lines */}
      {[{ v: 1, t: "Vcc" }, { v: 2 / 3, t: "⅔" }, { v: 1 / 3, t: "⅓" }, { v: 0, t: "0" }].map((g, i) => (
        <g key={i}>
          <line x1={x0} y1={Yv(g.v)} x2={x1} y2={Yv(g.v)} stroke="var(--rule-strong)" strokeWidth="1"
                strokeDasharray={g.v === 0 || g.v === 1 ? "none" : "4 5"} opacity="0.7" />
          <text x={x0 - 8} y={Yv(g.v) + 4} textAnchor="end" fontFamily="IBM Plex Mono, monospace" fontSize="12" fill="var(--ink-faint)">{g.t}</text>
        </g>
      ))}

      {/* axis labels / probed pin */}
      <text x={14} y={26} fontFamily="IBM Plex Mono, monospace" fontSize="14" fill="var(--current-deep)">
        PIN {scopedPin} · {info.label}
      </text>
      <text x={14} y={46} fontFamily="IBM Plex Mono, monospace" fontSize="11.5" fill="var(--ink-soft)">{info.name}</text>

      {/* the two traces */}
      {n > 1 && <polyline points={pts555} fill="none" stroke="var(--current)" strokeWidth="2.6" />}
      {has && n > 1 && <polyline points={ptsDisc} fill="none" stroke="var(--water)" strokeWidth="2.4" strokeDasharray="6 5" opacity="0.95" />}

      {/* legend */}
      <g fontFamily="IBM Plex Mono, monospace" fontSize="12">
        <line x1={14} y1={72} x2={40} y2={72} stroke="var(--current)" strokeWidth="2.6" />
        <text x={46} y={76} fill="var(--ink-soft)">555 pin</text>
        {has ? (
          <>
            <line x1={14} y1={94} x2={40} y2={94} stroke="var(--water)" strokeWidth="2.4" strokeDasharray="6 5" />
            <text x={46} y={98} fill="var(--ink-soft)">discrete node</text>
          </>
        ) : (
          <text x={14} y={98} fill="var(--ink-faint)" style={{ fontSize: 11 }}>{kids ? "(only the chip has this pin)" : "555-internal · no discrete node"}</text>
        )}
      </g>

      {/* match verdict + frequency */}
      <text x={x1} y={26} textAnchor="end" fontFamily="IBM Plex Mono, monospace" fontSize="13" fill="var(--ink-soft)">
        f ≈ {fTrue < 1 ? fTrue.toFixed(2) : fTrue.toFixed(1)} Hz · duty {dutyPct}%
      </text>
      {has && (
        <text x={x1} y={H - 14} textAnchor="end" fontFamily="IBM Plex Mono, monospace" fontSize="12.5" fill="var(--current-deep)">
          {scopedPin === 3
            ? (kids ? "both blink together ✓" : "both outputs square, same rate ✓")
            : (kids ? "same wiggle ✓" : "same ramp on both boards ✓")}
        </text>
      )}
    </svg>
  );
}

/* ─────────────────────────────────────────────────────────────────────────
   OscCompareBench — the whole mode. Shared R/C controls, two boards in
   lockstep, one scope.
   ───────────────────────────────────────────────────────────────────────── */
function OscCompareBench({ kids }) {
  const [running, setRunning] = ocUseState(true);
  const [scopedPin, setScopedPin] = ocUseState(3);   // OUT by default
  // shared timing parts (RA, RB in kΩ; C in µF). Duty from the 555 formula.
  const [RA, setRA] = ocUseState(10);
  const [RB, setRB] = ocUseState(47);
  const [C, setC]  = ocUseState(10);

  const tHigh = 0.693e-3 * (RA + RB) * C;   // s
  const tLow  = 0.693e-3 * RB * C;          // s
  const T = tHigh + tLow;
  const duty = tHigh / T;
  const fTrue = 1 / T;
  const dutyPct = Math.round(duty * 100);

  const phase = ocUsePhase(T, running);

  const ctlBtn = (active) => ({
    cursor: "pointer", fontSize: 12, padding: "5px 11px",
    background: active ? "var(--ink)" : "transparent",
    color: active ? "var(--bg-card)" : "var(--ink-soft)",
    border: `1.5px solid ${active ? "var(--ink)" : "var(--rule-strong)"}`,
    borderRadius: 7, fontFamily: "IBM Plex Mono, monospace",
  });
  const Row = ({ label, val, set, opts, unit }) => (
    <div style={{ display: "flex", alignItems: "center", gap: 7, flexWrap: "wrap" }}>
      <span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: 12, color: "var(--ink-faint)", width: 116 }}>{label}</span>
      {opts.map(o => <button key={o} style={ctlBtn(o === val)} onClick={() => set(o)}>{o} {unit}</button>)}
    </div>
  );

  return (
    <div className="oc-bench">
      <div className="oc-intro card" style={{ background: "transparent", padding: "14px 18px", marginBottom: 16, maxWidth: 860 }}>
        <div className="eyebrow" style={{ marginBottom: 8 }}>{kids ? "two blinkers, one beat" : "the same machine, built two ways"}</div>
        <p className="marg" style={{ margin: "0 0 4px", fontSize: 14.5 }}>
          {kids
            ? <>On the left, a blinker made from two transistors and two buckets. On the right, the same job done by one 555 chip. They share the SAME squeeze (R) and bucket (C) — so they blink together. Tap any chip pin to watch its wiggle.</>
            : <>Left: the discrete two-transistor astable. Right: an NE555 wired as an astable. Both run off the <em>same</em> R·C timing, so they oscillate in lockstep. Tap any of the 555's eight pins to scope it — where the discrete board has a matching node, the scope overlays it to prove the signals coincide.</>}
        </p>
      </div>

      {/* shared component controls */}
      <div className="card" style={{ background: "transparent", padding: "14px 18px", marginBottom: 16, maxWidth: 860, display: "grid", gap: 9 }}>
        <div className="eyebrow">shared timing · both circuits use these</div>
        <Row label={kids ? "squeeze A" : "R_A"} val={RA} set={setRA} opts={[10, 22, 47]} unit="kΩ" />
        <Row label={kids ? "squeeze B" : "R_B"} val={RB} set={setRB} opts={[10, 47, 100]} unit="kΩ" />
        <Row label={kids ? "bucket" : "C"} val={C} set={setC} opts={[1, 10, 47]} unit="µF" />
        <div style={{ display: "flex", alignItems: "center", gap: 12, marginTop: 2 }}>
          <button style={ctlBtn(running)} onClick={() => setRunning(r => !r)}>{running ? "⏸ pause" : "▶ run"}</button>
          <span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: 13, color: "var(--ink-soft)" }}>
            f ≈ {fTrue < 1 ? fTrue.toFixed(2) : fTrue.toFixed(1)} Hz · 555 duty {dutyPct}%
          </span>
        </div>
      </div>

      {/* the two boards */}
      <div className="oc-boards">
        <div className="oc-board-card">
          <div className="oc-board-head">
            <span className="oc-board-tag">DISCRETE</span>
            <span className="oc-board-sub">{kids ? "2 transistors + 2 buckets" : "two-transistor astable · 50% duty"}</span>
          </div>
          <OcDiscreteBoard phase={phase} scopedPin={scopedPin} kids={kids} />
        </div>
        <div className="oc-board-card">
          <div className="oc-board-head">
            <span className="oc-board-tag oc-tag-chip">NE555</span>
            <span className="oc-board-sub">{kids ? "one chip does it all" : "555 astable · RA, RB, C"}</span>
          </div>
          <Oc555Board phase={phase} duty={duty} scopedPin={scopedPin} onPickPin={setScopedPin} kids={kids} />
        </div>
      </div>

      {/* pin selector */}
      <div style={{ display: "flex", gap: 7, flexWrap: "wrap", margin: "16px 0 10px", alignItems: "center" }}>
        <span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: 12, color: "var(--ink-faint)" }}>scope pin:</span>
        {[1, 2, 3, 4, 5, 6, 7, 8].map(n => {
          const lbl = ocPinModel(0, duty).pins[n].label;
          return (
            <button key={n} style={ctlBtn(scopedPin === n)} onClick={() => setScopedPin(n)}>{n} {lbl}</button>
          );
        })}
      </div>

      {/* the scope */}
      <div className="oc-board-card" style={{ maxWidth: 920 }}>
        <OcScope scopedPin={scopedPin} duty={duty} fTrue={fTrue} dutyPct={dutyPct} running={running} kids={kids} />
      </div>

      <p className="marg" style={{ marginTop: 14, fontSize: 13, maxWidth: 860, color: "var(--ink-faint)" }}>
        {kids
          ? <>Pins 4, 5 and 7 live inside the chip — the transistor blinker doesn't have them, so the scope shows only the chip's line there.</>
          : <>Pins 1/3/2/6/8 map onto the discrete board (ground, output, the cap/base ramp, supply). Pins 4 (reset), 5 (control) and 7 (discharge) are internal to the 555 — no discrete counterpart. The discrete astable runs a clean 50% duty; the 555's is {dutyPct}% (set by R_A,R_B) — add a steering diode across R_B to pull it to 50%.</>}
      </p>
    </div>
  );
}

Object.assign(window, { OscCompareBench });
