/* shared.jsx — common UI scaffolding for every chapter.
   Exports onto window:
     fmt, useInViewCallback, useCrossChapterPersistence, useProgress,
     ProgressBar, TopBar, ChapterNav, ChapterStrip, Slider,
     CommonTweaks, NextChapterButton, PrevChapterButton,
     CoverPage, PredictReveal, ALL_CHAPTERS
*/

const { useState, useEffect, useRef, useCallback } = React;

/* ─── helpers ───────────────────────────────────────────────────────────── */

function fmt(n, decimals = 2) {
  if (!isFinite(n)) return "∞";
  let s = n.toFixed(decimals);
  if (s.indexOf(".") >= 0) s = s.replace(/\.?0+$/, "");   // only strip zeros after a decimal point
  return s || "0";
}

/* ─── Eq: render X_sub notation as real subscripts ───────────────────────
   Turns "V_CC", "I_B", "R_load", "f_0" etc. into V<sub>CC</sub> … so equations
   read correctly instead of showing raw underscores. Pass a string; non-string
   children (already-JSX) pass through untouched. Multi-char subscripts: use
   braces, e.g. "V_{out}". */
function subify(input) {
  if (typeof input !== "string" || input.indexOf("_") < 0) return input;
  const parts = [];
  const re = /([A-Za-zβτΩ0-9])_(\{[^}]+\}|[A-Za-z0-9]+)/g;
  let last = 0, m, i = 0;
  while ((m = re.exec(input))) {
    if (m.index > last) parts.push(input.slice(last, m.index));
    let sub = m[2];
    if (sub[0] === "{") sub = sub.slice(1, -1);
    parts.push(
      React.createElement("span", { key: "e" + i++, style: { whiteSpace: "nowrap" } },
        m[1], React.createElement("sub", null, sub))
    );
    last = re.lastIndex;
  }
  if (last < input.length) parts.push(input.slice(last));
  return parts;
}
function Eq({ children }) { return subify(children); }

/* ─── Electrons: evenly-spaced dots flowing along an SVG path ────────────────
   JS/rAF driven (not SMIL) so changing speed mid-flight never makes them
   clump: the period is read from a ref each frame, so the phase keeps
   advancing smoothly and the dots stay evenly distributed. */
function Electrons({ path, period, count = 6, flowing = true, color = "var(--current)", r = 5, filter }) {
  const pathRef = useRef(null);
  const groupRef = useRef(null);
  const periodRef = useRef(1);
  periodRef.current = Math.max(0.05, parseFloat(period) || 1);
  useEffect(() => {
    if (!flowing) return;
    const p = pathRef.current, g = groupRef.current;
    if (!p || !g) return;
    let len = 0;
    try { len = p.getTotalLength(); } catch (e) { return; }
    if (!len) return;
    const circles = g.querySelectorAll("circle");
    let raf, last = performance.now(), phase = 0;
    const tick = (now) => {
      const dt = (now - last) / 1000; last = now;
      phase = (phase + dt / periodRef.current) % 1;
      for (let i = 0; i < circles.length; i++) {
        const ph = (phase + i / count) % 1;
        const pt = p.getPointAtLength(ph * len);
        circles[i].setAttribute("transform", `translate(${pt.x.toFixed(2)} ${pt.y.toFixed(2)})`);
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [path, count, flowing]);
  if (!flowing) return null;
  return (
    <g ref={groupRef}>
      <path ref={pathRef} d={path} fill="none" stroke="none" pointerEvents="none" />
      {Array.from({ length: count }, (_, i) => (
        <circle key={i} r={r} fill={color} stroke="var(--current-deep)" strokeWidth="0.8"
                filter={filter} transform="translate(-99 -99)" />
      ))}
    </g>
  );
}

function useInViewCallback(onView) {
  const ref = useRef();
  useEffect(() => {
    if (!ref.current) return;
    const obs = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) onView();
    }, { rootMargin: "-28% 0px -67% 0px", threshold: 0 });
    obs.observe(ref.current);
    return () => obs.disconnect();
  }, []);
  return ref;
}

/* Persist tweaks (theme, audience) across chapters. */
function useCrossChapterPersistence(tweaks, setTweak, keys = ["audience", "theme"]) {
  const initialized = useRef(false);
  useEffect(() => {
    keys.forEach(k => {
      try {
        const stored = localStorage.getItem("hte-" + k);
        if (stored !== null && stored !== tweaks[k]) setTweak(k, stored);
      } catch {}
    });
    initialized.current = true;
  }, []);
  useEffect(() => {
    if (!initialized.current) return;
    keys.forEach(k => {
      try { localStorage.setItem("hte-" + k, tweaks[k]); } catch {}
    });
  }, keys.map(k => tweaks[k]));
}

/* Progress tracking — chapter N is marked complete when its #whats-next
   section enters the viewport center. */
function useProgress() {
  const [state, setState] = useState(() => {
    try { return JSON.parse(localStorage.getItem("hte-progress") || "{}"); }
    catch { return {}; }
  });
  const markComplete = useCallback((n) => {
    setState(prev => {
      if (prev[n] === "done") return prev;
      const next = { ...prev, [n]: "done" };
      try { localStorage.setItem("hte-progress", JSON.stringify(next)); } catch {}
      return next;
    });
  }, []);
  const markStarted = useCallback((n) => {
    setState(prev => {
      if (prev[n]) return prev;
      const next = { ...prev, [n]: "in-progress" };
      try { localStorage.setItem("hte-progress", JSON.stringify(next)); } catch {}
      return next;
    });
  }, []);
  return { progress: state, markComplete, markStarted };
}

// Component that marks chapter complete when scrolled into view — UNLESS the
// page has a ChapterQuiz (they register in window.__hteQuiz): then completion
// is earned by PASSING the quiz, and reaching the end only marks in-progress.
function ProgressMarker({ chapterN }) {
  const ref = useRef();
  useEffect(() => {
    if (!ref.current) return;
    const obs = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        try {
          const hasQuiz = !!(window.__hteQuiz && window.__hteQuiz[chapterN]);
          if (hasQuiz && !hteQuizPassed(chapterN)) {
            const cur = JSON.parse(localStorage.getItem("hte-progress") || "{}");
            if (!cur[chapterN]) {
              cur[chapterN] = "in-progress";
              localStorage.setItem("hte-progress", JSON.stringify(cur));
              window.dispatchEvent(new Event("hte-progress-changed"));
            }
            return;
          }
          const cur = JSON.parse(localStorage.getItem("hte-progress") || "{}");
          if (cur[chapterN] !== "done") {
            cur[chapterN] = "done";
            localStorage.setItem("hte-progress", JSON.stringify(cur));
            window.dispatchEvent(new Event("hte-progress-changed"));
          }
        } catch {}
      }
    }, { rootMargin: "-30% 0px -30% 0px", threshold: 0 });
    obs.observe(ref.current);
    return () => obs.disconnect();
  }, [chapterN]);
  return <div ref={ref} aria-hidden="true" />;
}

function ChapterStartMarker({ chapterN }) {
  useEffect(() => {
    try {
      const cur = JSON.parse(localStorage.getItem("hte-progress") || "{}");
      if (!cur[chapterN]) {
        cur[chapterN] = "in-progress";
        localStorage.setItem("hte-progress", JSON.stringify(cur));
        window.dispatchEvent(new Event("hte-progress-changed"));
      }
    } catch {}
  }, [chapterN]);
  return null;
}

