/* coursemap.jsx — the single-screen PCB course map.
   One copper trace runs Start → Level 1 (theory) → Level 1 exam →
   Level 2 (build) → Master Engineer exam. Story is an optional strip on top.
   Pads = chapters; chips = capstone exams. Done pads are soldered (filled);
   the next pad pulses; the trace lights copper along your completed run.

   All names are cm-/CM- prefixed to avoid the shared global babel scope. */

const CM_VB = { W: 1160, H: 872 };
const CM_Y = { story: 88, l1: 250, l2: 410, l3: 592, tools: 760, spineTop: 170 };
const CM_SPINE_X = 66;

function cmLineXs(n, x0, x1) {
  if (n <= 1) return [(x0 + x1) / 2];
  const step = (x1 - x0) / (n - 1);
  return Array.from({ length: n }, (_, i) => x0 + i * step);
}

function cmBuildNodes(progress) {
  const done = (k) => progress[k] === "done";
  const inprog = (k) => progress[k] === "in-progress";
  const mk = (c, region, band, x, y) => ({
    ...c, kind: "pad", region, band, x, y,
    done: done(c.n), inprog: inprog(c.n), ready: c.ready !== false,
  });

  // Level 1 — the Water Rules (theory ch 1–5), left → right
  const l1ch = ALL_CHAPTERS.slice(0, 5);
  const l1xs = cmLineXs(l1ch.length, 156, 720);
  const lvl1 = l1ch.map((c, i) => mk(c, "lv1", 1, l1xs[i], CM_Y.l1));

  // Level 2 — the Parts Catalog (theory ch 6–10), then the theory exam
  const l2ch = ALL_CHAPTERS.slice(5, 10);
  const l2xs = cmLineXs(l2ch.length, 156, 720);
  const lvl2 = l2ch.map((c, i) => mk(c, "lv2", 2, l2xs[i], CM_Y.l2));
  const recap = {
    n: "recap", t: "The Recap", sub: "Every part, and why — before the exam",
    href: "recap.html", kind: "pad", region: "lv2", band: 2, x: 810, y: CM_Y.l2, lbl: "Recap",
    done: done("recap"), inprog: inprog("recap"), ready: true,
  };
  const cap1 = {
    n: "capstone", t: "The Theory Exam", sub: "Levels 1–2 finale · four design briefs",
    href: "capstone.html", kind: "chip", region: "lv2", x: 918, y: CM_Y.l2,
    done: done("capstone"), ready: true, label: ["THEORY", "EXAM"],
  };

  // Level 3 — the Build, left → right, then the Beacon + master exam
  const buildCh = L2_CHAPTERS.filter(c => !c.capstone);
  const beaconCh = L2_CHAPTERS.find(c => c.capstone);
  const l3xs = cmLineXs(buildCh.length, 156, 812);
  const lvl3 = buildCh.map((c, i) => mk(c, "lv3", 3, l3xs[i], CM_Y.l3));
  const beacon = beaconCh ? {
    ...beaconCh, kind: "beacon", region: "lv3", band: 3, x: 922, y: CM_Y.l3,
    sub: "Assemble the whole device",
    done: done(beaconCh.n), inprog: inprog(beaconCh.n), ready: beaconCh.ready !== false,
  } : null;
  const cap2 = {
    n: "capstone-l2", t: "The Build Exam", sub: "Master Engineer · final certificate",
    href: "capstone-l2.html", kind: "chip", region: "lv3", x: 1052, y: CM_Y.l3, final: true,
    done: done("capstone-l2"), ready: true, label: ["MASTER", "ENGINEER"],
  };

  // Tools & electives — optional bottom band
  const toolDefs = [
    ...M_CHAPTERS.map((c, i) => ({ ...c, lbl: "M" + (i + 1) })),
    { n: "tool-scope", t: "The Oscilloscope", sub: "Read signals like a pro", href: "scope.html", min: 15, lbl: "SCP" },
    { n: "tool-cl", t: "Circuit Lab", sub: "One circuit, two views, in sync", href: "circuit-lab.html", lbl: "CLB" },
    { n: "tool-bench", t: "The Workbench", sub: "Build it for real · breadboard practice", href: "sandbox.html", lbl: "WB" },
    { n: "tool-lab", t: "The Sandbox", sub: "See how it works · wire & probe anything", href: "flow-sandbox.html", lbl: "LAB" },
    { n: "tool-drill", t: "Today's Drill", sub: "Three quick questions a day", href: "daily.html", lbl: "DRL" },
  ];
  const mathxs = cmLineXs(toolDefs.length, 150, 632);
  const math = toolDefs.map((c, i) => ({
    ...c, kind: "story", region: "aux", x: mathxs[i], y: CM_Y.tools,
    done: done(c.n), ready: true,
  }));
  const l15xs = cmLineXs(L15_CHAPTERS.length, 772, 1024);
  const l15 = L15_CHAPTERS.map((c, i) => ({
    ...c, kind: "pad", region: "aux", x: l15xs[i], y: CM_Y.tools,
    done: done(c.n), inprog: inprog(c.n), ready: true,
  }));

  // Story — optional top band
  const storyxs = cmLineXs(L0_CHAPTERS.length, 330, 1014);
  const primer = {
    n: "L0-00", t: "Before You Start", sub: "Optional math primer",
    href: "primer.html", kind: "story", region: "l0", x: 150, y: CM_Y.story,
    done: done("L0-00"), ready: true, primer: true,
  };
  const story = L0_CHAPTERS.map((c, i) => ({
    ...c, kind: "story", region: "l0", x: storyxs[i], y: CM_Y.story,
    done: done(c.n), ready: c.ready !== false,
  }));

  const mainSeq = [...lvl1, ...lvl2, recap, ...lvl3, beacon].filter(Boolean);
  const current = mainSeq.find(c => c.ready && !c.done) || null;

  return { lvl1, lvl2, recap, lvl3, beacon, cap1, cap2, primer, story, math, l15, current };
}

