/* pdf.jsx — TRUE text-based PDF export (selectable, ATS-parseable).
   Builds the PDF from the résumé DATA with jsPDF's standard fonts, in clean
   single-column reading order. Every character is real text — copy/paste and
   ATS keyword extraction work. Standard fonts (Helvetica / Times) are the
   safest possible choice for applicant-tracking systems. */

function ensureJsPDF() {
  if (window.jspdf && window.jspdf.jsPDF) return Promise.resolve(true);
  if (!window.__jspdfP) {
    window.__jspdfP = new Promise((res) => {
      const s = document.createElement("script");
      s.src = "https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js";
      s.onload = () => res(!!(window.jspdf && window.jspdf.jsPDF));
      s.onerror = () => res(false);
      document.head.appendChild(s);
    });
  }
  return window.__jspdfP;
}
function loadPdfLibs() { return ensureJsPDF(); }

function hexToRgb(hex) {
  const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex || "");
  return m ? [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)] : [20, 23, 31];
}

/* HTML (rich-text custom section) -> ordered blocks of {type, text} */
function htmlToBlocks(html) {
  if (!html) return [];
  const root = document.createElement("div");
  root.innerHTML = DOMPurify.sanitize(html);
  const blocks = [];
  const pushPara = (t) => { const s = t.replace(/\s+/g, " ").trim(); if (s) blocks.push({ type: "para", text: s }); };
  root.childNodes.forEach((node) => {
    if (node.nodeType === 3) { pushPara(node.textContent); return; }
    const tag = (node.tagName || "").toLowerCase();
    if (tag === "ul" || tag === "ol") {
      node.querySelectorAll("li").forEach((li) => {
        const s = li.textContent.replace(/\s+/g, " ").trim();
        if (s) blocks.push({ type: "bullet", text: s });
      });
    } else if (tag === "br") {
      // skip
    } else {
      pushPara(node.textContent);
    }
  });
  if (!blocks.length) pushPara(root.textContent);
  return blocks;
}

/* Per-template PDF style profiles — keep every template visually distinct
   while staying real, selectable, single-column text. */
