// curriculum.jsx — Curriculum: master catalog (Course×Pace×Stream) + per-class teaching order
// See docs/superpowers/specs/2026-05-24-curriculum-scope-sequence-design.md
// Default catalogs & classes live in components/curriculum-data.js (CURRICULUM_SEED / CURR_CLASSES_SEED),
// loaded BEFORE this file. localStorage holds the working edits; "Export" regenerates that data file.
// Aliased hooks (each file must alias to avoid global-scope redeclaration).

const { useState: useCur, useEffect: useCurEffect, useRef: useCurRef } = React;

// ── Booklet types ─────────────────────────────────────────────
const CURR_TYPES = {
  'Lesson':           { label: 'LESSON',      bg: 'var(--bg-elevated)', fg: 'var(--text-secondary)', accent: null,      exam: false },
  'Topic Test':       { label: 'TOPIC TEST',  bg: '#C9821F',            fg: '#fff',                  accent: '#C9821F', exam: false },
  'Mini Mock Trials': { label: 'MINI MOCK',   bg: '#5C93B8',            fg: '#fff',                  accent: '#4A8BB2', exam: true  },
  'Mock Trials':      { label: 'MOCK TRIALS', bg: '#3F7CA0',            fg: '#fff',                  accent: '#4A8BB2', exam: true  },
  'Mock HSC':         { label: 'MOCK HSC',    bg: '#2E5D78',            fg: '#fff',                  accent: '#4A8BB2', exam: true  },
};
const CURR_TYPE_LIST = Object.keys(CURR_TYPES);
const isExamType = t => CURR_TYPES[t] && CURR_TYPES[t].exam;

const CURR_COURSES = ['YR10', 'STND', 'ADVN', 'EXT1', 'EXT2'];
const CURR_PACES   = ['REG', 'ACC'];
const CURR_STREAMS = ['A1', 'B1', 'C1'];
const catKey = (course, pace, stream) => `${course}|${pace}|${stream}`;

// ── Persistence (localStorage working copy; default seed from curriculum-data.js) ──
const CURR_LS_KEY = 'tm_curriculum_v1';
function loadCurriculum() {
  try { const raw = localStorage.getItem(CURR_LS_KEY); if (raw) { const p = JSON.parse(raw); if (p && p.curriculum && p.classes) return p; } } catch (e) {}
  return { curriculum: JSON.parse(JSON.stringify(CURRICULUM_SEED)), classes: JSON.parse(JSON.stringify(CURR_CLASSES_SEED)) };
}
function saveCurriculum(state) { try { localStorage.setItem(CURR_LS_KEY, JSON.stringify(state)); } catch (e) {} }

