// ── SAT Results Report — chart + panel components ────────────────────────

function accColor(v) {
  if (v >= 85) return "#5C7E3F";
  if (v >= 70) return "#7CA158";
  if (v >= 55) return "#E0A82E";
  if (v >= 40) return "#E07A2E";
  return "var(--error)";
}
function accTint(v) {
  if (v >= 85) return "#E4EFD6";
  if (v >= 70) return "#EDF1DD";
  if (v >= 55) return "#FBEFCF";
  if (v >= 40) return "#FBE3CF";
  return "#FBD9D6";
}
const STATUS_COLOR = { right: "#7CA158", wrong: "var(--error)", skip: "#E0A82E", unvisited: "#6FA5DE" };

// Absolute pacing bands (seconds per question) — shared by the time chart + guide.
const PACING = [
  { max: 10,       key: "blind", label: "Blind Guess", range: "0–10s",   color: "var(--slate)", tint: "#EAECE2", desc: "This is too fast to read the question, so it is likely a panic guess." },
  { max: 40,       key: "rush",  label: "Rushed",      range: "11–40s",  color: "#E0A82E",      tint: "#FBEFCF", desc: "This is a speed-reading risk. Take a moment to verify your answers." },
  { max: 90,       key: "ideal", label: "Ideal",       range: "41–90s",  color: "#7CA158",      tint: "#E4EFD6", desc: "This is an ideal pace, and it is where strong performance shows up." },
  { max: 150,      key: "deep",  label: "Deep Dive",   range: "91–150s", color: "#6FA5DE",      tint: "#DCE9F1", desc: "This is a methodical approach that works well for hard questions." },
  { max: Infinity, key: "sink",  label: "Time Sink",   range: "150s+",   color: "var(--error)", tint: "#FBD9D6", desc: "This may indicate difficulty, so consider reviewing your approach." },
];
function pacingBucket(t) { return PACING.find(p => t <= p.max); }
function fmtDur(s) { const m = Math.floor(s / 60), r = s % 60; return m > 0 ? `${m}m ${r}s` : `${r}s`; }

