/* φQ Chemistry: IA Explorer catalogue app + shared components (window.IAX).
   Mounts on #iax-root when data-app="catalogue". The guided page reuses IAX. */
const { useState, useEffect, useLayoutEffect, useRef, useMemo, useCallback } = React;
const SYL = window.IA_SYL, IDEAS = window.IA_IDEAS || [], INV = window.IA_INVENTORY || [];
const HAS_INV = INV.length > 0;
const TOPIC = {}; SYL.topics.forEach(([c, n, ahl]) => TOPIC[c] = { c, n, ahl: !!ahl });
const themeOf = c => String(c).split('.')[0];
const tcol = c => (SYL.themes[themeOf(c)] || {}).col || 'var(--accent)';
const IDEA = {}; IDEAS.forEach(d => IDEA[d.id] = d);
const loadLS = (k, d) => { try { const v = JSON.parse(localStorage.getItem(k)); return v == null ? d : v; } catch (e) { return d; } };
const saveLS = (k, v) => { try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) {} };

/* ---------- maths helpers ---------- */
function seedRng(str) {
  let h = 1779033703 ^ str.length;
  for (let i = 0; i < str.length; i++) { h = Math.imul(h ^ str.charCodeAt(i), 3432918353); h = h << 13 | h >>> 19; }
  let a = h >>> 0;
  return () => { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; };
}
function gaussFn(r) { return () => { let u = 0, v = 0; while (!u) u = r(); while (!v) v = r(); return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v); }; }
function lsq(X, Y) {
  const n = X.length; if (n < 2) return { m: 0, c: 0, r2: 0 };
  const mx = X.reduce((a, b) => a + b, 0) / n, my = Y.reduce((a, b) => a + b, 0) / n;
  let sxy = 0, sxx = 0, syy = 0;
  for (let i = 0; i < n; i++) { sxy += (X[i] - mx) * (Y[i] - my); sxx += (X[i] - mx) ** 2; syy += (Y[i] - my) ** 2; }
  const m = sxx ? sxy / sxx : 0;
  return { m, c: my - m * mx, r2: syy && sxx ? sxy * sxy / (sxx * syy) : 1 };
}
function xsFor(g, k) {
  const [a, b] = g.x; k = Math.max(2, k || 7);
  return Array.from({ length: k }, (_, i) => g.log ? a * Math.pow(b / a, i / (k - 1)) : a + (b - a) * i / (k - 1));
}
function simulate(g, set, id) {
  const gs = gaussFn(seedRng(id || 'x'));
  const xs = xsFor(g, set && set.k || 7);
  const raw = xs.map(x => { const y0 = g.f(x); const y = y0 * (1 + 0.012 * gs()); return { x, y, e: Math.abs(y) * 0.03 }; });
  const lin = raw.map(p => { const Y = g.Y(p.x, p.y); const a = g.Y(p.x, p.y + p.e), b = g.Y(p.x, p.y - p.e); const e = isFinite(a) && isFinite(b) ? Math.abs(a - b) / 2 : 0; return { x: g.X(p.x), y: Y, e }; }).filter(p => isFinite(p.x) && isFinite(p.y));
  const fit = lsq(lin.map(p => p.x), lin.map(p => p.y));
  const [a, b] = g.x;
  const curve = Array.from({ length: 61 }, (_, i) => { const x = g.log ? a * Math.pow(b / a, i / 60) : a + (b - a) * i / 60; return [x, g.f(x)]; }).filter(p => isFinite(p[1]));
  return { raw, lin, fit, curve };
}
function niceStep(span, n) { const raw = span / Math.max(1, n); const p = Math.pow(10, Math.floor(Math.log10(raw))); const f = raw / p; return (f < 1.5 ? 1 : f < 2.25 ? 2 : f < 3.5 ? 2.5 : f < 7.5 ? 5 : 10) * p; }
function ticks(lo, hi, n) { if (!(hi > lo)) { hi = lo + 1; } const s = niceStep(hi - lo, n); const out = []; for (let v = Math.ceil(lo / s - 1e-9) * s; v <= hi + s * 1e-9; v += s) out.push(Math.abs(v) < s * 1e-9 ? 0 : v); return { list: out, step: s }; }
const SUPC = { '-': '⁻', '0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴', '5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹' };
function tickLabel(v, step, big) {
  if (v === 0) return '0';
  if (big) { const e = Math.floor(Math.log10(Math.abs(big))); const m = v / Math.pow(10, e); const d = Math.max(0, Math.min(3, -Math.floor(Math.log10(step / Math.pow(10, e)) + 1e-9))); return m.toFixed(d).replace('-', '−'); }
  const d = Math.max(0, Math.min(6, -Math.floor(Math.log10(step) + 1e-9) + (step / Math.pow(10, Math.floor(Math.log10(step))) === 2.5 ? 1 : 0)));
  return v.toFixed(d).replace('-', '−');
}

