/* Shared helpers, icons, and mock data for the ChiselAssess redesign prototype.
   Exposes everything to window so other JSX scripts can reach it. */

// ── Tiny utilities ─────────────────────────────────────────
const cls = (...xs) => xs.filter(Boolean).join(' ');
const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
const fmt = (n, d = 0) => Number(n).toFixed(d);
const initials = (name) =>
  (name || '?')
    .split(/\s+/)
    .filter(Boolean)
    .slice(0, 2)
    .map((s) => s[0])
    .join('')
    .toUpperCase();
const daysAgo = (n) => {
  const d = new Date();
  d.setDate(d.getDate() - n);
  return d;
};
// ── Date helpers (always render in IST = UTC+5:30) ──────────────
const IST = 'Asia/Kolkata';
const fmtDate = (d) => {
  if (!d || isNaN(d)) return '—';
  return d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric', timeZone: IST });
};
const fmtTime = (d) => {
  if (!d || isNaN(d)) return '—';
  return d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', timeZone: IST });
};
// Parse a backend timestamp (may be naive UTC like "2024-01-15 10:30:00")
const parseApiDate = (s) => {
  if (!s) return new Date();
  let str = String(s).trim();
  if (str.includes(' ') && !str.includes('T')) str = str.replace(' ', 'T');
  if (!str.endsWith('Z') && !/[+\-]\d{2}:\d{2}$/.test(str)) str += 'Z';
  const d = new Date(str);
  return isNaN(d) ? new Date() : d;
};
const relTime = (d) => {
  if (!d || isNaN(d)) return '—';
  const s = Math.round((Date.now() - d.getTime()) / 1000);
  if (s < 60) return `${s}s ago`;
  if (s < 3600) return `${Math.round(s / 60)}m ago`;
  if (s < 86400) return `${Math.round(s / 3600)}h ago`;
  if (s < 86400 * 7) return `${Math.round(s / 86400)}d ago`;
  return fmtDate(d);
};
const gradeOf = (score) => {
  // score is 0-10 (overall) OR 0-100 (per-criterion)
  // Normalise to 0-10 for grade thresholds
  const s = score > 10 ? score / 10 : score;
  if (s >= 9)   return { g: 'A+', label: 'Exemplary',   tone: 'ok' };
  if (s >= 8)   return { g: 'A',  label: 'Excellent',   tone: 'ok' };
  if (s >= 7)   return { g: 'B+', label: 'Very Good',   tone: 'ok' };
  if (s >= 6)   return { g: 'B',  label: 'Good',        tone: 'info' };
  if (s >= 5)   return { g: 'C+', label: 'Satisfactory',tone: 'warn' };
  if (s >= 4)   return { g: 'C',  label: 'Needs work',  tone: 'warn' };
  if (s >= 3)   return { g: 'D',  label: 'Weak',        tone: 'bad' };
  return              { g: 'E',  label: 'Insufficient', tone: 'bad' };
};
const seeded = (seed) => {
  let s = seed;
  return () => {
    s = (s * 9301 + 49297) % 233280;
    return s / 233280;
  };
};

// ── Icons (stroke-based, 1.5px) ─────────────────────────────
const Icon = ({ name, size = 14, strokeWidth = 1.6, style }) => {
  const common = {
    width: size,
    height: size,
    viewBox: '0 0 24 24',
    fill: 'none',
    stroke: 'currentColor',
    strokeWidth,
    strokeLinecap: 'round',
    strokeLinejoin: 'round',
    style,
  };
  const paths = {
    dashboard: <><rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/></>,
    mic: <><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M5 11a7 7 0 0 0 14 0"/><path d="M12 18v4"/></>,
    book: <><path d="M4 4h8a4 4 0 0 1 4 4v12H8a4 4 0 0 1-4-4V4Z"/><path d="M4 16a4 4 0 0 1 4-4h8"/></>,
    slots: <><rect x="3" y="4" width="18" height="18" rx="1"/><path d="M3 10h18"/><path d="M8 2v4M16 2v4"/></>,
    users: <><circle cx="9" cy="8" r="4"/><path d="M2 21v-1a5 5 0 0 1 5-5h4a5 5 0 0 1 5 5v1"/><circle cx="17" cy="9" r="3"/><path d="M22 20v-1a4 4 0 0 0-4-4h-1"/></>,
    file: <><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9l-6-6Z"/><path d="M14 3v6h6"/></>,
    chart: <><path d="M3 3v18h18"/><path d="M7 14l4-4 3 3 6-7"/></>,
    settings: <><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 0 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 0 1 0-4h.1A1.7 1.7 0 0 0 4.6 9 1.7 1.7 0 0 0 4.3 7.2l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 0 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 0 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z"/></>,
    logout: <><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/></>,
    plus: <><path d="M12 5v14M5 12h14"/></>,
    arrowRight: <><path d="M5 12h14M13 5l7 7-7 7"/></>,
    arrowLeft: <><path d="M19 12H5M11 5l-7 7 7 7"/></>,
    chevron: <><path d="m9 6 6 6-6 6"/></>,
    check: <><path d="M20 6 9 17l-5-5"/></>,
    close: <><path d="M18 6 6 18M6 6l12 12"/></>,
    search: <><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></>,
    download: <><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/></>,
    filter: <><path d="M22 3H2l8 9.5V19l4 2v-8.5L22 3Z"/></>,
    clock: <><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></>,
    calendar: <><rect x="3" y="4" width="18" height="18" rx="1"/><path d="M3 10h18M8 2v4M16 2v4"/></>,
    eye: <><path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></>,
    star: <><path d="m12 2 3 7h7l-5.5 4.5 2 7.5L12 17l-6.5 4L7.5 13.5 2 9h7l3-7Z"/></>,
    trending: <><path d="m3 17 6-6 4 4 8-8"/><path d="M14 7h7v7"/></>,
    pen: <><path d="m18 2 4 4-14 14H4v-4L18 2Z"/></>,
    play: <><path d="M6 4v16l14-8Z"/></>,
    pause: <><path d="M6 4v16M14 4v16" strokeLinecap="butt"/></>,
    stop: <><rect x="5" y="5" width="14" height="14" rx="1"/></>,
    upload: <><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m17 8-5-5-5 5"/><path d="M12 3v12"/></>,
    tune: <><path d="M4 6h10M4 12h6M4 18h14"/><circle cx="18" cy="6" r="2"/><circle cx="14" cy="12" r="2"/><circle cx="20" cy="18" r="2"/></>,
    sun: <><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></>,
    moon: <><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z"/></>,
    trophy: <><path d="M7 4h10v4a5 5 0 0 1-10 0V4Z"/><path d="M7 7H4a3 3 0 0 0 3 3M17 7h3a3 3 0 0 1-3 3M12 13v4M8 21h8"/></>,
    target: <><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></>,
    alert: <><path d="M12 2 2 20h20L12 2Z"/><path d="M12 9v5M12 17v.01"/></>,
    dots: <><circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/></>,
    grid: <><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></>,
    sparkle: <><path d="M12 3v4M12 17v4M3 12h4M17 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M5.6 18.4l2.8-2.8M15.6 8.4l2.8-2.8"/></>,
    lock: <><rect x="4" y="11" width="16" height="10" rx="1"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></>,
    copy: <><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></>,
    refresh: <><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M21 3v5h-5M3 21v-5h5"/></>,
    bell: <><path d="M6 8a6 6 0 1 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10 21a2 2 0 0 0 4 0"/></>,
    help: <><circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 1 1 3.5 2.3c-1 .4-1 1.2-1 1.7M12 17v.01"/></>,
    type: <><path d="M4 7V4h16v3"/><path d="M9 20h6M12 4v16"/></>,
    user: <><circle cx="12" cy="8" r="4"/><path d="M4 21v-1a6 6 0 0 1 6-6h4a6 6 0 0 1 6 6v1"/></>,
    layers: <><path d="m12 2 10 6-10 6L2 8l10-6Z"/><path d="m2 16 10 6 10-6M2 12l10 6 10-6"/></>,
    home: <><path d="M3 10 12 3l9 7v10a1 1 0 0 1-1 1h-5v-7h-6v7H4a1 1 0 0 1-1-1V10Z"/></>,
    list: <><path d="M8 6h13M8 12h13M8 18h13"/><circle cx="4" cy="6" r="1" fill="currentColor"/><circle cx="4" cy="12" r="1" fill="currentColor"/><circle cx="4" cy="18" r="1" fill="currentColor"/></>,
    trash: <><path d="M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6M10 11v6M14 11v6"/></>,
  };
  return <svg {...common}>{paths[name] || null}</svg>;
};

// ── Mock data ──────────────────────────────────────────────
const DEPTS = [];
const COLLEGES = [];
const TOPICS = [
  'Climate change mitigation strategies',
  'The role of AI in healthcare',
  'Cybersecurity in the IoT era',
  'Renewable energy in rural India',
  'Ethics of generative models',
  'Quantum computing: an introduction',
  'Blockchain beyond cryptocurrency',
  'Mental health in the digital age',
  'Urban mobility and smart cities',
  'Agricultural technology transformation',
];

const CRITERIA = [
  { key: 'content',    label: 'Content Depth',       desc: 'Accuracy, depth, and relevance of the material.' },
  { key: 'structure',  label: 'Structure & Flow',    desc: 'Logical progression and transitions.' },
  { key: 'clarity',    label: 'Clarity of Delivery', desc: 'Articulation, enunciation, and pacing.' },
  { key: 'engagement', label: 'Engagement',          desc: 'Tone variation, vocal energy, audience pull.' },
  { key: 'language',   label: 'Language Precision',  desc: 'Grammar, vocabulary, technical accuracy.' },
  { key: 'confidence', label: 'Confidence',          desc: 'Poise, composure, voice projection.' },
];

// ── Coaching aspects — things faculty can train students on ────
const COACHING_ASPECTS = [
  { key: 'fillers',    label: 'Filler-word reduction',     lever: 'delivery',  icon: 'mic',      hint: 'Replace "um/uh/like" with silent pauses.' },
  { key: 'pace',       label: 'Pace & rhythm',             lever: 'delivery',  icon: 'clock',    hint: 'Target 140–160 wpm with intentional pauses.' },
  { key: 'hedges',     label: 'Hedging language',          lever: 'language',  icon: 'pen',      hint: 'Cut "kind of / sort of / I think"; commit to claims.' },
  { key: 'specificity',label: 'Evidence & specificity',    lever: 'content',   icon: 'target',   hint: 'Attach a number, date, or source to each claim.' },
  { key: 'arc',        label: '3-beat narrative arc',      lever: 'structure', icon: 'trending', hint: 'Problem → Evidence → Implication.' },
  { key: 'projection', label: 'Voice projection',          lever: 'confidence',icon: 'sparkle',  hint: 'Breath from diaphragm; land sentence endings.' },
  { key: 'transitions',label: 'Transition discipline',     lever: 'structure', icon: 'arrowRight', hint: 'Signal every topic shift with a connector.' },
  { key: 'jargon',     label: 'Jargon calibration',        lever: 'language',  icon: 'book',     hint: 'Define any term a non-expert listener would miss.' },
];

// ── Role permissions ────────────────────────────────────
// user       = student; takes assessments
// admin      = faculty; runs a slot they have a passkey for; no slot creation; can't see passkeys
// superadmin = college admin; creates slots, manages users, sees all passkeys, grants analytics per slot
const PERMS = {
  user:       { canCreateSlot: false, canSeePasskeys: false, canManageUsers: false, canGrantAnalytics: false, canSeeAllSlots: false },
  admin:      { canCreateSlot: false, canSeePasskeys: false, canManageUsers: false, canGrantAnalytics: false, canSeeAllSlots: true  },
  superadmin: { canCreateSlot: true,  canSeePasskeys: true,  canManageUsers: true,  canGrantAnalytics: true,  canSeeAllSlots: true  },
};




// ── Empty dataset — all content comes from the API ──────────────────
const MOCK = {
  students: [],
  slots: [],
  heroSlot: null,
  heroStudents: [],
  reports: [],
  me: { id: '', name: 'Student', email: '', role: 'user', regNo: '', dept: '', college: '', avatarSeed: 0, trend: [] },
  myReports: [],
};

Object.assign(window, {
  cls, clamp, fmt, initials, daysAgo, fmtDate, fmtTime, relTime, gradeOf, seeded, parseApiDate,
  Icon, CRITERIA, COACHING_ASPECTS, PERMS, DEPTS, COLLEGES, MOCK,
});

// ── Download helpers (CSV + print-to-PDF) ────────────────
function downloadBlob(filename, blob) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = filename;
  document.body.appendChild(a); a.click();
  setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 0);
}

