/* ─────────────────────────────────────────────────────────────
   download.jsx — ChiselAssess Export System (clean rebuild)
   Exports: DownloadModal
   ───────────────────────────────────────────────────────────── */

const { useState, useEffect, useCallback, useMemo, useRef } = React;

// ── Helpers (available from helpers.jsx on window) ─────────────
const _api    = () => window.apiFetch || window._apiFetchRobust || (async (p,o) => {
  const tok = localStorage.getItem('ca_token');
  const r = await fetch((window.API_BASE||'') + p, {
    ...o,
    headers: { ...(o?.headers||{}), ...(tok ? {Authorization:`Bearer ${tok}`} : {}) }
  });
  if (!r.ok) { const j = await r.json().catch(()=>({})); throw new Error(j.detail||j.message||`HTTP ${r.status}`); }
  return r.json();
});
const _gradeOf= () => window.gradeOf    || ((s) => ({ g: s >= 9 ? 'A+' : s >= 8 ? 'A' : s >= 7 ? 'B+' : s >= 6 ? 'B' : s >= 5 ? 'C+' : s >= 4 ? 'C' : s >= 3 ? 'D' : 'E', label: '' }));
const _norm   = () => window.normaliseApiReport;
const _fmtD   = () => window.fmtDate  || ((d) => new Date(d).toLocaleDateString('en-IN'));
const _fmtT   = () => window.fmtTime  || ((d) => new Date(d).toLocaleTimeString('en-IN', {hour:'2-digit',minute:'2-digit'}));

// ── Criteria definition ─────────────────────────────────────────
const CRIT = [
  { key: 'content',    label: 'Content Depth',       short: 'Content'   },
  { key: 'structure',  label: 'Structure & Flow',     short: 'Structure' },
  { key: 'clarity',    label: 'Clarity of Delivery',  short: 'Clarity'   },
  { key: 'engagement', label: 'Engagement',           short: 'Engmt'     },
  { key: 'language',   label: 'Language Precision',   short: 'Language'  },
  { key: 'confidence', label: 'Confidence',           short: 'Confid'    },
];

// ── Download blob helper ────────────────────────────────────────
function _dl(blob, filename) {
  const url = URL.createObjectURL(blob);
  const a = Object.assign(document.createElement('a'), { href: url, download: filename });
  document.body.appendChild(a); a.click();
  setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 300);
}