/* ─── ProgressBar ───────────────────────────────────────────────────────── */
function ProgressBar() {
  const [pct, setPct] = useState(0);
  useEffect(() => {
    const onScroll = () => {
      const h = document.documentElement;
      const total = h.scrollHeight - h.clientHeight;
      const p = total > 0 ? (h.scrollTop / total) * 100 : 0;
      setPct(p);
    };
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, []);
  return (
    <div className="progress-rail" aria-hidden="true">
      <div style={{ width: pct + "%" }} />
    </div>
  );
}

/* ─── TopBar ────────────────────────────────────────────────────────────── */
function crumbInfo(currentN) {
  if (!currentN) return null;
  const l1 = ALL_CHAPTERS.findIndex(c => c.n === currentN);
  if (l1 >= 0) return { level: 1, idx: l1 + 1, total: ALL_CHAPTERS.length, title: ALL_CHAPTERS[l1].t };
  const l15 = L15_CHAPTERS.findIndex(c => c.n === currentN);
  if (l15 >= 0) return { level: "1½", idx: l15 + 1, total: L15_CHAPTERS.length, title: L15_CHAPTERS[l15].t };
  const mth = M_CHAPTERS.findIndex(c => c.n === currentN);
  if (mth >= 0) return { level: "M", idx: mth + 1, total: M_CHAPTERS.length, title: M_CHAPTERS[mth].t };
  const l2 = L2_CHAPTERS.findIndex(c => c.n === currentN);
  if (l2 >= 0) return { level: 2, idx: l2 + 1, total: L2_CHAPTERS.length, title: L2_CHAPTERS[l2].t };
  if (currentN === "capstone") return { level: 1, capstone: true, title: "Capstone Exam" };
  if (currentN === "capstone-l2") return { level: 2, capstone: true, title: "Build Exam" };
  return null;
}

function chapterNumLabel(currentN) {
  if (!currentN) return null;
  if (/^0\d$/.test(currentN)) return String(Number(currentN));
  if (/^1[1-3]$/.test(currentN)) return currentN;
  const mm = currentN.match(/^M-0?(\d+)$/);
  if (mm) return `M·${mm[1]}`;
  const m = currentN.match(/^(L\d)-(\d+)$/);
  if (m) return `${m[1]}·${Number(m[2])}`;
  return null;
}

function useActiveSection() {
  const [sec, setSec] = React.useState(null);
  React.useEffect(() => {
    const sel = ".beat-marker, .section > .marker, .section-inner .marker";
    const nodes = Array.from(document.querySelectorAll(sel));
    if (!nodes.length) return;
    const obs = new IntersectionObserver((entries) => {
      let best = null, bestY = Infinity;
      for (const e of entries) {
        if (!e.isIntersecting) continue;
        const y = Math.abs(e.boundingClientRect.top - window.innerHeight * 0.35);
        if (y < bestY) { bestY = y; best = e.target; }
      }
      if (best) {
        const m = best.textContent.match(/§\s*([\d]+)/);
        if (m) setSec(m[1].replace(/^0+(?=\d)/, ""));
      }
    }, { rootMargin: "-30% 0px -55% 0px", threshold: 0 });
    nodes.forEach(n => obs.observe(n));
    return () => obs.disconnect();
  }, []);
  return sec;
}

/* Feedback channel — shown site-wide in the CourseMenu footer.
   PLACEHOLDER address: swap for the real inbox (same deal as the Ko-fi URL). */
const HTE_FEEDBACK_URL = "mailto:hello@fathohm.com?subject=Fathohm%20feedback";

/* ─── Course Map dropdown (always-visible nav, every page) ─────────────── */
function CourseMenu({ currentN }) {
  const [open, setOpen] = useState(false);
  const progress = useProgressState();
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);
  const groups = [
    { tag: "Level 0 · Story", cls: "l0", items: [
        { n: "L0-00", t: "Before You Start", href: "primer.html" },
        ...L0_CHAPTERS,
      ] },
    { tag: "Level 1 · Theory", cls: "l1", items: [
        ...ALL_CHAPTERS,
        { n: "capstone", t: "Capstone Exam", href: "capstone.html" },
      ] },
    { tag: "Level 2 · Build", cls: "l2", items: [
        ...L2_CHAPTERS,
        { n: "capstone-l2", t: "Build Exam", href: "capstone-l2.html" },
      ] },
    { tag: "Level 1½ · Electives", cls: "l1", items: [
        ...L15_CHAPTERS,
      ] },
    { tag: "Math for Builders", cls: "l1", items: [
        ...M_CHAPTERS,
      ] },
    { tag: "The Project", cls: "pj", wide: true, items: [
        { n: "project", t: "The Beacon · build it as you learn", href: "project.html" },
        { n: "projects", t: "All projects · the build shelf", href: "projects.html" },
        { n: "kit", t: "Build it for real · the kit & field guide", href: "get-the-kit.html" },
      ] },
    { tag: "Tools", cls: "pj", wide: true, items: [
        { n: "flow-sandbox", t: "The Sandbox · see how a circuit works", href: "flow-sandbox.html" },
        { n: "sandbox", t: "The Workbench · free-build breadboard", href: "sandbox.html" },
        { n: "scope", t: "The Oscilloscope · read a waveform", href: "scope.html" },
        { n: "daily", t: "Today’s Drill · spaced review", href: "daily.html" },
        { n: "dashboard", t: "Your Progress · the dashboard", href: "dashboard.html" },
        { n: "teachers", t: "For teachers · run it as a class", href: "teachers.html" },
        { n: "about", t: "About & Safety", href: "about.html" },
      ] },
  ];
  return (
    <div className="coursemenu">
      <button className="cm-btn" onClick={() => setOpen(o => !o)} aria-expanded={open}
              aria-label="Course map">
        <span className="cm-grid" aria-hidden="true"><span></span><span></span><span></span><span></span></span>
        <span className="cm-btn-label">Course map</span>
      </button>
      {open && (
        <>
          <div className="cm-scrim" onClick={() => setOpen(false)}></div>
          <div className="cm-panel" role="menu">
            {groups.map(g => (
              <div key={g.tag} className={"cm-group" + (g.wide ? " cm-group-wide" : "")}>
                <div className={`cm-tag ${g.cls}`}>{g.tag}</div>
                <div className="cm-links">
                  {g.items.map(it => (
                    <a key={it.n} href={it.href} role="menuitem"
                       className={[
                         it.n === currentN ? "here" : "",
                         progress[it.n] === "done" ? "done" : "",
                       ].filter(Boolean).join(" ")}>
                      <span className="cm-link-t">{it.t}</span>
                      {progress[it.n] === "done" && <span className="cm-check">✓</span>}
                      {it.n === currentN && <span className="cm-you">you’re here</span>}
                    </a>
                  ))}
                </div>
              </div>
            ))}
            <a className="cm-map-link" href="map.html" role="menuitem">Full course map →</a>
            <a className="cm-map-link cm-feedback" href={HTE_FEEDBACK_URL} role="menuitem">Something confusing or broken? Send feedback →</a>
          </div>
        </>
      )}
    </div>
  );
}

/* ─── Always-visible course progress ────────────────────────────────────
   Counts the gated chapters (Level 1 + ready Level 2). Story is optional and
   excluded so the number reflects "the course" you're working through. */
function courseStats(progress) {
  const items = [...ALL_CHAPTERS, ...L2_CHAPTERS.filter(c => c.ready !== false)];
  const done = items.filter(c => progress[c.n] === "done").length;
  const total = items.length;
  const pct = total ? Math.round((done / total) * 100) : 0;
  return { done, total, pct };
}

function CourseProgress() {
  const progress = useProgressState();
  const { done, total, pct } = courseStats(progress);
  const C = 2 * Math.PI * 15;
  return (
    <a className="course-prog" href="dashboard.html"
       title={`Course progress — ${done} of ${total} chapters · view your dashboard`}
       aria-label={`Course progress: ${done} of ${total} chapters, ${pct} percent complete. Open your progress dashboard.`}>
      <svg viewBox="0 0 36 36" width="20" height="20" aria-hidden="true">
        <circle cx="18" cy="18" r="15" fill="none" stroke="var(--rule-strong)" strokeWidth="3" />
        <circle cx="18" cy="18" r="15" fill="none" stroke="var(--current)" strokeWidth="3"
                strokeLinecap="round" strokeDasharray={`${(pct / 100) * C} ${C}`}
                transform="rotate(-90 18 18)" />
      </svg>
      <span className="cp-text"><b>{done}</b>/{total}<i>{pct}%</i></span>
    </a>
  );
}

function TopBar({ chapterLabel, currentN, audience, setAudience }) {
  const crumb = crumbInfo(currentN);
  const chNum = chapterNumLabel(currentN);
  const sec = useActiveSection();
  const headRef = React.useRef(null);
  // Publish the real topbar height so sticky panels can clear the fixed bar
  // exactly (the pill makes it taller than a hard-coded guess on phones).
  React.useLayoutEffect(() => {
    const el = headRef.current;
    if (!el) return;
    const apply = () => document.documentElement.style.setProperty("--topbar-h", el.offsetHeight + "px");
    apply();
    const ro = new ResizeObserver(apply);
    ro.observe(el);
    window.addEventListener("resize", apply);
    return () => { ro.disconnect(); window.removeEventListener("resize", apply); };
  }, []);
  return (
    <header className="topbar" ref={headRef}>
      <div className="brand">
        <span className="mark"></span>
        <a href="index.html" style={{ border: "none", color: "inherit" }}>
          <span>Fath<span style={{ color: "var(--current)" }}>ohm</span></span>
        </a>
        <CourseMenu currentN={currentN} />
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 14, pointerEvents: "auto" }}>
        {chNum && sec && (
          <span className="section-badge" title="current section">{chNum}.{sec}</span>
        )}
        {crumb ? (
          <nav className="crumb" aria-label="Breadcrumb">
            <a href="index.html">Home</a>
            <span className="crumb-sep">›</span>
            <span className="crumb-lvl">Level {crumb.level}</span>
            <span className="crumb-sep">›</span>
            <span className="crumb-cur">
              {crumb.capstone ? crumb.title : `${crumb.idx}/${crumb.total} · ${crumb.title}`}
            </span>
          </nav>
        ) : (chapterLabel && <span>{chapterLabel}</span>)}
        <CourseProgress />
        {audience !== undefined && setAudience && (
          <div className="audience-pill" title="Reading level — Explorer keeps it plain; Engineer shows the full math.">
            {["kids", "adult"].map(a => (
              <button key={a}
                      onClick={() => setAudience(a)}
                      className={audience === a ? "active" : ""}>
                {a === "adult" ? "Engineer" : "Explorer"}
              </button>
            ))}
          </div>
        )}
      </div>
    </header>
  );
}

/* ─── ChapterNav (right-rail dots) ──────────────────────────────────────── */
function ChapterNav({ items }) {
  const [active, setActive] = useState(items[0]?.id || "");
  useEffect(() => {
    const obs = new IntersectionObserver(
      (entries) => {
        let best = null, bestRatio = 0;
        entries.forEach(e => {
          if (e.isIntersecting && e.intersectionRatio > bestRatio) {
            best = e.target.id; bestRatio = e.intersectionRatio;
          }
        });
        if (best) setActive(best);
      },
      { rootMargin: "-40% 0px -40% 0px", threshold: [0, 0.05, 0.1] }
    );
    items.forEach(i => {
      const el = document.getElementById(i.id);
      if (el) obs.observe(el);
    });
    return () => obs.disconnect();
  }, [items]);
  return (
    <nav className="chapter-nav" aria-label="Chapter sections">
      {items.map(i => (
        <a key={i.id} href={`#${i.id}`} className={active === i.id ? "active" : ""}>
          <span className="label">{i.label}</span>
        </a>
      ))}
    </nav>
  );
}

/* ─── Whole-curriculum chapter list & strip ─────────────────────────────── */
// Level 0 — optional, ungated narrative history. `story:true` pages.
const L0_CHAPTERS = [
  { n: "L0-01", t: "The Spark",      sub: "Volta, Galvani & the first battery", href: "story-spark.html",      ready: true, min: 10 },
  { n: "L0-02", t: "The Law",        sub: "Ohm, doubted for a decade",           href: "story-ohm.html",        ready: true, min: 10 },
  { n: "L0-03", t: "The Storehouse", sub: "Faraday & stored charge",             href: "story-faraday.html",    ready: true, min: 10 },
  { n: "L0-04", t: "Current Wars",   sub: "Edison vs. Tesla, DC vs. AC",         href: "story-wars.html",       ready: true, min: 10 },
  { n: "L0-05", t: "The Tiny Switch", sub: "Bell Labs & the transistor",          href: "story-transistor.html", ready: true, min: 10 },
];