/* ---------- kit / stock ---------- */
const lc = s => String(s).toLowerCase();
function splitItem(s) { const i = s.indexOf(';'); return i < 0 ? [s, ''] : [s.slice(0, i), s.slice(i + 1).trim()]; }
function kitInfo(str, chem) {
  const [key, detail] = chem ? splitItem(str) : [str, ''];
  const own = /^@/.test(key);
  const alts = key.split('|').map(a => ({ t: a.replace(/^[!@]/, '').trim(), bang: /^!/.test(a.trim()), at: /^@/.test(a.trim()) }));
  const label = alts.filter(a => !a.bang || alts.length === 1).map(a => a.t).join(' or ') || alts[0].t;
  if (own) return { kind: 'own', label: alts.map(a => a.t).join(' or '), detail, rows: [], key: lc(label) };
  let rows = [];
  if (HAS_INV) alts.filter(a => !a.at).forEach(a => { INV.forEach(r => { if ((chem ? r[4] === 'Chemical' : r[4] !== 'Chemical') && lc(r[1]).includes(lc(a.t))) rows.push(r); }); });
  rows = [...new Set(rows)];
  const stocked = rows.filter(r => r[2] > 0);
  const kind = stocked.length ? 'ok' : 'no';
  return { kind, label: (chem ? alts[0].t : label), detail, rows: stocked.length ? stocked : rows, key: lc(alts[0].t), chem: !!chem };
}
function ideaKit(d) { return [...(d.chem || []).map(s => kitInfo(s, true)), ...(d.kit || []).map(s => kitInfo(s, false))]; }
function toSource(d) { return ideaKit(d).filter(k => k.kind === 'no').length; }
const cap = s => s ? s[0].toUpperCase() + s.slice(1) : s;
const sym = s => String(s || '').split(' / ')[0];
const DIFF = ['', 'Straightforward', 'Moderate', 'Demanding'];

/* ---------- formula component ---------- */
function Tx({ s, as, className, style }) {
  const r = useRef(null);
  useLayoutEffect(() => { IA_SETTEX(r.current, s == null ? '' : String(s)); }, [s]);
  return React.createElement(as || 'span', { ref: r, className, style });
}
function SvgLabel({ s, x, y, anchor, rotate, cls }) {
  const segs = IA_SVG(s); let shift = 0;
  return <text className={cls || 'lbl'} x={x} y={y} textAnchor={anchor || 'middle'} transform={rotate ? `rotate(-90 ${x} ${y})` : undefined}>
    {segs.map((g, i) => { const want = g.sub ? 3.5 : g.sup ? -5 : 0; const dy = want - shift; shift = want; return <tspan key={i} dy={dy || undefined} fontSize={g.sub || g.sup ? 9 : undefined} fontStyle={g.it ? 'italic' : undefined}>{g.t}</tspan>; })}
  </text>;
}

/* ---------- plot ---------- */
function Plot({ pts, curve, fit, xl, yl, origin, title }) {
  const clipId = useMemo(() => 'c' + Math.random().toString(36).slice(2, 8), []);
  const W = 380, H = 250, L = 58, Rm = 12, T = 12, B = 44;
  const xsA = [...pts.map(p => p.x), ...(curve || []).map(p => p[0])], ysA = [...pts.flatMap(p => [p.y - (p.e || 0), p.y + (p.e || 0)]), ...(curve || []).map(p => p[1])];
  if (origin) { xsA.push(0); ysA.push(0); }
  if (!xsA.length) return null;
  let x0 = Math.min(...xsA), x1 = Math.max(...xsA), y0 = Math.min(...ysA), y1 = Math.max(...ysA);
  if (x1 === x0) { x1 += 1; x0 -= 1; } if (y1 === y0) { y1 += Math.abs(y1) * 0.1 || 1; y0 -= Math.abs(y0) * 0.1 || 1; }
  const px = (x1 - x0) * 0.06, py = (y1 - y0) * 0.08; x0 -= px; x1 += px; y0 -= py; y1 += py;
  if (origin) { if (x0 > -px && x0 < 0) x0 = Math.min(x0, 0); if (y0 > -py && y0 < 0) y0 = Math.min(y0, 0); }
  const tx = ticks(x0, x1, 5), ty = ticks(y0, y1, 5);
  const bigOf = (a, b) => { const m = Math.max(Math.abs(a), Math.abs(b)); return (m < 1e-2 || m >= 1e4) ? m : 0; };
  const bx = bigOf(x0, x1), by = bigOf(y0, y1);
  const sx = v => L + (v - x0) / (x1 - x0) * (W - L - Rm), sy = v => T + (1 - (v - y0) / (y1 - y0)) * (H - T - B);
  const expTag = b => b ? ' (× 10' + String(Math.floor(Math.log10(b))).replace(/[-0-9]/g, c => SUPC[c]) + ')' : '';
  return <svg className="iax-svg" viewBox={`0 0 ${W} ${H}`} role="img" aria-label={title || (yl + ' against ' + xl)}>
    <defs><clipPath id={clipId}><rect x={L} y={T} width={W - L - Rm} height={H - T - B} /></clipPath></defs>
    {tx.list.map((v, i) => <g key={'x' + i}><line className="gr" x1={sx(v)} x2={sx(v)} y1={T} y2={H - B} /><text className="tk" x={sx(v)} y={H - B + 13} textAnchor="middle">{tickLabel(v, tx.step, bx)}</text></g>)}
    {ty.list.map((v, i) => <g key={'y' + i}><line className="gr" x1={L} x2={W - Rm} y1={sy(v)} y2={sy(v)} /><text className="tk" x={L - 5} y={sy(v) + 3} textAnchor="end">{tickLabel(v, ty.step, by)}</text></g>)}
    <line className="ax" x1={L} x2={W - Rm} y1={H - B} y2={H - B} /><line className="ax" x1={L} x2={L} y1={T} y2={H - B} />
    {origin && x0 <= 0 && x1 >= 0 && <line className="ax" x1={sx(0)} x2={sx(0)} y1={T} y2={H - B} strokeDasharray="2 3" />}
    {origin && y0 <= 0 && y1 >= 0 && <line className="ax" x1={L} x2={W - Rm} y1={sy(0)} y2={sy(0)} strokeDasharray="2 3" />}
    <g clipPath={`url(#${clipId})`}>
      {curve && curve.length > 1 && <polyline className="th" points={curve.map(p => sx(p[0]) + ',' + sy(p[1])).join(' ')} />}
      {fit && <line className="fit" x1={sx(x0)} y1={sy(fit.m * x0 + fit.c)} x2={sx(x1)} y2={sy(fit.m * x1 + fit.c)} />}
      {pts.map((p, i) => <g key={i}>{p.e > 0 && <line className="eb" x1={sx(p.x)} x2={sx(p.x)} y1={sy(p.y - p.e)} y2={sy(p.y + p.e)} />}<circle className="pt" cx={sx(p.x)} cy={sy(p.y)} r="3" /></g>)}
    </g>
    <SvgLabel s={xl + expTag(bx)} x={L + (W - L - Rm) / 2} y={H - 8} />
    <SvgLabel s={yl + expTag(by)} x={13} y={T + (H - T - B) / 2} rotate />
  </svg>;
}