const PDF_STYLES = {
  classic:   { fam: "times",     align: "center", name: { size: 23 },                         title: { c: "sub", italic: true },        contact: { bar: "none", sep: "  •  " }, head: { rule: "full",        c: "ink",    upper: true,  size: 11.5, sp: 1.4 }, comp: { c: "ink",    italic: true } },
  modern:    { fam: "helvetica", align: "left",   name: { size: 24, bold: true },             title: { c: "accent" },                   contact: { bar: "none", sep: "   |   " },head: { rule: "accent",      c: "ink",    upper: true,  size: 11.5, sp: 1.2 }, comp: { c: "accent" } },
  minimal:   { fam: "helvetica", align: "left",   name: { size: 25 },                         title: { c: "gray" },                     contact: { bar: "none", sep: "   ·   " },head: { rule: "none",        c: "gray",   upper: true,  size: 10,   sp: 3   }, comp: { c: "ink" } },
  executive: { fam: "times",     align: "center", name: { size: 23, upper: true, sp: 2 },      title: { c: "accent", upper: true, size: 10.5, sp: 2 }, contact: { bar: "none", sep: "  •  " }, head: { rule: "shortAccent", c: "ink", upper: false, size: 13 }, comp: { c: "ink", italic: true } },
  technical: { fam: "courier",   align: "left",   name: { size: 21, bold: true },             title: { c: "accent" },                   contact: { bar: "none", sep: "  /  " }, head: { rule: "block",       c: "ink",    upper: true,  size: 11,   sp: .8  }, comp: { c: "accent" } },
  academic:  { fam: "times",     align: "center", name: { size: 22, upper: true, sp: 2.6 },    title: { c: "sub" },                      contact: { bar: "none", sep: "   ·   " },head: { rule: "full",        c: "ink",    upper: false, size: 13 }, comp: { c: "ink", italic: true } },
  cobalt:    { fam: "helvetica", align: "left",   name: { size: 24, bold: true, upper: true, split: true }, title: { c: "sub" },        contact: { bar: "dark", sep: "   |   " },head: { rule: "accent",      c: "ink",    upper: false, size: 13 }, comp: { c: "accent" } },
  slab:      { fam: "times",     align: "left",   name: { size: 25, bold: true, upper: true }, title: { c: "accent", bold: true },       contact: { bar: "none", sep: "  •  " }, head: { rule: "thick",       c: "ink",    upper: true,  size: 13,   sp: 1   }, comp: { c: "accent" } },
  indigo:    { fam: "helvetica", align: "left",   name: { size: 24 },                         title: { c: "gray" },                     contact: { bar: "none", sep: "   ·   " },head: { rule: "none",        c: "gray",   upper: true,  size: 10.5, sp: 3   }, comp: { c: "accent" } },
  novo:      { fam: "helvetica", align: "left",   name: { size: 25, bold: true },             title: { c: "accent" },                   contact: { bar: "dark", sep: "  •  " }, head: { rule: "accent",      c: "accent", upper: true,  size: 11.5, sp: 1   }, comp: { c: "accent" } },
  slate:     { fam: "helvetica", align: "left",   name: { size: 25, bold: true },             title: { c: "sub" },                      contact: { bar: "dark", sep: "   |   " },head: { rule: "none",        c: "accent", upper: true,  size: 12,   sp: 1.5 }, comp: { c: "accent" } },
  timeline:  { fam: "helvetica", align: "left",   name: { size: 23, bold: true, upper: true, color: "accent" }, title: { c: "accent" }, contact: { bar: "none", sep: "  •  " }, head: { rule: "none",        c: "accent", upper: true,  size: 12,   sp: 1   }, comp: { c: "accent" } },
  sidebar:   { fam: "helvetica", align: "left",   twocol: "left",  name: { size: 24, bold: true, upper: true }, title: { c: "accent" },                   contact: { bar: "none", sep: "   ·   " },head: { rule: "full",        c: "gray",   upper: true,  size: 11,   sp: 1.5 }, comp: { c: "accent" } },
  split:     { fam: "helvetica", align: "left",   twocol: "right", name: { size: 24, bold: true, upper: true, color: "ink" }, title: { c: "accent" },     contact: { bar: "accent", sep: "   ·   " },head: { rule: "full",      c: "gray",   upper: true,  size: 11,   sp: 1.5 }, comp: { c: "accent" } },
};
function getPdfStyle(t) { return PDF_STYLES[t] || PDF_STYLES.modern; }

