/* ============================================================
   admin.jsx — password-gated question bank manager
   Exports window.AdminApp
   ============================================================ */
(function () {
  const { useState, useEffect, useRef } = React;
  const S = window.TestStore;
  const LETTERS = ["A", "B", "C", "D"];

  /* ---- helpers ---- */
  function fmtDate(iso) {
    if (!iso) return "—";
    const d = new Date(iso);
    return d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) +
      " " + d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
  }

  /* ---- CSV helpers ---- */
  function csvEsc(val) {
    const s = String(val == null ? '' : val);
    return (s.includes(',') || s.includes('"') || s.includes('\n') || s.includes('\r'))
      ? '"' + s.replace(/"/g, '""') + '"'
      : s;
  }

  function parseCSV(text) {
    const rows = []; let row = [], field = '', inQ = false, i = 0;
    const flush = () => { row.push(field); field = ''; };
    while (i < text.length) {
      const c = text[i];
      if (inQ) {
        if (c === '"') { if (text[i + 1] === '"') { field += '"'; i++; } else inQ = false; }
        else field += c;
      } else {
        if (c === '"') { inQ = true; }
        else if (c === ',') { flush(); }
        else if (c === '\n' || c === '\r') {
          flush();
          if (row.some(Boolean)) rows.push(row);
          row = [];
          if (c === '\r' && text[i + 1] === '\n') i++;
        } else field += c;
      }
      i++;
    }
    flush();
    if (row.some(Boolean)) rows.push(row);
    return rows;
  }

  function csvToObjects(text) {
    const rows = parseCSV(text);
    if (rows.length < 2) return [];
    const headers = rows[0].map((h) => h.trim().toLowerCase().replace(/\s+/g, '_'));
    return rows.slice(1)
      .filter((r) => r.some(Boolean))
      .map((r) => {
        const obj = {};
        headers.forEach((h, i) => { obj[h] = (r[i] || '').trim(); });
        return obj;
      });
  }

  function groupCSVRows(rows) {
    const groups = [], audioMap = {}, passageMap = {};
    for (const row of rows) {
      const af = row.audio_file || '';
      const pf = row.passage_file || '';
      if (af) {
        if (!audioMap[af]) { audioMap[af] = []; groups.push(audioMap[af]); }
        audioMap[af].push(row);
      } else if (pf) {
        if (!passageMap[pf]) { passageMap[pf] = []; groups.push(passageMap[pf]); }
        passageMap[pf].push(row);
      } else {
        groups.push([row]);
      }
    }
    return groups;
  }

  function mimeFromExt(filename) {
    const ext = (filename.split('.').pop() || '').toLowerCase();
    return ({ mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', m4a: 'audio/mp4',
              aac: 'audio/aac', webm: 'audio/webm',
              jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png',
              gif: 'image/gif', webp: 'image/webp' })[ext] || 'application/octet-stream';
  }

  /* ---- ZIP export ---- */
  async function exportZip() {
    const zip = new JSZip();
    const COLS = ['item_id','part','question','option_a','option_b','option_c','option_d',
                  'correct','explanation','audio_file','transcript','image_file',
                  'passage_file','passage_label','passage2_file'];
    const csvRows = [COLS.map(csvEsc).join(',')];

    for (const item of S.allItems()) {
      const meta = S.PARTS[item.part];

      // Audio
      let audioFile = '';
      if (item.audioUrl && item.audioName) {
        audioFile = item.audioName;
        const b64 = item.audioUrl.split(',')[1];
        if (b64) zip.file(`audio/${audioFile}`, b64, { base64: true });
      }

      // Image (skip SVG placeholders)
      let imageFile = '';
      if (item.imageUrl && !item.imageUrl.startsWith('data:image/svg+xml')) {
        imageFile = `item_${item.id}.jpg`;
        const b64 = item.imageUrl.split(',')[1];
        if (b64) zip.file(`images/${imageFile}`, b64, { base64: true });
      }

      // Passage
      let passageFile = '', passage2File = '';
      if (item.passage) {
        passageFile = `passage_${item.id}.txt`;
        zip.file(`passages/${passageFile}`, item.passage);
      }
      if (item.passage2) {
        passage2File = `passage2_${item.id}.txt`;
        zip.file(`passages/${passage2File}`, item.passage2);
      }

      // One CSV row per question
      item.questions.forEach((q) => {
        const opts = q.options.slice(0, meta.options);
        const row = [
          item.id, item.part,
          meta.stemless ? '' : (q.text || ''),
          opts[0] || '', opts[1] || '', opts[2] || '', opts[3] || '',
          ['A','B','C','D'][q.correct] || 'A',
          q.explanation || '',
          audioFile, item.transcript || '',
          imageFile, passageFile,
          item.passageLabel || '', passage2File,
        ];
        csvRows.push(row.map(csvEsc).join(','));
      });
    }

    zip.file('questions.csv', csvRows.join('\r\n'));

    const blob = await zip.generateAsync({ type: 'blob' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url; a.download = 'question-bank.zip';
    document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }

  /* ---- Results ZIP export ---- */
  function extFromDataUrl(dataUrl) {
    const mime = ((dataUrl || '').split(';')[0] || '').split(':')[1] || '';
    return ({ 'audio/webm': 'webm', 'audio/ogg': 'ogg', 'audio/mp4': 'm4a', 'audio/mpeg': 'mp3', 'audio/wav': 'wav' })[mime] || 'audio';
  }

  async function exportResultsZip(results) {
    const zip = new JSZip();
    const MAX_SP = 3;
    const hdrs = [
      'id', 'submitted_at', 'participant_name',
      'listening_score', 'listening_correct', 'listening_total',
      'reading_score', 'reading_correct', 'reading_total',
      'total_score', 'band', 'speaking_graded_by', 'speaking_graded_comment',
    ];
    for (let i = 1; i <= MAX_SP; i++) {
      hdrs.push(
        `speaking_Q${i}_prompt`, `speaking_Q${i}_topic`, `speaking_Q${i}_attempted`,
        `speaking_Q${i}_duration_s`, `speaking_Q${i}_score`, `speaking_Q${i}_audio_file`,
      );
    }

    const csvRows = [hdrs.map(csvEsc).join(',')];

    for (const r of results) {
      const detail = await S.getResultDetail(r.id);
      const spItems = detail?.speaking?.items || [];
      const safeName = (r.participantName || 'anonymous').replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 30);

      const row = [
        r.id, r.savedAt || '', r.participantName || '',
        r.listening?.scaled ?? '', r.listening?.correct ?? '', r.listening?.total ?? '',
        r.reading?.scaled ?? '', r.reading?.correct ?? '', r.reading?.total ?? '',
        r.totalScaled ?? '', r.band || '', r.speaking?.gradedBy || '', detail?.speaking?.gradedComment || '',
      ];

      for (let i = 0; i < MAX_SP; i++) {
        const si = spItems[i];
        if (!si) { for (let j = 0; j < 6; j++) row.push(''); continue; }
        const spPrompt = S.getSpeakingItem(si.id);
        let audioFile = '';
        if (si.audioDataUrl) {
          const ext = extFromDataUrl(si.audioDataUrl);
          audioFile = `speaking/${safeName}_${r.id.slice(-8)}_Q${i + 1}.${ext}`;
          const b64 = si.audioDataUrl.split(',')[1];
          if (b64) zip.file(audioFile, b64, { base64: true });
        }
        row.push(
          spPrompt?.prompt || '', spPrompt?.topic || '',
          si.attempted ? 'yes' : 'no',
          si.duration ?? '', si.score ?? '', audioFile,
        );
      }

      csvRows.push(row.map(csvEsc).join(','));
    }

    zip.file('results.csv', csvRows.join('\r\n'));
    const blob = await zip.generateAsync({ type: 'blob' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url; a.download = 'test-results.zip';
    document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }

  /* ---- ZIP import parser ---- */
  function buildItemFromGroup(group, fileMap) {
    const errors = [];
    const first = group[0];
    const part = parseInt(first.part, 10);
    const meta = S.PARTS[part];
    if (!meta) { errors.push(`Invalid part "${first.part}" — skipping group`); return { item: null, errors }; }

    const item_id = (first.item_id || '').trim() || null;
    const af      = (first.audio_file  || '').trim();
    const imgf    = (first.image_file  || '').trim();
    const pf      = (first.passage_file || '').trim();
    const p2f     = (first.passage2_file || '').trim();

    const questions = group.map((row, gi) => {
      const opts = [row.option_a, row.option_b, row.option_c, row.option_d]
        .slice(0, meta.options)
        .map((o) => String(o || ''));
      const cIdx = ['A','B','C','D'].indexOf((row.correct || 'A').toUpperCase());
      if (cIdx < 0) errors.push(`Part ${part} row ${gi + 1}: unknown correct "${row.correct}" — defaulting to A`);
      return {
        id: '',
        text: meta.stemless ? '' : String(row.question || ''),
        options: opts,
        correct: Math.max(0, cIdx),
        explanation: String(row.explanation || ''),
      };
    });

    const item = {
      id: item_id,
      part,
      imageUrl: imgf && fileMap[imgf] ? fileMap[imgf] : (meta.hasImage ? null : null),
      _newImage: !!(imgf && fileMap[imgf]),
      _imageFile: imgf,
      audioUrl: af && fileMap[af] ? fileMap[af] : null,
      audioName: af || null,
      transcript: meta.section === 'listening' ? String(first.transcript || '') : null,
      passage: meta.hasPassage ? (pf && fileMap[pf] != null ? fileMap[pf] : '') : null,
      passageLabel: meta.hasPassage ? (String(first.passage_label || '') || 'Passage') : null,
      passage2: (meta.n === 7 && p2f && fileMap[p2f] != null) ? fileMap[p2f] : null,
      questions,
    };

    return { item, errors };
  }

  async function parseZipImport(zip) {
    const csvFile = zip.files['questions.csv'];
    if (!csvFile) throw new Error('questions.csv not found in ZIP');
    const csvText = await csvFile.async('string');
    const rows = csvToObjects(csvText);
    if (!rows.length) throw new Error('questions.csv has no data rows');

    // Build fileMap: filename → data URL (audio/image) or plain text (passages)
    const fileMap = {};
    for (const [path, zipEntry] of Object.entries(zip.files)) {
      if (zipEntry.dir) continue;
      if (path === 'questions.csv') continue;
      const filename = path.split('/').pop();
      if (!filename) continue;
      if (path.startsWith('passages/')) {
        fileMap[filename] = await zipEntry.async('string');
      } else {
        const b64  = await zipEntry.async('base64');
        const mime = mimeFromExt(filename);
        fileMap[filename] = `data:${mime};base64,${b64}`;
      }
    }

    const groups   = groupCSVRows(rows);
    const incoming = [];
    const errors   = [];

    for (const group of groups) {
      const { item, errors: errs } = buildItemFromGroup(group, fileMap);
      errors.push(...errs);
      if (item) incoming.push(item);
    }

    return { incoming, errors };
  }

  /* ---- Diff computation ---- */
  function getItemChanges(existing, incoming) {
    const CH = [];
    function chk(label, a, b) {
      if (String(a || '').trim() !== String(b || '').trim()) CH.push(label);
    }
    const maxQ = Math.max(existing.questions.length, incoming.questions.length);
    for (let i = 0; i < maxQ; i++) {
      const eq = existing.questions[i], iq = incoming.questions[i];
      if (!eq) { CH.push(`Q${i+1} added`); continue; }
      if (!iq) { CH.push(`Q${i+1} removed`); continue; }
      chk(`Q${i+1} text`, eq.text, iq.text);
      eq.options.forEach((o, j) => chk(`Q${i+1} option ${LETTERS[j]}`, o, iq.options[j]));
      chk(`Q${i+1} correct`, ['A','B','C','D'][eq.correct], ['A','B','C','D'][iq.correct]);
      chk(`Q${i+1} explanation`, eq.explanation, iq.explanation);
    }
    chk('audio', existing.audioName, incoming.audioName);
    if (incoming._newImage) CH.push('image');
    else if (!!existing.imageUrl !== !!incoming.imageUrl) CH.push('image removed');
    chk('transcript', existing.transcript, incoming.transcript);
    chk('passage', existing.passage, incoming.passage);
    chk('passage2', existing.passage2, incoming.passage2);
    chk('passage label', existing.passageLabel, incoming.passageLabel);
    return CH;
  }

  function computeDiff(incoming, parseErrors) {
    const updated = [], unchanged = [], inserted = [], errors = parseErrors || [];
    for (const item of incoming) {
      if (!item.id) { inserted.push(item); continue; }
      const existing = S.getItem(item.id);
      if (!existing) { inserted.push(item); continue; }
      const changes = getItemChanges(existing, item);
      if (changes.length) updated.push({ incoming: item, existing, changes });
      else unchanged.push(item);
    }
    return { updated, unchanged, inserted, errors };
  }

  /* ---- Template download ---- */
  async function downloadTemplate() {
    const zip = new JSZip();
    const HEADER = 'item_id,part,question,option_a,option_b,option_c,option_d,correct,explanation,audio_file,transcript,image_file,passage_file,passage_label,passage2_file';
    const rows = [
      // Part 1
      `,1,,She is typing on a keyboard.,She is reading a book.,She is talking on the phone.,She is walking away.,A,The woman is at a desk with her hands on the keyboard.,photo1.mp3,"(A) She is typing on a keyboard. (B) She is reading a book. (C) She is talking on the phone. (D) She is walking away.",photo1.jpg,,`,
      // Part 2
      `,2,,It's on your desk.,Around three o'clock.,"Yes, I reported it.",,A,Answers a where question.,part2_q1.mp3,"Where did you put the report? (A) It's on your desk. (B) Around three o'clock. (C) Yes, I reported it.",,, `,
      // Part 3 rows (same audio_file = same group)
      `,3,What are the speakers discussing?,A rental apartment,A job interview,A hotel room,A restaurant,A,The man asks about the listed apartment.,conversation1.mp3,"Man: Is the apartment on Oak Street still available? Woman: Yes it is. Would you like to see it?",,, `,
      `,3,When will they meet?,Saturday at 2 pm,Friday morning,Sunday at noon,Monday evening,A,They agree on Saturday at two.,conversation1.mp3,,,,`,
      // Part 4 rows (same audio_file = same group)
      `,4,What is the announcement about?,A flight delay,A gate change,A boarding call,A cancellation,A,It announces a delay of forty minutes.,announcement1.mp3,"Attention passengers: Flight 482 has been delayed by approximately forty minutes.",,, `,
      `,4,What is the new boarding time?,4:15,4:50,3:00,5:00,A,New boarding time is stated as 4:15.,announcement1.mp3,,,,`,
      // Part 5
      `,5,"The new software has significantly improved the ______ of the reports.",accurate,accuracy,accurately,accurateness,B,A noun is needed after the of.,,,,, `,
      // Part 6 rows (same passage_file = same group)
      `,6,(1),began,begins,begin,beginning,B,Present simple fits the scheduled future event.,,,,notice1.txt,Notice, `,
      `,6,(2),may,must not,should have,would have,A,may grants permission to use the lot.,,,,notice1.txt,, `,
      `,6,(3),"The elevator is also out of service.","Thank you for your cooperation.","Rent is due on the first.","The pool reopens in June.",B,A polite closing fits the apology.,,,,notice1.txt,, `,
      // Part 7 rows (same passage_file = same group)
      `,7,What is the purpose of the e-mail?,To request volunteers,To confirm an office move,To announce a holiday,To introduce new staff,B,The e-mail confirms the relocation to 50 Harbor Street.,,,,email1.txt,E-mail, `,
      `,7,When will packing boxes be delivered?,March 11,March 14,March 15,March 20,A,Boxes are delivered to each desk on March 11.,,,,email1.txt,, `,
    ];

    zip.file('questions.csv', [HEADER, ...rows].join('\r\n'));

    zip.file('passages/notice1.txt',
      'To all building tenants:\n\nThe parking garage will be closed for resurfacing next weekend. ' +
      'The work is expected to ______ (1) on Saturday morning and finish by Sunday evening. ' +
      'During this time, tenants ______ (2) use the temporary lot on Maple Avenue at no charge. ' +
      'We apologize for any inconvenience. ______ (3)\n\n— Building Management'
    );

    zip.file('passages/email1.txt',
      'From: Daniel Ortiz, Facilities\nTo: All Staff\nSubject: Office Relocation\n\n' +
      'Dear colleagues,\n\nI\'m writing to confirm that our office will move to 50 Harbor Street on March 14. ' +
      'Packing boxes will be delivered to each desk on March 11. ' +
      'Please label your boxes clearly with your name and department.\n\nBest regards,\nDaniel'
    );

    zip.file('README.txt',
      'QUESTION BANK TEMPLATE\n' +
      '======================\n\n' +
      'HOW TO USE\n' +
      '----------\n' +
      '1. Open questions.csv in Excel or Google Sheets.\n' +
      '2. Fill in one row per question. For group items (Parts 3, 4, 6, 7),\n' +
      '   use the SAME filename in audio_file or passage_file for all rows\n' +
      '   that share the same stimulus. That groups them into one item.\n' +
      '3. Place audio files in the audio/ folder, images in images/,\n' +
      '   passage text files (.txt) in passages/.\n' +
      '   Filenames must exactly match what you wrote in the CSV.\n' +
      '4. Re-ZIP the folder and import via Admin > Import.\n\n' +
      'COLUMN REFERENCE\n' +
      '----------------\n' +
      'item_id       : Leave blank for new items. Filled automatically on export.\n' +
      '                Re-import with item_id to update existing questions in place.\n' +
      'part          : 1-7  (see Part Guide below)\n' +
      'question      : Question text. Leave blank for Part 1 (no question stem).\n' +
      'option_a-d    : Answer choices. Part 2 uses only A-C (leave option_d blank).\n' +
      'correct       : A / B / C / D\n' +
      'explanation   : Shown in the answer review screen. Optional.\n' +
      'audio_file    : Filename of audio in the audio/ folder (Parts 1-4).\n' +
      '                Rows sharing the same audio_file form one grouped item.\n' +
      'transcript    : Spoken script text. Used for TTS if no audio file.\n' +
      'image_file    : Filename of image in the images/ folder (Part 1 only).\n' +
      'passage_file  : Filename of .txt in the passages/ folder (Parts 6-7).\n' +
      '                Rows sharing the same passage_file form one grouped item.\n' +
      'passage_label : Label shown above passage e.g. "E-mail" "Notice". Parts 6-7.\n' +
      'passage2_file : Optional second passage file for double-passage Part 7 items.\n\n' +
      'PART GUIDE\n' +
      '----------\n' +
      'Part 1  Listening  Photographs          1 Q per item  4 options\n' +
      'Part 2  Listening  Question-Response    1 Q per item  3 options\n' +
      'Part 3  Listening  Conversations        2-5 Q / group 4 options\n' +
      'Part 4  Listening  Talks                2-5 Q / group 4 options\n' +
      'Part 5  Reading    Incomplete Sentence  1 Q per item  4 options\n' +
      'Part 6  Reading    Text Completion      3 Q / group   4 options\n' +
      'Part 7  Reading    Reading Comprehens.  2-5 Q / group 4 options\n'
    );

    const blob = await zip.generateAsync({ type: 'blob' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url; a.download = 'question-bank-template.zip';
    document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }

  /* ---------------- Password gate ---------------- */
  function Gate({ onUnlock }) {
    const [pw, setPw] = useState("");
    const [err, setErr] = useState(false);
    const ref = useRef(null);
    useEffect(() => { ref.current && ref.current.focus(); }, []);
    function submit(e) {
      e.preventDefault();
      if (pw === S.getSettings().adminPassword) onUnlock();
      else { setErr(true); setPw(""); }
    }
    return (
      <div className="center" style={{ display: "flex", minHeight: "100vh", padding: 24 }}>
        <form onSubmit={submit} className="card view-in" style={{ width: 380, padding: 30 }}>
          <div className="col center" style={{ gap: 14, marginBottom: 22, textAlign: "center" }}>
            <div style={{ width: 52, height: 52, borderRadius: 13, background: "var(--primary-soft)", color: "var(--primary)", display: "flex", alignItems: "center", justifyContent: "center" }}>
              <Icon name="lock" size={24} />
            </div>
            <div className="col" style={{ gap: 4 }}>
              <h2 className="serif" style={{ fontSize: 22 }}>Admin Access</h2>
              <p className="faint" style={{ fontSize: 13.5 }}>Enter the administrator password to manage questions.</p>
            </div>
          </div>
          <Field label="Password">
            <input ref={ref} type="password" className="input" value={pw} placeholder="••••••••"
              onChange={(e) => { setPw(e.target.value); setErr(false); }}
              style={err ? { borderColor: "var(--incorrect)", boxShadow: "0 0 0 3px var(--incorrect-soft)" } : null} />
          </Field>
          {err && <p style={{ color: "var(--incorrect)", fontSize: 13, marginTop: 8 }}>Incorrect password. Try again.</p>}
          <button type="submit" className="btn btn-primary btn-block btn-lg" style={{ marginTop: 18 }}>Unlock</button>
        </form>
      </div>
    );
  }

  /* ---------------- File → data URL ---------------- */
  function readAsDataURL(file) {
    return new Promise((res, rej) => {
      const r = new FileReader();
      r.onload = () => res(r.result); r.onerror = rej;
      r.readAsDataURL(file);
    });
  }

  /* ---------------- Upload controls ---------------- */
  function ImageUpload({ value, onChange }) {
    const ref = useRef(null);
    async function pick(e) { const f = e.target.files[0]; if (!f) return; onChange(await readAsDataURL(f)); }
    return (
      <div className="col gap-2">
        <div style={{ borderRadius: 10, overflow: "hidden", border: "1px solid var(--line)", background: "var(--surface-3)", maxWidth: 340 }}>
          {value ? <img src={value} alt="" style={{ width: "100%", display: "block" }} />
            : <div className="center faint" style={{ height: 160, fontSize: 13 }}>No image</div>}
        </div>
        <div className="row gap-2">
          <button type="button" className="btn btn-ghost btn-sm" onClick={() => ref.current.click()}><Icon name="upload" size={15} /> Upload image</button>
          {value && <button type="button" className="btn btn-quiet btn-sm" onClick={() => onChange(null)}>Remove</button>}
          <input ref={ref} type="file" accept="image/*" onChange={pick} style={{ display: "none" }} />
        </div>
      </div>
    );
  }

  function AudioUpload({ value, name, onChange }) {
    const ref = useRef(null);
    async function pick(e) { const f = e.target.files[0]; if (!f) return; onChange(await readAsDataURL(f), f.name); }
    return (
      <div className="col gap-2">
        <div className="row gap-3" style={{ alignItems: "center", padding: "10px 12px", background: "var(--surface-2)", borderRadius: 8, border: "1px solid var(--line)" }}>
          <Icon name="volume" size={18} style={{ color: value ? "var(--primary)" : "var(--ink-faint)" }} />
          <span className="grow" style={{ fontSize: 13.5, color: value ? "var(--ink)" : "var(--ink-faint)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
            {value ? (name || "Audio attached") : "No audio — transcript used for speech synthesis"}
          </span>
        </div>
        <div className="row gap-2">
          <button type="button" className="btn btn-ghost btn-sm" onClick={() => ref.current.click()}><Icon name="upload" size={15} /> {value ? "Replace" : "Upload audio"}</button>
          {value && <button type="button" className="btn btn-quiet btn-sm" onClick={() => onChange(null, null)}>Remove</button>}
          <input ref={ref} type="file" accept="audio/*" onChange={pick} style={{ display: "none" }} />
        </div>
      </div>
    );
  }

  /* ---------------- MC Question editor ---------------- */
  function QuestionEditor({ q, meta, index, total, onChange, onRemove }) {
    const set    = (patch) => onChange({ ...q, ...patch });
    const setOpt = (i, v) => { const o = [...q.options]; o[i] = v; set({ options: o }); };
    return (
      <div className="panel" style={{ padding: 16, background: "var(--surface-2)" }}>
        <div className="row spread" style={{ alignItems: "center", marginBottom: 12 }}>
          <span className="badge badge-primary">Question {index + 1}{total > 1 ? ` of ${total}` : ""}</span>
          {total > 1 && <button type="button" className="btn btn-danger btn-sm" onClick={onRemove}><Icon name="trash" size={14} /></button>}
        </div>
        <div className="col gap-3">
          {!meta.stemless && (
            <Field label={meta.n === 2 ? "Spoken question / prompt" : "Question text"}
              hint={meta.n === 6 ? "Use the blank marker, e.g. (1), to match the passage." : null}>
              <textarea className="textarea" value={q.text} onChange={(e) => set({ text: e.target.value })}
                placeholder={meta.n === 2 ? "e.g. Where did you put the report?" : "Enter the question…"} style={{ minHeight: 56 }} />
            </Field>
          )}
          <Field label={`Answer options (${meta.options}) — select the correct one`}>
            <div className="col gap-2">
              {q.options.slice(0, meta.options).map((opt, i) => {
                const isCorrect = q.correct === i;
                return (
                  <div key={i} className="row gap-2" style={{ alignItems: "center" }}>
                    <button type="button" onClick={() => set({ correct: i })} title="Mark correct" style={{
                      width: 30, height: 30, borderRadius: "50%", flexShrink: 0, cursor: "pointer",
                      display: "flex", alignItems: "center", justifyContent: "center", fontWeight: 600, fontSize: 13,
                      background: isCorrect ? "var(--correct)" : "var(--surface)",
                      color: isCorrect ? "#fff" : "var(--ink-soft)",
                      border: `1.5px solid ${isCorrect ? "var(--correct)" : "var(--line-strong)"}`,
                    }}>{isCorrect ? <Icon name="check" size={15} stroke={3} /> : LETTERS[i]}</button>
                    <input className="input" value={opt} onChange={(e) => setOpt(i, e.target.value)}
                      placeholder={`Option ${LETTERS[i]}${meta.n === 1 ? " — spoken statement" : ""}`}
                      style={isCorrect ? { borderColor: "var(--correct-line)", background: "var(--correct-soft)" } : null} />
                  </div>
                );
              })}
            </div>
          </Field>
          <Field label="Explanation" hint="Shown in the answer review.">
            <textarea className="textarea" value={q.explanation} onChange={(e) => set({ explanation: e.target.value })}
              placeholder="Why is this the correct answer?" style={{ minHeight: 50 }} />
          </Field>
        </div>
      </div>
    );
  }

  /* ---------------- MC Item editor ---------------- */
  function ItemEditor({ item: initial, onSave, onCancel }) {
    const [item, setItem] = useState(() => JSON.parse(JSON.stringify(initial)));
    const meta = S.PARTS[item.part];
    const set  = (patch) => setItem((it) => ({ ...it, ...patch }));
    const setQ = (i, q) => { const qs = [...item.questions]; qs[i] = q; set({ questions: qs }); };
    const addQ = () => set({ questions: [...item.questions, S.blankQuestion(item.part)] });
    const removeQ = (i) => set({ questions: item.questions.filter((_, j) => j !== i) });

    function save() {
      const copy = { ...item };
      if (meta.n === 1 && !copy.transcript) {
        copy.transcript = copy.questions[0].options.map((o, i) => `(${LETTERS[i]}) ${o}`).join("\n");
      }
      onSave(copy);
    }

    const valid = item.questions.every((q) => q.options.slice(0, meta.options).every((o) => meta.n === 1 ? o.trim() : true));

    return (
      <div className="view-in" style={{ maxWidth: 820, margin: "0 auto" }}>
        <div className="row gap-3" style={{ alignItems: "center", marginBottom: 20 }}>
          <button className="btn btn-quiet btn-sm" onClick={onCancel}><Icon name="arrowLeft" size={16} /> Back</button>
          <div className="col grow">
            <h2 className="serif" style={{ fontSize: 22 }}>{initial._isNew ? "New" : "Edit"} — Part {item.part}: {meta.name}</h2>
            <span className="faint" style={{ fontSize: 13 }}>{meta.desc}</span>
          </div>
        </div>

        <div className="col gap-4">
          {(meta.hasImage || meta.hasAudio || meta.hasPassage) && (
            <div className="card" style={{ padding: 20 }}>
              <h3 style={{ fontSize: 15, marginBottom: 14 }} className="row gap-2">
                <Icon name={meta.hasPassage ? "doc" : meta.hasImage ? "image" : "volume"} size={17} style={{ color: "var(--primary)" }} /> Stimulus
              </h3>
              <div className="col gap-4">
                {meta.hasImage && <Field label="Photograph"><ImageUpload value={item.imageUrl} onChange={(v) => set({ imageUrl: v })} /></Field>}
                {meta.hasAudio && (
                  <>
                    <Field label="Audio file"><AudioUpload value={item.audioUrl} name={item.audioName} onChange={(url, nm) => set({ audioUrl: url, audioName: nm })} /></Field>
                    <Field label="Transcript / audio script" hint="Read aloud when no audio file is attached; shown in review.">
                      <textarea className="textarea" value={item.transcript || ""} onChange={(e) => set({ transcript: e.target.value })}
                        placeholder={meta.n <= 2 ? "Spoken prompt and options…" : "Full conversation / talk script…"} style={{ minHeight: meta.group ? 120 : 70 }} />
                    </Field>
                  </>
                )}
                {meta.hasPassage && (
                  <>
                    <Field label="Passage label"><input className="input" value={item.passageLabel || ""} onChange={(e) => set({ passageLabel: e.target.value })} placeholder="e.g. E-mail, Notice, Article" style={{ maxWidth: 260 }} /></Field>
                    <Field label="Passage text" hint={meta.n === 6 ? "Mark blanks with (1), (2), (3)…" : null}>
                      <textarea className="textarea" value={item.passage || ""} onChange={(e) => set({ passage: e.target.value })} placeholder="Paste the reading passage…" style={{ minHeight: 150 }} />
                    </Field>
                    {meta.n === 7 && (
                      <Field label="Second passage (optional)" hint="For double-passage questions.">
                        <textarea className="textarea" value={item.passage2 || ""} onChange={(e) => set({ passage2: e.target.value || null })} placeholder="Optional second passage…" style={{ minHeight: 90 }} />
                      </Field>
                    )}
                  </>
                )}
              </div>
            </div>
          )}

          <div className="card" style={{ padding: 20 }}>
            <div className="row spread" style={{ alignItems: "center", marginBottom: 14 }}>
              <h3 style={{ fontSize: 15 }} className="row gap-2"><Icon name="list" size={17} style={{ color: "var(--primary)" }} /> Questions</h3>
              {meta.group && <button className="btn btn-subtle btn-sm" onClick={addQ}><Icon name="plus" size={15} /> Add question</button>}
            </div>
            <div className="col gap-3">
              {item.questions.map((q, i) => (
                <QuestionEditor key={q.id} q={q} meta={meta} index={i} total={item.questions.length}
                  onChange={(nq) => setQ(i, nq)} onRemove={() => removeQ(i)} />
              ))}
            </div>
          </div>
        </div>

        <div className="row spread" style={{ marginTop: 22, position: "sticky", bottom: 0, padding: "16px 0", background: "linear-gradient(transparent, var(--paper) 30%)" }}>
          <button className="btn btn-ghost" onClick={onCancel}>Cancel</button>
          <div className="row gap-2">
            {!valid && <span className="hint" style={{ alignSelf: "center", color: "var(--warn)" }}>Fill in all option statements.</span>}
            <button className="btn btn-primary" onClick={save} disabled={!valid}><Icon name="check" size={16} /> Save question</button>
          </div>
        </div>
      </div>
    );
  }

  /* ---------------- MC Item row ---------------- */
  function ItemRow({ item, index, onEdit, onDuplicate, onDelete }) {
    const meta    = S.PARTS[item.part];
    const summary = item.questions[0]?.text
      || (item.passageLabel ? `${item.passageLabel} passage` : "")
      || (meta.hasImage ? "Photograph item" : "")
      || (item.transcript ? item.transcript.split("\n")[0] : "Untitled item");
    return (
      <div className="panel" style={{ padding: "14px 16px", display: "flex", alignItems: "center", gap: 14 }}>
        <span className="mono faint" style={{ fontSize: 13, width: 22, flexShrink: 0, textAlign: "right" }}>{index + 1}</span>
        <div className="grow col" style={{ minWidth: 0, gap: 3 }}>
          <span style={{ fontSize: 14.5, fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{summary}</span>
          <div className="row gap-2 wrap" style={{ alignItems: "center" }}>
            <span className="faint" style={{ fontSize: 12.5 }}>{item.questions.length} question{item.questions.length > 1 ? "s" : ""}</span>
            {item.imageUrl && <span className="badge" style={{ fontSize: 10.5 }}><Icon name="image" size={11} /> image</span>}
            {item.audioUrl && <span className="badge" style={{ fontSize: 10.5 }}><Icon name="volume" size={11} /> audio</span>}
            {item.transcript && !item.audioUrl && <span className="badge" style={{ fontSize: 10.5 }}>transcript</span>}
          </div>
        </div>
        <div className="row gap-2" style={{ flexShrink: 0 }}>
          <button className="btn btn-ghost btn-sm" onClick={onEdit}><Icon name="edit" size={14} /> Edit</button>
          <button className="btn btn-quiet btn-sm" onClick={onDuplicate} title="Duplicate"><Icon name="copy" size={15} /></button>
          <button className="btn btn-quiet btn-sm" onClick={onDelete} title="Delete" style={{ color: "var(--incorrect)" }}><Icon name="trash" size={15} /></button>
        </div>
      </div>
    );
  }

  /* ============================================================
     Speaking prompt editor
     ============================================================ */
  function SpeakingEditor({ item: initial, onSave, onCancel }) {
    const [item, setItem] = useState(() => JSON.parse(JSON.stringify(initial)));
    const set   = (patch) => setItem((it) => ({ ...it, ...patch }));
    const valid = item.prompt.trim().length > 0;

    return (
      <div className="view-in" style={{ maxWidth: 720, margin: "0 auto" }}>
        <div className="row gap-3" style={{ alignItems: "center", marginBottom: 20 }}>
          <button className="btn btn-quiet btn-sm" onClick={onCancel}><Icon name="arrowLeft" size={16} /> Back</button>
          <div className="col grow">
            <h2 className="serif" style={{ fontSize: 22 }}>{initial._isNew ? "New" : "Edit"} Speaking Prompt</h2>
            <span className="faint" style={{ fontSize: 13 }}>Configure the question shown to the test-taker during the speaking section.</span>
          </div>
        </div>

        <div className="card" style={{ padding: 22 }}>
          <div className="col gap-4">
            <Field label="Topic label" hint="Optional — shown as a badge above the prompt.">
              <input className="input" value={item.topic} onChange={(e) => set({ topic: e.target.value })}
                placeholder="e.g. Daily Life, Work, Travel" style={{ maxWidth: 300 }} />
            </Field>
            <Field label="Speaking prompt" hint="The question or task the test-taker must respond to.">
              <textarea className="textarea" value={item.prompt} onChange={(e) => set({ prompt: e.target.value })}
                placeholder="Describe your typical morning routine…" style={{ minHeight: 110 }} />
            </Field>
            <div className="row gap-4 wrap">
              <Field label="Preparation time (seconds)" hint="Time to read and prepare. Set to 0 to skip.">
                <input type="number" min="0" max="300" className="input" value={item.prepTime}
                  onChange={(e) => set({ prepTime: Math.max(0, +e.target.value) })} style={{ maxWidth: 160 }} />
              </Field>
              <Field label="Response time (seconds)" hint="Recording window for the spoken answer.">
                <input type="number" min="10" max="600" className="input" value={item.responseTime}
                  onChange={(e) => set({ responseTime: Math.max(10, +e.target.value) })} style={{ maxWidth: 160 }} />
              </Field>
            </div>
          </div>
        </div>

        <div className="row spread" style={{ marginTop: 22, position: "sticky", bottom: 0, padding: "16px 0", background: "linear-gradient(transparent, var(--paper) 30%)" }}>
          <button className="btn btn-ghost" onClick={onCancel}>Cancel</button>
          <div className="row gap-2">
            {!valid && <span className="hint" style={{ alignSelf: "center", color: "var(--warn)" }}>Enter a prompt to save.</span>}
            <button className="btn btn-primary" onClick={() => onSave(item)} disabled={!valid}><Icon name="check" size={16} /> Save prompt</button>
          </div>
        </div>
      </div>
    );
  }

  /* ============================================================
     Q&A breakdown — shows each answered question with result
     ============================================================ */
  function QABreakdown({ answers }) {
    if (!answers || !Object.keys(answers).length)
      return <p className="faint" style={{ fontSize: 13, padding: "8px 0" }}>No answer data saved.</p>;

    let qNum = 0;
    const blocks = [];
    ["listening", "reading"].forEach((key) => {
      const color = key === "listening" ? "#2A7AB0" : "var(--warn)";
      S.SECTIONS[key].parts.forEach((p) => {
        const meta = S.PARTS[p];
        S.itemsByPart(p).forEach((item) => {
          item.questions.forEach((q) => {
            qNum++;
            const answered = q.id in answers;
            const selected = answered ? answers[q.id] : null;
            const correct = q.correct;
            const isCorrect = answered && selected === correct;
            const borderColor = !answered ? "var(--line-strong)" : isCorrect ? "var(--correct)" : "var(--incorrect)";
            blocks.push(
              <div key={q.id} className="panel" style={{ padding: "10px 12px", borderLeft: `3px solid ${borderColor}` }}>
                <div className="row gap-2" style={{ alignItems: "flex-start", marginBottom: 6 }}>
                  <span className="mono" style={{ fontSize: 12, fontWeight: 700, color, flexShrink: 0, marginTop: 1 }}>{qNum}.</span>
                  <span className="badge" style={{ fontSize: 10, flexShrink: 0 }}>P{p}</span>
                  {!answered && <span className="badge" style={{ fontSize: 10, flexShrink: 0, background: "var(--surface-3)", color: "var(--ink-faint)" }}>skipped</span>}
                  {q.text
                    ? <span style={{ fontSize: 13.5, fontWeight: 500, flex: 1, lineHeight: 1.45 }}>{q.text}</span>
                    : <span className="faint" style={{ fontSize: 13, flex: 1 }}>{meta.name}</span>}
                </div>
                <div className="col gap-1" style={{ paddingLeft: 18 }}>
                  {q.options.slice(0, meta.options).map((opt, i) => {
                    const isSel = answered && i === selected;
                    const isCorr = i === correct;
                    const bg   = (isSel && isCorr) || isCorr ? "var(--correct-soft)" : isSel ? "var(--incorrect-soft)" : "var(--surface-2)";
                    const bdr  = (isSel && isCorr) || isCorr ? "var(--correct-line)" : isSel ? "var(--incorrect-line)" : "var(--line)";
                    const clr  = (isSel && isCorr) || isCorr ? "var(--correct)" : isSel ? "var(--incorrect)" : "var(--ink-soft)";
                    return (
                      <div key={i} className="row gap-2" style={{ padding: "5px 9px", borderRadius: 6, border: `1px solid ${bdr}`, background: bg, color: clr, fontSize: 13, alignItems: "center" }}>
                        <span style={{ fontWeight: 700, width: 14, flexShrink: 0 }}>{LETTERS[i]}</span>
                        <span style={{ flex: 1 }}>{opt || `(spoken option ${LETTERS[i]})`}</span>
                        {isSel && !isCorr && <Icon name="x" size={12} />}
                        {isCorr && <Icon name="check" size={12} />}
                      </div>
                    );
                  })}
                </div>
                {q.explanation && (
                  <p style={{ fontSize: 12, color: "var(--ink-faint)", marginTop: 6, paddingLeft: 18, lineHeight: 1.5 }}>{q.explanation}</p>
                )}
              </div>
            );
          });
        });
      });
    });

    return <div className="col gap-2">{blocks}</div>;
  }

  /* ============================================================
     Results viewer
     ============================================================ */
  function ResultsViewer({ results, onChanged }) {
    const [expanded, setExpanded]                 = useState(null);
    const [qaOpen, setQaOpen]                     = useState(null);
    const [confirmClear, setConfirmClear]         = useState(false);
    const [confirmDeleteResult, setConfirmDeleteResult] = useState(null);
    const [detail, setDetail]                     = useState({});
    const [detailLoading, setDetailLoading]       = useState({});
    const [localScores, setLocalScores]           = useState({});
    const [graderNames, setGraderNames]           = useState({});
    const [graderComments, setGraderComments]     = useState({});
    const [savingScores, setSavingScores]         = useState({});
    const [savedScores, setSavedScores]           = useState({});
    const [downloading, setDownloading]           = useState(false);

    function handleExpand(id) {
      const isOpen = expanded === id;
      setExpanded(isOpen ? null : id);
      if (isOpen) { setQaOpen(null); return; }
      if (!detail[id] && !detailLoading[id]) {
        setDetailLoading((dl) => ({ ...dl, [id]: true }));
        S.getResultDetail(id)
          .then((d) => {
            setDetail((dt) => ({ ...dt, [id]: d || {} }));
            setDetailLoading((dl) => ({ ...dl, [id]: false }));
            if (d?.speaking?.items) {
              const scoreMap = {};
              d.speaking.items.forEach((si) => { if (si.score != null) scoreMap[si.id] = si.score; });
              setLocalScores((prev) => ({ ...prev, [id]: scoreMap }));
            }
            if (d?.speaking?.gradedBy) {
              setGraderNames((prev) => ({ ...prev, [id]: d.speaking.gradedBy }));
            }
            if (d?.speaking?.gradedComment) {
              setGraderComments((prev) => ({ ...prev, [id]: d.speaking.gradedComment }));
            }
          })
          .catch(() => {
            setDetailLoading((dl) => ({ ...dl, [id]: false }));
          });
      }
    }

    async function handleSaveScores(resultId) {
      const det = detail[resultId];
      if (!det?.speaking?.items) return;
      const gradedBy = (graderNames[resultId] || '').trim();
      if (!gradedBy) return;
      const gradedComment = (graderComments[resultId] || '').trim() || null;
      const speakingItems = det.speaking.items.map((si) => ({
        id: si.id,
        score: (localScores[resultId] && si.id in localScores[resultId])
          ? localScores[resultId][si.id]
          : (si.score ?? null),
      }));
      setSavingScores((sv) => ({ ...sv, [resultId]: true }));
      const ok = await S.updateResultSpeakingScores(resultId, speakingItems, gradedBy, gradedComment);
      setSavingScores((sv) => ({ ...sv, [resultId]: false }));
      if (ok) {
        setSavedScores((sv) => ({ ...sv, [resultId]: true }));
        setTimeout(() => setSavedScores((sv) => ({ ...sv, [resultId]: false })), 2000);
        setDetail((dt) => ({
          ...dt,
          [resultId]: {
            ...dt[resultId],
            speaking: {
              ...dt[resultId].speaking,
              gradedBy,
              gradedComment,
              items: dt[resultId].speaking.items.map((si) => ({
                ...si,
                score: (localScores[resultId] && si.id in localScores[resultId])
                  ? localScores[resultId][si.id]
                  : si.score,
              })),
            },
          },
        }));
      }
    }

    async function handleDownload() {
      setDownloading(true);
      try { await exportResultsZip(results); } finally { setDownloading(false); }
    }

    if (!results.length) {
      return (
        <div className="view-in" style={{ maxWidth: 760 }}>
          <div className="row spread wrap gap-3" style={{ alignItems: "flex-end", marginBottom: 20 }}>
            <div className="col" style={{ gap: 4 }}>
              <span className="eyebrow">Admin · Results</span>
              <h2 className="serif" style={{ fontSize: 26 }}>Test Results</h2>
            </div>
          </div>
          <div className="card center" style={{ padding: 60, flexDirection: "column", gap: 14, textAlign: "center" }}>
            <div style={{ width: 52, height: 52, borderRadius: 13, background: "var(--surface-3)", color: "var(--ink-faint)", display: "flex", alignItems: "center", justifyContent: "center" }}>
              <Icon name="users" size={26} />
            </div>
            <div className="col" style={{ gap: 4 }}>
              <span style={{ fontWeight: 600, fontSize: 15 }}>No results yet</span>
              <span className="faint" style={{ fontSize: 13.5 }}>Results are saved automatically when a participant completes a test.</span>
            </div>
          </div>
        </div>
      );
    }

    /* summary stats */
    const avgTotal = Math.round(results.reduce((s, r) => s + (r.totalScaled || 0), 0) / results.length);
    const avgL     = Math.round(results.reduce((s, r) => s + (r.listening?.scaled || 0), 0) / results.length);
    const avgR     = Math.round(results.reduce((s, r) => s + (r.reading?.scaled || 0), 0) / results.length);

    return (
      <div className="view-in" style={{ maxWidth: 820 }}>
        <div className="row spread wrap gap-3" style={{ alignItems: "flex-end", marginBottom: 6 }}>
          <div className="col" style={{ gap: 4 }}>
            <span className="eyebrow">Admin · Results</span>
            <h2 className="serif" style={{ fontSize: 26 }}>Test Results</h2>
          </div>
          <div className="row gap-2">
            <button className="btn btn-ghost btn-sm" onClick={handleDownload} disabled={downloading}>
              <Icon name="download" size={14} /> {downloading ? "Preparing…" : "Download results"}
            </button>
            <button className="btn btn-danger btn-sm" onClick={() => setConfirmClear(true)}><Icon name="trash" size={14} /> Clear all</button>
          </div>
        </div>
        <p className="muted" style={{ fontSize: 14, marginBottom: 20 }}>{results.length} submission{results.length !== 1 ? "s" : ""} saved.</p>

        {/* summary strip */}
        <div className="results-summary" style={{ display: "grid", gridTemplateColumns: "repeat(3,1fr)", gap: 12, marginBottom: 24 }}>
          {[
            { label: "Avg Total", value: avgTotal, sub: "/ 990" },
            { label: "Avg Listening", value: avgL, sub: "/ 495" },
            { label: "Avg Reading", value: avgR, sub: "/ 495" },
          ].map((s) => (
            <div key={s.label} className="card" style={{ padding: "14px 18px", textAlign: "center" }}>
              <span className="serif" style={{ fontSize: 28, fontWeight: 700, color: "var(--primary)" }}>{s.value}</span>
              <span className="faint" style={{ fontSize: 12, display: "block" }}>{s.sub}</span>
              <span style={{ fontSize: 12, fontWeight: 600, color: "var(--ink-soft)", display: "block", marginTop: 2 }}>{s.label}</span>
            </div>
          ))}
        </div>

        {/* result rows */}
        <div className="col gap-2">
          {results.map((r) => {
            const open = expanded === r.id;
            return (
              <div key={r.id} className="card" style={{ overflow: "hidden" }}>
                {/* row header */}
                <button onClick={() => handleExpand(r.id)} style={{
                  width: "100%", display: "flex", alignItems: "center", gap: 14, padding: "14px 18px",
                  background: "none", border: "none", cursor: "pointer", textAlign: "left",
                }}>
                  <div style={{ width: 38, height: 38, borderRadius: 9, background: "var(--primary-soft)", color: "var(--primary)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
                    <Icon name="user" size={19} />
                  </div>
                  <div className="grow col" style={{ minWidth: 0, gap: 3 }}>
                    <span style={{ fontWeight: 600, fontSize: 15, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                      {r.participantName || <span className="faint">Anonymous</span>}
                    </span>
                    <span className="faint" style={{ fontSize: 12.5 }}>{fmtDate(r.savedAt)}</span>
                  </div>
                  {/* scores */}
                  <div className="row gap-3 wrap" style={{ flexShrink: 0, alignItems: "center" }}>
                    <div className="col" style={{ alignItems: "center", minWidth: 50 }}>
                      <span style={{ fontWeight: 700, fontSize: 18, color: "#2A7AB0", lineHeight: 1 }}>{r.listening?.scaled ?? "—"}</span>
                      <span className="faint" style={{ fontSize: 11 }}>Listen</span>
                    </div>
                    <div className="col" style={{ alignItems: "center", minWidth: 50 }}>
                      <span style={{ fontWeight: 700, fontSize: 18, color: "var(--warn)", lineHeight: 1 }}>{r.reading?.scaled ?? "—"}</span>
                      <span className="faint" style={{ fontSize: 11 }}>Read</span>
                    </div>
                    {r.hasSpeaking && (
                      <div className="col" style={{ alignItems: "center", minWidth: 50 }}>
                        <span style={{ fontWeight: 700, fontSize: 18, color: "var(--incorrect)", lineHeight: 1 }}>{r.speaking?.attempted ?? 0}/{r.speaking?.total ?? 0}</span>
                        {r.speaking?.totalScore != null && (
                          <span style={{ fontWeight: 600, fontSize: 11, color: "var(--correct)", lineHeight: 1 }}>{r.speaking.totalScore}pts</span>
                        )}
                        <span className="faint" style={{ fontSize: 11 }}>Speak</span>
                      </div>
                    )}
                    <div className="col" style={{ alignItems: "center", minWidth: 60 }}>
                      <span style={{ fontWeight: 700, fontSize: 22, color: "var(--primary)", lineHeight: 1 }}>{r.totalScaled}</span>
                      <span className="faint" style={{ fontSize: 11 }}>Total</span>
                    </div>
                    <span className="badge" style={{ fontSize: 10.5 }}>{r.band || "—"}</span>
                    <Icon name={open ? "chevDown" : "chevRight"} size={16} style={{ color: "var(--ink-faint)" }} />
                  </div>
                </button>

                {/* expanded detail */}
                {open && (
                  <div style={{ borderTop: "1px solid var(--line)", padding: "18px 18px 20px", background: "var(--surface-2)" }}>
                    <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(220px,1fr))", gap: 12, marginBottom: r.hasSpeaking ? 16 : 0 }}>
                      {[
                        { label: "Listening", scaled: r.listening?.scaled, correct: r.listening?.correct, total: r.listening?.total, color: "#2A7AB0" },
                        { label: "Reading",   scaled: r.reading?.scaled,   correct: r.reading?.correct,   total: r.reading?.total,   color: "var(--warn)" },
                      ].map((sec) => (
                        <div key={sec.label} className="panel" style={{ padding: "12px 16px" }}>
                          <span style={{ fontSize: 12, fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--ink-faint)" }}>{sec.label}</span>
                          <div className="row gap-2" style={{ alignItems: "baseline", marginTop: 6 }}>
                            <span style={{ fontSize: 28, fontWeight: 700, color: sec.color, lineHeight: 1 }}>{sec.scaled}</span>
                            <span className="faint" style={{ fontSize: 13 }}>/ 495</span>
                          </div>
                          <span className="faint" style={{ fontSize: 12.5, marginTop: 4, display: "block" }}>{sec.correct} of {sec.total} correct</span>
                        </div>
                      ))}
                    </div>

                    {/* speaking detail — loaded from KV on demand */}
                    {r.hasSpeaking && (
                      detailLoading[r.id]
                        ? <div className="panel row gap-2" style={{ padding: "10px 14px", marginBottom: 16, alignItems: "center" }}>
                            <span className="faint" style={{ fontSize: 13 }}>Loading speaking data…</span>
                          </div>
                        : detail[r.id]?.speaking?.items?.length > 0
                          ? (
                            <div className="col gap-2" style={{ marginBottom: 16 }}>
                              <div className="row gap-2" style={{ alignItems: "baseline", marginBottom: 2 }}>
                                <span style={{ fontSize: 12, fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--ink-faint)" }}>Speaking</span>
                                {detail[r.id]?.speaking?.gradedBy && (
                                  <span style={{ fontSize: 12, color: "var(--ink-faint)" }}>— graded by <strong style={{ color: "var(--ink-soft)" }}>{detail[r.id].speaking.gradedBy}</strong></span>
                                )}
                              </div>
                              {detail[r.id].speaking.items.map((si, idx) => {
                                const spItem = S.getSpeakingItem(si.id);
                                const scoreVal = (localScores[r.id] && si.id in localScores[r.id])
                                  ? (localScores[r.id][si.id] === null ? '' : String(localScores[r.id][si.id]))
                                  : (si.score == null ? '' : String(si.score));
                                return (
                                  <div key={si.id || idx} className="panel col" style={{ padding: "10px 14px", gap: 8 }}>
                                    <div className="row gap-3" style={{ alignItems: "center" }}>
                                      <span className="badge" style={{ background: si.attempted ? "var(--correct-soft)" : "var(--surface-3)", color: si.attempted ? "var(--correct)" : "var(--ink-faint)", border: `1px solid ${si.attempted ? "var(--correct-line)" : "var(--line)"}`, flexShrink: 0 }}>Q{idx + 1}</span>
                                      <span className="grow" style={{ fontSize: 13.5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                                        {spItem?.topic || "Speaking"}{spItem?.prompt ? ` — ${spItem.prompt.slice(0, 60)}…` : ""}
                                      </span>
                                      {si.attempted
                                        ? <span className="row gap-1" style={{ fontSize: 12.5, color: "var(--correct)", fontWeight: 600, flexShrink: 0 }}><Icon name="checkCircle" size={13} /> {si.duration}s</span>
                                        : <span className="faint" style={{ fontSize: 12.5, flexShrink: 0 }}>Not attempted</span>}
                                    </div>
                                    {si.attempted && (
                                      <div className="row gap-3" style={{ alignItems: "center", flexWrap: "wrap" }}>
                                        {si.audioDataUrl && <audio controls src={si.audioDataUrl} style={{ height: 28, flex: "1 1 180px", maxWidth: 300 }} />}
                                        <div className="row gap-2" style={{ alignItems: "center", flexShrink: 0 }}>
                                          <span style={{ fontSize: 12.5, color: "var(--ink-soft)" }}>Score:</span>
                                          <input
                                            type="number" min="0" max="100"
                                            className="input"
                                            value={scoreVal}
                                            placeholder="—"
                                            onChange={(e) => {
                                              const v = e.target.value;
                                              setLocalScores((prev) => ({
                                                ...prev,
                                                [r.id]: { ...(prev[r.id] || {}), [si.id]: v === '' ? null : Math.min(100, Math.max(0, +v)) },
                                              }));
                                            }}
                                            style={{ width: 62, textAlign: "center", padding: "3px 8px" }}
                                          />
                                          <span style={{ fontSize: 12.5, color: "var(--ink-soft)" }}>/100</span>
                                        </div>
                                      </div>
                                    )}
                                  </div>
                                );
                              })}
                            </div>
                          )
                          : null
                    )}

                    {detail[r.id] && r.hasSpeaking && detail[r.id]?.speaking?.items?.some((si) => si.attempted) && (
                      <div className="col gap-2" style={{ marginBottom: 12 }}>
                        <div className="row gap-2" style={{ alignItems: "center", flexWrap: "wrap" }}>
                          <input
                            type="text"
                            className="input"
                            value={graderNames[r.id] || ''}
                            onChange={(e) => setGraderNames((prev) => ({ ...prev, [r.id]: e.target.value }))}
                            placeholder="Grader name (required)"
                            style={{ flex: "1 1 160px", maxWidth: 200, padding: "3px 10px", fontSize: 13 }}
                          />
                          <button
                            className="btn btn-primary btn-sm"
                            onClick={() => handleSaveScores(r.id)}
                            disabled={savingScores[r.id] || !(graderNames[r.id] || '').trim()}
                          >
                            {savingScores[r.id]
                              ? "Saving…"
                              : savedScores[r.id]
                                ? <><Icon name="checkCircle" size={14} /> Scores saved</>
                                : <><Icon name="check" size={14} /> Save scores</>}
                          </button>
                        </div>
                        <textarea
                          className="textarea"
                          value={graderComments[r.id] || ''}
                          onChange={(e) => setGraderComments((prev) => ({ ...prev, [r.id]: e.target.value }))}
                          placeholder="Grader comment (optional) — feedback or notes on this submission…"
                          style={{ minHeight: 64, fontSize: 13 }}
                        />
                        {detail[r.id]?.speaking?.gradedComment && !(graderComments[r.id] != null) && (
                          <p className="hint" style={{ fontStyle: "italic" }}>Saved comment: {detail[r.id].speaking.gradedComment}</p>
                        )}
                      </div>
                    )}
                    <div className="row gap-2" style={{ justifyContent: "flex-end", flexWrap: "wrap" }}>
                      {detail[r.id] && (
                        <button className="btn btn-ghost btn-sm" onClick={() => setQaOpen(qaOpen === r.id ? null : r.id)}>
                          <Icon name="list" size={14} /> {qaOpen === r.id ? "Hide" : "View"} Q&amp;A
                        </button>
                      )}
                      <button className="btn btn-danger btn-sm" onClick={() => setConfirmDeleteResult(r.id)}>
                        <Icon name="trash" size={14} /> Delete this result
                      </button>
                    </div>
                    {qaOpen === r.id && detail[r.id] && (
                      <div style={{ marginTop: 14, borderTop: "1px solid var(--line)", paddingTop: 14 }}>
                        <span style={{ fontSize: 12, fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--ink-faint)", display: "block", marginBottom: 10 }}>Q&amp;A Breakdown</span>
                        <QABreakdown answers={detail[r.id].answers || {}} />
                      </div>
                    )}
                  </div>
                )}
              </div>
            );
          })}
        </div>

        {/* Delete single result modal */}
        <Modal open={!!confirmDeleteResult} onClose={() => setConfirmDeleteResult(null)} title="Delete this result?" width={400}>
          <p className="muted" style={{ fontSize: 14, marginBottom: 18 }}>
            This permanently removes this test submission. This cannot be undone.
          </p>
          <div className="row gap-3" style={{ justifyContent: "flex-end" }}>
            <button className="btn btn-ghost" onClick={() => setConfirmDeleteResult(null)}>Cancel</button>
            <button className="btn btn-danger" onClick={() => {
              const id = confirmDeleteResult;
              S.deleteResult(id);
              setDetail((dt) => { const n = { ...dt }; delete n[id]; return n; });
              setDetailLoading((dl) => { const n = { ...dl }; delete n[id]; return n; });
              setQaOpen(null);
              setConfirmDeleteResult(null);
              onChanged();
            }}>Delete</button>
          </div>
        </Modal>

        {/* Clear all modal */}
        <Modal open={confirmClear} onClose={() => setConfirmClear(false)} title="Clear all results?" width={400}>
          <p className="muted" style={{ fontSize: 14, marginBottom: 18 }}>
            This permanently deletes all {results.length} saved test results. This cannot be undone.
          </p>
          <div className="row gap-3" style={{ justifyContent: "flex-end" }}>
            <button className="btn btn-ghost" onClick={() => setConfirmClear(false)}>Cancel</button>
            <button className="btn btn-danger" onClick={() => { S.clearResults(); setConfirmClear(false); onChanged(); }}>Clear all</button>
          </div>
        </Modal>
      </div>
    );
  }

  /* ---------------- Settings panel ---------------- */
  function SettingsPanel({ onChanged }) {
    const [s, setS]           = useState(() => ({ ...S.getSettings() }));
    const [saved, setSaved]   = useState(false);
    const [confirmReset, setConfirmReset] = useState(false);
    const set = (patch) => { setS((x) => ({ ...x, ...patch })); setSaved(false); };
    function save() { S.updateSettings(s); setSaved(true); onChanged && onChanged(); setTimeout(() => setSaved(false), 1800); }

    return (
      <div className="view-in" style={{ maxWidth: 640 }}>
        <h2 className="serif" style={{ fontSize: 24, marginBottom: 4 }}>Test Settings</h2>
        <p className="muted" style={{ fontSize: 14, marginBottom: 22 }}>Configure the assessment and admin access.</p>

        <div className="card" style={{ padding: 22, marginBottom: 16 }}>
          <h3 style={{ fontSize: 15, marginBottom: 16 }}>General</h3>
          <div className="col gap-4">
            <Field label="Test title"><input className="input" value={s.testTitle} onChange={(e) => set({ testTitle: e.target.value })} /></Field>
            <Field label="Admin password"><input className="input" value={s.adminPassword} onChange={(e) => set({ adminPassword: e.target.value })} /></Field>
            <label className="row gap-3" style={{ alignItems: "center", cursor: "pointer" }}>
              <input type="checkbox" checked={!!s.showReview} onChange={(e) => set({ showReview: e.target.checked })} style={{ width: 17, height: 17, accentColor: "var(--primary)" }} />
              <span style={{ fontSize: 14 }}>Show correct answers &amp; explanations on the results screen</span>
            </label>
          </div>
        </div>

        <div className="card" style={{ padding: 22, marginBottom: 16 }}>
          <h3 style={{ fontSize: 15, marginBottom: 4 }} className="row gap-2"><Icon name="headphones" size={16} style={{ color: "#2A7AB0" }} /> Listening Section</h3>
          <p className="muted" style={{ fontSize: 13, marginBottom: 16 }}>Controls for the listening comprehension section.</p>
          <div className="col gap-4">
            <label className="row gap-3" style={{ alignItems: "center", cursor: "pointer" }}>
              <input type="checkbox" checked={s.listeningEnabled !== false} onChange={(e) => set({ listeningEnabled: e.target.checked })} style={{ width: 17, height: 17, accentColor: "var(--primary)" }} />
              <span style={{ fontSize: 14 }}>Include listening section in tests</span>
            </label>
            <Field label="Listening time (min)">
              <input type="number" min="1" className="input" value={s.durationListening} onChange={(e) => set({ durationListening: +e.target.value })} style={{ maxWidth: 160 }} />
            </Field>
          </div>
        </div>

        <div className="card" style={{ padding: 22, marginBottom: 16 }}>
          <h3 style={{ fontSize: 15, marginBottom: 4 }} className="row gap-2"><Icon name="book" size={16} style={{ color: "var(--warn)" }} /> Reading Section</h3>
          <p className="muted" style={{ fontSize: 13, marginBottom: 16 }}>Controls for the reading comprehension section.</p>
          <div className="col gap-4">
            <label className="row gap-3" style={{ alignItems: "center", cursor: "pointer" }}>
              <input type="checkbox" checked={s.readingEnabled !== false} onChange={(e) => set({ readingEnabled: e.target.checked })} style={{ width: 17, height: 17, accentColor: "var(--primary)" }} />
              <span style={{ fontSize: 14 }}>Include reading section in tests</span>
            </label>
            <Field label="Reading time (min)">
              <input type="number" min="1" className="input" value={s.durationReading} onChange={(e) => set({ durationReading: +e.target.value })} style={{ maxWidth: 160 }} />
            </Field>
          </div>
        </div>

        <div className="card" style={{ padding: 22, marginBottom: 16, borderColor: "var(--incorrect-line)" }}>
          <h3 style={{ fontSize: 15, marginBottom: 4 }} className="row gap-2"><Icon name="mic" size={16} style={{ color: "var(--incorrect)" }} /> Speaking Section</h3>
          <p className="muted" style={{ fontSize: 13, marginBottom: 16 }}>Controls for the recorded speaking section of the test.</p>
          <div className="col gap-4">
            <label className="row gap-3" style={{ alignItems: "center", cursor: "pointer" }}>
              <input type="checkbox" checked={s.speakingEnabled !== false} onChange={(e) => set({ speakingEnabled: e.target.checked })} style={{ width: 17, height: 17, accentColor: "var(--incorrect)" }} />
              <span style={{ fontSize: 14 }}>Include speaking section in tests</span>
            </label>
            <div className="col gap-1">
              <span className="hint">Applied when creating new speaking prompts.</span>
              <div className="row gap-4 wrap">
                <Field label="Default prep time (sec)">
                  <input type="number" min="0" max="300" className="input" value={s.defaultPrepTime ?? 30} onChange={(e) => set({ defaultPrepTime: +e.target.value })} style={{ maxWidth: 160 }} />
                </Field>
                <Field label="Default response time (sec)">
                  <input type="number" min="10" max="600" className="input" value={s.defaultResponseTime ?? 60} onChange={(e) => set({ defaultResponseTime: +e.target.value })} style={{ maxWidth: 160 }} />
                </Field>
              </div>
            </div>
          </div>
        </div>

        <div className="row gap-3" style={{ marginBottom: 24 }}>
          <button className="btn btn-primary" onClick={save}><Icon name="check" size={16} /> Save settings</button>
          {saved && <span className="row gap-2" style={{ color: "var(--correct)", fontSize: 13.5, fontWeight: 500 }}><Icon name="checkCircle" size={16} /> Saved</span>}
        </div>

        <div className="card" style={{ padding: 22, borderColor: "var(--incorrect-line)" }}>
          <h3 style={{ fontSize: 15, marginBottom: 6 }}>Reset content</h3>
          <p className="muted" style={{ fontSize: 13.5, marginBottom: 14 }}>Restore the original sample question bank. This replaces all current questions.</p>
          <button className="btn btn-danger btn-sm" onClick={() => setConfirmReset(true)}><Icon name="reset" size={15} /> Reset to sample content</button>
        </div>

        <Modal open={confirmReset} onClose={() => setConfirmReset(false)} title="Reset all content?" width={400}>
          <p className="muted" style={{ fontSize: 14, marginBottom: 18 }}>This permanently replaces your question bank with the original sample set. This cannot be undone.</p>
          <div className="row gap-3" style={{ justifyContent: "flex-end" }}>
            <button className="btn btn-ghost" onClick={() => setConfirmReset(false)}>Cancel</button>
            <button className="btn btn-danger" onClick={() => { S.resetToSeed(); setConfirmReset(false); onChanged && onChanged(); }}>Reset</button>
          </div>
        </Modal>
      </div>
    );
  }

  /* ---------------- Import / Export ---------------- */
  /* ---------------- Import / Export (ZIP) ---------------- */
  function ImportExport({ tab: initialTab, onClose, onChanged }) {
    const [tab,        setTab]       = useState(initialTab || 'import');
    const [phase,      setPhase]     = useState('pick');
    const [diff,       setDiff]      = useState(null);
    const [parseErr,   setParseErr]  = useState(null);
    const [legacyRes,  setLegacyRes] = useState(null);
    const [legacyMode, setLegacyMode]= useState('append');
    const fileRef = useRef(null);

    async function handleFile(e) {
      const file = e.target.files[0];
      if (!file) return;
      setParseErr(null);

      if (file.name.toLowerCase().endsWith('.json')) {
        try {
          const text = await file.text();
          const res  = S.validateImport(JSON.parse(text));
          setLegacyRes(res);
          setPhase('legacy');
        } catch (err) { setParseErr('Invalid JSON: ' + err.message); }
        return;
      }

      setPhase('parsing');
      try {
        const buf  = await file.arrayBuffer();
        const zip  = await JSZip.loadAsync(buf);
        const { incoming, errors } = await parseZipImport(zip);
        setDiff(computeDiff(incoming, errors));
        setPhase('preview');
      } catch (err) { setParseErr(err.message); setPhase('pick'); }
    }

    function doLegacyImport() {
      if (!legacyRes?.items?.length) return;
      S.importItems(legacyRes.items, legacyMode);
      setPhase('done'); onChanged?.();
    }

    function doZipImport() {
      if (!diff) return;
      const toWrite = [...diff.updated.map((d) => d.incoming), ...diff.inserted.map((item) => ({ ...item, id: null }))];
      if (toWrite.length) S.upsertItems(toWrite);
      setPhase('done'); onChanged?.();
    }

    function reset() { setPhase('pick'); setDiff(null); setLegacyRes(null); setParseErr(null); if (fileRef.current) fileRef.current.value = ''; }

    const totalQ = S.questionCount();

    return (
      <Modal open onClose={onClose} title="Import &amp; Export Questions" width={660}>
        <div style={{ marginBottom: 18 }}>
          <Segmented value={tab} onChange={(t) => { setTab(t); reset(); }} options={[
            { value: 'import', label: 'Import', icon: 'upload' },
            { value: 'export', label: 'Export', icon: 'download' },
          ]} />
        </div>

        {tab === 'import' && phase === 'pick' && (
          <div className="col gap-4">
            <p className="muted" style={{ fontSize: 13.5 }}>
              Upload a <strong>.zip</strong> file (CSV + audio/image/passage files) or a legacy <strong>.json</strong> file.
              {' '}<a href="#" onClick={(e) => { e.preventDefault(); downloadTemplate(); }}>Download template ZIP</a> to get started.
            </p>
            <div className="row gap-3 wrap" style={{ alignItems: 'center' }}>
              <button className="btn btn-ghost btn-sm" onClick={() => fileRef.current.click()}>
                <Icon name="upload" size={15} /> Choose file (.zip or .json)
              </button>
              <input ref={fileRef} type="file" accept=".zip,.json" onChange={handleFile} style={{ display: 'none' }} />
            </div>
            {parseErr && <p style={{ color: 'var(--incorrect)', fontSize: 13 }}>{parseErr}</p>}
          </div>
        )}

        {tab === 'import' && phase === 'parsing' && (
          <div className="col center" style={{ padding: '28px 0', gap: 14 }}>
            <div style={{ width: 32, height: 32, borderRadius: '50%', border: '3px solid var(--primary)', borderTopColor: 'transparent', animation: 'spin 0.8s linear infinite' }} />
            <span className="faint" style={{ fontSize: 14 }}>Parsing ZIP…</span>
          </div>
        )}

        {tab === 'import' && phase === 'preview' && diff && (
          <div className="col gap-4">
            {diff.updated.length > 0 && (
              <div>
                <span style={{ fontSize: 12, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--warn)', display: 'block', marginBottom: 8 }}>
                  ✏️ Updated ({diff.updated.length})
                </span>
                <div className="col gap-1">
                  {diff.updated.map(({ existing, changes }, i) => (
                    <div key={i} className="panel" style={{ padding: '8px 12px', fontSize: 13 }}>
                      <span style={{ fontWeight: 500 }}>Part {existing.part} — </span>
                      <span className="faint">{existing.passageLabel || existing.audioName || existing.questions[0]?.text?.slice(0, 50) || `Item ${i+1}`}</span>
                      <span className="faint"> · {changes.join(', ')}</span>
                    </div>
                  ))}
                </div>
              </div>
            )}
            {diff.unchanged.length > 0 && (
              <p className="faint" style={{ fontSize: 13 }}>✅ Unchanged ({diff.unchanged.length}) — no differences detected, will be skipped</p>
            )}
            {diff.inserted.length > 0 && (
              <div>
                <span style={{ fontSize: 12, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--correct)', display: 'block', marginBottom: 8 }}>
                  ➕ New ({diff.inserted.length})
                </span>
                <div className="col gap-1">
                  {diff.inserted.map((item, i) => (
                    <div key={i} className="panel" style={{ padding: '8px 12px', fontSize: 13 }}>
                      <span style={{ fontWeight: 500 }}>Part {item.part} · {item.questions.length} Q</span>
                      {item.audioName && <span className="faint"> — {item.audioName}</span>}
                      {item.passageLabel && <span className="faint"> — {item.passageLabel}</span>}
                    </div>
                  ))}
                </div>
              </div>
            )}
            {diff.errors.length > 0 && (
              <div>
                <span style={{ fontSize: 12, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--incorrect)', display: 'block', marginBottom: 8 }}>
                  ❌ Errors ({diff.errors.length}) — skipped
                </span>
                <ul style={{ margin: 0, paddingLeft: 18, fontSize: 12.5, color: 'var(--warn)' }}>
                  {diff.errors.slice(0, 8).map((e, i) => <li key={i}>{e}</li>)}
                  {diff.errors.length > 8 && <li>…and {diff.errors.length - 8} more</li>}
                </ul>
              </div>
            )}
            {diff.updated.length === 0 && diff.inserted.length === 0 && (
              <p className="faint" style={{ fontSize: 13 }}>Nothing to import — all rows are unchanged or errored.</p>
            )}
            <div className="row gap-3" style={{ justifyContent: 'flex-end', marginTop: 4 }}>
              <button className="btn btn-ghost" onClick={reset}>Back</button>
              <button className="btn btn-primary"
                onClick={doZipImport}
                disabled={diff.updated.length === 0 && diff.inserted.length === 0}>
                <Icon name="upload" size={16} /> Confirm ({diff.updated.length + diff.inserted.length} changes)
              </button>
            </div>
          </div>
        )}

        {tab === 'import' && phase === 'legacy' && legacyRes && (
          <div className="col gap-4">
            <p className="muted" style={{ fontSize: 13.5 }}>Legacy JSON file detected.</p>
            {legacyRes.stats && legacyRes.stats.items > 0 ? (
              <span className="row gap-2" style={{ color: 'var(--correct)', fontWeight: 600, fontSize: 14 }}>
                <Icon name="checkCircle" size={16} /> Found {legacyRes.stats.items} items · {legacyRes.stats.questions} questions
              </span>
            ) : (
              <span className="row gap-2" style={{ color: 'var(--incorrect)', fontWeight: 600, fontSize: 14 }}>
                <Icon name="alert" size={16} /> No valid items found.
              </span>
            )}
            {legacyRes.errors.length > 0 && (
              <ul style={{ margin: '6px 0 0', paddingLeft: 18, fontSize: 12.5, color: 'var(--warn)' }}>
                {legacyRes.errors.slice(0, 8).map((e, i) => <li key={i}>{e}</li>)}
              </ul>
            )}
            <div className="panel row spread" style={{ padding: '10px 14px', alignItems: 'center' }}>
              <span className="label" style={{ margin: 0 }}>On import</span>
              <Segmented value={legacyMode} onChange={setLegacyMode} options={[
                { value: 'append', label: `Add to bank (${totalQ} Q)` },
                { value: 'replace', label: 'Replace all' },
              ]} />
            </div>
            <div className="row gap-3" style={{ justifyContent: 'flex-end' }}>
              <button className="btn btn-ghost" onClick={reset}>Back</button>
              <button className="btn btn-primary" onClick={doLegacyImport} disabled={!legacyRes.items.length}>
                <Icon name="upload" size={16} /> Import {legacyRes.items.length ? `${legacyRes.stats.questions} questions` : ''}
              </button>
            </div>
          </div>
        )}

        {tab === 'import' && phase === 'done' && (
          <div className="col center" style={{ gap: 14, padding: '20px 0', textAlign: 'center' }}>
            <div style={{ width: 52, height: 52, borderRadius: 13, background: 'var(--correct-soft)', color: 'var(--correct)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
              <Icon name="checkCircle" size={26} />
            </div>
            <span style={{ fontWeight: 600, fontSize: 16 }}>Import complete</span>
            <button className="btn btn-primary" onClick={onClose}>Done</button>
          </div>
        )}

        {tab === 'export' && (
          <div className="col gap-4">
            <p className="muted" style={{ fontSize: 13.5 }}>
              Download the full question bank ({totalQ} questions) as a ZIP file containing <code>questions.csv</code> plus all audio, image, and passage files.
            </p>
            <div className="row gap-3">
              <button className="btn btn-primary" onClick={exportZip}>
                <Icon name="download" size={16} /> Download ZIP
              </button>
              <button className="btn btn-ghost" onClick={downloadTemplate}>
                <Icon name="doc" size={15} /> Download template
              </button>
            </div>
          </div>
        )}
      </Modal>
    );
  }

  /* ============================================================
     Help Panel
     ============================================================ */
  const CSV_COLS = [
    { col: 'item_id',       req: 'no',  parts: 'all',   notes: 'Leave blank for new items. Filled on export. Re-import to update in place.' },
    { col: 'part',          req: 'yes', parts: 'all',   notes: 'Integer 1–7' },
    { col: 'question',      req: 'no',  parts: '2–7',   notes: 'Blank for Part 1 (no stem)' },
    { col: 'option_a',      req: 'yes', parts: 'all',   notes: '' },
    { col: 'option_b',      req: 'yes', parts: 'all',   notes: '' },
    { col: 'option_c',      req: 'yes', parts: 'all',   notes: '' },
    { col: 'option_d',      req: 'no',  parts: '1,3–7', notes: 'Blank for Part 2 (3-option)' },
    { col: 'correct',       req: 'yes', parts: 'all',   notes: 'A / B / C / D' },
    { col: 'explanation',   req: 'no',  parts: 'all',   notes: 'Shown in answer review' },
    { col: 'audio_file',    req: 'no',  parts: '1–4',   notes: 'Filename in audio/ folder. Same value across rows → same group' },
    { col: 'transcript',    req: 'no',  parts: '1–4',   notes: 'Spoken script; used for TTS if no audio file' },
    { col: 'image_file',    req: 'no',  parts: '1',     notes: 'Filename in images/ folder' },
    { col: 'passage_file',  req: 'no',  parts: '6–7',   notes: 'Filename of .txt in passages/ folder. Same value across rows → same group' },
    { col: 'passage_label', req: 'no',  parts: '6–7',   notes: '"E-mail", "Notice", etc. First row of group only' },
    { col: 'passage2_file', req: 'no',  parts: '7',     notes: 'Second passage .txt for double-passage items' },
  ];

  const PARTS_TABLE = [
    { part: 1, section: 'Listening', name: 'Photographs',           stimulus: 'Photo + optional audio', qPerItem: '1',   opts: 4 },
    { part: 2, section: 'Listening', name: 'Question–Response',     stimulus: 'Audio/transcript',       qPerItem: '1',   opts: 3 },
    { part: 3, section: 'Listening', name: 'Conversations',         stimulus: 'Audio/transcript',       qPerItem: '2–5', opts: 4 },
    { part: 4, section: 'Listening', name: 'Talks',                 stimulus: 'Audio/transcript',       qPerItem: '2–5', opts: 4 },
    { part: 5, section: 'Reading',   name: 'Incomplete Sentences',  stimulus: 'None',                   qPerItem: '1',   opts: 4 },
    { part: 6, section: 'Reading',   name: 'Text Completion',       stimulus: 'Text passage',           qPerItem: '3',   opts: 4 },
    { part: 7, section: 'Reading',   name: 'Reading Comprehension', stimulus: 'Text passage (or two)',  qPerItem: '2–5', opts: 4 },
  ];

  function HelpPanel() {
    const H2 = ({ children }) => (
      <h2 className="serif" style={{ fontSize: 20, marginBottom: 10, marginTop: 28, paddingTop: 28, borderTop: '1px solid var(--line)' }}>{children}</h2>
    );
    const P = ({ children }) => <p className="muted" style={{ fontSize: 14, lineHeight: 1.65, marginBottom: 10 }}>{children}</p>;
    const Step = ({ n, children }) => (
      <div className="row gap-3" style={{ marginBottom: 8, alignItems: 'flex-start' }}>
        <span style={{ width: 24, height: 24, borderRadius: '50%', background: 'var(--primary)', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, flexShrink: 0, marginTop: 1 }}>{n}</span>
        <span style={{ fontSize: 14, lineHeight: 1.6 }}>{children}</span>
      </div>
    );
    const TH = ({ children }) => <th style={{ padding: '8px 12px', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--line)', whiteSpace: 'nowrap', background: 'var(--surface-2)' }}>{children}</th>;
    const TD = ({ children, mono, accent, faint }) => <td style={{ padding: '7px 12px', color: faint ? 'var(--ink-soft)' : accent ? 'var(--correct)' : 'var(--ink)', fontFamily: mono ? 'var(--font-mono)' : undefined, fontSize: mono ? 12.5 : 13.5, borderBottom: '1px solid var(--line-soft)' }}>{children}</td>;

    return (
      <div className="view-in" style={{ maxWidth: 780 }}>
        <div className="col" style={{ gap: 4, marginBottom: 4 }}>
          <span className="eyebrow">Admin · Help</span>
          <h2 className="serif" style={{ fontSize: 26 }}>How to use the Admin Panel</h2>
        </div>
        <P>This guide covers everything you need to manage this English Proficiency Test.</P>

        <H2>1. Quick Start</H2>
        <P>The admin panel has six sections in the sidebar: <strong>Listening</strong> (Parts 1–4), <strong>Reading</strong> (Parts 5–7), <strong>Speaking Prompts</strong>, <strong>Test Results</strong>, <strong>Settings</strong>, and <strong>Help</strong> (this page). Use <strong>Import</strong> / <strong>Export</strong> in the top bar to bulk-manage the question bank.</P>

        <H2>2. Managing Questions</H2>
        <P>Click a Part in the sidebar to see its items. Click <strong>Add item</strong> to create one or <strong>Edit</strong> on an existing row to modify it. To upload audio: inside the editor, click <strong>Upload audio</strong> and choose an MP3 or WAV. Passages (Parts 6–7) are typed directly into the Passage text field.</P>
        <div style={{ overflowX: 'auto', marginBottom: 14 }}>
          <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
            <thead><tr>{['Part','Section','Name','Stimulus','Q / item','Options'].map(h => <TH key={h}>{h}</TH>)}</tr></thead>
            <tbody>
              {PARTS_TABLE.map(r => (
                <tr key={r.part}>
                  <TD mono accent>P{r.part}</TD>
                  <TD>{r.section}</TD>
                  <TD>{r.name}</TD>
                  <TD faint>{r.stimulus}</TD>
                  <TD>{r.qPerItem}</TD>
                  <TD>{r.opts}</TD>
                </tr>
              ))}
            </tbody>
          </table>
        </div>

        <H2>3. Import — Building a question bank from a spreadsheet</H2>
        <Step n="1">Click <strong>Export → Download template</strong> to get a starter <code>question-bank-template.zip</code>.</Step>
        <Step n="2">Open <code>questions.csv</code> in Excel or Google Sheets. Fill in one row per question.</Step>
        <Step n="3">For group items (Parts 3, 4, 6, 7), put the <strong>same filename</strong> in <code>audio_file</code> or <code>passage_file</code> for all rows sharing a stimulus. That links them into one item.</Step>
        <Step n="4">Place audio files in <code>audio/</code>, images in <code>images/</code>, passage <code>.txt</code> files in <code>passages/</code>. Filenames must exactly match the CSV.</Step>
        <Step n="5">Re-ZIP the folder, then click <strong>Import</strong> in the admin header and choose your <code>.zip</code> file.</Step>
        <Step n="6">Review the <strong>preview</strong>: Updated shows what changed; New will be inserted; Unchanged are skipped; Errors listed by row.</Step>
        <Step n="7">Click <strong>Confirm</strong> to apply.</Step>

        <p style={{ fontSize: 14, fontWeight: 600, marginTop: 16, marginBottom: 8 }}>CSV column reference</p>
        <div style={{ overflowX: 'auto', marginBottom: 14 }}>
          <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12.5 }}>
            <thead><tr>{['Column','Required','Parts','Notes'].map(h => <TH key={h}>{h}</TH>)}</tr></thead>
            <tbody>
              {CSV_COLS.map(r => (
                <tr key={r.col}>
                  <TD mono accent>{r.col}</TD>
                  <td style={{ padding: '6px 10px', color: r.req === 'yes' ? 'var(--correct)' : 'var(--ink-faint)', fontSize: 13 }}>{r.req}</td>
                  <TD faint>{r.parts}</TD>
                  <TD faint>{r.notes}</TD>
                </tr>
              ))}
            </tbody>
          </table>
        </div>

        <H2>4. Export — Backing up or sharing the question bank</H2>
        <Step n="1">Click <strong>Export</strong> in the admin header.</Step>
        <Step n="2">Click <strong>Download ZIP</strong>. Saves <code>question-bank.zip</code> with <code>questions.csv</code> plus all audio, image, and passage files.</Step>
        <Step n="3">The CSV includes <code>item_id</code> pre-filled — use it to re-import and update questions in place.</Step>

        <H2>5. Updating existing questions</H2>
        <P>Always <strong>export first</strong> to get a CSV with <code>item_id</code>. Edit the CSV or swap audio files, then re-import. The preview shows three categories:</P>
        {[
          { badge: '✏️ Updated', color: 'var(--warn)',    desc: 'Matched by item_id, something changed. The existing item will be overwritten.' },
          { badge: '✅ Unchanged', color: 'var(--correct)', desc: 'Matched by item_id, nothing differs. Skipped — nothing written.' },
          { badge: '➕ New',       color: 'var(--ink-soft)', desc: 'No item_id in the row. Inserted as a new item.' },
        ].map(({ badge, color, desc }) => (
          <div key={badge} className="panel" style={{ padding: '10px 14px', fontSize: 13.5, marginBottom: 6 }}>
            <span style={{ fontWeight: 600, color }}>{badge}</span>
            <span className="faint"> — {desc}</span>
          </div>
        ))}
        <P style={{ marginTop: 8 }}>If you import a CSV <strong>without</strong> <code>item_id</code>, every row inserts as new. Always export first before updating.</P>

        <H2>6. Speaking Prompts</H2>
        <P>Navigate to <strong>Speaking Prompts</strong> in the sidebar. Click <strong>Add prompt</strong>, enter the prompt text, topic label, preparation time (seconds to read), and response time (recording window). Use ↑ ↓ to reorder. During a test, up to 3 prompts are selected at random.</P>

        <H2>7. Test Results</H2>
        <P>Navigate to <strong>Test Results</strong> in the sidebar. Click a row to expand it and see:</P>
        {[
          'Score breakdown for Listening and Reading (scaled 5–495 each)',
          'Speaking: prompts attempted, duration of each response, and an audio play button for each recorded response',
          'Speaking score inputs — enter a score (0–100) for each attempted prompt, type a grader name, then click Save scores. The grader name is required and saved alongside the scores. Different results can have different graders.',
          'View Q&A — every answered question with the participant\'s choice (green = correct, red = wrong) and explanation',
        ].map((t, i) => (
          <div key={i} className="row gap-2" style={{ alignItems: 'flex-start', fontSize: 14, marginBottom: 6 }}>
            <Icon name="chevRight" size={15} style={{ color: 'var(--primary)', flexShrink: 0, marginTop: 2 }} />
            <span>{t}</span>
          </div>
        ))}
        <P>To export all results, click <strong>Download results</strong> in the top-right of the Test Results page. This downloads <code>test-results.zip</code> containing:</P>
        {[
          <><code>results.csv</code> — one row per submission with listening, reading, and total scores, band, grader name, and per-prompt speaking columns (prompt text, topic, attempted, duration, score, audio filename)</>,
          <>A <code>speaking/</code> folder with each recorded audio file, named <code>participantName_resultId_Q1.webm</code> (etc.)</>,
        ].map((t, i) => (
          <div key={i} className="row gap-2" style={{ alignItems: 'flex-start', fontSize: 14, marginBottom: 6 }}>
            <Icon name="chevRight" size={15} style={{ color: 'var(--primary)', flexShrink: 0, marginTop: 2 }} />
            <span>{t}</span>
          </div>
        ))}

        <H2>8. Settings</H2>
        {[
          ['Test title',           'Shown on the home page and start screen'],
          ['Listening / Reading time', 'Timer duration in minutes for each section'],
          ['Admin password',       'Password required to access this admin panel'],
          ['Show correct answers', 'Whether participants see explanations on the results screen'],
          ['Speaking section',     'Enable/disable; set default prep and response times'],
        ].map(([label, desc]) => (
          <div key={label} className="panel" style={{ padding: '8px 12px', fontSize: 13.5, marginBottom: 6 }}>
            <span style={{ fontWeight: 600 }}>{label}</span>
            <span className="faint"> — {desc}</span>
          </div>
        ))}
        <P>Click <strong>Save settings</strong> after making changes.</P>
      </div>
    );
  }

  /* ============================================================
     Admin shell
     ============================================================ */
  window.AdminApp = function AdminApp({ onExit }) {
    const [unlocked, setUnlocked] = useState(false);
    const [, force] = useState(0);
    const [view, setView]       = useState({ kind: "part", part: 1 });
    const [editing, setEditing] = useState(null);  // MC item being edited
    const [editSp, setEditSp]   = useState(null);  // speaking item being edited
    const [confirmDel, setConfirmDel] = useState(null);
    const [io, setIo]           = useState(null);
    const [sidebarOpen, setSidebarOpen] = useState(false);

    useEffect(() => S.subscribe(() => force((n) => n + 1)), []);

    if (!unlocked) return <Gate onUnlock={() => setUnlocked(true)} />;

    /* MC helpers */
    function startNew(part) { const it = S.newItem(part); it._isNew = true; setEditing(it); }
    function saveItem(it) { const clean = { ...it }; delete clean._isNew; S.updateItem(clean); setEditing(null); }

    /* Speaking helpers */
    function startNewSp() { const it = S.newSpeakingItem(); it._isNew = true; setEditSp(it); }
    function saveSp(it) { const clean = { ...it }; delete clean._isNew; S.updateSpeakingItem(clean); setEditSp(null); }

    const counts = {};
    [1,2,3,4,5,6,7].forEach((p) => (counts[p] = S.questionCount(p)));
    const totalQ   = S.questionCount();
    const spItems  = S.allSpeakingItems();
    const results  = S.getResults();

    /* ---- sidebar nav item ---- */
    function NavBtn({ active, onClick, children }) {
      return (
        <button onClick={onClick} style={{
          width: "100%", display: "flex", alignItems: "center", gap: 10,
          padding: "9px 10px", borderRadius: 8, cursor: "pointer",
          background: active ? "var(--primary-soft)" : "transparent",
          border: "none", textAlign: "left", marginBottom: 2,
        }}>{children}</button>
      );
    }

    return (
      <div style={{ height: "100vh", display: "flex", flexDirection: "column", overflow: "hidden" }}>
        {/* top bar */}
        <header className="row spread" style={{ padding: "12px 22px", borderBottom: "1px solid var(--line)", background: "var(--surface)", alignItems: "center", position: "sticky", top: 0, zIndex: 20 }}>
          <div className="row gap-3" style={{ alignItems: "center" }}>
            <button className="btn btn-quiet admin-menu-btn" onClick={() => setSidebarOpen((o) => !o)} style={{ padding: 8 }} aria-label="Menu"><Icon name="list" size={20} /></button>
            <Logo size={30} />
            <span className="badge badge-primary"><Icon name="settings" size={12} /> Admin</span>
          </div>
          <div className="row gap-2">
            <span className="faint" style={{ fontSize: 13, alignSelf: "center", marginRight: 6 }}>{totalQ} questions</span>
            <button className="btn btn-ghost btn-sm" onClick={() => setIo("import")}><Icon name="upload" size={15} /><span className="admin-btn-label"> Import</span></button>
            <button className="btn btn-ghost btn-sm" onClick={() => setIo("export")}><Icon name="download" size={15} /><span className="admin-btn-label"> Export</span></button>
            <button className="btn btn-ghost btn-sm" onClick={onExit}><Icon name="logout" size={15} /><span className="admin-btn-label"> Exit</span></button>
          </div>
        </header>

        <div style={{ display: "flex", flex: 1, minHeight: 0 }}>
          {sidebarOpen && <div className="sidebar-backdrop" onClick={() => setSidebarOpen(false)} />}
          {/* sidebar */}
          <aside className={"admin-sidebar" + (sidebarOpen ? " sidebar-open" : "")} style={{ width: 252, borderRight: "1px solid var(--line)", background: "var(--surface)", padding: 16, flexShrink: 0, overflow: "auto" }}>
            {/* Listening + Reading parts */}
            {Object.values(S.SECTIONS).map((sec) => (
              <div key={sec.key} style={{ marginBottom: 18 }}>
                <div className="row gap-2" style={{ alignItems: "center", padding: "0 8px", marginBottom: 8 }}>
                  <Icon name={sec.key === "listening" ? "headphones" : "book"} size={15} style={{ color: "var(--ink-faint)" }} />
                  <span className="eyebrow">{sec.name}</span>
                </div>
                {sec.parts.map((p) => {
                  const active = view.kind === "part" && view.part === p && !editing && !editSp;
                  return (
                    <NavBtn key={p} active={active} onClick={() => { setEditing(null); setEditSp(null); setView({ kind: "part", part: p }); setSidebarOpen(false); }}>
                      <span className="mono" style={{ fontSize: 12, fontWeight: 600, color: active ? "var(--primary)" : "var(--ink-faint)", width: 20 }}>P{p}</span>
                      <span className="grow" style={{ fontSize: 13.5, fontWeight: active ? 600 : 500, color: active ? "var(--primary-deep)" : "var(--ink)" }}>{S.PARTS[p].short}</span>
                      <span className="badge" style={{ fontSize: 10.5, background: active ? "var(--surface)" : "var(--surface-3)" }}>{counts[p]}</span>
                    </NavBtn>
                  );
                })}
              </div>
            ))}

            <hr className="divider" style={{ margin: "8px 0 12px" }} />

            {/* Speaking */}
            {(function() {
              const active = view.kind === "speaking" && !editing && !editSp;
              return (
                <div style={{ marginBottom: 14 }}>
                  <div className="row gap-2" style={{ alignItems: "center", padding: "0 8px", marginBottom: 8 }}>
                    <Icon name="mic" size={15} style={{ color: "var(--ink-faint)" }} />
                    <span className="eyebrow">Speaking</span>
                  </div>
                  <NavBtn active={active} onClick={() => { setEditing(null); setEditSp(null); setView({ kind: "speaking" }); setSidebarOpen(false); }}>
                    <Icon name="mic" size={16} style={{ color: active ? "var(--incorrect)" : "var(--ink-faint)" }} />
                    <span className="grow" style={{ fontSize: 13.5, fontWeight: active ? 600 : 500, color: active ? "var(--incorrect)" : "var(--ink)" }}>Speaking Prompts</span>
                    <span className="badge" style={{ fontSize: 10.5, background: active ? "var(--incorrect-soft)" : "var(--surface-3)", color: active ? "var(--incorrect)" : undefined }}>{spItems.length}</span>
                  </NavBtn>
                </div>
              );
            })()}

            <hr className="divider" style={{ margin: "8px 0 12px" }} />

            {/* Results */}
            {(function() {
              const active = view.kind === "results" && !editing && !editSp;
              return (
                <div style={{ marginBottom: 14 }}>
                  <NavBtn active={active} onClick={() => { setEditing(null); setEditSp(null); setView({ kind: "results" }); setSidebarOpen(false); }}>
                    <Icon name="users" size={16} style={{ color: active ? "var(--primary)" : "var(--ink-faint)" }} />
                    <span className="grow" style={{ fontSize: 13.5, fontWeight: active ? 600 : 500, color: active ? "var(--primary-deep)" : "var(--ink)" }}>Test Results</span>
                    {results.length > 0 && <span className="badge badge-primary" style={{ fontSize: 10.5 }}>{results.length}</span>}
                  </NavBtn>
                </div>
              );
            })()}

            <hr className="divider" style={{ margin: "8px 0 12px" }} />

            {/* Settings */}
            {(function() {
              const active = view.kind === "settings" && !editing && !editSp;
              return (
                <NavBtn active={active} onClick={() => { setEditing(null); setEditSp(null); setView({ kind: "settings" }); setSidebarOpen(false); }}>
                  <Icon name="settings" size={16} style={{ color: active ? "var(--primary)" : "var(--ink-faint)" }} />
                  <span style={{ fontSize: 13.5, fontWeight: active ? 600 : 500, color: active ? "var(--primary-deep)" : "var(--ink)" }}>Test Settings</span>
                </NavBtn>
              );
            })()}

            <hr className="divider" style={{ margin: "8px 0 12px" }} />

            {/* Help */}
            {(function() {
              const active = view.kind === "help" && !editing && !editSp;
              return (
                <NavBtn active={active} onClick={() => { setEditing(null); setEditSp(null); setView({ kind: "help" }); setSidebarOpen(false); }}>
                  <Icon name="alert" size={16} style={{ color: active ? "var(--primary)" : "var(--ink-faint)" }} />
                  <span style={{ fontSize: 13.5, fontWeight: active ? 600 : 500, color: active ? "var(--primary-deep)" : "var(--ink)" }}>Help</span>
                </NavBtn>
              );
            })()}
          </aside>

          {/* main content */}
          <main className="grow" style={{ overflow: "auto", padding: "28px 30px 60px", background: "var(--paper)" }}>
            {/* MC item editing */}
            {editing ? (
              <ItemEditor item={editing} onSave={saveItem} onCancel={() => setEditing(null)} />
            ) : editSp ? (
              <SpeakingEditor item={editSp} onSave={saveSp} onCancel={() => setEditSp(null)} />
            ) : view.kind === "settings" ? (
              <SettingsPanel onChanged={() => force((n) => n + 1)} />
            ) : view.kind === "results" ? (
              <ResultsViewer results={results} onChanged={() => force((n) => n + 1)} />
            ) : view.kind === "speaking" ? (
              <SpeakingList />
            ) : view.kind === "help" ? (
              <HelpPanel />
            ) : (
              <PartList part={view.part} onNew={() => startNew(view.part)} onEdit={setEditing} onDelete={setConfirmDel} />
            )}
          </main>
        </div>

        {/* Delete MC item modal */}
        <Modal open={!!confirmDel} onClose={() => setConfirmDel(null)} title="Delete this item?" width={380}>
          <p className="muted" style={{ fontSize: 14, marginBottom: 18 }}>This removes the item and all its questions from the bank.</p>
          <div className="row gap-3" style={{ justifyContent: "flex-end" }}>
            <button className="btn btn-ghost" onClick={() => setConfirmDel(null)}>Cancel</button>
            <button className="btn btn-danger" onClick={() => { S.deleteItem(confirmDel); setConfirmDel(null); }}>Delete</button>
          </div>
        </Modal>

        {io && <ImportExport tab={io} onClose={() => setIo(null)} onChanged={() => force((n) => n + 1)} />}
      </div>
    );

    /* ---- MC Part list ---- */
    function PartList({ part, onNew, onEdit, onDelete }) {
      const meta  = S.PARTS[part];
      const items = S.itemsByPart(part);
      return (
        <div className="view-in" style={{ maxWidth: 760 }}>
          <div className="row spread wrap gap-3" style={{ alignItems: "flex-end", marginBottom: 6 }}>
            <div className="col" style={{ gap: 4 }}>
              <span className="eyebrow">Part {part} · {meta.section}</span>
              <h2 className="serif" style={{ fontSize: 26 }}>{meta.name}</h2>
            </div>
            <button className="btn btn-primary" onClick={onNew}><Icon name="plus" size={16} /> Add item</button>
          </div>
          <p className="muted" style={{ fontSize: 14, marginBottom: 22 }}>{meta.desc}</p>
          {items.length === 0 ? (
            <div className="card center" style={{ padding: 50, flexDirection: "column", gap: 14, textAlign: "center" }}>
              <div style={{ width: 48, height: 48, borderRadius: 12, background: "var(--surface-3)", color: "var(--ink-faint)", display: "flex", alignItems: "center", justifyContent: "center" }}>
                <Icon name={meta.hasPassage ? "doc" : meta.hasImage ? "image" : meta.hasAudio ? "volume" : "list"} size={24} />
              </div>
              <div className="col" style={{ gap: 4 }}>
                <span style={{ fontWeight: 600, fontSize: 15 }}>No items yet</span>
                <span className="faint" style={{ fontSize: 13.5 }}>Add your first {meta.name.toLowerCase()} item.</span>
              </div>
              <button className="btn btn-primary btn-sm" onClick={onNew}><Icon name="plus" size={15} /> Add item</button>
            </div>
          ) : (
            <div className="col gap-2">
              {items.map((it, i) => (
                <ItemRow key={it.id} item={it} index={i} onEdit={() => onEdit(it)}
                  onDuplicate={() => S.duplicateItem(it.id)} onDelete={() => onDelete(it.id)} />
              ))}
            </div>
          )}
        </div>
      );
    }

    /* ---- Speaking prompts list ---- */
    function SpeakingList() {
      const items = S.allSpeakingItems();
      return (
        <div className="view-in" style={{ maxWidth: 760 }}>
          <div className="row spread wrap gap-3" style={{ alignItems: "flex-end", marginBottom: 6 }}>
            <div className="col" style={{ gap: 4 }}>
              <span className="eyebrow">Speaking Section</span>
              <h2 className="serif" style={{ fontSize: 26 }}>Speaking Prompts</h2>
            </div>
            <button className="btn btn-primary" onClick={startNewSp} style={{ background: "var(--incorrect)", borderColor: "var(--incorrect)" }}>
              <Icon name="plus" size={16} /> Add prompt
            </button>
          </div>
          <p className="muted" style={{ fontSize: 14, marginBottom: 22 }}>
            Each prompt is shown one at a time during the speaking section. The participant reads, prepares, then speaks within the time limit.
          </p>

          {items.length === 0 ? (
            <div className="card center" style={{ padding: 50, flexDirection: "column", gap: 14, textAlign: "center" }}>
              <div style={{ width: 48, height: 48, borderRadius: 12, background: "var(--incorrect-soft)", color: "var(--incorrect)", display: "flex", alignItems: "center", justifyContent: "center" }}>
                <Icon name="mic" size={24} />
              </div>
              <div className="col" style={{ gap: 4 }}>
                <span style={{ fontWeight: 600, fontSize: 15 }}>No speaking prompts yet</span>
                <span className="faint" style={{ fontSize: 13.5 }}>Add your first speaking prompt to enable the speaking section.</span>
              </div>
              <button className="btn btn-sm" onClick={startNewSp} style={{ background: "var(--incorrect)", color: "#fff", border: "none" }}><Icon name="plus" size={15} /> Add prompt</button>
            </div>
          ) : (
            <div className="col gap-2">
              {items.map((it, i) => (
                <div key={it.id} className="panel" style={{ padding: "14px 16px", display: "flex", alignItems: "center", gap: 14 }}>
                  <span className="mono faint" style={{ fontSize: 13, width: 22, flexShrink: 0, textAlign: "right" }}>{i + 1}</span>
                  <div className="grow col" style={{ minWidth: 0, gap: 3 }}>
                    <span style={{ fontSize: 14.5, fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                      {it.prompt || <span className="faint">Empty prompt</span>}
                    </span>
                    <div className="row gap-2 wrap" style={{ alignItems: "center" }}>
                      {it.topic && <span className="badge badge-primary" style={{ fontSize: 10.5 }}>{it.topic}</span>}
                      <span className="faint" style={{ fontSize: 12.5 }}><Icon name="clock" size={12} /> Prep {it.prepTime}s · Respond {it.responseTime}s</span>
                    </div>
                  </div>
                  <div className="row gap-2" style={{ flexShrink: 0 }}>
                    <button className="btn btn-quiet btn-sm" onClick={() => S.moveSpeakingItem(it.id, -1)} disabled={i === 0} title="Move up">↑</button>
                    <button className="btn btn-quiet btn-sm" onClick={() => S.moveSpeakingItem(it.id, 1)} disabled={i === items.length - 1} title="Move down">↓</button>
                    <button className="btn btn-ghost btn-sm" onClick={() => setEditSp(it)}>
                      <Icon name="edit" size={14} /> Edit
                    </button>
                    <button className="btn btn-quiet btn-sm" onClick={() => S.deleteSpeakingItem(it.id)} title="Delete" style={{ color: "var(--incorrect)" }}><Icon name="trash" size={15} /></button>
                  </div>
                </div>
              ))}
            </div>
          )}
        </div>
      );
    }
  };
})();