function toCSV(rows) {
  const esc = (v) => {
    if (v == null) return '';
    const s = String(v);
    return /[,"\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
  };
  return rows.map((r) => r.map(esc).join(',')).join('\n');
}

function downloadAnalyticsCSV(slot, reports, critAvg) {
  if (!window.XLSX) { alert('Excel engine still loading — try again in a moment.'); return; }
  const XL = window.XLSX;
  const wb = XL.utils.book_new();

  // ── Sheet 1: Summary ────────────────────────────────────────────────────────
  const avgScore = reports.length
    ? (reports.reduce((a, r) => a + r.scores.overall, 0) / reports.length).toFixed(2)
    : 0;
  const passCount = reports.filter((r) => r.scores.overall >= 6).length;
  const summaryRows = [
    ['ChiselAssess — Cohort Analytics Export'],
    [],
    ['Slot Title',    slot.title],
    ['Slot ID',       slot.id.toUpperCase()],
    ['Department',    slot.dept    || 'All'],
    ['College',       slot.college || 'All'],
    ['Exported on',   new Date().toLocaleString()],
    [],
    ['COHORT SUMMARY'],
    ['Total submissions',  reports.length],
    ['Average score (/10)', avgScore],
    ['Pass rate (≥6)',      reports.length ? `${Math.round((passCount / reports.length) * 100)}%` : '—'],
    ['Highest score',      reports.length ? Math.max(...reports.map((r) => r.scores.overall)).toFixed(1) : '—'],
    ['Lowest score',       reports.length ? Math.min(...reports.map((r) => r.scores.overall)).toFixed(1) : '—'],
    [],
    ['CRITERION AVERAGES (/100)'],
    ['Criterion', 'Average', 'Grade'],
    ...CRITERIA.map((c, i) => [c.label, critAvg[i], gradeOf(critAvg[i]).g]),
  ];
  const wsSummary = XL.utils.aoa_to_sheet(summaryRows);
  wsSummary['!cols'] = [{ wch: 28 }, { wch: 22 }, { wch: 12 }];
  XL.utils.book_append_sheet(wb, wsSummary, 'Summary');

  // ── Sheet 2: Individual Submissions ────────────────────────────────────────
  const subHeader = [
    'Name', 'Reg No', 'Department', 'College', 'Topic',
    ...CRITERIA.map((c) => `${c.label} (/100)`),
    'Overall (/10)', 'Grade', 'Verdict',
    'Duration (s)', 'Words', 'WPM', 'Fillers', 'Submitted',
  ];
  const subRows = reports.map((r) => [
    r.student.name,
    r.student.regNo,
    r.student.dept,
    r.student.college,
    r.topic,
    ...CRITERIA.map((c) => r.scores[c.key] ?? 0),
    typeof r.scores.overall === 'number' ? r.scores.overall.toFixed(1) : r.scores.overall,
    r.grade?.g || '',
    r.verdict || '',
    r.duration,
    r.words,
    r.wpm,
    r.fillers,
    fmtDate(r.date),
  ]);
  const wsSubmissions = XL.utils.aoa_to_sheet([subHeader, ...subRows]);
  wsSubmissions['!cols'] = [
    { wch: 22 }, { wch: 14 }, { wch: 16 }, { wch: 18 }, { wch: 24 },
    ...CRITERIA.map(() => ({ wch: 18 })),
    { wch: 12 }, { wch: 8 }, { wch: 30 },
    { wch: 12 }, { wch: 8 }, { wch: 8 }, { wch: 8 }, { wch: 16 },
  ];
  // Bold header row
  const range = XL.utils.decode_range(wsSubmissions['!ref'] || 'A1');
  for (let C = range.s.c; C <= range.e.c; C++) {
    const addr = XL.utils.encode_cell({ r: 0, c: C });
    if (wsSubmissions[addr]) wsSubmissions[addr].s = { font: { bold: true } };
  }
  XL.utils.book_append_sheet(wb, wsSubmissions, 'Submissions');

  // ── Sheet 3: Score Distribution ─────────────────────────────────────────────
  const bands = [
    { label: '0 – 3', min: 0, max: 3 },
    { label: '3 – 5', min: 3, max: 5 },
    { label: '5 – 6', min: 5, max: 6 },
    { label: '6 – 7', min: 6, max: 7 },
    { label: '7 – 8', min: 7, max: 8 },
    { label: '8 – 10', min: 8, max: 10.01 },
  ];
  const distRows = [
    ['Score Band (/10)', 'Count', '% of Total', 'Grade Band'],
    ...bands.map((b) => {
      const count = reports.filter((r) => r.scores.overall >= b.min && r.scores.overall < b.max).length;
      return [
        b.label,
        count,
        reports.length ? `${Math.round((count / reports.length) * 100)}%` : '0%',
        gradeOf((b.min + b.max) / 2).g,
      ];
    }),
  ];
  const wsDist = XL.utils.aoa_to_sheet(distRows);
  wsDist['!cols'] = [{ wch: 16 }, { wch: 10 }, { wch: 14 }, { wch: 12 }];
  XL.utils.book_append_sheet(wb, wsDist, 'Distribution');

  XL.writeFile(wb, `ChiselAssess_${slot.id}_Analytics_${new Date().toISOString().slice(0, 10)}.xlsx`);
}

// ── Download report as PDF (direct jsPDF — no html2canvas oklch issues) ──────
// Core builder — returns a jsPDF instance fully populated (does NOT save/download).
// Called by both downloadReportAsPDF and _generateReportPDFBytes.
async function _buildReportPDFDoc(report) {
  if (!window.jspdf) { alert('PDF engine loading — try again in a moment.'); return; }
  const { jsPDF } = window.jspdf;
  const pdf = new jsPDF({ unit: 'pt', format: 'a4' });
  const pw = pdf.internal.pageSize.getWidth();
  const ph = pdf.internal.pageSize.getHeight();
  const ml = 44, mr = 44;
  const cw = pw - ml - mr;
  let y = 44;

  const NL = (n = 1) => { y += n; };
  const addPage = () => { pdf.addPage(); y = 44; };
  const checkY = (need = 30) => { if (y + need > ph - 40) addPage(); };

  // ── Header — branded with logo ──────────────────────────────────────────────
  pdf.setFillColor(30, 45, 60);
  pdf.rect(0, 0, pw, 90, 'F');
  pdf.setFillColor(169, 50, 38);
  pdf.rect(0, 90, pw, 3, 'F');
  // Add logo image
  try {
    pdf.addImage("data:image/png;base64,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", 'PNG', ml, 8, 34, 54);
  } catch(e) {}
  // Brand line
  pdf.setFont('helvetica', 'bold'); pdf.setFontSize(8); pdf.setTextColor(147, 197, 253);
  pdf.text('Learning Solutions @Chisel', ml + 40, 20);
  pdf.setFont('helvetica', 'bold'); pdf.setFontSize(21); pdf.setTextColor(255, 255, 255);
  pdf.text('ChiselAssess', ml + 40, 42);
  pdf.setFont('helvetica', 'normal'); pdf.setFontSize(8.5); pdf.setTextColor(147, 197, 253);
  pdf.text('Oral Presentation Evaluation Report', ml + 40, 56);
  pdf.setFont('helvetica', 'normal'); pdf.setFontSize(7.5); pdf.setTextColor(148, 163, 184);
  pdf.text(new Date().toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }), pw - mr, 24, { align: 'right' });
  pdf.setFillColor(100, 149, 200); pdf.setFontSize(7);
  pdf.setTextColor(100, 149, 200);
  pdf.text('www.ls-chisel.com', pw - mr, 36, { align: 'right' });
  y = 108;

  // ── Student info ────────────────────────────────────────────────────────────
  pdf.setFont('helvetica', 'bold'); pdf.setFontSize(16); pdf.setTextColor(30, 30, 30);
  const name = report.student?.name || report.name || 'Student';
  pdf.text(name, ml, y); y += 18;
  pdf.setFont('helvetica', 'normal'); pdf.setFontSize(10); pdf.setTextColor(100, 100, 100);
  const meta = [
    report.student?.regNo || report.reg || '',
    report.student?.dept  || report.dept || '',
    report.student?.college || report.college || '',
    report.topic || '',
  ].filter(Boolean).join('  ·  ');
  pdf.text(meta, ml, y); y += 22;

  // ── Score donut + grade band ────────────────────────────────────────────────
  const overall = report.scores?.overall || 0;
  const grade   = report.grade?.g || gradeOf(overall).g;
  const scoreColor = overall >= 8 ? [34,130,84] : overall >= 6 ? [30,86,160] : overall >= 4 ? [190,120,30] : [200,60,60];

  // Donut chart (jsPDF draws arcs via lines — approximate with segments)
  const dcx = ml + 40, dcy = y + 32, dcR = 28, dcThick = 9;
  const scoreAngle = (overall / 10) * 2 * Math.PI;
  // Background ring (gray)
  pdf.setDrawColor(230,233,240); pdf.setLineWidth(dcThick);
  for (let i=0; i<200; i++) {
    const bgA = (i/200)*2*Math.PI - Math.PI/2;
    const bgA2 = ((i+1)/200)*2*Math.PI - Math.PI/2;
    const x1=dcx+dcR*Math.cos(bgA), y1=dcy+dcR*Math.sin(bgA);
    const x2=dcx+dcR*Math.cos(bgA2), y2=dcy+dcR*Math.sin(bgA2);
    pdf.line(x1,y1,x2,y2);
  }
  // Score arc (colored)
  const segs = Math.max(1, Math.round((overall/10)*200));
  pdf.setDrawColor(...scoreColor); pdf.setLineWidth(dcThick);
  for (let i=0; i<segs; i++) {
    const fgA = (i/200)*2*Math.PI - Math.PI/2;
    const fgA2 = ((i+1)/200)*2*Math.PI - Math.PI/2;
    const x1f=dcx+dcR*Math.cos(fgA), y1f=dcy+dcR*Math.sin(fgA);
    const x2f=dcx+dcR*Math.cos(fgA2), y2f=dcy+dcR*Math.sin(fgA2);
    pdf.line(x1f,y1f,x2f,y2f);
  }
  pdf.setLineWidth(0.5); // reset
  // Score text inside donut
  pdf.setFont('helvetica','bold'); pdf.setFontSize(16); pdf.setTextColor(...scoreColor);
  pdf.text(String(overall), dcx, dcy+4, {align:'center'});
  pdf.setFont('helvetica','normal'); pdf.setFontSize(6.5); pdf.setTextColor(120,120,140);
  pdf.text('out of 10', dcx, dcy+13, {align:'center'});

  // Grade badge next to donut
  const gradeColors = {'A+':[22,101,52],'A':[30,120,60],'B+':[0,100,160],'B':[30,80,130],'C+':[146,64,14],'C':[160,80,0],'D':[180,60,0],'E':[200,40,40]};
  const gc = gradeColors[grade] || [100,100,100];
  pdf.setFillColor(...gc); pdf.roundedRect(ml+82, y+8, 55, 48, 6, 6, 'F');
  pdf.setFont('helvetica','bold'); pdf.setFontSize(28); pdf.setTextColor(255,255,255);
  pdf.text(grade, ml+109, y+36, {align:'center'});
  pdf.setFont('helvetica','normal'); pdf.setFontSize(7); pdf.setTextColor(200,220,255);
  pdf.text('GRADE', ml+109, y+48, {align:'center'});

  // Score % and Verdict pill on the right
  const pct = Math.round(overall*10);
  const verdictLabel = overall>=9?'Exemplary':overall>=8?'Excellent':overall>=7?'Very Good':overall>=6?'Good':overall>=5?'Satisfactory':overall>=4?'Needs Work':overall>=3?'Weak':'Insufficient';
  pdf.setFont('helvetica','normal'); pdf.setFontSize(9); pdf.setTextColor(80,80,100);
  pdf.text(`${pct}%`, ml+150, y+26);
  pdf.setFont('helvetica','bold'); pdf.setFontSize(9); pdf.setTextColor(...scoreColor);
  pdf.text(verdictLabel, ml+150, y+40);

  y += 76;

  const ev = report._raw?.evaluation || report._raw || {};
  const rawCriteria = ev.criteria || {};

  // ── Build unified criteria list (supports both old 4-key and new 6-key formats)
  const CRIT_DEFS = [
    { keys:['content','content_depth'],  label:'Content Depth',       color:[169,50,38]  },
    { keys:['structure','structure_flow'],label:'Structure & Flow',    color:[30,86,160]  },
    { keys:['clarity','speech_clarity'],  label:'Clarity of Delivery', color:[34,130,84]  },
    { keys:['engagement'],               label:'Engagement',           color:[120,50,160] },
    { keys:['language','grammatical_efficiency','word_choice'], label:'Language Precision', color:[190,120,30] },
    { keys:['confidence','thought_clarity'], label:'Confidence',       color:[30,130,130] },
  ];
  // Also handle old 4-key format with individual keys
  const OLD4 = {
    speech_clarity:'Speech Clarity & Fluency', grammatical_efficiency:'Grammatical Efficiency',
    word_choice:'Choice of Words', thought_clarity:'Clarity of Expression & Structure',
  };
  const hasOld4 = Object.keys(rawCriteria).some(k => k in OLD4);
  
  let critRows = [];
  if (hasOld4) {
    // Old format: 4 keys, each /2.5
    Object.entries(OLD4).forEach(([key, label]) => {
      const c = rawCriteria[key] || {};
      if (!c.score && !c.insight) return;
      const score100 = Math.round((parseFloat(c.score)||0) / 2.5 * 100);
      critRows.push({ label, score100, insight: c.insight||'', color:[60,80,140] });
    });
  } else {
    // New format: 6 keys, each 0-100
    CRIT_DEFS.forEach(({ keys, label, color }) => {
      const key = keys.find(k => rawCriteria[k]) || keys[0];
      const c = rawCriteria[key] || {};
      const scoreNorm = report.scores;
      const score100 = c.score != null ? Math.round(parseFloat(c.score))
                     : (scoreNorm?.[key] != null ? Math.round(scoreNorm[key]) : null);
      if (score100 == null && !c.insight) return;
      critRows.push({ label, score100: score100 ?? 0, insight: c.insight||'', color });
    });
  }

  // ── Criteria Performance section (left: bars, right: spider chart) ──────────
  checkY(160);
  pdf.setFont('helvetica','bold'); pdf.setFontSize(11); pdf.setTextColor(30,30,30);
  pdf.text('Criteria Performance', ml, y); y += 14;

  const barAreaW = cw * 0.55;
  const chartAreaX = ml + barAreaW + 16;
  const chartAreaW = cw - barAreaW - 16;
  const barStartY = y;

  // Left: criteria bars — 30px row height to prevent overlap
  const ROW_H = 30;
  critRows.forEach((cr, i) => {
    const barY = barStartY + i * ROW_H;
    const barW  = barAreaW - 50;
    const filled = Math.max(0, Math.min(barW, (cr.score100 / 100) * barW));
    const barColor = cr.score100 >= 70 ? [34,130,84] : cr.score100 >= 50 ? [190,120,30] : [200,60,60];
    // Label (bold, truncated to fit)
    pdf.setFont('helvetica','bold'); pdf.setFontSize(8); pdf.setTextColor(30,30,30);
    pdf.text(cr.label.slice(0,24), ml, barY + 8);
    // Background bar
    pdf.setFillColor(228,232,240); pdf.roundedRect(ml, barY + 11, barW, 6, 2, 2, 'F');
    // Filled bar with gradient effect (2 rects)
    if (filled > 2) {
      pdf.setFillColor(...barColor); pdf.roundedRect(ml, barY + 11, filled, 6, 2, 2, 'F');
    }
    // Score badge at end of bar
    pdf.setFillColor(...barColor); pdf.roundedRect(ml + filled + 3, barY + 10, 24, 8, 2, 2, 'F');
    pdf.setFont('helvetica','bold'); pdf.setFontSize(7); pdf.setTextColor(255,255,255);
    pdf.text(`${cr.score100}%`, ml + filled + 15, barY + 16, {align:'center'});
    // Insight text below bar — wrap to 2 lines max, font size 7
    if (cr.insight) {
      pdf.setFont('helvetica','normal'); pdf.setFontSize(6.5); pdf.setTextColor(90,95,115);
      const insightLines = pdf.splitTextToSize(cr.insight.slice(0,120), barAreaW - 10);
      insightLines.slice(0,2).forEach((ln, j) => { pdf.text(ln, ml, barY + 22 + j * 7); });
    }
  });

  // Right: spider/radar chart — positioned to match bar area height
  const nAxes = critRows.length || 6;
  const cx = chartAreaX + chartAreaW/2;
  const cy = barStartY + (nAxes * ROW_H) / 2;
  const R  = Math.min(chartAreaW/2 - 10, (nAxes * ROW_H)/2 - 12);

  const getPoint = (angle, radius) => ({
    x: cx + radius * Math.cos(angle - Math.PI/2),
    y: cy + radius * Math.sin(angle - Math.PI/2),
  });

  // Grid rings
  [0.25, 0.5, 0.75, 1.0].forEach(frac => {
    const ringR = R * frac;
    pdf.setDrawColor(210,215,225); pdf.setLineWidth(0.4);
    for (let i=0; i<nAxes; i++) {
      const a1 = (i/nAxes)*2*Math.PI, a2 = ((i+1)/nAxes)*2*Math.PI;
      const p1 = getPoint(a1, ringR), p2 = getPoint(a2, ringR);
      pdf.line(p1.x, p1.y, p2.x, p2.y);
    }
  });
  // Axis lines
  pdf.setDrawColor(180,185,200); pdf.setLineWidth(0.5);
  for (let i=0; i<nAxes; i++) {
    const p = getPoint((i/nAxes)*2*Math.PI, R);
    pdf.line(cx, cy, p.x, p.y);
  }
  // Score polygon
  if (critRows.length >= 3) {
    const pts = critRows.map((cr,i) => getPoint((i/nAxes)*2*Math.PI, R*(cr.score100/100)));
    pdf.setFillColor(169,50,38,0.2);
    pdf.setDrawColor(169,50,38); pdf.setLineWidth(1.2);
    // Fill polygon
    const pathStr = pts.map((p,i) => `${i===0?'moveTo':'lineTo'} ${p.x} ${p.y}`).join(' ');
    // Draw lines connecting points
    for (let i=0; i<pts.length; i++) {
      const next = pts[(i+1) % pts.length];
      pdf.line(pts[i].x, pts[i].y, next.x, next.y);
    }
    // Dots at each vertex
    pts.forEach((p, i) => {
      const [r2,g2,b2] = critRows[i].color;
      pdf.setFillColor(r2,g2,b2); pdf.circle(p.x, p.y, 2, 'F');
      // Score label
      const lp = getPoint((i/nAxes)*2*Math.PI, R*1.15);
      pdf.setFont('helvetica','bold'); pdf.setFontSize(7); pdf.setTextColor(r2,g2,b2);
      pdf.text(String(critRows[i].score100), lp.x, lp.y, {align:'center'});
    });
  }
  // Axis labels on outside
  critRows.forEach((cr,i) => {
    const lp = getPoint((i/nAxes)*2*Math.PI, R+12);
    pdf.setFont('helvetica','normal'); pdf.setFontSize(6); pdf.setTextColor(80,80,100);
    const shortLabel = cr.label.split(' ')[0]; // first word only
    pdf.text(shortLabel, lp.x, lp.y, {align:'center'});
  });

  y = barStartY + critRows.length * ROW_H + 14;

  // ── Evaluation Criteria detail table ────────────────────────────────────────
  checkY(40);
  pdf.setFont('helvetica','bold'); pdf.setFontSize(10); pdf.setTextColor(30,30,30);
  pdf.text('Evaluation Criteria', ml, y); y += 12;
  pdf.setFillColor(30,58,95); pdf.rect(ml, y, cw, 15, 'F');
  pdf.setFont('helvetica','bold'); pdf.setFontSize(8); pdf.setTextColor(255,255,255);
  pdf.text('Criterion', ml+5, y+10);
  pdf.text('Insight', ml+cw*0.38, y+10);
  pdf.text('Score', ml+cw-45, y+10);
  y += 15;
  critRows.forEach((cr, i) => {
    checkY(22);
    const rowH = 22;
    if (i%2===0) { pdf.setFillColor(248,250,252); pdf.rect(ml, y, cw, rowH, 'F'); }
    const [r2,g2,b2] = cr.color;
    const barColor = cr.score100>=70?[34,130,84]:cr.score100>=50?[190,120,30]:[200,60,60];
    pdf.setFont('helvetica','bold'); pdf.setFontSize(8); pdf.setTextColor(30,30,30);
    pdf.text(cr.label, ml+5, y+14);
    pdf.setFont('helvetica','normal'); pdf.setFontSize(7.5); pdf.setTextColor(70,70,90);
    const iLines = pdf.splitTextToSize(cr.insight||'', cw*0.52);
    pdf.text(iLines[0]||'', ml+cw*0.38, y+10);
    if (iLines[1]) pdf.text(iLines[1], ml+cw*0.38, y+17);
    // Score
    const scoreLabel = hasOld4 ? `${cr.score100} / 2.5`.replace(/\d+/,(n)=>Math.round(n/40*2.5*10)/10) : `${cr.score100}%`;
    pdf.setFont('helvetica','bold'); pdf.setFontSize(9); pdf.setTextColor(...barColor);
    pdf.text(hasOld4 ? `— / 2.5` : `${cr.score100}`, ml+cw-42, y+14, {align:'center'});
    pdf.setDrawColor(229,231,235); pdf.line(ml, y+rowH, ml+cw, y+rowH);
    y += rowH;
  });
  y += 10;

  // ── jsPDF text sanitiser — strip characters Helvetica can't render ───────────
  // Emoji and non-Latin glyphs silently corrupt jsPDF's character advance widths,
  // producing the wide-spaced "R  e  w  r  i  t  e" effect seen in the PDF.
  const _pdfSafe = (str) => {
    if (!str) return '';
    return String(str)
      // Remove everything above Latin Extended-B except safe typographic chars
      .replace(/[^\x09\x0A\x0D\x20-\x7E\xA0-\u024F\u2013\u2014\u2018\u2019\u201C\u201D\u2022]/g, '')
      // Strip the !' / !' artifact (corrupted emoji, all quote variants)
      .replace(/!['\u2018\u2019]\s*/g, '')
      // Strip %³ artifact (corrupted 📉 emoji)
      .replace(/^%\u00B3\s*/gm, '')
      // Collapse multiple spaces
      .replace(/  +/g, ' ')
      .trim();
  };
  const CRIT_LABELS_PDF = {
    content:'Content Depth',structure:'Structure & Flow',clarity:'Clarity of Delivery',
    engagement:'Engagement',language:'Language Precision',confidence:'Confidence',
    speech_clarity:'Speech Clarity',grammatical_efficiency:'Grammar',
    word_choice:'Word Choice',thought_clarity:'Thought Clarity',
  };
  let detFeedback = ev.detailed_feedback || report._raw?.detailed_feedback || '';
  if (!detFeedback) {
    const crits = ev.criteria || report._raw?.criteria || {};
    detFeedback = Object.entries(crits)
      .filter(([,c]) => typeof c==='object' && c?.insight)
      .sort(([,a],[,b]) => (parseFloat(a?.score)||100)-(parseFloat(b?.score)||100))
      .map(([k,c]) => `• ${CRIT_LABELS_PDF[k]||k} (${Math.round(parseFloat(c.score)||0)}/100): ${_pdfSafe(c.insight)}`)
      .join('\n');
  } else {
    detFeedback = _pdfSafe(detFeedback);
  }
  let commSugg = ev.communication_suggestions || report._raw?.communication_suggestions || '';
  if (!commSugg) {
    const commDrills = ev.coaching_drills || report._raw?.coaching_drills || [];
    const parts = commDrills.slice(0,4).map(d=>{
      const t=_pdfSafe(d.title||d.name||''); const b=_pdfSafe(d.description||d.desc||'');
      return b ? (t?`• ${t}: ${b}`:`• ${b}`) : null;
    }).filter(Boolean);
    commSugg = parts.join('\n');
  } else {
    commSugg = _pdfSafe(commSugg);
  }
  const areasForImprovement = _pdfSafe(ev.improvements || report._raw?.improvements || (Array.isArray(report.weaknesses) ? report.weaknesses.map(_pdfSafe).join('\n• ').replace(/^/, '• ') : (report.weaknesses||'')));

  // ── Helper: blue section heading ─────────────────────────────────────────────
  const sectionHead = (title) => {
    checkY(30);
    pdf.setFont('helvetica','bold'); pdf.setFontSize(11); pdf.setTextColor(33,114,196);
    pdf.text(title, ml, y); y += 14;
    pdf.setFont('helvetica','normal'); pdf.setFontSize(9.5); pdf.setTextColor(55,65,81);
  };
  const sectionText = (text, maxChars=1200) => {
    const stLines = pdf.splitTextToSize(String(text).slice(0,maxChars), cw);
    stLines.forEach((ln) => { checkY(14); pdf.text(ln, ml, y); y += 13; });
    y += 8;
  };

  // ── Verdict ─────────────────────────────────────────────────────────────────
  if (ev.verdict || report.verdict) {
    sectionHead('Verdict');
    sectionText(_pdfSafe(ev.verdict || report.verdict));
  }

  // ── Strengths & Areas side by side ──────────────────────────────────────────
  const strengthText = _pdfSafe(Array.isArray(report.strengths) ? report.strengths.map(_pdfSafe).join('\n• ').replace(/^/, '• ') : (report.strengths||ev.strengths||''));
  const halfW = (cw - 10) / 2;
  if (strengthText || areasForImprovement) {
    checkY(60);
    const boxY = y;
    // Strengths box (green tint)
    pdf.setFillColor(240,253,244); pdf.roundedRect(ml, boxY, halfW, 8, 2, 2, 'F');
    pdf.setFont('helvetica','bold'); pdf.setFontSize(10); pdf.setTextColor(22,101,52);
    pdf.text('+ Strengths', ml+6, boxY+6);
    pdf.setFont('helvetica','normal'); pdf.setFontSize(8.5); pdf.setTextColor(55,65,81);
    let sy = boxY + 18;
    const strLines = pdf.splitTextToSize(strengthText||'—', halfW-10);
    strLines.forEach(ln => { if(sy<boxY+120){ pdf.text(ln, ml+6, sy); sy+=12; } });
    // Areas box (amber tint)
    const areaX = ml + halfW + 10;
    pdf.setFillColor(255,251,235); pdf.roundedRect(areaX, boxY, halfW, 8, 2, 2, 'F');
    pdf.setFont('helvetica','bold'); pdf.setFontSize(10); pdf.setTextColor(146,64,14);
    pdf.text('> To improve', areaX+6, boxY+6);
    pdf.setFont('helvetica','normal'); pdf.setFontSize(8.5); pdf.setTextColor(55,65,81);
    let ay = boxY + 18;
    const areaLines = pdf.splitTextToSize(areasForImprovement||'—', halfW-10);
    areaLines.forEach(ln => { if(ay<boxY+120){ pdf.text(ln, areaX+6, ay); ay+=12; } });
    y = Math.max(sy, ay) + 14;
  }

  // ── Detailed Feedback ────────────────────────────────────────────────────────
  if (detFeedback) {
    sectionHead('Detailed Feedback');
    sectionText(detFeedback, 1500);
  }

  // ── Communication Suggestions ────────────────────────────────────────────────
  if (commSugg) {
    sectionHead('Communication Suggestions');
    sectionText(commSugg, 1000);
  }

  // ── Filler words ─────────────────────────────────────────────────────────────
  const fw = ev.filler_words || report._raw?.filler_words || {};
  if ((fw.total_count||0) > 0 || (fw.detected||[]).length > 0) {
    checkY(50);
    sectionHead('Filler Word Analysis');
    pdf.setFont('helvetica','normal'); pdf.setFontSize(9.5); pdf.setTextColor(55,65,81);
    pdf.text(`Total: ${fw.total_count||0}  ·  Severity: ${fw.severity||'—'}`, ml, y); y += 13;
    const fillerList = (fw.detected||[]).map(f=>`"${f.word}" ×${f.count}`).join(', ');
    if (fillerList) { const fLines = pdf.splitTextToSize(fillerList, cw); fLines.forEach(ln=>{ pdf.text(ln,ml,y); y+=13; }); }
    y += 8;
  }

  // ── Voice Telemetry ──────────────────────────────────────────────────────────
  const dur = report.duration || ev.duration_seconds || 0;
  const wpm = report.wpm || ev.wpm || 0;
  const words = report.words || ev.word_count || 0;
  const fillers = report.fillers || fw.total_count || 0;
  const pauseRate = report.pauseRate || ev.pause_rate || 0;
  if (dur || wpm || words) {
    checkY(55);
    pdf.setFont('helvetica','bold'); pdf.setFontSize(11); pdf.setTextColor(33,114,196);
    pdf.text('Voice Telemetry', ml, y); y += 14;
    const tels = [
      { label:'Duration', value: dur ? `${Math.floor(dur/60)}:${String(Math.round(dur%60)).padStart(2,'0')}` : '—', sub:'mm:ss' },
      { label:'Words', value: String(words||'—'), sub:'total' },
      { label:'Pace', value: wpm ? `${Math.round(wpm)}` : '—', sub:'wpm' },
      { label:'Fillers', value: String(fillers||0), sub:'count' },
      { label:'Pause rate', value: pauseRate ? pauseRate.toFixed(2) : '0', sub:'per min' },
    ];
    const tW = cw / tels.length;
    tels.forEach(({label,value,sub},i) => {
      const tx = ml + i*tW;
      pdf.setFillColor(248,250,252); pdf.roundedRect(tx, y, tW-4, 38, 3, 3, 'F');
      pdf.setFont('helvetica','bold'); pdf.setFontSize(16); pdf.setTextColor(30,45,60);
      pdf.text(value, tx + tW/2 - 2, y+20, {align:'center'});
      pdf.setFont('helvetica','normal'); pdf.setFontSize(7); pdf.setTextColor(100,110,130);
      pdf.text(label.toUpperCase(), tx + tW/2 - 2, y+30, {align:'center'});
      pdf.text(sub, tx + tW/2 - 2, y+37, {align:'center'});
    });
    y += 50;
  }

  // ── Suggested Rewrites (annotated_excerpts) ──────────────────────────────────
  const excerpts = ev.annotated_excerpts || report._raw?.annotated_excerpts || report.annotated_excerpts || [];
  if (excerpts.length > 0) {
    checkY(60);
    const nextGrade = gradeOf(Math.min(10, (report.scores?.overall||0) + 1.5));
    pdf.setFont('helvetica','bold'); pdf.setFontSize(12); pdf.setTextColor(30,30,30);
    pdf.text(`How to take this from ${grade} to ${nextGrade.g}: Suggested Rewrites`, ml, y); y += 16;
    pdf.setFont('helvetica','normal'); pdf.setFontSize(8.5); pdf.setTextColor(100,100,120);
    pdf.text('Three changes, applied to the actual transcript, with the projected score lift each would produce.', ml, y); y += 16;
    excerpts.slice(0,3).forEach((ex) => {
      checkY(70);
      const ts = ex.timestamp || ex.time || '';
      const phase = _pdfSafe(ex.phase || ex.label || 'Opening');
      const orig  = _pdfSafe(ex.original || ex.text || '');
      const rewrite = _pdfSafe(ex.rewrite || ex.suggested || '');
      const why   = _pdfSafe(ex.why || ex.explanation || '');
      // Phase header
      pdf.setFillColor(253,245,230); pdf.roundedRect(ml, y, 70, 14, 3, 3, 'F');
      pdf.setFont('helvetica','bold'); pdf.setFontSize(9); pdf.setTextColor(146,64,14);
      pdf.text(phase, ml+4, y+9.5);
      if (ts) { pdf.setFont('helvetica','normal'); pdf.setFontSize(7.5); pdf.setTextColor(170,130,80); pdf.text(ts, ml+72, y+9.5); }
      y += 18;
      // Original
      if (orig) {
        pdf.setFont('helvetica','normal'); pdf.setFontSize(7.5); pdf.setTextColor(120,120,120);
        pdf.text('ORIGINAL', ml, y); y += 10;
        pdf.setFont('helvetica','italic'); pdf.setFontSize(8.5); pdf.setTextColor(50,50,50);
        const origLines = pdf.splitTextToSize(`"${orig}"`, cw);
        origLines.slice(0,3).forEach(ln=>{ pdf.text(ln,ml,y); y+=11; });
      }
      // Rewrite
      if (rewrite) {
        y += 4;
        pdf.setFillColor(240,253,244); pdf.roundedRect(ml, y-2, cw, 16, 2, 2, 'F');
        pdf.setFont('helvetica','bold'); pdf.setFontSize(8.5); pdf.setTextColor(22,101,52);
        pdf.text('-> Rewrite:', ml+4, y+8);
        pdf.setFont('helvetica','normal'); pdf.setFontSize(8.5); pdf.setTextColor(30,30,30);
        const rlines = pdf.splitTextToSize(rewrite, cw-55);
        pdf.text(rlines[0]||'', ml+52, y+8); y += 20;
        if (rlines.length > 1) { rlines.slice(1,3).forEach(ln=>{ pdf.text(ln,ml+4,y); y+=11; }); }
      }
      // Why
      if (why) {
        pdf.setFont('helvetica','italic'); pdf.setFontSize(7.5); pdf.setTextColor(120,120,120);
        const wLines = pdf.splitTextToSize(`Why: ${why}`, cw);
        wLines.slice(0,2).forEach(ln=>{ pdf.text(ln,ml,y); y+=10; });
      }
      pdf.setDrawColor(229,231,235); pdf.line(ml, y+2, ml+cw, y+2); y += 12;
    });
    y += 4;
  }

  // ── Coaching Drills ──────────────────────────────────────────────────────────
  const drills = ev.coaching_drills || report._raw?.coaching_drills || [];
  if (drills.length > 0) {
    checkY(50);
    pdf.setFont('helvetica','bold'); pdf.setFontSize(11); pdf.setTextColor(33,114,196);
    pdf.text('Coaching Drills', ml, y); y += 16;
    drills.slice(0,3).forEach((d,i) => {
      checkY(40);
      const t = d.title || d.name || `Drill ${i+1}`;
      const b = d.description || d.desc || '';
      pdf.setFillColor(30,58,95); pdf.roundedRect(ml, y, 20, 20, 3, 3, 'F');
      pdf.setFont('helvetica','bold'); pdf.setFontSize(11); pdf.setTextColor(255,255,255);
      pdf.text(String(i+1).padStart(2,'0'), ml+10, y+13, {align:'center'});
      pdf.setFont('helvetica','bold'); pdf.setFontSize(9.5); pdf.setTextColor(30,30,30);
      pdf.text(t, ml+28, y+8);
      pdf.setFont('helvetica','normal'); pdf.setFontSize(8.5); pdf.setTextColor(60,70,80);
      const bLines = pdf.splitTextToSize(b, cw-32);
      bLines.slice(0,3).forEach((ln,j)=>{ pdf.text(ln, ml+28, y+18+j*11); });
      y += Math.max(28, 18 + Math.min(3,bLines.length)*11) + 6;
    });
    y += 4;
  }

  // ── Full Transcript ────────────────────────────────────────────────────────────
  const transcript = _pdfSafe((report.transcript || ev.transcript || '').trim());
  if (transcript) {
    // Start transcript on a fresh page for readability
    pdf.addPage(); y = 44;

    // Section header bar
    pdf.setFillColor(30, 58, 95);
    pdf.rect(ml, y, cw, 22, 'F');
    pdf.setFont('helvetica', 'bold'); pdf.setFontSize(11); pdf.setTextColor(255, 255, 255);
    pdf.text('Full Transcript', ml + 8, y + 14);
    const wordCount = transcript.split(/\s+/).filter(Boolean).length;
    pdf.setFont('helvetica', 'normal'); pdf.setFontSize(9); pdf.setTextColor(180, 200, 220);
    pdf.text(`${wordCount.toLocaleString()} words`, pw - mr, y + 14, { align: 'right' });
    y += 30;

    // Meta line
    pdf.setFont('helvetica', 'italic'); pdf.setFontSize(9); pdf.setTextColor(120, 120, 120);
    const metaParts = [
      name,
      report.student?.regNo || '',
      report.topic || '',
      new Date(report.date || Date.now()).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }),
    ].filter(Boolean);
    pdf.text(metaParts.join('  ·  '), ml, y); y += 16;

    // Divider
    pdf.setDrawColor(229, 231, 235); pdf.line(ml, y, ml + cw, y); y += 12;

    // Body — split into paragraphs, then wrap each
    pdf.setFont('helvetica', 'normal'); pdf.setFontSize(10); pdf.setTextColor(40, 40, 40);
    const LINE_H = 14;
    const rawParas = transcript.includes('\n')
      ? transcript.split(/\n{2,}/).filter(Boolean)
      : (() => {
          const transcriptWords = transcript.split(' ');
          const chunks = [];
          for (let i = 0; i < transcriptWords.length; i += 80) chunks.push(transcriptWords.slice(i, i + 80).join(' '));
          return chunks;
        })();

    rawParas.forEach((para) => {
      const lines = pdf.splitTextToSize(para.trim(), cw);
      lines.forEach((ln) => {
        checkY(LINE_H + 4);
        pdf.text(ln, ml, y); y += LINE_H;
      });
      y += 6; // paragraph gap
    });

    y += 8;
  }

  // ── 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', 'normal'); pdf.setFontSize(8); pdf.setTextColor(150, 150, 150);
    pdf.setFont('helvetica', 'bold'); pdf.setFontSize(7); pdf.text('Learning Solutions @Chisel  ·  ChiselAssess', ml, ph - 16);
    pdf.text(`Page ${i} of ${totalPages}`, pw - mr, ph - 16, { align: 'right' });
  }

  const safeName = name.replace(/\s+/g, '_');
  return pdf;
}