async function downloadResumePDF(filename, payload) {
  const ok = await ensureJsPDF();
  if (!ok || !payload) { window.print(); return false; }

  const { data = {}, template = "modern", accent = "#1C9BE6" } = payload;
  const PR = getPdfStyle(template);
  const FAM = PR.fam;
  const centered = PR.align === "center";
  const serif = FAM === "times";

  const { jsPDF } = window.jspdf;
  const pdf = new jsPDF({ unit: "pt", format: "letter", compress: !(payload.__noCompress) });

  // Standard PDF fonts (Helvetica/Times) only encode a limited Latin set —
  // normalise Unicode dashes/quotes/ellipsis so nothing silently drops out.
  const sanitize = (s) => String(s)
    .replace(/[\u2012-\u2015\u2212]/g, "-")
    .replace(/[\u2018\u2019\u201B]/g, "'")
    .replace(/[\u201C\u201D]/g, '"')
    .replace(/\u2026/g, "...")
    .replace(/\u00A0/g, " ")
    .replace(/[\u2022\u25CF\u25E6\u2023]/g, "\u2022");
  const __text = pdf.text.bind(pdf);
  pdf.text = (str, ...rest) => __text(typeof str === "string" ? sanitize(str) : str, ...rest);
  const __split = pdf.splitTextToSize.bind(pdf);
  pdf.splitTextToSize = (str, ...rest) => __split(sanitize(str == null ? "" : str), ...rest);
  const PW = 612, PH = 792;
  const MX = 52, MT = 50, MB = 48;
  const CW = PW - MX * 2;
  const L = MX, R = PW - MX, MID = PW / 2;

  const AC = hexToRgb(accent);
  const INK = [26, 28, 36];
  const SUB = [70, 76, 92];
  const META = [122, 128, 150];
  const RULE = [205, 210, 220];
  const COL = { ink: INK, sub: SUB, gray: META, accent: AC, white: [255, 255, 255] };
  const col = (k) => COL[k] || INK;
  const charsp = (sp) => pdf.setCharSpace(sp || 0);

  let y = MT;

  const setF = (style, size) => { pdf.setFont(FAM, style); pdf.setFontSize(size); };
  const setC = (rgb) => pdf.setTextColor(rgb[0], rgb[1], rgb[2]);
  const bulletDot = () => { pdf.setFillColor(META[0], META[1], META[2]); pdf.circle(L + 4, y + 5.4, 1.5, "F"); };
  const space = (h) => { if (y + h > PH - MB) { pdf.addPage(); y = MT; return true; } return false; };

  // draw wrapped text starting at top y (baseline-managed), return block height
  function lines(text, { size = 10, style = "normal", color = INK, x = L, w = CW, align = "left", lh = 1.34, gapAfter = 0 } = {}) {
    setF(style, size); setC(color);
    const arr = pdf.splitTextToSize(String(text == null ? "" : text), w);
    const step = size * lh;
    space(step); // make sure at least first line fits
    for (const ln of arr) {
      if (y + step > PH - MB) { pdf.addPage(); y = MT; }
      const bx = align === "center" ? MID : align === "right" ? x : x;
      pdf.text(ln, bx, y + size * 0.86, { align, maxWidth: w });
      y += step;
    }
    y += gapAfter;
    return arr.length * step + gapAfter;
  }

  // measure (no draw) wrapped line count for keeping blocks together
  function measure(text, size, w) {
    setF("normal", size);
    return pdf.splitTextToSize(String(text || ""), w).length * size * 1.34;
  }

  function rule(color = RULE, gapBefore = 2, gapAfter = 8) {
    y += gapBefore;
    pdf.setDrawColor(color[0], color[1], color[2]); pdf.setLineWidth(0.7);
    pdf.line(L, y, R, y);
    y += gapAfter;
  }

  function sectionHead(label) {
    const H = PR.head;
    const fs = H.size || 11.5;
    const need = fs * 1.34 + 14 + 14;
    if (y + need > PH - MB) { pdf.addPage(); y = MT; }
    y += 14;
    const txt = H.upper ? label.toUpperCase() : label;
    const headCol = col(H.c);

    if (H.rule === "block") {
      // accent bar to the left of the heading
      pdf.setFillColor(AC[0], AC[1], AC[2]);
      pdf.rect(L, y + 1, 3.5, fs, "F");
      setF("bold", fs); setC(headCol); charsp(H.sp);
      pdf.text(txt, L + 11, y + fs * 0.86); charsp(0);
      y += fs * 1.34 + 8;
      return;
    }

    setF("bold", fs); setC(headCol); charsp(H.sp);
    pdf.text(txt, centered ? MID : L, y + fs * 0.86, { align: centered ? "center" : "left" });
    charsp(0);
    y += fs * 1.34;

    if (H.rule === "full")   { rule(RULE, 2, 9); }
    else if (H.rule === "accent") { rule(AC, 2, 9); }
    else if (H.rule === "thick")  { y += 2; pdf.setDrawColor(28, 31, 42); pdf.setLineWidth(1.8); pdf.line(L, y, R, y); y += 9; }
    else if (H.rule === "shortAccent") {
      y += 3; pdf.setDrawColor(AC[0], AC[1], AC[2]); pdf.setLineWidth(2);
      if (centered) pdf.line(MID - 24, y, MID + 24, y); else pdf.line(L, y, L + 48, y);
      y += 10;
    } else { y += 8; } // none
  }

  // ---------- Header ----------
  const b = data.basics || {};

  // ===== Two-column layout (Sidebar / Split) — matches the on-screen preview =====
  function renderTwoCol() {
    const GAP = 22;
    const sideW = 168, mainW = CW - sideW - GAP;
    let sideX, mainX;
    if (PR.twocol === "left") { sideX = L; mainX = L + sideW + GAP; }
    else { mainX = L; sideX = L + mainW + GAP; }

    const exp = data.experience || [], edu = data.education || [], projects = data.projects || [];
    const skillItems = (data.skills || []).flatMap((g) => g.items || []);
    const certs = (data.certs || []).filter(Boolean);

    function ensurePage(p) { while (pdf.getNumberOfPages() < p) pdf.addPage(); }
    function flow(items, x, w, startPage, startY) {
      let page = startPage, yy = startY;
      for (const it of items) {
        const h = it(x, 0, w, true);
        if (yy + h > PH - MB) { page++; ensurePage(page); yy = MT; }
        pdf.setPage(page);
        it(x, yy, w, false);
        yy += h;
      }
      return { page, y: yy };
    }

    // ---- header (full width) ----
    function header() {
      let yy = MT;
      const photo = b.photo;
      const txtW = photo ? CW - 90 : CW;
      const nm = ((PR.name.upper ? (b.name || "").toUpperCase() : b.name) || "Your Name");
      setF("bold", 25); setC(PR.name.color === "accent" ? AC : INK); charsp(PR.name.sp);
      pdf.splitTextToSize(nm, txtW).forEach((ln) => { pdf.text(ln, L, yy + 22); yy += 27; });
      charsp(0);
      if (b.title) { setF("normal", 13); setC(AC); pdf.text(String(b.title), L, yy + 6, { maxWidth: txtW }); yy += 20; }
      const contact = [b.phone, b.email, b.linkedin, b.website, b.location].filter(Boolean);
      if (contact.length) {
        setF("normal", 9); setC(SUB);
        pdf.splitTextToSize(contact.join("     "), txtW).forEach((ln) => { pdf.text(ln, L, yy + 7); yy += 12.5; });
      }
      if (photo) {
        try {
          const m = /^data:image\/(\w+)/.exec(photo); let f = m ? m[1].toUpperCase() : "PNG"; if (f === "JPG") f = "JPEG";
          pdf.addImage(photo, f, R - 76, MT, 76, 76);
        } catch (e) {}
      }
      yy += 12;
      pdf.setDrawColor(60, 64, 78); pdf.setLineWidth(0.8); pdf.line(L, yy, R, yy);
      return yy + 16;
    }

    // ---- block builders: (x, yy, w, dry) => height ----
    const headBlk = (label) => (x, yy, w, dry) => {
      const fs = 11; let yc = yy + 10;
      setF("bold", fs); setC(META); charsp(1);
      if (!dry) pdf.text((label || "").toUpperCase(), x, yc + fs * 0.82);
      charsp(0); yc += fs * 1.2;
      if (!dry) { pdf.setDrawColor(RULE[0], RULE[1], RULE[2]); pdf.setLineWidth(0.7); pdf.line(x, yc + 2, x + w, yc + 2); }
      return (yc + 12) - yy;
    };
    const paraBlk = (text, o = {}) => (x, yy, w, dry) => {
      const size = o.size || 9.6; setF(o.style || "normal", size); setC(o.color || SUB);
      let yc = yy; pdf.splitTextToSize(String(text || ""), w).forEach((ln) => { if (!dry) pdf.text(ln, x, yc + size * 0.84); yc += size * 1.34; });
      return (yc - yy) + (o.gap || 0);
    };
    const bulletBlk = (text) => (x, yy, w, dry) => {
      const size = 9.3; setF("normal", size); setC(SUB);
      if (!dry) { pdf.setFillColor(META[0], META[1], META[2]); pdf.circle(x + 2.5, yy + 5, 1.3, "F"); }
      let yc = yy; pdf.splitTextToSize(String(text || ""), w - 12).forEach((ln) => { if (!dry) pdf.text(ln, x + 12, yc + size * 0.82); yc += size * 1.34; });
      return (yc - yy) + 2;
    };
    const entryBlk = (e) => (x, yy, w, dry) => {
      let yc = yy;
      setF("bold", 11); setC(INK);
      pdf.splitTextToSize(String(e.title || ""), w).forEach((ln) => { if (!dry) pdf.text(ln, x, yc + 11 * 0.86); yc += 11 * 1.28; });
      if (e.org) { setF(serif ? "normal" : "bold", 9.8); setC(col(PR.comp.c)); pdf.splitTextToSize(String(e.org), w).forEach((ln) => { if (!dry) pdf.text(ln, x, yc + 9.8 * 0.86); yc += 9.8 * 1.25; }); }
      const meta = [e.dateR, e.locR].filter(Boolean).join("   ·   ");
      if (meta) { setF("normal", 8.8); setC(META); pdf.splitTextToSize(meta, w).forEach((ln) => { if (!dry) pdf.text(ln, x, yc + 8.8 * 0.86); yc += 8.8 * 1.25; }); }
      if (e.detail) { setF("normal", 9.4); setC(SUB); pdf.splitTextToSize(String(e.detail), w).forEach((ln) => { if (!dry) pdf.text(ln, x, yc + 9.4 * 0.84); yc += 9.4 * 1.3; }); }
      yc += 3;
      (e.bullets || []).filter(Boolean).forEach((bt) => {
        const size = 9.3; setF("normal", size); setC(SUB);
        if (!dry) { pdf.setFillColor(META[0], META[1], META[2]); pdf.circle(x + 2.5, yc + 5, 1.3, "F"); }
        pdf.splitTextToSize(String(bt), w - 12).forEach((ln) => { if (!dry) pdf.text(ln, x + 12, yc + size * 0.82); yc += size * 1.34; });
      });
      return (yc - yy) + 9;
    };
    const skillBlk = (s) => (x, yy, w, dry) => {
      const size = 9.4; setF("normal", size); setC(INK);
      let yc = yy + 2; pdf.splitTextToSize(String(s), w).forEach((ln) => { if (!dry) pdf.text(ln, x, yc + size * 0.82); yc += size * 1.5; });
      if (!dry) { pdf.setDrawColor(236, 238, 242); pdf.setLineWidth(0.5); pdf.line(x, yc - 3, x + w, yc - 3); }
      return yc - yy;
    };
    const certBlk = (c) => (x, yy, w, dry) => {
      const size = 9.4; setF("normal", size); setC(AC);
      let yc = yy; pdf.splitTextToSize(String(c), w).forEach((ln) => { if (!dry) pdf.text(ln, x, yc + size * 0.82); yc += size * 1.32; });
      return (yc - yy) + 7;
    };
    const eduBlk = (e) => (x, yy, w, dry) => {
      let yc = yy;
      setF("bold", 10); setC(INK);
      pdf.splitTextToSize(String(e.degree || e.school || ""), w).forEach((ln) => { if (!dry) pdf.text(ln, x, yc + 10 * 0.86); yc += 10 * 1.25; });
      if (e.degree && e.school) { setF("normal", 9.4); setC(AC); pdf.splitTextToSize(String(e.school), w).forEach((ln) => { if (!dry) pdf.text(ln, x, yc + 9.4 * 0.84); yc += 9.4 * 1.25; }); }
      const meta = [[e.start, e.end].filter(Boolean).join(" - "), e.location].filter(Boolean).join("  ·  ");
      if (meta) { setF("normal", 8.8); setC(META); if (!dry) pdf.text(meta, x, yc + 8.8 * 0.84); yc += 8.8 * 1.3; }
      return (yc - yy) + 8;
    };

    // ---- assemble columns (matches preview section order) ----
    const mainItems = [];
    if (data.summary) { mainItems.push(headBlk("Summary"), paraBlk(data.summary, { gap: 4 })); }
    if (exp.length) { mainItems.push(headBlk("Experience")); exp.forEach((x) => mainItems.push(entryBlk({ title: x.role, org: x.company, dateR: [x.start, x.end].filter(Boolean).join(" - "), locR: x.location, bullets: x.bullets }))); }
    if (projects.length) { mainItems.push(headBlk("Projects")); projects.forEach((p) => mainItems.push(entryBlk({ title: p.name, org: p.tech, detail: p.detail, bullets: p.bullets }))); }

    const sideItems = [];
    if (skillItems.length) { sideItems.push(headBlk("Skills")); skillItems.forEach((s) => sideItems.push(skillBlk(s))); }
    if (certs.length) { sideItems.push(headBlk("Training & Courses")); certs.forEach((c) => sideItems.push(certBlk(c))); }
    if (edu.length) { sideItems.push(headBlk("Education")); edu.forEach((e) => sideItems.push(eduBlk(e))); }
    (data.custom || []).forEach((cs) => {
      const blocks = htmlToBlocks(cs.body);
      if (!(cs.heading && cs.heading.trim()) && !blocks.length) return;
      sideItems.push(headBlk(cs.heading || "Section"));
      blocks.forEach((bk) => sideItems.push(bk.type === "bullet" ? bulletBlk(bk.text) : paraBlk(bk.text, { size: 9.4, gap: 3 })));
    });

    const hb = header();
    flow(mainItems, mainX, mainW, 1, hb);
    flow(sideItems, sideX, sideW, 1, hb);
  }

  if (PR.twocol) {
    renderTwoCol();
  } else {
  const N = PR.name, T = PR.title;
  const nameRaw = String(b.name || "Your Name");
  const nameTxt = N.upper ? nameRaw.toUpperCase() : nameRaw;
  const nameStyle = N.bold ? "bold" : "normal";
  setF(nameStyle, N.size); charsp(N.sp);

  if (centered) {
    setC(col(N.color || "ink"));
    pdf.text(nameTxt, MID, y + N.size * 0.86, { align: "center" });
  } else if (N.split) {
    // two-tone: last word in accent (Cobalt)
    const parts = nameTxt.split(" ");
    const last = parts.length > 1 ? parts.pop() : "";
    const first = parts.join(" ");
    setC(INK); pdf.text(first, L, y + N.size * 0.86);
    if (last) {
      const fw = pdf.getTextWidth(first + " ");
      setC(AC); pdf.text(last, L + fw, y + N.size * 0.86);
    }
  } else {
    setC(col(N.color || "ink"));
    pdf.text(nameTxt, L, y + N.size * 0.86);
  }
  charsp(0);
  y += N.size * 1.18 + 4;

  if (b.title) {
    const ts = T.size || 12.5;
    setF(T.bold ? "bold" : (T.italic ? "italic" : "normal"), ts); charsp(T.sp);
    setC(col(T.c === "accent" ? "accent" : T.c === "gray" ? "gray" : "sub"));
    const tt = T.upper ? String(b.title).toUpperCase() : String(b.title);
    pdf.text(tt, centered ? MID : L, y + ts * 0.82, { align: centered ? "center" : "left" });
    charsp(0);
    y += ts * 1.4;
  }

  // ---------- Contact ----------
  const contact = [b.phone, b.email, b.location, b.website, b.linkedin].filter(Boolean);
  const sep = PR.contact.sep || "   •   ";
  if (contact.length) {
    const joined = contact.join(sep);
    if (PR.contact.bar === "dark" || PR.contact.bar === "accent") {
      const fill = PR.contact.bar === "accent" ? AC : [24, 27, 38];
      const barH = 23;
      y += 8;
      pdf.setFillColor(fill[0], fill[1], fill[2]);
      pdf.rect(0, y, PW, barH, "F"); // full-bleed bar
      setF("normal", 9.4); setC([255, 255, 255]);
      const one = pdf.splitTextToSize(joined, CW)[0] || joined;
      pdf.text(one, L, y + barH / 2 + 3.2);
      y += barH + 12;
    } else {
      setF("normal", 9.6); setC(SUB);
      const cl = pdf.splitTextToSize(joined, CW);
      for (const ln of cl) { pdf.text(ln, centered ? MID : L, y + 8, { align: centered ? "center" : "left" }); y += 13.5; }
      y += 4;
      rule([60, 64, 78], 4, 6);
    }
  } else {
    y += 2;
  }

  // ---------- Summary ----------
  if (data.summary) {
    sectionHead(centered ? "Profile" : "Summary");
    lines(data.summary, { size: 10, color: SUB, gapAfter: 2 });
  }

  // ---------- Experience ----------
  const drawEntry = ({ title, org, dateR, locR, detail, bullets }) => {
    // keep the title+org+first bullet together
    let need = 12 * 1.34 + 11 * 1.34 + 6;
    if (bullets && bullets[0]) need += measure("•  " + bullets[0], 10, CW - 14);
    if (y + need > PH - MB) { pdf.addPage(); y = MT; }

    setF("bold", 11.5); setC(INK);
    const dW = dateR ? pdf.getTextWidth(dateR) + 8 : 0;
    pdf.text(String(title || ""), L, y + 10, { maxWidth: CW - dW });
    if (dateR) { setF("normal", 9.6); setC(META); pdf.text(String(dateR), R, y + 10, { align: "right" }); }
    y += 16;
    if (org || locR) {
      const compStyle = PR.comp.italic ? "italic" : (serif ? "normal" : "bold");
      setF(compStyle, 10); setC(col(PR.comp.c));
      const lW = locR ? pdf.getTextWidth(locR) + 8 : 0;
      if (org) pdf.text(String(org), L, y + 8, { maxWidth: CW - lW });
      if (locR) { setF("normal", 9.6); setC(META); pdf.text(String(locR), R, y + 8, { align: "right" }); }
      y += 14;
    }
    if (detail) lines(detail, { size: 10, color: SUB, gapAfter: 1 });
    (bullets || []).filter(Boolean).forEach((bt) => {
      setF("normal", 10); setC(SUB);
      const wrap = pdf.splitTextToSize(String(bt), CW - 14);
      const step = 10 * 1.36;
      if (y + step > PH - MB) { pdf.addPage(); y = MT; }
      bulletDot(); setC(SUB);
      wrap.forEach((ln, i) => {
        if (y + step > PH - MB) { pdf.addPage(); y = MT; }
        pdf.text(ln, L + 14, y + 8.6); y += step;
      });
    });
    y += 8;
  };

  if ((data.experience || []).length) {
    sectionHead("Experience");
    data.experience.forEach((x) => drawEntry({
      title: x.role, org: x.company, dateR: [x.start, x.end].filter(Boolean).join(" - "),
      locR: x.location, bullets: x.bullets,
    }));
  }

  if ((data.projects || []).length) {
    sectionHead("Projects");
    data.projects.forEach((p) => drawEntry({
      title: p.name, org: p.tech, detail: p.detail, bullets: p.bullets,
    }));
  }

  if ((data.education || []).length) {
    sectionHead("Education");
    data.education.forEach((e) => drawEntry({
      title: e.degree || e.school, org: e.degree ? e.school : "",
      dateR: [e.start, e.end].filter(Boolean).join(" - "), locR: e.location,
      detail: e.detail,
    }));
  }

  if ((data.skills || []).length) {
    sectionHead("Skills");
    data.skills.forEach((g) => {
      const items = (g.items || []).join(", ");
      if (!items && !g.group) return;
      const step = 10 * 1.4;
      if (y + step > PH - MB) { pdf.addPage(); y = MT; }
      if (g.group) {
        setF("bold", 10); setC(INK);
        const gl = g.group + ":  ";
        pdf.text(gl, L, y + 8.6);
        const gw = pdf.getTextWidth(gl);
        setF("normal", 10); setC(SUB);
        const wrap = pdf.splitTextToSize(items, CW - gw);
        wrap.forEach((ln, i) => {
          if (i > 0 && y + step > PH - MB) { pdf.addPage(); y = MT; }
          pdf.text(ln, i === 0 ? L + gw : L, y + 8.6); y += step;
        });
      } else {
        lines(items, { size: 10, color: SUB });
      }
    });
    y += 2;
  }

  if ((data.certs || []).filter(Boolean).length) {
    sectionHead("Certifications");
    data.certs.filter(Boolean).forEach((c) => {
      const step = 10 * 1.36;
      if (y + step > PH - MB) { pdf.addPage(); y = MT; }
      setF("normal", 10); setC(SUB);
      bulletDot();
      const wrap = pdf.splitTextToSize(String(c), CW - 14);
      wrap.forEach((ln) => { if (y + step > PH - MB) { pdf.addPage(); y = MT; } pdf.text(ln, L + 14, y + 8.6); y += step; });
    });
  }

  (data.custom || []).forEach((cs) => {
    const blocks = htmlToBlocks(cs.body);
    if (!(cs.heading && cs.heading.trim()) && !blocks.length) return;
    sectionHead(cs.heading || "Section");
    blocks.forEach((bk) => {
      if (bk.type === "bullet") {
        const step = 10 * 1.36;
        if (y + step > PH - MB) { pdf.addPage(); y = MT; }
        setF("normal", 10); setC(SUB);
        bulletDot();
        const wrap = pdf.splitTextToSize(bk.text, CW - 14);
        wrap.forEach((ln) => { if (y + step > PH - MB) { pdf.addPage(); y = MT; } pdf.text(ln, L + 14, y + 8.6); y += step; });
      } else {
        lines(bk.text, { size: 10, color: SUB, gapAfter: 3 });
      }
    });
  });

  } // end single-column body

  // PDF metadata (helps ATS + nice file properties)
  try {
    pdf.setProperties({ title: (b.name ? b.name + " — Résumé" : "Résumé"), author: b.name || "", creator: "BDRecruit Resume Studio" });
  } catch (e) {}

  if (payload.__returnDoc) return pdf;

  pdf.save((filename || b.name || "resume").replace(/[\\/:*?"<>|]+/g, "").trim() + ".pdf");
  return true;
}

async function exportExactPDF(filename) {
  const el = document.getElementById("resume-page");
  if (!el) { alert("Resume element not found"); return false; }

  if (!window.html2canvas) { alert("html2canvas not loaded yet, try again in a moment."); return false; }
  const jsPDFOk = await ensureJsPDF();
  if (!jsPDFOk) { alert("jsPDF not loaded"); return false; }

  // Temporarily collapse min-height so the canvas matches actual content,
  // not the 1056px minimum that causes a blank half-page at the bottom.
  const prevMinH = el.style.minHeight;
  const prevH    = el.style.height;
  el.style.minHeight = "0";
  el.style.height    = "auto";

  let canvas;
  try {
    canvas = await window.html2canvas(el, {
      scale: 2,
      useCORS: true,
      allowTaint: true,   // needed for accurate background-color rendering
      backgroundColor: "#ffffff",
      logging: false,
      imageTimeout: 0,
    });
  } finally {
    el.style.minHeight = prevMinH;
    el.style.height    = prevH;
  }

  const { jsPDF } = window.jspdf;
  const pdf = new jsPDF({ unit: "pt", format: "letter", orientation: "portrait" });
  const PW = 612, PH = 792;

  // pixels-per-point ratio based on captured width
  const pxPerPt = canvas.width / PW;
  const pageHpx = Math.round(PH * pxPerPt);
  const totalH  = canvas.height;
  const ctx2    = canvas.getContext("2d");

  // Scan backward from targetY to find a clean all-white row so we never
  // slice through a text line. Search up to ~60pt worth of pixels.
  const findCleanSplit = (targetY) => {
    const searchPx = Math.round(60 * pxPerPt);
    for (let d = 0; d <= searchPx; d++) {
      const y = targetY - d;
      if (y <= 0) break;
      const { data } = ctx2.getImageData(0, y, canvas.width, 1);
      let clean = true;
      for (let i = 0; i < data.length; i += 4) {
        if (data[i] < 245 || data[i + 1] < 245 || data[i + 2] < 245) { clean = false; break; }
      }
      if (clean) return y;
    }
    return targetY; // fallback — split anyway
  };

  let yPx = 0;
  while (yPx < totalH) {
    if (yPx > 0) pdf.addPage();
    let splitAt = Math.min(yPx + pageHpx, totalH);
    if (splitAt < totalH) splitAt = findCleanSplit(splitAt);
    const sliceH = splitAt - yPx;
    const slice  = document.createElement("canvas");
    slice.width  = canvas.width;
    slice.height = sliceH;
    slice.getContext("2d").drawImage(canvas, 0, yPx, canvas.width, sliceH, 0, 0, canvas.width, sliceH);
    const imgData = slice.toDataURL("image/jpeg", 0.97);
    pdf.addImage(imgData, "JPEG", 0, 0, PW, sliceH / pxPerPt);
    yPx = splitAt;
  }

  const safe = (filename || "resume").replace(/[\\/:*?"<>|]+/g, "").trim();
  pdf.save(safe + ".pdf");
  return true;
}

window.downloadResumePDF = downloadResumePDF;
window.exportExactPDF    = exportExactPDF;
window.loadPdfLibs = loadPdfLibs;