// contiguous-done x along a band's nodes (left→right), for the lit copper overlay
function cmContigX(nodes) {
  let x = CM_SPINE_X;
  for (const n of nodes) { if (n && n.done) x = n.x; else break; }
  return x;
}

const cmToPath = (pts) => pts.map((p, i) => (i ? "L" : "M") + p[0] + " " + p[1]).join(" ");

/* Short, always-visible pad names. The rows have room for a word; full titles
   + subtitles still live in the hover tooltip. A few titles get hand-shortened
   so nothing crowds its neighbour. */
const CM_SHORT = {
  "The One-Way Valve": "Valve",
  "Power & Heat": "Power",
  "Sizing It Up": "Sizing",
  "The Adding Machine": "Adder",
  "Current Wars": "Wars",
};
function cmShort(node) {
  return CM_SHORT[node.t] || String(node.t || "").replace(/^The\s+/i, "");
}
function cmPadLabel(node) {
  if (node.lbl) return node.lbl;                                 // tools/math keep their codes
  if (node.kind === "story") return node.primer ? "Primer" : cmShort(node);
  if (node.region === "lv1" || node.region === "lv2") return parseInt(node.n, 10) + " \u00b7 " + cmShort(node);
  return cmShort(node);                                          // L3 builds + electives → name
}