// ── Derivation helpers ────────────────────────────────────────
const bookletCode = (cat, i) => `${cat.course}-${cat.pace}-${cat.stream}-${i + 1}`;
const letterFor = i => String.fromCharCode(65 + i);
let _newSeq = 0;
const newBookletId = () => `new-${Date.now().toString(36)}-${_newSeq++}`;
// Course → palette colour (STND maps to the STD teal); tint helper for soft pills.
const curCourseColor = c => SUBJECT_COLORS[c === 'STND' ? 'STD' : c] || '#4A9B7E';
function hexToRgba(hex, a) {
  if (typeof hex !== 'string' || hex[0] !== '#') return hex;
  const h = hex.length === 4 ? hex.slice(1).split('').map(c => c + c).join('') : hex.slice(1);
  const n = parseInt(h, 16);
  return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${a})`;
}

function seriesMap(booklets) {
  const totals = {};
  booklets.forEach(b => { if (b.type === 'Lesson') { const k = b.strand + '|' + b.title; totals[k] = (totals[k] || 0) + 1; } });
  const seen = {}, out = {};
  booklets.forEach(b => {
    if (b.type === 'Lesson') { const k = b.strand + '|' + b.title; const n = (seen[k] || 0); seen[k] = n + 1; out[b.id] = { letter: letterFor(n), pos: n + 1, total: totals[k] }; }
    else out[b.id] = null;
  });
  return out;
}

function topicRuns(booklets) {
  const runs = []; let cur = null;
  booklets.forEach((b, idx) => {
    const key = b.strand + '|' + b.title;
    if (!cur || cur.key !== key) { cur = { key, strand: b.strand, title: b.title, items: [], startIdx: idx }; runs.push(cur); }
    cur.items.push({ b, idx });
  });
  const counts = {}; runs.forEach(r => counts[r.key] = (counts[r.key] || 0) + 1);
  runs.forEach(r => r.split = counts[r.key] > 1);
  return runs;
}

function moveRange(arr, start, count, target) {
  const a = arr.slice(); const seg = a.splice(start, count);
  let t = target; if (t > start) t -= count;
  a.splice(t, 0, ...seg); return a;
}

const matchesFilter = (type, filter) => filter === 'All'
  || (filter === 'Lessons' && type === 'Lesson')
  || (filter === 'Tests' && type === 'Topic Test')
  || (filter === 'Exams' && isExamType(type));

function classRows(cat, cls) {
  const byId = {}; cat.booklets.forEach((b, i) => byId[b.id] = { b, idx: i });
  if (!cls.order) return { rows: cat.booklets.map(b => ({ b, status: 'normal' })), skipped: [], tailored: false, nChanges: 0 };
  const rows = cls.order.map(o => {
    if (o.custom) return { b: o.custom, status: 'added' };
    const found = byId[o.ref];
    return found ? { b: found.b, status: o.moved ? 'moved' : 'normal', fromIdx: found.idx } : null;
  }).filter(Boolean);
  const skipped = (cls.skipped || []).map(id => byId[id] && byId[id].b).filter(Boolean);
  const nChanges = rows.filter(r => r.status === 'moved' || r.status === 'added').length + skipped.length;
  return { rows, skipped, tailored: true, nChanges };
}

function lisIndices(arr) {
  const n = arr.length; if (!n) return new Set();
  const len = new Array(n).fill(1), prev = new Array(n).fill(-1); let best = 0;
  for (let i = 0; i < n; i++) { for (let j = 0; j < i; j++) if (arr[j] < arr[i] && len[j] + 1 > len[i]) { len[i] = len[j] + 1; prev[i] = j; } if (len[i] > len[best]) best = i; }
  const s = new Set(); let k = best; while (k !== -1) { s.add(k); k = prev[k]; } return s;
}
function movedSetFor(cat, refIds) {
  const dIdx = {}; cat.booklets.forEach((b, i) => dIdx[b.id] = i);
  const pos = refIds.map(id => dIdx[id] == null ? 1e9 : dIdx[id]);
  const keep = lisIndices(pos);
  const moved = new Set();
  refIds.forEach((id, i) => { if (!keep.has(i)) moved.add(id); });
  return moved;
}

// ════════════════════════════════════════════════════════════════
// COMPONENTS
// ════════════════════════════════════════════════════════════════

function TypeBadge({ type, mini }) {
  const t = CURR_TYPES[type] || CURR_TYPES['Lesson'];
  return <span style={{ fontSize: mini ? '8.5px' : '9px', fontWeight: 800, letterSpacing: '0.02em',
    padding: '2px 6px', borderRadius: '4px', background: t.bg, color: t.fg, whiteSpace: 'nowrap', flexShrink: 0 }}>{t.label}</span>;
}

function CodeChip({ cat, idx }) {
  const code = bookletCode(cat, idx); const tail = String(idx + 1);
  return <span style={{ fontFamily: 'monospace', fontSize: '10.5px', color: 'var(--text-muted)' }}>{code.slice(0, -tail.length)}<strong style={{ color: '#4A9B7E' }}>{tail}</strong></span>;
}

// ── Catalog view ──────────────────────────────────────────────
function CatalogView({ cat, dispatch, openEdit, openAdd }) {
  const [collapsed, setCollapsed] = useCur({});
  const [drag, setDrag] = useCur(null);
  const [over, setOver] = useCur(null);
  const series = seriesMap(cat.booklets);
  const runs = topicRuns(cat.booklets);
  const courseTint = curCourseColor(cat.course);
  const filter = 'All';   // type filters removed — always show every lesson
  const locked = false;   // (no filter lock — reordering always enabled)
  const toggle = k => setCollapsed(c => ({ ...c, [k]: !c[k] }));
  const setAll = val => { const next = {}; runs.forEach(r => next[r.startIdx] = val); setCollapsed(next); };
  const clearDrag = () => { setDrag(null); setOver(null); };
  const onDropBooklet = idx => { if (!drag) return; const line = over && over.idx === idx ? over.line : 'before'; dispatch.reorder(drag.start, drag.count, line === 'before' ? idx : idx + 1); clearDrag(); };
  const onDropTopic = run => { if (!drag) return; const line = over && over.topic === run.startIdx ? over.line : 'before'; dispatch.reorder(drag.start, drag.count, line === 'before' ? run.startIdx : run.startIdx + run.items.length); clearDrag(); };
  const overLine = isOver => isOver ? { boxShadow: `inset 0 ${over.line === 'before' ? 3 : -3}px 0 0 #4A9B7E` } : null;

  return (
    <div style={{ display: 'flex', gap: '16px', alignItems: 'flex-start' }}>
      <div style={{ width: '184px', flexShrink: 0 }}>
        <div style={{ fontSize: '9.5px', textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--text-muted)', padding: '2px 8px 6px' }}>Topics</div>
        {runs.map(run => (
          <div key={run.startIdx} onClick={() => { setCollapsed(c => ({ ...c, [run.startIdx]: false })); const el = document.getElementById('curtopic-' + run.startIdx); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' }); }}
            style={{ display: 'flex', alignItems: 'center', gap: '7px', padding: '5px 8px', borderRadius: '6px', cursor: 'pointer', fontSize: '11.5px' }}
            onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
            <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontWeight: 600, color: 'var(--text-primary)' }}>{run.title}</span>
            <span style={{ marginLeft: 'auto', fontSize: '10px', color: 'var(--text-muted)', fontFamily: 'monospace' }}>#{run.items[0].idx + 1}</span>
          </div>
        ))}
      </div>

      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '10px', flexWrap: 'wrap' }}>
          <span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>{cat.booklets.length} lessons</span>
          <span style={{ flex: 1 }} />
          <Button size="sm" variant="ghost" onClick={() => setAll(true)}>Collapse all</Button>
          <Button size="sm" variant="ghost" onClick={() => setAll(false)}>Expand all</Button>
        </div>

        {runs.map(run => {
          const isC = !!collapsed[run.startIdx];
          const lessons = run.items.filter(x => x.b.type === 'Lesson').length;
          const tests = run.items.filter(x => x.b.type === 'Topic Test').length;
          const exams = run.items.filter(x => isExamType(x.b.type)).length;
          const visItems = run.items.filter(x => matchesFilter(x.b.type, filter));
          if (locked && visItems.length === 0) return null;
          const topicOver = over && over.topic === run.startIdx;
          return (
            <div id={'curtopic-' + run.startIdx} key={run.startIdx}
              style={{ border: '1px solid var(--border-soft)', borderRadius: '9px', marginBottom: '8px', overflow: 'hidden', background: 'var(--bg-surface)', ...(topicOver ? overLine(true) : null) }}>
              <div draggable={!locked}
                onDragStart={() => !locked && setDrag({ kind: 'topic', start: run.startIdx, count: run.items.length })}
                onDragOver={e => { if (!drag || drag.kind !== 'topic') return; e.preventDefault(); const r = e.currentTarget.getBoundingClientRect(); setOver({ topic: run.startIdx, line: (e.clientY - r.top) < r.height / 2 ? 'before' : 'after' }); }}
                onDrop={() => drag && drag.kind === 'topic' && onDropTopic(run)} onDragEnd={clearDrag} onClick={() => toggle(run.startIdx)}
                style={{ display: 'flex', alignItems: 'center', gap: '9px', padding: '9px 13px', cursor: 'pointer', background: 'var(--bg-surface-muted)', opacity: drag && drag.kind === 'topic' && drag.start === run.startIdx ? 0.4 : 1 }}>
                {!locked && <span title="Drag to reorder topic" style={{ cursor: 'grab', color: 'var(--text-muted)', fontSize: '13px' }}>⠿</span>}
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--text-muted)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ transform: isC ? 'none' : 'rotate(90deg)', transition: '140ms ease' }}><path d="M9 18l6-6-6-6" /></svg>
                <span style={{ display: 'inline-flex', alignItems: 'center', gap: '9px' }}>
                  <span style={{ width: '7px', height: '7px', borderRadius: '50%', background: courseTint, flexShrink: 0 }} />
                  <strong style={{ fontSize: '14.5px', fontWeight: 700, color: 'var(--text-primary)', boxShadow: `inset 0 -2px 0 ${hexToRgba(courseTint, 0.55)}`, paddingBottom: '1px' }}>{run.title}</strong>
                </span>
                {run.split && <span style={{ fontSize: '9.5px', fontWeight: 700, color: '#C9821F', background: 'rgba(201,130,31,0.16)', borderRadius: '4px', padding: '1px 7px' }}>⚡ split topic</span>}
                <span style={{ marginLeft: 'auto', display: 'flex', gap: '9px', alignItems: 'center', fontSize: '11px', color: 'var(--text-muted)' }}>
                  <span style={{ color: 'var(--text-muted)' }}>Syllabus: <span style={{ color: 'var(--text-secondary)', fontWeight: 600 }}>{run.strand}</span></span>
                  {!!lessons && <span style={{ background: 'var(--bg-elevated)', borderRadius: '4px', padding: '1px 6px' }}>{lessons} booklet{lessons > 1 ? 's' : ''}</span>}
                  {!!tests && <span style={{ background: 'rgba(201,130,31,0.16)', color: '#C9821F', borderRadius: '4px', padding: '1px 6px' }}>{tests} test</span>}
                  {!!exams && <span style={{ background: 'rgba(74,139,178,0.18)', color: '#6FA8C9', borderRadius: '4px', padding: '1px 6px' }}>{exams} exam{exams > 1 ? 's' : ''}</span>}
                  <span style={{ fontFamily: 'monospace' }}>#{run.items[0].idx + 1}–{run.items[run.items.length - 1].idx + 1}</span>
                </span>
              </div>
              {!isC && (
                <div>
                  {run.items.map(({ b, idx }) => {
                    if (!matchesFilter(b.type, filter)) return null;
                    const s = series[b.id]; const t = CURR_TYPES[b.type] || CURR_TYPES['Lesson'];
                    const isDragged = drag && drag.kind === 'booklet' && drag.start === idx;
                    const isOver = over && over.idx === idx && drag && drag.kind === 'booklet';
                    return (
                      <div key={b.id} draggable={!locked}
                        onDragStart={e => { if (locked) return; e.stopPropagation(); setDrag({ kind: 'booklet', start: idx, count: 1 }); }}
                        onDragOver={e => { if (!drag || drag.kind !== 'booklet') return; e.preventDefault(); const r = e.currentTarget.getBoundingClientRect(); setOver({ idx, line: (e.clientY - r.top) < r.height / 2 ? 'before' : 'after' }); }}
                        onDrop={() => drag && drag.kind === 'booklet' && onDropBooklet(idx)} onDragEnd={clearDrag} onClick={() => openEdit(b.id)}
                        style={{ display: 'grid', gridTemplateColumns: '20px 28px minmax(150px, 230px) 1fr 56px 110px', gap: '10px', alignItems: 'center', cursor: 'pointer',
                          padding: '7px 13px', borderTop: '1px solid var(--border-soft)', borderLeft: `3px solid ${t.accent || 'transparent'}`, fontSize: '12px',
                          opacity: isDragged ? 0.4 : 1, ...(isOver ? overLine(true) : null) }}>
                        <span style={{ cursor: locked ? 'default' : 'grab', color: 'var(--text-muted)', textAlign: 'center' }}>{locked ? '' : '☰'}</span>
                        <span style={{ textAlign: 'center', fontWeight: 800, color: s ? '#4A9B7E' : 'var(--text-muted)' }}>{s ? s.letter : '–'}</span>
                        <span style={{ minWidth: 0, display: 'flex', alignItems: 'center', gap: '8px' }}>
                          {b.type !== 'Lesson' && <TypeBadge type={b.type} mini />}
                          <span style={{ fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{b.subtitle}</span>
                        </span>
                        <span style={{ color: 'var(--text-muted)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={b.content}>{b.content}</span>
                        <span style={{ textAlign: 'center', fontSize: '10.5px', fontWeight: 700, color: s ? 'var(--text-secondary)' : 'var(--text-muted)', fontVariantNumeric: 'tabular-nums' }}>{s ? `${s.pos}/${s.total}` : '–'}</span>
                        <span style={{ textAlign: 'right' }}><CodeChip cat={cat} idx={idx} /></span>
                      </div>
                    );
                  })}
                  {!locked && <div onClick={() => openAdd(run.items[run.items.length - 1].idx, run.strand, run.title)} style={{ padding: '6px 13px 6px 40px', fontSize: '11.5px', color: '#4A9B7E', cursor: 'pointer', borderTop: '1px solid var(--border-soft)' }}>+ Lesson</div>}
                </div>
              )}
            </div>
          );
        })}
        {!locked && <div onClick={() => dispatch.addTopic()} style={{ margin: '4px 0', padding: '9px', textAlign: 'center', border: '1px dashed var(--border-default)', borderRadius: '8px', color: 'var(--text-muted)', cursor: 'pointer', fontSize: '12px' }}>+ Add topic</div>}
      </div>
    </div>
  );
}