function PacingGuide() {
  return (
    <div style={{ border: "1px solid var(--sage-l)", borderRadius: 12, background: "#FBFCF6", padding: "14px 16px" }}>
      <div style={{ fontFamily: "var(--fp)", fontSize: "0.66rem", letterSpacing: "0.1em", textTransform: "uppercase", color: "var(--sage)", marginBottom: 10 }}>Pacing Analysis Guide</div>
      <div style={{ display: "flex", flexDirection: "column", gap: 9 }}>
        {PACING.map(p => (
          <div key={p.key} style={{ display: "grid", gridTemplateColumns: "66px 1fr", gap: 12, alignItems: "baseline" }}>
            <div style={{ display: "flex", alignItems: "center", gap: 7 }}>
              <span style={{ width: 8, height: 8, borderRadius: 2, background: p.color, flexShrink: 0 }} />
              <span style={{ fontFamily: "var(--fb)", fontSize: "0.72rem", fontWeight: 600, color: "var(--forest)", fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" }}>{p.range}</span>
            </div>
            <div>
              <span style={{ fontFamily: "var(--fp)", fontSize: "0.56rem", fontWeight: 700, letterSpacing: "0.05em", textTransform: "uppercase", color: p.color, background: p.tint, padding: "1px 7px", borderRadius: 99, marginRight: 7, whiteSpace: "nowrap" }}>{p.label}</span>
              <span style={{ fontFamily: "var(--fb)", fontSize: "0.75rem", color: "var(--slate)", lineHeight: 1.45 }}>{p.desc}</span>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ─── Per-question grid for one module ───
function ModuleGrid({ qs, label }) {
  return (
    <div>
      <div style={{ display: "flex", alignItems: "baseline", gap: 8, marginBottom: 7 }}>
        <span style={{ fontFamily: "var(--fp)", fontSize: "0.72rem", fontWeight: 700, letterSpacing: "0.06em", textTransform: "uppercase", color: "var(--forest)" }}>{label}</span>
        <span style={{ fontFamily: "var(--fp)", fontSize: "0.7rem", color: "var(--muted)" }}>
          {qs.filter(q => q.status === "right").length} / {qs.length}
        </span>
      </div>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(14, 1fr)", gap: 4 }}>
        {qs.map(q => (
          <div key={q.num} title={`Q${q.num} · ${q.status}${q.flagged ? " · flagged" : ""} · ${DIFF_LABEL[q.diff]}`}
            style={{ position: "relative", aspectRatio: "1", borderRadius: 5, background: STATUS_COLOR[q.status],
              color: "#fff", display: "grid", placeItems: "center",
              fontFamily: "var(--fp)", fontSize: "0.62rem", fontWeight: 600, border: "none" }}>
            {q.num}
            {q.flagged && <span style={{ position: "absolute", top: -5, right: -3, fontSize: "0.72rem", lineHeight: 1, color: "#F4C535", textShadow: "0 0 2px rgba(0,0,0,.6)" }}>★</span>}
          </div>
        ))}
      </div>
    </div>
  );
}

function PerQuestionPanel({ sec }) {
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
      <ModuleGrid qs={sec.modules[0]} label="Module 1" />
      <ModuleGrid qs={sec.modules[1]} label="Module 2" />
      <div style={{ display: "flex", gap: 14, flexWrap: "wrap", fontFamily: "var(--fp)", fontSize: "0.7rem", color: "var(--slate)", marginTop: 2 }}>
        <Legend c={STATUS_COLOR.right} l="Correct" />
        <Legend c={STATUS_COLOR.wrong} l="Wrong" />
        <Legend c={STATUS_COLOR.skip} l="Skipped" />
        <Legend c={STATUS_COLOR.unvisited} l="Unvisited" />
        <span style={{ display: "flex", alignItems: "center", gap: 4 }}><span style={{ color: "#F4C535", fontSize: "0.85rem", textShadow: "0 0 1px rgba(0,0,0,.35)" }}>★</span>Flagged</span>
      </div>
    </div>
  );
}

// ─── Time bars for one module ───
function TimeModule({ qs, rec, label }) {
  const [hover, setHover] = React.useState(null);
  const W = 560, H = 96, padL = 26, padR = 8, padT = 10, padB = 16;
  const max = Math.max(...qs.map(q => q.t), rec * 1.5) * 1.05;
  const barW = (W - padL - padR) / qs.length;
  const recY = padT + (1 - rec / max) * (H - padT - padB);
  const col = (q) => {
    if (q.status === "skip") return "#E0A82E";
    if (q.status === "unvisited") return "#CBD2B4";
    return pacingBucket(q.t).color;
  };
  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 4 }}>
        <span style={{ fontFamily: "var(--fp)", fontSize: "0.72rem", fontWeight: 700, letterSpacing: "0.06em", textTransform: "uppercase", color: "var(--forest)" }}>{label}</span>
        <span style={{ fontFamily: "var(--fp)", fontSize: "0.68rem", color: "var(--muted)" }}>avg {Math.round(qs.reduce((a, b) => a + b.t, 0) / qs.length)}s · rec {rec}s</span>
      </div>
      <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: "block", overflow: "visible" }}>
        {[60, 120].filter(v => v < max).map(v => {
          const y = padT + (1 - v / max) * (H - padT - padB);
          return <g key={v}><line x1={padL} y1={y} x2={W - padR} y2={y} stroke="var(--sage-l)" strokeWidth="1" strokeDasharray="2,3" /><text x={padL - 4} y={y + 3} fontFamily="var(--fp)" fontSize="8" fill="var(--muted)" textAnchor="end">{v}s</text></g>;
        })}
        {qs.map((q, i) => {
          const bh = (q.t / max) * (H - padT - padB);
          const x = padL + i * barW;
          return <g key={i} onMouseEnter={() => setHover(i)} onMouseLeave={() => setHover(h => h === i ? null : h)} style={{ cursor: "pointer" }}>
            <rect x={x + 0.5} y={padT} width={barW - 1} height={H - padT - padB} fill={hover === i ? "rgba(20,49,9,.05)" : "transparent"} />
            <rect x={x + 1} y={H - padB - bh} width={barW - 2} height={bh} rx={1.5} fill={col(q)} stroke={hover === i ? "var(--forest)" : "none"} strokeWidth={hover === i ? 1.2 : 0} />
          </g>;
        })}
        <line x1={padL} y1={recY} x2={W - padR} y2={recY} stroke="var(--forest)" strokeWidth="1.4" strokeDasharray="5,3" />
        {hover !== null && (() => {
          const q = qs[hover]; const x = padL + hover * barW + barW / 2;
          const tipW = 116, tx = Math.max(padL, Math.min(W - padR - tipW, x - tipW / 2));
          return <g pointerEvents="none">
            <rect x={tx} y={padT} width={tipW} height={34} rx={5} fill="var(--forest)" />
            <text x={tx + 8} y={padT + 14} fontFamily="var(--fh)" fontSize="10" fill="#fff">Q{q.num} · {q.status}</text>
            <text x={tx + 8} y={padT + 27} fontFamily="var(--fb)" fontSize="9.5" fill="rgba(255,255,255,.85)">{q.t}s · {DIFF_LABEL[q.diff]}</text>
          </g>;
        })()}
      </svg>
    </div>
  );
}

function TimePanel({ sec }) {
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
      <TimeModule qs={sec.modules[0]} rec={sec.perModuleRec} label="Module 1" />
      <TimeModule qs={sec.modules[1]} rec={sec.perModuleRec} label="Module 2" />
      <div style={{ display: "flex", alignItems: "center", gap: 7, fontFamily: "var(--fp)", fontSize: "0.68rem", color: "var(--slate)" }}>
        <svg width="20" height="6"><line x1="0" y1="3" x2="20" y2="3" stroke="var(--forest)" strokeWidth="1.4" strokeDasharray="4,3" /></svg>
        Dashed line = recommended pace · bars colored by pacing band below
      </div>
      <PacingGuide />
    </div>
  );
}