function Dots({ d }) { return <span className="iax-dots" title={DIFF[d]}>{[1, 2, 3].map(i => <i key={i} className={i <= d ? 'f' : ''} />)}</span>; }
function HzPill({ hz }) { return <span className={'iax-pill hz' + hz}>{hz === 1 ? 'Low hazard' : hz === 2 ? 'Moderate hazard' : 'Teacher sign-off'}</span>; }
function IdeaPills({ d, kit }) {
  const n = HAS_INV ? toSource(d) : 0;
  return <div className="iax-pills">
    <span className={'iax-pill' + (d.lv === 'HL' ? ' hl' : '')}>{d.lv === 'HL' ? 'HL' : 'SL + HL'}</span>
    <span className={'iax-pill' + (d.db ? ' db' : '')}>{d.db ? 'Database' : 'Lab'}</span>
    {d.bx ? <span className="iax-pill bx">Beyond IB</span> : null}
    {d.tc ? <span className="iax-pill tc" title={d.tech}>⚙ Tech-enabled</span> : null}
    {HAS_INV && kit !== false && !d.db && <span className={'iax-pill ' + (n ? 'no' : 'ok')}>{n ? n + (n === 1 ? ' item' : ' items') + ' to source' : 'Kit in stock'}</span>}
    {!d.db && <HzPill hz={d.hz} />}
  </div>;
}