const ALL_CHAPTERS = [
  { n: "01", t: "The Flow",        sub: "Ohm's Law via water",        href: "chapter1.html",  min: 25,
    recap: { adult: "Voltage pushes, current flows, resistance squeezes — and Ohm's law (V = I\u00b7R) locks the three together.",
             kids: "Push (V), flow (I), squeeze (R) are friends: more push = more flow, more squeeze = less flow." } },
  { n: "02", t: "The Branching",   sub: "Series & parallel",           href: "chapter2.html",  min: 25,
    recap: { adult: "Series shares one current and its resistances add; parallel shares one voltage and the branch currents add.",
             kids: "One road (series) shares the flow; two roads (parallel) each get the full push." } },
  { n: "03", t: "Power & Heat",    sub: "Watts and warmth",            href: "chapter3.html",  min: 20,
    recap: { adult: "Power P = V\u00b7I is energy per second; the squeeze turns spent push into heat.",
             kids: "Power is how much is really happening \u2014 and squeezing the flow makes heat." } },
  { n: "04", t: "The Bucket",      sub: "Capacitors that store charge", href: "chapter4.html", min: 25,
    recap: { adult: "A capacitor stores charge and fills or empties over time; \u03c4 = R\u00b7C sets how slowly.",
             kids: "A capacitor is a bucket that fills and empties over time \u2014 a bigger bucket fills slower." } },
  { n: "05", t: "The Switch",      sub: "On, off, and binary",         href: "chapter5.html",  min: 20,
    recap: { adult: "A switch opens or closes the loop; two in series make AND, two in parallel make OR.",
             kids: "A switch is a gate. Two in a row = AND (both on); side by side = OR (either on)." } },
  { n: "06", t: "The Transistor",  sub: "A valve made of electricity", href: "chapter6.html",  min: 25,
    recap: { adult: "A small base current controls a much larger one \u2014 a switch that electricity flips by itself.",
             kids: "A transistor is a tap a tiny signal can open to let a big flow through." } },
  { n: "07", t: "The One-Way Valve", sub: "Diodes & direction",         href: "chapter7.html",  min: 20,
    recap: { adult: "A diode passes current one way only and drops about 0.7 V doing it; an LED is one that glows.",
             kids: "A diode is a one-way door for flow \u2014 and an LED is one that lights up." } },
  { n: "08", t: "The Wave",         sub: "AC, DC & frequency",           href: "chapter8.html",  min: 25,
    recap: { adult: "DC is steady; AC sloshes back and forth; frequency (Hz) counts the cycles each second.",
             kids: "DC is steady, AC wiggles back and forth \u2014 frequency is how many wiggles per second." } },
  { n: "09", t: "The Flywheel",     sub: "Inductors & momentum",         href: "chapter9.html",  min: 20,
    recap: { adult: "An inductor resists CHANGES in current \u2014 a flywheel for flow that kicks back when interrupted.",
             kids: "A coil is a heavy flywheel for flow \u2014 it hates sudden changes and kicks back if you stop it fast." } },
  { n: "10", t: "The Blinker",      sub: "The 555 timer in action",      href: "chapter10.html", min: 20,
    recap: { adult: "A 555 timer charges and discharges a capacitor to blink on a steady beat.",
             kids: "The 555 chip fills and empties a bucket over and over to make a steady blink." } },
];

// Level 1½ — deeper theory, optional, after the Lv.1 exam. Same style as L1.
const L15_CHAPTERS = [
  { n: "11", t: "The Divider",        sub: "Two pinches share the push",   href: "chapter11.html", min: 20 },
  { n: "12", t: "The Gatekeeper",     sub: "Op-amps & feedback",           href: "chapter12.html", min: 25 },
  { n: "13", t: "The Adding Machine", sub: "Logic gates that count",       href: "chapter13.html", min: 25 },
];

// Math for Builders — optional support stream. Short, friendly, zero gatekeeping.
const M_CHAPTERS = [
  { n: "M-01", t: "Moving the Letters", sub: "Rearrange any equation",       href: "math1.html", min: 10 },
  { n: "M-02", t: "Powers of Ten",      sub: "milli, micro, kilo, mega",     href: "math2.html", min: 10 },
  { n: "M-03", t: "Reading Curves",     sub: "Graphs without fear",          href: "math3.html", min: 10 },
  { n: "M-04", t: "Good Guessing",      sub: "Estimation & sanity checks",   href: "math4.html", min: 10 },
  { n: "M-05", t: "The Shorthand",      sub: "Symbols, units & tolerances",  href: "math5.html", min: 10 },
];

// Level 2 — applied builds on a real breadboard. `applies` = the L1 chapter
// it puts into practice. `ready:false` marks chapters not built yet.
const L2_CHAPTERS = [
  { n: "L2-00", t: "The Language", sub: "Read a schematic, see it built",  href: "chapter-l2-language.html", ready: true, min: 15 },
  { n: "L2-01", t: "The Breadboard", sub: "Build a real LED circuit",     href: "breadboard.html",        applies: "01", ready: true, min: 20 },
  { n: "L2-02", t: "Two LEDs",       sub: "Series & parallel, for real",  href: "chapter-l2-series.html", applies: "02", ready: true, min: 15 },
  { n: "L2-03", t: "Sizing It Up",   sub: "Power & picking a resistor",   href: "chapter-l2-power.html",  applies: "03", ready: true, min: 15 },
  { n: "L2-04", t: "The Blink",      sub: "An RC timer that flashes",     href: "chapter-l2-rc.html",     applies: "04", ready: true, min: 20 },
  { n: "L2-05", t: "The Button",     sub: "Switches & logic on the board", href: "chapter-l2-switch.html", applies: "05", ready: true, min: 15 },
  { n: "L2-06", t: "The Driver",     sub: "A transistor driving a load",  href: "chapter-l2-transistor.html", applies: "06", ready: true, min: 20 },
  { n: "L2-07", t: "The Rectifier",  sub: "Diode + cap: AC to clean DC",  href: "chapter-l2-rectifier.html", applies: "07", ready: true, min: 20 },
  { n: "L2-08", t: "The Oscillator", sub: "Build a blinker, crack the 555", href: "chapter-l2-oscillator.html", applies: "06", ready: true, min: 25 },
  { n: "L2-09", t: "The Beacon",     sub: "Assemble the whole device",     href: "chapter-l2-beacon.html",    applies: "10", ready: true, capstone: true, min: 30 },
];

// Maps each Level-1 chapter to the Beacon project stage it unlocks.
const PROJECT_STAGE_BY_CH = {
  "01": { id: "light", t: "First Light" },
  "02": { id: "array", t: "Make It a Beacon" },
  "03": { id: "power", t: "Cool & Long-Lasting" },
  "04": { id: "soft", t: "Soft Start" },
  "05": { id: "arm", t: "Arm & Fire" },
  "06": { id: "drive", t: "Drive the Big Lamp" },
  "07": { id: "protect", t: "Protect It" },
  "08": { id: "smooth", t: "Tame the Wave" },
  "09": { id: "kick", t: "Catch the Kick" },
  "10": { id: "blink", t: "Make It Blink" },
};

function useProgressState() {
  const [progress, setProgress] = useState(() => {
    try { return JSON.parse(localStorage.getItem("hte-progress") || "{}"); }
    catch { return {}; }
  });
  useEffect(() => {
    const reload = () => {
      try { setProgress(JSON.parse(localStorage.getItem("hte-progress") || "{}")); }
      catch {}
    };
    window.addEventListener("hte-progress-changed", reload);
    window.addEventListener("storage", reload);
    return () => {
      window.removeEventListener("hte-progress-changed", reload);
      window.removeEventListener("storage", reload);
    };
  }, []);
  return progress;
}

/* ─── per-activity tracking (quiz / practice / build), keyed by chapter ───── */
function hteRecord(chapterN, kind) {
  if (!chapterN || !kind) return;
  try {
    const a = JSON.parse(localStorage.getItem("hte-activity") || "{}");
    if (!a[chapterN]) a[chapterN] = {};
    if (!a[chapterN][kind]) {
      a[chapterN][kind] = true;
      localStorage.setItem("hte-activity", JSON.stringify(a));
      window.dispatchEvent(new Event("hte-activity-changed"));
    }
  } catch (e) {}
}
function useActivityState() {
  const [act, setAct] = useState(() => {
    try { return JSON.parse(localStorage.getItem("hte-activity") || "{}"); } catch { return {}; }
  });
  useEffect(() => {
    const reload = () => { try { setAct(JSON.parse(localStorage.getItem("hte-activity") || "{}")); } catch {} };
    window.addEventListener("hte-activity-changed", reload);
    window.addEventListener("storage", reload);
    return () => { window.removeEventListener("hte-activity-changed", reload); window.removeEventListener("storage", reload); };
  }, []);
  return act;
}

/* Has the end-of-chapter quiz been passed? (recorded by ChapterQuiz at ≥70%) */
function hteQuizPassed(chapterN) {
  try {
    const a = JSON.parse(localStorage.getItem("hte-activity") || "{}");
    return !!(a[chapterN] && a[chapterN].quiz);
  } catch (e) { return false; }
}

/* Flip a chapter to "done" in hte-progress (idempotent). */
function hteMarkDone(chapterN) {
  if (!chapterN) return;
  try {
    const cur = JSON.parse(localStorage.getItem("hte-progress") || "{}");
    if (cur[chapterN] !== "done") {
      cur[chapterN] = "done";
      localStorage.setItem("hte-progress", JSON.stringify(cur));
      window.dispatchEvent(new Event("hte-progress-changed"));
    }
  } catch (e) {}
}

function ChapterStrip({ currentN }) {
  const progress = useProgressState();
  return (
    <div className="chapter-strip">
      {ALL_CHAPTERS.map(c => {
        const status = progress[c.n];
        const cls = [
          c.n === currentN ? "current" : "",
          status === "done" ? "done" : status === "in-progress" ? "in-progress" : "",
        ].filter(Boolean).join(" ");
        return (
          <a key={c.n} href={c.href} className={cls} style={{ border: "none" }}>
            <span className="num">{c.n}</span>
            <span className="title">{c.t}</span>
            {status === "done" && <span className="check" aria-label="completed">✓</span>}
          </a>
        );
      })}
    </div>
  );
}

/* ─── Slider ────────────────────────────────────────────────────────────── */
function Slider({ name, value, min, max, step = 0.1, unit, accent, onChange, hint, disabled = false }) {
  return (
    <div className="ctrl" style={{ opacity: disabled ? 0.4 : 1, pointerEvents: disabled ? "none" : "auto" }}>
      <div className="ctrl-head">
        <span className="ctrl-name">{name}</span>
        <span className="ctrl-val mono">
          {fmt(value, step < 1 ? 1 : 0)}<span className="unit">{unit}</span>
        </span>
      </div>
      <input type="range" className={`range range-${accent || ""}`}
             min={min} max={max} step={step} value={value}
             disabled={disabled}
             onChange={(e) => onChange(Number(e.target.value))} />
      {hint && <div className="marg" style={{ marginTop: 4 }}>{hint}</div>}
    </div>
  );
}

