/* statlab.jsx — StatTutor: the statistics lab (three modules). Built on window.BenchKit
   (loaded first), so this file is just the FIGURES + their field computations. The
   StatFig dispatcher picks the sub-figure from spec.module; each figure's report(...)
   emits EXACTLY the fields server/benches/statistics.js declares for that module. */
(function () {
  const { useState, useMemo, useEffect, useRef } = React;
  const K = window.BenchKit, C = K.C, SVG_BG = K.SVG_BG, btn = K.btn, fmt = K.fmt, clamp = K.clamp;
  const TaskStrip = K.TaskStrip, StatMeter = K.StatMeter, T = K.T;

  /* math */
  const sum = (a) => a.reduce((x, y) => x + y, 0);
  const mean = (a) => (a.length ? sum(a) / a.length : 0);
  function quantile(s, p) { const n = s.length; if (!n) return NaN; if (n === 1) return s[0]; const h = (n - 1) * p, lo = Math.floor(h), f = h - lo; return s[lo] + (s[Math.min(lo + 1, n - 1)] - s[lo]) * f; }
  function describe(a) {
    const s = [...a].sort((x, y) => x - y), n = a.length, m = mean(a);
    const md = quantile(s, 0.5), q1 = quantile(s, 0.25), q3 = quantile(s, 0.75), iqr = q3 - q1;
    const sd = n > 1 ? Math.sqrt(sum(a.map((x) => (x - m) ** 2)) / (n - 1)) : 0;
    const loF = q1 - 1.5 * iqr, hiF = q3 + 1.5 * iqr;
    const outliers = s.filter((x) => x < loF || x > hiF);
    const inb = s.filter((x) => x >= loF && x <= hiF);
    const wLo = inb.length ? Math.min(...inb) : s[0], wHi = inb.length ? Math.max(...inb) : s[n - 1];
    return { n, mean: m, median: md, q1, q3, iqr, sd, min: s[0], max: s[n - 1], outliers, wLo, wHi };
  }
  function erf(x) { const t = 1 / (1 + 0.3275911 * Math.abs(x)); const y = 1 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * Math.exp(-x * x); return x >= 0 ? y : -y; }
  const npdf = (x, mu, s) => Math.exp(-0.5 * ((x - mu) / s) ** 2) / (s * Math.sqrt(2 * Math.PI));
  const ncdf = (x, mu, s) => 0.5 * (1 + erf((x - mu) / (s * Math.SQRT2)));
  function invNorm(p) {
    const a = [-3.969683028665376e1, 2.209460984245205e2, -2.759285104469687e2, 1.38357751867269e2, -3.066479806614716e1, 2.506628277459239];
    const b = [-5.447609879822406e1, 1.615858368580409e2, -1.556989798598866e2, 6.680131188771972e1, -1.328068155288572e1];
    const c = [-7.784894002430293e-3, -3.223964580411365e-1, -2.400758277161838, -2.549732539343734, 4.374664141464968, 2.938163982698783];
    const d = [7.784695709041462e-3, 3.224671290700398e-1, 2.445134137142996, 3.754408661907416];
    const pl = 0.02425, ph = 1 - pl; let q, r;
    if (p < pl) { q = Math.sqrt(-2 * Math.log(p)); return (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1); }
    if (p <= ph) { q = p - 0.5; r = q * q; return (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q / (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1); }
    q = Math.sqrt(-2 * Math.log(1 - p)); return -(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1);
  }
  let _spare = null;
  function randn() { if (_spare !== null) { const v = _spare; _spare = null; return v; } const u = Math.random() || 1e-9, w = Math.random(); const r = Math.sqrt(-2 * Math.log(u)); _spare = r * Math.sin(2 * Math.PI * w); return r * Math.cos(2 * Math.PI * w); }
  const VBW = 600;
  function clientToVal(svg, clientX, x0, x1, vmin, vmax) { const rect = svg.getBoundingClientRect(); const sx = (clientX - rect.left) / rect.width * VBW; const t = (sx - x0) / (x1 - x0); return clamp(vmin + t * (vmax - vmin), vmin, vmax); }

  /* ===================== Module 1: Distribution ===================== */
  function DistributionFig({ task, setTask, tasks, report, event, done }) {
    const [vals, setVals] = useState([20, 28, 33, 38, 42, 45, 50, 56, 64]);
    const [drag, setDrag] = useState(null);
    const svgRef = useRef(null), dragV = useRef(0);
    const X0 = 55, X1 = 565, AY = 200, VMIN = 0, VMAX = 100;
    const px = (v) => X0 + (v - VMIN) / (VMAX - VMIN) * (X1 - X0);
    const st = useMemo(() => describe(vals), [vals]);

    useEffect(() => { report({ mean: st.mean, median: st.median, sd: st.sd, iqr: st.iqr, q1: st.q1, q3: st.q3, min: st.min, max: st.max, n: st.n, skew: st.mean - st.median, outliers: st.outliers.length, hasOutlier: st.outliers.length > 0 }); }, [vals]); // eslint-disable-line

    useEffect(() => {
      if (drag == null) return;
      const move = (e) => { const v = Math.round(clientToVal(svgRef.current, e.clientX, X0, X1, VMIN, VMAX)); dragV.current = v; setVals((a) => a.map((x, i) => (i === drag ? v : x))); };
      const up = () => { event("adjusted", "Moved a data point", { response: `${dragV.current}` }, C.blue); setDrag(null); };
      window.addEventListener("pointermove", move); window.addEventListener("pointerup", up);
      return () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); };
    }, [drag]); // eslint-disable-line

    const stacks = {}; const dots = vals.map((v, i) => { const b = Math.round(v / 4); stacks[b] = (stacks[b] || 0) + 1; return { v, i, k: stacks[b] - 1 }; });
    const bins = 10, bw = (VMAX - VMIN) / bins; const counts = Array(bins).fill(0); vals.forEach((v) => counts[Math.min(bins - 1, Math.floor(v / bw))]++); const mc = Math.max(1, ...counts);
    const t = tasks.find((x) => x.id === task) || tasks[0];

    return (
      <>
        <TaskStrip tasks={tasks} cur={task} setCur={setTask} done={done} goal={t && t.goal} />
        <svg ref={svgRef} viewBox="0 0 600 270" style={{ width: "100%", background: SVG_BG, border: `1px solid ${C.line}`, borderRadius: 8, touchAction: "none" }}>
          {counts.map((c, i) => <rect key={i} x={px(i * bw) + 1} y={AY - (c / mc) * 70} width={(X1 - X0) / bins - 2} height={(c / mc) * 70} fill={C.panel2} />)}
          <line x1={X0} y1={AY} x2={X1} y2={AY} stroke={C.faint} strokeWidth="1.5" />
          {[0, 25, 50, 75, 100].map((g) => <g key={g}><line x1={px(g)} y1={AY} x2={px(g)} y2={AY + 5} stroke={C.faint} /><text x={px(g)} y={AY + 18} textAnchor="middle" fontSize="10" fill={C.mute} fontFamily="monospace">{g}</text></g>)}
          <line x1={px(st.mean)} y1={AY - 95} x2={px(st.mean)} y2={AY} stroke={C.crimson} strokeWidth="2" strokeDasharray="4 3" />
          <text x={px(st.mean)} y={AY - 100} textAnchor="middle" fontSize="10" fill={C.crimson} fontFamily="monospace">x̄ {fmt(st.mean)}</text>
          <line x1={px(st.median)} y1={AY - 80} x2={px(st.median)} y2={AY} stroke={C.blue} strokeWidth="2" />
          <text x={px(st.median)} y={AY - 70} textAnchor="middle" fontSize="10" fill={C.blue} fontFamily="monospace">Md {fmt(st.median)}</text>
          {dots.map((d) => <circle key={d.i} cx={px(d.v)} cy={AY - 8 - d.k * 11} r="6" fill={drag === d.i ? C.amber : C.teal} stroke={SVG_BG} strokeWidth="1.5" style={{ cursor: "grab" }} onPointerDown={() => setDrag(d.i)} />)}
          <g>
            <line x1={px(st.wLo)} y1={235} x2={px(st.q1)} y2={235} stroke={C.mute} /><line x1={px(st.q3)} y1={235} x2={px(st.wHi)} y2={235} stroke={C.mute} />
            <rect x={px(st.q1)} y={226} width={px(st.q3) - px(st.q1)} height={18} fill="none" stroke={C.mute} />
            <line x1={px(st.median)} y1={226} x2={px(st.median)} y2={244} stroke={C.blue} strokeWidth="2" />
            {st.outliers.map((o, i) => <circle key={i} cx={px(o)} cy={235} r="3.5" fill={C.crimson} />)}
          </g>
        </svg>
        <div style={{ display: "grid", gridTemplateColumns: "repeat(5,1fr)", gap: 8, marginTop: 12 }}>
          <StatMeter label="Mean" v={st.mean} color={C.crimson} /><StatMeter label="Median" v={st.median} color={C.blue} />
          <StatMeter label="SD (s)" v={st.sd} color={C.teal} /><StatMeter label="IQR" v={st.iqr} color={C.amber} />
          <StatMeter label="Outliers" v={st.outliers.length} d={0} color={st.outliers.length ? C.crimson : C.mute} />
        </div>
        <div style={{ marginTop: 10, display: "flex", gap: 8 }}>
          <button onClick={() => { setVals([...vals, 50]); event("added", "Added a data point", {}, C.mute); }} style={btn(C.panel2, C.ink)}>+ point</button>
          <button onClick={() => vals.length > 3 && setVals(vals.slice(0, -1))} style={btn(C.panel2, C.ink)}>− point</button>
          <span style={{ alignSelf: "center", fontSize: 11.5, color: C.faint }}>n = {st.n} · {st.mean - st.median > 1.5 ? "right-skew →" : st.median - st.mean > 1.5 ? "← left-skew" : "≈ symmetric"}</span>
        </div>
      </>
    );
  }

  /* ===================== Module 2: Sampling & CLT ===================== */
  const POPS = { normal: { name: "Normal", mu: 50, sigma: 15, draw: () => clamp(randn() * 15 + 50, 0, 100) }, uniform: { name: "Uniform", mu: 50, sigma: 17.32, draw: () => 20 + Math.random() * 60 }, skew: { name: "Right-skew", mu: 40, sigma: 20, draw: () => clamp(20 + -Math.log(1 - Math.random()) * 20, 0, 120) }, bimodal: { name: "Bimodal", mu: 50, sigma: 21.54, draw: () => (Math.random() < 0.5 ? randn() * 8 + 30 : randn() * 8 + 70) } };
  function SamplingFig({ task, setTask, tasks, report, event, done }) {
    const [pop, setPop] = useState("skew");
    const [n, setN] = useState(5);
    const [means, setMeans] = useState([]);
    const seDropped = useRef(false), prevN = useRef(5);
    const P = POPS[pop];
    const theoSE = P.sigma / Math.sqrt(n);
    const obsMean = means.length ? mean(means) : NaN;
    const obsSE = means.length > 1 ? Math.sqrt(sum(means.map((x) => (x - obsMean) ** 2)) / (means.length - 1)) : NaN;

    useEffect(() => { if (n > prevN.current) seDropped.current = true; prevN.current = n; }, [n]);
    useEffect(() => { report({ count: means.length, n, theoSE, obsSE: isFinite(obsSE) ? obsSE : 0, obsMean: isFinite(obsMean) ? obsMean : 0, seDropped: seDropped.current, pop }); }, [means, n, pop]); // eslint-disable-line

    function drawK(k) { const nm = []; for (let j = 0; j < k; j++) { const s = Array.from({ length: n }, P.draw); nm.push(mean(s)); } setMeans((m) => [...m, ...nm]); event("sampled", `Drew ${k} sample${k > 1 ? "s" : ""} (n=${n})`, { response: `total ${means.length + k}` }, C.blue); }
    function changePop(p) { setPop(p); setMeans([]); seDropped.current = false; event("selected", `Population: ${POPS[p].name}`, {}, C.amber); }

    const DMIN = P.mu - 42, DMAX = P.mu + 42, BINS = 34, bw = (DMAX - DMIN) / BINS;
    const counts = Array(BINS).fill(0); means.forEach((m) => { const b = Math.floor((m - DMIN) / bw); if (b >= 0 && b < BINS) counts[b]++; });
    const X0 = 50, X1 = 565, BASE = 200, TOP = 40; const pxX = (v) => X0 + (v - DMIN) / (DMAX - DMIN) * (X1 - X0);
    const maxCount = Math.max(1, ...counts);
    const overlay = []; for (let i = 0; i <= 120; i++) { const x = DMIN + (i / 120) * (DMAX - DMIN); const dens = npdf(x, P.mu, theoSE) * means.length * bw; overlay.push([pxX(x), BASE - (dens / maxCount) * (BASE - TOP)]); }
    const t = tasks.find((x) => x.id === task) || tasks[0];

    return (
      <>
        <TaskStrip tasks={tasks} cur={task} setCur={setTask} done={done} goal={t && t.goal} />
        <div style={{ display: "flex", gap: 8, marginBottom: 10, flexWrap: "wrap" }}>
          {Object.keys(POPS).map((p) => <button key={p} onClick={() => changePop(p)} style={{ ...btn(pop === p ? C.amber : C.panel2, pop === p ? C.bg : C.mute), border: `1px solid ${pop === p ? C.amber : C.line}` }}>{POPS[p].name}</button>)}
        </div>
        <svg viewBox="0 0 600 230" style={{ width: "100%", background: SVG_BG, border: `1px solid ${C.line}`, borderRadius: 8 }}>
          {counts.map((c, i) => <rect key={i} x={pxX(DMIN + i * bw) + 1} y={BASE - (c / maxCount) * (BASE - TOP)} width={(X1 - X0) / BINS - 1.5} height={(c / maxCount) * (BASE - TOP)} fill={C.teal} opacity="0.55" />)}
          {means.length > 5 && <polyline points={overlay.map((p) => p.join(",")).join(" ")} fill="none" stroke={C.crimson} strokeWidth="2" />}
          <line x1={X0} y1={BASE} x2={X1} y2={BASE} stroke={C.faint} strokeWidth="1.5" />
          <line x1={pxX(P.mu)} y1={TOP} x2={pxX(P.mu)} y2={BASE} stroke={C.blue} strokeDasharray="4 3" />
          <text x={pxX(P.mu)} y={TOP - 6} textAnchor="middle" fontSize="10" fill={C.blue} fontFamily="monospace">μ = {P.mu}</text>
          {[DMIN, P.mu, DMAX].map((g) => <text key={g} x={pxX(g)} y={BASE + 15} textAnchor="middle" fontSize="10" fill={C.mute} fontFamily="monospace">{Math.round(g)}</text>)}
          <text x={X1} y={TOP - 6} textAnchor="end" fontSize="10" fill={C.crimson} fontFamily="monospace">theory: N(μ, σ/√n)</text>
        </svg>
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 12 }}>
          <span style={{ fontSize: 12, color: C.mute, width: 92, fontFamily: "monospace" }}>sample size n</span>
          <input type="range" min={2} max={60} value={n} onChange={(e) => { setN(parseInt(e.target.value)); setMeans([]); }} style={{ flex: 1 }} />
          <span style={{ color: C.amber, width: 40, textAlign: "right", fontFamily: "monospace" }}>{n}</span>
        </div>
        <div style={{ display: "flex", gap: 8, marginTop: 10, flexWrap: "wrap" }}>
          <button onClick={() => drawK(1)} style={btn(C.blue, C.bg)}>Draw 1</button>
          <button onClick={() => drawK(100)} style={btn(C.blue, C.bg)}>Draw 100</button>
          <button onClick={() => setMeans([])} style={btn(C.panel2, C.ink)}>Clear</button>
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "repeat(4,1fr)", gap: 8, marginTop: 12 }}>
          <StatMeter label="# means" v={means.length} d={0} color={C.ink} /><StatMeter label="mean of x̄" v={obsMean} color={C.blue} />
          <StatMeter label="SE (obs)" v={obsSE} d={2} color={C.teal} /><StatMeter label="SE = σ/√n" v={theoSE} d={2} color={C.crimson} />
        </div>
      </>
    );
  }

  /* ===================== Module 3: Hypothesis Testing ===================== */
  function HypothesisFig({ task, setTask, tasks, report, event, done }) {
    const [delta, setDelta] = useState(0.5), [n, setN] = useState(16), [alpha, setAlpha] = useState(0.05), [obs, setObs] = useState(0.2);
    const [drag, setDrag] = useState(false); const svgRef = useRef(null);
    const prevA = useRef(0.05), prevB = useRef(0), sawTrade = useRef(false), prevD = useRef(0.5), prevPow = useRef(0), sawEff = useRef(false);
    const SE = 1 / Math.sqrt(n); const zc = invNorm(1 - alpha); const xcrit = zc * SE;
    const power = 1 - ncdf(xcrit, delta, SE); const beta = 1 - power; const pvalue = 1 - ncdf(obs, 0, SE);

    useEffect(() => { if (alpha < prevA.current && beta > prevB.current + 0.005) sawTrade.current = true; prevA.current = alpha; prevB.current = beta; }, [alpha, beta]);
    useEffect(() => { if (delta > prevD.current && power > prevPow.current + 0.005) sawEff.current = true; prevD.current = delta; prevPow.current = power; }, [delta, power]);
    useEffect(() => { report({ power, beta, alpha, pvalue, SE, delta, n, rejected: pvalue < alpha, sawTradeoff: sawTrade.current, sawEffect: sawEff.current }); }, [delta, n, alpha, obs]); // eslint-disable-line

    const XMIN = -1.2, XMAX = 2.8, X0 = 50, X1 = 565, BASE = 230, TOP = 30; const pxX = (v) => X0 + (v - XMIN) / (XMAX - XMIN) * (X1 - X0);
    const N = 200; const xs = Array.from({ length: N + 1 }, (_, i) => XMIN + (i / N) * (XMAX - XMIN));
    let maxD = 0; xs.forEach((x) => { maxD = Math.max(maxD, npdf(x, 0, SE), npdf(x, delta, SE)); });
    const sy = (BASE - TOP) / maxD * 0.92; const pxY = (d) => BASE - d * sy;
    const curve = (mu) => xs.map((x) => `${pxX(x)},${pxY(npdf(x, mu, SE))}`).join(" ");
    const area = (mu, a, b) => { const pts = xs.filter((x) => x >= a && x <= b).map((x) => `${pxX(x)},${pxY(npdf(x, mu, SE))}`); if (!pts.length) return ""; return `M${pxX(Math.max(a, XMIN))},${BASE} L${pts.join(" L")} L${pxX(Math.min(b, XMAX))},${BASE} Z`; };

    useEffect(() => {
      if (!drag) return;
      const move = (e) => setObs(Math.round(clientToVal(svgRef.current, e.clientX, X0, X1, XMIN, XMAX) * 100) / 100);
      const up = () => { setDrag(false); event("adjusted", "Set observed mean", { response: `obs ${fmt(obs, 2)}, p ${fmt(pvalue, 3)}` }, C.blue); };
      window.addEventListener("pointermove", move); window.addEventListener("pointerup", up);
      return () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); };
    }, [drag]); // eslint-disable-line

    const t = tasks.find((x) => x.id === task) || tasks[0];
    const slide = (label, val, setter, min, max, step, color, onUp) => (
      <div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 8 }}>
        <span style={{ fontSize: 12, color: C.mute, width: 96, fontFamily: "monospace" }}>{label}</span>
        <input type="range" min={min} max={max} step={step} value={val} onChange={(e) => setter(parseFloat(e.target.value))} onPointerUp={onUp} style={{ flex: 1 }} />
        <span style={{ color, width: 52, textAlign: "right", fontFamily: "monospace" }}>{val}</span>
      </div>
    );
    return (
      <>
        <TaskStrip tasks={tasks} cur={task} setCur={setTask} done={done} goal={t && t.goal} />
        <svg ref={svgRef} viewBox="0 0 600 260" style={{ width: "100%", background: SVG_BG, border: `1px solid ${C.line}`, borderRadius: 8, touchAction: "none" }}>
          <path d={area(0, xcrit, XMAX)} fill={C.crimson} opacity="0.3" />
          <path d={area(delta, xcrit, XMAX)} fill={C.teal} opacity="0.32" />
          <path d={area(delta, XMIN, xcrit)} fill={C.amber} opacity="0.24" />
          <polyline points={curve(0)} fill="none" stroke={C.blue} strokeWidth="2" />
          <polyline points={curve(delta)} fill="none" stroke={C.ink} strokeWidth="2" opacity="0.85" />
          <line x1={pxX(xcrit)} y1={TOP - 5} x2={pxX(xcrit)} y2={BASE} stroke={C.crimson} strokeWidth="1.5" strokeDasharray="5 3" />
          <text x={pxX(xcrit)} y={TOP - 8} textAnchor="middle" fontSize="9.5" fill={C.crimson} fontFamily="monospace">x* {fmt(xcrit, 2)}</text>
          <line x1={pxX(obs)} y1={TOP} x2={pxX(obs)} y2={BASE} stroke={C.blue} strokeWidth="1" />
          <polygon points={`${pxX(obs)},${BASE - 4} ${pxX(obs) - 7},${BASE + 9} ${pxX(obs) + 7},${BASE + 9}`} fill={C.blue} style={{ cursor: "grab" }} onPointerDown={() => setDrag(true)} />
          <line x1={X0} y1={BASE} x2={X1} y2={BASE} stroke={C.faint} strokeWidth="1.5" />
          <text x={pxX(0)} y={BASE + 16} textAnchor="middle" fontSize="9.5" fill={C.blue} fontFamily="monospace">H₀: 0</text>
          <text x={pxX(delta)} y={BASE + 16} textAnchor="middle" fontSize="9.5" fill={C.ink} fontFamily="monospace">H₁: {delta}</text>
          <g fontFamily="monospace" fontSize="9.5">
            <rect x={70} y={TOP} width={10} height={10} fill={C.crimson} opacity="0.5" /><text x={85} y={TOP + 9} fill={C.mute}>α</text>
            <rect x={110} y={TOP} width={10} height={10} fill={C.teal} opacity="0.5" /><text x={125} y={TOP + 9} fill={C.mute}>power</text>
            <rect x={170} y={TOP} width={10} height={10} fill={C.amber} opacity="0.5" /><text x={185} y={TOP + 9} fill={C.mute}>β</text>
          </g>
        </svg>
        {slide("true effect δ", delta, setDelta, 0, 1.5, 0.05, C.ink, () => event("adjusted", `Set effect δ=${delta}`, { response: `power ${fmt(power, 2)}` }, C.amber))}
        {slide("sample size n", n, setN, 2, 200, 1, C.amber, () => event("adjusted", `Set n=${n}`, { response: `SE ${fmt(SE, 2)}, power ${fmt(power, 2)}` }, C.amber))}
        {slide("α", alpha, setAlpha, 0.001, 0.2, 0.001, C.crimson, () => event("adjusted", `Set α=${alpha}`, { response: `β ${fmt(beta, 2)}` }, C.amber))}
        <div style={{ display: "grid", gridTemplateColumns: "repeat(4,1fr)", gap: 8, marginTop: 12 }}>
          <StatMeter label="power (1−β)" v={power} d={2} color={C.teal} /><StatMeter label="β (Type II)" v={beta} d={2} color={C.amber} />
          <StatMeter label="p-value" v={pvalue} d={3} color={pvalue < alpha ? C.crimson : C.mute} /><StatMeter label="SE" v={SE} d={2} color={C.blue} />
        </div>
        <div style={{ marginTop: 10, padding: "8px 12px", borderRadius: 6, background: C.panel, border: `1px solid ${pvalue < alpha ? C.crimson : C.line}`, color: pvalue < alpha ? C.crimson : C.mute, fontSize: 12.5 }}>
          {pvalue < alpha ? `p = ${fmt(pvalue, 3)} < α = ${alpha} → reject H₀.` : `p = ${fmt(pvalue, 3)} ≥ α = ${alpha} → fail to reject H₀.`} Drag the ▲ to change the observed mean.
        </div>
      </>
    );
  }

  const FIGS = { distribution: DistributionFig, sampling: SamplingFig, hypothesis: HypothesisFig };
  const MODULE_LABEL = { distribution: "Distribution Builder", sampling: "Sampling & the CLT", hypothesis: "Hypothesis Testing" };

  /* StatFig — picks the sub-figure from spec.module, then delegates the shared props. */
  function StatFig(props) {
    const module = FIGS[props.spec && props.spec.module] ? props.spec.module : "distribution";
    const Fig = FIGS[module];
    return <Fig {...props} />;
  }

  window.StatTutor = K.makeTutor(StatFig, {
    benchId: "statistics",
    moduleLabel: (spec) => MODULE_LABEL[spec && spec.module] || "Statistics figures",
  });
})();