/* ─── a single solder-pad (chapter) ──────────────────────────────────────── */
function CMPad({ node, isCurrent, onEnter, onLeave }) {
  const ready = node.ready;
  const r = node.kind === "story" ? 12 : 16;
  const ring = node.done ? "var(--current)" : isCurrent ? "var(--water)" : "var(--rule-strong)";
  const go = () => { if (ready) window.location.href = node.href; };
  const label = cmPadLabel(node);
  const labelSize = node.region === "lv3" ? 8.5 : node.kind === "story" ? 8.5 : 9;
  /* pad SHAPE tells you which level you're in — real PCB vocabulary:
     lv1 round through-hole (theory/flow) · lv2 square SMD footprint (parts
     catalog) · lv3 breadboard socket: square body, round hole (build for real) */
  const shape = node.region === "lv2" ? "smd" : node.region === "lv3" ? "socket" : "round";
  return (
    <g className="cm-pad" opacity={ready ? 1 : 0.4}
       style={{ cursor: ready ? "pointer" : "default" }}
       onClick={go} onMouseEnter={() => onEnter(node)} onMouseLeave={onLeave}
       onFocus={() => onEnter(node)} onBlur={onLeave}
       tabIndex={ready ? 0 : -1} role="link" aria-label={node.t + (node.done ? " — completed" : "")}>
      {isCurrent && !node.done && (
        <circle cx={node.x} cy={node.y} r={r + 4} fill="none" stroke="var(--water)" strokeWidth="2">
          <animate attributeName="r" values={`${r + 2};${r + 13}`} dur="1.9s" repeatCount="indefinite" />
          <animate attributeName="opacity" values="0.65;0" dur="1.9s" repeatCount="indefinite" />
        </circle>
      )}
      {shape === "round" && (<>
        <circle className="cm-pad-ring" cx={node.x} cy={node.y} r={r} fill="var(--bg)" stroke={ring} strokeWidth="2.4" />
        {node.done
          ? <circle cx={node.x} cy={node.y} r={r - 5} fill="var(--current)" />
          : <circle cx={node.x} cy={node.y} r={r - 6.5} fill="var(--bg-deeper)" stroke={ring} strokeWidth="1" opacity="0.7" />}
      </>)}
      {shape === "smd" && (<>
        <rect className="cm-pad-ring" x={node.x - r + 1} y={node.y - r + 1} width={(r - 1) * 2} height={(r - 1) * 2} rx="4.5"
              fill="var(--bg)" stroke={ring} strokeWidth="2.4" />
        {node.done
          ? <rect x={node.x - r + 6} y={node.y - r + 6} width={(r - 6) * 2} height={(r - 6) * 2} rx="3" fill="var(--current)" />
          : <rect x={node.x - r + 7.5} y={node.y - r + 7.5} width={(r - 7.5) * 2} height={(r - 7.5) * 2} rx="2.5"
                  fill="var(--bg-deeper)" stroke={ring} strokeWidth="1" opacity="0.7" />}
      </>)}
      {shape === "socket" && (<>
        <rect className="cm-pad-ring" x={node.x - r + 1} y={node.y - r + 1} width={(r - 1) * 2} height={(r - 1) * 2} rx="7.5"
              fill="var(--bg)" stroke={ring} strokeWidth="2.4" />
        {node.done
          ? <circle cx={node.x} cy={node.y} r={r - 5.5} fill="var(--current)" />
          : <circle cx={node.x} cy={node.y} r={r - 7} fill="var(--bg-deeper)" stroke={ring} strokeWidth="1" opacity="0.7" />}
      </>)}
      {node.done && (
        <text x={node.x} y={node.y} dy="0.34em" textAnchor="middle"
              fontFamily="IBM Plex Mono, monospace" fontSize="12" fontWeight="700"
              fill="var(--bg-card)">✓</text>
      )}
      <text x={node.x} y={node.y + r + 15} textAnchor="middle"
            fontFamily="IBM Plex Mono, monospace" fontSize={labelSize} letterSpacing="0.02em"
            fill={node.done ? "var(--current)" : isCurrent ? "var(--water)" : ready ? "var(--ink-soft)" : "var(--ink-faint)"}>
        {label}
      </text>
    </g>
  );
}

/* ─── a capstone chip (IC package) ───────────────────────────────────────── */
function CMChip({ node, onEnter, onLeave }) {
  const w = node.final ? 138 : 100, h = node.final ? 60 : 52;
  const x = node.x - w / 2, y = node.y - h / 2;
  const fill = node.done ? "var(--current)" : "var(--bg-card)";
  const stroke = node.done ? "var(--current)" : "var(--rule-strong)";
  const tcol = node.done ? "var(--bg-card)" : "var(--ink)";
  const pinN = node.final ? 4 : 3;
  const pins = cmLineXs(pinN, x + 16, x + w - 16);
  const go = () => { window.location.href = node.href; };
  return (
    <g className="cm-chip" style={{ cursor: "pointer" }}
       onClick={go} onMouseEnter={() => onEnter(node)} onMouseLeave={onLeave}
       onFocus={() => onEnter(node)} onBlur={onLeave}
       tabIndex={0} role="link" aria-label={node.t + (node.done ? " — completed" : "")}>
      {pins.map((px, i) => (
        <g key={i}>
          <line x1={px} y1={y - 7} x2={px} y2={y} stroke={stroke} strokeWidth="2.4" />
          <line x1={px} y1={y + h} x2={px} y2={y + h + 7} stroke={stroke} strokeWidth="2.4" />
        </g>
      ))}
      <rect x={x} y={y} width={w} height={h} rx="7" fill={fill} stroke={stroke} strokeWidth="2.2" />
      <circle cx={x + 11} cy={y + 11} r="3" fill="none" stroke={node.done ? "var(--bg-card)" : "var(--ink-faint)"} strokeWidth="1.4" />
      {node.label.map((ln, i) => (
        <text key={i} x={node.x} y={y + (node.final ? 26 : 24) + i * 15} textAnchor="middle"
              fontFamily="IBM Plex Mono, monospace" fontSize={node.final ? 12.5 : 11.5}
              fontWeight="600" letterSpacing="0.08em" fill={tcol}>
          {ln}
        </text>
      ))}
    </g>
  );
}