// ─── Confidence vs accuracy — 4-quadrant matrix scatter ───
// Quadrants: Solid (green) · Unsure (blue) · Blind Spots (yellow) · Knowledge Gaps (red)
const CONF_Q = {
  solid: { t: "Solid",          c: "#5C7E3F", tint: "#E4EFD6", s: "correct & sure",    tip: "These are confident and correct, so they are your real strengths." },
  unsure:{ t: "Unsure",         c: "#2E6B8A", tint: "#DCE9F1", s: "correct but unsure", tip: "These are right but you were guessing, so work to convert them to Solid." },
  blind: { t: "Blind Spots",    c: "#E0A82E", tint: "#FBEFCF", s: "sure but wrong",     tip: "These are confident but wrong, so they are the most urgent to fix." },
  gaps:  { t: "Knowledge Gaps", c: "var(--error)", tint: "#FBD9D6", s: "unsure & wrong", tip: "You knew you did not know these." },
};
function ConfidenceMatrix({ sec }) {
  const W = 460, H = 360, padL = 50, padR = 14, padT = 38, padB = 48;
  const plotW = W - padL - padR, plotH = H - padT - padB;
  const x0 = padL, x1 = W - padR, y0 = padT, y1 = H - padB;
  const splitX = padL + plotW * 0.5;   // sure (conf 4–5) to the right
  const midY = padT + plotH * 0.5;     // correct above / wrong below
  const rng = (() => { let a = 99; return () => { a = (a * 16807) % 2147483647; return a / 2147483647; }; })();
  const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
  // conf 1..5 → x; put low conf (1–3) in left half, high (4–5) in right half
  const cx = conf => {
    const frac = conf <= 3 ? (conf - 1) / 3 * 0.46 + 0.02 : 0.52 + (conf - 4) / 1 * 0.44;
    return x0 + frac * plotW;
  };
  const r = 5;
  const dots = sec.all.map(q => {
    const correct = q.status === "right";
    const jx = (rng() - 0.5) * (plotW * 0.06);
    const jy = (rng() - 0.5) * (plotH * 0.30);
    const x = clamp(cx(q.conf) + jx, x0 + r + 1, x1 - r - 1);
    const yc = correct ? padT + plotH * 0.26 : padT + plotH * 0.74;
    const y = correct ? clamp(yc + jy, y0 + r + 1, midY - r - 1) : clamp(yc + jy, midY + r + 1, y1 - r - 1);
    const high = q.conf >= 4;
    const key = correct ? (high ? "solid" : "unsure") : (high ? "blind" : "gaps");
    return { x, y, key, correct, conf: q.conf };
  });
  const quad = { solid: 0, unsure: 0, blind: 0, gaps: 0 };
  dots.forEach(d => quad[d.key]++);
  const QLabel = ({ x, y, k, anchor }) => (
    <text x={x} y={y} textAnchor={anchor} fontFamily="var(--fp)" fontSize="9.5" letterSpacing="0.03em">
      <tspan fontWeight="700" fill={CONF_Q[k].c}>{CONF_Q[k].t}</tspan>
    </text>
  );
  return (
    <div>
      <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: "block" }}>
        {/* quadrant tints (clipped to plot box) */}
        <rect x={splitX} y={y0} width={x1 - splitX} height={midY - y0} fill="#E4EFD6" opacity="0.55" />
        <rect x={x0} y={y0} width={splitX - x0} height={midY - y0} fill="#DCE9F1" opacity="0.55" />
        <rect x={splitX} y={midY} width={x1 - splitX} height={y1 - midY} fill="#FBEFCF" opacity="0.6" />
        <rect x={x0} y={midY} width={splitX - x0} height={y1 - midY} fill="#FBD9D6" opacity="0.55" />
        {/* plot border + axes */}
        <rect x={x0} y={y0} width={plotW} height={plotH} fill="none" stroke="var(--sage-l)" strokeWidth="1" />
        <line x1={x0} y1={midY} x2={x1} y2={midY} stroke="var(--slate)" strokeWidth="1" opacity="0.5" />
        <line x1={splitX} y1={y0} x2={splitX} y2={y1} stroke="var(--slate)" strokeWidth="1" opacity="0.5" strokeDasharray="3,3" />
        {/* quadrant labels */}
        <QLabel x={x1 - 6} y={y0 + 14} k="solid" anchor="end" />
        <QLabel x={x0 + 6} y={y0 + 14} k="unsure" anchor="start" />
        <QLabel x={x1 - 6} y={y1 - 7} k="blind" anchor="end" />
        <QLabel x={x0 + 6} y={y1 - 7} k="gaps" anchor="start" />
        {/* dots */}
        {dots.map((d, i) => <circle key={i} cx={d.x} cy={d.y} r={r} fill={CONF_Q[d.key].c} opacity="0.8" stroke="#fff" strokeWidth="1" />)}
        {/* axis labels */}
        <text x={x0} y={y1 + 14} fontFamily="var(--fp)" fontSize="9" fill="var(--muted)">guessed</text>
        <text x={x1} y={y1 + 14} fontFamily="var(--fp)" fontSize="9" fill="var(--muted)" textAnchor="end">very sure</text>
        <text x={(x0 + x1) / 2} y={H - 6} fontFamily="var(--fp)" fontSize="9.5" fill="var(--slate)" textAnchor="middle">confidence →</text>
        <text x={14} y={midY} fontFamily="var(--fp)" fontSize="9" fill="var(--muted)" transform={`rotate(-90 14 ${midY})`} textAnchor="middle">wrong ↓   correct ↑</text>
      </svg>
      <div style={{ display: "flex", gap: 8, marginTop: 6, flexWrap: "wrap" }}>
        <QuadStat n={quad.blind} t={CONF_Q.blind.t} c={CONF_Q.blind.c} s={CONF_Q.blind.s} tip={CONF_Q.blind.tip} />
        <QuadStat n={quad.gaps} t={CONF_Q.gaps.t} c={CONF_Q.gaps.c} s={CONF_Q.gaps.s} tip={CONF_Q.gaps.tip} />
        <QuadStat n={quad.unsure} t={CONF_Q.unsure.t} c={CONF_Q.unsure.c} s={CONF_Q.unsure.s} tip={CONF_Q.unsure.tip} />
        <QuadStat n={quad.solid} t={CONF_Q.solid.t} c={CONF_Q.solid.c} s={CONF_Q.solid.s} tip={CONF_Q.solid.tip} />
      </div>
      <div style={{ marginTop: 10, display: "flex", flexDirection: "column", gap: 7 }}>
        {["blind", "gaps", "unsure", "solid"].map(k => (
          <div key={k} style={{ display: "grid", gridTemplateColumns: "auto 1fr", gap: 9, alignItems: "baseline" }}>
            <span style={{ fontFamily: "var(--fp)", fontSize: "0.6rem", fontWeight: 700, letterSpacing: "0.04em", textTransform: "uppercase", color: CONF_Q[k].c, background: CONF_Q[k].tint, padding: "1px 7px", borderRadius: 99, whiteSpace: "nowrap", justifySelf: "start" }}>{CONF_Q[k].t}</span>
            <span style={{ fontFamily: "var(--fb)", fontSize: "0.74rem", color: "var(--slate)", lineHeight: 1.4 }}>{CONF_Q[k].tip}</span>
          </div>
        ))}
      </div>
    </div>
  );
}
function QuadStat({ n, t, c, tip }) {
  return (
    <div title={tip} style={{ flex: 1, minWidth: 78, border: "1px solid var(--sage-l)", borderRadius: 8, padding: "8px 10px", borderTop: `3px solid ${c}` }}>
      <div style={{ fontFamily: "var(--fh)", fontSize: "1.3rem", color: "var(--forest)", lineHeight: 1 }}>{n}</div>
      <div style={{ fontFamily: "var(--fp)", fontSize: "0.66rem", color: "var(--slate)", marginTop: 2 }}>{t}</div>
    </div>
  );
}