async function downloadReportAsPDF(report) {
  if (!window.jspdf) { alert('PDF engine loading — try again in a moment.'); return; }
  const pdf      = await _buildReportPDFDoc(report);
  const name     = report.student?.name || report.name || 'Student';
  const safeName = name.replace(/\s+/g, '_');
  pdf.save(`ChiselAssess_${safeName}_Report.pdf`);
}

// ── Return PDF as ArrayBuffer (for ZIP inclusion) — identical to individual download ──
async function _generateReportPDFBytes(report) {
  if (!window.jspdf) return null;
  try {
    const pdf = await _buildReportPDFDoc(report);
    return pdf.output('arraybuffer');
  } catch (e) {
    console.warn('[PDF bytes] error:', e);
    return null;
  }
}
window._generateReportPDFBytes = _generateReportPDFBytes;

// ── Safe CSS token map — every oklch/color-mix token replaced with plain hex ──
const _PDF_SAFE_CSS = `
  *, *::before, *::after {
    --ok: #22c55e !important;          --ok-wash: #f0fdf4 !important;
    --warn: #f59e0b !important;        --warn-wash: #fffbeb !important;
    --bad: #ef4444 !important;         --bad-wash: #fef2f2 !important;
    --info: #3b82f6 !important;
    --accent: #a93226 !important;      --accent-wash: #f5e8e7 !important;
    --accent-2: #8b261e !important;
    --ink: #111827 !important;         --ink-2: #1f2937 !important;
    --ink-3: #374151 !important;       --ink-4: #6b7280 !important;
    --ink-5: #9ca3af !important;
    --card: #ffffff !important;        --card-2: #f8f8f8 !important;
    --paper: #ffffff !important;       --paper-2: #f9fafb !important;
    --paper-3: #f3f4f6 !important;     --paper-4: #e5e7eb !important;
    --rule: #e5e7eb !important;        --rule-2: #d1d5db !important;
    --viz-1: #dc2626 !important;       --viz-2: #2563eb !important;
    --viz-3: #16a34a !important;       --viz-4: #d97706 !important;
    --viz-5: #7c3aed !important;       --viz-6: #0891b2 !important;
    --crit-content: #c2410c !important;    --crit-content-w: #fff7ed !important;
    --crit-structure: #1d4ed8 !important;  --crit-structure-w: #eff6ff !important;
    --crit-delivery: #15803d !important;   --crit-delivery-w: #f0fdf4 !important;
    --crit-voice: #6d28d9 !important;      --crit-voice-w: #f5f3ff !important;
    --crit-engagement: #b45309 !important; --crit-engagement-w: #fffbeb !important;
    --crit-visual: #0e7490 !important;     --crit-visual-w: #ecfeff !important;
    animation: none !important;
    transition: none !important;
  }
  tbody tr:hover, .report-row:hover { background: #f3f4f6 !important; }
  .topbar { background: #f9fafb !important; backdrop-filter: none !important; }
  .analytics-card { border-top-color: #a93226 !important; }
  .btn-accent:hover { background: #8b261e !important; box-shadow: none !important; }
`;