// ══════════════════════════════════════════════════════════════
//  EXCEL BUILDER — Option 1
//  Sheet 1: Summary  |  Sheet 2+: one per department
// ══════════════════════════════════════════════════════════════
function buildExcel(reports, title = 'ChiselAssess Export') {
  const XL = window.XLSX;
  if (!XL) throw new Error('SheetJS not loaded');
  const wb = XL.utils.book_new();
  const gradeOf = _gradeOf();

  // ── Sort reports by college → dept → name
  const sorted = [...reports].sort((a, b) => {
    const ca = a.student?.college || ''; const cb = b.student?.college || '';
    const da = a.student?.dept    || ''; const db = b.student?.dept    || '';
    const na = a.student?.name    || ''; const nb = b.student?.name    || '';
    return ca.localeCompare(cb) || da.localeCompare(db) || na.localeCompare(nb);
  });

  // ── Row builder for the 30-column format ───────────────────
  const makeRow = (r, idx) => {
    const raw = r._raw || {};
    const ev  = (raw.evaluation && typeof raw.evaluation === 'object') ? raw.evaluation : raw;
    const fw  = raw.filler_words || ev.filler_words || {};
    const sa  = raw.self_assessment || ev.self_assessment || {};
    const faculty = raw.assessed_by_name || raw.faculty_name || ev.faculty_name || null;
    const fillerTxt = (fw.detected || []).map(f => `${f.word}×${f.count}`).join(', ') || null;
    const strTxt = Array.isArray(r.strengths) ? r.strengths.join('\n• ').replace(/^/, '• ') : (r.strengths || '');
    const wkTxt  = Array.isArray(r.weaknesses) ? r.weaknesses.join('\n• ').replace(/^/, '• ') : (r.weaknesses || '');

    // detailed_feedback: use stored value OR synthesize from criteria insights (lowest score first)
    let detFB = ev.detailed_feedback || raw.detailed_feedback || '';
    if (!detFB) {
      const CLABELS = {
        content:'Content Depth',structure:'Structure & Flow',clarity:'Clarity of Delivery',
        engagement:'Engagement',language:'Language Precision',confidence:'Confidence',
      };
      const crits = raw.criteria || ev.criteria || {};
      detFB = Object.entries(crits)
        .filter(([,c]) => typeof c==='object' && c?.insight)
        .sort(([,a],[,b]) => (parseFloat(a?.score)||100)-(parseFloat(b?.score)||100))
        .map(([k,c]) => `• ${CLABELS[k]||k} (${Math.round(parseFloat(c.score)||0)}/100): ${c.insight}`)
        .join('\n');
    }

    // communication_suggestions: use stored OR build from coaching_drills
    let commSg = ev.communication_suggestions || raw.communication_suggestions || '';
    if (!commSg) {
      const drills = raw.coaching_drills || ev.coaching_drills || [];
      const parts = drills.slice(0,3).map(d=>{
        const t=d.title||d.name||''; const b=d.description||d.desc||'';
        return b ? (t?`• ${t}: ${b}`:`• ${b}`) : null;
      }).filter(Boolean);
      if ((fw.total_count||0)>4) {
        const det=(fw.detected||[]).slice(0,4).map(f=>`"${f.word}"`).join(', ');
        if (det) parts.push(`• Filler reduction: Most-used: ${det}. Replace with deliberate pauses.`);
      }
      commSg = parts.join('\n');
    }
    return [
      idx + 1, r.student.name, r.student.regNo, r.student.college || '', r.student.dept,
      r.topic, faculty, `${_fmtD()(r.date)}, ${_fmtT()(r.date)} IST`,
      typeof r.scores.overall === 'number' ? r.scores.overall : 0,
      r.grade?.g || '',
      ...CRIT.map(c => r.scores[c.key] ?? 0),
      strTxt, wkTxt, detFB, commSg,
      r.words || 0, r.duration || 0, r.wpm || 0, r.pauseRate || 0,
      r.fillers || fw.total_count || 0, fw.severity || 'None', fillerTxt,
      sa.q1 != null ? +sa.q1 : null, sa.q2 != null ? +sa.q2 : null, sa.q3 != null ? +sa.q3 : null,
    ];
  };

  // ── Column header rows (3-row header matching report.xlsx) ──
  const TITLE_ROW = [`${title}`, ...Array(29).fill(null)];
  const GROUP_ROW = [
    'Student Information', null, null, null, null, null, null, null,
    'Performance Scores', null,
    'Criteria Breakdown', null, null, null, null, null,
    'Qualitative Feedback', null, null, null,
    'Voice Telemetry', null, null, null,
    'Filler Analysis', null, null,
    'Self Assessment', null, null,
  ];
  const HDR_ROW = [
    'S.No', 'Name', 'Register No.', 'College', 'Department', 'Topic', 'Faculty', 'Date',
    'Overall Score', 'Grade',
    ...CRIT.map(c => `${c.label}\n(0-100)`),
    'Strengths', 'Areas for Improvement', 'Detailed Feedback', 'Communication Suggestions',
    'Word Count', 'Duration (sec)', 'WPM', 'Pause Rate',
    'Filler Count', 'Severity', 'Fillers Detected',
    'Self: Speaking\n(1-10)', 'Self: Confidence\n(1-10)', 'Self: Fillers\n(1-10)',
  ];
  const COL_WIDTHS = [
    {wch:6},{wch:24},{wch:16},{wch:14},{wch:14},{wch:28},{wch:14},{wch:22},
    {wch:12},{wch:8},{wch:12},{wch:12},{wch:12},{wch:12},{wch:12},{wch:12},
    {wch:45},{wch:45},{wch:55},{wch:55},
    {wch:10},{wch:12},{wch:8},{wch:10},
    {wch:10},{wch:10},{wch:22},{wch:14},{wch:14},{wch:14},
  ];
  const MERGES = [
    {s:{r:0,c:0},e:{r:0,c:29}},
    {s:{r:1,c:0},e:{r:1,c:7}},{s:{r:1,c:8},e:{r:1,c:9}},{s:{r:1,c:10},e:{r:1,c:15}},
    {s:{r:1,c:16},e:{r:1,c:19}},{s:{r:1,c:20},e:{r:1,c:23}},{s:{r:1,c:24},e:{r:1,c:26}},{s:{r:1,c:27},e:{r:1,c:29}},
  ];

  // ── Style helpers ────────────────────────────────────────────
  const applyStyles = (ws, rowCount) => {
    const rng = XL.utils.decode_range(ws['!ref'] || 'A1:AD3');
    const groupColors = {
      'Student Information':'1E2D3C','Performance Scores':'A93226','Criteria Breakdown':'1E3A5F',
      'Qualitative Feedback':'2D6A4F','Voice Telemetry':'6B3FA0','Filler Analysis':'8A5A1B','Self Assessment':'4A4A4A',
    };
    const groupBounds = [[0,7,'Student Information'],[8,9,'Performance Scores'],[10,15,'Criteria Breakdown'],[16,19,'Qualitative Feedback'],[20,23,'Voice Telemetry'],[24,26,'Filler Analysis'],[27,29,'Self Assessment']];
    for (let C = 0; C <= 29; C++) {
      const t0 = XL.utils.encode_cell({r:0,c:C});
      if (ws[t0]) ws[t0].s = {font:{bold:true,sz:13,color:{rgb:'FFFFFF'}},fill:{fgColor:{rgb:'1E2D3C'}},alignment:{horizontal:'left'}};
      const t1 = XL.utils.encode_cell({r:1,c:C});
      if (ws[t1]) {
        const grp = ws[t1].v ? groupColors[ws[t1].v] : (groupBounds.find(([s,e])=>C>=s&&C<=e)?.[2] ? groupColors[groupBounds.find(([s,e])=>C>=s&&C<=e)[2]] : '1E2D3C');
        ws[t1].s = {font:{bold:true,sz:9,color:{rgb:'FFFFFF'}},fill:{fgColor:{rgb:grp||'1E2D3C'}},alignment:{horizontal:'center'}};
      }
      const t2 = XL.utils.encode_cell({r:2,c:C});
      if (ws[t2]) ws[t2].s = {font:{bold:true,sz:9,color:{rgb:'FFFFFF'}},fill:{fgColor:{rgb:'1E3A5F'}},alignment:{horizontal:'center',wrapText:true}};
    }
    for (let R = 3; R < 3 + rowCount; R++) {
      const even = (R-3)%2===0;
      for (let C = 0; C <= 29; C++) {
        const addr = XL.utils.encode_cell({r:R,c:C});
        if (!ws[addr]) ws[addr]={t:'z'};
        const base = {fill:{fgColor:{rgb:even?'FFFFFF':'F3F6FA'}},alignment:{vertical:'top',wrapText:C>=16}};
        if (C===8) {
          const v=ws[addr].v||0;
          ws[addr].s={...base,font:{bold:true,color:{rgb:v>=8?'16A34A':v>=6?'CA8A04':'DC2626'}}};
        } else if (C>=10&&C<=15) {
          const v=ws[addr].v||0;
          ws[addr].s={...base,font:{color:{rgb:v>=80?'16A34A':v>=60?'CA8A04':'DC2626'}},alignment:{horizontal:'center',vertical:'top'}};
        } else {
          ws[addr].s=base;
        }
      }
    }
  };

  const makeSheet = (reps, sheetTitle) => {
    const dataRows = reps.map((r, i) => makeRow(r, i));
    const ws = XL.utils.aoa_to_sheet([TITLE_ROW, GROUP_ROW, HDR_ROW, ...dataRows]);
    ws['!cols'] = COL_WIDTHS;
    ws['!merges'] = MERGES;
    applyStyles(ws, dataRows.length);
    return ws;
  };

  // ── Sheet 1: Summary ─────────────────────────────────────────
  const total = sorted.length;  // total submissions
  const uniqueStudents = new Set(sorted.map(r => r.student?.regNo).filter(Boolean)).size;
  const avg   = total ? (sorted.reduce((a,r)=>a+r.scores.overall,0)/total).toFixed(2) : 0;
  const pass  = sorted.filter(r=>r.scores.overall>=6).length;

  // Group by college → dept
  const byCollege = {};
  sorted.forEach(r => {
    const col  = r.student?.college || 'Unknown';
    const dept = r.student?.dept    || 'Unknown';
    if (!byCollege[col]) byCollege[col] = {};
    if (!byCollege[col][dept]) byCollege[col][dept] = [];
    byCollege[col][dept].push(r);
  });

  const summaryRows = [
    ['ChiselAssess — Analytics Summary', null, null, null],
    [`Generated: ${new Date().toLocaleString()}`, null, null, null],
    [],
    ['OVERALL SUMMARY'],
    ['Total students (unique)', uniqueStudents],
    ['Total submissions', total],
    ['Average score (/10)', avg],
    ['Pass rate (≥6)', total ? `${Math.round(pass/total*100)}%` : '—'],
    ['Highest score', total ? Math.max(...sorted.map(r=>r.scores.overall)).toFixed(1) : '—'],
    ['Lowest score',  total ? Math.min(...sorted.map(r=>r.scores.overall)).toFixed(1) : '—'],
    ['Need support (<4.5)', total - pass],
    [],
    ['CRITERIA AVERAGES'],
    ['Criterion','Avg (/100)','Grade'],
    ...CRIT.map(c => {
      const a = total ? Math.round(sorted.reduce((s,r)=>s+(r.scores[c.key]||0),0)/total) : 0;
      return [c.label, a, gradeOf(a/10).g];
    }),
    [],
    ['COLLEGE BREAKDOWN'],
    ['College','Submissions','Avg (/10)','Pass rate'],
    ...Object.entries(byCollege).map(([col, depts]) => {
      const reps = Object.values(depts).flat();
      const a = (reps.reduce((s,r)=>s+r.scores.overall,0)/reps.length).toFixed(1);
      const p = Math.round(reps.filter(r=>r.scores.overall>=6).length/reps.length*100);
      return [col, reps.length, a, `${p}%`];
    }),
    [],
    ['DEPARTMENT BREAKDOWN'],
    ['College','Department','Submissions','Avg (/10)','Pass rate'],
    ...Object.entries(byCollege).flatMap(([col, depts]) =>
      Object.entries(depts).map(([dept, reps]) => {
        const a = (reps.reduce((s,r)=>s+r.scores.overall,0)/reps.length).toFixed(1);
        const p = Math.round(reps.filter(r=>r.scores.overall>=6).length/reps.length*100);
        return [col, dept, reps.length, a, `${p}%`];
      })
    ),
  ];
  const wsSummary = XL.utils.aoa_to_sheet(summaryRows);
  wsSummary['!cols'] = [{wch:28},{wch:22},{wch:16},{wch:12},{wch:12}];

  // ── Style the summary sheet ───────────────────────────────────────────────
  const sumRange = XL.utils.decode_range(wsSummary['!ref'] || 'A1:E40');
  for (let R = sumRange.s.r; R <= sumRange.e.r; R++) {
    for (let C = sumRange.s.c; C <= sumRange.e.c; C++) {
      const addr = XL.utils.encode_cell({r:R, c:C});
      if (!wsSummary[addr]) continue;
      const v = wsSummary[addr].v;
      const vStr = String(v || '');
      // Title row
      if (R === 0) {
        wsSummary[addr].s = {font:{bold:true,sz:13,color:{rgb:'FFFFFF'}},fill:{fgColor:{rgb:'1E3A5F'}},alignment:{horizontal:'left'}};
      }
      // Generated row
      else if (R === 1) {
        wsSummary[addr].s = {font:{sz:8,color:{rgb:'6B7280'}},alignment:{horizontal:'left'}};
      }
      // Section headers (OVERALL, CRITERIA, COLLEGE, DEPARTMENT)
      else if (['OVERALL SUMMARY','CRITERIA AVERAGES','COLLEGE BREAKDOWN','DEPARTMENT BREAKDOWN'].includes(vStr)) {
        wsSummary[addr].s = {font:{bold:true,sz:9,color:{rgb:'FFFFFF'}},fill:{fgColor:{rgb:'0E7490'}},alignment:{horizontal:'left'}};
      }
      // Column sub-headers
      else if (['Criterion','College','Department'].includes(vStr) ||
               (R > 0 && ['Avg (/100)','Grade','Submissions','Avg (/10)','Pass rate'].includes(vStr))) {
        wsSummary[addr].s = {font:{bold:true,sz:8,color:{rgb:'FFFFFF'}},fill:{fgColor:{rgb:'1E3A5F'}},alignment:{horizontal:'center'}};
      }
      // Metric labels (first column data)
      else if (C === 0 && v && !['OVERALL SUMMARY','CRITERIA AVERAGES','COLLEGE BREAKDOWN','DEPARTMENT BREAKDOWN'].includes(vStr)) {
        wsSummary[addr].s = {font:{bold:true,sz:9,color:{rgb:'374151'}},fill:{fgColor:{rgb:'F1F5F9'}},alignment:{horizontal:'left'}};
      }
      // Numeric values in col 1 (avg score, pass rate etc.)
      else if (C === 1 && R > 3 && R < 12) {
        const numV = parseFloat(v) || 0;
        const fg = numV >= 7 ? '16A34A' : (numV >= 5 ? 'CA8A04' : 'DC2626');
        wsSummary[addr].s = {font:{bold:true,sz:11,color:{rgb:fg}},alignment:{horizontal:'center'}};
      }
      // Criteria score values
      else if (C === 1 && v && typeof v === 'number' && R > 12) {
        const fg = v >= 75 ? '16A34A' : (v >= 55 ? 'CA8A04' : 'DC2626');
        const bg = v >= 75 ? 'DCFCE7' : (v >= 55 ? 'FEF3C7' : 'FEE2E2');
        wsSummary[addr].s = {font:{bold:true,sz:10,color:{rgb:fg}},fill:{fgColor:{rgb:bg}},alignment:{horizontal:'center'}};
      }
      // Grade col
      else if (C === 2 && v && typeof v === 'string' && ['A+','A','B+','B','C+','C','D'].includes(v)) {
        wsSummary[addr].s = {font:{bold:true,sz:9},alignment:{horizontal:'center'}};
      }
      // College/dept avg scores (col 2 in breakdown)
      else if (C === 2 && v && typeof v === 'number' && v <= 10) {
        const fg = v >= 7 ? '16A34A' : (v >= 5.5 ? 'CA8A04' : 'DC2626');
        wsSummary[addr].s = {font:{bold:true,sz:10,color:{rgb:fg}},alignment:{horizontal:'center'}};
      }
      // Pass rate col
      else if (C === 3 && vStr.includes('%')) {
        wsSummary[addr].s = {font:{sz:9},alignment:{horizontal:'center'}};
      }
      else {
        wsSummary[addr].s = {font:{sz:9},alignment:{horizontal:'left'}};
      }
    }
  }
  XL.utils.book_append_sheet(wb, wsSummary, 'Summary');

  // ── Sheet 2+: One per department (deduplicate sheet names) ──
  const usedSheetNames = new Set(['Summary']);
  const getUniqueSheetName = (base) => {
    const clean = base.replace(/[:\\/?*[\]]/g,'').slice(0,28) || 'Dept';
    if (!usedSheetNames.has(clean)) { usedSheetNames.add(clean); return clean; }
    for (let i=2; i<=99; i++) {
      const n = `${clean.slice(0,25)}_${i}`;
      if (!usedSheetNames.has(n)) { usedSheetNames.add(n); return n; }
    }
    return clean + '_' + Date.now().toString(36).slice(-4);
  };

  Object.entries(byCollege).forEach(([col, depts]) => {
    Object.entries(depts).forEach(([dept, reps]) => {
      const sheetTitle = `${col} — ${dept}`;
      const sheetName  = getUniqueSheetName(dept);
      XL.utils.book_append_sheet(wb, makeSheet(reps, sheetTitle), sheetName);
    });
  });

  return XL.write(wb, { bookType: 'xlsx', type: 'array' });
}