/* ─── Glossary slide-over + floating button ─────────────────────────────── */
const GLOSSARY = [
  { term: "Voltage", sym: "V", unit: "volt (V)", water: "Water pressure — the push.",
    def: "The push that drives current. How hard the battery shoves charge around the loop." },
  { term: "Current", sym: "I", unit: "ampere / amp (A)", water: "Flow rate — how fast water moves.",
    def: "The flow of charge — how many electrons pass a point each second. It's an answer, set by V and R." },
  { term: "Resistance", sym: "R", unit: "ohm (Ω)", water: "A pinch in the pipe.",
    def: "How much a component fights the flow. More resistance, less current for the same voltage." },
  { term: "Power", sym: "P", unit: "watt (W)", water: "How hard the water is working right now.",
    def: "The RATE at which energy is used — energy per second. P = V·I. A 60 W bulb uses 60 joules every second." },
  { term: "Energy", sym: "E", unit: "joule (J) · watt-hour (Wh)", water: "The total amount of water moved.",
    def: "The TOTAL work done — power added up over time. Power is the speed; energy is the distance. A battery stores energy; it delivers power." },
  { term: "Capacitance", sym: "C", unit: "farad (F)", water: "The size of a bucket.",
    def: "How much charge a capacitor holds for a given voltage. Bigger C = bigger bucket = slower to fill or drain." },
  { term: "Charge", sym: "Q", unit: "coulomb (C)", water: "An amount of water.",
    def: "The actual quantity of electricity. Current is charge flowing per second; a capacitor stores charge." },
  { term: "Ohm's Law", sym: "V=IR", unit: "—", water: "Pressure = flow × pinch.",
    def: "The core relationship: voltage equals current times resistance. Rearranges to I = V/R and R = V/I." },
  { term: "Voltage drop", sym: "—", unit: "volt (V)", water: "Push used up crossing a pinch.",
    def: "The voltage a component 'uses up' as current flows through it. Around a loop, every drop adds back up to the source voltage. An LED drops a fixed ~2 V; a resistor drops I×R." },
  { term: "Series", sym: "—", unit: "—", water: "Pinches one after another in one pipe.",
    def: "Components in a single loop. Same current through all; resistances add; voltages divide." },
  { term: "Parallel", sym: "—", unit: "—", water: "The pipe splits into branches.",
    def: "Components across the same two points. Same voltage across each; currents add; total resistance drops." },
  { term: "Time constant", sym: "τ", unit: "second (s)", water: "How long the bucket takes to fill.",
    def: "τ = R·C. After one τ a capacitor is ~63% charged; after 5τ, essentially full or empty." },
  { term: "Transistor", sym: "—", unit: "—", water: "A small stream opening a big valve.",
    def: "A switch flipped by electricity: a tiny base signal controls a much larger current. The atom of computing." },
  { term: "Inductance", sym: "L", unit: "henry (H)", water: "A heavy flywheel in the stream.",
    def: "How much a coil resists CHANGES in current. It smooths flow — and kicks back hard if you try to stop it suddenly." },
  { term: "Frequency", sym: "f", unit: "hertz (Hz)", water: "Sloshes per second.",
    def: "How many complete back-and-forth cycles happen each second. Mains is 50–60 Hz; audio is 20–20,000 Hz." },
  { term: "Load", sym: "—", unit: "—", water: "The wheel at the end of the pipe.",
    def: "Whatever the circuit is powering — the lamp, motor, speaker, chip. 'Driving a load' just means delivering current to the thing doing the useful work." },
  { term: "Mains", sym: "—", unit: "—", water: "The city water main, but for electricity.",
    def: "The wall-socket supply — AC power arriving from the grid (120 V/60 Hz in North America, 230 V/50 Hz in much of the world)." },
  { term: "Rectify", sym: "—", unit: "—", water: "One-way valves straightening a slosh.",
    def: "To turn AC into DC by letting only one direction through. The leftover bumps are the 'rectified peaks', which a capacitor then smooths flat." },
  { term: "Bias", sym: "—", unit: "—", water: "Which way you lean on a one-way valve.",
    def: "The direction of push applied to a part. Forward bias pushes WITH a diode's arrow — it conducts. Reverse bias pushes against it — it blocks." },
  { term: "Saturation", sym: "—", unit: "—", water: "A valve already wide open.",
    def: "A transistor driven fully on. Once saturated, the supply and load set the current — pushing the base harder changes nothing more." },
  { term: "RMS", sym: "Vᵣₘₛ", unit: "volt (V)", water: "A wave's honest average push.",
    def: "The effective value of an AC wave — the steady DC voltage that would deliver the same heat. For a sine wave, RMS = peak ÷ √2 ≈ 0.707 × peak." },
  { term: "Capacity", sym: "mAh", unit: "milliamp-hour", water: "How much water the barrel holds.",
    def: "How much charge a battery stores. 500 mAh runs 500 mA for one hour, or 50 mA for ten. Capacity sets runtime; voltage sets push." },
  { term: "~ (tilde)", sym: "≈", unit: "—", water: "—",
    def: "Both ~ and ≈ mean 'roughly' or 'about'. '~0.7 V' reads 'around 0.7 volts'. Engineers estimate constantly — exact values live in datasheets, not napkins." },
  { term: "Internal resistance", sym: "R_int", unit: "ohm (Ω)", water: "The barrel's own built-in pinch.",
    def: "Every battery resists its own flow a little. As a battery depletes, this climbs — the push sags under load and the battery warms. It's why old batteries dim flashlights before dying outright." },
];

/* ── Feedback destination — REPLACE with your own ──────────────────────────
   FEEDBACK_EMAIL powers the "send via email" button. Set FEEDBACK_FORM_URL to
   also surface a link to an external form (Google Forms / Tally / Typeform). */
const FEEDBACK_EMAIL = "you@example.com";
const FEEDBACK_FORM_URL = "";

function FeedbackPanel({ onClose }) {
  const [msg, setMsg] = useState("");
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);
  const send = () => {
    const subject = encodeURIComponent("Fathohm — feedback");
    const where = typeof document !== "undefined" ? document.title : "";
    const url = typeof location !== "undefined" ? location.href : "";
    const body = encodeURIComponent(msg.trim() + "\n\n— page: " + where + "\n  " + url);
    window.location.href = `mailto:${FEEDBACK_EMAIL}?subject=${subject}&body=${body}`;
  };
  return (
    <div className="glossary-scrim" onClick={onClose}>
      <aside className="glossary-panel feedback-panel" onClick={(e) => e.stopPropagation()}
             role="dialog" aria-label="Send feedback">
        <div className="gp-head">
          <div>
            <div className="eyebrow" style={{ color: "var(--current)" }}>Help make it better</div>
            <h3 className="serif">Feedback</h3>
          </div>
          <button className="gp-close" onClick={onClose} aria-label="Close">✕</button>
        </div>
        <div className="fb-body">
          <p className="fb-lead">Spotted a bug, a confusing explanation, a wrong number, or have an idea? Tell me — it genuinely helps.</p>
          <textarea className="fb-text" placeholder="What's on your mind?" value={msg}
                    onChange={(e) => setMsg(e.target.value)} autoFocus rows={6} />
          <button className="fb-send" onClick={send} disabled={!msg.trim()}>Send via email →</button>
          {FEEDBACK_FORM_URL && (
            <a className="fb-form" href={FEEDBACK_FORM_URL} target="_blank" rel="noopener">…or open the feedback form ↗</a>
          )}
          <p className="fb-note">Opens your email app — nothing is sent automatically, and there's no tracking.</p>
        </div>
      </aside>
    </div>
  );
}

/* ─── DesignGate — "calculate first, then build" ──────────────────────────
   Wraps a build (children). The build stays locked until the learner computes
   the design number, mirroring the Beacon's gated assembly. */
function DesignGate({ spec, prompt, formula, unit, answer, tol, hint, solution, kids, chapterN, children }) {
  const [val, setVal] = useState("");
  const [checked, setChecked] = useState(false);
  const [solved, setSolved] = useState(false);
  const [diff] = useDifficulty();
  const tier = HTE_TIERS[diff] || HTE_TIERS.engineer;
  const tolAbs = Math.max(Math.abs(answer) * (tol || 0.06), 1e-9) * (tier.tolMul || 1);
  const num = parseFloat(val);
  const ok = !isNaN(num) && Math.abs(num - answer) <= tolAbs;
  const submit = () => { setChecked(true); if (ok) { setSolved(true); if (chapterN) hteRecord(chapterN, "build"); } };
  const T = (x) => (x && typeof x === "object" && ("adult" in x || "kids" in x)) ? (kids ? x.kids : x.adult) : x;
  const showFormula = formula && (tier.showFormula || solved);
  return (
    <div className="design-gate">
      <div className={`dg-card ${solved ? "done" : ""}`}>
        <div className="dg-tag">{solved ? "design confirmed ✓" : "① size it first"}</div>
        {spec && <div className="dg-spec"><Eq>{T(spec)}</Eq></div>}
        <div className="dg-prompt"><Eq>{T(prompt)}</Eq></div>
        {showFormula && <div className="dg-formula mono"><Eq>{T(formula)}</Eq></div>}
        {tier.hintUpfront && !solved && hint && <div className="dg-prehint">💡 {T(hint)}</div>}
        {!solved && (
          <div className="dg-row">
            <div className="dg-input">
              <input type="text" inputMode="decimal" value={val}
                     onChange={(e) => { setVal(e.target.value); setChecked(false); }}
                     onKeyDown={(e) => { if (e.key === "Enter") submit(); }}
                     placeholder="your answer" aria-label="Design answer" />
              <span className="dg-unit">{unit}</span>
            </div>
            <button className="dg-check" onClick={submit} disabled={val.trim() === ""}>Check</button>
          </div>
        )}
        {checked && !ok && !solved && <div className="dg-fb wrong">Not quite{(!tier.hintUpfront && hint) ? <> — {T(hint)}</> : ", try again."}</div>}
        {solved && <div className="dg-fb ok"><b>Correct.</b> <Eq>{T(solution)}</Eq></div>}
      </div>
      <div className="dg-build">
        {solved
          ? <><div className="dg-step2">② now build it on the breadboard</div>{children}</>
          : (
            <div className="dg-lock">
              <span className="dg-lock-icon">🔒</span>
              {kids ? "Work out the number above to unlock the breadboard." : "Solve the design above to unlock the breadboard builder."}
            </div>
          )}
      </div>
    </div>
  );
}