// ── Strip oklch/oklab/color-mix from a CSS text string ────────────────────────
// Uses a temp element to let the browser resolve the color to rgb(), then
// substitutes it. Falls back to a neutral grey if the browser rejects it.
function _sanitiseCssText(css) {
  if (!/oklch|oklab|color-mix/i.test(css)) return css;
  const tmp = document.createElement('div');
  document.body.appendChild(tmp);
  const resolved = css.replace(
    /(oklch|oklab|color-mix)\([^)]*\)/gi,
    (match) => {
      // Try as background-color first, then as color
      for (const prop of ['backgroundColor', 'color', 'borderColor']) {
        tmp.style[prop] = '';
        tmp.style[prop] = match;
        const val = getComputedStyle(tmp)[prop];
        if (val && val !== 'rgba(0, 0, 0, 0)' && val !== 'rgb(0, 0, 0)') return val;
      }
      return '#9ca3af'; // fallback neutral grey
    }
  );
  document.body.removeChild(tmp);
  return resolved;
}

// ── Walk every element and bake computed colors into inline styles ─────────────
function _fixOklchInlineStyles(root) {
  const PROPS = [
    'color', 'backgroundColor', 'borderTopColor', 'borderRightColor',
    'borderBottomColor', 'borderLeftColor', 'outlineColor', 'fill',
    'stroke', 'boxShadow', 'textDecorationColor', 'backgroundImage',
  ];
  const saved = [];
  const tmp = document.createElement('div');
  document.body.appendChild(tmp);

  const resolveColor = (raw) => {
    if (!raw || !/oklch|oklab|color-mix/i.test(raw)) return null;
    for (const prop of ['backgroundColor', 'color']) {
      tmp.style[prop] = '';
      tmp.style[prop] = raw;
      const v = getComputedStyle(tmp)[prop];
      if (v && v !== 'rgba(0, 0, 0, 0)') return v;
    }
    return '#9ca3af';
  };

  [root, ...root.querySelectorAll('*')].forEach((el) => {
    if (!(el instanceof HTMLElement)) return;
    const cs = getComputedStyle(el);
    const patch = {};
    PROPS.forEach((prop) => {
      const val = cs[prop] || '';
      if (/oklch|oklab|color-mix/i.test(val)) {
        const safe = resolveColor(val);
        if (safe) patch[prop] = safe;
      }
    });

    const inlineStyle = el.getAttribute('style') || '';
    let newInline = inlineStyle;
    if (/oklch|oklab|color-mix/i.test(inlineStyle)) {
      newInline = _sanitiseCssText(inlineStyle);
    }

    if (Object.keys(patch).length || newInline !== inlineStyle) {
      saved.push({ el, inlineStyle, props: {} });
      const entry = saved[saved.length - 1];
      Object.keys(patch).forEach((p) => { entry.props[p] = el.style[p]; el.style[p] = patch[p]; });
      if (newInline !== inlineStyle) el.setAttribute('style', newInline);
    }
  });

  document.body.removeChild(tmp);
  return saved;
}