// ── Teaching-order view ───────────────────────────────────────
function TeachingOrderView({ cat, dispatch }) {
  const [drag, setDrag] = useCur(null);
  const [over, setOver] = useCur(null);
  const series = seriesMap(cat.booklets);
  const clearDrag = () => { setDrag(null); setOver(null); };
  const onDrop = idx => { if (drag == null) return; const line = over && over.idx === idx ? over.line : 'before'; dispatch.reorder(drag, 1, line === 'before' ? idx : idx + 1); clearDrag(); };
  return (
    <div>
      <div style={{ fontSize: '12px', color: 'var(--text-muted)', marginBottom: '10px' }}>The company default sequence. Drag a booklet anywhere — a topic can be split across the year (it shows ⚡ in the Catalog view). Codes follow position.</div>
      <div style={{ border: '1px solid var(--border-soft)', borderRadius: '9px', overflow: 'hidden' }}>
        {cat.booklets.map((b, idx) => {
          const prev = cat.booklets[idx - 1];
          const newTopic = !prev || (prev.strand + '|' + prev.title) !== (b.strand + '|' + b.title);
          const s = series[b.id]; const t = CURR_TYPES[b.type] || CURR_TYPES['Lesson'];
          const isOver = over && over.idx === idx;
          return (
            <div key={b.id} draggable onDragStart={() => setDrag(idx)}
              onDragOver={e => { if (drag == null) return; e.preventDefault(); const r = e.currentTarget.getBoundingClientRect(); setOver({ idx, line: (e.clientY - r.top) < r.height / 2 ? 'before' : 'after' }); }}
              onDrop={() => onDrop(idx)} onDragEnd={clearDrag}
              style={{ display: 'grid', gridTemplateColumns: '20px 34px 150px 1fr 150px 116px', gap: '10px', alignItems: 'center',
                padding: '7px 13px', borderTop: newTopic && idx > 0 ? '2px solid var(--border-default)' : '1px solid var(--border-soft)', marginTop: newTopic && idx > 0 ? '3px' : 0,
                borderLeft: `3px solid ${t.accent || 'transparent'}`, fontSize: '12px', background: 'var(--bg-surface)', opacity: drag === idx ? 0.4 : 1, ...(isOver ? { boxShadow: `inset 0 ${over.line === 'before' ? 3 : -3}px 0 0 #4A9B7E` } : null) }}>
              <span style={{ cursor: 'grab', color: 'var(--text-muted)', textAlign: 'center' }}>☰</span>
              <span style={{ textAlign: 'center', fontWeight: 800, color: 'var(--text-secondary)', fontVariantNumeric: 'tabular-nums' }}>{idx + 1}</span>
              <span style={{ minWidth: 0, display: 'flex', alignItems: 'center', gap: '7px' }}>
                {b.type !== 'Lesson' ? <TypeBadge type={b.type} mini /> : <span style={{ fontWeight: 800, color: '#4A9B7E', width: '14px', textAlign: 'center' }}>{s ? s.letter : ''}</span>}
                <span style={{ color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{b.title}</span>
              </span>
              <span style={{ minWidth: 0 }}><strong style={{ color: 'var(--text-primary)' }}>{b.subtitle}</strong> <span style={{ color: 'var(--text-muted)' }}>· {b.content}</span></span>
              <span style={{ color: 'var(--text-muted)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{b.strand}</span>
              <span style={{ textAlign: 'right' }}><CodeChip cat={cat} idx={idx} /></span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ── Read-only compare list ────────────────────────────────────
function CompareList({ cat, rows, series, idxOf, annotate }) {
  let prevTopic = null;
  return (
    <div style={{ border: '1px solid var(--border-soft)', borderRadius: '9px', overflow: 'hidden' }}>
      {rows.map((r, i) => {
        const b = r.b; const topicKey = b.strand + '|' + b.title;
        const newTopic = i > 0 && topicKey !== prevTopic; prevTopic = topicKey;
        const s = series[b.id]; const t = CURR_TYPES[b.type] || CURR_TYPES['Lesson'];
        const accent = r.status === 'moved' ? '#C9821F' : r.status === 'added' ? '#4A9B7E' : (t.accent || 'transparent');
        return (
          <div key={b.id + i} style={{ display: 'grid', gridTemplateColumns: '30px 30px 1fr 150px 110px', gap: '10px', alignItems: 'center',
            padding: '7px 13px', borderTop: newTopic ? '2px solid var(--border-default)' : '1px solid var(--border-soft)', marginTop: newTopic ? '3px' : 0,
            borderLeft: `3px solid ${accent}`, fontSize: '12px', background: 'var(--bg-surface)' }}>
            <span style={{ textAlign: 'center', fontWeight: 700, color: 'var(--text-muted)', fontVariantNumeric: 'tabular-nums' }}>{i + 1}</span>
            <span style={{ textAlign: 'center', fontWeight: 800, color: s ? '#4A9B7E' : 'var(--text-muted)' }}>{s ? s.letter : (r.status === 'added' ? '＋' : '–')}</span>
            <span style={{ minWidth: 0, display: 'flex', alignItems: 'center', gap: '7px' }}>
              {annotate && r.status === 'moved' && <span style={{ fontSize: '8.5px', fontWeight: 800, color: '#fff', background: '#C9821F', borderRadius: '4px', padding: '2px 6px' }}>MOVED</span>}
              {annotate && r.status === 'added' && <span style={{ fontSize: '8.5px', fontWeight: 800, color: '#fff', background: '#4A9B7E', borderRadius: '4px', padding: '2px 6px' }}>ADDED</span>}
              {r.status !== 'added' && b.type !== 'Lesson' && <TypeBadge type={b.type} mini />}
              <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}><strong style={{ color: 'var(--text-primary)' }}>{b.title}</strong> <span style={{ color: 'var(--text-muted)' }}>· {b.subtitle}</span></span>
              {annotate && r.status === 'moved' && <span style={{ fontSize: '10px', fontWeight: 700, color: '#C9821F', whiteSpace: 'nowrap' }}>↑ from #{r.fromIdx + 1}</span>}
            </span>
            <span style={{ color: 'var(--text-muted)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{b.content}</span>
            <span style={{ textAlign: 'right', fontFamily: 'monospace', fontSize: '10px', color: 'var(--text-muted)' }}>{r.status === 'added' ? 'custom' : <CodeChip cat={cat} idx={idxOf[b.id]} />}</span>
          </div>
        );
      })}
    </div>
  );
}

function SideBySide({ cat, sel, info }) {
  const skipped = new Set(sel.skipped || []);
  const movedIds = new Set((sel.order || []).filter(o => o.moved).map(o => o.ref));
  const col = (title, children) => <div style={{ flex: 1, borderRight: '1px solid var(--border-soft)' }}>
    <div style={{ padding: '8px 13px', fontSize: '10.5px', textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-muted)', background: 'var(--bg-surface-muted)', borderBottom: '1px solid var(--border-soft)' }}>{title}</div>{children}</div>;
  const line = (key, pos, text, opts = {}) => <div key={key} style={{ display: 'flex', gap: '8px', padding: '6px 13px', borderTop: '1px solid var(--border-soft)', fontSize: '11px', borderLeft: `3px solid ${opts.accent || 'transparent'}`, opacity: opts.grey ? 0.4 : 1 }}>
    <span style={{ fontWeight: 800, color: 'var(--text-muted)', width: 22 }}>{pos}</span>
    <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textDecoration: opts.grey ? 'line-through' : 'none', color: 'var(--text-secondary)' }}>{text}</span></div>;
  return (
    <div style={{ display: 'flex', border: '1px solid var(--border-soft)', borderRadius: '9px', overflow: 'hidden' }}>
      {col('Default order', cat.booklets.map((b, i) => line(b.id, i + 1, `${b.title} · ${b.subtitle}`, { grey: skipped.has(b.id), accent: movedIds.has(b.id) ? '#C9821F' : null })))}
      {col(sel.name, info.rows.map((r, i) => line('c' + i, i + 1, (r.status === 'added' ? '＋ ' : '') + `${r.b.title} · ${r.b.subtitle}` + (r.status === 'moved' ? `  ↑#${r.fromIdx + 1}` : ''), { accent: r.status === 'moved' ? '#C9821F' : r.status === 'added' ? '#4A9B7E' : null })))}
    </div>
  );
}

// ── Class editor ──────────────────────────────────────────────
function ClassEditor({ cat, draft, skipped, series, onReorder, onSkip, onRestore, onRemoveCustom }) {
  const [dragI, setDragI] = useCur(null);
  const [overI, setOverI] = useCur(null);
  const [confirm, setConfirm] = useCur(null);   // { i, name, kind } — pending removal confirmation
  const skippedBooklets = skipped.map(id => cat.booklets.find(b => b.id === id)).filter(Boolean);
  return (
    <div>
      <div style={{ border: '1px solid var(--border-soft)', borderRadius: '9px', overflow: 'hidden' }}>
        {draft.map((e, i) => {
          const b = e.kind === 'custom' ? e.booklet : cat.booklets.find(x => x.id === e.id);
          if (!b) return null;
          const s = e.kind === 'ref' ? series[b.id] : null;
          const isOver = overI && overI.i === i;
          return (
            <div key={i} draggable onDragStart={() => setDragI(i)}
              onDragOver={ev => { if (dragI == null) return; ev.preventDefault(); const r = ev.currentTarget.getBoundingClientRect(); setOverI({ i, line: (ev.clientY - r.top) < r.height / 2 ? 'before' : 'after' }); }}
              onDrop={() => { if (dragI == null) return; const line = overI && overI.i === i ? overI.line : 'before'; onReorder(dragI, line === 'before' ? i : i + 1); setDragI(null); setOverI(null); }}
              onDragEnd={() => { setDragI(null); setOverI(null); }}
              style={{ display: 'grid', gridTemplateColumns: '20px 28px 1fr 150px 60px', gap: '10px', alignItems: 'center', padding: '7px 13px',
                borderTop: '1px solid var(--border-soft)', borderLeft: `3px solid ${e.kind === 'custom' ? '#4A9B7E' : 'transparent'}`, fontSize: '12px',
                background: 'var(--bg-surface)', opacity: dragI === i ? 0.4 : 1, ...(isOver ? { boxShadow: `inset 0 ${overI.line === 'before' ? 3 : -3}px 0 0 #4A9B7E` } : null) }}>
              <span style={{ cursor: 'grab', color: 'var(--text-muted)', textAlign: 'center' }}>☰</span>
              <span style={{ textAlign: 'center', fontWeight: 800, color: s ? '#4A9B7E' : 'var(--text-muted)' }}>{s ? s.letter : (e.kind === 'custom' ? '＋' : '–')}</span>
              <span style={{ minWidth: 0, display: 'flex', alignItems: 'center', gap: '7px' }}>
                {e.kind === 'custom' && <span style={{ fontSize: '8.5px', fontWeight: 800, color: '#fff', background: '#4A9B7E', borderRadius: '4px', padding: '2px 6px' }}>CUSTOM</span>}
                {e.kind === 'ref' && b.type !== 'Lesson' && <TypeBadge type={b.type} mini />}
                <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}><strong style={{ color: 'var(--text-primary)' }}>{b.title}</strong> <span style={{ color: 'var(--text-muted)' }}>· {b.subtitle}</span></span>
              </span>
              <span style={{ color: 'var(--text-muted)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{b.content}</span>
              <span style={{ textAlign: 'right' }}>
                <button onClick={() => setConfirm({ i, name: `${b.title}${b.subtitle ? ' · ' + b.subtitle : ''}`, kind: e.kind })}
                  title={e.kind === 'custom' ? 'Remove custom lesson' : 'Remove lesson'}
                  style={{ background: 'none', border: 'none', color: '#B44040', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'flex-end', width: '100%', padding: 0 }}>
                  <Icon name="trash" size={15} color="#B44040" />
                </button>
              </span>
            </div>
          );
        })}
      </div>
      {skippedBooklets.length > 0 && (
        <div style={{ marginTop: '10px', padding: '10px 13px', border: '1px dashed var(--border-default)', borderRadius: '8px' }}>
          <div style={{ fontSize: '10.5px', textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-muted)', marginBottom: '6px' }}>Skipped / deferred</div>
          {skippedBooklets.map(b => (
            <div key={b.id} style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '3px 0', fontSize: '12px', color: 'var(--text-muted)' }}>
              <span style={{ textDecoration: 'line-through' }}>⊘ {b.title} · {b.subtitle}</span>
              <span onClick={() => onRestore(b.id)} style={{ marginLeft: 'auto', color: '#4A9B7E', cursor: 'pointer', fontWeight: 600 }}>+ restore</span>
            </div>
          ))}
        </div>
      )}
      {confirm && (
        <Modal open onClose={() => setConfirm(null)} title="Remove lesson from schedule?" width={410}>
          <div style={{ display: 'flex', flexDirection: 'column', gap: '14px' }}>
            <div style={{ fontSize: '13px', color: 'var(--text-secondary)', lineHeight: 1.5 }}>
              Remove <strong style={{ color: 'var(--text-primary)' }}>{confirm.name}</strong> from this class’s teaching order?
              {confirm.kind === 'custom'
                ? ' This is a custom lesson — it can’t be restored once removed.'
                : ' It will move to Skipped / deferred, where you can restore it later.'}
            </div>
            <div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
              <Button variant="secondary" onClick={() => setConfirm(null)}>Cancel</Button>
              <Button variant="danger" onClick={() => { confirm.kind === 'custom' ? onRemoveCustom(confirm.i) : onSkip(confirm.i); setConfirm(null); }}>Remove lesson</Button>
            </div>
          </div>
        </Modal>
      )}
    </div>
  );
}

function SaveSummaryModal({ cat, draft, skipped, onPublish, onClose }) {
  const refIds = draft.filter(e => e.kind === 'ref').map(e => e.id);
  const moved = movedSetFor(cat, refIds).size;
  const added = draft.filter(e => e.kind === 'custom').length;
  const dot = (c, lbl) => <span style={{ display: 'inline-flex', alignItems: 'center', gap: '6px', fontSize: '12.5px' }}><span style={{ width: 9, height: 9, borderRadius: 2, background: c }} />{lbl}</span>;
  const none = moved === 0 && added === 0 && skipped.length === 0;
  return (
    <Modal open onClose={onClose} title="Save changes to this class" width={420}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
        {none
          ? <div style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>No changes — this class will match the default and follow it automatically.</div>
          : <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
              <div style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-muted)' }}>{moved + added + skipped.length} change{moved + added + skipped.length > 1 ? 's' : ''} vs default</div>
              {moved > 0 && dot('#C9821F', `${moved} moved`)}
              {added > 0 && dot('#4A9B7E', `${added} added`)}
              {skipped.length > 0 && dot('var(--text-muted)', `${skipped.length} skipped`)}
            </div>}
        <div style={{ fontSize: '11.5px', color: 'var(--text-muted)' }}>Publishing applies these to this class only.</div>
        <div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end', paddingTop: '4px' }}>
          <Button variant="secondary" onClick={onClose}>Cancel</Button>
          <Button variant="primary" onClick={onPublish}>Publish</Button>
        </div>
      </div>
    </Modal>
  );
}

function AddCustomModal({ onAdd, onClose }) {
  const [title, setTitle] = useCur('');
  const [subtitle, setSubtitle] = useCur('');
  const [content, setContent] = useCur('');
  return (
    <Modal open onClose={onClose} title="Add custom lesson" width={420}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
        <Input label="Title" value={title} onChange={setTitle} placeholder="e.g. Bridging revision" />
        <Input label="Subtitle" value={subtitle} onChange={setSubtitle} placeholder="e.g. Week 1 catch-up" />
        <Textarea label="Content" value={content} onChange={setContent} rows={2} />
        <div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
          <Button variant="secondary" onClick={onClose}>Cancel</Button>
          <Button variant="primary" onClick={() => onAdd({ title: title || 'Custom lesson', subtitle, content: content || '—' })}>Add</Button>
        </div>
      </div>
    </Modal>
  );
}

// Add lessons from the standard catalog that aren't currently in this class (incl. skipped ones).
function AddStandardModal({ cat, draft, onAdd, onClose }) {
  const inDraft = new Set(draft.filter(e => e.kind === 'ref').map(e => e.id));
  const available = cat.booklets.filter(b => !inDraft.has(b.id));
  const series = seriesMap(cat.booklets);
  const [sel, setSel] = useCur({});
  const toggle = id => setSel(s => ({ ...s, [id]: !s[id] }));
  const count = available.filter(b => sel[b.id]).length;
  const allChecked = available.length > 0 && available.every(b => sel[b.id]);
  const toggleAll = () => { if (allChecked) setSel({}); else { const n = {}; available.forEach(b => n[b.id] = true); setSel(n); } };
  return (
    <Modal open onClose={onClose} title="Add standard lessons" width={560}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
        <div style={{ fontSize: '12.5px', color: 'var(--text-muted)' }}>Lessons from the {cat.course}-{cat.pace}-{cat.stream} catalog that aren’t in this class. Tick any to add them back.</div>
        {available.length === 0
          ? <EmptyState message="Every catalog lesson is already in this class." subtle />
          : (
            <div>
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '6px' }}>
                <span style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-muted)' }}>{available.length} available</span>
                <span onClick={toggleAll} style={{ fontSize: '11.5px', color: '#4A9B7E', cursor: 'pointer', fontWeight: 600 }}>{allChecked ? 'Clear all' : 'Select all'}</span>
              </div>
              <div style={{ maxHeight: '320px', overflowY: 'auto', border: '1px solid var(--border-soft)', borderRadius: '8px' }}>
                {available.map((b, idx) => {
                  const s = series[b.id];
                  const di = cat.booklets.findIndex(x => x.id === b.id);
                  return (
                    <div key={b.id} onClick={() => toggle(b.id)}
                      style={{ display: 'grid', gridTemplateColumns: '26px 26px 1fr 110px', gap: '10px', alignItems: 'center', padding: '8px 12px', borderTop: idx ? '1px solid var(--border-soft)' : 'none', cursor: 'pointer', fontSize: '12px', background: sel[b.id] ? 'rgba(74,155,126,0.08)' : 'transparent' }}>
                      <Checkbox checked={!!sel[b.id]} onChange={() => toggle(b.id)} color="#4A9B7E" />
                      <span style={{ textAlign: 'center', fontWeight: 800, color: s ? '#4A9B7E' : 'var(--text-muted)' }}>{s ? s.letter : '–'}</span>
                      <span style={{ minWidth: 0, display: 'flex', alignItems: 'center', gap: '7px' }}>
                        {b.type !== 'Lesson' && <TypeBadge type={b.type} mini />}
                        <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}><strong style={{ color: 'var(--text-primary)' }}>{b.title}</strong> <span style={{ color: 'var(--text-muted)' }}>· {b.subtitle}</span></span>
                      </span>
                      <span style={{ textAlign: 'right' }}><CodeChip cat={cat} idx={di} /></span>
                    </div>
                  );
                })}
              </div>
            </div>
          )}
        <div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end', paddingTop: '2px' }}>
          <Button variant="secondary" onClick={onClose}>Cancel</Button>
          <Button variant="primary" disabled={count === 0} onClick={() => onAdd(available.filter(b => sel[b.id]).map(b => b.id))}>{count > 0 ? `Add ${count} lesson${count > 1 ? 's' : ''}` : 'Add lessons'}</Button>
        </div>
      </div>
    </Modal>
  );
}

function PublishDefaultModal({ cat, classes, classDispatch, onClose }) {
  const tailored = classes.filter(c => classRows(cat, c).tailored);
  const inSync = classes.length - tailored.length;
  return (
    <Modal open onClose={onClose} title={`Publish default — ${cat.course}-${cat.pace}-${cat.stream}`} width={460}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
        <div style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>{cat.booklets.length} booklets · {topicRuns(cat.booklets).length} topics.</div>
        <div style={{ padding: '9px 12px', borderRadius: '8px', background: 'rgba(74,155,126,0.1)', border: '1px solid rgba(74,155,126,0.3)', color: '#4A9B7E', fontSize: '12.5px' }}>
          ✓ {inSync} in-sync class{inSync === 1 ? '' : 'es'} follow the default automatically.
        </div>
        {tailored.length > 0 ? (
          <div>
            <div style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-muted)', marginBottom: '6px' }}>{tailored.length} tailored class{tailored.length > 1 ? 'es' : ''} — opt in per class</div>
            {tailored.map(c => (
              <div key={c.id} style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 0', borderTop: '1px solid var(--border-soft)' }}>
                <span style={{ fontSize: '12.5px', color: 'var(--text-primary)' }}>{c.name}</span>
                <span style={{ marginLeft: 'auto' }}><Button size="sm" variant="secondary" onClick={() => classDispatch.reset(c.id)}>Reset to default</Button></span>
              </div>
            ))}
          </div>
        ) : <div style={{ fontSize: '12.5px', color: 'var(--text-muted)' }}>All classes are in sync with the default.</div>}
        <div style={{ display: 'flex', justifyContent: 'flex-end', paddingTop: '4px' }}><Button variant="primary" onClick={onClose}>Done</Button></div>
      </div>
    </Modal>
  );
}

// ── Export: regenerate components/curriculum-data.js from current state ──
function ExportModal({ data, onClose }) {
  const text = `// curriculum-data.js — DEFAULT curriculum seed (the codebase source of truth).
// Generated by the Curriculum page "Export" on ${new Date().toISOString().slice(0, 10)}.
// To make this the permanent default: replace components/curriculum-data.js with this file
// (or hand it to your developer). It then loads as the default for everyone — including
// future server reloads and your cofounder's clone.

const CURRICULUM_SEED = ${JSON.stringify(data.curriculum, null, 2)};

const CURR_CLASSES_SEED = ${JSON.stringify(data.classes, null, 2)};
`;
  const download = () => {
    const blob = new Blob([text], { type: 'text/javascript' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a'); a.href = url; a.download = 'curriculum-data.js'; a.click();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  };
  const copy = () => { if (navigator.clipboard) navigator.clipboard.writeText(text); };
  return (
    <Modal open onClose={onClose} title="Export catalog as code" width={640}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
        <div style={{ fontSize: '12.5px', color: 'var(--text-secondary)' }}>Your current catalogs &amp; classes as a committable data file. Download it and replace <span style={{ fontFamily: 'monospace', color: 'var(--text-primary)' }}>components/curriculum-data.js</span> to make it the permanent default for the prototype and the handover build.</div>
        <textarea readOnly value={text} onFocus={e => e.target.select()} style={{ width: '100%', height: '240px', fontFamily: 'monospace', fontSize: '11px', background: 'var(--bg-surface-muted)', color: 'var(--text-secondary)', border: '1px solid var(--border-default)', borderRadius: '8px', padding: '10px', resize: 'vertical' }} />
        <div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
          <Button variant="secondary" onClick={copy}>Copy</Button>
          <Button variant="primary" onClick={download}>Download curriculum-data.js</Button>
        </div>
      </div>
    </Modal>
  );
}

// ── Classes view ──────────────────────────────────────────────
function ClassesView({ cat, classes, classDispatch }) {
  const [selId, setSel] = useCur(null);
  const [mode, setMode] = useCur('Annotated');
  const [editing, setEditing] = useCur(false);
  const [draft, setDraft] = useCur([]);
  const [skipped, setSkipped] = useCur([]);
  const [addOpen, setAddOpen] = useCur(false);
  const [addStdOpen, setAddStdOpen] = useCur(false);
  const [saveOpen, setSaveOpen] = useCur(false);
  const series = seriesMap(cat.booklets);
  const idxOf = {}; cat.booklets.forEach((b, i) => idxOf[b.id] = i);
  const sel = classes.find(c => c.id === selId);

  const enterEdit = cls => {
    if (cls.order) { setDraft(cls.order.map(o => o.custom ? { kind: 'custom', booklet: o.custom } : { kind: 'ref', id: o.ref })); setSkipped((cls.skipped || []).slice()); }
    else { setDraft(cat.booklets.map(b => ({ kind: 'ref', id: b.id }))); setSkipped([]); }
    setEditing(true);
  };
  const publish = () => {
    const refIds = draft.filter(e => e.kind === 'ref').map(e => e.id);
    const movedSet = movedSetFor(cat, refIds);
    const isDefault = skipped.length === 0 && draft.every(e => e.kind === 'ref') && refIds.length === cat.booklets.length && refIds.every((id, i) => cat.booklets[i].id === id);
    if (isDefault) classDispatch.reset(sel.id);
    else classDispatch.save(sel.id, draft.map(e => e.kind === 'custom' ? { custom: e.booklet } : { ref: e.id, moved: movedSet.has(e.id) }), skipped.slice());
    setSaveOpen(false); setEditing(false);
  };

  if (!sel) {
    return (
      <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
        {classes.length === 0 && <EmptyState message="No classes on this catalog yet" subtle />}
        {classes.map(c => {
          const info = classRows(cat, c);
          return (
            <div key={c.id} onClick={() => { setSel(c.id); setMode('Annotated'); setEditing(false); }}
              style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '12px 15px', borderRadius: '9px', border: '1px solid var(--border-soft)', background: 'var(--bg-surface)', cursor: 'pointer' }}
              onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-surface)'}>
              <Avatar name={c.tutor} size={30} />
              <div style={{ minWidth: 0 }}>
                <div style={{ fontSize: '13px', fontWeight: 600, color: 'var(--text-primary)' }}>{c.name}</div>
                <div style={{ fontSize: '11.5px', color: 'var(--text-muted)' }}>{c.tutor} · {c.size} students</div>
              </div>
              <span style={{ marginLeft: 'auto', fontSize: '11px', color: 'var(--text-secondary)', background: 'var(--bg-elevated)', borderRadius: '6px', padding: '3px 9px', whiteSpace: 'nowrap' }}>at #{c.at} of {cat.booklets.length}</span>
              {info.tailored
                ? <span style={{ fontSize: '11.5px', fontWeight: 700, color: '#C9A227', whiteSpace: 'nowrap' }}>⚡ {info.nChanges} change{info.nChanges > 1 ? 's' : ''}</span>
                : <span style={{ fontSize: '11.5px', fontWeight: 700, color: '#4A9B7E', whiteSpace: 'nowrap' }}>✓ matches default</span>}
              <Icon name="chevronR" size={15} color="var(--text-muted)" />
            </div>
          );
        })}
      </div>
    );
  }

  const info = classRows(cat, sel);
  const header = (
    <div style={{ marginBottom: '12px' }}>
      <span onClick={() => { setSel(null); setEditing(false); }} style={{ color: '#4A9B7E', cursor: 'pointer', fontSize: '12px', fontWeight: 600 }}>‹ Back to classes</span>
      <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginTop: '6px' }}>
        <div>
          <div style={{ fontSize: '15px', fontWeight: 700, color: 'var(--text-primary)' }}>{sel.name}</div>
          <div style={{ fontSize: '12px', color: 'var(--text-muted)' }}>{cat.course} · {cat.pace} · {cat.stream} · currently at #{sel.at}</div>
        </div>
        <span style={{ flex: 1 }} />
        {!editing && <Button size="sm" variant="secondary" onClick={() => enterEdit(sel)}>Edit teaching order</Button>}
        {!editing && info.tailored && <Button size="sm" variant="ghost" onClick={() => classDispatch.reset(sel.id)}>Reset to default</Button>}
      </div>
    </div>
  );

  if (editing) {
    return (
      <div>
        {header}
        <div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end', alignItems: 'center', marginBottom: '12px' }}>
          <Button variant="secondary" onClick={() => setAddStdOpen(true)}>+ Add standard lesson</Button>
          <Button variant="secondary" onClick={() => setAddOpen(true)}>+ Add custom lesson</Button>
          <span style={{ width: '1px', height: '22px', background: 'var(--border-default)', margin: '0 2px' }} />
          <Button variant="secondary" onClick={() => setEditing(false)}>Cancel</Button>
          <Button variant="primary" onClick={() => setSaveOpen(true)}>Save</Button>
        </div>
        <ClassEditor cat={cat} draft={draft} skipped={skipped} series={series}
          onReorder={(from, to) => setDraft(d => moveRange(d, from, 1, to))}
          onSkip={i => { const e = draft[i]; if (e.kind !== 'ref') return; setDraft(d => d.filter((_, j) => j !== i)); setSkipped(s => s.concat(e.id)); }}
          onRestore={id => { setSkipped(s => s.filter(x => x !== id)); setDraft(d => d.concat([{ kind: 'ref', id }])); }}
          onRemoveCustom={i => setDraft(d => d.filter((_, j) => j !== i))} />
        {addOpen && <AddCustomModal onClose={() => setAddOpen(false)} onAdd={f => { setDraft(d => d.concat([{ kind: 'custom', booklet: { id: newBookletId(), type: 'Lesson', strand: 'Custom', title: f.title, subtitle: f.subtitle, content: f.content } }])); setAddOpen(false); }} />}
        {addStdOpen && <AddStandardModal cat={cat} draft={draft} onClose={() => setAddStdOpen(false)} onAdd={ids => { setSkipped(s => s.filter(x => !ids.includes(x))); setDraft(d => d.concat(ids.map(id => ({ kind: 'ref', id })))); setAddStdOpen(false); }} />}
        {saveOpen && <SaveSummaryModal cat={cat} draft={draft} skipped={skipped} onClose={() => setSaveOpen(false)} onPublish={publish} />}
      </div>
    );
  }

  if (!info.tailored) {
    return (
      <div>{header}
        <div style={{ padding: '10px 13px', borderRadius: '8px', background: 'rgba(74,155,126,0.1)', border: '1px solid rgba(74,155,126,0.3)', color: '#4A9B7E', fontSize: '12.5px', marginBottom: '10px' }}>
          ✓ Follows the default teaching order exactly — publishing a default change updates this class automatically.
        </div>
        <CompareList cat={cat} rows={info.rows} series={series} idxOf={idxOf} />
      </div>
    );
  }

  const nMove = info.rows.filter(r => r.status === 'moved').length, nAdd = info.rows.filter(r => r.status === 'added').length, nSkip = info.skipped.length;
  const dot = (c, lbl) => <span style={{ display: 'inline-flex', alignItems: 'center', gap: '5px' }}><span style={{ width: 9, height: 9, borderRadius: 2, background: c }} />{lbl}</span>;
  return (
    <div>{header}
      <div style={{ display: 'flex', alignItems: 'center', gap: '14px', flexWrap: 'wrap', padding: '9px 13px', borderRadius: '8px', background: 'rgba(184,146,42,0.08)', border: '1px solid rgba(184,146,42,0.25)', color: '#9a5e10', fontSize: '12px', marginBottom: '12px' }}>
        <strong>{info.nChanges} changes vs default:</strong>
        {dot('#C9821F', `${nMove} moved`)}{dot('#4A9B7E', `${nAdd} added`)}{dot('var(--text-muted)', `${nSkip} skipped`)}
        <span style={{ marginLeft: 'auto', display: 'inline-flex', border: '1px solid var(--border-default)', borderRadius: '6px', overflow: 'hidden' }}>
          {['Annotated', 'Side-by-side'].map(m => <span key={m} onClick={() => setMode(m)} style={{ padding: '3px 10px', cursor: 'pointer', fontSize: '11px', background: mode === m ? '#1F4D3D' : 'var(--bg-surface)', color: mode === m ? '#fff' : 'var(--text-secondary)', fontWeight: mode === m ? 600 : 400 }}>{m}</span>)}
        </span>
      </div>
      {mode === 'Annotated'
        ? <div>
            <CompareList cat={cat} rows={info.rows} series={series} idxOf={idxOf} annotate />
            {info.skipped.length > 0 && (
              <div style={{ marginTop: '10px', padding: '10px 13px', border: '1px dashed var(--border-default)', borderRadius: '8px' }}>
                <div style={{ fontSize: '10.5px', textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-muted)', marginBottom: '6px' }}>Skipped / deferred this run</div>
                {info.skipped.map(b => <div key={b.id} style={{ fontSize: '12px', color: 'var(--text-muted)', padding: '3px 0', textDecoration: 'line-through' }}>⊘ {b.title} · {b.subtitle}</div>)}
              </div>
            )}
          </div>
        : <SideBySide cat={cat} sel={sel} info={info} />}
    </div>
  );
}

function BookletEditModal({ booklet, onClose, dispatch, isNew, onAdd }) {
  const [f, setF] = useCur(booklet);
  const set = (k, v) => setF(p => ({ ...p, [k]: v }));
  const star = k => (isNew && !String(f[k] || '').trim() ? ' *' : '');
  const canSave = !isNew || ['strand', 'title', 'subtitle', 'content'].every(k => String(f[k] || '').trim());
  const submit = () => { if (isNew) { if (!canSave) return; onAdd(f); } else dispatch.editBooklet(f.id, f); onClose(); };
  return (
    <Modal open onClose={onClose} title={isNew ? 'Add lesson' : 'Edit booklet'} width={460}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
        <TMSelect label="Type" value={f.type} onChange={v => set('type', v)} options={CURR_TYPE_LIST} />
        <Input label={'Syllabus strand' + star('strand')} value={f.strand} onChange={v => set('strand', v)} />
        <Input label={'Title (topic)' + star('title')} value={f.title} onChange={v => set('title', v)} />
        <Input label={'Subtitle' + star('subtitle')} value={f.subtitle} onChange={v => set('subtitle', v)} placeholder={isNew ? 'e.g. Algebra' : undefined} />
        <Textarea label={'Content' + star('content')} value={f.content} onChange={v => set('content', v)} rows={2} placeholder={isNew ? 'What this lesson covers' : undefined} />
        {isNew && <div style={{ fontSize: '11px', color: 'var(--text-muted)' }}>Fields marked <span style={{ color: '#C9821F' }}>*</span> are required.</div>}
        <div style={{ display: 'flex', gap: '8px', justifyContent: 'space-between', paddingTop: '4px' }}>
          {isNew ? <span /> : <Button variant="ghost" onClick={() => { dispatch.deleteBooklet(f.id); onClose(); }} style={{ color: '#B44040' }}>Delete</Button>}
          <div style={{ display: 'flex', gap: '8px' }}>
            <Button variant="secondary" onClick={onClose}>Cancel</Button>
            <Button variant="primary" onClick={submit} style={canSave ? {} : { opacity: 0.45, cursor: 'not-allowed' }}>{isNew ? 'Add lesson' : 'Save'}</Button>
          </div>
        </div>
      </div>
    </Modal>
  );
}

// ── Section root ──────────────────────────────────────────────
function CurriculumSection() {
  const [data, setData] = useCur(loadCurriculum);
  const [course, setCourse] = useCur('');
  const [pace, setPace] = useCur('');
  const [stream, setStream] = useCur('');
  const [locked, setLocked] = useCur(false);
  const [view, setView] = useCur('Catalog');
  const [editId, setEditId] = useCur(null);
  const [addCtx, setAddCtx] = useCur(null);   // { afterIdx, strand, title } when adding a new lesson via the modal
  const [publishOpen, setPublishOpen] = useCur(false);
  const [exportOpen, setExportOpen] = useCur(false);
  const ready = !!(course && pace && stream);
  const courseColor = c => SUBJECT_COLORS[c === 'STND' ? 'STD' : c] || 'var(--text-secondary)';

  const key = catKey(course, pace, stream);
  const cat = data.curriculum[key];
  const classes = data.classes[key] || [];

  const mutate = fn => setData(prev => { const next = JSON.parse(JSON.stringify(prev)); fn(next); saveCurriculum(next); return next; });
  const setBooklets = fn => mutate(d => { d.curriculum[key].booklets = fn(d.curriculum[key].booklets); });
  const dispatch = {
    reorder: (start, count, target) => setBooklets(bs => moveRange(bs, start, count, target)),
    addBookletFull: (afterIdx, fields) => setBooklets(bs => { const c = bs.slice(); c.splice(afterIdx + 1, 0, { id: newBookletId(), ...fields }); return c; }),
    addTopic: () => setBooklets(bs => bs.concat([{ id: newBookletId(), type: 'Lesson', strand: 'New strand', title: 'New topic', subtitle: 'Booklet A', content: '—' }])),
    editBooklet: (id, fields) => setBooklets(bs => bs.map(b => b.id === id ? { ...b, ...fields } : b)),
    deleteBooklet: id => setBooklets(bs => bs.filter(b => b.id !== id)),
  };
  const classDispatch = {
    save: (id, order, skipped) => mutate(d => { const c = (d.classes[key] || []).find(x => x.id === id); if (c) { c.order = order; c.skipped = skipped; } }),
    reset: id => mutate(d => { const c = (d.classes[key] || []).find(x => x.id === id); if (c) { delete c.order; delete c.skipped; } }),
  };
  const resetAll = () => {
    if (!window.confirm('Reset ALL curriculum edits back to the saved default (curriculum-data.js)? This clears your local changes in this browser.')) return;
    try { localStorage.removeItem(CURR_LS_KEY); } catch (e) {}
    setData(loadCurriculum());
  };
  const editingBooklet = cat ? cat.booklets.find(b => b.id === editId) : null;

  return (
    <div>
      <SectionHeader title="Curriculum" />

      {!locked ? (
        <div style={{ margin: '8px 0 0', border: '1px solid var(--border-soft)', borderRadius: '12px', background: 'var(--bg-surface)', padding: '22px 24px' }}>
          <div style={{ fontSize: '14px', fontWeight: 600, color: 'var(--text-primary)' }}>Choose a catalog</div>
          <div style={{ fontSize: '12.5px', color: 'var(--text-muted)', margin: '4px 0 16px' }}>Select the course, pace and stream to open its scope &amp; sequence.</div>
          <div style={{ display: 'flex', gap: '12px', alignItems: 'flex-end', flexWrap: 'wrap' }}>
            <div style={{ width: '200px' }}><TMSelect label="Course" value={course} onChange={setCourse} options={CURR_COURSES} /></div>
            <div style={{ width: '150px' }}><TMSelect label="Pace" value={pace} onChange={setPace} options={CURR_PACES} /></div>
            <div style={{ width: '150px' }}><TMSelect label="Stream" value={stream} onChange={setStream} options={CURR_STREAMS} /></div>
            <Button variant="primary" onClick={() => ready && setLocked(true)} style={{ justifyContent: 'center', ...(ready ? {} : { opacity: 0.45, cursor: 'not-allowed' }) }}>View scope &amp; sequence</Button>
          </div>
        </div>
      ) : (
        <div>
          <div style={{ display: 'flex', alignItems: 'center', gap: '10px', flexWrap: 'wrap', marginBottom: '16px' }}>
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: '9px', background: hexToRgba(courseColor(course), 0.13), border: `1px solid ${hexToRgba(courseColor(course), 0.34)}`, borderRadius: '9px', padding: '6px 13px' }}>
              <span style={{ width: '8px', height: '8px', borderRadius: '50%', background: courseColor(course), flexShrink: 0 }} />
              <strong style={{ fontSize: '14px', fontWeight: 800, color: courseColor(course), letterSpacing: '0.01em' }}>{course}</strong>
              <span style={{ width: '1px', height: '14px', background: 'var(--border-default)' }} />
              <span style={{ fontSize: '12.5px', fontWeight: 600, color: 'var(--text-secondary)' }}>{pace} · {stream}</span>
            </span>
            <span style={{ flex: 1 }} />
            <Button size="sm" variant="ghost" onClick={() => setLocked(false)}>Change</Button>
            <Button size="sm" variant="ghost" onClick={resetAll}>Reset to default</Button>
            <Button size="sm" variant="secondary" onClick={() => setExportOpen(true)}>Export code</Button>
            {cat && <Button size="sm" variant="secondary" onClick={() => setPublishOpen(true)}>Publish…</Button>}
          </div>

          {!cat ? (
            <div style={{ padding: '40px', textAlign: 'center', color: 'var(--text-muted)', border: '1px dashed var(--border-default)', borderRadius: '10px' }}>
              No catalog yet for <strong style={{ color: 'var(--text-secondary)' }}>{course}-{pace}-{stream}</strong>.
              <div style={{ marginTop: '10px' }}>
                <Button variant="secondary" size="sm" onClick={() => mutate(d => { d.curriculum[key] = { course, pace, stream, booklets: [{ id: newBookletId(), type: 'Lesson', strand: 'New strand', title: 'New topic', subtitle: 'Booklet A', content: '—' }] }; d.classes[key] = d.classes[key] || []; })}>+ Create catalog</Button>
              </div>
            </div>
          ) : (
            <div>
              <Tabs tabs={['Catalog', 'Teaching order', `Classes (${classes.length})`]}
                active={view === 'Classes' ? `Classes (${classes.length})` : view}
                onChange={t => setView(t.startsWith('Classes') ? 'Classes' : t)} />
              <div style={{ marginTop: '14px' }}>
                {view === 'Catalog' && <CatalogView cat={cat} dispatch={dispatch} openEdit={setEditId} openAdd={(afterIdx, strand, title) => setAddCtx({ afterIdx, strand, title })} />}
                {view === 'Teaching order' && <TeachingOrderView cat={cat} dispatch={dispatch} />}
                {view === 'Classes' && <ClassesView cat={cat} classes={classes} classDispatch={classDispatch} />}
              </div>
            </div>
          )}
        </div>
      )}

      {editingBooklet && <BookletEditModal key={editId} booklet={editingBooklet} onClose={() => setEditId(null)} dispatch={dispatch} />}
      {addCtx && <BookletEditModal key="add-lesson" isNew dispatch={dispatch}
        booklet={{ type: 'Lesson', strand: addCtx.strand, title: addCtx.title, subtitle: '', content: '' }}
        onClose={() => setAddCtx(null)}
        onAdd={fields => { dispatch.addBookletFull(addCtx.afterIdx, fields); setAddCtx(null); }} />}
      {publishOpen && cat && <PublishDefaultModal cat={cat} classes={classes} classDispatch={classDispatch} onClose={() => setPublishOpen(false)} />}
      {exportOpen && <ExportModal data={data} onClose={() => setExportOpen(false)} />}
    </div>
  );
}