/* RQ helpers shared with the guided page */
function modelEq(th) { const parts = String(th).split(/, so |: | so that /); for (let i = parts.length - 1; i >= 0; i--) if (/=/.test(parts[i])) return parts[i].trim(); return String(th); }
function shortLit(l) { return String(l).split(/ \(|; /)[0].trim(); }
function fromWhat(q) { const h = lc(q.how); return /intercept/.test(h) && !/gradient/.test(h) ? 'the intercept' : /intercept/.test(h) ? 'the gradient and intercept' : 'the gradient'; }

window.IAX = { SYL, IDEAS, IDEA, INV, HAS_INV, TOPIC, themeOf, tcol, loadLS, saveLS, seedRng, lsq, simulate, xsFor, kitInfo, ideaKit, toSource, cap, sym, Tx, Plot, Dots, HzPill, IdeaPills, modelEq, shortLit, fromWhat, DIFF };

/* =================== catalogue app =================== */
const KS = 'chemq-ia-explorer-shortlist', KD = 'chemq-ia-explorer-drafts', KN = 'chemq-ia-explorer-notes';
const RULES = [
  ['IV is calculated', 'From a formula, not just a reading: concentration from a dilution, 1/T from °C, mol from mass.'],
  ['DV is processed', 'Goes through a formula before plotting: rate = 1/t, ln A, ΔH from ΔT, pH to [⟨H+⟩].'],
  ['Uncertainty propagates', 'Glassware tolerances, balance and thermometer resolution, and timing carried through every step.'],
  ['A straight line', 'The raw graph may curve; the plotted graph is linearised.'],
  ['Gradient gives a quantity', 'Eₐ, order, k, ΔH, ΔS, pKₐ, F, ε, K_d, molar volume… compared with a literature value.']
];
const FILTERS = [['all', 'All'], ['lab', 'Lab'], ['db', 'Database'], ['bx', 'Beyond IB'], ['tc', 'Tech-enabled'], ['stock', 'Kit in stock'], ['hl', 'HL only'], ['low', 'Low hazard']];
const passF = (d, f) => f === 'all' || (f === 'lab' && !d.db) || (f === 'db' && d.db) || (f === 'bx' && d.bx) || (f === 'tc' && d.tc) || (f === 'stock' && !d.db && toSource(d) === 0) || (f === 'hl' && d.lv === 'HL') || (f === 'low' && d.hz === 1);
const searchText = d => lc([d.id, d.ti, d.sys, d.q.n, d.iv.n, d.dv.n, TOPIC[d.t].n, d.t, d.tech || '', d.tc ? 'tech technology sensor data logger' : '', ...(d.kit || []), ...(d.chem || [])].map(IA_PLAIN).join(' '));
const SEARCH = {}; IDEAS.forEach(d => SEARCH[d.id] = searchText(d));

function parseHash() { const h = new URLSearchParams(location.hash.slice(1)); return { topic: h.get('topic'), idea: h.get('idea'), view: h.get('view') }; }

function Card({ d, star, toggle, open }) {
  return <div className="iax-card" style={{ '--tc': tcol(d.t) }} onClick={() => open(d.id)} role="button" tabIndex="0" onKeyDown={e => { if (e.key === 'Enter') open(d.id); }}>
    <div className="iax-card-top"><span className="id">{d.id}</span><Dots d={d.d} />
      <button className={'iax-star' + (star ? ' on' : '')} aria-label={star ? 'Remove from shortlist' : 'Add to shortlist'} onClick={e => { e.stopPropagation(); toggle(d.id); }}>{star ? '★' : '☆'}</button></div>
    <Tx as="h3" s={d.ti} />
    <Tx className="vs" s={sym(d.lin.y) + ' against ' + sym(d.lin.x)} />
    <IdeaPills d={d} />
    <div className="gq"><Tx s={'gradient → ' + d.lin.m} /> · <Tx s={d.q.n} /></div>
  </div>;
}

function Explore({ topic, setTopic, shortlist, toggle, open }) {
  const [filter, setFilter] = useState('all');
  const ideas = IDEAS.filter(d => d.t === topic);
  const shown = ideas.filter(d => passF(d, filter));
  const T = TOPIC[topic];
  return <>
    <div className="iax-rules">{RULES.map(([b, s], i) => <div className="iax-rule" key={b}><i>RULE {i + 1}</i><b>{b}</b><Tx s={s} /></div>)}</div>
    <div className="iax-layout">
      <nav className="iax-rail" aria-label="Topics">
        {Object.keys(SYL.themes).map(th => <div className="iax-rail-group" key={th}>
          <span className="iax-rail-h" style={{ color: tcol(th) }}>{th} · {SYL.themes[th].n}</span>
          {SYL.topics.filter(t => themeOf(t[0]) === th).map(([c, n]) => <button key={c} className={'iax-rail-t' + (c === topic ? ' on' : '')} onClick={() => setTopic(c)}><b style={{ color: tcol(c) }}>{c}</b><span>{n}</span><em>{IDEAS.filter(d => d.t === c).length}</em></button>)}
        </div>)}
      </nav>
      <div className="iax-main">
        <select className="iax-select iax-rail-sel" value={topic} onChange={e => setTopic(e.target.value)} aria-label="Topic">
          {SYL.topics.map(([c, n]) => <option key={c} value={c}>{c} {n} ({IDEAS.filter(d => d.t === c).length})</option>)}
        </select>
        <div className="iax-head">
          <span className="iax-mono" style={{ color: tcol(topic) }}>{themeOf(topic)} · {SYL.themes[themeOf(topic)].n}</span>
          <h2>{topic} {T.n}{T.ahl ? ' (AHL)' : ''}</h2>
          <span className="iax-muted">{ideas.length} {ideas.length === 1 ? 'idea' : 'ideas'}</span>
        </div>
        {SYL.note[topic] && <div className="iax-note">{SYL.note[topic]}</div>}
        <div className="iax-chips" role="group" aria-label="Filters">{FILTERS.filter(f => HAS_INV || f[0] !== 'stock').map(([k, l]) => <button key={k} className={'iax-chip' + (filter === k ? ' on' : '')} onClick={() => setFilter(k)}>{l}</button>)}</div>
        {shown.length ? <div className="iax-grid">{shown.map(d => <Card key={d.id} d={d} star={shortlist.includes(d.id)} toggle={toggle} open={open} />)}</div>
          : <div className="iax-empty">No ideas in this topic match that filter.</div>}
      </div>
    </div>
  </>;
}

function defaultDraft(d) { const s = d.iv.set; return { min: s.r[0], max: s.r[1], k: s.k, sys: d.sys, ctrls: d.ct.map((_, i) => i < 2), model: true }; }
function buildRQ(d, dr) {
  const s = d.iv.set, u = s.u ? ' ' + s.u : '';
  const ctrl = d.ct.filter((_, i) => dr.ctrls[i]);
  const ctrlTxt = ctrl.length > 1 ? ctrl.slice(0, -1).join(', ') + ' and ' + ctrl[ctrl.length - 1] : ctrl[0] || '';
  return 'How does varying ' + s.v + ' from ' + dr.min + ' to ' + dr.max + u + ' (' + dr.k + ' values), and hence ' + d.iv.n + ' (' + d.iv.s + '), affect ' + d.dv.n + ' (' + d.dv.s + ') of ' + dr.sys +
    (ctrlTxt ? ', with ' + ctrlTxt + ' kept constant' : '') + (dr.model ? ', using the model ' + modelEq(d.th) : '') +
    ', and how does the value of ' + d.q.n + ' found from ' + fromWhat(d.q) + ' of ' + sym(d.lin.y) + ' against ' + sym(d.lin.x) + ' compare with ' + shortLit(d.q.lit) + '?';
}

function RQBuilder({ d, drafts, setDrafts, star, toggle }) {
  const dr = Object.assign(defaultDraft(d), drafts[d.id] || {});
  const set = p => setDrafts(x => ({ ...x, [d.id]: Object.assign({}, dr, p) }));
  const rq = buildRQ(d, dr);
  const nC = dr.ctrls.filter(Boolean).length;
  const ticksL = [['IV with range', +dr.max > +dr.min && dr.k >= 2], ['DV named', true], ['System specified', String(dr.sys).trim().length > 20], ['Controls stated (≥ 2)', nC >= 2], ['Theory named', dr.model], ['Comparison value', true]];
  const warns = [];
  if (+dr.k < 5) warns.push('Fewer than 5 values: you need at least five to judge whether the line is straight.');
  if (+dr.min > 0 && +dr.max / +dr.min < 2) warns.push('The highest value is less than twice the lowest: widen the range so the trend stands out from the uncertainty.');
  const [copied, setCopied] = useState(false);
  const copy = () => { const t = IA_PLAIN(rq); (navigator.clipboard ? navigator.clipboard.writeText(t) : Promise.reject()).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); }).catch(() => window.prompt('Copy your research question:', t)); };
  return <section className="iax-panel no-print">
    <div className="iax-sec-h"><b>RQ</b><h3>Draft your research question</h3></div>
    <div className="iax-rq">
      <div className="iax-field"><label>Lowest value{d.iv.set.u ? ' / ' + d.iv.set.u : ''}</label><input className="iax-input" type="number" value={dr.min} onChange={e => set({ min: e.target.value })} /></div>
      <div className="iax-field"><label>Highest value{d.iv.set.u ? ' / ' + d.iv.set.u : ''}</label><input className="iax-input" type="number" value={dr.max} onChange={e => set({ max: e.target.value })} /></div>
      <div className="iax-field"><label>Number of values</label><input className="iax-input" type="number" min="2" max="20" value={dr.k} onChange={e => set({ k: e.target.value })} /></div>
      <div className="iax-field"><label>You set</label><Tx className="iax-muted" s={d.iv.set.v} /></div>
      <div className="iax-field wide"><label>The system</label><textarea className="iax-ta" style={{ minHeight: '3.5rem' }} value={IA_PLAIN(dr.sys)} onChange={e => set({ sys: e.target.value })} /></div>
    </div>
    <div className="iax-checks">{d.ct.map((c, i) => <label key={i}><input type="checkbox" checked={!!dr.ctrls[i]} onChange={e => { const a = dr.ctrls.slice(); a[i] = e.target.checked; set({ ctrls: a }); }} /><Tx s={c} /></label>)}
      <label><input type="checkbox" checked={dr.model} onChange={e => set({ model: e.target.checked })} />Name the model in the RQ</label></div>
    <Tx as="div" className="iax-rq-out" s={rq} />
    <div className="iax-chips">{ticksL.map(([l, ok]) => <span key={l} className={'iax-tick' + (ok ? ' ok' : '')}>{ok ? '✓ ' : '○ '}{l}</span>)}</div>
    {warns.map(w => <span key={w} className="iax-warn">{w}</span>)}
    <div className="iax-chips"><button className="iax-btn pri" onClick={copy}>{copied ? 'Copied' : 'Copy RQ'}</button>
      <button className="iax-btn" onClick={() => { if (!star) toggle(d.id); }} disabled={star}>{star ? 'On your shortlist' : 'Save to shortlist'}</button>
      <button className="iax-btn" onClick={() => setDrafts(x => { const y = { ...x }; delete y[d.id]; return y; })}>Reset</button></div>
  </section>;
}

