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

/* ===================== i18n helpers (hash-based, team-i18n) =====================
 * window.I18N / window.I18NUtils / window.i18nHash come from the three scripts
 * loaded before this file (public/js/shared/*). Static visible text is wrapped
 * with <T> → <span data-i18n="hash">…</span> (the dynamic translator keeps it
 * translated across React re-renders). Attributes use tt(); strings with values
 * use tf() with {placeholders} (translate the whole phrase, never concatenate). */
const tt = (s) => (window.I18NUtils ? window.I18NUtils.t(s) : String(s == null ? "" : s));
const tf = (tmpl, vars) => tt(tmpl).replace(/\{(\w+)\}/g, (m, k) => (vars && vars[k] != null ? vars[k] : m));
function T({ children }) {
  const s = Array.isArray(children) ? children.join("") : String(children == null ? "" : children);
  const h = window.i18nHash ? window.i18nHash(s) : "";
  return <span data-i18n={h}>{tt(s)}</span>;
}
// Re-render the subtree when the language (and thus the registry) changes.
function useI18nVersion() {
  const [, setV] = useState(0);
  useEffect(() => {
    const cb = () => setV((v) => v + 1);
    document.addEventListener("i18n:changed", cb);
    return () => document.removeEventListener("i18n:changed", cb);
  }, []);
}
const LANGS = [
  ["en", "English"], ["zh", "中文"], ["es", "Español"], ["de", "Deutsch"], ["fr", "Français"],
  ["it", "Italiano"], ["pt", "Português"], ["ja", "日本語"], ["ko", "한국어"], ["th", "ไทย"],
  ["vi", "Tiếng Việt"], ["ru", "Русский"], ["hi", "हिन्दी"], ["ar", "العربية"], ["he", "עברית"],
  ["fa", "فارسی"], ["ur", "اردو"],
];
function LanguagePicker() {
  const [lang, setLang] = useState((window.I18N && window.I18N.lang) || "en");
  function change(e) { const l = e.target.value; setLang(l); if (window.I18N) window.I18N.setLang(l); }
  return (
    <select className="lang-select" value={lang} onChange={change} title={tt("Language")} aria-label={tt("Language")}>
      {LANGS.map(([v, label]) => <option key={v} value={v}>{label}</option>)}
    </select>
  );
}

/* ===================== palette (SVG strokes + dynamic colors) ===================== */
const C = {
  bg: "#0d1117", panel: "#161b22", panel2: "#1c232c", line: "#2b333f",
  ink: "#e6edf3", mute: "#8b97a6", faint: "#5b6675",
  teal: "#54f2b2", amber: "#ffb454", crimson: "#ff5d6c", blue: "#6cb6ff", gold: "#e3b341",
};

/* ===================== domain constants (fixed physics) ===================== */
const LED_VF = 2.0, LED_RD = 10, LED_SAFE = 20, LED_BURN = 40, R_RATING = 0.25;
const PALETTE = [
  { type: "resistor", label: "Resistor", hint: "Limits current (Ω)" },
  { type: "led", label: "LED", hint: "Lights when forward-biased" },
  { type: "wire", label: "Wire", hint: "Ideal conductor (~0 Ω)" },
];
const W_PRIMARY = 0.9, W_SECONDARY = 0.6, W_PENALTY = 0.4;