function _restoreOklchInlineStyles(saved) {
  saved.forEach(({ el, inlineStyle, props }) => {
    Object.keys(props).forEach((p) => { el.style[p] = props[p]; });
    if (inlineStyle) el.setAttribute('style', inlineStyle);
    else el.removeAttribute('style');
  });
}

// ── downloadElementAsPDF — pixel-perfect DOM capture ─────────────────────────
// Root cause of the oklch crash: html2canvas clones the document, which causes
// the browser to re-parse every linked <style>/<link> stylesheet in the clone.
// Those stylesheets contain color-mix(in oklab, …) which html2canvas cannot
// parse. Fix: in onclone(), disable every linked stylesheet in the clone and
// inject a single sanitised inline <style> in its place.
async function downloadElementAsPDF(selector, filename = 'report') {
  if (!window.html2canvas || !window.jspdf) {
    alert('PDF engine still loading — try again in a moment.');
    return;
  }
  const root = typeof selector === 'string' ? document.querySelector(selector) : selector;
  if (!root) { alert('Could not find the element to export.'); return; }

  // ── 1. Bake computed colors into inline styles on the live DOM ─────────────
  const savedStyles = _fixOklchInlineStyles(root);

  // ── 2. Expand the container so html2canvas sees the full height ────────────
  const prevRoot = {
    maxHeight: root.style.maxHeight,
    overflow:  root.style.overflow,
    height:    root.style.height,
    position:  root.style.position,
  };
  root.style.maxHeight = 'none';
  root.style.overflow  = 'visible';
  root.style.height    = 'auto';
  root.style.position  = root.style.position || 'relative';

  // ── 3. Hide buttons / chrome that should not appear in PDF ─────────────────
  root.querySelectorAll('.print-hide, .no-print').forEach((e) => {
    e.setAttribute('data-ph-save', e.style.display || '');
    e.style.display = 'none';
  });

  // ── 4. Wait two frames so layout settles ──────────────────────────────────
  await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));

  try {
    const canvas = await window.html2canvas(root, {
      scale: 2,
      backgroundColor: '#ffffff',
      useCORS: true,
      allowTaint: false,
      scrollX: 0,
      scrollY: -window.scrollY,
      windowWidth:  Math.max(root.scrollWidth, 900),
      windowHeight: root.scrollHeight + 100,
      x: 0, y: 0,
      width:  root.scrollWidth,
      height: root.scrollHeight,
      logging: false,
      onclone: (clonedDoc) => {
        // ── KEY FIX: disable every <link rel="stylesheet"> in the clone so
        // html2canvas never parses the original CSS that contains oklch/color-mix.
        clonedDoc.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
          link.disabled = true;
          link.setAttribute('media', 'print-disabled');
        });
        // Also disable any <style> tags that contain problematic color functions
        clonedDoc.querySelectorAll('style').forEach((s) => {
          if (/oklch|oklab|color-mix/i.test(s.textContent)) {
            s.textContent = _sanitiseCssText(s.textContent);
          }
        });
        // Inject our complete safe token stylesheet
        const safeStyle = clonedDoc.createElement('style');
        safeStyle.textContent = _PDF_SAFE_CSS;
        clonedDoc.head.appendChild(safeStyle);
        // Strip any remaining oklch/color-mix from inline styles in the clone
        clonedDoc.querySelectorAll('*').forEach((el) => {
          const s = el.getAttribute('style') || '';
          if (/oklch|oklab|color-mix/i.test(s)) {
            el.setAttribute('style', _sanitiseCssText(s));
          }
        });
      },
    });

    const img   = canvas.toDataURL('image/png');
    const { jsPDF } = window.jspdf;
    const pdf   = new jsPDF({ unit: 'pt', format: 'a4', compress: true });
    const pageW = pdf.internal.pageSize.getWidth();
    const pageH = pdf.internal.pageSize.getHeight();
    const margin = 20;
    const imgW  = pageW - margin * 2;
    const imgH  = canvas.height * (imgW / canvas.width);
    let yOffset = 0, pageIdx = 0;
    while (yOffset < imgH) {
      if (pageIdx > 0) pdf.addPage();
      pdf.addImage(img, 'PNG', margin, margin - yOffset, imgW, imgH, undefined, 'FAST');
      pdf.setFillColor(255, 255, 255);
      if (yOffset + (pageH - margin * 2) < imgH) {
        pdf.rect(0, pageH - margin, pageW, margin + 10, 'F');
        pdf.rect(0, 0, pageW, margin, 'F');
      }
      yOffset += pageH - margin * 2;
      if (++pageIdx > 80) break;
    }
    pdf.save(`${filename}.pdf`);
  } catch (err) {
    console.error('[PDF]', err);
    alert('PDF export failed — ' + (err.message || 'see console for details.'));
  } finally {
    _restoreOklchInlineStyles(savedStyles);
    root.style.maxHeight = prevRoot.maxHeight;
    root.style.overflow  = prevRoot.overflow;
    root.style.height    = prevRoot.height;
    root.style.position  = prevRoot.position;
    root.querySelectorAll('.print-hide, .no-print').forEach((e) => {
      e.style.display = e.getAttribute('data-ph-save') || '';
      e.removeAttribute('data-ph-save');
    });
  }
}

// ── Default password pattern ─────────────────────────────
// Tokens: {department}, {fullname}, {first4name}, {regnumber}
// Mutable object — admin can override via Settings → Password Pattern.
// Saved overrides are persisted in localStorage under key 'ca_pw_patterns'.
const DEFAULT_PASSWORD_PATTERNS = /*EDITMODE-BEGIN-PW*/{
  "superadmin": "{department}_{fullname}",
  "admin":      "{department}_{fullname}",
  "user":       "{first4name}_{regnumber}",
}/*EDITMODE-END-PW*/;

// On page load, merge any superadmin-saved patterns on top of the defaults.
(function() {
  try {
    var saved = localStorage.getItem('ca_pw_patterns');
    if (saved) {
      var parsed = JSON.parse(saved);
      Object.keys(parsed).forEach(function(k) {
        DEFAULT_PASSWORD_PATTERNS[k] = parsed[k];
      });
    }
  } catch(e) { /* ignore */ }
})();