// ══════════════════════════════════════════════════════════════
//  ANALYTICS PDF BUILDER — matches ChiselAssess_Slot23_Analytics.pdf
// ══════════════════════════════════════════════════════════════
function buildAnalyticsPDF(reports, title = 'Analytics Report', meta = {}) {
  const { jsPDF } = window.jspdf;
  const pdf = new jsPDF({ unit: 'pt', format: 'a4' });
  const PW = 595.28, PH = 841.89, ML = 40, MR = 40, CW = PW - ML - MR;
  let y = ML;
  const gradeOf = _gradeOf();

  const addPage = () => { pdf.addPage(); y = ML; };
  const checkY  = (n=40) => { if (y + n > PH - 50) addPage(); };

  // ── Header bar ────────────────────────────────────────────────
  pdf.setFillColor(30, 45, 60);
  pdf.rect(0, 0, PW, 70, 'F');
  pdf.setFillColor(169, 50, 38);
  pdf.rect(0, 70, PW, 3, 'F');
  pdf.setFont('helvetica','bold'); pdf.setFontSize(9); pdf.setTextColor(147,197,253);
  pdf.text('Learning Solutions @Chisel', ML, 22);
  pdf.setFontSize(20); pdf.setTextColor(255,255,255);
  pdf.text('ChiselAssess', ML, 44);
  pdf.setFont('helvetica','normal'); pdf.setFontSize(9); pdf.setTextColor(147,197,253);
  pdf.text('Slot Analytics Report', ML, 58);
  pdf.setFontSize(8); pdf.setTextColor(148,163,184);
  pdf.text(new Date().toLocaleDateString('en-GB',{day:'2-digit',month:'short',year:'numeric'}), PW-MR, 30, {align:'right'});
  y = 85;

  // ── Title ─────────────────────────────────────────────────────
  pdf.setFont('helvetica','bold'); pdf.setFontSize(16); pdf.setTextColor(30,30,30);
  pdf.text(title, ML, y); y += 18;
  const subtitle = [
    meta.dept && meta.dept !== 'All' ? `Dept: ${meta.dept}` : null,
    meta.college ? `College: ${meta.college}` : null,
  ].filter(Boolean).join('  ·  ') || 'All departments · All colleges';
  pdf.setFont('helvetica','normal'); pdf.setFontSize(9); pdf.setTextColor(100,100,100);
  pdf.text(subtitle, ML, y); y += 20;

  // ── Stats row (4 cards) ───────────────────────────────────────
  const total = reports.length;
  const avg   = total ? (reports.reduce((a,r)=>a+r.scores.overall,0)/total) : 0;
  const top   = total ? Math.max(...reports.map(r=>r.scores.overall)) : 0;
  const needSupport = reports.filter(r=>r.scores.overall<4.5).length;
  const statCards = [
    { label:'Submissions', value: String(total),            color:[30,45,60]  },
    { label:'Avg Score',   value: `${avg.toFixed(1)}/10`,   color:[169,50,38] },
    { label:'Top Score',   value: `${top.toFixed(1)}/10`,   color:[34,130,84] },
    { label:'Need Support',value: String(needSupport),       color:[200,100,30]},
  ];
  const cardW = (CW - 12) / 4;
  statCards.forEach(({ label, value, color }, i) => {
    const cx = ML + i * (cardW + 4);
    pdf.setFillColor(...color); pdf.roundedRect(cx, y, cardW, 48, 4, 4, 'F');
    pdf.setFont('helvetica','bold'); pdf.setFontSize(20); pdf.setTextColor(255,255,255);
    pdf.text(value, cx + cardW/2, y + 28, {align:'center'});
    pdf.setFont('helvetica','normal'); pdf.setFontSize(7.5); pdf.setTextColor(200,220,240);
    pdf.text(label.toUpperCase(), cx + cardW/2, y + 40, {align:'center'});
  });
  y += 60;

  // ── Score Distribution ────────────────────────────────────────
  checkY(100);
  pdf.setFont('helvetica','bold'); pdf.setFontSize(10); pdf.setTextColor(30,30,30);
  pdf.text('Score Distribution', ML, y); pdf.setFontSize(8); pdf.setTextColor(100,100,100);
  pdf.text(`· ${total} submissions`, ML + 95, y); y += 14;

  const buckets = [
    {min:0,max:5,label:'<5',color:[239,68,68]},
    {min:5,max:6,label:'5–6',color:[249,115,22]},
    {min:6,max:7,label:'6–7',color:[234,179,8]},
    {min:7,max:8,label:'7–8',color:[34,197,94]},
    {min:8,max:9,label:'8–9',color:[16,185,129]},
    {min:9,max:11,label:'9–10',color:[16,163,219]},
  ];
  const bCounts = buckets.map(b => reports.filter(r=>r.scores.overall>=b.min&&r.scores.overall<b.max).length);
  const bMax = Math.max(...bCounts, 1);
  const bW = (CW - 30) / 6, bH = 60;
  bCounts.forEach((cnt, i) => {
    const bx = ML + i*(bW+6);
    const barH = Math.max(4, (cnt/bMax)*bH);
    pdf.setFillColor(...buckets[i].color);
    pdf.roundedRect(bx, y + bH - barH, bW, barH, 2, 2, 'F');
    if (cnt > 0) {
      pdf.setFont('helvetica','bold'); pdf.setFontSize(8); pdf.setTextColor(30,30,30);
      pdf.text(String(cnt), bx + bW/2, y + bH - barH - 3, {align:'center'});
    }
    pdf.setFont('helvetica','normal'); pdf.setFontSize(7.5); pdf.setTextColor(100,100,100);
    pdf.text(buckets[i].label, bx + bW/2, y + bH + 10, {align:'center'});
  });
  y += bH + 22;

  // ── Criteria Averages ─────────────────────────────────────────
  checkY(120);
  pdf.setFont('helvetica','bold'); pdf.setFontSize(10); pdf.setTextColor(30,30,30);
  pdf.text('Criteria Averages  (out of 100)', ML, y); y += 14;
  const critColors = [[169,50,38],[30,86,160],[34,130,84],[120,50,160],[190,120,30],[30,130,130]];
  CRIT.forEach((c, i) => {
    const avg100 = total ? Math.round(reports.reduce((s,r)=>s+(r.scores[c.key]||0),0)/total) : 0;
    const barLen = (avg100/100) * (CW - 80);
    pdf.setFillColor(240,242,245); pdf.rect(ML + 120, y - 9, CW - 120, 13, 'F');
    pdf.setFillColor(...critColors[i]); pdf.roundedRect(ML + 120, y - 9, Math.max(4, barLen), 13, 2, 2, 'F');
    pdf.setFont('helvetica','normal'); pdf.setFontSize(8.5); pdf.setTextColor(50,50,50);
    pdf.text(c.label, ML, y);
    pdf.setFont('helvetica','bold'); pdf.setFontSize(8.5); pdf.setTextColor(...critColors[i]);
    pdf.text(String(avg100), ML + CW, y, {align:'right'});
    y += 16;
  });
  y += 6;

  // ── Grade Mix + Dept Breakdown (2 columns) ────────────────────
  checkY(120);
  const gradeList = ['A+','A','B+','B','C+','C','D','E'];
  const gradeColors = {
    'A+':[0,150,80],'A':[30,120,60],'B+':[0,100,160],'B':[30,80,130],
    'C+':[200,140,0],'C':[180,120,0],'D':[200,80,0],'E':[200,50,50],
  };
  const gradeCounts = {};
  gradeList.forEach(g => { gradeCounts[g] = reports.filter(r=>(r.grade?.g||gradeOf(r.scores.overall).g)===g).length; });

  const col1W = CW * 0.38, col2W = CW * 0.58;
  const colY = y;

  pdf.setFont('helvetica','bold'); pdf.setFontSize(10); pdf.setTextColor(30,30,30);
  pdf.text('Grade Mix', ML, colY); let gy = colY + 14;
  gradeList.forEach(g => {
    const cnt = gradeCounts[g];
    const [r2,g2,b2] = gradeColors[g];
    pdf.setFillColor(r2,g2,b2); pdf.roundedRect(ML, gy-8, 20, 11, 2, 2, 'F');
    pdf.setFont('helvetica','bold'); pdf.setFontSize(8); pdf.setTextColor(255,255,255);
    pdf.text(g, ML+10, gy, {align:'center'});
    pdf.setFont('helvetica','normal'); pdf.setFontSize(8); pdf.setTextColor(60,60,60);
    pdf.text(String(cnt), ML+28, gy);
    gy += 13;
  });

  // ── Department Breakdown ─────────────────────────────────────
  const byDept = {};
  reports.forEach(r => {
    const d = r.student?.dept || 'Unknown';
    if (!byDept[d]) byDept[d] = [];
    byDept[d].push(r);
  });
  const col2X = ML + col1W + 10;
  pdf.setFont('helvetica','bold'); pdf.setFontSize(10); pdf.setTextColor(30,30,30);
  pdf.text('Department Breakdown', col2X, colY);
  const dHdr = ['Department','N','Avg','Pass%'];
  const dW = [col2W-80,20,24,26].map(w=>w);
  let dy = colY + 14;
  pdf.setFillColor(30,45,60); pdf.rect(col2X, dy-9, col2W, 12, 'F');
  let dx = col2X + 4;
  dHdr.forEach((h,i)=>{ pdf.setFont('helvetica','bold'); pdf.setFontSize(7.5); pdf.setTextColor(255,255,255); pdf.text(h, dx, dy); dx += dW[i]+10; });
  dy += 13;
  Object.entries(byDept).sort((a,b)=>b[1].length-a[1].length).forEach(([dept,reps],ri)=>{
    if (dy > PH - 80) return;
    const da = (reps.reduce((s,r)=>s+r.scores.overall,0)/reps.length).toFixed(1);
    const dp = Math.round(reps.filter(r=>r.scores.overall>=6).length/reps.length*100);
    if (ri%2===0) { pdf.setFillColor(248,250,252); pdf.rect(col2X, dy-9, col2W, 12, 'F'); }
    dx = col2X + 4;
    [dept.slice(0,22), String(reps.length), da, `${dp}%`].forEach((v,i)=>{
      pdf.setFont('helvetica','normal'); pdf.setFontSize(7.5); pdf.setTextColor(40,40,40);
      pdf.text(v, dx, dy); dx += dW[i]+10;
    });
    dy += 12;
  });
  y = Math.max(gy, dy) + 10;

  // ── Top Performers ─────────────────────────────────────────────
  checkY(80);
  const top5 = [...reports].sort((a,b)=>b.scores.overall-a.scores.overall).slice(0,10);
  pdf.setFont('helvetica','bold'); pdf.setFontSize(10); pdf.setTextColor(30,30,30);
  pdf.text('Top Performers', ML, y); y += 14;
  pdf.setFillColor(30,45,60); pdf.rect(ML, y-9, CW, 12, 'F');
  const topHdrs = ['#','Name','Reg No','Dept','College','Score','Grade'];
  const topW    = [14,100,80,70,70,40,30];
  let tx = ML + 4;
  topHdrs.forEach((h,i)=>{ pdf.setFont('helvetica','bold'); pdf.setFontSize(7.5); pdf.setTextColor(255,255,255); pdf.text(h,tx,y); tx+=topW[i]+4; });
  y += 13;
  top5.forEach((r,ri) => {
    checkY(13);
    if (ri%2===0) { pdf.setFillColor(248,250,252); pdf.rect(ML,y-9,CW,12,'F'); }
    tx = ML + 4;
    [String(ri+1), (r.student.name||'').slice(0,18), (r.student.regNo||'').slice(0,14), (r.student.dept||'').slice(0,12), (r.student.college||'').slice(0,12), `${r.scores.overall.toFixed(1)}/10`, r.grade?.g||''].forEach((v,i)=>{
      const col = i===5 ? [34,130,84] : [40,40,40];
      pdf.setFont(i===5?'helvetica':'helvetica', i===5?'bold':'normal'); pdf.setFontSize(7.5); pdf.setTextColor(...col);
      pdf.text(v, tx, y); tx += topW[i]+4;
    });
    y += 12;
  });
  y += 8;

  // ── Needs Support ─────────────────────────────────────────────
  const ns = [...reports].filter(r=>r.scores.overall<4.5).sort((a,b)=>a.scores.overall-b.scores.overall).slice(0,20);
  if (ns.length > 0) {
    checkY(60);
    pdf.setFont('helvetica','bold'); pdf.setFontSize(10); pdf.setTextColor(200,50,50);
    pdf.text(`Needs Support — ${reports.filter(r=>r.scores.overall<4.5).length} students scored below 4.5`, ML, y); y += 14;
    pdf.setFillColor(200,50,50); pdf.rect(ML, y-9, CW, 12, 'F');
    const nsHdrs = ['Name','Reg No','Dept','Score','Weakest Criterion'];
    const nsW    = [110,80,80,40,CW-330];
    let nx = ML + 4;
    nsHdrs.forEach((h,i)=>{ pdf.setFont('helvetica','bold'); pdf.setFontSize(7.5); pdf.setTextColor(255,255,255); pdf.text(h,nx,y); nx+=nsW[i]+4; });
    y += 13;
    ns.forEach((r,ri) => {
      checkY(13);
      if (ri%2===0) { pdf.setFillColor(255,245,245); pdf.rect(ML,y-9,CW,12,'F'); }
      const weakest = CRIT.reduce((a,c)=>(r.scores[c.key]||0)<(r.scores[a.key]||0)?c:a, CRIT[0]);
      nx = ML + 4;
      [(r.student.name||'').slice(0,18),(r.student.regNo||'').slice(0,14),(r.student.dept||'').slice(0,12),`${r.scores.overall.toFixed(1)}/10`,weakest.label].forEach((v,i)=>{
        const col = i===3 ? [200,50,50] : [40,40,40];
        pdf.setFont(i===3?'helvetica':'helvetica',i===3?'bold':'normal'); pdf.setFontSize(7.5); pdf.setTextColor(...col);
        pdf.text(v,nx,y); nx+=nsW[i]+4;
      });
      y += 12;
    });
  }

  // ── Footer on every page ──────────────────────────────────────
  const totalPages = pdf.internal.getNumberOfPages();
  for (let i=1; i<=totalPages; i++) {
    pdf.setPage(i);
    pdf.setDrawColor(229,231,235); pdf.line(ML, PH-30, PW-MR, PH-30);
    pdf.setFont('helvetica','bold'); pdf.setFontSize(7); pdf.setTextColor(150,150,150);
    pdf.text('ChiselAssess  ·  Confidential Analytics Report', ML, PH-16);
    pdf.text(`Page ${i} of ${totalPages}`, PW-MR, PH-16, {align:'right'});
  }

  return pdf.output('arraybuffer');
}