/* ===================== api helper ===================== */
async function api(path, { method = "GET", body } = {}) {
  const res = await fetch(`/api${path}`, {
    method, credentials: "same-origin",
    headers: body ? { "Content-Type": "application/json" } : {},
    body: body ? JSON.stringify(body) : undefined,
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) {
    // team-bugfix: forward server-side failures to BUG_LRS (skip 401/403 — expected auth gating).
    if (res.status !== 401 && res.status !== 403 && typeof window.reportError === "function") {
      window.reportError({
        source: "client", severity: res.status >= 500 ? "error" : "warning",
        message: `API ${method} ${path} → ${res.status}: ${data.error || res.statusText}`,
        context: { status: res.status, method, path },
      });
    }
    throw new Error(data.error || `HTTP ${res.status}`);
  }
  return data;
}

/* ===================== physics (ported from the mockup) ===================== */
function simulate(V, slotA, slotB) {
  const slots = [slotA, slotB];
  const closed = slots.every(Boolean);
  const hasLED = slots.some((c) => c && c.type === "led");
  const hasResistor = slots.some((c) => c && c.type === "resistor");
  const damaged = slots.some((c) => c && c.type === "led" && c.damaged);
  const base = { closed, hasLED, hasResistor, damaged, Ima: 0, R: 0, Vf: 0, Vr: 0, Pr: 0, status: "open", short: false, brightness: 0, willBurn: false };
  if (!closed) return { ...base, status: "open" };
  if (damaged) return { ...base, status: "burnt" };
  let R = 0, Vf = 0;
  slots.forEach((c) => {
    if (!c) return;
    if (c.type === "resistor") R += c.R;
    else if (c.type === "wire") R += 0.01;
    else if (c.type === "led") { R += LED_RD; Vf += LED_VF; }
  });
  const drive = V - Vf;
  let Ima = drive <= 0 ? 0 : (drive / R) * 1000;
  const short = Ima > 1000;
  Ima = Math.min(Ima, 8000);
  const I = Ima / 1000;
  const Rres = slots.reduce((a, c) => a + (c && c.type === "resistor" ? c.R : 0), 0);
  const Vr = I * Rres, Pr = I * I * Rres;
  let status = "off", brightness = 0;
  if (!hasLED) status = Ima > 0.5 ? "flow" : "off";
  else if (Ima < 1) status = "off";
  else if (Ima > LED_SAFE) { status = "overdrive"; brightness = 1; }
  else { status = "lit"; brightness = Math.max(0.12, Math.min(1, Ima / LED_SAFE)); }
  return { ...base, Ima, R, Vf, Vr, Pr, short, status, brightness, willBurn: hasLED && Ima > LED_BURN };
}

/* ===================== declarative check evaluator ===================== */
function evalCheck(sim, c) {
  const v = sim[c.field];
  switch (c.op) {
    case "truthy": return !!v;
    case "falsy": return !v;
    case "eq": return v === c.value;
    case "gt": return v > c.value;
    case "gte": return v >= c.value;
    case "lt": return v < c.value;
    case "lte": return v <= c.value;
    case "between": return v >= c.min && v <= c.max;
    case "approx": return Math.abs(v - c.value) <= (c.tol == null ? 0 : c.tol);
    default: return false;
  }
}
const taskMet = (sim, task) => task.checks.length > 0 && task.checks.every((c) => evalCheck(sim, c));
const misconceptionFired = (sim, m) => m.when.length > 0 && m.when.every((c) => evalCheck(sim, c));

/* ===================== small UI atoms ===================== */
function H({ children }) { return <div className="h nr">{children}</div>; }
function Sub({ children }) { return <div className="sub">{children}</div>; }
function timeStr() { const d = new Date(); return d.toLocaleTimeString([], { hour12: false }); }
function barColor(v) { return v >= 0.85 ? C.teal : v >= 0.5 ? C.gold : v > 0 ? C.crimson : C.line; }
function meterColor(sim) { if (sim.status === "overdrive" || sim.status === "burnt") return C.crimson; if (sim.Ima > 0.5) return C.teal; return C.mute; }

function Meter({ label, v, unit, digits, color }) {
  return (
    <div className="meter">
      <div className="meter__label"><T>{label}</T></div>
      <div className="meter__value" style={{ color }}>{v.toFixed(digits)}<span className="meter__unit"> {unit}</span></div>
    </div>
  );
}
function Gauge({ label, value, suffix, color }) {
  return (
    <div className="gauge">
      <div className="gauge__label"><T>{label}</T></div>
      <div className="gauge__value" style={{ color }}>{value}{suffix}</div>
    </div>
  );
}
function MiniSym({ type }) {
  if (type === "resistor") return <svg width="34" height="12" className="vmid"><path d="M0 6 L6 6 L9 1 L15 11 L21 1 L27 11 L30 6 L34 6" fill="none" stroke={C.gold} strokeWidth="1.6" /></svg>;
  if (type === "led") return <svg width="34" height="14" className="vmid"><polygon points="8,2 8,12 18,7" fill="none" stroke={C.teal} strokeWidth="1.6" /><line x1="18" y1="2" x2="18" y2="12" stroke={C.teal} strokeWidth="1.6" /><line x1="0" y1="7" x2="8" y2="7" stroke={C.faint} strokeWidth="1.6" /><line x1="18" y1="7" x2="34" y2="7" stroke={C.faint} strokeWidth="1.6" /></svg>;
  return <svg width="34" height="12" className="vmid"><line x1="0" y1="6" x2="34" y2="6" stroke={C.faint} strokeWidth="1.8" /></svg>;
}
function StatusBar({ sim }) {
  const map = {
    open: { node: <T>Open circuit — no complete path. Fill both slots.</T>, c: C.faint },
    off: { node: <T>Closed, but no light. Check forward voltage / current.</T>, c: C.mute },
    flow: { node: <T>Current flows, but there's no LED in the loop.</T>, c: C.blue },
    lit: { node: tf("LED lit · {mA} mA (safe ≤ {safe} mA).", { mA: sim.Ima.toFixed(1), safe: LED_SAFE }), c: C.teal },
    overdrive: { node: tf("OVERDRIVE · {mA} mA exceeds {safe} mA — heading for burnout.", { mA: sim.Ima.toFixed(0), safe: LED_SAFE }), c: C.crimson },
    burnt: { node: <T>LED destroyed. Replace it and add current limiting.</T>, c: C.crimson },
  };
  const s = map[sim.status] || map.off;
  return (
    <div className="statusbar" style={{ borderColor: s.c, color: s.c }}>
      {sim.short ? tt("⚠ Near short-circuit — almost no resistance. ") : ""}{s.node}
    </div>
  );
}

/* ===================== schematic ===================== */
function Battery({ x, y, v }) {
  return (
    <g transform={`rotate(90 ${x} ${y})`}>
      <line x1={x - 26} y1={y} x2={x - 8} y2={y} stroke={C.faint} strokeWidth="2.5" />
      <line x1={x - 8} y1={y - 16} x2={x - 8} y2={y + 16} stroke={C.amber} strokeWidth="3" />
      <line x1={x + 2} y1={y - 8} x2={x + 2} y2={y + 8} stroke={C.amber} strokeWidth="3" />
      <line x1={x + 12} y1={y - 16} x2={x + 12} y2={y + 16} stroke={C.amber} strokeWidth="3" />
      <line x1={x + 22} y1={y - 8} x2={x + 22} y2={y + 8} stroke={C.amber} strokeWidth="3" />
      <line x1={x + 22} y1={y} x2={x + 30} y2={y} stroke={C.faint} strokeWidth="2.5" />
      <text x={x} y={y + 40} textAnchor="middle" fill={C.amber} fontSize="13" transform={`rotate(-90 ${x} ${y + 40})`}>{v}V</text>
    </g>
  );
}
function ResistorSym({ x, y, R }) {
  const zig = "M-44 0 L-30 0 L-26 -10 L-18 10 L-10 -10 L-2 10 L6 -10 L14 10 L22 -10 L30 0 L44 0";
  return (
    <g transform={`translate(${x} ${y})`}>
      <path d={zig} fill="none" stroke={C.gold} strokeWidth="2.5" />
      <text x="0" y="24" textAnchor="middle" fill={C.gold} fontSize="12">{R} Ω</text>
    </g>
  );
}
function LedSym({ x, y, sim, damaged }) {
  const lit = !damaged && sim.brightness > 0 && sim.hasLED;
  const over = sim.status === "overdrive";
  const col = damaged ? C.crimson : over ? C.crimson : C.teal;
  return (
    <g transform={`translate(${x} ${y})`}>
      {lit && <circle cx="0" cy="0" r={26 + sim.brightness * 22} fill="url(#glow)" opacity={sim.brightness} style={{ animation: over ? "flick .25s infinite" : "none" }} />}
      <line x1="-44" y1="0" x2="-12" y2="0" stroke={C.faint} strokeWidth="2.5" />
      <polygon points="-12,-12 -12,12 10,0" fill={lit ? "#ffe08a" : "#243"} stroke={col} strokeWidth="2" />
      <line x1="10" y1="-13" x2="10" y2="13" stroke={col} strokeWidth="2.5" />
      <line x1="10" y1="0" x2="44" y2="0" stroke={C.faint} strokeWidth="2.5" />
      <g stroke={lit ? C.gold : C.faint} strokeWidth="1.6">
        <line x1="2" y1="-18" x2="12" y2="-28" /><polygon points="12,-28 7,-25 11,-23" fill={lit ? C.gold : C.faint} stroke="none" />
        <line x1="10" y1="-16" x2="20" y2="-26" /><polygon points="20,-26 15,-23 19,-21" fill={lit ? C.gold : C.faint} stroke="none" />
      </g>
      {damaged && <text x="0" y="6" textAnchor="middle" fill={C.crimson} fontSize="22">✕</text>}
      <text x="0" y="30" textAnchor="middle" fill={col} fontSize="11">{damaged ? tt("burnt") : "LED"}</text>
    </g>
  );
}
function Slot({ k, x, y, horizontal, slot, sim, selected, onSelect }) {
  const w = 116, h = 64;
  return (
    <g transform={horizontal ? "" : `rotate(90 ${x} ${y})`}>
      <rect x={x - w / 2} y={y - h / 2} width={w} height={h} fill="#0a0d12" rx="6"
        stroke={selected ? C.blue : "transparent"} strokeWidth="2" strokeDasharray={slot ? "0" : "5 4"}
        style={{ cursor: "pointer" }} onClick={() => onSelect(k)} />
      {!slot && (<>
        <rect x={x - w / 2} y={y - h / 2} width={w} height={h} fill="none" rx="6" stroke={selected ? C.blue : C.faint} strokeWidth="1.5" strokeDasharray="5 4" />
        <text x={x} y={y - 4} textAnchor="middle" fill={selected ? C.blue : C.faint} fontSize="13">{tf("Slot {slot}", { slot: k })}</text>
        <text x={x} y={y + 14} textAnchor="middle" fill={C.faint} fontSize="10">{tt("empty · tap +")}</text>
      </>)}
      {slot && slot.type === "resistor" && <ResistorSym x={x} y={y} R={slot.R} />}
      {slot && slot.type === "wire" && (<>
        <line x1={x - w / 2} y1={y} x2={x + w / 2} y2={y} stroke={C.faint} strokeWidth="2.5" />
        <text x={x} y={y + 22} textAnchor="middle" fill={C.faint} fontSize="10">{tt("wire")}</text>
      </>)}
      {slot && slot.type === "led" && <LedSym x={x} y={y} sim={sim} damaged={slot.damaged} />}
    </g>
  );
}
// AI-generated, per-problem concept diagram (illustrative; sanitized server-side).
function ConceptDiagram({ svg, concept }) {
  if (!svg) return null;
  return (
    <div className="concept-diagram">
      <H><T>Concept diagram</T> <span className="h dim">· <T>AI-drawn for</T> “{concept}”</span></H>
      <div className="concept-diagram__svg" dangerouslySetInnerHTML={{ __html: svg }} />
      <div className="muted-note"><T>Illustration only — build the circuit on the interactive bench below to be assessed.</T></div>
    </div>
  );
}
function Schematic({ sim, voltage, slots, selected, onSelect }) {
  const L = 120, R = 480, T = 90, B = 300, MIDY = 195;
  const dur = sim.Ima > 0.5 && sim.status !== "burnt" ? Math.max(0.5, Math.min(6, 90 / sim.Ima)) : 0;
  const flowing = dur > 0, dots = 9;
  return (
    <svg viewBox="0 0 600 380" className="schematic">
      <defs>
        <radialGradient id="glow" cx="50%" cy="50%" r="50%">
          <stop offset="0%" stopColor="#ffe08a" stopOpacity="0.95" />
          <stop offset="100%" stopColor="#ffe08a" stopOpacity="0" />
        </radialGradient>
        <pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
          <path d="M20 0H0V20" fill="none" stroke="#141a22" strokeWidth="1" />
        </pattern>
        <path id="loopPath" d={`M${L},${T} L${R},${T} L${R},${B} L${L},${B} Z`} fill="none" />
      </defs>
      <rect x="0" y="0" width="600" height="380" fill="url(#grid)" />
      <path d={`M${L},${T} L${R},${T} L${R},${B} L${L},${B} Z`} fill="none" stroke={flowing ? C.teal : C.faint} strokeWidth="2.5" opacity={flowing ? 0.9 : 0.55} />
      {flowing && (
        <g key={`flow-${Math.round(sim.Ima)}`}>
          {Array.from({ length: dots }).map((_, i) => (
            <circle key={i} r="3.4" fill={C.teal}>
              <animateMotion dur={`${dur}s`} repeatCount="indefinite" begin={`-${(i / dots) * dur}s`}>
                <mpath href="#loopPath" />
              </animateMotion>
            </circle>
          ))}
        </g>
      )}
      <Battery x={L} y={MIDY} v={voltage} />
      <Slot k="A" x={300} y={T} horizontal slot={slots.A} sim={sim} selected={selected === "A"} onSelect={onSelect} />
      <Slot k="B" x={R} y={MIDY} slot={slots.B} sim={sim} selected={selected === "B"} onSelect={onSelect} />
      <text x="300" y="350" textAnchor="middle" fill={C.faint} fontSize="11">{tt("● = conventional current (+ → −) · bottom & left rails = wire")}</text>
    </svg>
  );
}

/* ===================== CircuitLab (spec-driven) ===================== */
function CircuitLab({ tutor, role, onExit }) {
  useI18nVersion();
  const spec = tutor.spec;
  const objectives = spec.objectives;
  const tasks = spec.tasks;
  const misc = spec.misconceptions || [];
  const persist = role === "learner"; // only learners write progress

  const [voltage, setVoltage] = useState(spec.supplyDefault || 9);
  const [slots, setSlots] = useState({ A: { type: "wire" }, B: null });
  const [selected, setSelected] = useState("B");
  const [taskId, setTaskId] = useState(tasks[0] && tasks[0].id);
  const [mastery, setMastery] = useState(() => Object.fromEntries(objectives.map((o) => [o.id, 0])));
  const [evidence, setEvidence] = useState([]);
  const [showXapi, setShowXapi] = useState(false);
  const [showHints, setShowHints] = useState(false);

  const sim = useMemo(() => simulate(voltage, slots.A, slots.B), [voltage, slots]);
  const task = tasks.find((t) => t.id === taskId) || tasks[0];

  const credited = useRef(new Set());
  const miscActive = useRef({});
  const pending = useRef([]);   // unsent evidence
  const syncTimer = useRef(null);
  const completedRef = useRef(new Set());

  const scheduleSync = useCallback((nextMastery, nextCompleted) => {
    if (!persist) return;
    if (syncTimer.current) clearTimeout(syncTimer.current);
    syncTimer.current = setTimeout(async () => {
      const events = pending.current; pending.current = [];
      try {
        await api(`/tutors/${tutor.id}/events`, { method: "POST", body: { events, mastery: nextMastery, completedTasks: [...nextCompleted] } });
      } catch (e) { pending.current = events.concat(pending.current); /* retry next time */ }
    }, 700);
  }, [persist, tutor.id]);

  function pushEvent(verb, object, result, color, masterySnap) {
    const stmt = {
      actor: { name: "learner", account: { name: "demo" } },
      verb: { id: `https://w3id.org/xapi/dod/verbs/${verb}`, display: { en: verb } },
      object: { id: `https://circuitlab.edu/xapi/activities/${object.id}`, definition: { name: { en: object.name }, type: "http://adlnet.gov/expapi/activities/interaction" } },
      result, context: { extensions: { tutorId: tutor.id, taskId, voltage, slotA: slots.A && slots.A.type || null, slotB: slots.B && slots.B.type || null } },
      timestamp: new Date().toISOString(),
    };
    const ev = { id: Math.random().toString(36).slice(2), verb, text: object.name, result, color, stmt, t: timeStr() };
    setEvidence((e) => [ev, ...e].slice(0, 40));
    pending.current = [ev, ...pending.current];
    scheduleSync(masterySnap || mastery, completedRef.current);
  }

  // task-credit
  useEffect(() => {
    if (!task) return;
    if (taskMet(sim, task) && !credited.current.has(task.id)) {
      credited.current.add(task.id);
      completedRef.current.add(task.id);
      setMastery((m) => {
        const n = { ...m };
        if (n[task.primary] != null) n[task.primary] = Math.max(n[task.primary], W_PRIMARY);
        (task.secondary || []).forEach((o) => { if (n[o] != null) n[o] = Math.max(n[o], W_SECONDARY); });
        pushEvent("mastered", { id: task.id, name: `Goal met: ${task.title}` }, { completion: true, success: true, response: `${sim.Ima.toFixed(1)} mA` }, C.teal, n);
        return n;
      });
    }
  }, [sim, taskId]); // eslint-disable-line

  // misconceptions
  useEffect(() => {
    misc.forEach((m) => {
      const fired = misconceptionFired(sim, m);
      if (fired && !miscActive.current[m.id]) {
        miscActive.current[m.id] = true;
        setMastery((mm) => {
          const n = { ...mm };
          if (n[m.penalizes] != null) n[m.penalizes] = Math.min(n[m.penalizes], W_PENALTY);
          pushEvent("misconceived", { id: m.id, name: m.name }, { success: false, response: `${sim.Ima.toFixed(0)} mA` }, C.crimson, n);
          return n;
        });
      } else if (!fired && miscActive.current[m.id]) {
        miscActive.current[m.id] = false;
      }
    });
  }, [sim]); // eslint-disable-line

  // destructive overcurrent
  useEffect(() => {
    if (sim.willBurn) {
      setSlots((s) => {
        const n = { ...s };
        ["A", "B"].forEach((k) => { if (n[k] && n[k].type === "led" && !n[k].damaged) n[k] = { ...n[k], damaged: true }; });
        return n;
      });
      pushEvent("failed", { id: "burnout", name: "LED destroyed by overcurrent" }, { success: false, response: `${Math.round(sim.Ima)} mA > ${LED_BURN} mA` }, C.crimson);
    }
  }, [sim.willBurn]); // eslint-disable-line

  function place(type) {
    if (!selected) return;
    const comp = type === "resistor" ? { type, R: 470 } : { type };
    setSlots((s) => ({ ...s, [selected]: comp }));
    pushEvent("placed", { id: `slot-${selected}`, name: `Placed ${type} in slot ${selected}` }, { response: type }, C.blue);
  }
  function clearSlot(k) { setSlots((s) => ({ ...s, [k]: null })); pushEvent("removed", { id: `slot-${k}`, name: `Cleared slot ${k}` }, {}, C.faint); }
  function commitV() { pushEvent("adjusted", { id: "supply", name: `Set supply to ${voltage} V` }, { response: `${voltage} V` }, C.amber); }
  function commitR(k, val) { pushEvent("adjusted", { id: `R-${k}`, name: `Set slot ${k} resistor to ${val} Ω` }, { response: `${val} Ω` }, C.amber); }
  function resetLab() {
    credited.current = new Set(); miscActive.current = {}; completedRef.current = new Set();
    setSlots({ A: { type: "wire" }, B: null }); setVoltage(spec.supplyDefault || 9); setSelected("B");
    setMastery(Object.fromEntries(objectives.map((o) => [o.id, 0]))); setEvidence([]);
  }

  const latest = evidence[0];
  const mvals = objectives.map((o) => mastery[o.id] || 0);
  const overall = Math.round((mvals.reduce((a, b) => a + b, 0) / (objectives.length || 1)) * 100);

  return (
    <div className="lab" data-help="circuit-lab">
      {/* header */}
      <div className="lab__header">
        <div>
          <div className="lab__title nr">
            {spec.title} <span className="t-teal">·</span> <span className="brand__sub"><T>interaction-as-assessment</T></span>
          </div>
          <div className="lab__summary">{spec.summary}</div>
        </div>
        <div className="row" style={{ gap: 16 }}>
          <Gauge label="MASTERY" value={overall} suffix="%" color={C.teal} />
          <button onClick={resetLab} className="btn btn-reset"><T>Reset</T></button>
          <button onClick={onExit} className="btn btn-muted"><T>← Back</T></button>
        </div>
      </div>

      {/* task strip */}
      <div className="lab__tasks">
        {tasks.map((t) => {
          const active = t.id === taskId, done = credited.current.has(t.id);
          return (
            <button key={t.id} onClick={() => setTaskId(t.id)} className={`btn task-btn${active ? " is-active" : ""}${done ? " is-done" : ""}`}>
              {done ? "✓ " : ""}{t.id}: {t.title}
            </button>
          );
        })}
        {role !== "learner" && <span className="t-amber" style={{ fontSize: 11 }}><T>preview mode — progress not recorded</T></span>}
      </div>
      <div className="lab__goal">▸ {task && task.goal}</div>

      {/* main grid */}
      <div className="lab__grid">
        {/* LEFT: build */}
        <div className="lab__col">
          <H><T>Bench</T></H>
          <Sub><T>Supply voltage</T></Sub>
          <div className="range-row">
            <input type="range" min={0} max={12} step={0.5} value={voltage} onChange={(e) => setVoltage(parseFloat(e.target.value))} onPointerUp={commitV} className="grow" />
            <span className="range-val">{voltage.toFixed(1)} V</span>
          </div>
          <Sub><T>Target slot</T></Sub>
          <div className="row" style={{ gap: 8, marginBottom: 14 }}>
            {["A", "B"].map((k) => (
              <button key={k} onClick={() => setSelected(k)} className={`btn toggle grow${selected === k ? " is-active" : ""}`}>
                {slots[k] ? tf("Slot {slot} · {type}", { slot: k, type: slots[k].type }) : tf("Slot {slot} · empty", { slot: k })}
              </button>
            ))}
          </div>
          <Sub>{tf("Add component → slot {slot}", { slot: selected })}</Sub>
          <div className="stack" style={{ marginBottom: 14 }}>
            {PALETTE.map((p) => (
              <button key={p.type} onClick={() => place(p.type)} className="btn btn-outline btn-start palette-row">
                <span><MiniSym type={p.type} /> &nbsp;<T>{p.label}</T></span>
                <span className="muted-note"><T>{p.hint}</T></span>
              </button>
            ))}
            <button onClick={() => selected && clearSlot(selected)} className="btn btn-clear">{tf("Clear slot {slot}", { slot: selected })}</button>
          </div>
          {["A", "B"].map((k) => slots[k] && slots[k].type === "resistor" ? (
            <div key={k} style={{ marginBottom: 12 }}>
              <Sub>{tf("Slot {slot} resistance", { slot: k })}</Sub>
              <div className="row">
                <input type="range" min={10} max={2200} step={10} value={slots[k].R}
                  onChange={(e) => setSlots((s) => ({ ...s, [k]: { ...s[k], R: parseInt(e.target.value) } }))}
                  onPointerUp={(e) => commitR(k, parseInt(e.target.value))} className="grow" />
                <span className="range-val range-val--wide">{slots[k].R} Ω</span>
              </div>
            </div>
          ) : null)}
          {sim.damaged && (
            <button onClick={() => setSlots((s) => { const n = { ...s }; ["A", "B"].forEach((k) => { if (n[k] && n[k].type === "led") n[k] = { type: "led" }; }); return n; })} className="btn btn-replace"><T>⟳ Replace burnt-out LED</T></button>
          )}
          {spec.hints && spec.hints.length > 0 && (
            <div style={{ marginTop: 16 }}>
              <button onClick={() => setShowHints((h) => !h)} className="btn btn-muted btn-outline btn-block">
                {showHints ? tf("Hide hints ({n})", { n: spec.hints.length }) : tf("Show hints ({n})", { n: spec.hints.length })}
              </button>
              {showHints && <ul className="hints">{spec.hints.map((h, i) => <li key={i}>{h}</li>)}</ul>}
            </div>
          )}
        </div>

        {/* CENTER: schematic */}
        <div className="lab__col lab__col--flex">
          <ConceptDiagram svg={spec.diagram} concept={tutor.concept} />
          <H><T>Interactive bench</T> <span className="h dim"><T>· tap a slot, then add a part</T></span></H>
          <Schematic sim={sim} voltage={voltage} slots={slots} selected={selected} onSelect={setSelected} />
          <StatusBar sim={sim} />
          <div className="meter-grid">
            <Meter label="Current" v={sim.Ima} unit="mA" digits={1} color={meterColor(sim)} />
            <Meter label="Supply" v={voltage} unit="V" digits={1} color={C.amber} />
            <Meter label="V·R" v={sim.Vr} unit="V" digits={2} color={C.blue} />
            <Meter label="P·R" v={sim.Pr} unit="W" digits={3} color={sim.Pr > R_RATING ? C.crimson : C.mute} />
          </div>
        </div>

        {/* RIGHT: assessment */}
        <div className="lab__col lab__col--flex lab__col--assess">
          <H><T>Assessment</T> <span className="h dim"><T>· live</T></span></H>
          <Sub><T>Objective mastery</T></Sub>
          <div className="bars">
            {objectives.map((o) => (
              <div key={o.id} title={o.desc}>
                <div className="bar-head">
                  <span>{o.id} · {o.label}</span><span className="t-mute">{Math.round((mastery[o.id] || 0) * 100)}%</span>
                </div>
                <div className="bar-track">
                  <div className="bar-fill" style={{ width: `${(mastery[o.id] || 0) * 100}%`, background: barColor(mastery[o.id] || 0) }} />
                </div>
              </div>
            ))}
          </div>
          <div className="between">
            <Sub><T>Evidence stream</T></Sub>
            <button onClick={() => setShowXapi((x) => !x)} className="btn btn-muted btn-outline btn-tiny">{showXapi ? tt("hide xAPI") : tt("view xAPI")}</button>
          </div>
          {showXapi && latest && (
            <pre className="scroll xapi">{JSON.stringify(latest.stmt, null, 2)}</pre>
          )}
          <div className="scroll evidence">
            {evidence.length === 0 && <div className="evidence__empty"><T>Interact with the circuit — evidence appears here.</T></div>}
            {evidence.map((e) => (
              <div key={e.id} className="evidence__row">
                <span className="evidence__verb" style={{ color: e.color }}>{e.verb}</span>
                <span className="grow">{e.text}{e.result && e.result.response ? <span className="t-faint"> · {e.result.response}</span> : null}</span>
                <span className="evidence__time">{e.t}</span>
              </div>
            ))}
          </div>
          <div className="footnote">
            {persist
              ? <T>Every interaction is captured as xAPI evidence, mapped to objectives, and scored by a transparent rubric and synced to your progress record.</T>
              : <T>Every interaction is captured as xAPI evidence, mapped to objectives, and scored by a transparent rubric.</T>}
          </div>
        </div>
      </div>
    </div>
  );
}

/* ===================== shared chrome ===================== */
const VIEW_AS = [["admin", "Admin"], ["educator", "Teacher"], ["learner", "Student"]];
const ROLE_LABEL = { admin: "Admin", educator: "Teacher", learner: "Student" };
function TopBar({ user, provider, subject, viewAs, onViewAs, onLogout, children }) {
  const roleLabel = ROLE_LABEL[user.role] || user.role;
  const [mail, setMail] = useState(null); // null | 'sending' | 'sent' | error string
  const [explain, setExplain] = useState(false); // "Explain-It" quick-sketch modal
  async function testEmail() {
    setMail("sending");
    try { await api("/email/test", { method: "POST" }); setMail("sent"); setTimeout(() => setMail(null), 4000); }
    catch (e) { setMail(e.message); setTimeout(() => setMail(null), 5000); }
  }
  const canEmail = provider && provider !== "dev";
  const subjectLabel = SUBJECT_LABELS[subject] || "Tutors";
  return (
    <div className="topbar">
      <div className="brand nr">STEM&nbsp;Bench <span className="t-teal">·</span> <span className="brand__sub"><T>{subjectLabel}</T></span>
        {subject && <a href="/" className="muted-note" style={{ marginInlineStart: 8, fontSize: 11 }} title={tt("Switch subject")}>⇄ <T>switch</T></a>}</div>
      <div className="topbar__right">
        {children}
        <button onClick={() => setExplain(true)} className="btn btn-muted" title={tt("Type any concept → an animated sketch of how it works")}>✨ <T>Explain-It</T></button>
        {explain && <ExplainItModal onClose={() => setExplain(false)} />}
        <LanguagePicker />
        {onViewAs && (
          <div className="seg" title={tt("As admin you can use the app as a teacher or student")}>
            {VIEW_AS.map(([v, label]) => (
              <button key={v} onClick={() => onViewAs(v)} className={`seg__btn${viewAs === v ? " is-active" : ""}`}>
                <T>{label}</T>
              </button>
            ))}
          </div>
        )}
        {canEmail && (
          <button onClick={testEmail} title={tt("Send a confirmation email to yourself via the gateway SMTP proxy")}
            className={`btn${mail === "sent" ? " btn-primary" : " btn-muted"}`}>
            {mail === "sending" ? tt("Sending…") : mail === "sent" ? tt("Email sent ✓") : mail && mail !== "sent" ? tt("Email failed") : tt("Email me a test")}
          </button>
        )}
        <span className="usermeta">{user.name} · <span className="t-blue">{tt(roleLabel)}</span>{viewAs && viewAs !== user.role ? <span className="t-amber"> → {tf("as {role}", { role: tt(ROLE_LABEL[viewAs]) })}</span> : null}{provider && provider !== "dev" ? <span className="t-faint"> · {provider}</span> : null}</span>
        <button onClick={onLogout} className="btn btn-muted"><T>Sign out</T></button>
      </div>
    </div>
  );
}
function Card({ children, className, ...rest }) {
  // `rest` forwards passthrough attrs like data-help (team-help analytics anchor) to the DOM.
  return <div className={`card${className ? " " + className : ""}`} {...rest}>{children}</div>;
}
function Pill({ children, color }) {
  return <span className="pill" style={color ? { borderColor: color, color } : undefined}>{children}</span>;
}

/* ===================== login ===================== */
const PROVIDER_LABEL = { google: "Google", microsoft: "Microsoft", github: "GitHub" };

// Email + password auth via the gateway SMTP proxy: login / register (->verify code).
function EmailAuth({ onLogin }) {
  const [mode, setMode] = useState("login"); // login | register | verify
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [name, setName] = useState("");
  const [code, setCode] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  const [msg, setMsg] = useState(null);
  async function submit() {
    setBusy(true); setErr(null); setMsg(null);
    try {
      if (mode === "login") { const { user } = await api("/auth/email/login", { method: "POST", body: { email: email.trim(), password } }); onLogin(user); return; }
      if (mode === "register") {
        const r = await api("/auth/email/register", { method: "POST", body: { email: email.trim(), password, name: name.trim() || undefined } });
        if (r.user) { onLogin(r.user); return; }   // already in SMTP → signed in automatically, no verify step
        setMsg(r.message || tt("Verification code sent — check your email.")); setMode("verify"); return;
      }
      const { user } = await api("/auth/email/verify", { method: "POST", body: { email: email.trim(), code: code.trim() } }); onLogin(user);
    } catch (e) {
      // Existing SMTP account but the password didn't authenticate → drop into sign-in.
      if (mode === "register" && /already registered/i.test(e.message || "")) setMode("login");
      setErr(e.message);
    } finally { setBusy(false); }
  }
  async function resend() { setErr(null); setMsg(null); try { const r = await api("/auth/email/resend", { method: "POST", body: { email: email.trim() } }); setMsg(r.message || tt("Code resent.")); } catch (e) { setErr(e.message); } }
  const submitLabel = busy ? "…" : mode === "login" ? tt("Sign in") : mode === "register" ? tt("Create account") : tt("Verify & enter");
  return (
    <div>
      {err && <div className="err" style={{ marginBottom: 8 }}>{err}</div>}
      {msg && <div className="ok">{msg}</div>}
      {mode !== "verify" && <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder={tt("email")} className="field" />}
      {mode === "register" && <input value={name} onChange={(e) => setName(e.target.value)} placeholder={tt("name (optional)")} className="field" />}
      {mode !== "verify" && <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder={tt("password")} onKeyDown={(e) => e.key === "Enter" && submit()} className="field" />}
      {mode === "verify" && (<>
        <div className="helptext">{tf("Enter the 6-digit code sent to {email}.", { email })}</div>
        <input value={code} onChange={(e) => setCode(e.target.value)} placeholder="123456" onKeyDown={(e) => e.key === "Enter" && submit()} className="field" />
      </>)}
      <button onClick={submit} disabled={busy} className="btn btn-primary btn-block">{submitLabel}</button>
      <div className="auth-foot">
        {mode === "login" && <a onClick={() => { setMode("register"); setErr(null); setMsg(null); }} className="link"><T>Create an account</T></a>}
        {mode === "register" && <a onClick={() => { setMode("login"); setErr(null); setMsg(null); }} className="link"><T>Have an account? Sign in</T></a>}
        {mode === "verify" && <a onClick={resend} className="link"><T>Resend code</T></a>}
        {mode === "verify" && <a onClick={() => { setMode("login"); setErr(null); setMsg(null); }} className="link--mute"><T>Back</T></a>}
      </div>
    </div>
  );
}

function Login({ config, authError, onLogin }) {
  useI18nVersion();
  const [role, setRole] = useState("educator");
  const [name, setName] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(authError || null);
  const roles = [
    { v: "admin", label: "Admin", desc: "Oversee users, tutors & usage" },
    { v: "educator", label: "Teacher", desc: "Author tutors from a concept" },
    { v: "learner", label: "Student", desc: "Learn by building circuits" },
  ];
  async function go() {
    setBusy(true); setErr(null);
    try { const { user } = await api("/auth/login", { method: "POST", body: { role, name: name.trim() || undefined } }); onLogin(user); }
    catch (e) { setErr(e.message); } finally { setBusy(false); }
  }
  const oauth = config && config.oauth;
  const emailAuth = config && config.emailAuth;
  return (
    <div className="center-screen">
      <Card className="login-card">
        <div className="flex-end"><LanguagePicker /></div>
        <div className="brand brand--lg nr">STEM&nbsp;Bench</div>
        <div className="helptext"><T>Interactive simulation &amp; tutoring across STEM. Sign in to continue.</T></div>
        {err && <div className="err-box">{err}</div>}

        {oauth && (
          <div style={{ marginBottom: dev(config) ? 18 : 0 }}>
            <Sub><T>Sign in with</T></Sub>
            <div className="stack">
              {(config.providers || []).map((p) => (
                <a key={p} href={`/api/auth/oauth/start?provider=${p}`} className="btn btn-outline provider-link">
                  {tf("Continue with {provider}", { provider: PROVIDER_LABEL[p] || p })}
                </a>
              ))}
            </div>
            <div className="muted-note" style={{ marginTop: 8 }}><T>Your role is assigned automatically from your email (admin / teacher / student).</T></div>
          </div>
        )}

        {emailAuth && (
          <div style={{ marginTop: oauth ? 14 : 0 }}>
            {oauth && <div className="divider"><T>or use email & password</T></div>}
            <EmailAuth onLogin={onLogin} />
          </div>
        )}

        {dev(config) && (<>
          {(oauth || emailAuth) && <div className="divider"><T>or dev sign-in</T></div>}
          <Sub><T>Role</T></Sub>
          <div className="stack" style={{ marginBottom: 16 }}>
            {roles.map((r) => (
              <button key={r.v} onClick={() => setRole(r.v)} className={`btn role-btn${role === r.v ? " is-active" : ""}`}>
                <span className="btn-bold"><T>{r.label}</T></span><span style={{ fontSize: 11, color: role === r.v ? C.bg : C.faint }}><T>{r.desc}</T></span>
              </button>
            ))}
          </div>
          <Sub><T>Display name (optional)</T></Sub>
          <input value={name} onChange={(e) => setName(e.target.value)} placeholder={tt("e.g. Ms. Rivera")} className="field" />
          <button onClick={go} disabled={busy} className="btn btn-primary btn-block">{busy ? tt("Signing in…") : tt("Enter (dev)")}</button>
        </>)}
      </Card>
    </div>
  );
}
function dev(config) { return config && config.devLogin; }

/* ===================== landing ===================== */
// Public marketing/intro page for logged-out visitors. Frames STEM Bench as a pilot,
// explains the interaction-as-assessment "bench" idea, showcases the live benches, and
// reveals the existing <Login> card on "Sign in" (OAuth / email / dev — all reused).
const SHOWCASE_MAX = 8; // benches shown per domain tab — a fresh random sample each visit
function BenchShowcase() {
  // Grouped by STEM course (Mathematics, Physics, …), mirroring the SUBJECTS catalog and
  // the logged-in SubjectPicker. subjectsByCat()/SUBJECTS are defined below; both are
  // resolved at render time (function declarations are hoisted; SUBJECTS exists by then).
  const groups = (typeof subjectsByCat !== "undefined") ? subjectsByCat() : [];
  const [tab, setTab] = useState(0);
  // Logged-out visitors can try a live example (preview popup) or read the bench's
  // documentation — description + learning objectives — in a separate doc modal.
  const [demo, setDemo] = useState(null);
  const [doc, setDoc] = useState(null);
  // Pick a random sample (up to SHOWCASE_MAX) per domain ONCE per mount, so the strip feels
  // alive and different on every visit but stays stable while the visitor clicks between tabs.
  const samples = useMemo(
    () => groups.map((g) => shuffle(g.items).slice(0, SHOWCASE_MAX)),
    [groups.length] // eslint-disable-line react-hooks/exhaustive-deps
  );
  if (!groups.length) return null;
  const active = groups[Math.min(tab, groups.length - 1)];
  const shown = samples[Math.min(tab, groups.length - 1)] || [];
  return (
    <div>
      <div className="bench-tabs" role="tablist">
        {groups.map((g, i) => (
          <button key={g.cat} type="button" role="tab" aria-selected={i === tab}
            className={"bench-tab" + (i === tab ? " is-active" : "")} onClick={() => setTab(i)}>
            <T>{g.cat}</T><span className="bench-tab__count">{g.items.length}</span>
          </button>
        ))}
      </div>
      <div className="bench-tabpanel" role="tabpanel">
        {active.items.length > shown.length && (
          <div className="bench-sample-note"><T>Showing</T> {shown.length} <T>of</T> {active.items.length} — <T>a fresh pick each visit</T></div>
        )}
        <div className="bench-grid">
          {shown.map((s) => (
            <div key={s.id} className="bench-card bench-card--live" style={{ borderTop: `3px solid ${s.color}` }}>
              <div className="bench-card__name" style={{ color: s.color }}><T>{s.label}</T></div>
              <div className="bench-card__desc"><T>{s.desc}</T></div>
              <div className="bench-card__actions">
                <button type="button" className="bench-card__btn" style={{ color: s.color, borderColor: s.color }} onClick={() => setDemo(s.id)} title={tt("Try a live example")}><T>Try an example</T> →</button>
                <button type="button" className="bench-card__link" onClick={() => setDoc(s.id)} title={tt("Read the description & learning objectives")}>ⓘ <T>About & objectives</T></button>
              </div>
            </div>
          ))}
        </div>
      </div>
      {demo && <BenchExampleModal subject={demo} onClose={() => setDemo(null)} />}
      {doc && <BenchDocModal subject={doc} onClose={() => setDoc(null)} />}
    </div>
  );
}

// Animated bench vignettes for the landing hero. Each is a self-contained, scriptless
// SVG (CSS/SMIL) generated by scripts/gen-landing-anim.js → public/landing/*.svg: a
// student cursor grabs a control, the bench responds, a live-grade meter fills to ✓.
// We randomly pick a few each load so the hero feels alive and different on every visit.
// Fallback list mirrors landing/manifest.json so the strip renders even before the fetch.
const ANIM_FALLBACK = [
  { id: "electronics", title: "Electronics", area: "Physics", color: "#6cb6ff", file: "electronics.svg" },
  { id: "optics", title: "Optics", area: "Physics", color: "#ffce6b", file: "optics.svg" },
  { id: "scatter", title: "Scatterplot", area: "Mathematics", color: "#54f2b2", file: "scatter.svg" },
  { id: "projectile", title: "Projectile", area: "Physics", color: "#ff7a6b", file: "projectile.svg" },
  { id: "pendulum", title: "Pendulum", area: "Physics", color: "#e3b341", file: "pendulum.svg" },
  { id: "waves", title: "Waves", area: "Physics", color: "#6cb6ff", file: "waves.svg" },
];
function shuffle(arr) {
  const r = arr.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;
}
function BenchHeroAnimations({ count = 3 }) {
  // Stable random pick on first paint; upgrade to the live manifest once it loads.
  const [picks, setPicks] = useState(() => shuffle(ANIM_FALLBACK).slice(0, count));
  useEffect(() => {
    let alive = true;
    fetch("/landing/manifest.json")
      .then((r) => r.json())
      .then((list) => { if (alive && Array.isArray(list) && list.length) setPicks(shuffle(list).slice(0, count)); })
      .catch(() => {});
    return () => { alive = false; };
  }, [count]);
  return (
    <div className="bench-anim-strip" aria-hidden="true">
      {picks.map((s) => (
        <figure key={s.id} className="bench-anim" style={{ "--anim-accent": s.color }}>
          <img src={`/landing/${s.file}`} alt={`${s.title} bench — animated demo`} loading="lazy" className="bench-anim__img" />
        </figure>
      ))}
    </div>
  );
}

// A self-contained, no-login example of a single bench, shown in a popup over the landing
// page. Pulls the bench's deterministic fallback spec from the public endpoint and renders
// the real lab in "preview" mode (no persistence, no coach calls) as the anonymous user.
function BenchExampleModal({ subject, onClose }) {
  useI18nVersion();
  const [spec, setSpec] = useState(null);
  const [err, setErr] = useState(null);
  useEffect(() => {
    let alive = true;
    api(`/benches/${subject}/example`).then((r) => { if (alive) setSpec(r.spec); }).catch((e) => { if (alive) setErr(e.message); });
    return () => { alive = false; };
  }, [subject]);
  // Lock background scroll while the popup is open; restore on close.
  useEffect(() => { const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, []);
  // Esc to close.
  useEffect(() => { const h = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [onClose]);
  const sub = (typeof SUBJECTS !== "undefined" ? SUBJECTS : []).find((x) => x.id === subject);
  const Lab = benchLabFor(subject);
  const tutor = spec ? { id: `example-${subject}`, subject, concept: (sub && sub.label) || subject, level: "beginner", spec, userId: "anonymous" } : null;
  return (
    <div className="bench-demo-overlay" onClick={onClose}>
      <div className="bench-demo-shell" onClick={(e) => e.stopPropagation()}>
        <div className="bench-demo-toolbar">
          {/* "About & objectives" lives in the workbench masthead (benchkit Tutor) now. */}
          <button className="bench-demo-close" onClick={onClose} aria-label={tt("Close example")} title={tt("Close")}>✕</button>
        </div>
        {err && <div className="bench-demo-msg t-crimson"><T>Couldn’t load this example.</T> {err}</div>}
        {!spec && !err && <div className="bench-demo-msg"><T>Loading example…</T></div>}
        {tutor && <Lab key={subject} tutor={tutor} role="preview" onExit={onClose} />}
      </div>
    </div>
  );
}

// Playable preview of a CUSTOM (teacher-designed) bench: fetches a deterministic example spec (no
// LLM, no progress saved) and renders it through the generic DynamicTutor harness — the same way an
// authored tutor over this bench renders — so a teacher can TRY the microworld before authoring on it.
function BenchPreviewModal({ benchId, label, onClose }) {
  useI18nVersion();
  const [spec, setSpec] = useState(null);
  const [err, setErr] = useState(null);
  useEffect(() => {
    let alive = true;
    api(`/benches/${benchId}/preview`).then((r) => { if (alive) setSpec(r.spec); }).catch((e) => { if (alive) setErr(e.message); });
    return () => { alive = false; };
  }, [benchId]);
  useEffect(() => { const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, []);
  useEffect(() => { const h = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [onClose]);
  const Dyn = window.DynamicTutor;
  const tutor = spec ? { id: `preview-${benchId}`, subject: benchId, concept: label || spec.title || benchId, level: "beginner", spec, userId: "anonymous" } : null;
  return (
    <div className="bench-demo-overlay" onClick={onClose}>
      <div className="bench-demo-shell" onClick={(e) => e.stopPropagation()}>
        <div className="bench-demo-toolbar">
          <span className="muted-note" style={{ marginInlineEnd: "auto" }}>👁 <T>Bench preview</T> <span className="t-faint">· <T>nothing is saved</T></span></span>
          <button className="bench-demo-close" onClick={onClose} aria-label={tt("Close preview")} title={tt("Close")}>✕</button>
        </div>
        {err && <div className="bench-demo-msg t-crimson"><T>Couldn’t load this preview.</T> {err}</div>}
        {!spec && !err && <div className="bench-demo-msg"><T>Loading preview…</T></div>}
        {tutor && Dyn && <Dyn key={benchId} tutor={tutor} role="preview" onExit={onClose} />}
        {tutor && !Dyn && <div className="bench-demo-msg t-crimson"><T>Preview is unavailable right now.</T></div>}
      </div>
    </div>
  );
}

// ── "Explain-It": type any concept → an animated, AI-drawn SVG of how it works ──
// A quick, ungrounded explainer that sits BESIDE the gradeable benches (not inside them);
// labeled as a sketch so it's never confused with the honest-physics simulations. Reuses the
// learn-overlay/shell layout. Signed-in only + lightly throttled, enforced server-side (/api/explain).
function ExplainItModal({ onClose }) {
  useI18nVersion();
  const [concept, setConcept] = useState("");
  const [svg, setSvg] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");
  useEffect(() => { const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, []);
  useEffect(() => { const h = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [onClose]);
  async function run(e) {
    if (e) e.preventDefault();
    const c = concept.trim();
    if (c.length < 3) { setErr(tt("Describe what you want to understand (a few words).")); return; }
    setBusy(true); setErr(""); setSvg("");
    try {
      const r = await api("/explain", { method: "POST", body: { concept: c } });
      setSvg(r.svg || "");
      if (!r.svg) setErr(tt("Couldn’t draw that one — try rephrasing."));
    } catch (ex) { setErr(ex.message); }
    finally { setBusy(false); }
  }
  return (
    <div className="learn-overlay" onClick={onClose} role="dialog" aria-modal="true">
      <div className="learn-shell" onClick={(e) => e.stopPropagation()}>
        <div className="learn-head">
          <span className="learn-title">✨ <T>Explain-It</T> <span className="h dim">· <T>quick animated sketch</T></span></span>
          <div className="learn-actions">
            <button className="learn-close" onClick={onClose} aria-label={tt("Close")} title={tt("Close")}>✕</button>
          </div>
        </div>
        <div className="learn-body" style={{ padding: 16, overflow: "auto" }}>
          <form onSubmit={run} style={{ display: "flex", gap: 8, marginBottom: 12 }}>
            <input autoFocus value={concept} onChange={(e) => setConcept(e.target.value)}
              placeholder={tt("e.g. how a pin-tumbler lock works")} aria-label={tt("Describe what you want to understand")}
              maxLength={400}
              style={{ flex: 1, padding: "9px 12px", borderRadius: 8, border: `1px solid ${C.line}`, background: C.panel2, color: C.ink, fontSize: 14 }} />
            <button type="submit" className="btn btn-primary" disabled={busy}>{busy ? tt("Drawing…") : tt("Show me")}</button>
          </form>
          {err && <div className="muted-note" style={{ color: C.crimson }}>{err}</div>}
          {busy && <div className="learn-loading"><T>Sketching how it works…</T></div>}
          {!busy && svg && (
            <>
              <div className="concept-diagram__svg" dangerouslySetInnerHTML={{ __html: svg }} />
              <div className="muted-note" style={{ marginTop: 8 }}>✦ <T>AI sketch — illustrative only, not a graded bench. For honest, gradeable practice, open a bench.</T></div>
            </>
          )}
          {!busy && !svg && !err && <div className="muted-note"><T>Type any concept and get an animated diagram of how it works.</T></div>}
        </div>
      </div>
    </div>
  );
}

// ── "learn relevant concepts" via the alwaysAI platform (server/alwaysai.js) ──
// LearnModal frames alwaysAI's hosted learning UI in a large in-app popup — the SAME
// pattern as the "Explore concepts" map modal (benchkit ExploreModal) — instead of
// opening a new browser tab. alwaysai10.skoonline.org is a peer skoonline service that
// allows framing (like ke.skoonline.org). We still offer an "Open in new tab" escape
// hatch for learners who want the journey full-screen.
function LearnModal({ url, title, onClose }) {
  useI18nVersion();
  const [ready, setReady] = useState(false);
  useEffect(() => { const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, []);
  useEffect(() => { const h = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [onClose]);
  const label = title || tt("Guided learning journey");
  return (
    <div className="learn-overlay" onClick={onClose} role="dialog" aria-modal="true">
      <div className="learn-shell" onClick={(e) => e.stopPropagation()}>
        <div className="learn-head">
          <span className="learn-title">🎓 {label}</span>
          <div className="learn-actions">
            <a href={url} target="_blank" rel="noopener noreferrer" className="btn btn-outline btn-tiny" title={tt("Open this learning journey in a new browser tab")}>↗ <T>Open in new tab</T></a>
            <button className="learn-close" onClick={onClose} aria-label={tt("Close")} title={tt("Close")}>✕</button>
          </div>
        </div>
        <div className="learn-body">
          {!ready && <div className="learn-loading"><T>Building your learning journey…</T></div>}
          <iframe key={url} src={url} title={label} onLoad={() => setReady(true)} className="learn-frame" />
        </div>
      </div>
    </div>
  );
}

// Resolve the alwaysAI learning URL for a single concept in a given learning mode
// (pbl | sbl | case). The server maps the mode onto alwaysAI's learningType.
async function conceptLearnUrl(subject, concept, mode) {
  // Pass the UI language explicitly (?lang=) so the journey is built/localized for it; the
  // learner's id + email are attributed server-side from the session (req.user).
  const lang = (window.I18N && window.I18N.lang) || "en";
  const r = await api(`/learn/concept?lang=${encodeURIComponent(lang)}`, { method: "POST", body: { subject, concept, learningType: mode } });
  if (!r || !r.url) throw new Error("No learning URL returned.");
  return r.url;
}

// The three learning modes offered per concept. alwaysAI natively supports pbl & case;
// "sbl" (scenario-based) isn't a platform type, so the server maps it onto pbl.
const LEARN_MODES = [
  { id: "pbl", label: "PBL", title: "Problem-Based Learning" },
  { id: "sbl", label: "SBL", title: "Scenario-Based Learning" },
  { id: "case", label: "CBL", title: "Case-Based Learning" },
];

// Module-scoped so every component (topbar, TeacherDashboard, …) resolves it; standard-subject display names.
const SUBJECT_LABELS = { statistics: "Statistics", electronics: "Electronics", advelectronics: "Advanced Electronics", optics: "Optics", anova: "ANOVA", scatter: "Regression", geometry: "Geometry", advgeometry: "Advanced Geometry", advstats: "Advanced Statistics", advalgebra: "Advanced Algebra", gaslaws: "Gas Laws", titration: "Titration & pH", equilibrium: "Equilibrium", punnett: "Punnett Square", population: "Population Growth", hardyweinberg: "Hardy–Weinberg", kepler: "Kepler Orbits", halflife: "Radioactive Dating", seasons: "Seasons", logicgates: "Logic Gates", bigo: "Big-O Growth", binary: "Binary Numbers" };
const FRAMEWORK_LABELS = { ap: "AP", ngss: "NGSS", ccssm: "CCSS-M", csta: "CSTA", ib: "IB", gaokao: "高考", zhongkao: "中考" };
const FIT_COLORS = { core: C.teal, related: C.gold, stretch: C.crimson };
function prettyField(field) {
  return String(field || "").split(".").map((s) => s ? s[0].toUpperCase() + s.slice(1) : s).join(" · ");
}

// Documentation modal for a bench: plain-language description, the learning objectives,
// the governing law/theory, and curriculum alignment. (The catalog of concepts to study
// lives in its own "Learn" modal — see BenchLearnModal.)
function BenchDocModal({ subject, onClose }) {
  useI18nVersion();
  const [doc, setDoc] = useState(null);
  const [err, setErr] = useState(null);
  useEffect(() => {
    let alive = true;
    api(`/benches/${subject}/about`).then((r) => { if (alive) setDoc(r); }).catch((e) => { if (alive) setErr(e.message); });
    return () => { alive = false; };
  }, [subject]);
  useEffect(() => { const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, []);
  useEffect(() => { const h = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [onClose]);
  const sub = (typeof SUBJECTS !== "undefined" ? SUBJECTS : []).find((x) => x.id === subject);
  const accent = (sub && sub.color) || C.teal;
  const dom = doc && doc.theory && doc.theory.domain;
  const learn = doc && doc.theory && doc.theory.learning;
  return (
    <div className="bench-doc-overlay" onClick={onClose}>
      <div className="bench-doc" onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true">
        <div className="bench-doc__head" style={{ borderBlockEnd: `3px solid ${accent}` }}>
          <div>
            <div className="bench-doc__title">{doc ? doc.label : (sub && sub.label) || subject}</div>
            {doc && <div className="bench-doc__field">{prettyField(doc.field)}</div>}
          </div>
          <button className="bench-doc__close" onClick={onClose} aria-label={tt("Close")} title={tt("Close")}>✕</button>
        </div>
        <div className="bench-doc__body">
          {err && <div className="t-crimson"><T>Couldn’t load this document.</T> {err}</div>}
          {!doc && !err && <div className="muted-note"><T>Loading…</T></div>}
          {doc && (<>
            {doc.summary && <p className="bench-doc__lede">{doc.summary}</p>}

            <h3 className="bench-doc__h"><T>Learning objectives</T></h3>
            <ul className="bench-doc__objlist">
              {doc.objectives.map((o) => (
                <li key={o.id}><b>{o.label}</b>{o.desc ? <> — {o.desc}</> : null}</li>
              ))}
            </ul>

            {dom && (dom.law || (dom.equations || []).length > 0) && (<>
              <h3 className="bench-doc__h"><T>The governing law</T></h3>
              {dom.law && <div className="bench-doc__law">{dom.law}</div>}
              {(dom.equations || []).length > 0 && (
                <div className="bench-doc__eqs">{dom.equations.map((e, i) => <code key={i}>{e}</code>)}</div>
              )}
              {(dom.assumptions || []).length > 0 && (
                <div className="bench-doc__assume"><span className="bench-doc__cap"><T>Assumptions</T>:</span> {dom.assumptions.join(" · ")}</div>
              )}
            </>)}

            {learn && (learn.framework || learn.progression) && (<>
              <h3 className="bench-doc__h"><T>How you’ll learn</T></h3>
              {learn.framework && <div className="bench-doc__assume"><span className="bench-doc__cap"><T>Approach</T>:</span> {learn.framework}</div>}
              {learn.progression && <p className="bench-doc__prog">{learn.progression}</p>}
              {(learn.principles || []).length > 0 && (
                <div className="bench-doc__chips">{learn.principles.map((p, i) => <span key={i} className="bench-doc__chip">{p}</span>)}</div>
              )}
            </>)}

            {(doc.frameworks || []).length > 0 && (<>
              <h3 className="bench-doc__h"><T>Curriculum alignment</T></h3>
              <div className="bench-doc__chips">{doc.frameworks.map((f) => <span key={f} className="bench-doc__chip bench-doc__chip--std">{FRAMEWORK_LABELS[f] || f.toUpperCase()}</span>)}</div>
            </>)}
          </>)}
        </div>
      </div>
    </div>
  );
}
// Exposed globally so the bench workbench (benchkit.js, loaded earlier) can open the
// "About & objectives" doc next to its "Explore concepts" button.
window.BenchDocModal = BenchDocModal;

// ── "Learn" modal: explore-concept LAYOUT (left list of concepts → content on the right) ──
// The left column lists every relevant concept (the catalog formerly shown in the About
// modal); clicking one loads its guided learning journey in the right pane, with a PBL /
// SBL / CBL mode switch above the frame. alwaysAI's hosted UI is framed here (same pattern
// as ExploreModal) — an "Open in new tab" escape hatch covers hosts that block embedding.
function BenchLearnModal({ subject, onClose }) {
  useI18nVersion();
  const [doc, setDoc] = useState(null);
  const [err, setErr] = useState(null);
  const [sel, setSel] = useState(0);           // selected concept index (into flat list)
  const [mode, setMode] = useState("pbl");     // pbl | sbl | case
  // Each visited concept × mode pair keeps its OWN iframe alive, so switching back to a
  // journey shows the already-generated one (preserving the learner's progress) instead of
  // re-generating a fresh one. Map: key → { url, status, label }. status: loading|ready|error.
  const [frames, setFrames] = useState({});
  useEffect(() => {
    let alive = true;
    api(`/benches/${subject}/about`).then((r) => { if (alive) setDoc(r); }).catch((e) => { if (alive) setErr(e.message); });
    return () => { alive = false; };
  }, [subject]);
  useEffect(() => { const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, []);
  useEffect(() => { const h = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [onClose]);
  // Flatten the categorized catalog into a single ordered list of concepts.
  const concepts = useMemo(() => {
    if (!doc) return [];
    const out = [];
    (doc.concepts || []).forEach((cat) => (cat.items || []).forEach((it) => out.push({ ...it, category: cat.category })));
    return out;
  }, [doc]);
  const cur = concepts[sel];
  const learnSubject = (doc && (doc.subject || doc.id)) || subject;
  const activeKey = cur ? `${mode}::${cur.label}` : "";
  const active = frames[activeKey];
  // Generate a journey the FIRST time a concept × mode pair is opened; on later visits the
  // existing frame is just shown again (no new POST, no new iframe).
  useEffect(() => {
    if (!cur || !activeKey || frames[activeKey]) return undefined;
    let alive = true;
    const label = cur.label;
    setFrames((f) => ({ ...f, [activeKey]: { url: "", status: "loading", label } }));
    conceptLearnUrl(learnSubject, label, mode)
      .then((url) => { if (alive) setFrames((f) => ({ ...f, [activeKey]: { url, status: "ready", label } })); })
      .catch(() => { if (alive) setFrames((f) => ({ ...f, [activeKey]: { url: "", status: "error", label } })); });
    return () => { alive = false; };
  }, [activeKey]); // eslint-disable-line
  return (
    <div className="learn-overlay" onClick={onClose} role="dialog" aria-modal="true">
      <div className="learn-shell" onClick={(e) => e.stopPropagation()}>
        <div className="learn-head">
          <span className="learn-title">🎓 <T>Learn the concepts</T></span>
          <button className="learn-close" onClick={onClose} aria-label={tt("Close")} title={tt("Close")}>✕</button>
        </div>
        <div className="learn-split">
          <div className="learn-list">
            <div className="learn-list__cap"><T>Pick a concept to learn</T></div>
            {err && <div className="t-crimson" style={{ fontSize: 12, padding: "4px 6px" }}>{err}</div>}
            {!doc && !err && <div className="muted-note" style={{ padding: "4px 6px" }}><T>Loading…</T></div>}
            {concepts.map((it, i) => (
              <button key={i} onClick={() => setSel(i)} title={it.category || ""} className={`learn-item${i === sel ? " is-active" : ""}`}>
                <span className="learn-item__fit" style={{ background: FIT_COLORS[it.fit] || C.faint }}>{it.fit}</span>
                <span className="learn-item__label">{it.label}</span>
              </button>
            ))}
            {doc && !concepts.length && <div className="muted-note" style={{ padding: "4px 6px" }}><T>No concepts listed for this bench yet.</T></div>}
          </div>
          <div className="learn-pane">
            <div className="learn-modebar">
              <div className="learn-modes">
                {LEARN_MODES.map((m) => (
                  <button key={m.id} onClick={() => setMode(m.id)} title={tt(m.title)} className={`learn-mode${mode === m.id ? " is-active" : ""}`}>{m.label}</button>
                ))}
              </div>
              {cur && <span className="learn-modebar__concept">{cur.label}</span>}
              {active && active.url && <a href={active.url} target="_blank" rel="noopener noreferrer" className="btn btn-outline btn-tiny" title={tt("Open this learning journey in a new browser tab")}>↗ <T>Open in new tab</T></a>}
            </div>
            <div className="learn-body learn-body--split">
              {!cur && <div className="learn-loading"><T>Pick a concept on the left to begin.</T></div>}
              {cur && active && active.status === "loading" && !active.url && <div className="learn-loading"><T>Building your learning journey…</T></div>}
              {cur && active && active.status === "error" && <div className="learn-loading t-crimson"><T>Couldn’t start this learning journey. Try another concept or mode.</T></div>}
              {/* Every visited journey stays mounted; only the active one is shown (display) so
                  switching back is instant and keeps its progress. */}
              {Object.entries(frames).map(([k, fr]) => fr.url ? (
                <iframe key={k} src={fr.url} title={fr.label || k} className="learn-frame"
                  style={{ display: k === activeKey ? "block" : "none" }}
                  onLoad={() => setFrames((f) => (f[k] ? { ...f, [k]: { ...f[k], status: "ready" } } : f))} />
              ) : null)}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}
// Exposed globally so the bench workbench (benchkit.js) can open the "Learn" modal.
window.BenchLearnModal = BenchLearnModal;

// ── "Deep Learning" modal: three LLM-generated activities that deepen understanding of the
// bench's core/related/stretch concepts — Q2L (ask deep questions), When Things Break Down
// (diagnose a failure), and Wonderment (everyday curiosity → scholarly inquiry). Each is a
// streamlined multi-step flow driven by /api/tutors/:id/{q2l|wtb|wonderment} (see server/activities).
const DL = {
  ink: "#23303f", soft: "#52617a", line: "rgba(120,110,70,.28)", card: "rgba(255,255,255,.6)",
  teal: "#1f7a68", blue: "#2f5d8a", vermil: "#c2451f", gold: "#9a6a14", mono: "'IBM Plex Mono', monospace",
};
function dlBtn(bg, fg, on = true) { return { cursor: on ? "pointer" : "default", fontFamily: DL.mono, fontSize: 12.5, fontWeight: 600, border: "none", borderRadius: 8, padding: "8px 14px", background: bg, color: fg, opacity: on ? 1 : 0.5 }; }
function dlGhost(color) { return { cursor: "pointer", fontFamily: DL.mono, fontSize: 12, border: `1px solid ${color}`, background: "transparent", color, borderRadius: 8, padding: "7px 12px" }; }
const dlTextarea = { inlineSize: "100%", minBlockSize: 78, resize: "vertical", borderRadius: 9, border: `1px solid ${DL.line}`, padding: "10px 12px", fontFamily: "var(--serif, Spectral), Georgia, serif", fontSize: 14, color: DL.ink, background: "rgba(255,255,255,.7)", boxSizing: "border-box" };
function DLLoading({ label }) { return <div style={{ padding: "26px 4px", textAlign: "center", color: DL.soft, fontFamily: DL.mono, fontSize: 12.5 }}>✦ <T>{label || "Generating…"}</T></div>; }
function DLErr({ msg }) { return <div style={{ color: DL.vermil, fontFamily: DL.mono, fontSize: 12.5, padding: "8px 0" }}>⚠ {msg}</div>; }

// Deep-Learning activity work persists across tab switches AND modal close/reopen, keyed by
// tutor+activity, so re-entering simply shows prior work (the scenario, your questions,
// diagnoses and discussion threads) — D2L-style — instead of resetting to a fresh LLM
// scenario every time. In-memory for the SPA session; transient flags (busy/err) stay local.
const DL_STORE = {};
function useDLState(key, initial) {
  if (!DL_STORE[key]) DL_STORE[key] = { ...initial };
  const [, bump] = useState(0);
  const set = useCallback((patch) => {
    const s = DL_STORE[key];
    Object.assign(s, typeof patch === "function" ? patch(s) : patch);
    bump((n) => n + 1);
  }, [key]);
  return [DL_STORE[key], set];
}

// A small Socratic chat thread shared by Q2L discussion + WTB investigation/discussion.
function DLChat({ thread, onSend, placeholder, accent, busy }) {
  const [text, setText] = useState("");
  const send = () => { const t = text.trim(); if (!t || busy) return; setText(""); onSend(t); };
  return (
    <div>
      <div style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 10 }}>
        {thread.map((m, i) => (
          <div key={i} style={{ alignSelf: m.role === "me" ? "flex-end" : "flex-start", maxInlineSize: "85%", background: m.role === "me" ? "rgba(47,93,138,.12)" : DL.card, border: `1px solid ${DL.line}`, borderRadius: 10, padding: "8px 11px", fontSize: 13.5, color: DL.ink, lineHeight: 1.5 }}>{m.text}</div>
        ))}
        {busy && <div style={{ alignSelf: "flex-start", color: DL.soft, fontFamily: DL.mono, fontSize: 12 }}>✦ <T>thinking…</T></div>}
      </div>
      <div style={{ display: "flex", gap: 8 }}>
        <input value={text} onChange={(e) => setText(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") send(); }} placeholder={tt(placeholder || "Type a message…")} style={{ flex: 1, borderRadius: 8, border: `1px solid ${DL.line}`, padding: "8px 11px", fontSize: 13.5, color: DL.ink, background: "rgba(255,255,255,.7)" }} />
        <button onClick={send} disabled={busy} style={dlBtn(accent, "#fff", !busy)}><T>Send</T></button>
      </div>
    </div>
  );
}

function Q2LPanel({ tutorId, subject, concept }) {
  const call = (step, payload) => api(`/tutors/${tutorId}/q2l`, { method: "POST", body: { step, ...payload } });
  const [s, set] = useDLState(`${tutorId}:q2l`, { sc: null, sel: {}, q: "", showEx: false, res: null, thread: [] });
  const [err, setErr] = useState(null), [busy, setBusy] = useState(false);
  useEffect(() => { if (s.sc) return; let a = true; call("scenario", {}).then((r) => { if (a) set({ sc: r }); }).catch((e) => a && setErr(e.message)); return () => { a = false; }; }, [tutorId]);
  if (err) return <DLErr msg={err} />;
  if (!s.sc) return <DLLoading label="Setting the scene…" />;
  const sc = s.sc;
  const submit = async () => {
    if (!s.q.trim() || busy) return; setBusy(true);
    try { const r = await call("evaluate", { question: s.q.trim(), selected: Object.keys(s.sel).filter((k) => s.sel[k]) }); set({ res: r, ...(r.isDeepQuestion ? { thread: [{ role: "ai", text: r.response }] } : {}) }); }
    catch (e) { setErr(e.message); } finally { setBusy(false); }
  };
  const discuss = async (t) => { const history = s.thread.map((m) => ({ role: m.role === "me" ? "learner" : "ai", text: m.text })); set((st) => ({ thread: [...st.thread, { role: "me", text: t }] })); setBusy(true); try { const r = await call("discuss", { message: t, history }); set((st) => ({ thread: [...st.thread, { role: "ai", text: r.reply }] })); } catch (e) { setErr(e.message); } finally { setBusy(false); } };
  const deep = s.res && s.res.isDeepQuestion;
  return (
    <div>
      <div style={{ fontSize: 14, color: DL.ink, lineHeight: 1.6, background: DL.card, border: `1px solid ${DL.line}`, borderRadius: 10, padding: "12px 14px" }}>{sc.scenario}</div>
      {(sc.contextItems || []).length > 0 && (<>
        <div style={{ fontFamily: DL.mono, fontSize: 11, textTransform: "uppercase", letterSpacing: ".06em", color: DL.soft, margin: "14px 0 7px" }}><T>Focus your question on…</T></div>
        <div style={{ display: "flex", flexWrap: "wrap", gap: 7 }}>
          {sc.contextItems.map((c) => { const on = !!s.sel[c.label]; return <button key={c.id} title={c.why} onClick={() => set((st) => ({ sel: { ...st.sel, [c.label]: !st.sel[c.label] } }))} style={{ cursor: "pointer", fontFamily: DL.mono, fontSize: 12, borderRadius: 999, padding: "5px 12px", border: `1px solid ${on ? DL.teal : DL.line}`, background: on ? "rgba(31,122,104,.14)" : "transparent", color: on ? DL.teal : DL.soft }}>{c.label}</button>; })}
        </div>
      </>)}
      {!deep && (<>
        <textarea value={s.q} onChange={(e) => set({ q: e.target.value })} placeholder={tt("Ask your own deep question about the scenario…")} style={{ ...dlTextarea, marginTop: 14 }} />
        <div style={{ display: "flex", gap: 8, alignItems: "center", marginTop: 8, flexWrap: "wrap" }}>
          <button onClick={submit} disabled={busy || !s.q.trim()} style={dlBtn(DL.teal, "#fff", !busy && !!s.q.trim())}>{busy ? <T>Evaluating…</T> : <T>Ask this question</T>}</button>
          {(sc.examples || []).length > 0 && <button onClick={() => set((st) => ({ showEx: !st.showEx }))} style={dlGhost(DL.blue)}>{s.showEx ? <T>Hide examples</T> : <T>Show example deep questions</T>}</button>}
        </div>
        {s.showEx && <div style={{ marginTop: 10, display: "flex", flexDirection: "column", gap: 8 }}>{sc.examples.map((e, i) => <div key={i} style={{ background: DL.card, border: `1px solid ${DL.line}`, borderRadius: 9, padding: "9px 12px" }}><div style={{ fontSize: 13.5, color: DL.ink, cursor: "pointer" }} onClick={() => set({ q: e.question })}>“{e.question}”</div><div style={{ fontSize: 11.5, color: DL.soft, marginTop: 4 }}>↳ {e.hint}</div></div>)}</div>}
        {s.res && !deep && <div style={{ marginTop: 12, background: "rgba(194,69,31,.08)", border: `1px solid rgba(194,69,31,.3)`, borderRadius: 9, padding: "10px 13px", fontSize: 13.5, color: DL.ink, lineHeight: 1.55 }}><b style={{ color: DL.vermil }}><T>Go deeper</T>:</b> {s.res.response}</div>}
      </>)}
      {deep && (<>
        <div style={{ marginTop: 12, marginBottom: 10, fontFamily: DL.mono, fontSize: 11.5, color: DL.teal }}>✓ <T>Deep question — discussion unlocked</T></div>
        <DLChat thread={s.thread} onSend={discuss} placeholder="Follow up…" accent={DL.teal} busy={busy} />
        <button onClick={() => set({ res: null, thread: [], q: "" })} style={{ ...dlGhost(DL.soft), marginTop: 10 }}>↺ <T>Ask a new question</T></button>
      </>)}
    </div>
  );
}

function WTBPanel({ tutorId, subject, concept }) {
  const call = (step, payload) => api(`/tutors/${tutorId}/wtb`, { method: "POST", body: { step, ...payload } });
  const [s, set] = useDLState(`${tutorId}:wtb`, { sc: null, mode: "investigate", thread: [], diag: "", verdict: null, disc: [] });
  const [err, setErr] = useState(null), [busy, setBusy] = useState(false);
  useEffect(() => { if (s.sc) return; let a = true; call("scenario", {}).then((r) => { if (a) set({ sc: r }); }).catch((e) => a && setErr(e.message)); return () => { a = false; }; }, [tutorId]);
  if (err) return <DLErr msg={err} />;
  if (!s.sc) return <DLLoading label="Staging the breakdown…" />;
  const sc = s.sc, mode = s.mode, verdict = s.verdict;
  const ask = async (t) => { set((st) => ({ thread: [...st.thread, { role: "me", text: t }] })); setBusy(true); try { const r = await call("investigate", { question: t }); set((st) => ({ thread: [...st.thread, { role: "ai", text: r.answer }] })); } catch (e) { setErr(e.message); } finally { setBusy(false); } };
  const submit = async () => { if (!s.diag.trim() || busy) return; setBusy(true); try { const r = await call("evaluate", { diagnosis: s.diag.trim() }); set({ verdict: r, disc: [{ role: "ai", text: r.feedback }] }); } catch (e) { setErr(e.message); } finally { setBusy(false); } };
  const discuss = async (t) => { set((st) => ({ disc: [...st.disc, { role: "me", text: t }] })); setBusy(true); try { const r = await call("discuss", { message: t }); set((st) => ({ disc: [...st.disc, { role: "ai", text: r.reply }] })); } catch (e) { setErr(e.message); } finally { setBusy(false); } };
  const accColor = { correct: DL.teal, partial: DL.gold, incorrect: DL.vermil };
  return (
    <div>
      <div style={{ fontFamily: "var(--display, Fraunces), Georgia, serif", fontWeight: 600, fontSize: 18, color: DL.ink }}>{sc.title}</div>
      <div style={{ fontSize: 13.5, color: DL.soft, lineHeight: 1.55, margin: "6px 0 10px" }}>{sc.context}</div>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8, marginBottom: 10 }}>
        <div style={{ background: "rgba(194,69,31,.07)", border: `1px solid rgba(194,69,31,.25)`, borderRadius: 9, padding: "9px 11px" }}><div style={{ fontFamily: DL.mono, fontSize: 10.5, textTransform: "uppercase", color: DL.vermil, letterSpacing: ".06em" }}><T>What's wrong</T></div><div style={{ fontSize: 13, color: DL.ink, marginTop: 3, lineHeight: 1.5 }}>{sc.situation}</div></div>
        <div style={{ background: DL.card, border: `1px solid ${DL.line}`, borderRadius: 9, padding: "9px 11px" }}><div style={{ fontFamily: DL.mono, fontSize: 10.5, textTransform: "uppercase", color: DL.teal, letterSpacing: ".06em" }}><T>Expected</T></div><div style={{ fontSize: 13, color: DL.ink, marginTop: 3, lineHeight: 1.5 }}>{sc.expected}</div></div>
      </div>
      <div style={{ fontFamily: DL.mono, fontSize: 11, textTransform: "uppercase", letterSpacing: ".06em", color: DL.soft, margin: "4px 0 6px" }}><T>Clues</T></div>
      <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>{(sc.clues || []).map((c, i) => <div key={i} style={{ display: "flex", gap: 8, alignItems: "baseline", fontSize: 13, color: DL.ink }}><span style={{ fontFamily: DL.mono, fontSize: 10, color: DL.blue, textTransform: "uppercase", flex: "0 0 auto", inlineSize: 78 }}>{c.type}</span><span style={{ lineHeight: 1.45 }}>{c.text}</span></div>)}</div>

      {!verdict ? (<>
        <div style={{ display: "flex", gap: 8, margin: "16px 0 10px" }}>
          <button onClick={() => set({ mode: "investigate" })} style={mode === "investigate" ? dlBtn(DL.blue, "#fff") : dlGhost(DL.blue)}>🔎 <T>Investigate</T></button>
          <button onClick={() => set({ mode: "diagnose" })} style={mode === "diagnose" ? dlBtn(DL.vermil, "#fff") : dlGhost(DL.vermil)}>⚖ <T>Diagnose</T></button>
        </div>
        {mode === "investigate" ? (
          <DLChat thread={s.thread} onSend={ask} placeholder="Ask the expert witness…" accent={DL.blue} busy={busy} />
        ) : (<>
          <textarea value={s.diag} onChange={(e) => set({ diag: e.target.value })} placeholder={tt("What is the root cause? Explain the mechanism, not just the symptom…")} style={dlTextarea} />
          <button onClick={submit} disabled={busy || !s.diag.trim()} style={{ ...dlBtn(DL.vermil, "#fff", !busy && !!s.diag.trim()), marginTop: 8 }}>{busy ? <T>Evaluating…</T> : <T>Submit diagnosis</T>}</button>
        </>)}
      </>) : (<>
        <div style={{ display: "flex", gap: 10, alignItems: "center", margin: "16px 0 8px", flexWrap: "wrap" }}>
          <span style={{ fontFamily: DL.mono, fontSize: 12, fontWeight: 700, color: accColor[verdict.causeAccuracy] || DL.soft, textTransform: "uppercase" }}>{verdict.causeAccuracy}</span>
          <span style={{ fontFamily: DL.mono, fontSize: 11.5, color: DL.soft }}>· {verdict.reasoningDepth === "deep" ? tt("deep reasoning") : tt("shallow — describe the why")}</span>
        </div>
        <div style={{ fontSize: 13.5, color: DL.ink, lineHeight: 1.55, marginBottom: 10 }}>{verdict.feedback}</div>
        {verdict.hiddenCause && <div style={{ background: DL.card, border: `1px solid ${DL.line}`, borderRadius: 9, padding: "10px 13px", marginBottom: 12 }}><div style={{ fontFamily: DL.mono, fontSize: 10.5, textTransform: "uppercase", color: DL.teal, letterSpacing: ".06em", marginBottom: 4 }}><T>The actual root cause</T></div><div style={{ fontSize: 13.5, color: DL.ink, lineHeight: 1.55 }}>{verdict.hiddenCause}</div></div>}
        <DLChat thread={s.disc} onSend={discuss} placeholder="Discuss the cause…" accent={DL.teal} busy={busy} />
      </>)}
    </div>
  );
}

function WondermentPanel({ tutorId, subject, concept }) {
  const call = (step, payload) => api(`/tutors/${tutorId}/wonderment`, { method: "POST", body: { step, ...payload } });
  const [s, set] = useDLState(`${tutorId}:wonderment`, { phase: "observe", obs: null, custom: "", chosenObs: "", exp: null, genius: null, answer: "", critics: null, synth: null });
  const [err, setErr] = useState(null), [busy, setBusy] = useState(false);
  useEffect(() => { if (s.obs) return; let a = true; call("observe", {}).then((r) => { if (a) set({ obs: r.observations || [] }); }).catch((e) => a && setErr(e.message)); return () => { a = false; }; }, [tutorId]);
  if (err) return <DLErr msg={err} />;
  const { phase, obs, chosenObs, exp, genius, critics, synth } = s;
  const G_COLOR = { reframe: DL.blue, paradox: DL.vermil, scale: DL.teal, analogy: DL.gold, inversion: "#7a4fa0" };
  const chooseObs = async (text) => { if (!text.trim() || busy) return; set({ chosenObs: text.trim(), phase: "explore" }); setBusy(true); try { const r = await call("explore", { observation: text.trim() }); set({ exp: r }); } catch (e) { setErr(e.message); } finally { setBusy(false); } };
  const sendAnswer = async () => { if (!s.answer.trim() || busy) return; setBusy(true); set({ phase: "critics" }); try { const r = await call("critics", { question: genius.question, answer: s.answer.trim() }); set({ critics: r.turns || [] }); } catch (e) { setErr(e.message); } finally { setBusy(false); } };
  const reachSynthesis = async () => { setBusy(true); set({ phase: "synthesis" }); try { const r = await call("synthesis", { question: genius.question, answer: s.answer }); set({ synth: r }); } catch (e) { setErr(e.message); } finally { setBusy(false); } };

  if (phase === "observe") {
    if (!obs) return <DLLoading label="Noticing the everyday…" />;
    return (
      <div>
        <div style={{ fontSize: 13.5, color: DL.soft, marginBottom: 12, lineHeight: 1.55 }}><T>Pick something you genuinely wonder about — or write your own.</T></div>
        <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
          {obs.map((o) => <button key={o.id} onClick={() => chooseObs(o.text)} style={{ textAlign: "left", cursor: "pointer", background: DL.card, border: `1px solid ${DL.line}`, borderRadius: 10, padding: "11px 13px", fontSize: 14, color: DL.ink, lineHeight: 1.5, fontFamily: "var(--serif, Spectral), Georgia, serif" }}>{o.text}</button>)}
        </div>
        <div style={{ marginTop: 12 }}>
          <textarea value={s.custom} onChange={(e) => set({ custom: e.target.value })} placeholder={tt("I've always wondered why…")} style={dlTextarea} />
          <button onClick={() => chooseObs(s.custom)} disabled={!s.custom.trim()} style={{ ...dlBtn(DL.gold, "#fff", !!s.custom.trim()), marginTop: 8 }}><T>Wonder about this</T> →</button>
        </div>
      </div>
    );
  }
  if (phase === "explore") {
    if (!exp) return <DLLoading label="Transforming into inquiry…" />;
    return (
      <div>
        {/* Keep BOTH the original everyday observation and its academic reframe in view, so the
            learner sees the transformation — the whole point of the activity. */}
        {chosenObs && (<>
          <div style={{ fontFamily: DL.mono, fontSize: 10.5, textTransform: "uppercase", letterSpacing: ".06em", color: DL.soft, marginBottom: 5 }}>✦ <T>Your observation</T></div>
          <div style={{ fontSize: 14, color: DL.ink, lineHeight: 1.55, fontStyle: "italic", background: DL.card, border: `1px solid ${DL.line}`, borderRadius: 10, padding: "10px 13px" }}>“{chosenObs}”</div>
          <div style={{ textAlign: "center", color: DL.gold, fontFamily: DL.mono, fontSize: 15, margin: "8px 0 4px" }}>↓ <span style={{ fontSize: 11, color: DL.soft }}><T>reframed as scholarly inquiry</T></span></div>
        </>)}
        <div style={{ fontFamily: DL.mono, fontSize: 10.5, textTransform: "uppercase", letterSpacing: ".06em", color: DL.soft, marginBottom: 5 }}><T>The academic question</T></div>
        <div style={{ fontSize: 15, color: DL.ink, lineHeight: 1.55, fontWeight: 600 }}>{exp.academicQuestion}</div>
        {exp.transformNote && <div style={{ fontSize: 12.5, color: DL.soft, marginTop: 6, lineHeight: 1.5, fontStyle: "italic" }}>↳ {exp.transformNote}</div>}
        <div style={{ fontFamily: DL.mono, fontSize: 10.5, textTransform: "uppercase", letterSpacing: ".06em", color: DL.soft, margin: "16px 0 7px" }}><T>Questions a great thinker would ask — pick one</T></div>
        <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
          {(exp.geniusQuestions || []).map((g, i) => <button key={i} onClick={() => set({ genius: g, phase: "answer" })} style={{ textAlign: "left", cursor: "pointer", background: DL.card, border: `1px solid ${DL.line}`, borderRadius: 10, padding: "10px 12px" }}><span style={{ fontFamily: DL.mono, fontSize: 9.5, textTransform: "uppercase", color: G_COLOR[g.type] || DL.blue, border: `1px solid ${G_COLOR[g.type] || DL.blue}`, borderRadius: 999, padding: "1px 7px" }}>{g.type}</span><div style={{ fontSize: 14, color: DL.ink, marginTop: 6, lineHeight: 1.5 }}>{g.question}</div></button>)}
        </div>
      </div>
    );
  }
  if (phase === "answer") {
    return (
      <div>
        <div style={{ fontSize: 15, color: DL.ink, lineHeight: 1.55, fontWeight: 600, marginBottom: 4 }}>{genius.question}</div>
        {genius.hint && <div style={{ fontSize: 12.5, color: DL.soft, fontStyle: "italic", marginBottom: 10 }}>↳ {genius.hint}</div>}
        <textarea value={s.answer} onChange={(e) => set({ answer: e.target.value })} placeholder={tt("Your raw thinking — no format required…")} style={{ ...dlTextarea, minBlockSize: 120 }} />
        <button onClick={sendAnswer} disabled={busy || !s.answer.trim()} style={{ ...dlBtn(DL.gold, "#fff", !busy && !!s.answer.trim()), marginTop: 8 }}><T>Face the critics</T> →</button>
      </div>
    );
  }
  if (phase === "critics") {
    if (!critics) return <DLLoading label="Convening the critics…" />;
    const VOICE = { "The Empiricist": DL.blue, "The Connector": DL.teal, "The Provocateur": DL.vermil };
    return (
      <div>
        <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
          {critics.map((t, i) => <div key={i} style={{ borderLeft: `3px solid ${VOICE[t.critic] || DL.soft}`, paddingLeft: 11 }}><div style={{ fontFamily: DL.mono, fontSize: 11, fontWeight: 700, color: VOICE[t.critic] || DL.soft }}>{t.critic}</div><div style={{ fontSize: 13.5, color: DL.ink, lineHeight: 1.55, marginTop: 2 }}>{t.text}</div></div>)}
        </div>
        <button onClick={reachSynthesis} disabled={busy} style={{ ...dlBtn(DL.gold, "#fff", !busy), marginTop: 14 }}><T>See the synthesis</T> →</button>
      </div>
    );
  }
  // synthesis
  if (!synth) return <DLLoading label="Synthesising…" />;
  return (
    <div>
      <div style={{ fontSize: 14.5, color: DL.ink, lineHeight: 1.65 }}>{synth.synthesis}</div>
      <div style={{ marginTop: 16, background: "linear-gradient(180deg, rgba(154,106,20,.12), rgba(154,106,20,.04))", border: `1px solid ${DL.line}`, borderRadius: 11, padding: "14px 16px" }}>
        <div style={{ fontFamily: DL.mono, fontSize: 10.5, textTransform: "uppercase", letterSpacing: ".08em", color: DL.gold, marginBottom: 5 }}>✦ <T>The Wonderment Moment</T></div>
        <div style={{ fontSize: 16, color: DL.ink, lineHeight: 1.5, fontWeight: 600, fontFamily: "var(--display, Fraunces), Georgia, serif" }}>{synth.wondermentMoment}</div>
      </div>
    </div>
  );
}

const DL_TABS = [
  { id: "q2l", label: "Ask deep questions", short: "Q2L", icon: "❓", color: DL.teal, Panel: Q2LPanel, blurb: "Generate a question worth asking — depth unlocks discussion." },
  { id: "wtb", label: "When things break", short: "Diagnose", icon: "🧩", color: DL.vermil, Panel: WTBPanel, blurb: "Investigate a failure and diagnose the hidden root cause." },
  { id: "wonderment", label: "Wonderment", short: "Wonder", icon: "✦", color: DL.gold, Panel: WondermentPanel, blurb: "Turn an everyday hunch into scholarly insight." },
];
// Last-open Deep-Learning tab, per tutor — survives modal close/reopen for the SPA session.
const DL_TAB_STORE = {};
function BenchDeepLearnModal({ subject, concept, tutorId, onClose }) {
  useI18nVersion();
  // Remember the last-open tab per tutor, so re-entering lands where you left off (D2L-style).
  const [tab, setTabRaw] = useState(() => DL_TAB_STORE[tutorId] || 0);
  const setTab = (i) => { DL_TAB_STORE[tutorId] = i; setTabRaw(i); };
  // Activity work persists across tab switches and modal close/reopen (see DL_STORE); the only
  // reset is the explicit "Start over" — which clears this tab's store and forces a remount.
  const [resetN, setResetN] = useState(0);
  useEffect(() => { const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, []);
  useEffect(() => { const h = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [onClose]);
  const active = DL_TABS[tab];
  const Panel = active.Panel;
  const startOver = () => { delete DL_STORE[`${tutorId}:${active.id}`]; setResetN((n) => n + 1); };
  return (
    <div className="bench-doc-overlay" onClick={onClose}>
      <div className="bench-doc" style={{ inlineSize: "min(760px, 100%)" }} onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true">
        <div className="bench-doc__head" style={{ borderBlockEnd: `3px solid ${active.color}` }}>
          <div>
            <div className="bench-doc__title"><T>Deep Learning</T></div>
            <div className="bench-doc__field" style={{ color: DL.soft }}>{concept ? `${concept} · ` : ""}<T>LLM activities over this bench's concepts</T></div>
          </div>
          <button className="bench-doc__close" onClick={onClose} aria-label={tt("Close")} title={tt("Close")}>✕</button>
        </div>
        <div style={{ display: "flex", gap: 7, padding: "12px 22px 0", flexWrap: "wrap" }}>
          {DL_TABS.map((t, i) => (
            <button key={t.id} role="tab" aria-selected={i === tab} onClick={() => setTab(i)}
              style={{ cursor: "pointer", fontFamily: DL.mono, fontSize: 12.5, fontWeight: 600, border: `1px solid ${i === tab ? t.color : DL.line}`, background: i === tab ? t.color : "transparent", color: i === tab ? "#fff" : DL.soft, borderRadius: 999, padding: "7px 14px" }}>{t.icon} <T>{t.short}</T></button>
          ))}
        </div>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12, padding: "6px 22px 4px" }}>
          <div style={{ fontSize: 12.5, color: DL.soft, fontStyle: "italic" }}><T>{active.blurb}</T></div>
          <button onClick={startOver} title={tt("Clear your work on this activity and start fresh")} style={{ ...dlGhost(DL.soft), flex: "0 0 auto", padding: "5px 10px", fontSize: 11.5 }}>↺ <T>Start over</T></button>
        </div>
        <div className="bench-doc__body" style={{ paddingTop: 8 }}>
          <Panel key={`${active.id}-${resetN}`} tutorId={tutorId} subject={subject} concept={concept} />
        </div>
      </div>
    </div>
  );
}
// Exposed globally so the bench workbench (benchkit.js) can open the "Deep Learning" modal.
window.BenchDeepLearnModal = BenchDeepLearnModal;

// Full-bleed looping background video for the hero. React doesn't reliably set the boolean
// `muted` *property* from the JSX attribute, and browsers gate autoplay on `video.muted`
// being true at play() time — so we force muted via a ref and kick off playback ourselves.
// Decorative (aria-hidden); a dark scrim over it keeps the hero copy readable.
function HeroVideo({ src }) {
  const ref = React.useRef(null);
  useEffect(() => {
    const v = ref.current;
    if (!v) return;
    v.muted = true; v.defaultMuted = true;
    const tryPlay = () => { const p = v.play(); if (p && p.catch) p.catch(() => {}); };
    tryPlay();
    // Some browsers only allow autoplay once enough data is buffered / on first interaction.
    v.addEventListener("canplay", tryPlay, { once: true });
    document.addEventListener("pointerdown", tryPlay, { once: true });
    return () => document.removeEventListener("pointerdown", tryPlay);
  }, []);
  return (
    <div className="landing-hero__bg" aria-hidden="true">
      <video ref={ref} className="landing-hero__video" autoPlay loop muted playsInline preload="auto">
        <source src={src} type="video/mp4" />
      </video>
      <div className="landing-hero__scrim" />
    </div>
  );
}

function Landing({ config, authError, onLogin }) {
  useI18nVersion();
  const [signin, setSignin] = useState(!!authError); // OAuth errors return here → open sign-in
  if (signin) {
    return (
      <div>
        <div className="landing-back"><a onClick={() => setSignin(false)} className="link">← <T>Back to home</T></a></div>
        <Login config={config} authError={authError} onLogin={onLogin} />
      </div>
    );
  }
  const features = [
    { t: "Manipulate a real figure", d: "Drive an interactive simulation — a circuit, a lens, a scatterplot — not a multiple-choice quiz." },
    { t: "Graded as you go", d: "Every action is scored live against learning objectives. Interaction is the assessment." },
    { t: "Authored by AI, over fixed physics", d: "Educators type a concept; the tutor — objectives, tasks, misconception traps — is generated over a bench that stays honest and gradeable." },
  ];
  return (
    <div className="landing">
      <header className="landing-nav">
        <div className="landing-brand nr">STEM&nbsp;Bench <span className="pilot-badge"><T>PILOT</T></span></div>
        <div className="row" style={{ gap: 12, alignItems: "center" }}>
          <LanguagePicker />
          <button onClick={() => setSignin(true)} className="btn btn-primary"><T>Sign in</T></button>
        </div>
      </header>

      <section className="landing-hero">
        <HeroVideo src="/img/stembench_background_loop.mp4" />
        <div className="pilot-tag"><T>Pilot project · actively expanding</T></div>
        <h1 className="landing-title nr">STEM Bench</h1>
        <div className="landing-tagline"><T>Interactive Simulation &amp; Tutoring</T></div>
        <p className="landing-lede">
          <T>A learning platform built on “benches” — fixed, manipulable simulations where students demonstrate understanding by doing, and every interaction is graded in real time. No quizzes.</T>
        </p>
        <div className="row" style={{ gap: 12, justifyContent: "center", flexWrap: "wrap" }}>
          <button onClick={() => setSignin(true)} className="btn btn-primary btn-lg"><T>Sign in to start</T></button>
          <a href="#benches" className="btn btn-outline btn-lg"><T>Explore the benches</T></a>
        </div>
        <BenchHeroAnimations count={3} />
        <div className="bench-anim-caption"><T>A peek at the workbench — students drag a control, the bench responds, and every move is graded live.</T></div>
      </section>

      <section className="landing-section">
        <div className="feature-grid">
          {features.map((f) => (
            <div key={f.t} className="feature-card">
              <div className="feature-card__title"><T>{f.t}</T></div>
              <div className="feature-card__desc"><T>{f.d}</T></div>
            </div>
          ))}
        </div>
      </section>

      <section id="benches" className="landing-section">
        <H><T>Benches in this pilot</T></H>
        <div className="helptext"><T>Each bench is a self-contained interactive lab for a STEM domain. This is a growing pilot — more benches are on the way.</T></div>
        <BenchShowcase />
      </section>

      <footer className="landing-footer">
        <span><T>STEM Bench — a pilot in interaction-as-assessment learning.</T> · <T>STEMBENCH is created by Xiangen Hu</T> (<a href="mailto:xiangenhu@gmail.com" className="link">xiangenhu@gmail.com</a>)</span>
        <button onClick={() => setSignin(true)} className="link"><T>Sign in</T></button>
      </footer>
    </div>
  );
}

/* ===================== teacher ===================== */
const VIS_PILL = { private: { c: C.faint, label: "private" }, public: { c: C.teal, label: "public" }, code: { c: C.blue, label: "by code" } };
function TutorRow({ t, children }) {
  const vis = t.visibility || (t.status === "published" ? "public" : "private");
  const vm = VIS_PILL[vis] || VIS_PILL.private;
  return (
    <Card className="card-mb-sm">
      <div className="tutor-row">
        <div className="tutor-row__main">
          <div className="tutor-row__title">
            <span className="tutor-name nr">{t.title || t.concept}</span>
            <Pill color={vm.c}>{tt(vm.label)}</Pill>
            {(() => { const sgs = safeguardState(t); return <Pill color={sgs.c}>🛡 {tt(sgs.label)}</Pill>; })()}
            {t.access === "assigned" && <Pill color={C.amber}>{tt("assigned to you")}</Pill>}
            <Pill color={t.generator === "llm" ? C.blue : C.faint}>{t.generator === "llm" ? "LLM" : tt("template")}</Pill>
          </div>
          <div className="helptext" style={{ marginBottom: 0, marginTop: 4 }}>{t.summary}</div>
          <div className="muted-note" style={{ marginTop: 4 }}>{tf("concept: {concept} · {owner}", { concept: t.concept, owner: t.ownerName })}</div>
        </div>
        <div className="tutor-row__actions">{children}</div>
      </div>
    </Card>
  );
}

// Per-tutor "mini-class" controls: the access tier (Private / Public / Code) and, when code-gated,
// the access code with copy / rotate / revoke. Each tutor is a mini-class; the code is the join token.
const VIS_META = { private: { label: "Private", c: C.faint, desc: "Only you" }, public: { label: "Public", c: C.teal, desc: "Anyone" }, code: { label: "By code", c: C.blue, desc: "Students with the code" } };
function TutorAssignControls({ t, onChange, onNeedSafeguard }) {
  const [busy, setBusy] = useState(false);
  const [copied, setCopied] = useState(false);
  const [shareOpen, setShareOpen] = useState(false);
  const [analyzeOpen, setAnalyzeOpen] = useState(false);
  const vis = t.visibility || (t.status === "published" ? "public" : "private");
  const approved = !!(t.safeguard && t.safeguard.status === "approved"); // publishing requires approval
  const shareable = approved && vis !== "private"; // public or code-gated → there's a link/QR to hand out
  const run = async (fn) => { setBusy(true); try { await fn(); await onChange(); } catch (e) { window.alert(e.message); } finally { setBusy(false); } };
  const setVis = (v) => run(() => api(`/tutors/${t.id}/visibility`, { method: "POST", body: { visibility: v } }));
  const genCode = () => run(() => api(`/tutors/${t.id}/access-code`, { method: "POST", body: {} }));
  const revoke = () => run(() => api(`/tutors/${t.id}/access-code`, { method: "DELETE" }));
  const copy = () => { if (t.accessCode && navigator.clipboard) { navigator.clipboard.writeText(t.accessCode).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); }).catch(() => {}); } };
  // Sharing (public / by code) is gated on safeguard approval; clicking it un-approved opens the review.
  const pickVis = (v) => { if ((v === "public" || v === "code") && !approved) { if (onNeedSafeguard) onNeedSafeguard(); return; } if (v === "code") { if (!t.accessCode) genCode(); } else setVis(v); };
  return (
    <div className="assign-controls">
      <div className="seg" role="group" aria-label={tt("Visibility")}>
        {["private", "public", "code"].map((v) => {
          const locked = (v === "public" || v === "code") && !approved;
          return (
          <button key={v} type="button" disabled={busy} className={`seg__btn${vis === v ? " is-active" : ""}`} title={locked ? tt("Run the AI safeguard review and approve it to share with students") : tt(VIS_META[v].desc)}
            onClick={() => pickVis(v)} style={vis === v ? { background: VIS_META[v].c, color: C.bg } : undefined}>{locked ? "🛡 " : ""}<T>{VIS_META[v].label}</T></button>
          );
        })}
      </div>
      {!approved && <div className="muted-note" style={{ fontSize: 11, marginTop: 2 }}>🛡 <T>Safeguard & approve to share</T></div>}
      {vis === "code" && (
        <div className="row" style={{ gap: 6, alignItems: "center", flexWrap: "wrap", marginTop: 6 }}>
          <code className="access-code" title={tt("Share this code with your students")}>{t.accessCode || "—"}</code>
          <button type="button" className="btn btn-tiny btn-outline" disabled={busy || !t.accessCode} onClick={copy}>{copied ? tt("Copied ✓") : tt("Copy")}</button>
          <button type="button" className="btn btn-tiny btn-muted" disabled={busy} onClick={genCode} title={tt("Generate a new code — the old one stops working; students already joined keep access")}><T>Rotate</T></button>
          <button type="button" className="btn btn-tiny btn-danger" disabled={busy} onClick={revoke} title={tt("Revoke the code and make this private")}><T>Revoke</T></button>
        </div>
      )}
      {shareable && (
        <div className="row" style={{ gap: 6, marginTop: 6 }}>
          <button type="button" className="btn btn-tiny btn-primary" onClick={() => setShareOpen(true)} title={tt("Get a shareable link & QR code anyone can scan to open this bench")}>🔗 <T>Share link / QR</T></button>
        </div>
      )}
      <div className="row" style={{ gap: 6, marginTop: 6 }}>
        <button type="button" className="btn btn-tiny btn-outline" onClick={() => setAnalyzeOpen(true)} title={tt("Review the AI-authored ideal action path & misconception signals for each task — verify them for learning analytics")}>📊 <T>Task × Action analysis</T></button>
      </div>
      {shareOpen && <ShareModal tutor={t} onClose={() => setShareOpen(false)} />}
      {analyzeOpen && <TaskAnalysisModal tutor={t} onClose={() => setAnalyzeOpen(false)} />}
    </div>
  );
}

// Teacher verification of the Task × Action analysis: per task, the AI-authored ideal action path
// (each move → the understanding it shows → the objective) and the misconception signals (wrong
// moves and what they reveal). The teacher verifies each (human-in-the-loop). GET ensures the
// analyses exist (generating any missing); verify persists the sign-off. See docs/task-action-analysis.md.
function TaskAnalysisModal({ tutor, onClose }) {
  const [analyses, setAnalyses] = useState(null);
  const [err, setErr] = useState(null);
  const [busy, setBusy] = useState({});
  useEffect(() => {
    let alive = true;
    api(`/tutors/${tutor.id}/analyses`).then((r) => { if (alive) setAnalyses(r.analyses || []); }).catch((e) => { if (alive) setErr(e.message); });
    return () => { alive = false; };
  }, [tutor.id]);
  useEffect(() => { const h = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [onClose]);

  const setOne = (taskId, patch) => setAnalyses((list) => (list || []).map((a) => (a.taskId === taskId ? { ...a, ...patch } : a)));
  const verify = async (a, verified) => {
    setBusy((b) => ({ ...b, [a.taskId]: true }));
    try { const r = await api(`/tutors/${tutor.id}/tasks/${a.taskId}/analysis/verify`, { method: "POST", body: { verified, note: a.teacherNote || "" } }); setOne(a.taskId, r.analysis); }
    catch (e) { setErr(e.message); }
    finally { setBusy((b) => ({ ...b, [a.taskId]: false })); }
  };
  const regen = async (a) => {
    setBusy((b) => ({ ...b, [a.taskId]: true }));
    try { const r = await api(`/tutors/${tutor.id}/tasks/${a.taskId}/analysis`, { method: "POST", body: { force: true } }); setOne(a.taskId, r.analysis); }
    catch (e) { setErr(e.message); }
    finally { setBusy((b) => ({ ...b, [a.taskId]: false })); }
  };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal bench-doc" style={{ maxWidth: 720, maxHeight: "88vh", overflowY: "auto" }} onClick={(e) => e.stopPropagation()}>
        <div className="bench-doc__head">
          <div className="bench-doc__title">📊 <T>Task × Action analysis</T></div>
          <button className="modal-x" onClick={onClose} aria-label={tt("Close")}>×</button>
        </div>
        <p className="bench-doc__lede" style={{ marginTop: 0 }}><T>For each task, the AI authored the ideal action path (the moves a learner should make and what each shows they understand) and the misconception signals (wrong moves and what they reveal). Verify each so it becomes trusted learning-analytics ground truth.</T></p>
        {err && <div className="muted-note" style={{ color: C.crimson }}>{err}</div>}
        {!analyses && !err && <div className="muted-note"><T>Authoring & loading the analyses…</T></div>}
        {analyses && analyses.length === 0 && <div className="muted-note"><T>This tutor has no tasks to analyze.</T></div>}
        {analyses && analyses.map((a) => (
          <div key={a.taskId} style={{ border: `1px solid ${a.verified ? C.teal : C.line}`, borderRadius: 10, padding: "12px 14px", marginBottom: 12, background: "rgba(255,255,255,.45)" }}>
            <div className="row" style={{ justifyContent: "space-between", alignItems: "baseline", gap: 8, flexWrap: "wrap" }}>
              <div style={{ fontWeight: 650 }}>{a.taskId} · {a.taskTitle}</div>
              <div className="row" style={{ gap: 6 }}>
                <span style={{ fontFamily: "monospace", fontSize: 10.5, color: "#8a7a55" }}>{a.generator === "llm" ? `AI · ${a.model || "gateway"}` : "deterministic"}</span>
                {a.verified ? <span style={{ color: C.teal, fontSize: 12, fontWeight: 600 }}>✓ <T>verified</T></span> : <span style={{ color: "#b07a1e", fontSize: 12 }}><T>unverified</T></span>}
              </div>
            </div>
            <div style={{ fontSize: 12, color: "#5d6a75", margin: "2px 0 8px" }}><BenchFormulaInline text={a.goal} /></div>
            <div style={{ fontFamily: "monospace", fontSize: 10.5, color: "#8a7a55", textTransform: "uppercase", letterSpacing: ".06em", marginBottom: 4 }}><T>Ideal action path</T></div>
            <ol style={{ margin: "0 0 10px", paddingLeft: 18, fontSize: 12.5, lineHeight: 1.5 }}>
              {(a.idealPath || []).map((s) => (
                <li key={s.n} style={{ marginBottom: 3 }}>
                  <b>{s.action}</b> <code style={{ background: "rgba(0,0,0,.05)", borderRadius: 4, padding: "0 4px" }}>{s.paramLabel || s.param}</code>
                  {s.drivesField ? <span style={{ color: "#5d6a75" }}> → {s.drivesField}</span> : null}
                  {s.understanding ? <span style={{ color: "#3a4550" }}> — {s.understanding}</span> : null}
                  {s.objective ? <span style={{ color: "#8a7a55", fontFamily: "monospace", fontSize: 10.5 }}> [{s.objective}]</span> : null}
                </li>
              ))}
            </ol>
            {(a.misconceptionSignals || []).length > 0 && <>
              <div style={{ fontFamily: "monospace", fontSize: 10.5, color: "#a85", textTransform: "uppercase", letterSpacing: ".06em", marginBottom: 4 }}><T>Misconception signals</T></div>
              <ul style={{ margin: "0 0 10px", paddingLeft: 18, fontSize: 12, lineHeight: 1.5 }}>
                {a.misconceptionSignals.map((m) => (
                  <li key={m.id} style={{ marginBottom: 3, color: C.crimson }}>
                    {m.param ? <><b>{m.direction}</b> <code style={{ background: "rgba(0,0,0,.05)", borderRadius: 4, padding: "0 4px" }}>{m.param}</code> </> : null}
                    <span style={{ color: "#7a3b30" }}>{m.indicates}</span>
                    {m.mapsTo ? <span style={{ color: "#8a7a55", fontFamily: "monospace", fontSize: 10.5 }}> [{m.mapsTo}]</span> : null}
                  </li>
                ))}
              </ul>
            </>}
            <textarea value={a.teacherNote || ""} onChange={(e) => setOne(a.taskId, { teacherNote: e.target.value })} placeholder={tt("Note for verification (optional)…")} className="field" rows={2} style={{ width: "100%", fontSize: 12, marginBottom: 8 }} />
            <div className="row" style={{ gap: 6 }}>
              {a.verified
                ? <button className="btn btn-tiny btn-muted" disabled={busy[a.taskId]} onClick={() => verify(a, false)}><T>Un-verify</T></button>
                : <button className="btn btn-tiny btn-primary" disabled={busy[a.taskId]} onClick={() => verify(a, true)}>✓ <T>Verify</T></button>}
              <button className="btn btn-tiny btn-outline" disabled={busy[a.taskId]} onClick={() => regen(a)} title={tt("Re-author this analysis with AI (clears verification)")}>↻ <T>Regenerate</T></button>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}
// Render a goal string that may contain inline math/chem the same way the bench does (KaTeX/mhchem).
function BenchFormulaInline({ text }) {
  const K = window.BenchKit;
  return K && K.RichText ? <K.RichText>{text}</K.RichText> : <>{text}</>;
}

// A unique, shareable deep link that opens this tutor directly (QR-friendly). Code-gated units carry
// their join token as ?c=CODE so a single scan both enrolls and opens; public units need no token.
function shareUrlFor(t) {
  const base = `${window.location.origin}/t/${t.id}`;
  const vis = t.visibility || (t.status === "published" ? "public" : "private");
  return vis === "code" && t.accessCode ? `${base}?c=${encodeURIComponent(t.accessCode)}` : base;
}
// Share modal: the shareable URL + a scannable QR code. The QR encoder (window.qrcode, loaded in
// index.html) is optional — if it didn't load we still show the copyable link, never an error.
function ShareModal({ tutor, onClose }) {
  useI18nVersion();
  const [copied, setCopied] = useState(false);
  const url = shareUrlFor(tutor);
  const vis = tutor.visibility || (tutor.status === "published" ? "public" : "private");
  // A GIF data-URL QR (window.qrcode from index.html). Data-URL → trivially shown in <img> and saved.
  const qrSrc = useMemo(() => {
    try {
      if (typeof window.qrcode !== "function") return null;
      const q = window.qrcode(0, "M"); q.addData(url); q.make();
      return q.createDataURL(6, 16);
    } catch (_) { return null; }
  }, [url]);
  const copy = () => { if (navigator.clipboard) navigator.clipboard.writeText(url).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); }).catch(() => {}); };
  const download = () => { if (!qrSrc) return; const a = document.createElement("a"); a.href = qrSrc; a.download = `bench-${tutor.id}-qr.gif`; a.click(); };
  return (
    <div className="modal-overlay" onClick={onClose}>
      <Card className="modal" style={{ inlineSize: "min(420px,100%)" }}>
        <div onClick={(e) => e.stopPropagation()}>
          <div className="between" style={{ alignItems: "center" }}>
            <H><T>Share</T> · {tutor.title || tutor.concept}</H>
            <button onClick={onClose} className="btn btn-muted"><T>Close</T></button>
          </div>
          <div className="helptext" style={{ marginTop: 4 }}>
            {vis === "code"
              ? <T>Anyone who scans this code or opens the link joins your bench and starts right away — no code to type.</T>
              : <T>Anyone signed in who scans this code or opens the link goes straight to this bench.</T>}
          </div>
          <div style={{ display: "flex", justifyContent: "center", margin: "10px 0" }}>
            {qrSrc
              ? <img src={qrSrc} alt={tt("QR code for this bench")} width={216} height={216} style={{ imageRendering: "pixelated", background: "#fff", padding: 8, borderRadius: 10, border: "1px solid var(--line, rgba(0,0,0,.08))" }} />
              : <div className="muted-note" style={{ fontSize: 12, textAlign: "center", maxWidth: 240 }}><T>QR code unavailable offline — share the link below instead.</T></div>}
          </div>
          <div className="row" style={{ gap: 6, alignItems: "center", flexWrap: "wrap" }}>
            <input readOnly value={url} onFocus={(e) => e.target.select()} className="field" style={{ flex: 1, minWidth: 180, marginBottom: 0, fontFamily: "monospace", fontSize: 12 }} />
            <button type="button" className="btn btn-primary btn-tiny" onClick={copy}>{copied ? tt("Copied ✓") : tt("Copy link")}</button>
            {qrSrc && <button type="button" className="btn btn-outline btn-tiny" onClick={download}><T>Download QR</T></button>}
          </div>
        </div>
      </Card>
    </div>
  );
}

// The mini-class roster: enrolled students (joined by code) merged with anyone who has worked the
// unit (covers public units where students open without enrolling). Shared by teacher + admin.
async function loadRosterRows(tid) {
  const [enr, prog] = await Promise.all([
    api(`/tutors/${tid}/enrollments`).catch(() => ({ enrollments: [] })),
    api(`/tutors/${tid}/progress`).catch(() => ({ roster: [] })),
  ]);
  const map = {};
  for (const e of enr.enrollments || []) map[e.userId] = { userId: e.userId, userName: e.userName, joined: true, mastery: e.mastery, completed: e.completed };
  for (const r of prog.roster || []) { const m = map[r.userId] || (map[r.userId] = { userId: r.userId, userName: r.userName }); if (m.mastery == null) m.mastery = r.overall; if (m.completed == null) m.completed = (r.completedTasks || []).length; m.events = r.events; }
  return Object.values(map);
}
function RosterModal({ tutor, rows, onClose }) {
  return (
    <div className="modal-overlay" onClick={onClose}>
      <Card className="modal">
        <div onClick={(e) => e.stopPropagation()}>
          <div className="between" style={{ alignItems: "center" }}>
            <H><T>Roster</T> · {tutor.title || tutor.concept}</H>
            <button onClick={onClose} className="btn btn-muted"><T>Close</T></button>
          </div>
          {rows.length === 0 && <div className="muted-note" style={{ fontSize: 13 }}><T>No students yet — share the access code, or publish it.</T></div>}
          {rows.map((r) => (
            <div key={r.userId} className="roster-row">
              <div><div style={{ fontSize: 13 }}>{r.userName} {r.joined && <Pill color={C.blue}>{tt("joined")}</Pill>}</div><div className="muted-note">{tf("{n} tasks done · {m} events", { n: r.completed || 0, m: r.events || 0 })}</div></div>
              <Gauge label="MASTERY" value={r.mastery || 0} suffix="%" color={barColor((r.mastery || 0) / 100)} />
            </div>
          ))}
        </div>
      </Card>
    </div>
  );
}

// ── AI safeguard review (gate before publishing an AI-authored tutor) ────────────
const RATING_META = { pass: { c: C.teal, label: "pass" }, minor: { c: C.gold, label: "minor" }, major: { c: C.crimson, label: "major" } };
const VERDICT_META = { pass: { c: C.teal, label: "Passed review" }, concerns: { c: C.gold, label: "Concerns to weigh" }, fail: { c: C.crimson, label: "Should not publish as-is" } };
function safeguardState(t) {
  const sg = t && t.safeguard;
  if (!sg) return { key: "none", label: "not reviewed", c: C.faint };
  if (sg.status === "approved") return { key: "approved", label: "safeguard ✓", c: C.teal };
  return { key: "reviewed", label: "reviewed · approve to publish", c: C.gold };
}
function SafeguardModal({ tutorId, onClose, onChange }) {
  useI18nVersion();
  const [t, setT] = useState(null);
  const [busy, setBusy] = useState(null); // 'run' | 'approve' | 'withdraw'
  const [err, setErr] = useState(null);
  const [readme, setReadme] = useState("");
  const [rbusy, setRbusy] = useState(null); // 'gen' | 'save'
  const [composeOpen, setComposeOpen] = useState(false); // compose the note in a focused, larger modal
  const readmeInit = useRef(false);
  const load = useCallback(async () => { try { const { tutor } = await api(`/tutors/${tutorId}`); setT(tutor); if (!readmeInit.current) { setReadme((tutor.readme && tutor.readme.text) || ""); readmeInit.current = true; } } catch (e) { setErr(e.message); } }, [tutorId]);
  useEffect(() => { load(); }, [load]);
  const genReadme = async () => { setRbusy("gen"); setErr(null); try { const r = await api(`/tutors/${tutorId}/readme/generate`, { method: "POST", body: {} }); setReadme(r.draft); } catch (e) { setErr(e.message); } finally { setRbusy(null); } };
  const saveReadme = async () => { setRbusy("save"); setErr(null); try { await api(`/tutors/${tutorId}/readme`, { method: "PUT", body: { text: readme } }); await load(); if (onChange) await onChange(); } catch (e) { setErr(e.message); } finally { setRbusy(null); } };
  useEffect(() => { const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, []);
  useEffect(() => { const h = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [onClose]);
  const run = (kind, fn) => async () => { setBusy(kind); setErr(null); try { await fn(); await load(); if (onChange) await onChange(); } catch (e) { setErr(e.message); } finally { setBusy(null); } };
  const doRun = run("run", () => api(`/tutors/${tutorId}/safeguard`, { method: "POST", body: {} }));
  const doApprove = run("approve", () => api(`/tutors/${tutorId}/safeguard/approve`, { method: "POST", body: {} }));
  const doWithdraw = run("withdraw", () => api(`/tutors/${tutorId}/safeguard/revoke`, { method: "POST", body: {} }));
  const sg = t && t.safeguard;
  const vm = sg ? (VERDICT_META[sg.verdict] || VERDICT_META.concerns) : null;
  const approved = sg && sg.status === "approved";
  return (
    <div className="bench-doc-overlay" onClick={onClose}>
      <div className="bench-doc" style={{ inlineSize: "min(720px,100%)" }} onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true">
        <div className="bench-doc__head" style={{ borderBlockEnd: `3px solid ${sg ? vm.c : C.faint}` }}>
          <div>
            <div className="bench-doc__title">🛡 <T>AI Safeguard Review</T></div>
            <div className="bench-doc__field">{t ? (t.title || t.concept) : ""}</div>
          </div>
          <button className="bench-doc__close" onClick={onClose} aria-label={tt("Close")}>✕</button>
        </div>
        <div className="bench-doc__body">
          {err && <div className="t-crimson" style={{ marginBottom: 8 }}>{err}</div>}
          {!t && <div className="muted-note"><T>Loading…</T></div>}
          {t && !sg && (
            <div>
              <p className="bench-doc__lede"><T>This tutor was generated by AI. Run an independent safeguard review across critical dimensions — age appropriateness, factual accuracy, potential misconceptions, cultural sensitivity, and more — before it can reach students.</T></p>
              <button onClick={doRun} disabled={busy} className="btn btn-primary">{busy === "run" ? tt("Reviewing…") : tt("Run safeguard review")}</button>
            </div>
          )}
          {t && sg && (<>
            <div className="row" style={{ gap: 10, alignItems: "center", flexWrap: "wrap", marginBottom: 8 }}>
              <span className="seg__btn is-active" style={{ background: vm.c, color: "#fff", borderRadius: 999, padding: "4px 12px", fontWeight: 600 }}>{tt(vm.label)}</span>
              {approved ? <Pill color={C.teal}>{tf("approved by {who}", { who: sg.approvedByName || tt("you") })}</Pill> : <Pill color={C.gold}>{tt("awaiting your approval")}</Pill>}
              <span className="muted-note">{sg.generator === "llm" ? tt("AI review") : tt("offline heuristic")}</span>
            </div>
            {sg.summary && <p className="bench-doc__lede" style={{ marginTop: 0 }}>{sg.summary}</p>}
            <div style={{ display: "flex", flexDirection: "column", gap: 8, margin: "10px 0" }}>
              {(sg.dimensions || []).map((d, i) => {
                const rm = RATING_META[d.rating] || RATING_META.minor;
                return (
                  <div key={i} style={{ borderInlineStart: `3px solid ${rm.c}`, paddingInlineStart: 11, background: "rgba(255,255,255,.5)", borderRadius: 8, padding: "9px 12px" }}>
                    <div className="row" style={{ gap: 8, alignItems: "center" }}><b style={{ fontSize: 13.5 }}>{d.label}</b><Pill color={rm.c}>{tt(rm.label)}</Pill></div>
                    {d.findings && <div style={{ fontSize: 12.5, marginTop: 4, lineHeight: 1.5 }}>{d.findings}</div>}
                    {d.recommendation && <div style={{ fontSize: 12, marginTop: 3, color: "#7a6a3a", fontStyle: "italic" }}>↳ {d.recommendation}</div>}
                  </div>
                );
              })}
            </div>
            {(() => {
              const savedReadme = (t.readme && t.readme.text) || "";
              const hasReadme = !!savedReadme;
              const dirty = readme.trim() !== savedReadme.trim();
              const needsReadme = (sg.verdict === "concerns" || sg.verdict === "fail") && !hasReadme;
              return (<>
                {/* "Read me first" note — words from the teacher; required when the review flagged concerns. */}
                <div style={{ borderTop: `1px solid var(--parchEdge, rgba(120,110,70,.28))`, marginTop: 12, paddingTop: 11 }}>
                  <div className="row between" style={{ alignItems: "center" }}>
                    <b style={{ fontSize: 13.5 }}>📋 <T>Read me first — a note for your students</T></b>
                    {hasReadme && !dirty && <Pill color={C.teal}>{tt("saved")}</Pill>}
                  </div>
                  <div className="muted-note" style={{ fontSize: 12 }}>{needsReadme ? <T>Required: the review flagged concerns, so students must see your heads-up before they start. Students will read this first; they can still learn from the bench if they stay thoughtful.</T> : <T>Optional heads-up shown to students before they start — your words.</T>}</div>
                  <textarea value={readme} onChange={(e) => setReadme(e.target.value)} placeholder={tt("e.g. As you work, double-check anything about… — think critically and ask me if something looks off.")} className="field" style={{ width: "100%", minHeight: 84, marginTop: 6, marginBottom: 6 }} maxLength={2000} />
                  <div className="row" style={{ gap: 8, flexWrap: "wrap" }}>
                    <button onClick={() => setComposeOpen(true)} className="btn btn-outline btn-tiny" title={tt("Open a larger window to write the note")}>⤢ <T>Compose in a larger window</T></button>
                    <button onClick={genReadme} disabled={rbusy} className="btn btn-outline btn-tiny">{rbusy === "gen" ? tt("Drafting…") : tt("✨ Draft from the review")}</button>
                    <button onClick={saveReadme} disabled={rbusy || !dirty} className="btn btn-primary btn-tiny">{rbusy === "save" ? tt("Saving…") : tt("Save note")}</button>
                  </div>
                </div>
                {composeOpen && <NoteComposeModal title={t.title || t.concept} value={readme} onChange={setReadme} onSave={saveReadme} onGenerate={genReadme} saving={rbusy === "save"} generating={rbusy === "gen"} dirty={dirty} onClose={() => setComposeOpen(false)} />}
                <div className="row" style={{ gap: 8, flexWrap: "wrap", marginTop: 12 }}>
                  {!approved && <button onClick={doApprove} disabled={busy || needsReadme} title={needsReadme ? tt("Add and save the read-me-first note above to approve") : ""} className="btn btn-primary">{busy === "approve" ? tt("Approving…") : tt("Approve — allow publishing")}</button>}
                  {approved && <button onClick={doWithdraw} disabled={busy} className="btn btn-warn">{busy === "withdraw" ? tt("Withdrawing…") : tt("Withdraw approval")}</button>}
                  <button onClick={doRun} disabled={busy} className="btn btn-muted">{busy === "run" ? tt("Reviewing…") : tt("Re-run review")}</button>
                </div>
                {sg.verdict === "fail" && !approved && <div className="muted-note" style={{ marginTop: 8, color: C.crimson }}><T>The reviewer flagged a major issue — consider regenerating or editing before you approve.</T></div>}
              </>);
            })()}
          </>)}
        </div>
      </div>
    </div>
  );
}

// Teacher-facing: compose the "read me first" note in a large, focused modal so the message gets
// the teacher's full attention (the inline field is small on purpose). Edits the same `readme`
// state as the Safeguard panel; "Save note" persists and closes.
function NoteComposeModal({ title, value, onChange, onSave, onGenerate, saving, generating, dirty, onClose }) {
  useI18nVersion();
  useEffect(() => { const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, []);
  useEffect(() => { const h = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [onClose]);
  return (
    <div className="bench-doc-overlay" onClick={onClose}>
      <div className="bench-doc" style={{ inlineSize: "min(760px,100%)" }} onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true">
        <div className="bench-doc__head" style={{ borderBlockEnd: `3px solid ${C.gold}` }}>
          <div>
            <div className="bench-doc__title">📋 <T>A note for your students</T></div>
            <div className="bench-doc__field"><T>They’ll read this first, before they start</T>{title ? ` · ${title}` : ""}</div>
          </div>
          <button className="bench-doc__close" onClick={onClose} aria-label={tt("Close")}>✕</button>
        </div>
        <div className="bench-doc__body">
          <div className="muted-note" style={{ fontSize: 12, marginBottom: 8 }}><T>Write it as if you’re speaking to them — what to watch for, how to think, when to ask you.</T></div>
          <textarea value={value} onChange={(e) => onChange(e.target.value)} autoFocus placeholder={tt("e.g. As you work, double-check anything about… — think critically and ask me if something looks off.")} className="field" style={{ width: "100%", minHeight: 320, fontSize: 15, lineHeight: 1.6 }} maxLength={2000} />
          <div className="row between" style={{ alignItems: "center", marginTop: 8, flexWrap: "wrap", gap: 8 }}>
            <span className="muted-note" style={{ fontSize: 11 }}>{value.length}/2000</span>
            <div className="row" style={{ gap: 8, flexWrap: "wrap" }}>
              <button onClick={onGenerate} disabled={generating} className="btn btn-outline btn-tiny">{generating ? tt("Drafting…") : tt("✨ Draft from the review")}</button>
              <button onClick={async () => { await onSave(); onClose(); }} disabled={saving || !dirty} className="btn btn-primary">{saving ? tt("Saving…") : tt("Save note")}</button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// Student-facing "read me first": a note from the teacher shown before interacting with a bench
// that carries one (the result of the safeguard process).
//   • timed (first visit): the note pops up automatically and the student must spend READ_SECS
//     seconds on it before they can start — enough time to actually read it.
//   • review (later visits / on demand): the same note, available to re-read with no countdown.
const READ_SECS = 20;
function ReadmeGate({ tutor, timed = false, onStart, onClose }) {
  useI18nVersion();
  const [left, setLeft] = useState(timed ? READ_SECS : 0);
  useEffect(() => {
    if (!timed) return;
    const id = setInterval(() => setLeft((n) => { if (n <= 1) { clearInterval(id); return 0; } return n - 1; }), 1000);
    return () => clearInterval(id);
  }, [timed]);
  useEffect(() => { const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, []);
  // Escape closes a re-read; during the timed first view we don't let a stray keypress dismiss it.
  useEffect(() => { const h = (e) => { if (e.key === "Escape" && !timed) onClose(); }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [onClose, timed]);
  const r = tutor.readme || {};
  const waiting = timed && left > 0;
  return (
    <div className="bench-doc-overlay" onClick={timed ? undefined : onClose}>
      <div className="bench-doc" style={{ inlineSize: "min(560px,100%)" }} onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true">
        <div className="bench-doc__head" style={{ borderBlockEnd: `3px solid ${C.gold}` }}>
          <div>
            <div className="bench-doc__title">📋 <T>Read me first</T></div>
            <div className="bench-doc__field"><T>A note from your teacher</T> · {tutor.title || tutor.concept}</div>
          </div>
          {!timed && <button className="bench-doc__close" onClick={onClose} aria-label={tt("Close")}>✕</button>}
        </div>
        <div className="bench-doc__body">
          <div style={{ whiteSpace: "pre-wrap", fontSize: 14.5, lineHeight: 1.6, color: "#23303f", background: "rgba(201,150,42,.08)", border: `1px solid rgba(201,150,42,.3)`, borderRadius: 10, padding: "14px 16px" }}>{r.text}</div>
          {timed ? (
            <div className="row" style={{ gap: 8, marginTop: 14, justifyContent: "flex-end", alignItems: "center" }}>
              {waiting && <span className="muted-note" style={{ marginInlineEnd: "auto", fontSize: 12.5 }}><T>Take a moment to read this</T> · {tf("{n}s", { n: left })}</span>}
              <button onClick={onClose} className="btn btn-muted"><T>Back</T></button>
              <button onClick={onStart} disabled={waiting} className="btn btn-primary" title={waiting ? tt("Please read the note first") : ""}>{waiting ? tf("Start in {n}s", { n: left }) : tt("I’ve read this — start")}</button>
            </div>
          ) : (
            <div className="row" style={{ gap: 8, marginTop: 14, justifyContent: "flex-end" }}>
              <button onClick={onClose} className="btn btn-primary"><T>Close</T></button>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

/* ===================== concept catalog (authoring aid) ===================== */
const FIT_META = {
  core: { color: "#54f2b2", label: "core" },
  related: { color: "#6cb6ff", label: "related" },
  stretch: { color: "#ffb454", label: "stretch" },
};
function FitBadge({ fit }) {
  const m = FIT_META[fit] || FIT_META.related;
  return <span className="fit" title={fit} style={{ color: m.color }}>{m.label}</span>;
}
function ConceptChip({ c, onPick }) {
  return (
    <button onClick={() => onPick(c)} title={c.why || tf('Use "{label}" as the concept', { label: c.label })} className="chip">
      <FitBadge fit={c.fit} />
      <span>{c.label}</span>
    </button>
  );
}
function ConceptCatalog({ onPick, onClose, subject = "electronics" }) {
  const [cat, setCat] = useState(null);          // { categories, legend }
  const [open, setOpen] = useState(null);        // open category id
  const [extra, setExtra] = useState({});        // { [catId]: { concepts, source } }
  const [busy, setBusy] = useState(null);        // catId being suggested
  const [err, setErr] = useState(null);

  useEffect(() => { api(`/concepts?subject=${subject}`).then(setCat).catch((e) => setErr(e.message)); }, [subject]);
  async function suggest(category) {
    setBusy(category.id); setErr(null);
    try { const out = await api("/concepts/suggest", { method: "POST", body: { categoryId: category.id, topic: category.name, subject } }); setExtra((p) => ({ ...p, [category.id]: out })); }
    catch (e) { setErr(e.message); } finally { setBusy(null); }
  }

  return (
    <div onClick={(e) => e.stopPropagation()}>
      <div className="between" style={{ alignItems: "center" }}>
        <H><T>Browse the concept catalog</T></H>
        <button onClick={onClose} className="btn btn-muted"><T>Close</T></button>
      </div>
      <div className="helptext"><T>Not sure what to call it? Pick a category, choose a concept (it fills the box and closes this), or let AI suggest more. Tags show how well the fixed series-circuit lab can teach each one.</T></div>
      {!cat && !err && <div className="muted-note" style={{ fontSize: 13 }}><T>Loading…</T></div>}
      {err && <div className="err" style={{ marginBottom: 8 }}>{err}</div>}
      {cat && (<>
      <div className="legend">
        {Object.entries(cat.legend).map(([k, v]) => (
          <span key={k} className="legend__item"><FitBadge fit={k} /> {v}</span>
        ))}
      </div>
      {cat.categories.map((category) => {
        const isOpen = open === category.id;
        const ai = extra[category.id];
        return (
          <div key={category.id} className="cat">
            <button onClick={() => setOpen(isOpen ? null : category.id)} className="cat__head">
              <span><span className="cat__caret">{isOpen ? "▾" : "▸"}</span><span className="cat__name nr">{category.name}</span><span className="cat__count">· {category.concepts.length}</span></span>
            </button>
            {isOpen && (
              <div className="cat__body">
                <div className="cat__blurb">{category.blurb}</div>
                <div className="chip-grid">
                  {category.concepts.map((c) => <ConceptChip key={c.label} c={c} onPick={onPick} />)}
                </div>
                <div className="cat__ai">
                  <button onClick={() => suggest(category)} disabled={busy === category.id} className="btn btn-ai">
                    {busy === category.id ? tt("Asking AI…") : `✨ ${tt("AI suggest more")}`}
                  </button>
                  {ai && <span className="muted-note">{ai.source === "llm" ? tt("AI-suggested") : tt("from catalog")}</span>}
                </div>
                {ai && (
                  <div className="chip-grid" style={{ marginTop: 9 }}>
                    {ai.concepts.map((c, i) => <ConceptChip key={`ai-${i}-${c.label}`} c={c} onPick={onPick} />)}
                  </div>
                )}
              </div>
            )}
          </div>
        );
      })}
      </>)}
    </div>
  );
}

// Informative wait while a tutor is generated (spec via the gateway, then the SVG
// diagram). Steps mirror the real server pipeline; the last one holds until done.
// The diagram model name is read from the server (LLM_DIAGRAM_MODEL in .env), not hardcoded.
const GEN_STEPS = [
  "Designing objectives and a task progression…",
  "Choosing component values and misconception traps…",
  "Rating how well the fixed lab can teach it…",
  "Drawing the circuit schematic with {model}… (the slow step)",
  "Finalizing and saving the tutor…",
];
// Configured diagram model, fetched once from /api/health and cached across mounts.
let _diagModel = null, _diagModelP = null;
function diagramModelLabel() {
  if (_diagModel) return Promise.resolve(_diagModel);
  if (!_diagModelP) _diagModelP = api("/health").then((h) => (_diagModel = h.diagramModel || "a larger model")).catch(() => "a larger model");
  return _diagModelP;
}
function GenProgress({ concept, level }) {
  const [step, setStep] = useState(0);
  const [elapsed, setElapsed] = useState(0);
  const [model, setModel] = useState(_diagModel || "a larger model");
  useEffect(() => { diagramModelLabel().then(setModel); }, []);
  useEffect(() => {
    const t = setInterval(() => setElapsed((e) => e + 1), 1000);
    const s = setInterval(() => setStep((i) => Math.min(i + 1, GEN_STEPS.length - 1)), 2600);
    return () => { clearInterval(t); clearInterval(s); };
  }, []);
  const pct = Math.round(((step + 1) / GEN_STEPS.length) * 100);
  return (
    <div className="gen">
      <div className="gen__head">
        <span className="spinner" aria-hidden="true" />
        <span className="gen__title">{tf("Generating “{concept}” · {level}", { concept, level: tt(level) })}</span>
        <span className="gen__time">{tf("{s}s", { s: elapsed })}</span>
      </div>
      <div className="gen__bar"><div className="gen__bar-fill" style={{ width: `${pct}%` }} /></div>
      <div className="gen__steps">
        {GEN_STEPS.map((s, i) => (
          <div key={i} className={`gen__step${i < step ? " is-done" : i === step ? " is-active" : ""}`}>
            <span className="gen__dot">{i < step ? "✓" : i === step ? "•" : "○"}</span> {tf(s, { model })}
          </div>
        ))}
      </div>
      <div className="muted-note" style={{ marginBlockStart: 6 }}><T>This usually takes 5–15 seconds. The diagram is drawn by a larger model, so it’s the slowest part.</T></div>
    </div>
  );
}

// ── API access: mint/list/revoke keys so a third-party app can drive a tutor headlessly ──
// Keys are scoped to this educator's OWN safeguard-approved tutors and used against /api/v1 with
// `Authorization: Bearer <token>`. The full token is shown ONCE at mint time (we store only its hash).
// See docs/api-access.md.
function ApiKeysPanel({ approvedTutors = [] }) {
  useI18nVersion();
  const [keys, setKeys] = useState(null);
  const [name, setName] = useState("");
  const [fresh, setFresh] = useState(null); // the just-minted token (shown once)
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  const [copied, setCopied] = useState(false);
  const load = useCallback(async () => { try { const { keys } = await api("/keys"); setKeys(keys); } catch (e) { setErr(e.message); } }, []);
  useEffect(() => { load(); }, [load]);
  const mint = async () => {
    setBusy(true); setErr(null);
    try { const { token } = await api("/keys", { method: "POST", body: { name: name.trim() || "API key" } }); setFresh(token); setName(""); await load(); }
    catch (e) { setErr(e.message); } finally { setBusy(false); }
  };
  const revoke = async (id) => { if (!window.confirm(tt("Revoke this API key? Apps using it will stop working immediately."))) return; await api(`/keys/${id}`, { method: "DELETE" }); await load(); };
  const copy = (text) => { if (navigator.clipboard) navigator.clipboard.writeText(text).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); }).catch(() => {}); };
  const base = `${window.location.origin}/api/v1`;
  const sampleTid = (approvedTutors[0] && approvedTutors[0].id) || "TUTOR_ID";
  const curl = `curl ${base}/tutors/${sampleTid} \\\n  -H "Authorization: Bearer YOUR_TOKEN"`;
  return (
    <Card className="card-mb">
      <H><T>API access</T></H>
      <div className="helptext"><T>Give a third-party learning app direct access to your tutors. A key reaches only your safeguard-approved tutors; the app drives them over the headless /api/v1 endpoints. The full token is shown only once.</T></div>

      <div className="row" style={{ gap: 8, marginTop: 10, flexWrap: "wrap" }}>
        <input value={name} onChange={(e) => setName(e.target.value)} placeholder={tt("Key name (e.g. partner-app)")} style={{ flex: "1 1 220px", padding: "8px 11px", borderRadius: 8 }} />
        <button onClick={mint} disabled={busy} className="btn btn-primary">{busy ? <T>Minting…</T> : <T>Mint API key</T>}</button>
      </div>
      {err && <div className="muted-note" style={{ color: "var(--danger, #c2451f)", marginTop: 8 }}>⚠ {err}</div>}

      {fresh && (
        <div style={{ marginTop: 12, border: "1px solid var(--gold, #c9962a)", background: "rgba(201,150,42,.1)", borderRadius: 10, padding: "11px 13px" }}>
          <div style={{ fontWeight: 600, marginBottom: 4 }}>✓ <T>Copy your token now — it will not be shown again.</T></div>
          <code style={{ display: "block", wordBreak: "break-all", fontSize: 12.5, padding: "8px 10px", background: "rgba(0,0,0,.05)", borderRadius: 7 }}>{fresh}</code>
          <div className="row" style={{ gap: 8, marginTop: 8 }}>
            <button onClick={() => copy(fresh)} className="btn btn-secondary">{copied ? <T>Copied ✓</T> : <T>Copy token</T>}</button>
            <button onClick={() => setFresh(null)} className="btn btn-muted"><T>Done</T></button>
          </div>
        </div>
      )}

      {keys && keys.length > 0 && (
        <div style={{ marginTop: 14 }}>
          {keys.map((k) => (
            <div key={k.id} className="between" style={{ alignItems: "center", padding: "8px 0", borderTop: "1px solid var(--line, rgba(0,0,0,.08))", opacity: k.revoked ? 0.5 : 1 }}>
              <div>
                <div style={{ fontWeight: 600 }}>{k.name} {k.revoked && <span className="dim">· <T>revoked</T></span>}</div>
                <div className="dim" style={{ fontSize: 12 }}><code>{k.prefix}…</code> · <T>created</T> {new Date(k.createdAt).toLocaleDateString()}{k.lastUsedAt ? ` · ${tt("last used")} ${new Date(k.lastUsedAt).toLocaleDateString()}` : ` · ${tt("never used")}`}</div>
              </div>
              {!k.revoked && <button onClick={() => revoke(k.id)} className="btn btn-danger"><T>Revoke</T></button>}
            </div>
          ))}
        </div>
      )}
      {keys && keys.length === 0 && <div className="muted-note" style={{ fontSize: 13, marginTop: 10 }}><T>No API keys yet.</T></div>}

      <details style={{ marginTop: 12 }}>
        <summary style={{ cursor: "pointer", fontSize: 13 }}><T>How a third-party app uses a tutor</T></summary>
        <div style={{ marginTop: 8, fontSize: 13 }}>
          <div><T>Base URL</T>: <code>{base}</code></div>
          <div style={{ marginTop: 6 }}><T>Pass the learner's id with each call</T>: <code>X-Learner-Id: their-user-id</code></div>
          <pre style={{ marginTop: 8, padding: "10px 12px", background: "rgba(0,0,0,.05)", borderRadius: 8, overflowX: "auto", fontSize: 12.5 }}>{curl}</pre>
          {approvedTutors.length === 0 && <div className="muted-note" style={{ fontSize: 12.5 }}><T>Tip: approve a tutor (Safeguard → approve) to make it reachable through the API.</T></div>}
        </div>
      </details>
    </Card>
  );
}

function TeacherDashboard({ user, subject = "electronics", home = false }) {
  useI18nVersion();
  const [concept, setConcept] = useState("");
  const [level, setLevel] = useState("beginner");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  const [tutors, setTutors] = useState([]);
  const [preview, setPreview] = useState(null);
  const [roster, setRoster] = useState(null); // {tutor, rows}
  const [safeguardId, setSafeguardId] = useState(null); // tutor id whose safeguard review is open
  const [manageId, setManageId] = useState(null); // tutor id whose share/visibility modal is open
  const [showCatalog, setShowCatalog] = useState(false);
  // Custom benches: the teacher's own (or colleagues' published) data-only benches. When one is
  // selected it becomes the "active subject" everything below authors over.
  const [benches, setBenches] = useState([]);
  const [activeBenchId, setActiveBenchId] = useState(null);
  const [showWizard, setShowWizard] = useState(false);
  const [previewBenchId, setPreviewBenchId] = useState(null); // custom bench being previewed
  const activeBench = benches.find((b) => b.id === activeBenchId) || null;
  const activeSubject = activeBench ? activeBench.id : subject;

  const loadBenches = useCallback(async () => { try { const { benches } = await api("/benches"); setBenches(benches); } catch (_) { /* benches are optional */ } }, []);
  useEffect(() => { loadBenches(); }, [loadBenches]);

  const [library, setLibrary] = useState([]); // all publicly-published units (any teacher)
  // The dashboard lists ALL the teacher's own benches across subjects (a true home); the active
  // subject/bench above only governs what the next authored tutor is built over.
  const load = useCallback(async () => { const { tutors } = await api(`/tutors`); setTutors(tutors); }, []);
  const loadLibrary = useCallback(async () => { try { const { tutors } = await api(`/tutors?scope=public`); setLibrary(tutors.filter((t) => t.ownerId !== user.id)); } catch (_) { /* optional */ } }, [user.id]);
  useEffect(() => { load(); }, [load]);
  useEffect(() => { loadLibrary(); }, [loadLibrary]);

  async function create() {
    if (!concept.trim()) return;
    setBusy(true); setErr(null);
    try { await api("/tutors", { method: "POST", body: { concept: concept.trim(), level, subject: activeSubject } }); setConcept(""); await load(); }
    catch (e) { setErr(e.message); } finally { setBusy(false); }
  }
  async function benchAct(id, path, body) { await api(`/benches/${id}${path}`, { method: "POST", body }); await loadBenches(); }
  async function deleteBench(id) {
    if (!window.confirm(tt("Delete this custom bench? Tutors already authored on it keep working."))) return;
    await api(`/benches/${id}`, { method: "DELETE" }); if (activeBenchId === id) setActiveBenchId(null); await loadBenches();
  }
  function pickConcept(c) {
    setConcept(c.label);
    if (c.suggestedLevel) setLevel(c.suggestedLevel);
    setShowCatalog(false);
  }
  async function act(id, path, body) { await api(`/tutors/${id}${path}`, { method: "POST", body }); await load(); }
  async function openPreview(id) { const { tutor } = await api(`/tutors/${id}`); setPreview(tutor); }
  async function openRoster(t) { setRoster({ tutor: t, rows: await loadRosterRows(t.id) }); }

  if (preview) return <TutorPlayer base={preview} role="educator" onExit={() => setPreview(null)} />;

  return (
    <div className="page">
      <Card className="card-mb">
        <div className="between" style={{ alignItems: "center" }}>
          <H><T>Your bench</T></H>
          <div className="row" style={{ gap: 8 }}>
            <a href="/browse" className="btn btn-muted" title={tt("Browse the full catalog of standard benches")}>🔭 <T>Browse benches</T></a>
            <button type="button" className="btn btn-outline" onClick={() => setShowWizard(true)}>✨ <T>Create custom bench</T></button>
          </div>
        </div>
        <div className="helptext"><T>Author tutors over a standard STEM bench, or design your OWN bench from a scenario. Custom benches stay private to you until you publish them to other teachers.</T></div>
        <div className="row" style={{ gap: 6, flexWrap: "wrap", marginTop: 8 }}>
          <button type="button" className={`btn ${!activeBench ? "btn-secondary" : "btn-muted"}`} onClick={() => setActiveBenchId(null)}>{SUBJECT_LABELS[subject] || subject} <span className="t-faint">· <T>standard</T></span></button>
        </div>
        {benches.length > 0
          ? <CustomBenchTable benches={benches} activeBenchId={activeBenchId} onSelect={setActiveBenchId} />
          : <div className="muted-note" style={{ marginTop: 8 }}><T>No custom benches yet — tap “Create custom bench” to design one.</T></div>}
        {activeBench && (
          <div className="row" style={{ gap: 8, marginTop: 10, alignItems: "center", flexWrap: "wrap" }}>
            <span className="t-faint" style={{ fontSize: 12, flex: 1, minWidth: 200 }}>{activeBench.microworld}</span>
            <button type="button" className="btn btn-outline" onClick={() => setPreviewBenchId(activeBench.id)} title={tt("Try the bench interactively — nothing is saved")}>👁 <T>Preview</T></button>
            {activeBench.mine && (activeBench.status !== "published"
              ? <button type="button" className="btn btn-primary" onClick={() => benchAct(activeBench.id, "/publish")}><T>Publish to teachers</T></button>
              : <button type="button" className="btn btn-warn" onClick={() => benchAct(activeBench.id, "/publish", { status: "draft" })}><T>Unpublish</T></button>)}
            {activeBench.mine && <button type="button" className="btn btn-danger" onClick={() => deleteBench(activeBench.id)}><T>Delete bench</T></button>}
          </div>
        )}
      </Card>

      {showWizard && <BenchDesignModal onClose={() => setShowWizard(false)} onCreated={async (b) => { setShowWizard(false); await loadBenches(); setActiveBenchId(b.id); }} />}
      {previewBenchId && <BenchPreviewModal benchId={previewBenchId} label={(benches.find((b) => b.id === previewBenchId) || {}).label} onClose={() => setPreviewBenchId(null)} />}

      <Card className="card-mb" data-help="author-tutor">
        <H><T>Author a tutor</T></H>
        {activeBench
          ? <div className="helptext"><T>Enter a concept and the LLM generates a hands-on tutor — objectives, graded tasks, and misconception traps — over your custom bench:</T> <b>{activeBench.label}</b>.</div>
          : <div className="helptext">{{
          statistics: <T>Enter any statistics concept or skill. The LLM picks the right interactive figure (distribution, sampling, or hypothesis test) and generates objectives, graded tasks, and misconception traps over it.</T>,
          optics: <T>Enter any geometric-optics concept. The LLM generates a hands-on tutor — objectives, graded tasks, and misconception traps — over the converging-lens optical bench.</T>,
          anova: <T>Enter any concept about comparing several group means. The LLM generates a hands-on tutor — objectives, graded tasks, and misconception traps — over the one-way ANOVA bench.</T>,
          scatter: <T>Enter any correlation or regression concept. The LLM generates a hands-on tutor — objectives, graded tasks, and misconception traps — over the scatter / regression bench.</T>,
          geometry: <T>Enter any plane-geometry concept. The LLM picks the right figure (semicircle/inscribed angle, power of a point, or angle bisector) and generates objectives, graded tasks, and misconception traps over it.</T>,
        }[subject] || <T>Enter any electronics concept or skill. The LLM generates a hands-on tutor — objectives, a graded task progression, and misconception traps — over the series-circuit lab.</T>}</div>}
        <Sub><T>Concept / skill</T></Sub>
        <div className="row" style={{ gap: 8, alignItems: "flex-start" }}>
          <input value={concept} onChange={(e) => setConcept(e.target.value)} placeholder={activeBench ? tt("e.g. a skill your custom bench should teach") : ({ statistics: tt("e.g. Why the sampling distribution narrows as n grows"), optics: tt("e.g. When a converging lens forms a virtual image"), anova: tt("e.g. Why within-group spread hides a real difference"), scatter: tt("e.g. How one outlier swings the regression line"), geometry: tt("e.g. Why the angle in a semicircle is always a right angle") }[subject] || tt("e.g. Choosing a series resistor for an LED"))}
            onKeyDown={(e) => e.key === "Enter" && create()} className="field" style={{ flex: 1, marginBottom: 0 }} />
          {!activeBench && <button type="button" onClick={() => setShowCatalog(true)} className="btn btn-outline" title={tt("Browse the concept catalog")}>📚 <T>Browse catalog</T></button>}
        </div>
        <div className="row" style={{ gap: 12, marginBlockStart: 12 }}>
          <select value={level} onChange={(e) => setLevel(e.target.value)} className="select">
            <option value="beginner">{tt("Beginner")}</option><option value="intermediate">{tt("Intermediate")}</option><option value="advanced">{tt("Advanced")}</option><option value="zhongkao">{tt("Zhongkao 中考")}</option><option value="gaokao">{tt("Gaokao 高考")}</option>
          </select>
          <button onClick={create} disabled={busy} className="btn btn-primary">{busy ? tt("Generating…") : tt("Generate tutor")}</button>
          {err && <span className="err">{err}</span>}
        </div>
        {busy && <GenProgress concept={concept} level={level} />}
      </Card>

      {showCatalog && (
        <div className="modal-overlay" onClick={() => setShowCatalog(false)}>
          <Card className="modal modal--wide">
            <ConceptCatalog subject={subject} onPick={pickConcept} onClose={() => setShowCatalog(false)} />
          </Card>
        </div>
      )}

      <H><T>Your benches</T> <span className="dim">· {tutors.length}</span></H>
      <div className="helptext" style={{ marginTop: -4 }}><T>Each tutor is a mini-class: keep it private, publish to everyone, or share an access code so only your students can join.</T></div>
      {tutors.length === 0
        ? <div className="muted-note" style={{ fontSize: 13 }}><T>No tutors yet — author one above.</T></div>
        : <TutorTable
            tutors={tutors}
            onManage={setManageId}
            onPreview={openPreview}
            onRoster={openRoster}
            onSafeguard={setSafeguardId}
            onRegenerate={(id) => act(id, "/regenerate")}
            onDelete={(t) => { if (window.confirm(tt("Delete this tutor?"))) api(`/tutors/${t.id}`, { method: "DELETE" }).then(load); }}
            onBulkDelete={(ids) => Promise.all(ids.map((id) => api(`/tutors/${id}`, { method: "DELETE" }))).then(load)}
          />}

      {library.length > 0 && (<>
        <H style={{ marginTop: 22 }}><T>Public library</T> <span className="dim">· {library.length}</span></H>
        <div className="helptext" style={{ marginTop: -4 }}><T>Benches other teachers have published publicly — preview any of them.</T></div>
        {library.map((t) => (
          <TutorRow key={t.id} t={t}>
            <button onClick={() => openPreview(t.id)} className="btn btn-secondary"><T>Preview</T></button>
          </TutorRow>
        ))}
      </>)}

      <ApiKeysPanel approvedTutors={tutors.filter((t) => t.safeguard && t.safeguard.status === "approved")} />

      {roster && <RosterModal tutor={roster.tutor} rows={roster.rows} onClose={() => setRoster(null)} />}
      {safeguardId && <SafeguardModal tutorId={safeguardId} onClose={() => setSafeguardId(null)} onChange={load} />}
      {manageId && (() => {
        // Look the tutor up fresh each render so the modal reflects a just-generated code / changed tier.
        const mt = tutors.find((t) => t.id === manageId);
        return mt ? <TutorManageModal tutor={mt} onClose={() => setManageId(null)} onChange={load} onNeedSafeguard={() => { setManageId(null); setSafeguardId(mt.id); }} /> : null;
      })()}
    </div>
  );
}

/* ===================== student ===================== */
// Wraps the lab with a difficulty switcher. The teacher's authored level is the default (•);
// other levels are generated on demand from the same concept (GET /tutors/:id/play?level=).
const DIFFICULTY = [["beginner", "Beginner"], ["intermediate", "Intermediate"], ["advanced", "Advanced"], ["zhongkao", "Zhongkao 中考"], ["gaokao", "Gaokao 高考"]];
// Frontend bench registry: each lab file registers window.<Bench>Tutor; default = CircuitLab.
// Shared by the authored-tutor player and the public landing-page example popup.
function benchLabFor(subject) {
  const BENCH_LABS = { electronics: window.ElectronicsTutor, statistics: window.StatTutor, optics: window.OpticsTutor, anova: window.AnovaTutor, scatter: window.ScatterTutor, geometry: window.GeometryTutor, advgeometry: window.AdvGeometryTutor, advstats: window.AdvStatsTutor, advalgebra: window.AdvAlgebraTutor, gaslaws: window.GasLawsTutor, titration: window.TitrationTutor, equilibrium: window.EquilibriumTutor, punnett: window.PunnettTutor, population: window.PopulationTutor, hardyweinberg: window.HardyWeinbergTutor, kepler: window.KeplerTutor, halflife: window.HalfLifeTutor, seasons: window.SeasonsTutor, logicgates: window.LogicGatesTutor, bigo: window.BigOTutor, binary: window.BinaryTutor, kinematics: window.KinematicsTutor, projectile: window.ProjectileTutor, forces: window.ForcesTutor, momentum: window.MomentumTutor, energy: window.EnergyTutor, springs: window.SpringsTutor, pendulum: window.PendulumTutor, waves: window.WavesTutor, calorimetry: window.CalorimetryTutor, coulomb: window.CoulombTutor, refraction: window.RefractionTutor, density: window.DensityTutor, advelectronics: window.AdvElectronicsTutor, linear: window.LinearTutor, quadratic: window.QuadraticTutor, systems: window.SystemsTutor, exponential: window.ExponentialTutor, trig: window.TrigTutor, unitcircle: window.UnitCircleTutor, trigwave: window.TrigWaveTutor, pythagorean: window.PythagoreanTutor, vectors: window.VectorsTutor, derivative: window.DerivativeTutor, integral: window.IntegralTutor, limits: window.LimitsTutor, secant: window.SecantTutor, relatedrates: window.RelatedRatesTutor, optimization: window.OptimizationTutor, mvt: window.MvtTutor, accumulation: window.AccumulationTutor, areabetween: window.AreaBetweenTutor, slopefield: window.SlopeFieldTutor, taylor: window.TaylorTutor, series: window.SeriesTutor, partials: window.PartialsTutor, doubleintegral: window.DoubleIntegralTutor, volumerev: window.VolumeRevTutor, gradient: window.GradientTutor, newton: window.NewtonTutor, linearization: window.LinearizationTutor, curvesketch: window.CurveSketchTutor, implicit: window.ImplicitTutor, lhopital: window.LHopitalTutor, avgvalue: window.AvgValueTutor, improper: window.ImproperTutor, powerseries: window.PowerSeriesTutor, parametric: window.ParametricTutor, polar: window.PolarTutor, arclength: window.ArcLengthTutor, tangentplane: window.TangentPlaneTutor, criticalpoints: window.CriticalPointsTutor, lagrange: window.LagrangeTutor, vectorfield: window.VectorFieldTutor, lineintegral: window.LineIntegralTutor, tripleintegral: window.TripleIntegralTutor, euler: window.EulerTutor, probability: window.ProbabilityTutor, stoichiometry: window.StoichiometryTutor, molarity: window.MolarityTutor, kinetics: window.KineticsTutor, thermochem: window.ThermochemTutor, periodictrends: window.PeriodicTrendsTutor, organicchem: window.OrganicChemTutor, enzymes: window.EnzymesTutor, predatorprey: window.PredatorPreyTutor, dihybrid: window.DihybridTutor, photosynthesis: window.PhotosynthesisTutor, osmosis: window.OsmosisTutor, moonphases: window.MoonPhasesTutor, platetectonics: window.PlateTectonicsTutor, greenhouse: window.GreenhouseTutor, stellar: window.StellarTutor, hubble: window.HubbleTutor, sorting: window.SortingTutor, recursion: window.RecursionTutor, cryptography: window.CryptographyTutor, networking: window.NetworkingTutor, hashing: window.HashingTutor, floatingpoint: window.FloatingPointTutor };
  return BENCH_LABS[subject] || window.ElectronicsTutor || CircuitLab;
}
function TutorPlayer({ base, role, onExit }) {
  useI18nVersion();
  const [level, setLevel] = useState(base.level || "beginner");
  const [spec, setSpec] = useState(base.spec);
  const [loading, setLoading] = useState(false);
  const [err, setErr] = useState(null);
  const baseLevel = base.level || "beginner";
  const [difficulty, setDifficulty] = useState(0);   // 0 = teacher's base problem
  const [problemId, setProblemId] = useState(null);  // non-null = a generated library problem
  const [library, setLibrary] = useState([]);
  const [showLib, setShowLib] = useState(false);
  const [learning, setLearning] = useState(false);
  const [learnUrl, setLearnUrl] = useState(null);    // non-null = LearnModal open
  const [noteOpen, setNoteOpen] = useState(false);   // re-read the teacher's "read me first" note
  const hasNote = role === "learner" && base.readme && base.readme.text;
  const LVL_NUM = { beginner: 1, intermediate: 2, advanced: 3, zhongkao: 5, gaokao: 7 };
  // The 中考/高考 exam options are Chinese-exam specific: only meaningful for Chinese learners,
  // and only for benches the registry tags with the matching exam framework (zhongkao = grade-9
  // exit, gaokao = grade-12 exit). Pull the bench's frameworks so the buttons stay content- and
  // language-aware instead of showing on every bench for everyone.
  const [frameworks, setFrameworks] = useState([]);
  useEffect(() => {
    let alive = true;
    if (!base.subject) { setFrameworks([]); return; }
    api(`/benches/${base.subject}/about`).then((r) => { if (alive) setFrameworks(r.frameworks || []); }).catch(() => { if (alive) setFrameworks([]); });
    return () => { alive = false; };
  }, [base.subject]);
  const lang = (window.I18N && window.I18N.lang) || "en";
  const isChinese = /^zh/i.test(lang);
  const showZhongkao = isChinese && frameworks.includes("zhongkao");
  const showGaokao = isChinese && frameworks.includes("gaokao");
  const visibleDifficulty = DIFFICULTY.filter(([v]) => (v === "zhongkao" ? showZhongkao : v === "gaokao" ? showGaokao : true));
  async function refreshLibrary() { try { const r = await api(`/tutors/${base.id}/problems`); setLibrary(r.problems || []); } catch (e) { /* ignore */ } }
  useEffect(() => { refreshLibrary(); }, []); // eslint-disable-line
  async function pick(lv) {
    if (lv === level || loading) return;
    setLoading(true); setErr(null);
    try { const r = await api(`/tutors/${base.id}/play?level=${lv}`); setSpec(r.spec); setLevel(lv); setDifficulty(0); setProblemId(null); }
    catch (e) { setErr(e.message); } finally { setLoading(false); }
  }
  // Generate a fresh EXAM-style problem on THIS bench: same microworld, a hard 高考/中考
  // scenario (compound multi-field goals, exam phrasing) authored at the exam tier.
  async function generateExam(examLevel) {
    if (loading) return;
    setLoading(true); setErr(null);
    try {
      const r = await api(`/tutors/${base.id}/problems`, { method: "POST", body: { level: examLevel, difficulty: LVL_NUM[examLevel] || 7 } });
      setSpec(r.problem.spec); setDifficulty(r.problem.difficulty); setProblemId(r.problem.id); setLevel(examLevel);
      setLibrary((l) => [...l.filter((p) => p.id !== r.problem.id), r.problem]);
    } catch (e) { setErr(e.message); } finally { setLoading(false); }
  }
  // Generate a fresh problem one tier easier (delta −1) or harder (delta +1) than the one open.
  async function generate(delta) {
    if (loading) return;
    setLoading(true); setErr(null);
    const cur = difficulty || LVL_NUM[level] || 2;
    const next = Math.max(1, Math.min(8, cur + delta));
    try {
      const r = await api(`/tutors/${base.id}/problems`, { method: "POST", body: { difficulty: next } });
      setSpec(r.problem.spec); setDifficulty(r.problem.difficulty); setProblemId(r.problem.id);
      setLibrary((l) => [...l.filter((p) => p.id !== r.problem.id), r.problem]);
    } catch (e) { setErr(e.message); } finally { setLoading(false); }
  }
  // Author a multi-phase learning journey for the OPEN problem's concept/skills on the
  // alwaysAI platform (server/alwaysai.js) and open its hosted learning UI in a large
  // in-app popup (LearnModal) — same pattern as the "Explore concepts" map modal.
  async function learnProblem() {
    if (learning) return;
    setLearning(true); setErr(null);
    try {
      const r = await api(`/tutors/${base.id}/problems/${problemId || "base"}/learn`, { method: "POST" });
      if (!r || !r.url) throw new Error("No learning activity was returned.");
      setLearnUrl(r.url);
    } catch (e) { setErr(e.message); } finally { setLearning(false); }
  }
  function loadProblem(p) { setSpec(p.spec); setDifficulty(p.difficulty); setProblemId(p.id); setShowLib(false); }
  function openBase() { setSpec(base.spec); setLevel(baseLevel); setDifficulty(0); setProblemId(null); setShowLib(false); }
  return (
    <div>
      <div className="difficulty-bar">
        <span className="sub" style={{ margin: 0 }}><T>Difficulty</T></span>
        <div className="seg">
          {visibleDifficulty.map(([v, label]) => (
            <button key={v} onClick={() => pick(v)} disabled={loading} className={`seg__btn${level === v && !problemId ? " is-active" : ""}`}>
              <T>{label}</T>{v === baseLevel ? " •" : ""}
            </button>
          ))}
        </div>
        <button onClick={() => generate(-1)} disabled={loading} className="btn btn-outline btn-tiny" title={tt("Generate a fresh, easier problem for this concept")}>🧊 <T>Create an easier problem</T></button>
        <button onClick={() => generate(1)} disabled={loading} className="btn btn-outline btn-tiny" title={tt("Generate a fresh, harder problem for this concept")}>🔥 <T>Create a harder problem</T></button>
        {showZhongkao && <button onClick={() => generateExam("zhongkao")} disabled={loading} className="btn btn-outline btn-tiny" title={tt("Generate a Zhongkao (中考) exam-style question on this bench")}>🀄 <T>Try 中考 Question</T></button>}
        {showGaokao && <button onClick={() => generateExam("gaokao")} disabled={loading} className="btn btn-outline btn-tiny" title={tt("Generate a Gaokao (高考) exam-style question on this bench")}>🎓 <T>Try 高考 Question</T></button>}
        <button onClick={learnProblem} disabled={loading || learning} className="btn btn-outline btn-tiny" title={tt("Learn this problem's concepts & skills in a guided, multi-phase journey")}>🎓 {learning ? <T>opening…</T> : <T>Learn these concepts</T>}</button>
        {difficulty > 0 && <span className="muted-note" title={tt("difficulty tier")}><span className="t-amber">{"★".repeat(Math.min(5, difficulty))}</span> <T>tier</T> {difficulty}</span>}
        <div style={{ position: "relative" }}>
          <button onClick={() => { setShowLib((s) => !s); if (!showLib) refreshLibrary(); }} disabled={loading} className="btn btn-outline btn-tiny">📚 <T>Versions</T> ({library.length + 1})</button>
          {showLib && (
            <div className="card" style={{ position: "absolute", zIndex: 80, top: "112%", insetInlineStart: 0, width: 340, maxHeight: 340, overflowY: "auto", padding: 8 }}>
              <div className="muted-note" style={{ padding: "2px 6px 6px" }}><T>All versions of this concept, by difficulty:</T></div>
              <button onClick={openBase} className={`btn btn-start btn-block${!problemId ? " is-active" : ""}`} style={{ textAlign: "start", marginBottom: 4 }}>
                <span><span className="t-teal">●</span> <span style={{ fontSize: 12 }}>{base.spec.title}</span></span>
                <span className="muted-note" style={{ display: "block", fontSize: 10 }}><T>teacher default</T> · {baseLevel}</span>
              </button>
              {library.slice().sort((a, b) => a.difficulty - b.difficulty).map((p) => (
                <button key={p.id} onClick={() => loadProblem(p)} className={`btn btn-start btn-block${problemId === p.id ? " is-active" : ""}`} style={{ textAlign: "start", marginBottom: 4 }}>
                  <span><span className="t-amber">{"★".repeat(Math.min(5, p.difficulty))}{p.difficulty > 5 ? `+${p.difficulty - 5}` : ""}</span> <span style={{ fontSize: 12 }}>{p.title}</span></span>
                  <span className="muted-note" style={{ display: "block", fontSize: 10 }}><T>tier</T> {p.difficulty}{p.byName ? ` · by ${p.byName}` : ""}</span>
                </button>
              ))}
            </div>
          )}
        </div>
        {hasNote && <button onClick={() => setNoteOpen(true)} className="btn btn-outline btn-tiny" title={tt("Re-read the note your teacher left for you")}>📋 <T>Teacher’s note</T></button>}
        {loading && <span className="muted-note"><T>generating…</T></span>}
        {err && <span className="t-crimson" style={{ fontSize: 11 }}>{err}</span>}
        <span className="muted-note">• <T>teacher default</T></span>
      </div>
      {(() => {
        const t = { ...base, spec, level, difficulty, problemId };
        // Auto-generated bench: a concept the fixed subject-bench couldn't model was minted into a
        // data-only bench (spec.dynamicBench). The generic lab renders + grades it. (Takes priority:
        // such specs carry fit "core" already, but route on the design's presence to be explicit.)
        if (spec.dynamicBench && window.DynamicTutor) return <window.DynamicTutor key={problemId || level} tutor={t} role={role} onExit={onExit} />;
        if (spec.fit === "stretch") return <StudyView key={problemId || level} tutor={t} onExit={onExit} />;
        const Lab = benchLabFor(base.subject);
        return <Lab key={problemId || level} tutor={t} role={role} onExit={onExit} />;
      })()}
      {learnUrl && <LearnModal url={learnUrl} title={tt("Learn these concepts")} onClose={() => setLearnUrl(null)} />}
      {noteOpen && <ReadmeGate tutor={base} onClose={() => setNoteOpen(false)} />}
    </div>
  );
}

// Non-graded view for concepts the fixed lab cannot simulate (fit === "stretch"):
// the AI concept diagram + objectives + key ideas, clearly marked as conceptual.
function StudyView({ tutor, onExit }) {
  useI18nVersion();
  const spec = tutor.spec;
  return (
    <div className="lab">
      <div className="lab__header">
        <div>
          <div className="lab__title nr">{spec.title} <span className="t-amber" style={{ fontSize: 13 }}>· <T>conceptual</T></span></div>
          <div className="lab__summary">{spec.summary}</div>
        </div>
        <div className="row" style={{ gap: 16 }}>
          <button onClick={onExit} className="btn btn-muted"><T>← Back</T></button>
        </div>
      </div>
      <div className="lab__goal">{{
        statistics: <T>This concept can’t be demonstrated on the statistics figures, so it’s a guided explanation — not graded.</T>,
        optics: <T>This concept can’t be demonstrated on the converging-lens bench, so it’s a guided explanation — not graded.</T>,
        anova: <T>This concept can’t be demonstrated on the ANOVA bench, so it’s a guided explanation — not graded.</T>,
        scatter: <T>This concept can’t be demonstrated on the scatter / regression bench, so it’s a guided explanation — not graded.</T>,
        electronics: <T>This concept can’t be built on the series-circuit bench, so it’s a guided explanation — not lab-graded.</T>,
        geometry: <T>This concept can’t be demonstrated on the geometry bench, so it’s a guided explanation — not graded.</T>,
        gaslaws: <T>This concept can’t be demonstrated on the gas-laws bench, so it’s a guided explanation — not graded.</T>,
        titration: <T>This concept can’t be demonstrated on the titration bench, so it’s a guided explanation — not graded.</T>,
        equilibrium: <T>This concept can’t be demonstrated on the equilibrium bench, so it’s a guided explanation — not graded.</T>,
        punnett: <T>This concept can’t be demonstrated on the Punnett-square bench, so it’s a guided explanation — not graded.</T>,
        population: <T>This concept can’t be demonstrated on the population-growth bench, so it’s a guided explanation — not graded.</T>,
        hardyweinberg: <T>This concept can’t be demonstrated on the Hardy–Weinberg bench, so it’s a guided explanation — not graded.</T>,
        kepler: <T>This concept can’t be demonstrated on the Kepler-orbits bench, so it’s a guided explanation — not graded.</T>,
        halflife: <T>This concept can’t be demonstrated on the radioactive-dating bench, so it’s a guided explanation — not graded.</T>,
        seasons: <T>This concept can’t be demonstrated on the seasons bench, so it’s a guided explanation — not graded.</T>,
        logicgates: <T>This concept can’t be demonstrated on the logic-gates bench, so it’s a guided explanation — not graded.</T>,
        bigo: <T>This concept can’t be demonstrated on the Big-O growth bench, so it’s a guided explanation — not graded.</T>,
        binary: <T>This concept can’t be demonstrated on the binary-numbers bench, so it’s a guided explanation — not graded.</T>,
      }[tutor.subject] || <T>This concept can’t be demonstrated on this bench, so it’s a guided explanation — not graded.</T>}</div>
      <div className="study">
        <ConceptDiagram svg={spec.diagram} concept={tutor.concept} />
        <H><T>What you’ll learn</T></H>
        <ul className="study__list">{spec.objectives.map((o) => <li key={o.id}><b>{o.label}</b> — {o.desc}</li>)}</ul>
        {spec.hints && spec.hints.length > 0 && (<>
          <H><T>Key ideas</T></H>
          <ul className="study__list">{spec.hints.map((h, i) => <li key={i}>{h}</li>)}</ul>
        </>)}
      </div>
    </div>
  );
}

function StudentDashboard({ user, subject = "electronics", home = false }) {
  useI18nVersion();
  const [data, setData] = useState({ assigned: [], public: [], continue: [] });
  const [active, setActive] = useState(null);
  const [gate, setGate] = useState(null); // a tutor with a "read me first" note, awaiting acknowledgment
  const [code, setCode] = useState("");
  const [joinMsg, setJoinMsg] = useState(null);
  const [joining, setJoining] = useState(false);
  const load = useCallback(async () => { try { const d = await api(`/my/benches`); setData({ assigned: d.assigned || [], public: d.public || [], continue: d.continue || [] }); } catch (_) { /* keep prior */ } }, []);
  useEffect(() => { load(); }, [load]);
  // If the bench carries a teacher's "read me first" note (from the safeguard process): on the
  // student's FIRST visit, pop it up with a timed read; on later visits go straight in (the note
  // stays available to re-read from inside the workbench). "Seen" is tracked per user + bench.
  const seenKey = (id) => `etutor:readme-seen:${user.id}:${id}`;
  const hasSeen = (id) => { try { return !!localStorage.getItem(seenKey(id)); } catch (_) { return false; } };
  async function open(id) {
    const { tutor } = await api(`/tutors/${id}`);
    if (tutor.readme && tutor.readme.text && !hasSeen(id)) setGate(tutor);
    else setActive(tutor);
  }
  async function join() {
    const c = code.trim(); if (!c || joining) return;
    setJoining(true); setJoinMsg(null);
    try { const r = await api(`/enroll`, { method: "POST", body: { code: c } }); setCode(""); setJoinMsg({ ok: true, text: tf("Joined “{name}”.", { name: r.tutor.title || r.tutor.concept }) }); await load(); }
    catch (e) { setJoinMsg({ ok: false, text: e.message === "invalid or expired code" ? tt("That code isn’t valid. Check it with your teacher.") : e.message }); }
    finally { setJoining(false); }
  }
  if (active) return <TutorPlayer base={active} role="learner" onExit={() => { setActive(null); load(); }} />;
  const card = (t, opts = {}) => (
    <Card key={(opts.tag || "") + t.id}>
      <div className="row" style={{ gap: 8, alignItems: "center", marginBottom: 4, flexWrap: "wrap" }}>
        <span className="tutor-name nr">{t.title || t.concept}</span>
        <Pill color={t.gradeable ? C.teal : C.amber}>{t.gradeable ? <T>Interactive</T> : <T>Conceptual</T>}</Pill>
        {t.access === "assigned" && <Pill color={C.blue}>{tt("assigned")}</Pill>}
      </div>
      <div className="helptext" style={{ minHeight: 38, marginBottom: 0 }}>{t.summary}</div>
      <div className="muted-note" style={{ margin: "8px 0" }}>{tf("by {owner} · {level}", { owner: t.ownerName, level: t.level })}{opts.progress != null ? tf(" · {m}% mastery", { m: opts.progress }) : ""}</div>
      <button onClick={() => open(t.id)} className="btn btn-primary btn-block">{opts.resume ? <T>Continue</T> : (t.gradeable ? <T>Start learning</T> : <T>Study concept</T>)}</button>
    </Card>
  );
  const assignedIds = new Set(data.assigned.map((t) => t.id));
  const publicOnly = data.public.filter((t) => !assignedIds.has(t.id));
  return (
    <div className="page">
      <Card className="card-mb">
        <div className="between" style={{ alignItems: "center" }}>
          <H><T>Join a bench</T></H>
          <a href="/browse" className="btn btn-muted" title={tt("Browse the full catalog of standard benches")}>🔭 <T>Browse benches</T></a>
        </div>
        <div className="helptext"><T>Got an access code from your teacher? Enter it to join their bench.</T></div>
        <div className="row" style={{ gap: 8, alignItems: "center", flexWrap: "wrap" }}>
          <input value={code} onChange={(e) => setCode(e.target.value.toUpperCase())} onKeyDown={(e) => e.key === "Enter" && join()} placeholder="BENCH-XXXX" className="field" style={{ flex: 1, minWidth: 160, marginBottom: 0, fontFamily: "monospace", letterSpacing: ".05em" }} maxLength={16} />
          <button onClick={join} disabled={joining || !code.trim()} className="btn btn-primary">{joining ? tt("Joining…") : tt("Join")}</button>
        </div>
        {joinMsg && <div className="muted-note" style={{ marginTop: 8, color: joinMsg.ok ? C.teal : C.crimson }}>{joinMsg.text}</div>}
      </Card>

      {data.continue.length > 0 && (<>
        <H><T>Continue where you left off</T> <span className="dim">· {data.continue.length}</span></H>
        <div className="tutor-grid">{data.continue.map((t) => card(t, { tag: "c", resume: true, progress: t.mastery }))}</div>
      </>)}

      <H style={{ marginTop: data.continue.length ? 18 : 0 }}><T>Assigned to you</T> <span className="dim">· {data.assigned.length}</span></H>
      {data.assigned.length === 0 && <div className="muted-note" style={{ fontSize: 13 }}><T>Nothing assigned yet — enter an access code above to join a bench.</T></div>}
      <div className="tutor-grid">{data.assigned.map((t) => card(t, { tag: "a" }))}</div>

      <H style={{ marginTop: 18 }}><T>Public benches</T> <span className="dim">· {publicOnly.length}</span></H>
      {publicOnly.length === 0 && <div className="muted-note" style={{ fontSize: 13 }}><T>No public benches yet.</T></div>}
      <div className="tutor-grid">{publicOnly.map((t) => card(t, { tag: "p" }))}</div>
      {gate && <ReadmeGate tutor={gate} timed onStart={() => { try { localStorage.setItem(seenKey(gate.id), String(Date.now())); } catch (_) { /* private mode — just proceed */ } setActive(gate); setGate(null); }} onClose={() => setGate(null)} />}
    </div>
  );
}

// Sortable table of the teacher's custom benches (+ colleagues' published ones). Replaces the chip
// row once there are several: click any header to sort; shows the creation date. Clicking a row sets
// the ACTIVE bench (what the next authored tutor is built over); per-bench actions stay in the card.
function CustomBenchTable({ benches, activeBenchId, onSelect }) {
  const [sort, setSort] = useState({ key: "createdAt", dir: "desc" });
  const fmt = (iso) => { if (!iso) return "—"; try { return new Date(iso).toLocaleDateString([], { year: "2-digit", month: "short", day: "numeric" }); } catch (_) { return "—"; } };
  const cols = [
    { key: "label", label: "Name" }, { key: "field", label: "Field" }, { key: "tasks", label: "Tasks", num: true },
    { key: "status", label: "Status" }, { key: "createdAt", label: "Created" }, { key: "ownerName", label: "Owner" },
  ];
  const sorted = useMemo(() => {
    const { key, dir } = sort, f = dir === "asc" ? 1 : -1;
    return [...benches].sort((a, b) => {
      let av = a[key], bv = b[key];
      if (typeof av === "number" || typeof bv === "number") { av = Number(av) || 0; bv = Number(bv) || 0; }
      else { av = String(av || "").toLowerCase(); bv = String(bv || "").toLowerCase(); }
      return av < bv ? -f : av > bv ? f : 0;
    });
  }, [benches, sort]);
  const click = (key) => setSort((s) => (s.key === key ? { key, dir: s.dir === "asc" ? "desc" : "asc" } : { key, dir: key === "createdAt" ? "desc" : "asc" }));
  const arrow = (key) => (sort.key === key ? (sort.dir === "asc" ? " ▲" : " ▼") : "");
  return (
    <table className="bench-table">
      <thead><tr>{cols.map((c) => <th key={c.key} className={c.num ? "num" : ""} onClick={() => click(c.key)} title={tt("Sort by this column")}><T>{c.label}</T>{arrow(c.key)}</th>)}</tr></thead>
      <tbody>
        {sorted.map((b) => (
          <tr key={b.id} className={activeBenchId === b.id ? "is-active" : ""} onClick={() => onSelect(b.id)} title={b.microworld}>
            <td><b>{b.label}</b></td>
            <td className="t-faint">{b.field}</td>
            <td className="num">{b.tasks}</td>
            <td>{b.status === "published" ? <span style={{ color: "var(--teal)" }}>✓ {tt("published")}</span> : <span className="t-faint">{tt("draft")}</span>}</td>
            <td className="t-faint" title={b.createdAt || ""}>{fmt(b.createdAt)}</td>
            <td className="t-faint">{b.mine ? tt("you") : b.ownerName}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

// The per-tutor "share & visibility" controls (the segmented tier, access code, share link/QR) used to
// sit inline on every row, which made the "Your benches" list rows uneven. They now live in this modal,
// opened from a row's Share action, so the list itself stays a tidy uniform table.
function TutorManageModal({ tutor, onClose, onChange, onNeedSafeguard }) {
  useI18nVersion();
  return (
    <div className="modal-overlay" onClick={onClose}>
      <Card className="modal" style={{ inlineSize: "min(460px,100%)" }}>
        <div onClick={(e) => e.stopPropagation()}>
          <div className="between" style={{ alignItems: "center" }}>
            <H><T>Share & visibility</T></H>
            <button onClick={onClose} className="btn btn-muted"><T>Close</T></button>
          </div>
          <div className="muted-note" style={{ marginBottom: 10 }}>{tutor.title || tutor.concept}</div>
          <TutorAssignControls t={tutor} onChange={onChange} onNeedSafeguard={onNeedSafeguard} />
        </div>
      </Card>
    </div>
  );
}

// "Your benches" as a sortable table (mirrors CustomBenchTable / the bench catalog): uniform rows,
// click any header to sort. Per-row actions are compact; Share opens the visibility/code/QR modal.
function TutorTable({ tutors, onManage, onPreview, onRoster, onSafeguard, onRegenerate, onDelete, onBulkDelete }) {
  const [sort, setSort] = useState({ key: "updatedAt", dir: "desc" });
  const [sel, setSel] = useState(() => new Set());
  const fmt = (iso) => { if (!iso) return "—"; try { return new Date(iso).toLocaleString([], { year: "2-digit", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); } catch (_) { return "—"; } };
  const visOf = (t) => t.visibility || (t.status === "published" ? "public" : "private");
  const LVL = { beginner: "Beginner", intermediate: "Intermediate", advanced: "Advanced", zhongkao: "Zhongkao 中考", gaokao: "Gaokao 高考" };
  const cols = [
    { key: "name", label: "Name" }, { key: "concept", label: "Concept" }, { key: "level", label: "Level" },
    { key: "visibility", label: "Visibility" }, { key: "safeguard", label: "Safeguard" },
    { key: "generator", label: "Type" }, { key: "updatedAt", label: "Updated" },
  ];
  const valOf = (t, key) => {
    if (key === "name") return (t.title || t.concept || "").toLowerCase();
    if (key === "concept") return (t.concept || "").toLowerCase();
    if (key === "level") return (t.level || "").toLowerCase();
    if (key === "visibility") return visOf(t);
    if (key === "safeguard") return safeguardState(t).label.toLowerCase();
    if (key === "generator") return t.generator || "";
    return t.updatedAt || ""; // updatedAt
  };
  const sorted = useMemo(() => {
    const { key, dir } = sort, f = dir === "asc" ? 1 : -1;
    return [...tutors].sort((a, b) => { const av = valOf(a, key), bv = valOf(b, key); return av < bv ? -f : av > bv ? f : 0; });
  }, [tutors, sort]);
  const click = (key) => setSort((s) => (s.key === key ? { key, dir: s.dir === "asc" ? "desc" : "asc" } : { key, dir: key === "updatedAt" ? "desc" : "asc" }));
  const arrow = (key) => (sort.key === key ? (sort.dir === "asc" ? " ▲" : " ▼") : "");
  const allIds = sorted.map((t) => t.id);
  const allSelected = allIds.length > 0 && allIds.every((id) => sel.has(id));
  const toggle = (id) => setSel((s) => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
  const toggleAll = () => setSel(allSelected ? new Set() : new Set(allIds));
  const bulkDelete = () => {
    const ids = allIds.filter((id) => sel.has(id));
    if (ids.length && window.confirm(tt("Delete the selected tutors? This cannot be undone.") + ` (${ids.length})`)) { onBulkDelete(ids); setSel(new Set()); }
  };
  return (
    <>
    {sel.size > 0 && (
      <div className="row" style={{ gap: 8, alignItems: "center", marginTop: 8 }}>
        <span className="t-faint" style={{ fontSize: 12.5 }}>{sel.size} {tt("selected")}</span>
        <button className="btn btn-tiny btn-danger" onClick={bulkDelete}>🗑 <T>Delete selected</T></button>
        <button className="btn btn-tiny btn-muted" onClick={() => setSel(new Set())}><T>Clear</T></button>
      </div>
    )}
    <table className="bench-table" style={{ marginTop: 8 }}>
      <thead><tr>
        <th className="chk"><input type="checkbox" ref={(el) => { if (el) el.indeterminate = sel.size > 0 && !allSelected; }} checked={allSelected} onChange={toggleAll} title={tt("Select all")} /></th>
        {cols.map((c) => <th key={c.key} className={c.key === "safeguard" ? "sg-col" : undefined} onClick={() => click(c.key)} title={tt("Sort by this column")}><T>{c.label}</T>{arrow(c.key)}</th>)}
        <th className="actions"><T>Actions</T></th>
      </tr></thead>
      <tbody>
        {sorted.map((t) => {
          const vis = visOf(t); const vm = VIS_PILL[vis] || VIS_PILL.private; const sgs = safeguardState(t);
          return (
            <tr key={t.id} className={sel.has(t.id) ? "is-active" : undefined} onClick={() => onManage(t.id)} title={tt("Share & visibility")}>
              <td className="chk" onClick={(e) => e.stopPropagation()}><input type="checkbox" checked={sel.has(t.id)} onChange={() => toggle(t.id)} /></td>
              <td><b className="nr">{t.title || t.concept}</b>{t.access === "assigned" ? <> <Pill color={C.amber}>{tt("assigned")}</Pill></> : null}</td>
              <td className="t-faint">{t.concept}</td>
              <td className="t-faint">{tt(LVL[t.level] || t.level || "—")}</td>
              <td><Pill color={vm.c}>{tt(vm.label)}</Pill>{vis === "code" && t.accessCode ? <span className="t-faint" style={{ marginInlineStart: 6, fontFamily: "monospace", fontSize: 11 }}>{t.accessCode}</span> : null}</td>
              <td className="sg-col"><Pill color={sgs.c}>🛡 {tt(sgs.label)}</Pill></td>
              <td className="t-faint">{t.generator === "llm" ? "LLM" : tt("template")}</td>
              <td className="t-faint" title={t.updatedAt || ""} style={{ whiteSpace: "nowrap" }}>{fmt(t.updatedAt)}</td>
              <td className="actions" onClick={(e) => e.stopPropagation()}>
                <div className="row" style={{ gap: 4, flexWrap: "nowrap" }}>
                  <button className="btn btn-tiny btn-primary" onClick={() => onManage(t.id)} title={tt("Visibility, access code & share link / QR")}>🔗 <T>Share</T></button>
                  <button className="btn btn-tiny btn-secondary" onClick={() => onSafeguard(t.id)} title={tt("AI safeguard review")}>🛡</button>
                  <button className="btn btn-tiny btn-secondary" onClick={() => onPreview(t.id)} title={tt("Preview")}>👁</button>
                  <button className="btn btn-tiny btn-secondary" onClick={() => onRoster(t)} title={tt("Roster")}>👥</button>
                  <button className="btn btn-tiny btn-muted" onClick={() => onRegenerate(t.id)} title={tt("Regenerate")}>↻</button>
                  <button className="btn btn-tiny btn-danger" onClick={() => onDelete(t)} title={tt("Delete")}>🗑</button>
                </div>
              </td>
            </tr>
          );
        })}
      </tbody>
    </table>
    </>
  );
}

/* ===================== teacher custom benches ===================== */
// Educator-facing guided wizard: describe a SCENARIO + the variables / outcomes / tasks, and
// the strongest model designs a data-only, auto-graded microworld (no coding, nothing hot-loaded).
// On success the bench is saved as the teacher's private draft; they can then author tutors over
// it and publish it to other teachers. Mirrors the admin BenchWizard but hides the code scaffold.
function BenchDesignModal({ onClose, onCreated }) {
  useI18nVersion();
  const [form, setForm] = useState({ scenario: "", label: "", field: "", variables: "", outcomes: "", tasks: "" });
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  const [result, setResult] = useState(null); // { bench, design }
  const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));
  const [suggesting, setSuggesting] = useState(null); // which field is being AI-suggested
  async function suggest(k) {
    setSuggesting(k); setErr(null);
    try {
      const r = await api("/benches/suggest", { method: "POST", body: { field: k, form } });
      if (r && r.suggestion) setForm((f) => ({ ...f, [k]: r.suggestion }));
    } catch (e) { setErr(e.message); } finally { setSuggesting(null); }
  }
  // Small "✨ Suggest" affordance per field — fills it with an AI suggestion (grounded in the
  // rest of the form) so a teacher who isn't sure what to type gets a concrete starting point.
  const suggestBtn = (k) => (
    <button type="button" className="btn btn-outline btn-tiny" style={{ marginInlineStart: 8 }}
      disabled={suggesting !== null} onClick={() => suggest(k)} title={tt("Let AI suggest what to type here")}>
      {suggesting === k ? tt("Suggesting…") : "✨ " + tt("Suggest")}
    </button>
  );
  async function design() {
    setBusy(true); setErr(null);
    try { setResult(await api("/benches", { method: "POST", body: form })); }
    catch (e) { setErr(e.message); } finally { setBusy(false); }
  }
  const d = result && result.design;
  return (
    <div className="modal-overlay" onClick={onClose}>
      <Card className="modal modal--wide">
        <div onClick={(e) => e.stopPropagation()}>
          <div className="between" style={{ alignItems: "center" }}>
            <H><T>Design a custom bench</T></H>
            <button className="btn btn-muted" onClick={onClose}><T>Close</T></button>
          </div>
          {!result && <>
            <div className="helptext"><T>Describe your scenario and what learners should be able to do. The strongest model turns it into an interactive, auto-graded microworld — no coding. It stays private to you until you publish it to other teachers.</T> <span className="t-faint"><T>Not sure what to type? Tap ✨ Suggest on any field.</T></span></div>
            <Sub><T>Scenario</T> {suggestBtn("scenario")}</Sub>
            <textarea className="field" rows={3} value={form.scenario} placeholder={tt("e.g. A lemonade stand: learners set the price per cup and how many cups they make, then see revenue, cost and profit — and must reach a target profit.")} onChange={set("scenario")} />
            <div className="row" style={{ gap: 8, alignItems: "center" }}>
              <input className="field" style={{ flex: 1 }} value={form.label} placeholder={tt("Bench name (optional)")} onChange={set("label")} />
              {suggestBtn("label")}
              <input className="field" style={{ flex: 1 }} value={form.field} placeholder={tt("STEM field (optional), e.g. math.economics")} onChange={set("field")} />
              {suggestBtn("field")}
            </div>
            <Sub><T>Variables the learner can change</T> {suggestBtn("variables")}</Sub>
            <textarea className="field" rows={2} value={form.variables} placeholder={tt("e.g. price per cup (0–5), cups made (0–100), cost per cup (0–2)")} onChange={set("variables")} />
            <Sub><T>Target outcomes</T> {suggestBtn("outcomes")}</Sub>
            <textarea className="field" rows={2} value={form.outcomes} placeholder={tt("e.g. reach a profit ≥ $50 with no unsold cups")} onChange={set("outcomes")} />
            <Sub><T>Example tasks</T> <span className="dim">· <T>optional</T></span> {suggestBtn("tasks")}</Sub>
            <textarea className="field" rows={2} value={form.tasks} placeholder={tt("e.g. find the break-even price; then maximize profit")} onChange={set("tasks")} />
            {err && <div className="t-amber" style={{ marginTop: 8 }}>⚠ {err}</div>}
            <div className="row" style={{ gap: 8, marginTop: 10 }}>
              <button className="btn btn-primary" disabled={busy || !form.scenario.trim()} onClick={design}>{busy ? <T>Designing…</T> : <T>Design bench</T>}</button>
            </div>
            {busy && <div className="muted-note" style={{ marginTop: 8, fontSize: 13 }}><T>The strongest model is designing your microworld — this can take ~20s.</T></div>}
          </>}
          {result && d && <>
            <div className="kv"><b>{d.label}</b> <Pill color={C.blue}>{d.field}</Pill></div>
            <div className="lab__summary" style={{ marginTop: 4 }}>{d.microworld}</div>
            <H><T>Variables</T> <span className="dim">· {d.params.length}</span></H>
            <ul className="study__list">{d.params.map((p) => <li key={p.key}><code>{p.key}</code> <span className="t-faint">({p.label}{p.unit ? ` · ${p.unit}` : ""})</span></li>)}</ul>
            <H><T>Graded state</T> <span className="dim">· {d.fields.length}</span></H>
            <ul className="study__list">{d.fields.map((f) => <li key={f.name}><code>{f.name}</code> <span className="t-faint">({f.type})</span> — {f.desc}</li>)}</ul>
            <H><T>Sample tasks</T></H>
            <ul className="study__list">{d.tasks.map((t) => <li key={t.id}><b>{t.title}</b> — {t.goal}</li>)}</ul>
            <div className="row" style={{ gap: 8, marginTop: 10 }}>
              <button className="btn btn-primary" onClick={() => onCreated(result.bench)}><T>Use this bench</T></button>
              <button className="btn btn-muted" onClick={() => setResult(null)}><T>Tweak &amp; redesign</T></button>
            </div>
            <div className="muted-note" style={{ marginTop: 6, fontSize: 12 }}><T>Saved as a private draft. Select it to author tutors; publish it to share with other teachers.</T></div>
          </>}
        </div>
      </Card>
    </div>
  );
}

/* ===================== admin ===================== */
// Admin-only meta-authoring: design a whole BENCH (the gradeable microworld tutors are
// built on) with the strongest model. Returns a design + code scaffold for review — the
// generated bench is NEVER hot-loaded; a developer reviews the scaffold and commits it.
function BenchWizard() {
  useI18nVersion();
  const [brief, setBrief] = useState("");
  const [field, setField] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  const [draft, setDraft] = useState(null);
  const [tab, setTab] = useState("server");
  async function generate() {
    setBusy(true); setErr(null); setDraft(null);
    try { setDraft(await api("/admin/benches/design", { method: "POST", body: { brief, field } })); }
    catch (e) { setErr(e.message); }
    finally { setBusy(false); }
  }
  function download(name, text) {
    const a = document.createElement("a");
    a.href = URL.createObjectURL(new Blob([text], { type: "text/plain" }));
    a.download = name; a.click(); URL.revokeObjectURL(a.href);
  }
  const d = draft && draft.design;
  const LIT = d ? [["small state · broad coverage", d.litmus.smallStateBroadCoverage], ["observable is the concept", d.litmus.observableIsConcept], ["causal transparency", d.litmus.causalTransparency], ["misconception surfaces", d.litmus.misconceptionSurfaces], ["generative", d.litmus.generative]] : [];
  return (
    <Card className="card-mb">
      <Sub><T>Bench Wizard</T> <span className="dim">· <T>design a new gradeable bench with the strongest model</T></span></Sub>
      <div className="helptext"><T>Describe a STEM concept or domain. The wizard designs the fixed microworld, its state fields, tasks and catalog, plus a code scaffold to review. The generated bench is a reviewable draft — a developer commits it; it is never auto-deployed.</T></div>
      <textarea className="field" rows={3} value={brief} placeholder="e.g. Thin-lens optics: object/focal distance → image distance & magnification" onChange={(e) => setBrief(e.target.value)} />
      <div className="row" style={{ gap: 8 }}>
        <input className="field" style={{ flex: 1 }} value={field} placeholder="STEM field (optional), e.g. physics.optics" onChange={(e) => setField(e.target.value)} />
        <button className="btn btn-primary" disabled={busy || !brief.trim()} onClick={generate}>{busy ? <T>Designing…</T> : <T>Design bench</T>}</button>
      </div>
      {err && <div className="t-amber" style={{ marginTop: 8 }}>⚠ {err}</div>}
      {d && (
        <div style={{ marginTop: 14 }}>
          <div className="kv"><b>{d.label}</b> <Pill color={C.blue}>{d.field}</Pill> <span className="t-faint">· {d.id}</span> <Pill color={draft.model === "gateway-default" ? C.amber : C.teal}>{draft.model}</Pill></div>
          <div className="lab__summary" style={{ marginTop: 4 }}>{d.microworld}</div>
          <div className="row" style={{ gap: 6, flexWrap: "wrap", marginTop: 8 }}>
            {LIT.map(([k, v]) => <Pill key={k} color={v ? C.teal : C.crimson}>{v ? "✓" : "✗"} {k}</Pill>)}
          </div>
          <H><T>State fields</T> <span className="dim">· {d.fields.length}</span></H>
          <ul className="study__list">{d.fields.map((f) => <li key={f.name}><code>{f.name}</code> <span className="t-faint">({f.type}{f.type === "number" && (f.min != null || f.max != null) ? ` ${f.min ?? "?"}..${f.max ?? "?"}` : ""})</span> — {f.desc}</li>)}</ul>
          <H><T>Sample tasks</T></H>
          <ul className="study__list">{d.tasks.map((t) => <li key={t.id}><b>{t.title}</b> — {t.goal}</li>)}</ul>
          <H><T>Scaffold</T> <span className="dim">· <T>review before committing</T></span></H>
          <div className="row" style={{ gap: 8 }}>
            <button className={`btn ${tab === "server" ? "btn-secondary" : "btn-muted"}`} onClick={() => setTab("server")}>server/benches/{d.id}.js</button>
            <button className={`btn ${tab === "client" ? "btn-secondary" : "btn-muted"}`} onClick={() => setTab("client")}>{d.id} component</button>
            <button className="btn btn-muted" onClick={() => download(tab === "server" ? `${d.id}.js` : `${d.id}-lab.jsx`, tab === "server" ? draft.scaffold.serverModule : draft.scaffold.componentStub)}><T>Download</T></button>
          </div>
          <pre style={{ maxHeight: 320, overflow: "auto", marginTop: 8, background: "var(--bg)", border: "1px solid var(--line)", borderRadius: 6, padding: 12, fontSize: 12, fontFamily: "monospace", whiteSpace: "pre", color: "var(--ink)" }}>{tab === "server" ? draft.scaffold.serverModule : draft.scaffold.componentStub}</pre>
        </div>
      )}
    </Card>
  );
}

function AdminConsole({ user, subject = "electronics", home = false }) {
  useI18nVersion();
  const [ov, setOv] = useState(null);
  const [users, setUsers] = useState([]);
  const [tutors, setTutors] = useState([]);
  const [preview, setPreview] = useState(null);
  const [roster, setRoster] = useState(null);
  const [safeguardId, setSafeguardId] = useState(null);
  const reloadTutors = useCallback(() => api(`/tutors`).then(({ tutors }) => setTutors(tutors)), []);
  useEffect(() => {
    api("/admin/overview").then(({ counts, runtime }) => setOv({ counts, runtime }));
    api("/admin/users").then(({ users }) => setUsers(users));
    reloadTutors(); // every unit, across subjects
  }, [reloadTutors]);
  async function openPreview(id) { const { tutor } = await api(`/tutors/${id}`); setPreview(tutor); }
  async function openRoster(t) { setRoster({ tutor: t, rows: await loadRosterRows(t.id) }); }
  // Admin reassigns a user's role. Confirm before granting admin (privilege escalation); on success
  // update the row in place. Server enforces the real guardrails (self-change, configured admins).
  async function changeRole(u, nextRole) {
    if (nextRole === u.role) return;
    if (nextRole === "admin" && !window.confirm(tt("Grant admin? This user will get full platform access."))) return;
    try {
      const { user: updated } = await api(`/admin/users/${u.id}/role`, { method: "PUT", body: { role: nextRole } });
      setUsers((list) => list.map((x) => (x.id === updated.id ? { ...x, role: updated.role } : x)));
    } catch (e) { alert(e.message || tt("Could not change role")); }
  }
  const fmtDateTime = (iso) => { if (!iso) return ""; try { return new Date(iso).toLocaleString([], { year: "2-digit", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); } catch (_) { return ""; } };
  // Most-recently active users first; those who never signed in sort to the bottom.
  const sortedUsers = [...users].sort((a, b) => (b.lastLoginAt || "").localeCompare(a.lastLoginAt || ""));
  if (preview) return <TutorPlayer base={preview} role="admin" onExit={() => setPreview(null)} />;
  return (
    <div className="page">
      <H><T>System overview</T></H>
      {ov && (
        <div className="stat-grid">
          <Card><Gauge label="TUTORS" value={ov.counts.tutors} suffix="" color={C.teal} /></Card>
          <Card><Gauge label="PUBLISHED" value={ov.counts.published} suffix="" color={C.blue} /></Card>
          <Card><Gauge label="USERS" value={ov.counts.users} suffix="" color={C.amber} /></Card>
          <Card><Gauge label="ATTEMPTS" value={ov.counts.attempts} suffix="" color={C.gold} /></Card>
          <Card className="span2">
            <Sub><T>Runtime</T></Sub>
            <div className="kv"><T>persistence:</T> <span className="t-blue">{ov.runtime.persistence}</span></div>
            <div className="kv"><T>llm:</T> <span className="t-blue">{ov.runtime.llm}</span></div>
          </Card>
        </div>
      )}
      <H><T>Bench authoring</T></H>
      <BenchWizard />
      <H><T>Users</T> <span className="dim">· {users.length}</span></H>
      <Card className="card-mb card--flush">
        {sortedUsers.map((u) => {
          const roleColor = u.role === "admin" ? C.crimson : u.role === "educator" ? C.blue : C.teal;
          return (
            <div key={u.id} className="user-row">
              <span>{u.name} <span className="t-faint">· {u.email} · {u.lastLoginAt ? <><T>last seen</T> {fmtDateTime(u.lastLoginAt)}</> : <T>never signed in</T>}</span></span>
              {u.id === user.id ? (
                <Pill color={roleColor}>{u.role} · <T>you</T></Pill>
              ) : (
                <select value={u.role} onChange={(e) => changeRole(u, e.target.value)} aria-label={tt("Change role")}
                  style={{ background: C.panel2, color: roleColor, border: `1px solid ${C.line}`, borderRadius: 6, padding: "4px 10px", font: "inherit", fontWeight: 600, cursor: "pointer" }}>
                  <option value="learner">learner</option>
                  <option value="educator">educator</option>
                  <option value="admin">admin</option>
                </select>
              )}
            </div>
          );
        })}
      </Card>
      <H><T>All tutors</T> <span className="dim">· {tutors.length}</span></H>
      {tutors.map((t) => (
        <TutorRow key={t.id} t={t}>
          {t.hasCode && <code className="access-code" title={tt("Access code")}>{t.accessCode || "•••"}</code>}
          <button onClick={() => setSafeguardId(t.id)} className="btn btn-secondary">🛡 <T>Safeguard</T></button>
          <button onClick={() => openPreview(t.id)} className="btn btn-secondary"><T>Preview</T></button>
          <button onClick={() => openRoster(t)} className="btn btn-secondary"><T>Roster</T></button>
        </TutorRow>
      ))}
      {roster && <RosterModal tutor={roster.tutor} rows={roster.rows} onClose={() => setRoster(null)} />}
      {safeguardId && <SafeguardModal tutorId={safeguardId} onClose={() => setSafeguardId(null)} onChange={reloadTutors} />}
    </div>
  );
}

/* ===================== shareable deep links ===================== */
// A share link is /t/{tutorId}[?c=CODE]. We read it from the URL; if the visitor has to sign in first
// (esp. an OAuth round-trip that loses the path) we stash it in sessionStorage and recover it on return.
function readDeepLink() {
  const m = window.location.pathname.match(/^\/t\/([A-Za-z0-9_-]{3,})\/?$/);
  if (m) {
    const sp = new URLSearchParams(window.location.search);
    return { tutorId: m[1], code: sp.get("c") || sp.get("code") || null, fromUrl: true };
  }
  try { const s = sessionStorage.getItem("etutor:deeplink"); if (s) { const o = JSON.parse(s); if (o && o.tutorId) return { tutorId: o.tutorId, code: o.code || null, fromUrl: false }; } } catch (_) { /* private mode */ }
  return null;
}

// Opens one tutor directly from a share link. If the link carries a join code, redeem it first so a
// code-gated bench becomes accessible, then render the player (honoring the teacher's "read me first"
// note the same way the student dashboard does). On exit, go home.
function TutorDeepLink({ link, user, role, onExit }) {
  useI18nVersion();
  const [st, setSt] = useState({ phase: "loading" }); // loading | gate | play | error
  useEffect(() => {
    let alive = true;
    // Recovered from an OAuth round-trip (URL is "/")? Canonicalize to /t/{id} so any re-render keeps
    // resolving to this same link, and drop the one-shot stash.
    if (!link.fromUrl) { try { window.history.replaceState({}, "", `/t/${link.tutorId}${link.code ? `?c=${encodeURIComponent(link.code)}` : ""}`); } catch (_) { /* noop */ } }
    try { sessionStorage.removeItem("etutor:deeplink"); } catch (_) { /* consumed */ }
    (async () => {
      try {
        // A join token in the link → enroll first (learners/admins). Failures are non-fatal: a public
        // unit, or one already joined, still opens via the fetch below.
        if (link.code && (role === "learner" || role === "admin")) { try { await api(`/enroll`, { method: "POST", body: { code: link.code } }); } catch (_) { /* fall through */ } }
        const { tutor } = await api(`/tutors/${link.tutorId}`);
        if (!alive) return;
        let seen = false; try { seen = !!localStorage.getItem(`etutor:readme-seen:${user.id}:${link.tutorId}`); } catch (_) { /* private mode */ }
        if (role === "learner" && tutor.readme && tutor.readme.text && !seen) setSt({ phase: "gate", tutor });
        else setSt({ phase: "play", tutor });
      } catch (e) { if (alive) setSt({ phase: "error", error: e.message }); }
    })();
    return () => { alive = false; };
  }, [link.tutorId, link.code]);

  if (st.phase === "loading") return <div className="loading"><T>Opening…</T></div>;
  if (st.phase === "error") return (
    <div className="page"><Card className="card-mb">
      <H><T>Can’t open this bench</T></H>
      <div className="helptext">{st.error === "forbidden" ? tt("This bench isn’t shared with you, or its link has changed. Ask your teacher for a fresh link or code.") : st.error}</div>
      <button className="btn btn-primary" onClick={onExit}><T>Go to my dashboard</T></button>
    </Card></div>
  );
  if (st.phase === "gate") return <ReadmeGate tutor={st.tutor} timed onStart={() => { try { localStorage.setItem(`etutor:readme-seen:${user.id}:${link.tutorId}`, String(Date.now())); } catch (_) { /* private mode */ } setSt({ phase: "play", tutor: st.tutor }); }} onClose={onExit} />;
  return <TutorPlayer base={st.tutor} role="learner" onExit={onExit} />;
}

/* ===================== root ===================== */
function App() {
  useI18nVersion();
  const [user, setUser] = useState(undefined); // undefined = loading
  const [provider, setProvider] = useState(null);
  const [config, setConfig] = useState(null);
  const [authError, setAuthError] = useState(() => new URLSearchParams(window.location.search).get("authError"));
  useEffect(() => {
    // strip authError/token from the URL after reading it
    if (window.history.replaceState && (window.location.search.includes("authError") || window.location.search.includes("token"))) {
      window.history.replaceState({}, "", window.location.pathname);
    }
    api("/auth/config").then(setConfig).catch(() => setConfig({ oauth: false, devLogin: true }));
    api("/auth/me").then(({ user, provider }) => { setUser(user); setProvider(provider); }).catch(() => setUser(null));
  }, []);
  const [viewAs, setViewAs] = useState(null); // admin only: which role's view to render
  async function logout() { await api("/auth/logout", { method: "POST" }); setUser(null); setProvider(null); setViewAs(null); }
  if (user === undefined || config === null) return <div className="loading"><T>Loading…</T></div>;
  // A shareable tutor link (/t/{id}) opens that bench directly. Stash it before sign-in so it survives
  // an OAuth round-trip, then resume into it once the visitor is authenticated.
  const deepLink = readDeepLink();
  if (!user) {
    if (deepLink && deepLink.fromUrl) { try { sessionStorage.setItem("etutor:deeplink", JSON.stringify({ tutorId: deepLink.tutorId, code: deepLink.code })); } catch (_) { /* private mode */ } }
    return <Landing config={config} authError={authError} onLogin={setUser} />;
  }
  // Path → subject (one entry per registered bench); anything else → subject picker.
  const path = window.location.pathname;
  const SUBJECT_PATHS = ["electronics", "statistics", "optics", "anova", "scatter", "geometry", "advgeometry", "advstats", "advalgebra", "gaslaws", "titration", "equilibrium", "punnett", "population", "hardyweinberg", "kepler", "halflife", "seasons", "logicgates", "bigo", "binary", "kinematics", "projectile", "forces", "momentum", "energy", "springs", "pendulum", "waves", "calorimetry", "coulomb", "refraction", "density", "advelectronics", "linear", "quadratic", "systems", "exponential", "trig", "unitcircle", "trigwave", "pythagorean", "vectors", "limits", "secant", "derivative", "integral", "relatedrates", "optimization", "mvt", "accumulation", "areabetween", "slopefield", "taylor", "series", "partials", "doubleintegral", "curvesketch", "lhopital", "implicit", "linearization", "newton", "avgvalue", "volumerev", "arclength", "improper", "parametric", "polar", "powerseries", "euler", "gradient", "tangentplane", "criticalpoints", "lagrange", "tripleintegral", "lineintegral", "vectorfield", "probability", "stoichiometry", "molarity", "kinetics", "thermochem", "periodictrends", "organicchem", "enzymes", "predatorprey", "dihybrid", "photosynthesis", "osmosis", "moonphases", "platetectonics", "greenhouse", "stellar", "hubble", "sorting", "recursion", "cryptography", "networking"];
  // Opaque /b/{code} resolves to the internal bench id; a legacy readable /{id} path still works.
  const subject = benchIdFromPath(path) || SUBJECT_PATHS.find((s) => path.indexOf(`/${s}`) === 0) || null;
  const wantBrowse = path.indexOf("/browse") === 0; // the raw-bench catalog, reachable from the dashboard
  // Admins can use the app as a teacher or student; everyone else is fixed to their role.
  const isAdmin = user.role === "admin";
  const effectiveRole = isAdmin ? (viewAs || "admin") : user.role;
  const Dash = effectiveRole === "admin" ? AdminConsole : effectiveRole === "educator" ? TeacherDashboard : StudentDashboard;
  // A share link (/t/{id}) opens that one bench full-screen; exiting returns to the dashboard.
  const exitDeepLink = () => { try { sessionStorage.removeItem("etutor:deeplink"); } catch (_) { /* noop */ } window.location.assign("/"); };
  // Home (no subject) is the role DASHBOARD; a subject path scopes it (teacher authoring / deep link);
  // /browse opens the raw-bench catalog.
  return (
    <div className="app">
      <TopBar user={user} provider={provider} subject={subject} viewAs={isAdmin ? effectiveRole : null} onViewAs={isAdmin ? setViewAs : null} onLogout={logout} />
      {deepLink
        ? <TutorDeepLink link={deepLink} user={user} role={effectiveRole === "admin" ? "admin" : effectiveRole === "educator" ? "educator" : "learner"} onExit={exitDeepLink} />
        : wantBrowse
        ? <SubjectPicker />
        : <Dash key={effectiveRole + (subject || "home")} user={user} subject={subject || "electronics"} home={!subject} />}
      <footer className="app-footnote"><T>STEMBENCH is created by Xiangen Hu</T> (<a href="mailto:xiangenhu@gmail.com" className="link">xiangenhu@gmail.com</a>)</footer>
    </div>
  );
}

// Each bench's STEM `cat` (category) mirrors server registry areaOf(field) — the catalog
// groups benches into Mathematics / Physics / … (Chemistry, Biology, CS reserved for future).
const SUBJECTS = [
  { id: "electronics", label: "Electronics", cat: "Physics", desc: "Build a series DC circuit — Ohm’s law, LEDs, current limiting.", color: C.teal, href: "/electronics" },
  { id: "advelectronics", label: "Advanced Electronics", cat: "Physics", desc: "Turn the knobs on six circuit laws — Ohm, KVL & KCL, the RC clock, a low-pass corner and series RLC resonance.", color: C.blue, href: "/advelectronics" },
  { id: "optics", label: "Optics", cat: "Physics", desc: "Drag an object on a lens bench — real/virtual images, magnification.", color: C.amber, href: "/optics" },
  { id: "kinematics", label: "Kinematics", cat: "Physics", desc: "Drive a track — v = v₀+at, displacement and the v–t graph.", color: C.blue, href: "/kinematics" },
  { id: "projectile", label: "Projectile Motion", cat: "Physics", desc: "Launch angle & speed — range, max height, time of flight.", color: C.crimson, href: "/projectile" },
  { id: "forces", label: "Forces & Friction", cat: "Physics", desc: "Block on an incline — F = ma, normal force & friction.", color: C.gold, href: "/forces" },
  { id: "momentum", label: "Momentum", cat: "Physics", desc: "Collide two carts — p = mv, elastic vs inelastic, KE lost.", color: C.teal, href: "/momentum" },
  { id: "energy", label: "Energy", cat: "Physics", desc: "Cart on a hill — KE = ½mv², PE = mgh and conservation.", color: C.amber, href: "/energy" },
  { id: "springs", label: "Springs & SHM", cat: "Physics", desc: "Mass on a spring — F = −kx, elastic PE and the period.", color: C.blue, href: "/springs" },
  { id: "pendulum", label: "Pendulum", cat: "Physics", desc: "Length & gravity — T = 2π√(L/g), frequency, max speed.", color: C.crimson, href: "/pendulum" },
  { id: "waves", label: "Waves", cat: "Physics", desc: "Frequency & wavelength — v = fλ, period and wave number.", color: C.gold, href: "/waves" },
  { id: "calorimetry", label: "Calorimetry", cat: "Physics", desc: "Mix hot & cold water — Q = mcΔT and thermal equilibrium.", color: C.teal, href: "/calorimetry" },
  { id: "coulomb", label: "Coulomb's Law", cat: "Physics", desc: "Two charges — F = kq₁q₂/r², attraction vs repulsion.", color: C.amber, href: "/coulomb" },
  { id: "refraction", label: "Refraction", cat: "Physics", desc: "Light at a boundary — Snell's law, critical angle, TIR.", color: C.blue, href: "/refraction" },
  { id: "density", label: "Density & Buoyancy", cat: "Physics", desc: "Object in a fluid — ρ = m/V, float vs sink, buoyant force.", color: C.crimson, href: "/density" },
  { id: "statistics", label: "Statistics", cat: "Mathematics", desc: "Manipulate distributions, sampling & hypothesis tests.", color: C.blue, href: "/statistics" },
  { id: "anova", label: "ANOVA", cat: "Mathematics", desc: "Drag group means — between/within variance, the F-ratio & p-value.", color: C.crimson, href: "/anova" },
  { id: "scatter", label: "Regression", cat: "Mathematics", desc: "Drag points on a scatterplot — correlation, the least-squares line & R².", color: C.gold, href: "/scatter" },
  { id: "geometry", label: "Geometry", cat: "Mathematics", desc: "Drag a point or slider — inscribed angles, power of a point, the bisector ratio.", color: C.teal, href: "/geometry" },
  { id: "advgeometry", label: "Advanced Geometry", cat: "Mathematics", desc: "Drag a triangle's vertices — the circumcircle, the incircle, and the Euler line & nine-point circle.", color: C.blue, href: "/advgeometry" },
  { id: "advstats", label: "Advanced Statistics", cat: "Mathematics", desc: "Tune a distribution — the normal curve as area, the binomial's normal approximation, and the t & F distributions.", color: C.crimson, href: "/advstats" },
  { id: "advalgebra", label: "Advanced Algebra", cat: "Mathematics", desc: "Tune six algebra structures — polynomial roots & Vieta, rational asymptotes, logarithms, the ellipse, geometric series and complex numbers.", color: C.teal, href: "/advalgebra" },
  { id: "linear", label: "Linear Functions", cat: "Mathematics", desc: "Drag a line — slope, intercepts and y = mx + b.", color: C.gold, href: "/linear" },
  { id: "quadratic", label: "Quadratics", cat: "Mathematics", desc: "Tune a, b, c — discriminant, vertex and the roots of a parabola.", color: C.crimson, href: "/quadratic" },
  { id: "systems", label: "Systems of Equations", cat: "Mathematics", desc: "Two lines — one, none or infinite solutions at the intersection.", color: C.blue, href: "/systems" },
  { id: "exponential", label: "Exponentials", cat: "Mathematics", desc: "y = a·bˣ — growth vs decay, percent rate and doubling time.", color: C.teal, href: "/exponential" },
  { id: "trig", label: "Right-Triangle Trig", cat: "Mathematics", desc: "Angle & hypotenuse — sine, cosine, tangent and SOH-CAH-TOA.", color: C.amber, href: "/trig" },
  { id: "unitcircle", label: "Unit Circle", cat: "Mathematics", desc: "Sweep an angle — (cosθ, sinθ), quadrants and radians.", color: C.gold, href: "/unitcircle" },
  { id: "trigwave", label: "Sinusoids", cat: "Mathematics", desc: "y = A sin(Bx+C)+D — amplitude, period, phase and midline.", color: C.crimson, href: "/trigwave" },
  { id: "pythagorean", label: "Pythagorean Theorem", cat: "Mathematics", desc: "Two legs — a² + b² = c², triples, area and perimeter.", color: C.blue, href: "/pythagorean" },
  { id: "vectors", label: "Vectors", cat: "Mathematics", desc: "Add two vectors — resultant, magnitude, direction and dot product.", color: C.teal, href: "/vectors" },
  // ── Calculus strand (its own landing tab) — Calc I differential, Calc II integral & series, ODE, Calc III multivariable ──
  // Calc I — limits & differential calculus
  { id: "limits", label: "Limits & Continuity", cat: "Calculus", desc: "Tune a jump and a hole — one-sided limits, when the limit exists, continuity at c.", color: C.crimson, href: "/limits" },
  { id: "secant", label: "Definition of the Derivative", cat: "Calculus", desc: "Shrink the step h — the secant slope becomes the tangent f′(x₀) as h→0.", color: C.teal, href: "/secant" },
  { id: "derivative", label: "Derivatives", cat: "Calculus", desc: "Drag a tangent — slope f′(x), critical points and concavity.", color: C.amber, href: "/derivative" },
  { id: "curvesketch", label: "Curve Analysis", cat: "Calculus", desc: "f′ and f″ signs — increasing/decreasing, concavity, critical points & inflection.", color: C.gold, href: "/curvesketch" },
  { id: "lhopital", label: "L'Hôpital's Rule", cat: "Calculus", desc: "The 0/0 form sin(ax)/(bx) — resolve the limit with the ratio of derivatives.", color: C.blue, href: "/lhopital" },
  { id: "implicit", label: "Implicit Differentiation", cat: "Calculus", desc: "Differentiate an ellipse implicitly — dy/dx = −(B²x)/(A²y), vertical & horizontal tangents.", color: C.teal, href: "/implicit" },
  { id: "relatedrates", label: "Related Rates", cat: "Calculus", desc: "A sliding ladder — implicit differentiation in time, dy/dt = −(x/y)·dx/dt.", color: C.blue, href: "/relatedrates" },
  { id: "linearization", label: "Linear Approximation", cat: "Calculus", desc: "The tangent line L(x) = f(a)+f′(a)(x−a) — local linear approximation and its error.", color: C.amber, href: "/linearization" },
  { id: "newton", label: "Newton's Method", cat: "Calculus", desc: "Tangent-line steps xₙ₊₁ = xₙ − f/f′ converging quadratically on a root.", color: C.crimson, href: "/newton" },
  { id: "optimization", label: "Optimization", cat: "Calculus", desc: "Fold a box from a sheet — maximize volume where V′(c) = 0.", color: C.teal, href: "/optimization" },
  { id: "mvt", label: "Mean Value Theorem", cat: "Calculus", desc: "Place c on [a,b] so the tangent f′(c) is parallel to the secant.", color: C.amber, href: "/mvt" },
  // Calc II — integral calculus & series
  { id: "integral", label: "Definite Integrals", cat: "Calculus", desc: "Riemann rectangles — area under a curve and the limit n → ∞.", color: C.gold, href: "/integral" },
  { id: "accumulation", label: "Accumulation (FTC)", cat: "Calculus", desc: "F(x) = ∫₀ˣ f — signed area, F′(x) = f(x), and where F turns.", color: C.gold, href: "/accumulation" },
  { id: "avgvalue", label: "Average Value", cat: "Calculus", desc: "Average value f̄ = (1/(b−a))∫f — the equal-area rectangle and the MVT point c.", color: C.blue, href: "/avgvalue" },
  { id: "areabetween", label: "Area Between Curves", cat: "Calculus", desc: "A line vs a parabola — find the bounds, integrate top − bottom.", color: C.crimson, href: "/areabetween" },
  { id: "volumerev", label: "Volumes of Revolution", cat: "Calculus", desc: "Revolve a region about an axis — the disk method V = π∫f² as a stack of disks.", color: C.teal, href: "/volumerev" },
  { id: "arclength", label: "Arc Length", cat: "Calculus", desc: "Arc length L = ∫√(1+(f′)²) — an inscribed polygon converging up to the curve.", color: C.amber, href: "/arclength" },
  { id: "improper", label: "Improper Integrals", cat: "Calculus", desc: "∫₁^∞ x⁻ᵖ — partial areas, the p>1 convergence threshold, divergence for p≤1.", color: C.gold, href: "/improper" },
  { id: "parametric", label: "Parametric Curves", cat: "Calculus", desc: "x = A cos t, y = sin(mt) — velocity, the tangent slope dy/dx and speed.", color: C.blue, href: "/parametric" },
  { id: "polar", label: "Polar Area", cat: "Calculus", desc: "Polar area A = ½∫r²dθ of a cardioid — swept sectors converging to the integral.", color: C.crimson, href: "/polar" },
  { id: "series", label: "Series Convergence", cat: "Calculus", desc: "A geometric series — the ratio test, partial sums and the limit.", color: C.amber, href: "/series" },
  { id: "powerseries", label: "Power Series", cat: "Calculus", desc: "Σ(x/R)ⁿ — the ratio test, the radius of convergence and partial sums.", color: C.teal, href: "/powerseries" },
  { id: "taylor", label: "Taylor / Maclaurin", cat: "Calculus", desc: "Approximate eˣ — add terms to shrink the error and the Lagrange bound.", color: C.teal, href: "/taylor" },
  // Differential equations
  { id: "slopefield", label: "Slope Fields & ODEs", cat: "Calculus", desc: "dy/dx = k·y — the slope field, y = y₀eᵏˣ, growth, decay and doubling.", color: C.blue, href: "/slopefield" },
  { id: "euler", label: "Euler's Method", cat: "Calculus", desc: "Step along the slope field — the Euler staircase converges to y₀eˣ as h→0.", color: C.gold, href: "/euler" },
  // Calc III — multivariable calculus
  { id: "partials", label: "Partial Derivatives", cat: "Calculus", desc: "A surface — ∂z/∂x, ∂z/∂y, the gradient and critical points (Calc III).", color: C.gold, href: "/partials" },
  { id: "gradient", label: "Gradient & Directional Derivative", cat: "Calculus", desc: "∇f and Dᵤf = ∇f·u — steepest-ascent vs level directions on a contour map.", color: C.teal, href: "/gradient" },
  { id: "tangentplane", label: "Tangent Plane", cat: "Calculus", desc: "Linearize f(x,y) = x²+y² — the tangent plane in 2 variables and its error.", color: C.amber, href: "/tangentplane" },
  { id: "criticalpoints", label: "Critical Points (2-D)", cat: "Calculus", desc: "Critical points of f(x,y) — the discriminant test for min, max & saddle.", color: C.crimson, href: "/criticalpoints" },
  { id: "lagrange", label: "Lagrange Multipliers", cat: "Calculus", desc: "Maximize f = xy on a circle — the optimum where ∇f = λ∇g.", color: C.blue, href: "/lagrange" },
  { id: "doubleintegral", label: "Double Integrals", cat: "Calculus", desc: "Volume under a surface — Riemann boxes over a region converge to ∬ (Calc III).", color: C.crimson, href: "/doubleintegral" },
  { id: "tripleintegral", label: "Triple Integrals", cat: "Calculus", desc: "∭(x²+y²+z²)dV over a box — a 3-D Riemann sum converging to the integral.", color: C.gold, href: "/tripleintegral" },
  { id: "lineintegral", label: "Line Integrals", cat: "Calculus", desc: "Work ∫_C F·dr along an arc — a Riemann sum converging to the line integral.", color: C.teal, href: "/lineintegral" },
  { id: "vectorfield", label: "Vector Fields", cat: "Calculus", desc: "A vector field's divergence (source) and curl (rotation) from two knobs.", color: C.blue, href: "/vectorfield" },
  { id: "probability", label: "Normal Distribution", cat: "Mathematics", desc: "Bell curve — z-scores, tail probabilities and percentiles.", color: C.blue, href: "/probability" },
  { id: "gaslaws", label: "Gas Laws", cat: "Chemistry", desc: "Tune a gas piston — PV = nRT, Boyle’s & Gay-Lussac’s laws.", color: C.blue, href: "/gaslaws" },
  { id: "titration", label: "Titration & pH", cat: "Chemistry", desc: "Add base to an acid — the pH curve and the equivalence point.", color: C.crimson, href: "/titration" },
  { id: "equilibrium", label: "Equilibrium", cat: "Chemistry", desc: "Set concentrations — the reaction quotient Q vs. the constant K.", color: C.gold, href: "/equilibrium" },
  { id: "stoichiometry", label: "Stoichiometry", cat: "Chemistry", desc: "2A + B → 2C — limiting reactant, theoretical & percent yield.", color: C.teal, href: "/stoichiometry" },
  { id: "molarity", label: "Molarity & Dilution", cat: "Chemistry", desc: "Dilute a stock — M = mol/L and C₁V₁ = C₂V₂.", color: C.blue, href: "/molarity" },
  { id: "kinetics", label: "Reaction Kinetics", cat: "Chemistry", desc: "Rate = k[A]ⁿ — order, rate constant and half-life.", color: C.crimson, href: "/kinetics" },
  { id: "thermochem", label: "Thermochemistry", cat: "Chemistry", desc: "ΔH from formation enthalpies — exo vs endothermic.", color: C.amber, href: "/thermochem" },
  { id: "periodictrends", label: "Periodic Trends", cat: "Chemistry", desc: "Pick period & group — Zeff, atomic radius, ionization energy.", color: C.gold, href: "/periodictrends" },
  { id: "organicchem", label: "Organic Chemistry", cat: "Chemistry", desc: "Build a molecule — formula, degree of unsaturation, functional groups & boiling point.", color: C.teal, href: "/organicchem" },
  { id: "punnett", label: "Punnett Square", cat: "Biology", desc: "Cross two genotypes — Mendelian genotype & phenotype ratios.", color: C.teal, href: "/punnett" },
  { id: "population", label: "Population Growth", cat: "Biology", desc: "Logistic growth — r, carrying capacity K and dN/dt.", color: C.amber, href: "/population" },
  { id: "hardyweinberg", label: "Hardy–Weinberg", cat: "Biology", desc: "Allele frequency p — genotype frequencies p², 2pq, q².", color: C.blue, href: "/hardyweinberg" },
  { id: "enzymes", label: "Enzyme Kinetics", cat: "Biology", desc: "Michaelis–Menten — v = Vmax[S]/(Km+[S]), saturation.", color: C.crimson, href: "/enzymes" },
  { id: "predatorprey", label: "Predator–Prey", cat: "Biology", desc: "Lotka–Volterra — coupled growth rates and equilibrium.", color: C.gold, href: "/predatorprey" },
  { id: "dihybrid", label: "Dihybrid Cross", cat: "Biology", desc: "Two genes — independent assortment and the 9:3:3:1 ratio.", color: C.teal, href: "/dihybrid" },
  { id: "photosynthesis", label: "Photosynthesis", cat: "Biology", desc: "Light, CO₂ & temperature — the limiting-factor rate.", color: C.amber, href: "/photosynthesis" },
  { id: "osmosis", label: "Osmosis & Tonicity", cat: "Biology", desc: "Solute gradient — hypotonic/hypertonic and cell state.", color: C.blue, href: "/osmosis" },
  { id: "kepler", label: "Kepler Orbits", cat: "Earth & Space", desc: "Orbit size & star mass — Kepler’s third law T² = a³/M.", color: C.gold, href: "/kepler" },
  { id: "halflife", label: "Radioactive Dating", cat: "Earth & Space", desc: "Half-life decay — fraction remaining and radiometric age.", color: C.crimson, href: "/halflife" },
  { id: "seasons", label: "Seasons", cat: "Earth & Space", desc: "Latitude, tilt & day — solar declination, Sun angle, insolation.", color: C.amber, href: "/seasons" },
  { id: "moonphases", label: "Moon Phases", cat: "Earth & Space", desc: "Orbital angle — illuminated fraction and the named phases.", color: C.blue, href: "/moonphases" },
  { id: "platetectonics", label: "Plate Tectonics", cat: "Earth & Space", desc: "Seafloor spreading — crust age = distance ÷ rate.", color: C.crimson, href: "/platetectonics" },
  { id: "greenhouse", label: "Climate Balance", cat: "Earth & Space", desc: "Solar flux, albedo & greenhouse — planetary temperature.", color: C.teal, href: "/greenhouse" },
  { id: "stellar", label: "Stellar Properties", cat: "Earth & Space", desc: "Radius & temperature — L = 4πR²σT⁴, color and magnitude.", color: C.gold, href: "/stellar" },
  { id: "hubble", label: "Hubble's Law", cat: "Earth & Space", desc: "Galaxy distance — v = H₀d, redshift and cosmic expansion.", color: C.blue, href: "/hubble" },
  { id: "logicgates", label: "Logic Gates", cat: "Computer Science", desc: "Toggle inputs & gate — build the Boolean truth table.", color: C.teal, href: "/logicgates" },
  { id: "bigo", label: "Big-O Growth", cat: "Computer Science", desc: "Algorithm & input size — O(n), O(log n), O(n log n), O(n²).", color: C.blue, href: "/bigo" },
  { id: "binary", label: "Binary Numbers", cat: "Computer Science", desc: "Flip 8 bits — place value, decimal & hex representation.", color: C.gold, href: "/binary" },
  { id: "sorting", label: "Sorting Algorithms", cat: "Computer Science", desc: "Pick a sort — comparisons, swaps, stability and Big-O.", color: C.crimson, href: "/sorting" },
  { id: "recursion", label: "Recursion", cat: "Computer Science", desc: "Factorial vs Fibonacci — call count, depth and the call tree.", color: C.teal, href: "/recursion" },
  { id: "cryptography", label: "Caesar Cipher", cat: "Computer Science", desc: "Shift a letter — c = (p+k) mod 26, keyspace and decryption.", color: C.amber, href: "/cryptography" },
  { id: "networking", label: "Network Performance", cat: "Computer Science", desc: "Size, bandwidth & latency — transfer time and throughput.", color: C.blue, href: "/networking" },
  { id: "hashing", label: "Hash Tables", cat: "Computer Science", desc: "Table size & items — load factor α, collisions and probe cost.", color: C.teal, href: "/hashing" },
  { id: "floatingpoint", label: "Floating-Point", cat: "Computer Science", desc: "Exponent & mantissa bits — IEEE-754 precision vs range.", color: C.gold, href: "/floatingpoint" },
];
// Each standard bench is reachable at an OPAQUE, stable URL — /b/{code} — instead of its readable id
// (/electronics). The code is a deterministic FNV-1a hash of the bench id: stable across reloads and
// users (no storage), unique across the catalog (collision-avoided below, asserted by the bench test).
// Internal ids stay the canonical key for every /api call, the lab component, and stored tutor.subject;
// only the browser URL is encoded. Legacy /{id} links keep resolving (see benchIdFromPath).
function hashBenchId(s) {
  let h = 0x811c9dc5;
  for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193); }
  return (h >>> 0).toString(36).padStart(7, "0").slice(0, 7);
}
const BENCH_CODE = {};    // internal id -> opaque url code
const BENCH_BY_CODE = {}; // opaque url code -> internal id
for (const s of SUBJECTS) {
  let code = hashBenchId(s.id);
  while (BENCH_BY_CODE[code] && BENCH_BY_CODE[code] !== s.id) code = hashBenchId(code + s.id); // keep codes unique
  BENCH_CODE[s.id] = code; BENCH_BY_CODE[code] = s.id;
}
// The opaque URL for a bench id (falls back to the raw id for anything not in the standard catalog).
const benchHref = (id) => `/b/${BENCH_CODE[id] || id}`;
// Resolve a URL path to an internal bench id: opaque /b/{code} first, then a legacy readable /{id}.
function benchIdFromPath(path) {
  const m = path.match(/^\/b\/([A-Za-z0-9]+)\/?/);
  if (m && BENCH_BY_CODE[m[1]]) return BENCH_BY_CODE[m[1]];
  return null;
}

// Category display order (areas with no benches yet are simply absent from the picker).
const SUBJECT_CATS = ["Mathematics", "Calculus", "Physics", "Chemistry", "Biology", "Earth & Space", "Computer Science"];
function subjectsByCat() {
  const groups = SUBJECT_CATS.map((cat) => ({ cat, items: SUBJECTS.filter((s) => s.cat === cat) })).filter((g) => g.items.length);
  const known = new Set(SUBJECT_CATS);
  for (const s of SUBJECTS) if (!known.has(s.cat)) { known.add(s.cat); groups.push({ cat: s.cat, items: SUBJECTS.filter((x) => x.cat === s.cat) }); }
  return groups;
}
// Sortable table view of the standard bench catalog (the /browse alternative to the card grid).
// Click a header to sort (Subject sorts in the curated category order, then by name); a whole row,
// or the Open button, navigates to the bench. Reuses the .bench-table styles.
function BenchCatalogTable({ subjects }) {
  const [sort, setSort] = useState({ key: "cat", dir: "asc" });
  const catIndex = (c) => { const i = SUBJECT_CATS.indexOf(c); return i < 0 ? 99 : i; };
  const sorted = useMemo(() => {
    const { key, dir } = sort, f = dir === "asc" ? 1 : -1;
    return [...subjects].sort((a, b) => {
      const cmp = key === "cat"
        ? (catIndex(a.cat) - catIndex(b.cat)) || a.label.localeCompare(b.label)
        : String(a[key] || "").localeCompare(String(b[key] || ""));
      return cmp * f;
    });
  }, [subjects, sort]);
  const click = (key) => setSort((s) => (s.key === key ? { key, dir: s.dir === "asc" ? "desc" : "asc" } : { key, dir: "asc" }));
  const arrow = (key) => (sort.key === key ? (sort.dir === "asc" ? " ▲" : " ▼") : "");
  const cols = [{ key: "label", label: "Bench" }, { key: "cat", label: "Subject" }, { key: "desc", label: "What you do" }];
  return (
    <table className="bench-table" style={{ marginTop: 12 }}>
      <thead><tr>{cols.map((c) => <th key={c.key} onClick={() => click(c.key)} title={tt("Sort by this column")}><T>{c.label}</T>{arrow(c.key)}</th>)}<th></th></tr></thead>
      <tbody>
        {sorted.map((s) => (
          <tr key={s.id} onClick={() => { window.location.href = benchHref(s.id); }} title={tt(s.desc)}>
            <td><span style={{ color: s.color }}>●</span> <b>{tt(s.label)}</b></td>
            <td className="t-faint">{tt(s.cat)}</td>
            <td className="t-faint" style={{ maxWidth: 460 }}>{tt(s.desc)}</td>
            <td onClick={(e) => e.stopPropagation()}><a href={benchHref(s.id)} className="btn btn-primary btn-tiny"><T>Open</T></a></td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
function SubjectPicker() {
  // Custom (teacher-designed) benches live alongside the standard catalog here so a teacher can find
  // and preview them after creating one. Educator/admin only on the server → a learner just gets [].
  const [benches, setBenches] = useState([]);
  const [previewBenchId, setPreviewBenchId] = useState(null);
  const [view, setView] = useState("table"); // "table" | "cards"
  useEffect(() => { api("/benches").then((r) => setBenches(r.benches || [])).catch(() => {}); }, []);
  return (
    <div className="page">
      <div className="between" style={{ alignItems: "center" }}>
        <H><T>Choose a subject</T></H>
        <div className="seg" title={tt("Switch between a sortable table and the card grid")}>
          <button className={`seg__btn${view === "table" ? " is-active" : ""}`} onClick={() => setView("table")}><T>Table</T></button>
          <button className={`seg__btn${view === "cards" ? " is-active" : ""}`} onClick={() => setView("cards")}><T>Cards</T></button>
        </div>
      </div>
      <div className="helptext"><T>Every bench is its own tutor library, sharing your account and progress on this server. Sort the table by any column, or switch to cards.</T></div>
      {benches.length > 0 && (
        <div style={{ marginTop: 18 }}>
          <div className="sub nr" style={{ textTransform: "uppercase", letterSpacing: ".08em", fontSize: 12, marginBottom: 8 }}>✨ <T>Your custom benches</T></div>
          <div className="tutor-grid">
            {benches.map((b) => (
              <Card key={b.id}>
                <div className="tutor-name nr" style={{ marginBottom: 4 }}><span style={{ color: "var(--teal)" }}>●</span> {b.label}{b.status === "published" ? " ✓" : ""}</div>
                <div className="helptext" style={{ minHeight: 38, marginBottom: 8 }}>{b.microworld || (b.mine ? tt("Your custom bench") : tf("By {name}", { name: b.ownerName }))}</div>
                <div className="row" style={{ gap: 6 }}>
                  <button type="button" className="btn btn-primary" style={{ flex: 1 }} onClick={() => setPreviewBenchId(b.id)}>👁 <T>Preview</T></button>
                  <a href="/" className="btn btn-muted" title={tt("Author a tutor over this bench from your dashboard")}><T>Author →</T></a>
                </div>
              </Card>
            ))}
          </div>
        </div>
      )}
      {previewBenchId && <BenchPreviewModal benchId={previewBenchId} label={(benches.find((b) => b.id === previewBenchId) || {}).label} onClose={() => setPreviewBenchId(null)} />}
      {view === "table" && <BenchCatalogTable subjects={SUBJECTS} />}
      {view === "cards" && subjectsByCat().map((g) => (
        <div key={g.cat} style={{ marginTop: 18 }}>
          <div className="sub nr" style={{ textTransform: "uppercase", letterSpacing: ".08em", fontSize: 12, marginBottom: 8 }}><T>{g.cat}</T></div>
          <div className="tutor-grid">
            {g.items.map((s) => (
              <a key={s.id} href={benchHref(s.id)} style={{ textDecoration: "none" }}>
                <Card>
                  <div className="tutor-name nr" style={{ marginBottom: 4 }}><span style={{ color: s.color }}>●</span> <T>{s.label}</T></div>
                  <div className="helptext" style={{ minHeight: 38, marginBottom: 8 }}><T>{s.desc}</T></div>
                  <span className="btn btn-primary btn-block" style={{ textAlign: "center", display: "block" }}><T>Open</T></span>
                </Card>
              </a>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