function KitTable({ d }) {
  const items = ideaKit(d);
  if (!items.length) return null;
  return <section className="iax-panel">
    <div className="iax-sec-h"><b>KIT</b><h3>Apparatus and chemicals</h3></div>
    <div className="iax-scroll"><table className="iax-table"><thead><tr><th>Item</th><th>Status</th>{HAS_INV && <th>Location</th>}</tr></thead><tbody>
      {items.map((k, i) => <tr key={i}>
        <td><Tx s={cap(k.label) + (k.detail ? ', ' + k.detail : '')} /></td>
        {k.kind === 'own' ? <td className="own">Own device or online data</td>
          : !HAS_INV ? <td className="iax-muted">Check with your technician</td>
          : k.kind === 'ok' ? <td className="ok">In stock: {k.rows.slice(0, 3).map(r => r[1] + ' × ' + r[2]).join('; ')}{k.rows.length > 3 ? ' …' : ''}</td>
          : <td className="no">{k.rows.length ? 'None in stock (' + k.rows[0][1] + ')' : 'Not in inventory'}</td>}
        {HAS_INV && <td>{k.kind === 'ok' ? [...new Set(k.rows.map(r => r[3]).filter(Boolean))].slice(0, 3).join(', ') : ''}</td>}
      </tr>)}
    </tbody></table></div>
  </section>;
}