// ══════════════════════════════════════════════════════════════
//  ZIP BUILDER — Option 2
// ══════════════════════════════════════════════════════════════
async function buildZip(reports, title, onProgress, includePDFs = true, slotIds = '') {
  if (!window.JSZip) throw new Error('JSZip not loaded');
  const zip = new window.JSZip();

  // ── Fetch styled Excel from backend (uses openpyxl with full colour support) ──
  // Falls back to client-side buildExcel if backend is unavailable.
  const _fetchStyledExcel = async (collegeRaw, deptRaw) => {
    const token = localStorage.getItem('ca_token');
    if (!token) return null;
    try {
      const params = new URLSearchParams();
      if (slotIds) params.set('slot_ids', slotIds);
      if (collegeRaw) params.set('college', collegeRaw);
      if (deptRaw)    params.set('dept', deptRaw);
      if (title)      params.set('title', title);
      const resp = await fetch(
        `${window.API_BASE || ''}/export-excel?${params}`,
        { headers: { Authorization: `Bearer ${token}` } }
      );
      if (!resp.ok) return null;
      return await resp.arrayBuffer();
    } catch { return null; }
  };

  // Group: college → dept → reports
  const byCollege = {};
  reports.forEach(r => {
    const col  = (r.student?.college || 'Unknown').replace(/[\\/:*?"<>|]/g,'_');
    const dept = (r.student?.dept    || 'Unknown').replace(/[\\/:*?"<>|]/g,'_');
    if (!byCollege[col]) byCollege[col]={};
    if (!byCollege[col][dept]) byCollege[col][dept]=[];
    byCollege[col][dept].push(r);
  });

  const allDepts = Object.values(byCollege).flatMap(d => Object.values(d));
  const totalStudents = reports.length;
  const totalDepts = allDepts.length;
  let studentsDone = 0;

  for (const [college, depts] of Object.entries(byCollege)) {
    const collegeFolder = zip.folder(college);
    const collegeReps = Object.values(depts).flat();

    // Collect original (un-sanitised) names for the API filter
    const collegeRaw = collegeReps[0]?.student?.college || '';

    onProgress?.(`📊 Building Excel for ${college} (${collegeReps.length} students)…`);
    const collegeExcel = await _fetchStyledExcel(collegeRaw, '');
    collegeFolder.file(
      `${college}_Analytics.xlsx`,
      collegeExcel || buildExcel(collegeReps, `${title} — ${college}`)
    );

    onProgress?.(`📄 Building Analytics PDF for ${college}…`);
    collegeFolder.file(`${college}_Analytics.pdf`,
      buildAnalyticsPDF(collegeReps, `${title} — ${college}`, { college }));

    for (const [dept, deptReps] of Object.entries(depts)) {
      const deptFolder = collegeFolder.folder(dept);
      const deptRaw = deptReps[0]?.student?.dept || '';
      onProgress?.(`📊 ${college} / ${dept} — Excel…`);
      const deptExcel = await _fetchStyledExcel(collegeRaw, deptRaw);
      deptFolder.file(
        `${dept}_Analytics.xlsx`,
        deptExcel || buildExcel(deptReps, `${title} — ${dept}`)
      );

      onProgress?.(`📄 ${college} / ${dept} — Analytics PDF…`);
      deptFolder.file(`${dept}_Analytics.pdf`,
        buildAnalyticsPDF(deptReps, `${title} — ${dept}`, { college, dept }));

      if (includePDFs && window._generateReportPDFBytes && window.jspdf) {
        // Generate student PDFs in chunks of 5 to avoid freezing the browser
        const CHUNK = 5;
        for (let ci = 0; ci < deptReps.length; ci += CHUNK) {
          const chunk = deptReps.slice(ci, ci + CHUNK);
          onProgress?.(`🎓 Student PDFs — ${college}/${dept}: ${ci+1}–${Math.min(ci+CHUNK, deptReps.length)} of ${deptReps.length} (total: ${studentsDone+ci+1}/${totalStudents})`);
          await Promise.all(chunk.map(async r => {
            try {
              const bytes = await window._generateReportPDFBytes(r);
              if (bytes) {
                const safeName = (r.student.name||'Student').replace(/[^a-zA-Z0-9_\- ]/g,'').replace(/\s+/g,'_');
                deptFolder.file(`${r.student.regNo||'XX'}_${safeName}.pdf`, bytes);
              }
            } catch(e) { console.warn('PDF skip:', r.student?.name, e); }
          }));
          // Yield to browser between chunks to prevent freeze
          await new Promise(res => setTimeout(res, 0));
        }
        studentsDone += deptReps.length;
      }
    }
  }

  onProgress?.('🗜️ Compressing ZIP…');
  const blob = await zip.generateAsync(
    { type:'blob', compression:'DEFLATE', compressionOptions:{level:4} },
    (meta) => onProgress?.(`🗜️ Compressing… ${meta.percent.toFixed(0)}%`)
  );
  return blob;
}


// ══════════════════════════════════════════════════════════════
//  DOWNLOAD MODAL — clean UI
// ══════════════════════════════════════════════════════════════
function DownloadModal({ onClose, slotId = null, slotTitle = null, slots = null }) {
  // slotId+slotTitle = single slot mode
  // slots = overview mode (array of slot objects)

  const [colleges, setColleges] = useState([]);
  const [filterBy, setFilterBy] = useState('all');
  const [selCollege, setSelCollege] = useState('');
  const [selDept, setSelDept]       = useState('');
  const [busy, setBusy]        = useState(false);
  const [progress, setProgress] = useState('');
  const [error, setError]       = useState('');
  const [includePDFs, setIncludePDFs] = useState(false); // off by default — slow for large batches
  const cancelRef = useRef(false);

  useEffect(() => {
    _api()('/colleges/').then(d => setColleges(Array.isArray(d)?d:[])).catch(()=>{});
  }, []);

  const collegeList = colleges.map(c=>c.name);
  const deptList    = useMemo(()=>{
    if (filterBy!=='dept') return [];
    if (selCollege) { const c=colleges.find(c=>c.name===selCollege); return c?c.departments.map(d=>d.name):[]; }
    return colleges.flatMap(c=>c.departments.map(d=>d.name));
  }, [filterBy, selCollege, colleges]);

  const canExport = filterBy==='all' || (filterBy==='college'&&selCollege) || (filterBy==='dept'&&selDept);

  // ── Fetch reports ─────────────────────────────────────────────
  const fetchReports = useCallback(async () => {
    const norm = _norm();
    if (!norm) throw new Error('normaliseApiReport not loaded');
    setProgress('Fetching reports…');

    let raw = [];
    if (slotId) {
      const d = await _api()(`/slots/${slotId}/reports`);
      raw = Array.isArray(d) ? d : (d.reports||[]);
    } else {
      const allSlots = slots || [];
      const BATCH = 5;
      for (let i=0; i<allSlots.length; i+=BATCH) {
        setProgress(`Fetching slots ${i+1}–${Math.min(i+BATCH,allSlots.length)} of ${allSlots.length}…`);
        const batch = allSlots.slice(i,i+BATCH);
        const results = await Promise.allSettled(
          batch.map(s => _api()(`/slots/${s.id}/reports`).then(d=>{
            const list = Array.isArray(d)?d:(d.reports||[]);
            return list.map(r=>({...r,_slotTitle:s.title}));
          }))
        );
        results.forEach(r=>{ if(r.status==='fulfilled') raw=raw.concat(r.value); });
      }
    }

    // Normalise + filter
    let reports = raw.map(r=>norm(r));
    if (filterBy==='college' && selCollege) {
      reports = reports.filter(r=>(r.student?.college||'').toLowerCase()===selCollege.toLowerCase());
    } else if (filterBy==='dept') {
      if (selCollege) reports = reports.filter(r=>(r.student?.college||'').toLowerCase()===selCollege.toLowerCase());
      if (selDept)    reports = reports.filter(r=>(r.student?.dept||'').toLowerCase()===selDept.toLowerCase());
    }
    if (!reports.length) throw new Error('No reports found for the selected filter');
    return reports;
  }, [slotId, slotTitle, slots, filterBy, selCollege, selDept]);

  // ── Option 1: Excel download ──────────────────────────────────
  const handleExcel = async () => {
    setBusy(true); setError('');
    try {
      setProgress('Requesting Excel from server…');
      const params = new URLSearchParams();

      if (slotId) {
        // Single slot mode
        params.set('slot_id', String(slotId));
        if (slotTitle) params.set('title', slotTitle);
      } else if (slots && slots.length > 0) {
        // Overview mode — pass all slot IDs as comma-separated
        const ids = slots.map(s => s.id).filter(Boolean).join(',');
        params.set('slot_ids', ids);
        params.set('title', slotTitle || 'All Slots');
      }

      // College / dept filter
      if (filterBy === 'college' && selCollege) params.set('college', selCollege);
      if (filterBy === 'dept'    && selDept)    params.set('dept',    selDept);

      const token = localStorage.getItem('ca_token');
      const url = `${window.API_BASE || ''}/export-excel?${params.toString()}`;

      setProgress(`Fetching from server (${slots ? slots.length : 1} slot${slots?.length !== 1 ? 's' : ''})…`);
      const res = await fetch(url, {
        headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}) }
      });

      if (!res.ok) {
        const j = await res.json().catch(() => ({}));
        throw new Error(j.detail || j.message || `Server error ${res.status} — check backend logs`);
      }

      const blob = await res.blob();
      if (blob.size < 500) {
        // File too small — likely an error JSON wrapped as blob
        const text = await blob.text();
        throw new Error(`Empty file returned. Server said: ${text.slice(0,200)}`);
      }

      const date = new Date().toISOString().slice(0,10);
      const safeName = (slotTitle || 'All').replace(/[^a-zA-Z0-9]/g, '_');
      _dl(blob, `ChiselAssess_${safeName}_${date}.xlsx`);
      setProgress('✓ Done');
      setTimeout(onClose, 800);
    } catch(e) {
      setError(String(e.message || 'Export failed'));
    } finally { setBusy(false); }
  };

  // ── Option 2: ZIP download ────────────────────────────────────
  const handleZip = async () => {
    if (!window.JSZip) { setError('JSZip not loaded — try refreshing'); return; }
    cancelRef.current = false;
    setBusy(true); setError('');
    setProgress('Starting…');
    try {
      const reports = await fetchReports();
      if (!reports || !reports.length) { setError('No reports found'); return; }
      setProgress(`Building ZIP for ${reports.length} students…`);
      const slotIds = slotId ? String(slotId) : (slots || []).map(s => s.id).join(',');
      const blob = await buildZip(
        reports, slotTitle||'ChiselAssess',
        (msg) => { if (!cancelRef.current) setProgress(msg); },
        includePDFs,
        slotIds,
      );
      if (cancelRef.current) { setProgress('Cancelled.'); return; }
      const date = new Date().toISOString().slice(0,10);
      _dl(blob, `ChiselAssess_ZIP_${(slotTitle||'All').replace(/[^a-zA-Z0-9]/g,'_')}_${date}.zip`);
      setProgress('✓ Done — check your Downloads folder');
      setTimeout(onClose, 1500);
    } catch(e) {
      console.error('[ZIP error]', e);
      setError(`ZIP failed: ${e.message || String(e)}`);
    }
    finally { setBusy(false); }
  };

  return (
    <Modal onClose={onClose} title="Download Reports"
      subtitle={slotTitle ? `Slot: ${slotTitle}` : `All slots (${(slots||[]).length})`}
      width={500}>

      {/* Filter */}
      <Field label="Scope" hint="Filter which students to include">
        <div style={{ display:'flex', gap:6, marginTop:6, flexWrap:'wrap' }}>
          {[['all','All records'],['college','By college'],['dept','By department']].map(([v,l])=>(
            <button key={v} className={`btn btn-sm ${filterBy===v?'btn-primary':'btn-ghost'}`}
              onClick={()=>{setFilterBy(v); setSelCollege(''); setSelDept('');}}>{l}</button>
          ))}
        </div>
      </Field>

      {filterBy==='college'&&(
        <Field label="College" required>
          <select className="input" value={selCollege} onChange={e=>setSelCollege(e.target.value)} style={{marginTop:6}}>
            <option value="">— choose college —</option>
            {collegeList.map(c=><option key={c} value={c}>{c}</option>)}
          </select>
        </Field>
      )}
      {filterBy==='dept'&&(
        <>
          <Field label="College">
            <select className="input" value={selCollege} onChange={e=>{setSelCollege(e.target.value); setSelDept('');}} style={{marginTop:6}}>
              <option value="">— all colleges —</option>
              {collegeList.map(c=><option key={c} value={c}>{c}</option>)}
            </select>
          </Field>
          <Field label="Department" required>
            <select className="input" value={selDept} onChange={e=>setSelDept(e.target.value)} style={{marginTop:6}}>
              <option value="">— choose department —</option>
              {deptList.map(d=><option key={d} value={d}>{d}</option>)}
            </select>
          </Field>
        </>
      )}

      {/* Progress / Error */}
      {busy && progress && (
        <div style={{ marginTop:10, padding:'8px 12px', background:'color-mix(in oklab, var(--accent) 8%, transparent)',
          border:'1px solid color-mix(in oklab, var(--accent) 22%, var(--rule))', borderRadius:6,
          fontSize:11, color:'var(--accent)', display:'flex', alignItems:'center', gap:8 }}>
          <div style={{ width:11, height:11, border:'2px solid color-mix(in oklab, var(--accent) 30%, transparent)',
            borderTopColor:'var(--accent)', borderRadius:'50%', animation:'rotate 1s linear infinite', flexShrink:0 }}/>
          {progress}
        </div>
      )}
      {error && <div style={{ marginTop:8, padding:'6px 12px', background:'color-mix(in oklab, var(--bad) 8%, transparent)',
        border:'1px solid color-mix(in oklab, var(--bad) 22%, var(--rule))', borderRadius:6, fontSize:11, color:'var(--bad)' }}>
        {error}
      </div>}

      {/* Option cards */}
      <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:12, marginTop:16 }}>
        {/* Option 1 */}
        <div style={{ border:'1px solid var(--rule)', borderRadius:10, padding:'14px 16px', background:'var(--paper-2)' }}>
          <div style={{ fontSize:11, fontWeight:700, color:'var(--ink)', marginBottom:6 }}>
            <Icon name="download" size={11}/> Option 1 — Excel
          </div>
          <div style={{ fontSize:10.5, color:'var(--ink-3)', lineHeight:1.5, marginBottom:12 }}>
            Single .xlsx file with:<br/>
            • Summary sheet<br/>
            • One sheet per department<br/>
            • 30 columns, full feedback
          </div>
          <button className="btn btn-sm btn-primary" style={{ width:'100%' }}
            onClick={handleExcel} disabled={busy||!canExport}>
            {busy?'…':'Download Excel'}
          </button>
        </div>

        {/* Option 2 */}
        <div style={{ border:'1px solid var(--accent)', borderRadius:10, padding:'14px 16px', background:'color-mix(in oklab, var(--accent) 5%, var(--paper-2))' }}>
          <div style={{ fontSize:11, fontWeight:700, color:'var(--accent)', marginBottom:6 }}>
            <Icon name="download" size={11}/> Option 2 — Full ZIP
          </div>
          <div style={{ fontSize:10.5, color:'var(--ink-3)', lineHeight:1.5, marginBottom:10 }}>
            Structured ZIP with:<br/>
            • College / Dept folders<br/>
            • Analytics Excel + PDF per level<br/>
            • Individual student PDFs (optional)
          </div>
          <label style={{ display:'flex', alignItems:'center', gap:8, marginBottom:12, cursor:'pointer', fontSize:11 }}>
            <input type="checkbox" checked={includePDFs} onChange={e=>setIncludePDFs(e.target.checked)}/>
            <span style={{ color:'var(--ink-2)' }}>Include individual student PDFs</span>
            <span style={{ color:'var(--ink-4)', fontSize:10 }}>
              {includePDFs ? '⚠ slow for 100+ students' : '(faster — analytics only)'}
            </span>
          </label>
          <div style={{ display:'flex', gap:6 }}>
            <button className="btn btn-sm btn-primary" style={{ flex:1 }}
              onClick={handleZip} disabled={busy||!canExport}>
              {busy ? <><span style={{ display:'inline-block', width:10, height:10, border:'2px solid rgba(255,255,255,.3)', borderTopColor:'#fff', borderRadius:'50%', animation:'rotate 1s linear infinite', marginRight:6 }}/>Building…</> : 'Download ZIP'}
            </button>
            {busy && (
              <button className="btn btn-sm btn-ghost" onClick={()=>{cancelRef.current=true; setBusy(false); setProgress('Cancelled.');}}>
                Cancel
              </button>
            )}
          </div>
        </div>
      </div>
    </Modal>
  );
}

Object.assign(window, { DownloadModal, buildAnalyticsPDF, buildExcel, buildZip });