/* ─── the Beacon capstone — a featured, glowing device node ───────────────── */
function CMBeacon({ node, isCurrent, onEnter, onLeave }) {
  const ready = node.ready;
  const go = () => { if (ready) window.location.href = node.href; };
  const cx = node.x, cy = node.y, lit = node.done;
  const beamCol = lit ? "var(--current)" : isCurrent ? "var(--water)" : "var(--rule-strong)";
  return (
    <g className="cm-beacon" style={{ cursor: ready ? "pointer" : "default" }} opacity={ready ? 1 : 0.4}
       onClick={go} onMouseEnter={() => onEnter(node)} onMouseLeave={onLeave}
       onFocus={() => onEnter(node)} onBlur={onLeave} tabIndex={ready ? 0 : -1}
       role="link" aria-label={node.t + (lit ? " — completed" : "")}>
      {(isCurrent || lit) && (
        <circle cx={cx} cy={cy} r="26" fill="none" stroke={lit ? "var(--current)" : "var(--water)"} strokeWidth="2">
          <animate attributeName="r" values="22;36" dur="1.8s" repeatCount="indefinite" />
          <animate attributeName="opacity" values="0.55;0" dur="1.8s" repeatCount="indefinite" />
        </circle>
      )}
      {Array.from({ length: 8 }, (_, i) => {
        const a = (i / 8) * Math.PI * 2 - Math.PI / 2;
        return <line key={i} x1={cx + Math.cos(a) * 23} y1={cy + Math.sin(a) * 23}
                     x2={cx + Math.cos(a) * 31} y2={cy + Math.sin(a) * 31}
                     stroke={beamCol} strokeWidth="2.6" strokeLinecap="round" opacity={lit ? 0.95 : 0.5} />;
      })}
      <circle cx={cx} cy={cy} r="19" fill={lit ? "var(--current)" : "var(--bg)"} stroke={beamCol} strokeWidth="2.6" />
      <circle cx={cx} cy={cy} r="9" fill={lit ? "var(--bg-card)" : "var(--bg-deeper)"}
              stroke={lit ? "var(--bg-card)" : beamCol} strokeWidth="1.4" opacity={lit ? 0.9 : 0.7} />
      {lit && <text x={cx} y={cy} dy="0.34em" textAnchor="middle" fontFamily="IBM Plex Mono, monospace"
                    fontSize="11" fontWeight="700" fill="var(--current)">✓</text>}
      <text x={cx} y={cy + 40} textAnchor="middle" fontFamily="IBM Plex Mono, monospace"
            fontSize="11" fontWeight="600" letterSpacing="0.08em"
            fill={lit ? "var(--current)" : isCurrent ? "var(--water)" : "var(--ink)"}>THE BEACON</text>
      <text x={cx} y={cy + 54} textAnchor="middle" fontFamily="IBM Plex Mono, monospace"
            fontSize="8.5" letterSpacing="0.1em" fill="var(--ink-faint)">ASSEMBLE IT ALL</text>
    </g>
  );
}

/* ─── start / power node ─────────────────────────────────────────────────── */
function CMStart({ x, y }) {
  return (
    <g aria-hidden="true">
      <circle cx={x} cy={y} r="16" fill="var(--bg)" stroke="var(--current)" strokeWidth="2.4" />
      <line x1={x - 5} y1={y - 7} x2={x - 5} y2={y + 7} stroke="var(--current)" strokeWidth="3" />
      <line x1={x + 4} y1={y - 4} x2={x + 4} y2={y + 4} stroke="var(--current)" strokeWidth="3" />
      <text x={x} y={y + 31} textAnchor="middle" fontFamily="IBM Plex Mono, monospace"
            fontSize="10" letterSpacing="0.14em" fill="var(--ink-faint)">START</text>
    </g>
  );
}