/* ─── CourseSearch — jump to any chapter or glossary term ────────────────── */
function CourseSearch({ onClose }) {
  const [q, setQ] = useState("");
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);
  const ql = q.trim().toLowerCase();
  const chapters = [
    ...L0_CHAPTERS.map(c => ({ ...c, level: "Lv.0 · Story" })),
    ...ALL_CHAPTERS.map(c => ({ ...c, level: "Lv.1 · Theory" })),
    ...L2_CHAPTERS.map(c => ({ ...c, level: "Lv.2 · Build" })),
  ];
  const chMatches = ql
    ? chapters.filter(c => (c.t + " " + c.sub + " " + c.n).toLowerCase().includes(ql))
    : chapters;
  const glMatches = ql
    ? GLOSSARY.filter(g => (g.term + " " + g.def + " " + g.sym).toLowerCase().includes(ql))
    : [];
  return (
    <div className="glossary-scrim" onClick={onClose}>
      <aside className="search-panel" onClick={(e) => e.stopPropagation()} role="dialog" aria-label="Search the course">
        <div className="sp-head">
          <span className="sp-mag">⌕</span>
          <input className="sp-input" placeholder="Search chapters & terms…" value={q}
                 onChange={(e) => setQ(e.target.value)} autoFocus />
          <button className="gp-close" onClick={onClose} aria-label="Close">✕</button>
        </div>
        <div className="sp-results">
          {chMatches.length > 0 && <div className="sp-group">Chapters</div>}
          {chMatches.map(c => {
            const soon = c.ready === false;
            const inner = (
              <>
                <span className="sp-n mono">{c.n}</span>
                <span className="sp-title">{c.t}<i>{c.sub}</i></span>
                <span className="sp-level">{soon ? "soon" : c.level}</span>
              </>
            );
            return soon
              ? <div key={c.n} className="sp-row soon">{inner}</div>
              : <a key={c.n} className="sp-row" href={c.href}>{inner}</a>;
          })}
          {glMatches.length > 0 && <div className="sp-group">Glossary</div>}
          {glMatches.map(g => (
            <div key={g.term} className="sp-row term">
              <span className="sp-n mono">{g.sym !== "—" ? g.sym : "·"}</span>
              <span className="sp-title">{g.term}<i>{g.def}</i></span>
            </div>
          ))}
          {ql && chMatches.length === 0 && glMatches.length === 0 && (
            <div className="gp-empty">No matches for “{q}”.</div>
          )}
        </div>
        <div className="sp-foot">Press <kbd>/</kbd> anywhere to search · <kbd>Esc</kbd> to close</div>
      </aside>
    </div>
  );
}

/* Fact card — the emphasized "interesting fact" aside used across chapters
   (e.g. "where do the charges come from?", "what IS resistance?"). One
   consistent component + style everywhere it's used. */
function FactCard({ eyebrow, children }) {
  return (
    <div className="fact-card">
      <div className="fact-eyebrow">{eyebrow}</div>
      {children}
    </div>
  );
}

function GlossaryFab() {
  const [open, setOpen] = useState(false);
  const [fbOpen, setFbOpen] = useState(false);
  const [srch, setSrch] = useState(false);
  const [q, setQ] = useState("");
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === "Escape") setOpen(false);
      // "/" opens search, unless typing in a field
      const tag = (e.target && e.target.tagName) || "";
      const typing = tag === "INPUT" || tag === "TEXTAREA" || (e.target && e.target.isContentEditable);
      if (e.key === "/" && !typing) { e.preventDefault(); setSrch(true); }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);
  const ql = q.trim().toLowerCase();
  const rows = ql
    ? GLOSSARY.filter(g => (g.term + " " + g.sym + " " + g.def + " " + g.water).toLowerCase().includes(ql))
    : GLOSSARY;
  return (
    <>
      {typeof ReadAlongBar !== "undefined" && <ReadAlongBar />}
      <div className="fab-stack">
        {typeof SoundToggleButton !== "undefined" && <SoundToggleButton />}
        <button className="search-fab" onClick={() => setSrch(true)} aria-label="Search the course"
                title="Search chapters & terms ( / )">
          <span className="gf-q">⌕</span>
          <span className="gf-label">Search</span>
        </button>
        <button className="feedback-fab" onClick={() => setFbOpen(true)} aria-label="Send feedback"
                title="Send feedback">
          <span className="gf-q">✉</span>
          <span className="gf-label">Feedback</span>
        </button>
        <button className="glossary-fab" onClick={() => setOpen(true)} aria-label="Open glossary"
                title="Glossary — quick reference">
          <span className="gf-q">?</span>
          <span className="gf-label">Terms</span>
        </button>
      </div>
      {srch && <CourseSearch onClose={() => setSrch(false)} />}
      {fbOpen && <FeedbackPanel onClose={() => setFbOpen(false)} />}
      {open && (
        <div className="glossary-scrim" onClick={() => setOpen(false)}>
          <aside className="glossary-panel" onClick={(e) => e.stopPropagation()}
                 role="dialog" aria-label="Glossary">
            <div className="gp-head">
              <div>
                <div className="eyebrow" style={{ color: "var(--current)" }}>Quick reference</div>
                <h3 className="serif">Glossary</h3>
              </div>
              <button className="gp-close" onClick={() => setOpen(false)} aria-label="Close">✕</button>
            </div>
            <input className="gp-search" placeholder="Search terms…" value={q}
                   onChange={(e) => setQ(e.target.value)} autoFocus />
            <div className="gp-list">
              {rows.map(g => (
                <div key={g.term} className="gp-item">
                  <div className="gp-item-head">
                    <span className="gp-term">{g.term}</span>
                    {g.sym !== "—" && <span className="gp-sym mono">{g.sym}</span>}
                    {g.unit !== "—" && <span className="gp-unit mono">{g.unit}</span>}
                  </div>
                  <div className="gp-def">{g.def}</div>
                  <div className="gp-water"><span className="gp-water-tag">💧</span>{g.water}</div>
                </div>
              ))}
              {rows.length === 0 && <div className="gp-empty">No matches for “{q}”.</div>}
            </div>
          </aside>
        </div>
      )}
    </>
  );
}

/* ─── Common Tweaks body (no audience — that lives in the topbar) ────── */
function CommonTweaks({ t, setTweak, animationToggles = [] }) {
  return (
    <>
      <TweakSection label="Aesthetic" />
      <TweakRadio label="Look"
        value={t.theme}
        options={[
          { value: "paper", label: "Paper" },
          { value: "blueprint", label: "B-print" },
          { value: "botanical", label: "Botanic" },
        ]}
        onChange={(v) => setTweak("theme", v)} />

      {animationToggles.length > 0 && <TweakSection label="Animations" />}
      {animationToggles.map(a => (
        <TweakToggle key={a.key} label={a.label}
          value={t[a.key]} onChange={(v) => setTweak(a.key, v)} />
      ))}
    </>
  );
}

/* ─── Cover page — full-bleed, sits OUTSIDE the lesson grid ────────────── */
/* Replaces the "cover beat" that used to live inside the sticky grid and
   collide with the animation panel. */
function CoverPage({ chapterN, chapterTitle, chapterSub, lede, kids, children }) {
  const cpMeta = [...ALL_CHAPTERS, ...L15_CHAPTERS, ...M_CHAPTERS, ...L2_CHAPTERS, ...L0_CHAPTERS].find(c => c.n === chapterN);
  // "Previously" recap for returning learners — the essence of the prior L1
  // chapter, with a one-tap link back to reread it.
  const allIdx = ALL_CHAPTERS.findIndex(c => c.n === chapterN);
  const prevCh = allIdx > 0 ? ALL_CHAPTERS[allIdx - 1] : null;
  const recap = prevCh && prevCh.recap ? (kids ? prevCh.recap.kids : prevCh.recap.adult) : null;
  const isL15 = L15_CHAPTERS.some(c => c.n === chapterN);
  const isM = String(chapterN).startsWith("M-");
  const chSlot = isL15 ? `Elective · Ch. ${chapterN}`
    : isM ? `Math for Builders · unit ${chapterN.replace("M-0", "")} of ${M_CHAPTERS.length}`
    : `Ch. ${chapterN} of ${ALL_CHAPTERS.length}`;
  return (
    <section className="cover-page" id="cover" data-screen-label={`${chapterN} Cover`}>
      <div className="hero-grid"></div>
      <div className="cover-inner">
        <ChapterStrip currentN={chapterN} />
        <div style={{ display: "flex", alignItems: "baseline", gap: 16, marginBottom: 30, marginTop: 28 }}>
          <span className="eyebrow">{kids ? "A guide for builders" : "A field guide"}</span>
          <span className="eyebrow" style={{ color: "var(--ink-faint)" }}>·</span>
          <span className="eyebrow">{chSlot}</span>
          {cpMeta && cpMeta.min && (
            <>
              <span className="eyebrow" style={{ color: "var(--ink-faint)" }}>·</span>
              <span className="eyebrow" style={{ color: "var(--ink-faint)" }}>≈ {cpMeta.min} min</span>
            </>
          )}
        </div>

        <h1 className="serif cover-title">{chapterTitle}</h1>

        <div className="eyebrow" style={{ marginTop: 36, marginBottom: 16, color: "var(--current)" }}>
          {chapterSub}
        </div>

        <div className="cover-lede">{lede}</div>

        {typeof NarrateButton !== "undefined" && (
          <div style={{ marginTop: 22 }}>
            <NarrateButton label="Listen to the intro"
              text={() => [chapterTitle, chapterSub, hteNodeText(lede)].map(hteNodeText).join(". ")} />
          </div>
        )}

        {children && <div style={{ marginTop: 28 }}>{children}</div>}

        {recap && (
          <a className="cover-recap" href={prevCh.href}>
            <span className="cover-recap-tag">{kids ? "Last time" : "Previously"} · {prevCh.t}</span>
            <span className="cover-recap-text">{recap}</span>
            <span className="cover-recap-go">{kids ? "Read it again →" : "Reread →"}</span>
          </a>
        )}

        <div className="cover-cta">
          <a href="#begin" className="arrow-link">
            <span>Begin chapter</span>
            <span className="arrow-glyph">↓</span>
          </a>
        </div>
      </div>
    </section>
  );
}

/* ─── Predict-then-reveal teaching pattern ──────────────────────────────── */
function PredictReveal({ question, options, correct, explanation, accent = "current" }) {
  const [picked, setPicked] = useState(null);
  const isCorrect = picked === correct;
  return (
    <div className="predict-reveal">
      <div className="eyebrow" style={{ color: `var(--${accent})` }}>before you scroll · predict</div>
      <p className="predict-q"><Eq>{question}</Eq></p>
      {typeof NarrateButton !== "undefined" && (
        <div style={{ marginBottom: 14 }}>
          <NarrateButton label="Read the question"
            text={() => hteNodeText(question)} />
        </div>
      )}
      <div className="predict-opts">
        {options.map((o, i) => {
          const cls = picked === i
            ? (i === correct ? "correct" : "wrong")
            : (picked !== null && i === correct) ? "reveal-correct" : "";
          return (
            <button key={i} onClick={() => setPicked(i)} className={cls}
                    disabled={picked !== null}>
              <span className="letter">{String.fromCharCode(65 + i)}</span>
              <span><Eq>{o}</Eq></span>
            </button>
          );
        })}
      </div>
      {picked !== null && (
        <div className={`predict-explain ${isCorrect ? "correct" : "wrong"}`}>
          <strong>{isCorrect ? "Right." : "Not quite."}</strong> <Eq>{explanation}</Eq>
        </div>
      )}
    </div>
  );
}