function Detail({ d, shortlist, toggle, drafts, setDrafts, back, open }) {
  const sim = useMemo(() => simulate(d.g, d.iv.set, d.id), [d.id]);
  const sibs = IDEAS.filter(x => x.t === d.t), i = sibs.indexOf(d);
  const star = shortlist.includes(d.id);
  const originY = /^0/.test(d.lin.c);
  return <div className="iax-main" style={{ '--tc': tcol(d.t) }}>
    <div className="iax-detail-nav no-print"><button className="iax-btn" onClick={back}>← {d.t} {TOPIC[d.t].n}</button>
      <span style={{ marginLeft: 'auto', display: 'flex', gap: '.4rem' }}>
        <button className="iax-btn" disabled={i <= 0} onClick={() => open(sibs[i - 1].id)}>← Previous</button>
        <button className="iax-btn" disabled={i >= sibs.length - 1} onClick={() => open(sibs[i + 1].id)}>Next →</button></span></div>
    <header className="iax-detail-h">
      <div className="iax-card-top"><span className="id iax-mono" style={{ color: 'var(--tc)', fontWeight: 700 }}>{d.id}</span><Dots d={d.d} /><span className="iax-muted">{DIFF[d.d]}</span>
        <button className={'iax-star' + (star ? ' on' : '')} onClick={() => toggle(d.id)} aria-label="Shortlist">{star ? '★' : '☆'}</button></div>
      <Tx as="h2" s={d.ti} />
      <div className="iax-sys"><b style={{ color: 'var(--text)' }}>The system: </b><Tx s={d.sys} /></div>
      {d.tc ? <div className="iax-tech"><b>⚙ Technology: </b><Tx s={d.tech} /></div> : null}
      <IdeaPills d={d} />
      {d.hz === 3 && <div className="iax-hz3">Agree this with your teacher before you start.</div>}
    </header>

    <section className="iax-panel">
      <div className="iax-sec-h"><b>1–3</b><h3>The two variables</h3></div>
      <div className="iax-two">
        <div className="iax-box"><span className="k">Independent variable</span><h4><Tx s={cap(d.iv.n) + ' ' + d.iv.s + (d.iv.u ? ' / ' + d.iv.u : '')} /></h4>
          <div className="iax-kv"><span>You measure</span><Tx s={d.iv.m} /><span>You calculate</span><Tx s={d.iv.c} /></div></div>
        <div className="iax-box"><span className="k">Dependent variable</span><h4><Tx s={cap(d.dv.n) + ' ' + d.dv.s + (d.dv.u ? ' / ' + d.dv.u : '')} /></h4>
          <div className="iax-kv"><span>You measure</span><Tx s={d.dv.m} /><span>You process</span><Tx s={d.dv.p} /><span>Uncertainty</span><Tx s={d.dv.e} /></div></div>
      </div>
    </section>

    <section className="iax-panel">
      <div className="iax-sec-h"><b>4</b><h3>From a curve to a straight line</h3></div>
      <div className="iax-plots">
        <div className="iax-plot"><span className="cap">Raw data</span><Plot pts={sim.raw} curve={sim.curve} xl={d.g.rx} yl={d.g.ry} /></div>
        <div className="iax-plot"><span className="cap">Linearised</span><Plot pts={sim.lin} fit={sim.fit} xl={d.lin.x} yl={d.lin.y} origin={originY} /></div>
      </div>
      <p className="iax-fine">Simulated from the model with 1.2 % random scatter and 3 % error bars on the raw values; the same every time you open this idea. Shape only: your own data and error bars decide the result. Draw maximum and minimum slopes through your own error bars.</p>
      <div className="iax-three">
        <div className="iax-box"><span className="k">Model</span><Tx className="v" s={d.th} /></div>
        <div className="iax-box"><span className="k">Plot</span><Tx className="v" s={sym(d.lin.y) + ' against ' + sym(d.lin.x)} /></div>
        <div className="iax-box"><span className="k">Intercept</span><Tx className="v" s={d.lin.c} /></div>
      </div>
    </section>

    <section className="iax-panel">
      <div className="iax-sec-h"><b>5</b><h3>What the gradient gives you</h3></div>
      <div className="iax-three">
        <div className="iax-box"><span className="k">Gradient =</span><Tx className="v" s={d.lin.m} /></div>
        <div className="iax-box"><span className="k">So you find</span><Tx className="v" s={cap(d.q.n) + ': ' + d.q.how} /></div>
        <div className="iax-box gold"><span className="k">Compare with</span><Tx className="v" s={d.q.lit} /></div>
      </div>
      <div className="iax-result"><span className="iax-mono" style={{ color: 'var(--text-muted)' }}>From the simulated data</span><Tx s={d.g.fit(sim.fit.m, sim.fit.c)} /></div>
    </section>

    <RQBuilder d={d} drafts={drafts} setDrafts={setDrafts} star={star} toggle={toggle} />
    <KitTable d={d} />
    {(d.sf.length > 0 || !d.db) && <section className="iax-panel"><div className="iax-sec-h"><b>SAFE</b><h3>Safety and disposal</h3></div>
      {d.sf.length ? <ul className="iax-list">{d.sf.map(s => <li key={s}><Tx s={s} /></li>)}</ul> : <span className="iax-muted">Standard lab rules apply.</span>}</section>}
    <div className="iax-two">
      <section className="iax-panel"><div className="iax-sec-h"><b>+</b><h3>Make it yours</h3></div><ul className="iax-list">{d.tw.map(s => <li key={s}><Tx s={s} /></li>)}</ul></section>
      <section className="iax-panel"><div className="iax-sec-h"><b>!</b><h3>Watch out</h3></div><ul className="iax-list">{d.pf.map(s => <li key={s}><Tx s={s} /></li>)}</ul></section>
    </div>
  </div>;
}

/* equipment view */
const KIT_INDEX = (() => {
  const map = {};
  IDEAS.forEach(d => ideaKit(d).forEach(k => { if (k.kind !== 'ok') return; const id = (k.chem ? 'c:' : 'e:') + k.key; (map[id] = map[id] || { id, label: cap(k.label), chem: k.chem, ideas: new Set(), rows: new Set() }); map[id].ideas.add(d.id); k.rows.forEach(r => map[id].rows.add(r[0])); }));
  return Object.values(map).sort((a, b) => b.ideas.size - a.ideas.size || a.label.localeCompare(b.label));
})();