/* ─── hover tooltip (drawn last, on top) ─────────────────────────────────── */
function CMTip({ node }) {
  const w = 218, h = node.sub ? 72 : 50;
  let x = node.x - w / 2;
  x = Math.max(18, Math.min(CM_VB.W - w - 18, x));
  const padR = node.kind === "story" ? 12 : node.kind === "chip" ? 34 : node.kind === "beacon" ? 34 : 16;
  let y = node.y - padR - h - 12;
  if (y < 18) y = node.y + padR + 12;
  const status = node.done ? "Completed ✓"
    : node.ready ? (node.kind === "chip" ? "Open exam →" : "Open lesson →")
    : "Coming soon";
  return (
    <g pointerEvents="none">
      <rect x={x} y={y} width={w} height={h} rx="9" fill="var(--ink)" />
      <rect x={x} y={y} width={w} height={h} rx="9" fill="none" stroke="var(--current)" strokeWidth="1" opacity="0.5" />
      <text x={x + 15} y={y + 26} fontFamily="Newsreader, serif" fontStyle="italic"
            fontSize="19" fill="var(--bg-card)">{node.t}</text>
      {node.sub && (
        <text x={x + 15} y={y + 46} fontFamily="IBM Plex Mono, monospace" fontSize="10.5"
              fill="var(--ink-faint)">{node.sub}</text>
      )}
      <text x={x + 15} y={y + (node.sub ? 62 : 40)} fontFamily="IBM Plex Mono, monospace"
            fontSize="9.5" letterSpacing="0.12em" fill="var(--current-soft)">
        {status.toUpperCase()}{node.min ? `  ·  ≈${node.min} MIN` : ""}
      </text>
    </g>
  );
}