/* ─── Next/prev chapter footer link ─────────────────────────────────────── */
function NextChapterButton({ href, label }) {
  return (
    <a href={href} className="cta cta-primary">
      {label} →
    </a>
  );
}
function PrevChapterButton({ href, label }) {
  return (
    <a href={href} className="cta cta-secondary">
      ← {label}
    </a>
  );
}

/* ─── WhatsNext — unified final-section component ───────────────────────── */
function WhatsNext({ currentN, kids, summary, nextHref, nextLabel, prevHref, prevLabel }) {
  const wnAct = useActivityState();
  const wnQuizGated = typeof window !== "undefined" && !!(window.__hteQuiz && window.__hteQuiz[currentN]);
  const wnQuizPassed = !!(wnAct[currentN] && wnAct[currentN].quiz);
  // ── cross-track wiring (all data-driven from the chapter lists) ──
  const isL2 = String(currentN).startsWith("L2-");
  let prog = {};
  try { prog = JSON.parse(localStorage.getItem("hte-progress") || "{}"); } catch (e) {}
  // L1 → the L2 chapter that applies this theory
  const l2Build = !isL2 ? L2_CHAPTERS.find(c => c.applies === currentN && c.ready) : null;
  // L2 → back to the theory chapter it applies, and forward to the next build
  const meL2 = isL2 ? L2_CHAPTERS.find(c => c.n === currentN) : null;
  const l1Theory = meL2 ? ALL_CHAPTERS.find(c => c.n === meL2.applies) : null;
  const idxL2 = isL2 ? L2_CHAPTERS.findIndex(c => c.n === currentN) : -1;
  const autoNext = isL2 && idxL2 >= 0 ? L2_CHAPTERS.slice(idxL2 + 1).find(c => c.ready) : null;
  // L2 pages historically pass nextHref="index.html"/"map.html" — upgrade to the real next build
  const overrideNext = isL2 && autoNext && (!nextHref || nextHref === "index.html" || nextHref === "map.html");
  const fNextHref = overrideNext ? autoNext.href : nextHref;
  const fNextLabel = overrideNext ? `L2 · ${autoNext.t}` : nextLabel;
  return (
    <section className="section" id="whats-next" data-screen-label="What's next"
             style={{ background: "var(--bg-deeper)", paddingBottom: "12vh" }}>
      <ProgressMarker chapterN={currentN} />
      <div className="marker">end of chapter</div>
      <div className="section-inner">
        <div className="wn-grid">
          <div>
            <h2 className="serif">You've done <em>Chapter {currentN}</em>.</h2>
            <div className="lede" style={{ marginTop: 14 }}>{summary}</div>
            <div style={{ display: "flex", gap: 12, marginTop: 26, flexWrap: "wrap" }}>
              {prevHref && <PrevChapterButton href={prevHref} label={prevLabel || "Previous"} />}
              {fNextHref && <NextChapterButton href={fNextHref} label={fNextLabel || "Next chapter"} />}
            </div>
            {overrideNext && (
              <p className="marg" style={{ marginTop: 12 }}>
                or <a href="map.html">back to the course map</a>
              </p>
            )}
          </div>
          <div>
            <ChapterStrip currentN={currentN} />
            <p className="marg" style={{ marginTop: 14 }}>
              {wnQuizGated && !wnQuizPassed
                ? (kids ? "To earn your checkmark, pass the quiz above! You can try as many times as you like."
                        : "The checkmark is earned, not scrolled past — pass the quiz above to mark this chapter complete.")
                : "Your progress is saved in your browser. Refresh anytime — checkmarks stay."}
            </p>
          </div>
        </div>
        {(PROJECT_STAGE_BY_CH[currentN] || l2Build || l1Theory) && (
          <div className="wn-more-label">{kids ? "More places you can go" : "Other paths from here"}</div>
        )}
        {PROJECT_STAGE_BY_CH[currentN] && (
          <a className="wn-beacon" href={`project.html#stage-${PROJECT_STAGE_BY_CH[currentN].id}`}>
            <span className="wn-beacon-icon" aria-hidden="true">
              <svg viewBox="0 0 40 40" width="34" height="34">
                <circle cx="20" cy="20" r="6" fill="var(--current)" />
                <circle cx="20" cy="20" r="11" fill="none" stroke="var(--current)" strokeWidth="1.4" opacity="0.6" />
                <circle cx="20" cy="20" r="16" fill="none" stroke="var(--current)" strokeWidth="1.1" opacity="0.3" />
              </svg>
            </span>
            <span className="wn-beacon-text">
              <span className="wn-beacon-tag">The Project · new stage unlocked</span>
              <span className="wn-beacon-title">
                {kids ? "Add your new part to The Beacon" : <>Apply this chapter to <em>“{PROJECT_STAGE_BY_CH[currentN].t}”</em></>}
              </span>
            </span>
            <span className="wn-beacon-go">Expand your build →</span>
          </a>
        )}
        {l2Build && (
          <a className="wn-beacon" href={l2Build.href} style={{ marginTop: 14 }}>
            <span className="wn-beacon-icon" aria-hidden="true">
              <svg viewBox="0 0 40 40" width="34" height="34">
                <rect x="8" y="12" width="24" height="16" rx="2.5" fill="none" stroke="var(--water)" strokeWidth="1.6" />
                {[14, 20, 26].map(x => <circle key={x} cx={x} cy="17" r="1.4" fill="var(--water)" />)}
                {[14, 20, 26].map(x => <circle key={x} cx={x} cy="23" r="1.4" fill="var(--water)" />)}
              </svg>
            </span>
            <span className="wn-beacon-text">
              <span className="wn-beacon-tag">Level 2 · the build</span>
              <span className="wn-beacon-title">
                {prog[l2Build.n] === "done"
                  ? <>Revisit the build: <em>“{l2Build.t}”</em></>
                  : (kids ? <>Now build it for real: “{l2Build.t}”</> : <>Wire this chapter for real: <em>“{l2Build.t}”</em></>)}
              </span>
            </span>
            <span className="wn-beacon-go">{prog[l2Build.n] === "done" ? "Build again →" : "To the breadboard →"}</span>
          </a>
        )}
        {(() => {
          // elective cross-link: "finished this chapter? go deeper"
          const ELECTIVE_BY_CH = { "02": "11", "05": "13", "06": "12" };
          const el = !isL2 ? L15_CHAPTERS.find(c => c.n === ELECTIVE_BY_CH[currentN]) : null;
          if (!el) return null;
          return (
            <a className="wn-beacon" href={el.href} style={{ marginTop: 14 }}>
              <span className="wn-beacon-icon" aria-hidden="true">
                <svg viewBox="0 0 40 40" width="34" height="34">
                  <path d="M 8 30 L 8 14 L 20 14" fill="none" stroke="var(--water)" strokeWidth="1.8" strokeLinecap="round" />
                  <path d="M 16 22 L 30 22 M 25 16 L 31 22 L 25 28" fill="none" stroke="var(--water)" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
                </svg>
              </span>
              <span className="wn-beacon-text">
                <span className="wn-beacon-tag">Elective · deeper theory</span>
                <span className="wn-beacon-title">
                  {prog[el.n] === "done"
                    ? <>Revisit the elective: <em>“{el.t}”</em></>
                    : (kids ? <>Curious for more? “{el.t}”</> : <>Optional side-quest: <em>“{el.t}”</em></>)}
                </span>
              </span>
              <span className="wn-beacon-go">Go deeper →</span>
            </a>
          );
        })()}
        {l1Theory && (
          <a className="wn-beacon" href={l1Theory.href} style={{ marginTop: 14 }}>
            <span className="wn-beacon-icon" aria-hidden="true">
              <svg viewBox="0 0 40 40" width="34" height="34">
                <path d="M 11 9 L 29 9 L 29 31 L 11 31 Z" fill="none" stroke="var(--water)" strokeWidth="1.6" />
                {[14, 19, 24].map(y => <line key={y} x1="15" y1={y} x2="25" y2={y} stroke="var(--water)" strokeWidth="1.3" />)}
              </svg>
            </span>
            <span className="wn-beacon-text">
              <span className="wn-beacon-tag">Level 1 · the theory</span>
              <span className="wn-beacon-title">
                {prog[l1Theory.n] === "done"
                  ? <>Refresh the theory behind this: <em>“{l1Theory.t}”</em></>
                  : <>Read the theory behind this build: <em>“{l1Theory.t}”</em></>}
              </span>
            </span>
            <span className="wn-beacon-go">Back to the water →</span>
          </a>
        )}
      </div>
    </section>
  );
}

/* ─── ChapterQuiz — end-of-chapter graded quiz ──────────────────────────
   props:
     chapterN   — for the "passed" badge / progress
     title, intro
     questions  — [{ q, options:[...], correct:Index, explain }]
     pick       — optional: draw this many at random from the bank each attempt
   Tracks answers, gives per-question feedback, shows a final score and a
   retry button. Options are shuffled each attempt. */
function shuffleArr(a) {
  const r = a.slice();
  for (let i = r.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [r[i], r[j]] = [r[j], r[i]];
  }
  return r;
}

/* ─── Practice problems — numeric, worked-solution drills ────────────────────
   Like the Beacon's calc checks, but for any chapter. Each problem:
     { q, unit, answer, tol (relative, default 0.06), abs (min absolute tol),
       hint, solution }   — q/solution/hint may be {adult,kids} or plain strings. */
function pp_text(x, kids) { return (x && typeof x === "object" && ("adult" in x || "kids" in x)) ? (kids ? x.kids : x.adult) : x; }