function KitView({ sel, setSel, open, shortlist, toggle }) {
  const [q, setQ] = useState('');
  const chips = KIT_INDEX.filter(k => !q || lc(k.label).includes(lc(q)));
  const chosen = KIT_INDEX.filter(k => sel.includes(k.id));
  const ideas = chosen.length ? IDEAS.filter(d => chosen.every(k => k.ideas.has(d.id))) : [];
  return <div className="iax-main">
    <div className="iax-head"><h2>Start from equipment</h2><span className="iax-muted">Pick what you have. Ideas that use every item you pick appear below. Only items in stock are listed.</span></div>
    <input className="iax-input" placeholder="Filter items…" value={q} onChange={e => setQ(e.target.value)} />
    <div className="iax-chips">{chips.map(k => <button key={k.id} className={'iax-chip' + (sel.includes(k.id) ? ' on' : '')} onClick={() => setSel(sel.includes(k.id) ? sel.filter(x => x !== k.id) : [...sel, k.id])}>{k.chem ? '⚗ ' : ''}{k.label}<em>{k.ideas.size}</em></button>)}</div>
    {chosen.length > 0 && <div className="iax-chips"><span className="iax-muted">{ideas.length} {ideas.length === 1 ? 'idea uses' : 'ideas use'} all of: {chosen.map(k => k.label).join(', ')}</span><button className="iax-link" onClick={() => setSel([])}>Clear</button></div>}
    {chosen.length > 0 && (ideas.length ? <div className="iax-grid">{ideas.map(d => <Card key={d.id} d={d} star={shortlist.includes(d.id)} toggle={toggle} open={open} />)}</div> : <div className="iax-empty">No single idea uses all of these. Remove one.</div>)}
  </div>;
}

function InventoryView({ goKit }) {
  const [q, setQ] = useState(''), [cat, setCat] = useState('');
  const cats = [...new Set(INV.map(r => r[4]))];
  const used = useMemo(() => { const m = {}; KIT_INDEX.forEach(k => k.rows.forEach(rid => { (m[rid] = m[rid] || []).push(k); })); return m; }, []);
  const rows = INV.filter(r => (!cat || r[4] === cat) && (!q || lc(r[1] + ' ' + r[3] + ' ' + (r[5] || '')).includes(lc(q))));
  return <div className="iax-main">
    <div className="iax-head"><h2>Lab inventory</h2><span className="iax-muted">{INV.length} rows from the school inventory. "Used in" counts ideas that list the item.</span></div>
    <div className="iax-chips"><input className="iax-input" style={{ flex: '1 1 220px' }} placeholder="Search items, locations, hazards…" value={q} onChange={e => setQ(e.target.value)} />
      <select className="iax-select" value={cat} onChange={e => setCat(e.target.value)}><option value="">All categories</option>{cats.map(c => <option key={c}>{c}</option>)}</select></div>
    <div className="iax-scroll"><table className="iax-table"><thead><tr><th>Item</th><th>In stock</th><th>Location</th><th>Category</th><th>Hazard</th><th>Used in</th></tr></thead><tbody>
      {rows.slice(0, 400).map(r => { const u = used[r[0]] || []; const n = new Set(u.flatMap(k => [...k.ideas])).size; return <tr key={r[0]}>
        <td style={{ color: 'var(--text)' }}>{r[1]}</td><td className={r[2] > 0 ? '' : 'no'}>{r[2]}</td><td>{r[3]}</td><td>{r[4]}</td><td>{r[5] || ''}</td>
        <td>{n ? <button className="iax-link" onClick={() => goKit(u.map(k => k.id))}>{n} {n === 1 ? 'idea' : 'ideas'}</button> : '–'}</td></tr>; })}
    </tbody></table></div>
    {rows.length > 400 && <span className="iax-muted">Showing the first 400 of {rows.length}. Narrow the search.</span>}
  </div>;
}