function applyPasswordPattern(pattern, { department, fullname, regnumber }) {
  const norm     = (s) => String(s || '').trim().toLowerCase().replace(/\s+/g, '').replace(/[^a-z0-9]/g, '');
  const first4   = (s) => norm(s).slice(0, 4);
  return String(pattern || '')
    .replace(/\{department\}/g,  norm(department))
    .replace(/\{fullname\}/g,    norm(fullname))
    .replace(/\{first4name\}/g,  first4(fullname))
    .replace(/\{regnumber\}/g,   norm(regnumber));
}

// ── Coaching PDF / share helpers (fired from Coaching tab) ─
async function downloadCoachingProgramPDF({ slot, weakest, strongest, selected, aspects }) {
  if (!window.jspdf) { alert('PDF engine still loading — try again.'); return; }
  const { jsPDF } = window.jspdf;
  const pdf = new jsPDF({ unit: 'pt', format: 'a4' });
  const pageW = pdf.internal.pageSize.getWidth();
  const margin = 44;
  let y = margin + 6;

  pdf.setFont('helvetica', 'normal');
  pdf.setFontSize(9);
  pdf.setTextColor(120);
  pdf.text('CHISELASSESS · COACHING PROGRAM', margin, y);
  y += 22;

  pdf.setFont('helvetica', 'bold');
  pdf.setFontSize(22);
  pdf.setTextColor(30);
  const title = (slot && slot.title) || 'Cohort coaching program';
  const titleLines = pdf.splitTextToSize(title, pageW - margin * 2);
  titleLines.forEach((line) => { pdf.text(line, margin, y); y += 26; });
  y += 4;

  pdf.setFont('helvetica', 'normal');
  pdf.setFontSize(10);
  pdf.setTextColor(90);
  const subtitle = `Auto-drawn from cohort weakest dimensions · ${new Date().toLocaleDateString()}`;
  pdf.text(subtitle, margin, y); y += 18;
  if (slot) {
    pdf.text(`${slot.dept || 'All departments'} · ${slot.college || 'All colleges'} · Slot ${slot.id.toUpperCase()}`, margin, y);
    y += 22;
  }

  // Weakest dimensions
  pdf.setFont('helvetica', 'bold'); pdf.setFontSize(12); pdf.setTextColor(30);
  pdf.text('Focus dimensions', margin, y); y += 18;
  pdf.setFont('helvetica', 'normal'); pdf.setFontSize(10.5); pdf.setTextColor(55);
  (weakest || []).forEach((w) => {
    pdf.text(`•  ${w.c.label} — cohort avg ${w.v}/100`, margin + 4, y);
    y += 16;
  });
  y += 6;
  if (strongest && strongest.length) {
    pdf.setFont('helvetica', 'italic'); pdf.setTextColor(110);
    pdf.text(`Strongest: ${strongest.map((s) => s.c.label).join(' · ')} — maintain current practice.`, margin, y);
    y += 20;
  }

  // Training aspects
  pdf.setFont('helvetica', 'bold'); pdf.setFontSize(12); pdf.setTextColor(30);
  pdf.text('Training aspects', margin, y); y += 18;
  pdf.setFont('helvetica', 'normal'); pdf.setFontSize(10); pdf.setTextColor(55);
  const selectedAspects = (aspects || []).filter((a) => (selected || []).includes(a.key));
  selectedAspects.forEach((a) => {
    if (y > 760) { pdf.addPage(); y = margin + 6; }
    pdf.setFont('helvetica', 'bold'); pdf.setTextColor(30);
    pdf.text(`· ${a.label}`, margin + 4, y); y += 14;
    pdf.setFont('helvetica', 'normal'); pdf.setTextColor(85);
    const hintLines = pdf.splitTextToSize(a.hint, pageW - margin * 2 - 12);
    hintLines.forEach((ln) => { pdf.text(ln, margin + 12, y); y += 13; });
    y += 4;
  });

  // Footer
  pdf.setFontSize(8); pdf.setTextColor(140);
  pdf.text(`${selectedAspects.length} aspects selected · estimated 4 sessions`, margin, 820);

  const safe = (slot?.id || 'cohort').toUpperCase();
  pdf.save(`ChiselAssess_${safe}_coaching_program.pdf`);
}