/* ─── Difficulty tiers (Apprentice / Engineer / Pro), persisted site-wide ──── */
const HTE_TIERS = {
  apprentice: { label: "Apprentice", hintUpfront: true,  showFormula: true,  tolMul: 1.6,  blurb: "Guided — hints up front, formulas shown, generous tolerance." },
  engineer:   { label: "Engineer",   hintUpfront: false, showFormula: true,  tolMul: 1.0,  blurb: "Hints appear after a miss. Standard tolerance." },
  pro:        { label: "Pro",        hintUpfront: false, showFormula: false, tolMul: 0.55, blurb: "No hints, formulas hidden until you solve it, tight tolerance." },
};
const HTE_TIER_ORDER = ["apprentice", "engineer", "pro"];

function readDifficulty() {
  // validate against known tiers — a stale/renamed key in localStorage must not crash the UI
  try { const v = localStorage.getItem("hte-difficulty"); return HTE_TIERS[v] ? v : "engineer"; } catch (e) { return "engineer"; }
}
function useDifficulty() {
  const [d, setD] = useState(readDifficulty);
  useEffect(() => {
    const sync = () => setD(readDifficulty());
    window.addEventListener("hte-difficulty-changed", sync);
    window.addEventListener("storage", sync);
    return () => { window.removeEventListener("hte-difficulty-changed", sync); window.removeEventListener("storage", sync); };
  }, []);
  const set = (v) => { try { localStorage.setItem("hte-difficulty", v); } catch (e) {} setD(v); window.dispatchEvent(new Event("hte-difficulty-changed")); };
  return [d, set];
}

function DifficultyToggle({ compact, showBlurb }) {
  const [d, setD] = useDifficulty();
  return (
    <div className={`diff-toggle ${compact ? "compact" : ""}`} role="group" aria-label="Difficulty">
      {!compact && <span className="diff-label">Difficulty</span>}
      <div className="diff-seg">
        {HTE_TIER_ORDER.map(k => (
          <button key={k} className={d === k ? "active" : ""} onClick={() => setD(k)}
                  title={HTE_TIERS[k].blurb}>{HTE_TIERS[k].label}</button>
        ))}
      </div>
      {showBlurb && <span className="diff-blurb">{HTE_TIERS[d].blurb}</span>}
    </div>
  );
}

// RNG helpers passed to problem generators (re-roll for replayable practice)
function hteRng() {
  return {
    int: (a, b, step = 1) => { const n = Math.floor((b - a) / step) + 1; return a + step * Math.floor(Math.random() * n); },
    pick: (arr) => arr[Math.floor(Math.random() * arr.length)],
  };
}

function PracticeProblem({ p, n, kids, tier, chapterN }) {
  const [val, setVal] = useState("");
  const [checked, setChecked] = useState(false);
  const [showSol, setShowSol] = useState(false);
  const T = tier || HTE_TIERS.engineer;
  const num = parseFloat(val);
  const baseTol = Math.max(Math.abs(p.answer) * (p.tol || 0.06), p.abs || 0);
  const tol = baseTol * (T.tolMul || 1);
  const ok = checked && !isNaN(num) && Math.abs(num - p.answer) <= tol;
  useEffect(() => { if (ok && chapterN) hteRecord(chapterN, "practice"); }, [ok, chapterN]);
  const q = pp_text(p.q, kids);
  const sol = pp_text(p.solution, kids);
  const hint = pp_text(p.hint, kids);
  return (
    <div className={`pp-item ${checked ? (ok ? "ok" : "no") : ""}`}>
      <div className="pp-q"><span className="pp-n mono">{String(n).padStart(2, "0")}</span><span className="pp-q-text"><Eq>{q}</Eq></span></div>
      {T.hintUpfront && hint && <div className="pp-prehint">💡 {hint}</div>}
      <div className="pp-row">
        <div className="pp-input">
          <input type="text" inputMode="decimal" value={val}
                 onChange={(e) => { setVal(e.target.value); setChecked(false); }}
                 onKeyDown={(e) => { if (e.key === "Enter") setChecked(true); }}
                 placeholder="your answer" aria-label={`Answer for problem ${n}`} />
          {p.unit && <span className="pp-unit">{p.unit}</span>}
        </div>
        <button className="pp-check" onClick={() => setChecked(true)} disabled={val.trim() === ""}>Check</button>
        <button className="pp-soltoggle" onClick={() => setShowSol((s) => !s)}>
          {(showSol || ok) ? "Hide" : "Show"} solution
        </button>
      </div>
      {checked && (
        <div className={`pp-verdict ${ok ? "ok" : "no"}`}>
          {ok ? "Correct ✓"
              : isNaN(num) ? "Type a number to check."
              : <>Not quite — you entered <b>{val}{p.unit ? " " + p.unit : ""}</b>.{(!T.hintUpfront && hint) ? <span className="pp-hint"> {hint}</span> : null}</>}
        </div>
      )}
      {(showSol || ok) && sol && (
        <div className="pp-solution">
          <span className="pp-sol-tag">worked solution</span>
          <span className="pp-sol-body"><Eq>{sol}</Eq></span>
          <span className="pp-answer mono">answer = {p.answer}{p.unit ? " " + p.unit : ""}</span>
        </div>
      )}
    </div>
  );
}

function PracticeProblems({ chapterN, title, intro, problems, kids }) {
  const [diff] = useDifficulty();
  const tier = HTE_TIERS[diff] || HTE_TIERS.engineer;
  const [nonce, setNonce] = useState(0);
  const [extra, setExtra] = useState(0);
  // Explorer mode gets a shorter drill (2 problems, no "+more" grind).
  const baseProblems = React.useMemo(() => (kids ? problems.slice(0, 2) : problems), [problems, kids]);
  const hasGen = baseProblems.some(p => typeof p === "function");
  const resolved = React.useMemo(
    () => baseProblems.map(p => (typeof p === "function" ? p(hteRng()) : p)),
    [baseProblems, nonce]
  );
  // extra reps drawn from the chapter's generators, cycling through them
  const extraResolved = React.useMemo(() => {
    const gens = baseProblems.filter(p => typeof p === "function");
    if (!gens.length) return [];
    return Array.from({ length: extra }, (_, i) => gens[i % gens.length](hteRng()));
  }, [extra, nonce, baseProblems]);
  return (
    <section className="section practice" id="practice" data-screen-label="Practice">
      <div className="section-inner">
        <div className="marker">practice · run the numbers</div>
        <div className="practice-head">
          <h2 className="serif">{title || (kids ? "Try the math." : "Practice the math.")}</h2>
          <div className="practice-tools">
            <DifficultyToggle compact />
            {hasGen && (
              <button className="pp-reroll" onClick={() => setNonce(n => n + 1)} title="Fresh numbers to practice with">
                ↻ New numbers
              </button>
            )}
          </div>
        </div>
        <p className="lede">{intro || (kids
          ? "A few number puzzles, just like the ones the Beacon project will ask. Type an answer and check it — peek at the worked solution any time."
          : "Calculation drills in the style of the Beacon project. Enter a number, check it (graded with a little tolerance), and reveal the full working whenever you like.")}</p>
        <div className="pp-list">
          {resolved.map((p, i) => <PracticeProblem key={i + "-" + nonce} p={p} n={i + 1} kids={kids} tier={tier} chapterN={chapterN} />)}
          {extraResolved.map((p, i) => <PracticeProblem key={"x" + i + "-" + nonce} p={p} n={resolved.length + i + 1} kids={kids} tier={tier} chapterN={chapterN} />)}
        </div>
        {hasGen && !kids && (
          <div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 18, flexWrap: "wrap" }}>
            <span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: 12.5, color: "var(--ink-faint)", letterSpacing: "0.06em" }}>
              {kids ? "want more puzzles?" : "want more reps?"}
            </span>
            <button className="pp-reroll" onClick={() => setExtra(e => e + 5)}>+ 5 more</button>
            <button className="pp-reroll" onClick={() => setExtra(e => e + 10)}>+ 10 more</button>
            {extra > 0 && (
              <button className="pp-reroll" onClick={() => setExtra(0)} title="Back to the original set">clear extras</button>
            )}
          </div>
        )}
        <p className="pp-foot">These mirror the design math you'll do in <a href="project.html">The Beacon</a>.</p>
      </div>
    </section>
  );
}