function ShortlistView({ shortlist, toggle, drafts, notes, setNotes, open }) {
  const items = shortlist.map(id => IDEA[id]).filter(Boolean);
  return <div className="iax-main">
    <div className="iax-head no-print" style={{ flexDirection: 'row', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}><h2>My shortlist</h2><span className="iax-muted">{items.length} {items.length === 1 ? 'idea' : 'ideas'}</span>{items.length > 0 && <button className="iax-btn pri" style={{ marginLeft: 'auto' }} onClick={() => window.print()}>Print</button>}</div>
    {!items.length && <div className="iax-empty">Star an idea (☆) to keep it here with its research-question draft and your notes.</div>}
    {items.map(d => { const dr = Object.assign(defaultDraft(d), drafts[d.id] || {}); return <div key={d.id} className="iax-sl" style={{ '--tc': tcol(d.t) }}>
      <div className="iax-card-top"><span className="iax-mono" style={{ color: 'var(--tc)', fontWeight: 700 }}>{d.id} · {d.t}</span><button className="iax-star on no-print" onClick={() => toggle(d.id)} aria-label="Remove">★</button></div>
      <button className="iax-link" style={{ textAlign: 'left', textDecoration: 'none', color: 'var(--text)' }} onClick={() => open(d.id)}><Tx as="h3" s={d.ti} style={{ fontFamily: 'var(--font-serif)', fontSize: '1.2rem' }} /></button>
      <Tx as="div" className="iax-rq-out" s={buildRQ(d, dr)} />
      <div className="iax-kv"><span>IV</span><Tx s={cap(d.iv.n) + ' (' + d.iv.s + ')'} /><span>DV</span><Tx s={cap(d.dv.n) + ' (' + d.dv.s + ')'} /><span>Plot</span><Tx s={sym(d.lin.y) + ' against ' + sym(d.lin.x)} /><span>Gradient</span><Tx s={d.lin.m + ' → ' + d.q.n} /></div>
      <textarea className="iax-ta" placeholder="Notes: what you need to ask, order or try first" value={notes[d.id] || ''} onChange={e => setNotes(n => ({ ...n, [d.id]: e.target.value }))} />
    </div>; })}
  </div>;
}

function Catalogue() {
  const first = SYL.topics[0][0];
  const [route, setRoute] = useState(() => { const h = parseHash(); return { topic: TOPIC[h.topic] ? h.topic : (IDEA[h.idea] ? IDEA[h.idea].t : first), idea: IDEA[h.idea] ? h.idea : null, view: ['kit', 'inventory', 'shortlist'].includes(h.view) && (HAS_INV || h.view === 'shortlist') ? h.view : 'explore' }; });
  const [shortlist, setShortlist] = useState(() => loadLS(KS, []).filter(id => IDEA[id]));
  const [drafts, setDrafts] = useState(() => loadLS(KD, {}));
  const [notes, setNotes] = useState(() => loadLS(KN, {}));
  const [kitSel, setKitSel] = useState([]);
  const [q, setQ] = useState('');
  useEffect(() => saveLS(KS, shortlist), [shortlist]);
  useEffect(() => saveLS(KD, drafts), [drafts]);
  useEffect(() => saveLS(KN, notes), [notes]);
  useEffect(() => { const f = () => { const h = parseHash(); setRoute(r => ({ topic: TOPIC[h.topic] ? h.topic : (IDEA[h.idea] ? IDEA[h.idea].t : r.topic), idea: IDEA[h.idea] ? h.idea : null, view: h.view || 'explore' })); }; window.addEventListener('hashchange', f); return () => window.removeEventListener('hashchange', f); }, []);
  const nav = (r, scroll) => { const nr = { ...route, ...r }; setRoute(nr); const h = nr.view !== 'explore' ? 'view=' + nr.view : nr.idea ? 'idea=' + nr.idea : 'topic=' + nr.topic; history.replaceState(null, '', '#' + h); if (scroll !== false) { const top = document.getElementById('iax-root').offsetTop - 70; if (window.scrollY > top) window.scrollTo({ top, behavior: 'smooth' }); } };
  const toggle = id => setShortlist(s => s.includes(id) ? s.filter(x => x !== id) : [...s, id]);
  const open = id => { setQ(''); nav({ view: 'explore', idea: id, topic: IDEA[id].t }); };
  const surprise = () => { const pool = IDEAS.filter(d => !shortlist.includes(d.id)); const p = pool.length ? pool : IDEAS; open(p[Math.floor(Math.random() * p.length)].id); };
  const hits = q.trim().length > 1 ? IDEAS.filter(d => lc(q).split(/\s+/).every(w => SEARCH[d.id].includes(w))) : null;
  const tabs = [['explore', 'Explore by topic'], ...(HAS_INV ? [['kit', 'Start from equipment'], ['inventory', 'Lab inventory']] : []), ['shortlist', 'My shortlist (' + shortlist.length + ')']];
  return <div className="iax">
    <div className="iax-bar no-print">
      <div className="iax-tabs" role="tablist">{tabs.map(([k, l]) => <button key={k} role="tab" aria-selected={route.view === k} className={'iax-tab' + (route.view === k && !hits ? ' on' : '')} onClick={() => { setQ(''); nav({ view: k, idea: null }); }}>{l}</button>)}</div>
      <div className="iax-search"><input type="search" placeholder="Search ideas, chemicals, kit…" value={q} onChange={e => setQ(e.target.value)} aria-label="Search ideas" /><button className="iax-btn" onClick={surprise}>Surprise me</button></div>
    </div>
    {hits ? <div className="iax-main"><div className="iax-head"><h2>Search</h2><span className="iax-muted">{hits.length} {hits.length === 1 ? 'idea matches' : 'ideas match'} "{q}"</span></div>
      {hits.length ? <div className="iax-grid">{hits.map(d => <Card key={d.id} d={d} star={shortlist.includes(d.id)} toggle={toggle} open={open} />)}</div> : <div className="iax-empty">Nothing matches. Try a chemical name, a technique or a quantity such as "activation energy".</div>}</div>
      : route.view === 'kit' ? <KitView sel={kitSel} setSel={setKitSel} open={open} shortlist={shortlist} toggle={toggle} />
      : route.view === 'inventory' ? <InventoryView goKit={ids => { setKitSel(ids.slice(0, 1)); nav({ view: 'kit', idea: null }); }} />
      : route.view === 'shortlist' ? <ShortlistView shortlist={shortlist} toggle={toggle} drafts={drafts} notes={notes} setNotes={setNotes} open={open} />
      : route.idea ? <Detail key={route.idea} d={IDEA[route.idea]} shortlist={shortlist} toggle={toggle} drafts={drafts} setDrafts={setDrafts} back={() => nav({ idea: null })} open={open} />
      : <Explore topic={route.topic} setTopic={t => nav({ topic: t, idea: null }, false)} shortlist={shortlist} toggle={toggle} open={open} />}
  </div>;
}

(function () { const el = document.getElementById('iax-root'); if (el && el.dataset.app === 'catalogue') ReactDOM.createRoot(el).render(<Catalogue />); })();