// ─── Answer-change icon (✓/✗ → ✓/✗) ───
function ChangeIcon({ from, to }) {
  const dot = (ok) => <span style={{ width: 15, height: 15, borderRadius: "50%", background: ok ? "#7CA158" : "var(--error)", color: "#fff", display: "grid", placeItems: "center", fontSize: "0.58rem", fontWeight: 700, lineHeight: 1 }}>{ok ? "✓" : "✗"}</span>;
  return <span style={{ display: "flex", alignItems: "center", gap: 4 }}>{dot(from)}<span style={{ color: "var(--muted)", fontSize: "0.78rem" }}>→</span>{dot(to)}</span>;
}

// ─── Performance by difficulty — one card per band, toggled Easy / Medium / Hard ───
function DifficultyPanel({ sec }) {
  const [d, setD] = React.useState("M");
  const band = sec.bands.find(b => b.d === d) || sec.bands[0];
  const diffColor = { E: "#7CA158", M: "#E0A82E", H: "var(--error)" };
  const dc = diffColor[band.d] || "var(--sage)";
  const vs = band.acc - (band.cohort ?? band.acc);
  const counts = [
    { l: "Correct",   n: band.correct   || 0, c: STATUS_COLOR.right },
    { l: "Incorrect", n: band.incorrect || 0, c: STATUS_COLOR.wrong },
    { l: "Skipped",   n: band.skipped   || 0, c: STATUS_COLOR.skip },
    { l: "Unvisited", n: band.unvisited || 0, c: STATUS_COLOR.unvisited },
  ];
  const changes = [
    { l: "Correct → Incorrect",   n: band.c2i || 0, from: true,  to: false },
    { l: "Incorrect → Correct",   n: band.i2c || 0, from: false, to: true },
    { l: "Incorrect → Incorrect", n: band.i2i || 0, from: false, to: false },
  ];
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
      {/* difficulty toggle */}
      <div style={{ display: "grid", gridTemplateColumns: "repeat(3,1fr)", gap: 5, background: "#F1F3E8", border: "1px solid var(--sage-l)", borderRadius: 10, padding: 4 }}>
        {sec.bands.map(b => {
          const on = b.d === d;
          return (
            <button key={b.d} onClick={() => setD(b.d)} style={{ fontFamily: "var(--fh)", fontSize: "0.82rem", padding: "6px 6px", borderRadius: 7, border: "none", cursor: "pointer", background: on ? "var(--white)" : "transparent", color: on ? "var(--forest)" : "var(--muted)", boxShadow: on ? "0 1px 3px rgba(20,49,9,.14)" : "none", display: "flex", flexDirection: "column", alignItems: "center", gap: 1, transition: "background .15s" }}>
              <span>{b.label}</span>
              <span style={{ fontFamily: "var(--fb)", fontSize: "0.66rem", color: on ? accColor(b.acc) : "var(--muted)", fontVariantNumeric: "tabular-nums" }}>{b.acc}%</span>
            </button>
          );
        })}
      </div>

      {/* selected band card */}
      <div style={{ border: "1px solid var(--sage-l)", borderRadius: 12, overflow: "hidden", background: "#FBFCF6", borderLeft: `4px solid ${dc}` }}>
        {/* score + time */}
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
          <div style={{ padding: "14px 16px", borderRight: "1px dashed var(--sage-l)" }}>
            <div style={{ fontFamily: "var(--fp)", fontSize: "0.62rem", letterSpacing: "0.06em", textTransform: "uppercase", color: "var(--muted)" }}>Score</div>
            <div style={{ display: "flex", alignItems: "baseline", gap: 7, marginTop: 2 }}>
              <span style={{ fontFamily: "var(--fh)", fontSize: "1.7rem", color: accColor(band.acc), lineHeight: 1, fontVariantNumeric: "tabular-nums" }}>{band.acc}%</span>
              <span style={{ fontFamily: "var(--fp)", fontSize: "0.7rem", color: "var(--muted)" }}>{band.correct} / {band.n}</span>
            </div>
            {band.cohort != null && <div style={{ fontFamily: "var(--fp)", fontSize: "0.64rem", color: vs >= 0 ? "var(--success)" : "var(--error)", marginTop: 3 }}>{vs >= 0 ? "+" : ""}{vs} vs cohort</div>}
          </div>
          <div style={{ padding: "14px 16px" }}>
            <div style={{ fontFamily: "var(--fp)", fontSize: "0.62rem", letterSpacing: "0.06em", textTransform: "uppercase", color: "var(--muted)" }}>Time</div>
            <div style={{ fontFamily: "var(--fh)", fontSize: "1.7rem", color: "var(--forest)", lineHeight: 1, marginTop: 2, fontVariantNumeric: "tabular-nums" }}>{fmtDur(band.time || 0)}</div>
            <div style={{ fontFamily: "var(--fp)", fontSize: "0.64rem", color: "var(--muted)", marginTop: 3 }}>{band.n ? Math.round((band.time || 0) / band.n) : 0}s / question avg</div>
          </div>
        </div>
        {/* outcome counts */}
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 1, background: "var(--sage-l)", borderTop: "1px solid var(--sage-l)" }}>
          {counts.map(c => (
            <div key={c.l} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, padding: "9px 16px", background: "var(--white)" }}>
              <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
                <span style={{ width: 10, height: 10, borderRadius: 3, background: c.c, flexShrink: 0 }} />
                <span style={{ fontFamily: "var(--fb)", fontSize: "0.78rem", color: "var(--slate)" }}>{c.l}</span>
              </span>
              <span style={{ fontFamily: "var(--fh)", fontSize: "0.95rem", color: "var(--forest)", fontVariantNumeric: "tabular-nums" }}>{c.n}</span>
            </div>
          ))}
        </div>
        {/* answer changes */}
        <div style={{ borderTop: "1px solid var(--sage-l)", background: "var(--white)", padding: "12px 16px" }}>
          <div style={{ fontFamily: "var(--fp)", fontSize: "0.6rem", letterSpacing: "0.09em", textTransform: "uppercase", color: "var(--sage)", marginBottom: 9 }}>Answer changes</div>
          <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            {changes.map(ch => (
              <div key={ch.l} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
                <span style={{ display: "flex", alignItems: "center", gap: 9 }}>
                  <ChangeIcon from={ch.from} to={ch.to} />
                  <span style={{ fontFamily: "var(--fb)", fontSize: "0.78rem", color: "var(--slate)" }}>{ch.l}</span>
                </span>
                <span style={{ fontFamily: "var(--fh)", fontSize: "0.95rem", color: "var(--forest)", fontVariantNumeric: "tabular-nums" }}>{ch.n}</span>
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

// ─── Score trajectory — box & whisker ───
function Trajectory({ tests, target }) {
  const W = 720, H = 240, padL = 46, padR = 70, padT = 20, padB = 42;
  // Dynamic y-domain from the data + target. This was hardcoded 1180-1500, which pushed
  // any score below 1180 off the chart. Round to hundreds, clamp to the 400-1600 scale.
  const vals = tests.reduce((a, t) => a.concat([t.low, t.high, t.med].filter(v => v != null)), []).concat([target]);
  const dataMin = Math.min.apply(null, vals), dataMax = Math.max.apply(null, vals);
  const yMin = Math.max(400, Math.floor((dataMin - 40) / 100) * 100);
  const yMax = Math.min(1600, Math.ceil((dataMax + 40) / 100) * 100);
  const y = v => padT + (1 - (v - yMin) / (yMax - yMin)) * (H - padT - padB);
  const cw = (W - padL - padR) / tests.length;
  const gridlines = [];
  for (let v = yMin + 100; v < yMax; v += 100) gridlines.push(v);
  return (
    <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: "block" }}>
      <defs>
        <linearGradient id="bwFill" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor="#D9E8C7" /><stop offset="100%" stopColor="#C5D9A8" /></linearGradient>
        <linearGradient id="bwCur" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor="#7CA158" /><stop offset="100%" stopColor="#5C7E3F" /></linearGradient>
      </defs>
      <rect x={padL} y={y(yMax)} width={W - padL - padR} height={y(target) - y(yMax)} fill="var(--forest)" opacity="0.06" />
      <line x1={padL} y1={y(target)} x2={W - padR} y2={y(target)} stroke="var(--forest)" strokeWidth="1.2" strokeDasharray="6,4" />
      <text x={W - padR - 4} y={y(target) - 6} fontFamily="var(--fb)" fontSize="10" fill="var(--forest)" textAnchor="end" letterSpacing="0.05em">TARGET · {target}</text>
      {gridlines.map(v => (
        <g key={v}><line x1={padL} y1={y(v)} x2={W - padR} y2={y(v)} stroke="var(--sage-l)" strokeWidth="1" strokeDasharray="2,3" /><text x={padL - 8} y={y(v) + 3} fontFamily="var(--fp)" fontSize="10" fill="var(--muted)" textAnchor="end">{v}</text></g>
      ))}
      {(() => {
        const pts = tests.map((t, i) => ({ x: padL + (i + 0.5) * cw, y: y(t.med) }));
        const half = 19; // half of box width (bw = 38) — keep the connector outside the boxes
        const segs = [];
        for (let i = 0; i < pts.length - 1; i++) segs.push(`M ${pts[i].x + half} ${pts[i].y} L ${pts[i + 1].x - half} ${pts[i + 1].y}`);
        return <path d={segs.join(" ")} fill="none" stroke="var(--forest)" strokeWidth="1.6" strokeLinecap="round" opacity="0.4" />;
      })()}
      {tests.map((t, i) => {
        const cx = padL + (i + 0.5) * cw, bw = 38;
        const cur = t.kind === "current", ext = t.kind === "external";
        const stroke = cur ? "var(--forest)" : "var(--sage)";
        return <g key={i} opacity={ext ? 0.6 : 1}>
          <line x1={cx} y1={y(t.high)} x2={cx} y2={y(t.low)} stroke={stroke} strokeWidth="1.4" strokeDasharray={ext ? "3,2" : "none"} />
          <line x1={cx - 10} y1={y(t.high)} x2={cx + 10} y2={y(t.high)} stroke={stroke} strokeWidth="1.4" />
          <line x1={cx - 10} y1={y(t.low)} x2={cx + 10} y2={y(t.low)} stroke={stroke} strokeWidth="1.4" />
          <rect x={cx - bw / 2} y={y(t.q3)} width={bw} height={y(t.q1) - y(t.q3)} rx="3" fill={ext ? "var(--white)" : (cur ? "url(#bwCur)" : "url(#bwFill)")} stroke={stroke} strokeWidth="1.4" />
          <text x={cx} y={y(t.med) + 3.6} fontFamily="var(--fb)" fontSize="10.5" fontWeight="700" fill={cur ? "#fff" : "var(--forest)"} textAnchor="middle" style={{ fontVariantNumeric: "tabular-nums" }}>{t.med}</text>
          <text x={cx} y={y(t.high) - 9} fontFamily="var(--fb)" fontSize="10" fill="var(--muted)" textAnchor="middle">{t.high}</text>
          <text x={cx} y={y(t.low) + 15} fontFamily="var(--fb)" fontSize="10" fill="var(--muted)" textAnchor="middle">{t.low}</text>
          <text x={cx} y={H - 15} fontFamily="var(--fp)" fontSize="8.5" fill={cur ? "var(--forest)" : "var(--slate)"} textAnchor="middle" fontWeight={cur ? "700" : "600"}>{t.label}</text>
          <text x={cx} y={H - 4} fontFamily="var(--fp)" fontSize="8" fill="var(--muted)" textAnchor="middle">{t.date}</text>
        </g>;
      })}
    </svg>
  );
}

// ─── Cohort distribution ───
// Standard-normal CDF (Abramowitz–Stegun) for percentile-within-cohort.
function _normCdf(z) {
  const t = 1 / (1 + 0.2316419 * Math.abs(z));
  const d = 0.3989423 * Math.exp(-z * z / 2);
  let p = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274))));
  return z > 0 ? 1 - p : p;
}
function _ordinal(n) {
  const s = ["th", "st", "nd", "rd"], v = n % 100;
  return n + (s[(v - 20) % 10] || s[v] || s[0]);
}
function CohortDistribution({ sec }) {
  const W = 520, H = 124, padL = 10, padR = 10, padT = 30, padB = 18;
  const lo = 400, hi = 800;
  const x = v => padL + ((v - lo) / (hi - lo)) * (W - padL - padR);
  const mean = sec.cohortMid, sd = sec.cohortSd, you = sec.scoreMid;
  const norm = v => Math.exp(-0.5 * Math.pow((v - mean) / sd, 2));
  const pts = [];
  for (let v = lo; v <= hi; v += 8) pts.push([x(v), padT + (1 - norm(v)) * (H - padT - padB)]);
  const area = `M ${x(lo)} ${H - padB} L ${pts.map(p => p.join(" ")).join(" L ")} L ${x(hi)} ${H - padB} Z`;
  const pct = Math.max(1, Math.min(99, Math.round(_normCdf((you - mean) / sd) * 100)));
  const ahead = you >= mean;
  // clamp the "you" label so it never runs off the edges
  const labelX = Math.max(padL + 22, Math.min(W - padR - 22, x(you)));
  return (
    <div>
      <div style={{ fontFamily: "var(--fh)", color: "var(--forest)", fontSize: "0.95rem", marginBottom: 2 }}>{sec.name}</div>
      <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: "block" }}>
        <clipPath id={`clipYou-${sec.key}`}><rect x={padL} y={0} width={x(you) - padL} height={H} /></clipPath>
        <path d={area} fill="var(--sage-l)" opacity="0.45" />
        <path d={area} fill={sec.color} opacity="0.3" clipPath={`url(#clipYou-${sec.key})`} />
        <path d={`M ${pts.map(p => p.join(" ")).join(" L ")}`} fill="none" stroke="var(--sage)" strokeWidth="1.5" />
        {/* cohort median */}
        <line x1={x(mean)} y1={padT - 2} x2={x(mean)} y2={H - padB} stroke="var(--muted)" strokeWidth="1.3" strokeDasharray="4,3" />
        <text x={x(mean)} y={H - 5} fontFamily="var(--fp)" fontSize="8.5" fill="var(--muted)" textAnchor="middle">median</text>
        {/* you */}
        <line x1={x(you)} y1={padT - 6} x2={x(you)} y2={H - padB} stroke={sec.color} strokeWidth="2.2" />
        <g transform={`translate(${labelX}, 12)`}>
          <rect x="-22" y="-10" width="44" height="18" rx="9" fill={sec.color} />
          <text x="0" y="3" fontFamily="var(--fh)" fontSize="11" fill="#fff" textAnchor="middle">{you}</text>
        </g>
        {[400, 500, 600, 700, 800].map(v => <text key={v} x={x(v)} y={H - 5} fontFamily="var(--fp)" fontSize="8" fill="var(--muted)" textAnchor="middle" opacity={Math.abs(v - mean) < 24 ? 0 : 0.9}>{v}</text>)}
      </svg>
      {/* stats underneath — percentile leads */}
      <div style={{ display: "flex", alignItems: "baseline", gap: 18, marginTop: 10, paddingTop: 12, borderTop: "1px dashed var(--sage-l)" }}>
        <div>
          <div style={{ display: "flex", alignItems: "baseline", gap: 4 }}>
            <span style={{ fontFamily: "var(--fh)", color: sec.color, fontSize: "1.7rem", lineHeight: 1, fontVariantNumeric: "tabular-nums" }}>{_ordinal(pct)}</span>
            <span style={{ fontFamily: "var(--fp)", fontSize: "0.74rem", color: "var(--muted)" }}>percentile</span>
          </div>
          <div style={{ fontFamily: "var(--fp)", fontSize: "0.66rem", color: "var(--muted)", marginTop: 2 }}>within the 1500+ cohort</div>
        </div>
        <div style={{ width: 1, alignSelf: "stretch", background: "var(--sage-l)" }} />
        <div style={{ display: "flex", gap: 18 }}>
          <div>
            <div style={{ fontFamily: "var(--fb)", color: "var(--forest)", fontSize: "1rem", fontVariantNumeric: "tabular-nums" }}>{you}</div>
            <div style={{ fontFamily: "var(--fp)", fontSize: "0.64rem", color: "var(--muted)", letterSpacing: "0.04em", textTransform: "uppercase" }}>Your score</div>
          </div>
          <div>
            <div style={{ fontFamily: "var(--fb)", color: "var(--muted)", fontSize: "1rem", fontVariantNumeric: "tabular-nums" }}>{mean}</div>
            <div style={{ fontFamily: "var(--fp)", fontSize: "0.64rem", color: "var(--muted)", letterSpacing: "0.04em", textTransform: "uppercase" }}>Cohort median</div>
          </div>
        </div>
      </div>
      <div style={{ fontFamily: "var(--fb)", fontSize: "0.76rem", color: "var(--slate)", lineHeight: 1.5, marginTop: 10 }}>
        {ahead
          ? <>You are ahead of <b style={{ color: "var(--success)" }}>{pct}%</b> of students aiming for 1500+ in {sec.name}, which is a strong relative position.</>
          : <>You are ahead of <b style={{ color: "var(--error)" }}>{pct}%</b> of students aiming for 1500+ in {sec.name}, and this is the gap to close.</>}
      </div>
    </div>
  );
}

function Legend({ c, l, border }) {
  return <span style={{ display: "flex", alignItems: "center", gap: 5 }}><span style={{ width: 10, height: 10, borderRadius: 3, background: c, border: border ? "1px solid var(--sage-l)" : "none" }} />{l}</span>;
}

Object.assign(window, { PerQuestionPanel, TimePanel, ConfidenceMatrix, DifficultyPanel, Trajectory, CohortDistribution, accColor, accTint, Legend, PacingGuide, PACING, pacingBucket, fmtDur, ChangeIcon, STATUS_COLOR });