/* ─── the board ──────────────────────────────────────────────────────────── */
/* ─── small zone glyphs — one quiet mark of what each level OFFERS ──────── */
function cmZoneGlyph(id, x, y) {
  const s = { fill: "none", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", opacity: 0.75 };
  switch (id) {
    case "l0": // an open book — the story
      return <g {...s} stroke="var(--ink-faint)"><path d={`M${x} ${y - 6} q -7 -4 -13 -1 v 13 q 6 -3 13 1 q 7 -4 13 -1 v -13 q -6 -3 -13 1 v 13`} /></g>;
    case "lv1": // a water drop — the water rules
      return <g {...s} stroke="var(--water)"><path d={`M${x} ${y - 8} q 7 9 7 13.5 a 7 7 0 0 1 -14 0 q 0 -4.5 7 -13.5 Z`} /></g>;
    case "lv2": // a resistor — the parts catalog
      return <g {...s} stroke="var(--current)"><path d={`M${x - 15} ${y} h 4 l 2.5 -6 l 5 12 l 5 -12 l 5 12 l 2.5 -6 h 4`} /></g>;
    case "lv3": // a breadboard corner — the build
      return (
        <g {...s} stroke="var(--current)">
          <rect x={x - 14} y={y - 9} width="28" height="18" rx="3" />
          {[-7, 0, 7].map(dx => <circle key={dx} cx={x + dx} cy={y - 3.5} r="1.1" fill="var(--current)" stroke="none" />)}
          {[-7, 0, 7].map(dx => <circle key={"b" + dx} cx={x + dx} cy={y + 3.5} r="1.1" fill="var(--current)" stroke="none" />)}
        </g>
      );
    case "tools": // a probe — tools & electives
      return <g {...s} stroke="var(--ink-faint)"><path d={`M${x - 10} ${y + 8} l 12 -12`} /><path d={`M${x + 2} ${y - 4} l 6 -6`} strokeWidth="3" /><circle cx={x - 10} cy={y + 8} r="2.2" fill="var(--ink-faint)" stroke="none" /></g>;
    default: return null;
  }
}

function CourseMap({ progress }) {
  const [hover, setHover] = React.useState(null);
  const { lvl1, lvl2, recap, lvl3, beacon, cap1, cap2, primer, story, math, l15, current } = cmBuildNodes(progress);
  const isCur = (node) => current && node.n === current.n;

  const storyAll = [primer, ...story];
  const sx = CM_SPINE_X;

  // current band (1/2/3) decides how far the vertical bus is soldered
  const curBand = current ? current.band : 3;
  const spineLitY = ({ 1: CM_Y.l1, 2: CM_Y.l2, 3: CM_Y.l3 })[curBand];

  // per-band horizontal traces: dashed base to endX, lit copper to contiguous-done x
  const bands = [
    { y: CM_Y.l1, endX: 720,    litX: cmContigX(lvl1) },
    { y: CM_Y.l2, endX: cap1.x, litX: cmContigX([...lvl2, cap1]) },
    { y: CM_Y.l3, endX: cap2.x, litX: cmContigX([...lvl3, beacon, cap2]) },
  ];

  const zones = [
    { id: "l0",  tag: "THE STORY · optional",        note: "history · read anytime",                  x: 30, y: 46,  w: CM_VB.W - 60, h: 84,  wash: null },
    { id: "lv1", tag: "LEVEL 1 · THE WATER RULES",   note: "chapters 1–5 · the rules of flow",         x: 30, y: 196, w: CM_VB.W - 60, h: 116, wash: "var(--water)" },
    { id: "lv2", tag: "LEVEL 2 · THE PARTS CATALOG", note: "chapters 6–10 · then the theory exam",      x: 30, y: 356, w: CM_VB.W - 60, h: 116, wash: "var(--current)" },
    { id: "lv3", tag: "LEVEL 3 · THE BUILD",         note: "nine builds → the Beacon → master exam",   x: 30, y: 536, w: CM_VB.W - 60, h: 132, wash: "var(--current)" },
    { id: "tools", tag: "TOOLS & ELECTIVES · optional", note: "math · scope · bench · drill · deeper theory", x: 30, y: 704, w: CM_VB.W - 60, h: 120, wash: null },
  ];

  return (
    <div className="pcb-wrap">
      <div className="pcb-scroll">
        <svg className="pcb" viewBox={`0 0 ${CM_VB.W} ${CM_VB.H}`} role="group" aria-label="Course map">
          {/* board substrate */}
          <rect x="12" y="14" width={CM_VB.W - 24} height={CM_VB.H - 28} rx="16"
                fill="var(--bg-card)" stroke="var(--rule)" strokeWidth="1.5"
                style={{ filter: "drop-shadow(0 16px 26px color-mix(in oklch, var(--ink) 22%, transparent))" }} />
          {[[34, 36], [CM_VB.W - 34, 36], [34, CM_VB.H - 36], [CM_VB.W - 34, CM_VB.H - 36]].map((p, i) => (
            <circle key={i} cx={p[0]} cy={p[1]} r="6" fill="var(--bg-deeper)" stroke="var(--rule-strong)" strokeWidth="1.4" />
          ))}

          {/* region zones — faint themed wash + a corner glyph per level */}
          {zones.map(z => (
            <g key={z.id}>
              <rect x={z.x} y={z.y} width={z.w} height={z.h} rx="11"
                    fill={z.wash ? `color-mix(in oklch, ${z.wash} 5%, var(--bg-deeper))` : "var(--bg-deeper)"} opacity="0.45"
                    stroke={z.wash ? `color-mix(in oklch, ${z.wash} 30%, var(--rule))` : "var(--rule)"} strokeWidth="1" strokeDasharray="2 6" />
              <rect x={z.x + 16} y={z.y - 11} width={z.tag.length * 7.4 + 20} height={22} rx="5"
                    fill="var(--bg)" stroke="var(--rule-strong)" strokeWidth="1" />
              <text x={z.x + 26} y={z.y + 4} fontFamily="IBM Plex Mono, monospace" fontSize="11"
                    fontWeight="600" letterSpacing="0.1em" fill="var(--ink)">{z.tag}</text>
              {cmZoneGlyph(z.id, z.x + z.w - 40, z.y + z.h - 26)}
              <text x={z.x + z.w - 16} y={z.y + 18} textAnchor="end"
                    fontFamily="IBM Plex Mono, monospace" fontSize="9.5" letterSpacing="0.1em"
                    fill="var(--ink-faint)">{z.note}</text>
            </g>
          ))}

          {/* ── vertical bus (spine) + per-level horizontal traces ── */}
          <path d={`M${sx} ${CM_Y.spineTop} L${sx} ${CM_Y.l3}`} fill="none"
                stroke="var(--rule-strong)" strokeWidth="2.4" strokeDasharray="2 7" />
          {spineLitY > CM_Y.spineTop && (
            <path d={`M${sx} ${CM_Y.spineTop} L${sx} ${spineLitY}`} fill="none"
                  stroke="var(--current)" strokeWidth="3.4" strokeLinecap="round" />
          )}
          {bands.map((b, i) => (
            <g key={"band" + i}>
              <line x1={sx} y1={b.y} x2={b.endX} y2={b.y} stroke="var(--rule-strong)"
                    strokeWidth="2.4" strokeDasharray="2 7" />
              {b.litX > sx + 1 && (
                <line x1={sx} y1={b.y} x2={b.litX} y2={b.y} stroke="var(--current)"
                      strokeWidth="3.4" strokeLinecap="round" />
              )}
              <circle cx={sx} cy={b.y} r="4.5" fill="var(--bg)" stroke="var(--current)" strokeWidth="2" />
            </g>
          ))}

          {/* START terminal at the top of the bus */}
          <g aria-hidden="true">
            <circle cx={sx} cy={CM_Y.spineTop} r="13" fill="var(--bg)" stroke="var(--current)" strokeWidth="2.2" />
            <line x1={sx - 4} y1={CM_Y.spineTop - 6} x2={sx - 4} y2={CM_Y.spineTop + 6} stroke="var(--current)" strokeWidth="2.6" />
            <line x1={sx + 4} y1={CM_Y.spineTop - 4} x2={sx + 4} y2={CM_Y.spineTop + 4} stroke="var(--current)" strokeWidth="2.6" />
            <text x={sx} y={CM_Y.spineTop - 20} textAnchor="middle" fontFamily="IBM Plex Mono, monospace"
                  fontSize="10" letterSpacing="0.14em" fill="var(--ink-faint)">START</text>
          </g>

          {/* story strip (optional, top) */}
          <path d={cmToPath(storyAll.map(n => [n.x, n.y]))} fill="none" stroke="var(--rule-strong)"
                strokeWidth="1.6" strokeDasharray="2 6" />
          {storyAll.map(n => (
            <CMPad key={n.n} node={n} isCurrent={false}
                   onEnter={setHover} onLeave={() => setHover(null)} />
          ))}

          {/* tools + electives (optional, bottom) */}
          <path d={cmToPath(math.map(n => [n.x, n.y]))} fill="none" stroke="var(--rule-strong)"
                strokeWidth="1.6" strokeDasharray="2 6" />
          {math.map(n => (
            <CMPad key={n.n} node={n} isCurrent={false}
                   onEnter={setHover} onLeave={() => setHover(null)} />
          ))}
          <path d={cmToPath(l15.map(n => [n.x, n.y]))} fill="none" stroke="var(--rule-strong)"
                strokeWidth="1.6" strokeDasharray="2 6" />
          {l15.map(n => (
            <CMPad key={n.n} node={n} isCurrent={false}
                   onEnter={setHover} onLeave={() => setHover(null)} />
          ))}

          {/* level pads (top → bottom: L1, L2 + theory exam, L3 + beacon + master) */}
          {lvl1.map(n => (
            <CMPad key={n.n} node={n} isCurrent={isCur(n)}
                   onEnter={setHover} onLeave={() => setHover(null)} />
          ))}
          {lvl2.map(n => (
            <CMPad key={n.n} node={n} isCurrent={isCur(n)}
                   onEnter={setHover} onLeave={() => setHover(null)} />
          ))}
          <CMPad node={recap} isCurrent={isCur(recap)}
                 onEnter={setHover} onLeave={() => setHover(null)} />
          <CMChip node={cap1} onEnter={setHover} onLeave={() => setHover(null)} />
          {lvl3.map(n => (
            <CMPad key={n.n} node={n} isCurrent={isCur(n)}
                   onEnter={setHover} onLeave={() => setHover(null)} />
          ))}
          {beacon && (
            <CMBeacon node={beacon} isCurrent={isCur(beacon)}
                      onEnter={setHover} onLeave={() => setHover(null)} />
          )}
          <CMChip node={cap2} onEnter={setHover} onLeave={() => setHover(null)} />

          {hover && <CMTip node={hover} />}
        </svg>
      </div>

      <div className="pcb-legend">
        <span><i className="dot done"></i>Soldered (done)</span>
        <span><i className="dot now"></i>Up next</span>
        <span><i className="dot ready"></i>Ready</span>
        <span className="pcb-legend-hint">Hover a pad for the title · click to open</span>
      </div>
    </div>
  );
}

Object.assign(window, { CourseMap });