function shareCoachingWithCohort({ slot, reports, selected, aspects }) {
  const picked = (aspects || []).filter((a) => (selected || []).includes(a.key));
  const names = picked.map((a) => a.label).join(', ');
  const to = (reports || []).map((r) => r.student?.email || `${r.student?.regNo?.toLowerCase?.()}@college.ac.in`).filter(Boolean);
  const subject = `Coaching plan · ${(slot && slot.title) || 'your assessment'}`;
  const body = [
    `Hi team,`,
    ``,
    `Based on your latest submissions in "${slot?.title || 'this slot'}", the following coaching aspects are scheduled over the next two weeks:`,
    ``,
    ...picked.map((a) => `• ${a.label} — ${a.hint}`),
    ``,
    `Session details and materials will follow separately.`,
    ``,
    `— Faculty`,
  ].join('\n');
  const href = `mailto:${to.slice(0, 40).join(',')}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
  try { window.location.href = href; } catch {}
  try { window.__toast && window.__toast(`Shared with ${to.length} students · ${names || 'coaching plan'}`); } catch {}
}

function emailFlaggedStudents({ c, students, slot }) {
  const to = (students || []).map((r) => r.student?.email || `${r.student?.regNo?.toLowerCase?.()}@college.ac.in`).filter(Boolean);
  const subject = `Coaching brief · ${c.label}`;
  const body = [
    `Hi,`,
    ``,
    `Your recent submission in "${slot?.title || 'the assessment'}" scored below cohort median on ${c.label}.`,
    ``,
    `Over the next week, practice:`,
    `• Short focused drills targeting ${c.label.toLowerCase()}`,
    `• Re-recording one answer daily with the technique in mind`,
    ``,
    `Reply if you'd like a 1:1 session.`,
    ``,
    `— Faculty`,
  ].join('\n');
  const href = `mailto:${to.join(',')}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
  try { window.location.href = href; } catch {}
}

function downloadFlaggedCSV({ c, students, slot }) {
  const lines = [['Criterion focus', c.label]];
  lines.push(['Slot', slot?.title || '']);
  lines.push([]);
  lines.push(['Name', 'Reg No', 'Department', `${c.label} score`, 'Overall']);
  (students || []).forEach((r) => {
    lines.push([r.student.name, r.student.regNo, r.student.dept, r.scores[c.key], r.scores.overall]);
  });
  const csv = toCSV(lines);
  downloadBlob(`ChiselAssess_${(slot?.id || 'cohort').toUpperCase()}_${c.key}_flagged.csv`, new Blob([csv], { type: 'text/csv;charset=utf-8' }));
}

// ── Analytics PDF — visual replica of the analytics page ─────────────────────
async function downloadSlotAnalyticsPDF(slot, reports, critAvg) {
  if (!window.jspdf) { alert('PDF engine loading — try again.'); return; }
  const { jsPDF } = window.jspdf;
  const pdf = new jsPDF({ unit: 'pt', format: 'a4', compress: true });
  const pw = pdf.internal.pageSize.getWidth();   // 595
  const ph = pdf.internal.pageSize.getHeight();  // 842
  const ml = 32, mr = 32, cw = pw - ml - mr;
  let y = 0;
  const criteria = window.CRITERIA || [];

  const addPage = () => { pdf.addPage(); y = 32; };
  const chk = (need = 30) => { if (y + need > ph - 36) addPage(); };

  // Colour helpers
  const hex = (h) => {
    const r = parseInt(h.slice(1,3),16), g = parseInt(h.slice(3,5),16), b = parseInt(h.slice(5,7),16);
    return [r,g,b];
  };
  const scoreCol = (v) => v >= 80 ? [34,197,94] : v >= 60 ? [234,179,8] : [239,68,68];
  const NAVY = [30,58,95], WHITE = [255,255,255];

  // Computed data
  const total   = reports.length;
  const scores  = reports.map(r => r.scores?.overall ?? 0);
  const avg     = total ? +(scores.reduce((a,b)=>a+b,0)/total).toFixed(1) : 0;
  const passRate= total ? Math.round(scores.filter(s=>s>=6).length/total*100) : 0;
  const topSc   = total ? Math.max(...scores).toFixed(1) : 0;
  const below6  = scores.filter(s=>s<6).length;
  const sorted  = [...reports].sort((a,b)=>b.scores.overall-a.scores.overall);

  // Grade distribution
  const grades = {};
  reports.forEach(r => { const g = r.grade?.g||'?'; grades[g]=(grades[g]||0)+1; });

  // Department breakdown
  const deptMap = {};
  reports.forEach(r => {
    const d = r.student?.dept || 'Other';
    if (!deptMap[d]) deptMap[d] = [];
    deptMap[d].push(r.scores.overall);
  });
  const deptRows = Object.entries(deptMap).map(([d,vs]) => ({
    dept: d, n: vs.length,
    avg: Math.round(vs.reduce((a,b)=>a+b,0)/vs.length),
  })).sort((a,b)=>b.avg-a.avg);

  // ── PAGE 1 ────────────────────────────────────────────────────────────────

  // ── Gradient header bar ──
  pdf.setFillColor(...NAVY); pdf.rect(0,0,pw,68,'F');
  // Decorative accent strip
  pdf.setFillColor(169,50,38); pdf.rect(0,64,pw,4,'F');
  pdf.setFont('helvetica','bold'); pdf.setFontSize(18); pdf.setTextColor(...WHITE);
  pdf.text('ChiselAssess — Slot Analytics Report', ml, 26);
  pdf.setFont('helvetica','normal'); pdf.setFontSize(9); pdf.setTextColor(180,210,240);
  pdf.text(`${slot?.title || 'Slot'}`, ml, 42);
  pdf.text(new Date().toLocaleDateString('en-GB',{day:'2-digit',month:'short',year:'numeric'}), pw-mr, 42, {align:'right'});
  pdf.text(`${total} submissions · Dept: ${slot?.dept||'All'} · College: ${slot?.college||'All'}`, ml, 56);
  y = 82;

  // ── KPI cards row ──
  const kpis = [
    { label:'Submissions', value:String(total),          color:[59,130,246]  },
    { label:'Avg Score',   value:`${avg}/10`,             color:scoreCol(avg*10) },
    { label:'Top Score',   value:`${topSc}/10`,           color:[34,197,94]   },
    { label:'Need Support',value:String(below6),          color:below6>0?[239,68,68]:[34,197,94] },
  ];
  const kw = cw/kpis.length - 4;
  kpis.forEach((k,i) => {
    const x = ml + i*(kw+5);
    pdf.setFillColor(248,250,252); pdf.roundedRect(x,y,kw,48,5,5,'F');
    pdf.setDrawColor(...k.color); pdf.setLineWidth(1.5); pdf.roundedRect(x,y,kw,48,5,5,'S');
    pdf.setLineWidth(0.5);
    pdf.setFont('helvetica','bold'); pdf.setFontSize(18); pdf.setTextColor(...k.color);
    pdf.text(k.value, x+kw/2, y+26, {align:'center'});
    pdf.setFont('helvetica','normal'); pdf.setFontSize(8); pdf.setTextColor(100,100,100);
    pdf.text(k.label, x+kw/2, y+40, {align:'center'});
  });
  y += 60;

  // ── Row 1: Score distribution + Criteria averages ──
  chk(130);
  const col1x = ml, col2x = ml + cw/2 + 6, colW = cw/2 - 6;

  // -- Score Distribution (bar chart) --
  pdf.setFillColor(248,250,252); pdf.roundedRect(col1x, y, colW, 130, 5, 5, 'F');
  pdf.setDrawColor(229,231,235); pdf.setLineWidth(0.5); pdf.roundedRect(col1x, y, colW, 130, 5, 5, 'S');
  pdf.setFont('helvetica','bold'); pdf.setFontSize(10); pdf.setTextColor(30,30,30);
  pdf.text('Score Distribution', col1x+10, y+14);
  pdf.setFont('helvetica','normal'); pdf.setFontSize(7.5); pdf.setTextColor(120,120,120);
  pdf.text(`${total} submissions`, col1x+colW-10, y+14, {align:'right'});

  const bands = [
    {label:'<5', min:0, max:5,  color:[239,68,68]  },
    {label:'5–6',min:5, max:6,  color:[251,146,60] },
    {label:'6–7',min:6, max:7,  color:[234,179,8]  },
    {label:'7–8',min:7, max:8,  color:[59,130,246] },
    {label:'8–9',min:8, max:9,  color:[34,197,94]  },
    {label:'9–10',min:9,max:10.1,color:[22,163,74] },
  ];
  const bCounts = bands.map(b => scores.filter(s=>s>=b.min&&s<b.max).length);
  const maxB = Math.max(...bCounts, 1);
  const bChartH = 72, bChartY = y+28, bBarW = (colW-24)/bands.length - 3;
  bands.forEach((b,i) => {
    const bh = Math.max(3, (bCounts[i]/maxB)*bChartH);
    const bx = col1x+12+i*(bBarW+3);
    pdf.setFillColor(229,231,235);
    pdf.roundedRect(bx, bChartY, bBarW, bChartH, 2, 2, 'F');
    pdf.setFillColor(...b.color);
    pdf.roundedRect(bx, bChartY+bChartH-bh, bBarW, bh, 2, 2, 'F');
    if (bCounts[i] > 0) {
      pdf.setFont('helvetica','bold'); pdf.setFontSize(7.5); pdf.setTextColor(30,30,30);
      pdf.text(String(bCounts[i]), bx+bBarW/2, bChartY+bChartH-bh-3, {align:'center'});
    }
    pdf.setFont('helvetica','normal'); pdf.setFontSize(6.5); pdf.setTextColor(100,100,100);
    pdf.text(b.label, bx+bBarW/2, bChartY+bChartH+9, {align:'center'});
  });

  // -- Criteria Averages (horizontal bars) --
  pdf.setFillColor(248,250,252); pdf.roundedRect(col2x, y, colW, 130, 5, 5, 'F');
  pdf.setDrawColor(229,231,235); pdf.roundedRect(col2x, y, colW, 130, 5, 5, 'S');
  pdf.setFont('helvetica','bold'); pdf.setFontSize(10); pdf.setTextColor(30,30,30);
  pdf.text('Criteria Averages', col2x+10, y+14);
  pdf.setFont('helvetica','normal'); pdf.setFontSize(7.5); pdf.setTextColor(120,120,120);
  pdf.text('out of 100', col2x+colW-10, y+14, {align:'right'});

  const critColors = [[220,38,38],[59,130,246],[22,163,74],[124,58,237],[217,119,6],[8,145,178]];
  const barMaxW = colW - 90;
  criteria.forEach((c,i) => {
    const v = critAvg[i] || 0;
    const cy2 = y + 28 + i*17;
    pdf.setFont('helvetica','normal'); pdf.setFontSize(7.5); pdf.setTextColor(55,65,81);
    pdf.text(c.label.slice(0,16), col2x+10, cy2+8);
    // track
    pdf.setFillColor(229,231,235); pdf.roundedRect(col2x+78, cy2+2, barMaxW, 8, 2, 2, 'F');
    // fill
    pdf.setFillColor(...(critColors[i]||[59,130,246]));
    pdf.roundedRect(col2x+78, cy2+2, Math.max(4,barMaxW*(v/100)), 8, 2, 2, 'F');
    // value
    pdf.setFont('helvetica','bold'); pdf.setFontSize(7.5); pdf.setTextColor(...(critColors[i]||[59,130,246]));
    pdf.text(`${v}`, col2x+colW-8, cy2+8, {align:'right'});
  });
  y += 142;

  // ── Row 2: Grade mix + Department breakdown ──
  chk(120);
  // -- Grade mix (visual tiles) --
  pdf.setFillColor(248,250,252); pdf.roundedRect(col1x, y, colW, 120, 5, 5, 'F');
  pdf.setDrawColor(229,231,235); pdf.roundedRect(col1x, y, colW, 120, 5, 5, 'S');
  pdf.setFont('helvetica','bold'); pdf.setFontSize(10); pdf.setTextColor(30,30,30);
  pdf.text('Grade Mix', col1x+10, y+14);

  const gradeOrder = ['A+','A','B+','B','C+','C','D','E'];
  const gradeColors = { 'A+':[22,163,74],'A':[34,197,94],'B+':[59,130,246],'B':[99,102,241],'C+':[234,179,8],'C':[251,146,60],'D':[249,115,22],'E':[239,68,68] };
  const gradeMaxW = colW - 24;
  const gradeMaxCount = Math.max(...gradeOrder.map(g => grades[g]||0), 1);
  gradeOrder.forEach((g, i) => {
    const cnt = grades[g] || 0;
    const gy = y + 24 + i*11;
    const col = gradeColors[g] || [150,150,150];
    pdf.setFont('helvetica','bold'); pdf.setFontSize(7.5); pdf.setTextColor(...col);
    pdf.text(g, col1x+12, gy+7);
    pdf.setFillColor(229,231,235); pdf.roundedRect(col1x+30, gy+1, gradeMaxW-44, 7, 2, 2, 'F');
    if (cnt > 0) {
      pdf.setFillColor(...col);
      pdf.roundedRect(col1x+30, gy+1, Math.max(4,(gradeMaxW-44)*(cnt/gradeMaxCount)), 7, 2, 2, 'F');
    }
    pdf.setFont('helvetica','normal'); pdf.setFontSize(7); pdf.setTextColor(80,80,80);
    pdf.text(`${cnt}`, col1x+colW-10, gy+7, {align:'right'});
  });

  // -- Department breakdown (table) --
  pdf.setFillColor(248,250,252); pdf.roundedRect(col2x, y, colW, 120, 5, 5, 'F');
  pdf.setDrawColor(229,231,235); pdf.roundedRect(col2x, y, colW, 120, 5, 5, 'S');
  pdf.setFont('helvetica','bold'); pdf.setFontSize(10); pdf.setTextColor(30,30,30);
  pdf.text('Department Breakdown', col2x+10, y+14);

  // Table header
  pdf.setFillColor(...NAVY); pdf.rect(col2x+6, y+18, colW-12, 12, 'F');
  pdf.setFont('helvetica','bold'); pdf.setFontSize(7); pdf.setTextColor(...WHITE);
  ['Department','N','Avg Score'].forEach((h,i) => {
    const xs = [col2x+10, col2x+colW-52, col2x+colW-22];
    pdf.text(h, xs[i], y+26, i===2?{align:'right'}:{});
  });
  const deptMaxAvg = Math.max(...deptRows.map(d=>d.avg), 10);
  deptRows.slice(0,6).forEach((d,i) => {
    const dy = y + 32 + i*13;
    if (i%2===0) { pdf.setFillColor(243,244,246); pdf.rect(col2x+6, dy, colW-12, 13, 'F'); }
    pdf.setFont('helvetica','normal'); pdf.setFontSize(7.5); pdf.setTextColor(30,30,30);
    pdf.text((d.dept||'?').slice(0,18), col2x+10, dy+9);
    pdf.text(String(d.n), col2x+colW-50, dy+9);
    pdf.setFont('helvetica','bold'); pdf.setTextColor(...scoreCol(d.avg));
    pdf.text(`${d.avg}/10`, col2x+colW-10, dy+9, {align:'right'});
    pdf.setFont('helvetica','normal'); pdf.setTextColor(30,30,30);
  });
  y += 132;

  // ── Row 3: Top performers ──
  chk(100);
  pdf.setFillColor(248,250,252); pdf.roundedRect(ml, y, cw, Math.min(sorted.length*14+36, 120), 5, 5, 'F');
  pdf.setDrawColor(229,231,235); pdf.roundedRect(ml, y, cw, Math.min(sorted.length*14+36, 120), 5, 5, 'S');
  pdf.setFont('helvetica','bold'); pdf.setFontSize(10); pdf.setTextColor(30,30,30);
  pdf.text('Top Performers', ml+10, y+14);

  // Header
  pdf.setFillColor(...NAVY); pdf.rect(ml+6, y+18, cw-12, 12, 'F');
  const tCols = [ml+10, ml+28, ml+150, ml+260, ml+340, ml+400, ml+cw-10];
  pdf.setFont('helvetica','bold'); pdf.setFontSize(7); pdf.setTextColor(...WHITE);
  ['#','Name','Reg No','Dept','College','Score','Grade'].forEach((h,i) => {
    pdf.text(h, tCols[i], y+26, i===6?{align:'right'}:{});
  });
  sorted.slice(0,6).forEach((r,i) => {
    chk(14);
    const ry = y+32+i*13;
    if (i%2===0) { pdf.setFillColor(243,244,246); pdf.rect(ml+6, ry, cw-12, 13, 'F'); }
    pdf.setFont('helvetica','normal'); pdf.setFontSize(8); pdf.setTextColor(30,30,30);
    pdf.text(String(i+1), tCols[0], ry+9);
    pdf.text((r.student?.name||'').slice(0,22), tCols[1], ry+9);
    pdf.text((r.student?.regNo||'').slice(0,14), tCols[2], ry+9);
    pdf.text((r.student?.dept||'').slice(0,12), tCols[3], ry+9);
    pdf.text((r.student?.college||'').slice(0,10), tCols[4], ry+9);
    pdf.setFont('helvetica','bold'); pdf.setTextColor(...scoreCol(r.scores.overall));
    pdf.text(`${r.scores.overall}/10`, tCols[5], ry+9);
    pdf.setFont('helvetica','bold'); pdf.setFontSize(7); pdf.setTextColor(80,80,80);
    pdf.text(r.grade?.g||'', tCols[6], ry+9, {align:'right'});
  });
  y += Math.min(sorted.length, 6)*13 + 48;

  // ── Needs support section ──
  if (below6 > 0) {
    chk(60);
    const belowReports = sorted.slice().reverse().filter(r=>r.scores.overall<6).slice(0,6);
    pdf.setFillColor(254,226,226); pdf.roundedRect(ml,y,cw,Math.min(belowReports.length*13+32,100),5,5,'F');
    pdf.setDrawColor(239,68,68); pdf.setLineWidth(1); pdf.roundedRect(ml,y,cw,Math.min(belowReports.length*13+32,100),5,5,'S');
    pdf.setLineWidth(0.5);
    pdf.setFont('helvetica','bold'); pdf.setFontSize(10); pdf.setTextColor(153,27,27);
    pdf.text(`⚑  Needs Support — ${below6} student${below6>1?'s':''} scored below 6`, ml+10, y+14);
    pdf.setFillColor(254,202,202); pdf.rect(ml+6,y+18,cw-12,11,'F');
    pdf.setFont('helvetica','bold'); pdf.setFontSize(7); pdf.setTextColor(153,27,27);
    ['Name','Reg No','Dept','Score','Weakest Criterion'].forEach((h,i)=>{
      const xs = [ml+10,ml+140,ml+240,ml+310,ml+355];
      pdf.text(h, xs[i], y+25);
    });
    belowReports.forEach((r,i) => {
      const ry = y+30+i*13;
      if(i%2===0){pdf.setFillColor(255,241,242);pdf.rect(ml+6,ry,cw-12,13,'F');}
      pdf.setFont('helvetica','normal'); pdf.setFontSize(8); pdf.setTextColor(80,30,30);
      pdf.text((r.student?.name||'').slice(0,22),ml+10,ry+9);
      pdf.text((r.student?.regNo||'').slice(0,14),ml+140,ry+9);
      pdf.text((r.student?.dept||'').slice(0,12),ml+240,ry+9);
      pdf.setFont('helvetica','bold'); pdf.setTextColor(239,68,68);
      pdf.text(`${r.scores.overall}/10`,ml+310,ry+9);
      const wkKey = criteria.reduce((a,c)=>((r.scores[c.key]||0)<(r.scores[a.key]||0)?c:a), criteria[0]||{key:'',label:''});
      pdf.setFont('helvetica','normal'); pdf.setFontSize(7.5); pdf.setTextColor(100,50,50);
      pdf.text((wkKey.label||'').slice(0,22),ml+355,ry+9);
    });
    y += belowReports.length*13 + 38;
  }

  // ── Footer on every page ──────────────────────────────────────────────────
  const np = pdf.internal.getNumberOfPages();
  for (let i=1;i<=np;i++) {
    pdf.setPage(i);
    pdf.setDrawColor(229,231,235); pdf.setLineWidth(0.5); pdf.line(ml,ph-28,pw-mr,ph-28);
    pdf.setFont('helvetica','normal'); pdf.setFontSize(7.5); pdf.setTextColor(150,150,150);
    pdf.text('ChiselAssess · Confidential Analytics Report',ml,ph-16);
    pdf.text(`Page ${i} of ${np}`,pw-mr,ph-16,{align:'right'});
  }

  const safe = (slot?.id||'slot').toString().replace(/\s+/g,'_');
  pdf.save(`ChiselAssess_Slot${safe}_Analytics.pdf`);
}


Object.assign(window, {
  DEFAULT_PASSWORD_PATTERNS, applyPasswordPattern,
  downloadCoachingProgramPDF, shareCoachingWithCohort,
  emailFlaggedStudents, downloadFlaggedCSV,
  downloadReportAsPDF, downloadSlotAnalyticsPDF, downloadElementAsPDF,
});

// ── Backend API connection ───────────────────────────────
const API_BASE = (location.hostname === 'localhost' || location.hostname === '127.0.0.1')
  ? 'http://localhost:8001'
  : 'https://chisel-scaled-backend.onrender.com';

// Patch fetch to inject JWT on API calls
(function patchFetch() {
  const _fetch = window.fetch.bind(window);
  window.fetch = function(url, opts = {}) {
    if (typeof url === 'string' && url.startsWith(API_BASE)) {
      const token = localStorage.getItem('ca_token');
      if (token) {
        opts = { ...opts, headers: { ...(opts.headers || {}), Authorization: `Bearer ${token}` } };
      }
    }
    return _fetch(url, opts);
  };
})();

async function apiFetch(path, opts = {}) {
  const res = await fetch(`${API_BASE}${path}`, opts);
  if (!res.ok) {
    let msg = `${res.status}`;
    try { const j = await res.json(); msg = j.detail || j.message || msg; } catch {}
    throw new Error(msg);
  }
  return res.json();
}

function _saveAuth(d) {
  localStorage.setItem('ca_token', d.access_token);
  localStorage.setItem('ca_token_exp', d.expires_at || '');
  const user = {
    id: d.user_id, name: d.name, email: d.email,
    role: d.role, dept: d.department, regNo: d.reg_number,
    college: d.college, avatarSeed: (d.name || '').length,
  };
  localStorage.setItem('ca_user', JSON.stringify(user));
  return user;
}

function _loadAuth() {
  const token = localStorage.getItem('ca_token');
  const exp   = localStorage.getItem('ca_token_exp');
  if (!token) return null;
  if (exp && new Date(exp) < new Date()) { _clearAuth(); return null; }
  try { return JSON.parse(localStorage.getItem('ca_user') || 'null'); } catch { return null; }
}

function _clearAuth() {
  ['ca_token', 'ca_token_exp', 'ca_user', 'ca_authed', 'ca_role', 'ca_view'].forEach((k) => localStorage.removeItem(k));
}

function normaliseApiSlot(s) {
  const expires = s.expires_at ? parseApiDate(s.expires_at) : daysAgo(-7);
  const created = s.created_at ? parseApiDate(s.created_at) : daysAgo(1);
  const passkey = s.passkey_plain || (s.has_passkey ? true : null);
  return {
    id: String(s.id || s._id || ''),
    title: s.title || 'Untitled slot',
    dept: s.allowed_dept || null,
    college: s.allowed_college || null,
    status: s.is_open ? 'open' : 'closed',
    created,
    expires,
    passkey,
    attempts: s.max_attempts || null,
    facultyOwner: s.faculty_name || s.created_by || '',
    analyticsAccess: !!s.analytics_access,
    studentCount: s.student_count || 0,
    submitted: s.eval_count || s.my_eval_count || 0,
    avgScore: s.avg_score ? Math.round(s.avg_score * 10) : null,
    reports: [],
    _raw: s,
  };
}

function normaliseApiReport(r) {
  const ev = (r.evaluation && typeof r.evaluation === 'object') ? r.evaluation : {};

  // ── Criteria: prefer new 6-key format, fall back to old 4-key ─────────────
  const criteria = r.criteria || ev.criteria || {};
  const toOld  = (v, max) => Math.min(100, Math.round((parseFloat(v) || 0) / max * 100));

  let scores;
  if (criteria.content !== undefined) {
    // New format: criteria.content.score is already 0-100
    const get = (k) => Math.min(100, Math.round(parseFloat(criteria[k]?.score || 0)));
    scores = {
      content:    get('content'),
      structure:  get('structure'),
      clarity:    get('clarity'),
      engagement: get('engagement'),
      language:   get('language'),
      confidence: get('confidence'),
    };
  } else {
    // Old 4-criterion format (backward compat)
    const sc = criteria.speech_clarity         ? toOld(criteria.speech_clarity.score,         2.5) : 0;
    const ge = criteria.grammatical_efficiency  ? toOld(criteria.grammatical_efficiency.score,  2.5) : 0;
    const wc = criteria.word_choice             ? toOld(criteria.word_choice.score,             2.5) : 0;
    const tc = criteria.thought_clarity         ? toOld(criteria.thought_clarity.score,         2.5) : 0;
    scores = { content: tc, structure: tc, clarity: sc, engagement: wc, language: ge, confidence: sc };
  }

  // overall_score from AI is 0-10; multiply × 10 for bar/chart display (0-100)
  const rawOverall = parseFloat(r.overall_score || ev.overall_score || 0);
  const overall10  = Math.min(10, rawOverall);                // 0-10 for display
  const overall100 = Math.min(100, Math.round(rawOverall * 10)); // 0-100 for bars/thresholds
  scores.overall = overall10;    // stored as 0-10 float
  scores.overall_pct = overall100; // stored as 0-100 int (for analytics comparisons)

  // ── Voice telemetry ────────────────────────────────────────────────────────
  const fw          = r.filler_words  || ev.filler_words  || {};
  const cd          = r.content_density || ev.content_density || {};
  const sa          = r.self_assessment || ev.self_assessment || {};
  const durationSec = r.duration_seconds || ev.duration_seconds || 0;
  const wordCount   = r.word_count  || ev.word_count  || 0;
  const wpm         = r.wpm         || ev.wpm         || (durationSec > 0 && wordCount > 0 ? Math.round(wordCount / (durationSec / 60)) : 0);
  const pauseRate   = r.pause_rate  || ev.pause_rate  || 1.5;

  // ── Student identity ───────────────────────────────────────────────────────
  const name  = r.name  || ev.name  || 'Unknown';
  const regNo = r.reg   || r.register_number || 'UNKNOWN';
  const dept  = r.department || r.dept || ev.dept || 'Unknown';
  const college = r.college || ev.college || '';

  const date   = r.created_at ? parseApiDate(r.created_at) : new Date();
  const grade  = gradeOf(overall10);

  const student = {
    id: regNo, name, email: `${name.split(' ')[0].toLowerCase()}@college.ac.in`,
    regNo, dept, college, avatarSeed: regNo.length,
    trend: Array.from({ length: 6 }, (_, i) => Math.max(3, Math.min(9.9, overall10 - 1 + i * 0.2))),
  };

  const toList = (v) => typeof v === 'string'
    ? v.split(/\n|•/).map((s) => s.trim()).filter(Boolean).slice(0, 4)
    : (Array.isArray(v) ? v.slice(0, 4) : [String(v || '')].filter(Boolean));

  // ── Self score: derive from self_assessment or estimate ───────────────────
  const selfRaw   = sa.q1 ? ((+sa.q1 + (+sa.q2 || 5) + (+sa.q3 || 5)) / 30 * 10) : Math.max(3, overall10 - 0.8);
  const selfScore = Math.round(selfRaw * 10) / 10; // one decimal, 0-10

  return {
    id:         String(r.id || r._id || ('r' + Math.random().toString(36).slice(2))),
    studentId:  student.id,
    student,
    topic:      r.topic || 'Unknown topic',
    date,
    // Voice telemetry
    duration:   Math.round(durationSec),
    words:      wordCount,
    wpm:        wpm,
    fillers:    cd.fillers ?? fw.total_count ?? 0,
    pauseRate,
    // Scores
    scores,
    grade,
    selfScore,
    // Content
    strengths:  toList(r.strengths  || ev.strengths  || ''),
    weaknesses: toList(r.improvements || ev.improvements || r.weaknesses || ev.weaknesses || ''),
    verdict:    r.verdict || ev.verdict || grade.label,
    // Rich coaching data (real from AI)
    annotated_excerpts: r.annotated_excerpts || ev.annotated_excerpts || [],
    coaching_drills:    r.coaching_drills    || ev.coaching_drills    || [],
    weakest_dimension:  r.weakest_dimension  || ev.weakest_dimension  || 'clarity',
    content_density: {
      fillers:       cd.fillers       ?? fw.total_count ?? 0,
      hedges:        cd.hedges        ?? 0,
      vague:         cd.vague         ?? 0,
      filler_words:  cd.filler_words  ?? (fw.detected || []).map((d) => d.word).slice(0, 5),
      hedge_words:   cd.hedge_words   ?? [],
      vague_phrases: cd.vague_phrases ?? [],
    },
    // Raw data for PDF / export
    _raw: r, _api: true,
    transcript: r.transcript || ev.transcript || '',
  };
}

Object.assign(window, { API_BASE, apiFetch, _saveAuth, _loadAuth, _clearAuth, normaliseApiSlot, normaliseApiReport });

// ── apiFetch with retry + abort support ─────────────────────────────────────
// Wraps the original apiFetch with:
// 1. Automatic retry (×2) with exponential back-off for 5xx / network errors
// 2. Request deduplication — same URL+method within 80ms → returns same promise
// 3. Timeout (30 s) to prevent hung requests under stress
const _pendingRequests = new Map();

const _apiFetchRobust = async (path, opts = {}, _retry = 0) => {
  const key = `${(opts.method || 'GET')}:${path}`;
  if ((opts.method || 'GET') === 'GET' && _pendingRequests.has(key)) {
    return _pendingRequests.get(key);
  }
  const controller = new AbortController();
  const tId = setTimeout(() => controller.abort(), 30000);
  const promise = (async () => {
    try {
      const res = await fetch(`${API_BASE}${path}`, {
        ...opts,
        signal: controller.signal,
        headers: {
          ...(opts.headers || {}),
          ...(localStorage.getItem('ca_token') ? { Authorization: `Bearer ${localStorage.getItem('ca_token')}` } : {}),
        },
      });
      clearTimeout(tId);
      if (!res.ok) {
        let msg = `${res.status}`;
        try { const j = await res.json(); msg = j.detail || j.message || msg; } catch {}
        if (res.status >= 500 && _retry < 2) {
          await new Promise((r) => setTimeout(r, 600 * Math.pow(2, _retry)));
          return _apiFetchRobust(path, opts, _retry + 1);
        }
        throw new Error(msg);
      }
      return res.json();
    } catch (e) {
      clearTimeout(tId);
      if (e.name === 'AbortError') throw new Error('Request timed out — try again.');
      if (e.message === 'Failed to fetch' && _retry < 2) {
        await new Promise((r) => setTimeout(r, 800 * Math.pow(2, _retry)));
        return _apiFetchRobust(path, opts, _retry + 1);
      }
      throw e;
    } finally {
      if ((opts.method || 'GET') === 'GET') _pendingRequests.delete(key);
    }
  })();
  if ((opts.method || 'GET') === 'GET') {
    _pendingRequests.set(key, promise);
    // Auto-clear the dedup slot after 80 ms so rapid successive calls still each get fresh data
    setTimeout(() => _pendingRequests.delete(key), 80);
  }
  return promise;
};

// Replace the module-level apiFetch with the robust version
window.apiFetch = _apiFetchRobust;

// ── Mobile hamburger — injected once after DOM ready ────────────────────────
(function initMobileNav() {
  const doInit = () => {
    if (document.getElementById('sb-hamburger-btn')) return; // already done
    const btn = document.createElement('button');
    btn.id = 'sb-hamburger-btn';
    btn.className = 'sb-hamburger';
    btn.setAttribute('aria-label', 'Open navigation');
    btn.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>`;
    document.body.appendChild(btn);

    const overlay = document.createElement('div');
    overlay.className = 'sb-overlay';
    overlay.id = 'sb-overlay';
    document.body.appendChild(overlay);

    const close = () => {
      const sb = document.querySelector('.sb');
      if (sb) sb.classList.remove('open');
      overlay.classList.remove('visible');
      btn.setAttribute('aria-expanded', 'false');
    };
    const open = () => {
      const sb = document.querySelector('.sb');
      if (sb) sb.classList.add('open');
      overlay.classList.add('visible');
      btn.setAttribute('aria-expanded', 'true');
    };

    btn.addEventListener('click', () => {
      const sb = document.querySelector('.sb');
      if (sb && sb.classList.contains('open')) close(); else open();
    });
    overlay.addEventListener('click', close);
    document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); });

    // Close sidebar when a nav item is clicked (mobile)
    document.addEventListener('click', (e) => {
      const item = e.target.closest('.sb-item');
      if (item && window.innerWidth <= 960) close();
    });
  };

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => setTimeout(doInit, 400));
  } else {
    setTimeout(doInit, 400);
  }
  // Also re-check after React renders
  window.addEventListener('load', () => setTimeout(doInit, 800));
})();

// ── Debounce utility ─────────────────────────────────────────────────────────
function _debounce(fn, ms) {
  let t;
  return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
}
window._debounce = _debounce;