function hteIsKids() {
  try { return localStorage.getItem("hte-audience") === "kids"; } catch (e) { return false; }
}
function ChapterQuiz({ chapterN, title, intro, questions, pick: pickN }) {
  // Explorer mode draws a shorter quiz (max 3) so the end-of-chapter block
  // stays light; Engineer keeps the full draw.
  const effPick = (hteIsKids() && pickN) ? Math.min(pickN, 3) : pickN;
  // Build a fresh, shuffled set of questions (and shuffled options within each).
  // If the bank tags questions with kind: "math" | "concept", the draw is
  // BALANCED: at least 2 of each (1 each in short Explorer draws) so every
  // attempt mixes calculation with understanding. Untagged banks draw as before.
  const buildSet = React.useCallback((count) => {
    const n = count || effPick;
    let pool;
    const math = questions.filter(q => q.kind === "math");
    const concept = questions.filter(q => q.kind === "concept");
    if (n && n < questions.length && math.length && concept.length) {
      const minEach = Math.min(n <= 3 ? 1 : 2, math.length, concept.length, Math.floor(n / 2));
      const pickM = shuffleArr(math).slice(0, minEach);
      const pickC = shuffleArr(concept).slice(0, minEach);
      const rest = shuffleArr(questions.filter(q => !pickM.includes(q) && !pickC.includes(q)))
        .slice(0, Math.max(0, n - pickM.length - pickC.length));
      pool = shuffleArr([...pickM, ...pickC, ...rest]);
    } else {
      pool = shuffleArr(questions);
      if (n && n < pool.length) pool = pool.slice(0, n);
    }
    return pool.map(q => {
      const order = shuffleArr(q.options.map((_, i) => i));
      return {
        q: q.q,
        explain: q.explain,
        options: order.map(i => q.options[i]),
        correct: order.indexOf(q.correct),
      };
    });
  }, [questions, effPick]);

  const [quizQs, setQuizQs] = React.useState(buildSet);
  const [answers, setAnswers] = React.useState({});   // { qIndex: optionIndex }
  const [submitted, setSubmitted] = React.useState(false);
  const [attempt, setAttempt] = React.useState(0);

  const total = quizQs.length;
  const answeredCount = Object.keys(answers).length;
  const allAnswered = answeredCount === total;
  const score = quizQs.reduce(
    (acc, q, i) => acc + (answers[i] === q.correct ? 1 : 0), 0);
  const pct = Math.round((score / total) * 100);
  const passed = pct >= 70;
  // Register: this page's completion is gated on this quiz (see ProgressMarker).
  if (typeof window !== "undefined" && chapterN) {
    window.__hteQuiz = window.__hteQuiz || {};
    window.__hteQuiz[chapterN] = true;
  }
  useEffect(() => {
    if (submitted && passed && chapterN) { hteRecord(chapterN, "quiz"); hteMarkDone(chapterN); }
  }, [submitted, passed, chapterN]);

  const pick = (qi, oi) => {
    if (submitted) return;
    setAnswers(prev => ({ ...prev, [qi]: oi }));
  };
  const retry = (count) => {
    setQuizQs(buildSet(count));
    setAnswers({});
    setSubmitted(false);
    setAttempt(a => a + 1);
  };
  const bankN = questions.length;

  return (
    <section className="section quiz-section" id="quiz" data-screen-label="Quiz"
             style={{ background: "var(--bg-deeper)" }}>
      <div className="marker">§ quiz · check your understanding</div>
      <div className="section-inner" style={{ maxWidth: 820 }}>
        <h2 className="serif" style={{ marginBottom: 10 }}>{title}</h2>
        <p className="lede" style={{ marginBottom: 8 }}>{intro}</p>

        <div className="quiz-list" key={attempt}>
          {quizQs.map((q, qi) => {
            const picked = answers[qi];
            return (
              <div key={qi} className="quiz-q">
                <div className="quiz-q-num">Question {qi + 1} of {total}</div>
                <p className="quiz-q-text"><Eq>{q.q}</Eq></p>
                <div className="quiz-opts">
                  {q.options.map((o, oi) => {
                    let cls = "";
                    if (submitted) {
                      if (oi === q.correct) cls = "correct";
                      else if (oi === picked) cls = "wrong";
                    } else if (picked === oi) {
                      cls = "picked";
                    }
                    return (
                      <button key={oi} className={cls} onClick={() => pick(qi, oi)}
                              disabled={submitted}>
                        <span className="letter">{String.fromCharCode(65 + oi)}</span>
                        <span><Eq>{o}</Eq></span>
                        {submitted && oi === q.correct && <span className="tick">✓</span>}
                        {submitted && oi === picked && oi !== q.correct && <span className="cross">✕</span>}
                      </button>
                    );
                  })}
                </div>
                {submitted && (
                  <div className={`quiz-explain ${picked === q.correct ? "correct" : "wrong"}`}>
                    <Eq>{q.explain}</Eq>
                  </div>
                )}
              </div>
            );
          })}
        </div>

        {!submitted ? (
          <div className="quiz-actions">
            <button className="quiz-submit" disabled={!allAnswered}
                    onClick={() => setSubmitted(true)}>
              {allAnswered ? "Submit answers" : `Answer all ${total} questions (${answeredCount}/${total})`}
            </button>
          </div>
        ) : (
          <div className={`quiz-result ${passed ? "passed" : "review"}`}>
            <div className="quiz-score">
              <span className="quiz-score-num">{score}<span className="quiz-score-den">/{total}</span></span>
              <span className="quiz-score-pct">{pct}%</span>
            </div>
            <div className="quiz-verdict">
              <strong>{passed ? "Nicely done." : "Worth another look."}</strong>{" "}
              {passed
                ? "You've got the core of this chapter down cold."
                : "Scroll back up, replay the animations, and try again — no rush."}
              <div className="quiz-actions" style={{ marginTop: 16, display: "flex", gap: 10, flexWrap: "wrap" }}>
                <button className="quiz-retry" onClick={() => retry()}>Try again · {total} new</button>
                {bankN > total && (
                  <button className="quiz-retry" onClick={() => retry(Math.min(bankN, total + 5))}>
                    Go deeper · {Math.min(bankN, total + 5)} questions
                  </button>
                )}
                {bankN >= total + 10 && (
                  <button className="quiz-retry" onClick={() => retry(Math.min(bankN, total + 10))}>
                    The whole bank · {Math.min(bankN, total + 10)}
                  </button>
                )}
              </div>
            </div>
          </div>
        )}
      </div>
    </section>
  );
}

/* ─── VisTabs — mobile-only switcher between the sticky panel's two scenes ─
   On narrow screens only one vis-block fits; these tabs let the reader flip
   to the second one (circuit/scope/chart) instead of never seeing it. */
function VisTabs({ labels = ["WATER", "CIRCUIT"] }) {
  const ref = React.useRef();
  const [tab, setTab] = React.useState(0);
  React.useEffect(() => {
    const aside = ref.current && ref.current.closest(".lesson-sticky");
    if (aside) aside.setAttribute("data-vis-tab", String(tab));
  }, [tab]);
  return (
    <div className="vis-tabs" ref={ref} role="tablist" aria-label="Choose scene">
      {labels.map((l, i) => (
        <button key={l} role="tab" aria-selected={tab === i}
                className={tab === i ? "on" : ""} onClick={() => setTab(i)}>{l}</button>
      ))}
    </div>
  );
}

/* ─── EqEase — standing reassurance under an early equation ──────────── */
function EqEase({ kids, children }) {
  return (
    <div className="marg" style={{ marginTop: 10 }}>
      {children || (kids
        ? <>If the math part looks scary — don't worry! Keep scrolling. The pictures below tell the same story.</>
        : <>If this equation doesn't click yet, that's expected — examples and sliders below unpack every symbol. Scroll on; circle back.</>)}
    </div>
  );
}

/* ─── CheckpointQuiz ──────────────────────────────────────────────────────────────
   Compact mid-chapter check. Draws `pick` questions from a bank, grades
   each one the moment it's answered (no submit gate), and offers a
   reshuffle for more reps. Keep banks grounded in REAL circuits or the
   water analogy — these are application checks, not trivia. */
function CheckpointQuiz({ title, intro, questions, pick: pickN = 2, kids, label }) {
  // Keep chapters from running three quiz blocks in a row: the SECOND checkpoint
  // ("Out in the wild") drops entirely in Explorer mode and trims to a single
  // question in Engineer mode. The first checkpoint is untouched.
  const isSecond = /(^|\D)2(\D|$)|wild|real life/i.test(String(label || "") + " " + String(title || ""));
  const effPick = (!kids && isSecond) ? 1 : pickN;
  const buildSet = React.useCallback(() => {
    let pool = shuffleArr(questions);
    if (effPick && effPick < pool.length) pool = pool.slice(0, effPick);
    return pool.map(q => {
      const order = shuffleArr(q.options.map((_, i) => i));
      return {
        q: q.q, explain: q.explain,
        options: order.map(i => q.options[i]),
        correct: order.indexOf(q.correct),
      };
    });
  }, [questions, effPick]);

  const [qs, setQs] = React.useState(buildSet);
  const [answers, setAnswers] = React.useState({});
  const [round, setRound] = React.useState(0);
  const answeredAll = qs.length > 0 && Object.keys(answers).length === qs.length;
  const score = qs.reduce((acc, q, i) => acc + (answers[i] === q.correct ? 1 : 0), 0);
  const reshuffle = () => { setQs(buildSet()); setAnswers({}); setRound(r => r + 1); };

  if (kids && isSecond) return null;

  return (
    <section className="section checkpoint-section" data-screen-label={label || "Checkpoint"}>
      <div className="section-inner" style={{ maxWidth: 780 }}>
        <div className="card" style={{ padding: "26px 30px 22px" }}>
          <div className="eyebrow" style={{ marginBottom: 6 }}>
            ✓ checkpoint {kids ? "· quick brain check" : "· prove it before you scroll on"}
          </div>
          {title && <h3 className="serif" style={{ margin: "0 0 4px", fontSize: 24 }}>{title}</h3>}
          {intro && <p style={{ margin: "0 0 6px", fontSize: 15, color: "var(--ink-soft)" }}>{intro}</p>}
          <div key={round}>
            {qs.map((q, qi) => {
              const picked = answers[qi];
              const isDone = picked !== undefined;
              return (
                <div key={qi} className="quiz-q" style={{ marginTop: qi ? 16 : 12, padding: "18px 20px" }}>
                  <p className="quiz-q-text" style={{ fontSize: 19 }}><Eq>{q.q}</Eq></p>
                  <div className="quiz-opts">
                    {q.options.map((o, oi) => {
                      let cls = "";
                      if (isDone) {
                        if (oi === q.correct) cls = "correct";
                        else if (oi === picked) cls = "wrong";
                      }
                      return (
                        <button key={oi} className={cls} disabled={isDone}
                                onClick={() => setAnswers(prev => prev[qi] !== undefined ? prev : { ...prev, [qi]: oi })}>
                          <span className="letter">{String.fromCharCode(65 + oi)}</span>
                          <span><Eq>{o}</Eq></span>
                          {isDone && oi === q.correct && <span className="tick">✓</span>}
                          {isDone && oi === picked && oi !== q.correct && <span className="cross">✕</span>}
                        </button>
                      );
                    })}
                  </div>
                  {isDone && (
                    <div className={`quiz-explain ${picked === q.correct ? "correct" : "wrong"}`}>
                      <Eq>{q.explain}</Eq>
                    </div>
                  )}
                </div>
              );
            })}
          </div>
          {answeredAll && (
            <div style={{ display: "flex", alignItems: "center", flexWrap: "wrap", gap: 14, marginTop: 16 }}>
              <span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: 13.5,
                             color: score === qs.length ? "var(--water-deep)" : "var(--ink-soft)" }}>
                {score}/{qs.length}{" "}
                {score === qs.length
                  ? (kids ? "— nailed it!" : "— solid.")
                  : (kids ? "— read the why above, then grab more!" : "— read the explanations, then draw again.")}
              </span>
              <button className="quiz-retry" style={{ fontSize: 13, padding: "8px 16px" }} onClick={reshuffle}>
                {kids ? "More questions!" : "Draw two more"}
              </button>
            </div>
          )}
        </div>
      </div>
    </section>
  );
}

Object.assign(window, {
  fmt, useInViewCallback, useCrossChapterPersistence,
  Eq, subify, Electrons, FactCard,
  PracticeProblems, DesignGate, CourseSearch,
  useDifficulty, DifficultyToggle, HTE_TIERS, HTE_TIER_ORDER,
  useProgress, useProgressState, ProgressMarker, ChapterStartMarker,
  ProgressBar, TopBar, CourseProgress, courseStats, ChapterNav, ChapterStrip, Slider,
  CommonTweaks, NextChapterButton, PrevChapterButton,
  CoverPage, PredictReveal, WhatsNext, ChapterQuiz, CheckpointQuiz, GlossaryFab, EqEase, VisTabs,
  ALL_CHAPTERS, L2_CHAPTERS, L0_CHAPTERS, L15_CHAPTERS, M_CHAPTERS,
});
