// Main resume tool app
const { useState, useEffect, useRef, useMemo } = React;

const STYLE_OPTIONS = [
  { id: 'editorial-minimal', name: 'Editorial Minimal', desc: 'Serif name, wide margins, warm sand. Best for Creative, Healthcare, Academic.' },
  { id: 'structured-corporate', name: 'Structured Corporate', desc: 'High-contrast header, bold labels. Best for Business, Finance, Legal.' },
  { id: 'technical-clean', name: 'Technical Clean', desc: 'Mono accents, sidebar pills. Best for Tech, Data, Product.' },
  { id: 'bold-creative', name: 'Bold Creative', desc: 'Full-bleed accent header, display name. Best for Designers, Filmmakers.' },
  { id: 'classic-professional', name: 'Classic Professional', desc: 'Two-column, divider lines, timeless. Works anywhere.' }
];

const FONT_OPTIONS = [
  { id: 'modern-sans', name: 'Modern Sans', sample: 'Montserrat + Lato' },
  { id: 'classic-serif', name: 'Classic Serif', sample: 'Garamond + Source Sans' },
  { id: 'editorial-mix', name: 'Editorial Mix', sample: 'Playfair + Inter' },
  { id: 'technical-mono', name: 'Technical Mono', sample: 'Space Grotesk + JetBrains Mono' }
];

const PROFESSIONS = ['Healthcare', 'Creative & Media', 'Tech & Engineering', 'Business & Finance', 'Legal & Academic', 'Other'];

const ACCENT_PRESETS = [
  { name: 'Warm sand', hex: '#b08a4f' },
  { name: 'Terracotta', hex: '#b14a2e' },
  { name: 'Forest', hex: '#2e5d4a' },
  { name: 'Navy', hex: '#1f3a5f' },
  { name: 'Slate', hex: '#4a4f55' },
  { name: 'Plum', hex: '#5a2e4a' },
  { name: 'Ink', hex: '#101521' },
  { name: 'Rust gold', hex: '#9a6a1f' }
];

const DEFAULT_SECTION_ORDER = ['summary', 'experience', 'education', 'skills', 'certifications', 'awards', 'development', 'volunteer'];

const STYLE_RECOMMENDATIONS = {
  'Healthcare': 'editorial-minimal',
  'Creative & Media': 'bold-creative',
  'Tech & Engineering': 'technical-clean',
  'Business & Finance': 'structured-corporate',
  'Legal & Academic': 'classic-professional'
};

function App() {
  const [raw, setRaw] = useState('');
  const [uploadStatus, setUploadStatus] = useState('');
  const [viewAll, setViewAll] = useState(false);
  const [profession, setProfession] = useState('Healthcare');
  const [otherProfession, setOtherProfession] = useState('');
  const [style, setStyle] = useState('bold-creative');
  const [accent, setAccent] = useState('#b14a2e');
  const [font, setFont] = useState('editorial-mix');
  const [pageSize, setPageSize] = useState('A4');
  const [printBleed, setPrintBleed] = useState(() => {
    const saved = parseFloat(localStorage.getItem('yessigh-print-bleed'));
    return Number.isFinite(saved) && saved >= 0 && saved <= 20 ? saved : 0;
  });
  const [bleedApplyToExport, setBleedApplyToExport] = useState(() => {
    const saved = localStorage.getItem('yessigh-bleed-export');
    return saved == null ? true : saved === '1';
  });
  useEffect(() => { localStorage.setItem('yessigh-bleed-export', bleedApplyToExport ? '1' : '0'); }, [bleedApplyToExport]);
  // Collision behavior for free-positioned sections: 'warn' (highlight overlap),
  // 'push' (shove other free sections out of the way), or 'off'.
  const [collisionMode, setCollisionMode] = useState(() => {
    const v = localStorage.getItem('yessigh-collision-mode');
    return v === 'push' || v === 'off' ? v : 'warn';
  });
  useEffect(() => { localStorage.setItem('yessigh-collision-mode', collisionMode); }, [collisionMode]);
  // Page footer (small name + "Page X of Y") visibility. On by default.
  const [showPageFooter, setShowPageFooter] = useState(() => {
    const v = localStorage.getItem('yessigh-page-footer');
    return v == null ? true : v === '1';
  });
  useEffect(() => { localStorage.setItem('yessigh-page-footer', showPageFooter ? '1' : '0'); }, [showPageFooter]);
  // Free-positioning: allow dragging into the absolute page corners, or
  // constrain to the printable safe area. 'corners' = full freedom (default).
  const [freeEdgeMode, setFreeEdgeMode] = useState(() => {
    const v = localStorage.getItem('yessigh-free-edge-mode');
    return v === 'safe' ? 'safe' : 'corners';
  });
  useEffect(() => { localStorage.setItem('yessigh-free-edge-mode', freeEdgeMode); }, [freeEdgeMode]);
  // Continuation header (page 2+ slim header with name + page count). On by default.
  const [showContinuationHeader, setShowContinuationHeader] = useState(() => {
    const v = localStorage.getItem('yessigh-cont-header');
    return v == null ? true : v === '1';
  });
  useEffect(() => { localStorage.setItem('yessigh-cont-header', showContinuationHeader ? '1' : '0'); }, [showContinuationHeader]);
  const [photo, setPhoto] = useState(null);
  const [photoShape, setPhotoShape] = useState('circle');
  const [photoPlacement, setPhotoPlacement] = useState('header');
  const [pages, setPages] = useState(1);
  const [activePage, setActivePage] = useState(1);
  const [motif, setMotif] = useState('pulse');
  const [motifWidth, setMotifWidth] = useState('compact');
  // Per-template layout persistence. Each template (style id) gets its own
  // saved { sectionOrder, enabled, sectionScale } so a layout customised for
  // "bold-creative" doesn't bleed into "editorial-minimal".
  const layoutKey = (s) => `yessigh-layout-v1::${s}`;
  const loadLayoutFor = (s) => {
    try {
      const raw = localStorage.getItem(layoutKey(s));
      if (!raw) return null;
      const j = JSON.parse(raw);
      return {
        sectionOrder: Array.isArray(j.sectionOrder) && j.sectionOrder.length ? j.sectionOrder : DEFAULT_SECTION_ORDER,
        enabled: new Set(Array.isArray(j.enabled) ? j.enabled : DEFAULT_SECTION_ORDER),
        sectionScale: j.sectionScale && typeof j.sectionScale === 'object' ? j.sectionScale : {},
        flushEdges: typeof j.flushEdges === 'boolean' ? j.flushEdges : false,
        printBleed: Number.isFinite(j.printBleed) ? j.printBleed : null,
        pagePlan: j.pagePlan && typeof j.pagePlan === 'object' ? j.pagePlan : {},
        freePos: j.freePos && typeof j.freePos === 'object' ? j.freePos : {},
      };
    } catch (_) { return null; }
  };
  const initialLayout = loadLayoutFor(style);
  const [sectionOrder, setSectionOrder] = useState(initialLayout?.sectionOrder || DEFAULT_SECTION_ORDER);
  const [enabled, setEnabled] = useState(initialLayout?.enabled || new Set(DEFAULT_SECTION_ORDER));
  const [editMode, setEditMode] = useState(false);
  const [flushEdges, setFlushEdges] = useState(initialLayout?.flushEdges ?? false);
  const [sectionScale, setSectionScale] = useState(initialLayout?.sectionScale || {}); // { [key]: 0.8..1.4 }
  // Per-template explicit page assignment for sections, e.g. { experience: 1, skills: 2 }.
  // Wins over the auto distribution heuristic when present.
  const [pagePlan, setPagePlan] = useState(initialLayout?.pagePlan || {});
  // Per-section absolute position on its sheet, expressed as percentages so it
  // survives page-size changes. Shape: { [key]: { page, leftPct, topPct, widthPct } }
  const [freePos, setFreePos] = useState(initialLayout?.freePos || {});

  // Persist whenever any per-template layout setting changes for the active template.
  useEffect(() => {
    try {
      localStorage.setItem(layoutKey(style), JSON.stringify({
        sectionOrder,
        enabled: Array.from(enabled),
        sectionScale,
        flushEdges,
        printBleed,
        pagePlan,
        freePos,
      }));
    } catch (_) {}
  }, [style, sectionOrder, enabled, sectionScale, flushEdges, printBleed, pagePlan, freePos]);

  // When the user switches templates, swap in that template's saved layout
  // (or fall back to defaults). Skip the very first render — initial state
  // already loaded the right layout above.
  const styleRef = useRef(style);
  useEffect(() => {
    if (styleRef.current === style) return;
    styleRef.current = style;
    const next = loadLayoutFor(style);
    if (next) {
      setSectionOrder(next.sectionOrder);
      setEnabled(next.enabled);
      setSectionScale(next.sectionScale);
      setFlushEdges(next.flushEdges);
      setPagePlan(next.pagePlan || {});
      setFreePos(next.freePos || {});
      if (next.printBleed != null) setPrintBleed(next.printBleed);
    } else {
      setSectionOrder(DEFAULT_SECTION_ORDER);
      setEnabled(new Set(DEFAULT_SECTION_ORDER));
      setSectionScale({});
      setFlushEdges(false);
      setPagePlan({});
      setFreePos({});
    }
  }, [style]);
  const [polishing, setPolishing] = useState(false);
  const [polishStatus, setPolishStatus] = useState('');
  const [overrideExperience, setOverrideExperience] = useState(null);
  const [pageInfo, setPageInfo] = useState({ pages: 1, fits: true });
  const [pagesLocked, setPagesLocked] = useState(() => localStorage.getItem('yessigh-pages-locked') === '1');
  useEffect(() => { localStorage.setItem('yessigh-pages-locked', pagesLocked ? '1' : '0'); }, [pagesLocked]);
  const [tab, setTab] = useState('input');
  const [editedCV, setEditedCV] = useState(null);
  const [zoom, setZoom] = useState(1);
  const [photoOffset, setPhotoOffset] = useState({ x: 0, y: 0 });
  const [panelW, setPanelW] = useState(() => {
    const saved = parseInt(localStorage.getItem('yessigh-panel-w'), 10);
    return Number.isFinite(saved) && saved > 280 && saved < 900 ? saved : 380;
  });
  const dragging = useRef(false);
  useEffect(() => {
    document.documentElement.style.setProperty('--panel-w', panelW + 'px');
    localStorage.setItem('yessigh-panel-w', String(panelW));
  }, [panelW]);
  const onResizerDown = (e) => {
    dragging.current = true;
    document.body.style.cursor = 'col-resize';
    document.body.style.userSelect = 'none';
    const onMove = (ev) => {
      if (!dragging.current) return;
      const x = ev.clientX;
      const next = Math.max(300, Math.min(900, x));
      setPanelW(next);
    };
    const onUp = () => {
      dragging.current = false;
      document.body.style.cursor = '';
      document.body.style.userSelect = '';
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
    };
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
  };

  const recommendedStyle = STYLE_RECOMMENDATIONS[profession];

  // When the profession chip is chosen, auto-switch the template + accent to
  // match. The user can still hand-pick a different template afterwards.
  const PROFESSION_ACCENTS = {
    'Healthcare': '#b14a2e',
    'Engineering & Tech': '#2e6cb1',
    'Design & Creative': '#7c4ab1',
    'Business & Finance': '#1f6b4d',
    'Legal & Academic': '#3a3a3a',
  };
  const lastAutoStyleRef = useRef(null);
  useEffect(() => {
    const rec = STYLE_RECOMMENDATIONS[profession];
    if (!rec) return;
    setStyle(rec);
    lastAutoStyleRef.current = rec;
    const a = PROFESSION_ACCENTS[profession];
    if (a) setAccent(a);
    // eslint-disable-next-line
  }, [profession]);

  const parsedFresh = useMemo(() => window.parseCV(raw), [raw]);
  const cv = useMemo(() => {
    if (editedCV) return editedCV;
    if (!overrideExperience) return parsedFresh;
    return { ...parsedFresh, experience: overrideExperience };
  }, [parsedFresh, overrideExperience, editedCV]);

  const opts = { style, accent, font, pageSize, sections: sectionOrder, enabled, photo, photoShape, photoPlacement, photoOffset, motif, motifWidth, pages, activePage, sectionScale, pagePlan, showPageFooter, showContinuationHeader };

  const toggleSection = (key) => {
    setEnabled(prev => {
      const n = new Set(prev);
      if (n.has(key)) n.delete(key); else n.add(key);
      return n;
    });
  };

  const dragKey = useRef(null);
  const onDragStart = (k) => () => { dragKey.current = k; };
  const onDragOver = (k) => (e) => {
    e.preventDefault();
    if (!dragKey.current || dragKey.current === k) return;
    setSectionOrder(prev => {
      const a = [...prev];
      const from = a.indexOf(dragKey.current);
      const to = a.indexOf(k);
      if (from < 0 || to < 0) return prev;
      a.splice(from, 1);
      a.splice(to, 0, dragKey.current);
      return a;
    });
  };

  // Apply print bleed as a CSS variable + persist. The variable feeds an
  // injected @page rule below so exports get the requested edge margin.
  useEffect(() => {
    document.documentElement.style.setProperty('--print-bleed', printBleed + 'mm');
    localStorage.setItem('yessigh-print-bleed', String(printBleed));
    let tag = document.getElementById('yessigh-print-bleed-style');
    if (!tag) {
      tag = document.createElement('style');
      tag.id = 'yessigh-print-bleed-style';
      document.head.appendChild(tag);
    }
    // When bleedApplyToExport is OFF, force a 0 export margin so the on-screen
    // bleed value never reaches the printed PDF.
    const exportMm = bleedApplyToExport ? printBleed : 0;
    // On-screen preview: paint the bleed as a visible inner inset on every
    // sheet so the margin choice is obvious before exporting.
    tag.textContent = `
      .sheet { box-shadow: 0 30px 80px -20px rgba(0,0,0,0.25), 0 8px 20px -10px rgba(0,0,0,0.2), inset 0 0 0 ${printBleed}mm rgba(177,74,46,0.06); }
      .sheet .rsx-root { padding: ${printBleed}mm; box-sizing: border-box; }
      @media print {
        @page { size: ${pageSize}; margin: ${exportMm}mm; }
        .sheet { box-shadow: none !important; }
        .sheet .rsx-root { padding: ${exportMm}mm !important; }
      }
    `;
  }, [printBleed, pageSize, bleedApplyToExport]);

  const print = () => {
    document.body.classList.add('printing');
    setTimeout(() => {
      window.print();
      setTimeout(() => document.body.classList.remove('printing'), 500);
    }, 60);
  };

  const polishBullets = async () => {
    if (!cv.experience.length) return;
    setPolishing(true);
    setPolishStatus('Polishing bullets…');
    try {
      const payload = cv.experience.map((r, i) => ({ i, title: r.title, company: r.company, bullets: r.bullets }));
      const prompt = `You are rewriting CV bullet points to be sharper and achievement-focused. For each role, rewrite each bullet to:
- Start with a strong action verb
- Quantify scale where possible (preserve any numbers; do not invent new ones)
- Remove buzzwords and filler
- Match a ${profession === 'Other' ? otherProfession : profession} tone
- Keep each bullet under 22 words

Input (JSON):
${JSON.stringify(payload)}

Return ONLY a JSON array with shape [{i, bullets: [string,...]}] in the same order. No prose, no markdown fences.`;
      const resp = await fetch('/api/ai-assist', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ prompt })
      });
      if (!resp.ok) {
        const j = await resp.json().catch(() => ({}));
        throw new Error(j.error || `Server error ${resp.status}`);
      }
      const { text: out } = await resp.json();
      let json = out.trim().replace(/^```(json)?/i, '').replace(/```$/, '').trim();
      const arr = JSON.parse(json);
      const next = cv.experience.map((r, i) => {
        const upd = arr.find(x => x.i === i);
        return upd ? { ...r, bullets: upd.bullets } : r;
      });
      setOverrideExperience(next);
      setPolishStatus('Bullets polished.');
    } catch (e) {
      console.error(e);
      setPolishStatus('Polish failed: ' + (e.message || 'parse error'));
    } finally {
      setPolishing(false);
      setTimeout(() => setPolishStatus(''), 4000);
    }
  };

  const resetPolish = () => { setOverrideExperience(null); setPolishStatus('Reverted.'); setTimeout(() => setPolishStatus(''), 2500); };

  const onPhoto = (e) => {
    const f = e.target.files?.[0];
    if (!f) return;
    const reader = new FileReader();
    reader.onload = () => setPhoto(reader.result);
    reader.readAsDataURL(f);
  };

  const pageRef = useRef(null);
  // Track an in-progress drag so we don't run pagination math (which can
  // briefly add/remove sheets and cause visible jumps or a flash of a
  // blank trailing page) while the user is actively dragging.
  const isDraggingRef = useRef(false);
  useEffect(() => {
    const onStart = () => { isDraggingRef.current = true; document.body.classList.add('rsx-dragging'); };
    const onEnd = () => { isDraggingRef.current = false; document.body.classList.remove('rsx-dragging'); };
    window.addEventListener('rsx:dragstart', onStart);
    window.addEventListener('rsx:dragend', onEnd);
    return () => {
      window.removeEventListener('rsx:dragstart', onStart);
      window.removeEventListener('rsx:dragend', onEnd);
    };
  }, []);

  useEffect(() => {
    const t = setTimeout(() => {
      if (isDraggingRef.current) return; // defer until drop completes
      if (pagesLocked) return; // user has pinned the page count
      const el = pageRef.current;
      if (!el) return;
      const sheets = el.querySelectorAll('.sheet');
      if (!sheets.length) return;
      const target = pageSize === 'A4' ? 1123 : 1056;
      let totalH = 0, anyOverflow = false;
      sheets.forEach(s => {
        const inner = s.querySelector('.rsx-root');
        const h = inner ? inner.scrollHeight : s.scrollHeight;
        totalH += h;
        if (h > target * 1.08) anyOverflow = true;
      });
      const needed = Math.max(1, Math.min(8, Math.ceil(totalH / target)));
      setPageInfo({ pages: needed, fits: needed === 1 });
      // Hysteresis: require a meaningful gap before shrinking to avoid
      // bouncing between page counts on near-boundary content.
      // Also: if the user has explicitly placed sections on later pages via
      // pagePlan, never shrink below the highest planned page.
      const planMax = Object.values(pagePlan || {}).reduce((m, v) => Math.max(m, Number(v) || 0), 0);
      if (anyOverflow && needed > pages) {
        setPages(needed);
      } else if (!anyOverflow && pages > needed && pages > 1 && (totalH < (pages - 1) * target * 0.95) && pages > planMax) {
        setPages(Math.max(needed, planMax || needed));
        if (activePage > needed) setActivePage(1);
      }
    }, 220);
    return () => clearTimeout(t);
    // eslint-disable-next-line
  }, [cv, style, accent, font, pageSize, sectionOrder, enabled, photo, pagesLocked]);

  const wordCount = useMemo(() => {
    const text = JSON.stringify(cv).replace(/[{}":,\[\]]/g, ' ');
    return text.split(/\s+/).filter(w => /[a-z]/i.test(w)).length;
  }, [cv]);

  return (
    <div className="app">
      <aside className="panel">
        <div className="brand">
          <div className="logo">
            <span style={{ background: accent }} />
            <span style={{ background: accent, opacity: 0.4 }} />
            <span style={{ background: accent, opacity: 0.15 }} />
          </div>
          <div>
            <div className="brandTitle">Yessigh / Resume</div>
            <div className="brandSub">Bespoke CV designer · client billing tool</div>
          </div>
        </div>

        {/* Persistent top toolbar — always visible across every tab */}
        <div className="topbar">
          <div className="row1">
            <div className="pageNav">
              {Array.from({length: pages}, (_, i) => i+1).map(n => (
                <button key={n} className={!viewAll && activePage === n ? 'pBtn on' : 'pBtn'} onClick={() => { setViewAll(false); setActivePage(n); }} title={`View page ${n}`}>{n}</button>
              ))}
              {pages > 1 && (
                <button className={viewAll ? 'pBtn on' : 'pBtn ghost'} onClick={() => setViewAll(v => !v)} title="View all pages stacked">All</button>
              )}
              <button className="pBtn ghost" onClick={() => { if (pages < 4) setPages(pages + 1); }} disabled={pages >= 4} title="Add page">+</button>
              {pages > 1 && (
                <button className="pBtn ghost" onClick={() => { const n = pages - 1; setPages(n); if (activePage > n) setActivePage(1); }} title="Remove last page">−</button>
              )}
              <button
                className={pagesLocked ? 'pBtn on' : 'pBtn ghost'}
                onClick={() => setPagesLocked(v => !v)}
                title={pagesLocked ? `Page count locked at ${pages} — auto-pagination paused` : 'Lock the page count so dragging never adds or removes pages'}
              >{pagesLocked ? '🔒' : '🔓'}</button>
            </div>
          </div>
          <div className="row2">
            <div className="miniSeg" title="Page size">
              <button className={pageSize === 'A4' ? 'on' : ''} onClick={() => setPageSize('A4')}>A4</button>
              <button className={pageSize === 'Letter' ? 'on' : ''} onClick={() => setPageSize('Letter')}>Letter</button>
            </div>
            <button className="exportBtn" onClick={print}>Export PDF</button>
          </div>
          <div className={pageInfo.fits ? 'pageMeta' : 'pageMeta bad'}>
            <span className="dot" /> Fits {pageInfo.pages} {pageInfo.pages === 1 ? 'page' : 'pages'} · {wordCount} words
          </div>
        </div>

        <div className="tabs">
          {['input', 'edit', 'design', 'sections'].map(t => (
            <button key={t} className={tab === t ? 'tab on' : 'tab'} onClick={() => setTab(t)}>
              {t === 'input' ? '01 · Paste' : t === 'edit' ? '02 · Edit' : t === 'design' ? '03 · Design' : '04 · Sections'}
            </button>
          ))}
        </div>

        {tab === 'input' && (
          <div className="tabBody">
            <Field label="Upload CV (PDF, DOCX, or TXT)">
              <label className="filePick">
                <input type="file" accept=".pdf,.docx,.txt,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/plain" onChange={async (e) => {
                  const f = e.target.files?.[0];
                  if (!f) return;
                  setUploadStatus('Reading file…');
                  try {
                    let text = '';
                    if (f.type === 'text/plain' || f.name.endsWith('.txt')) {
                      text = await f.text();
                    } else if (f.name.toLowerCase().endsWith('.pdf')) {
                      if (!window.pdfjsLib) {
                        await new Promise((res, rej) => {
                          const s = document.createElement('script');
                          s.src = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/legacy/build/pdf.min.js';
                          s.onload = res;
                          s.onerror = () => rej(new Error('Failed to load PDF reader from CDN'));
                          document.head.appendChild(s);
                        });
                        window.pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/legacy/build/pdf.worker.min.js';
                      }
                      const buf = await f.arrayBuffer();
                      const pdf = await window.pdfjsLib.getDocument({ data: buf }).promise;
                      const parts = [];
                      for (let i = 1; i <= pdf.numPages; i++) {
                        const page = await pdf.getPage(i);
                        parts.push(await window.extractPdfPageText(page));
                      }
                      text = parts.join('\n\n');
                    } else if (f.name.toLowerCase().endsWith('.docx')) {
                      if (!window.mammoth) {
                        await new Promise((res, rej) => {
                          const s = document.createElement('script');
                          s.src = 'https://cdn.jsdelivr.net/npm/mammoth@1.6.0/mammoth.browser.min.js';
                          s.onload = res; s.onerror = rej; document.head.appendChild(s);
                        });
                      }
                      const buf = await f.arrayBuffer();
                      const out = await window.mammoth.extractRawText({ arrayBuffer: buf });
                      text = out.value;
                    } else {
                      text = await f.text();
                    }
                     if (!text || text.trim().length < 20) throw new Error('Could not extract text from file');
                    // Cap to keep AI request well within edge timeout limits
                    const trimmed = text.length > 30000 ? text.slice(0, 30000) : text;
                    setUploadStatus('Parsing with AI… (this can take 20–40s)');
                    const ctrl = new AbortController();
                    const timer = setTimeout(() => ctrl.abort(), 90000);
                    let resp;
                    try {
                      resp = await fetch('/api/parse-cv', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ text: trimmed }),
                        signal: ctrl.signal,
                      });
                    } catch (netErr) {
                      throw new Error(netErr.name === 'AbortError' ? 'AI parser timed out. Try a shorter resume or paste the text directly.' : ('Network error: ' + netErr.message));
                    } finally {
                      clearTimeout(timer);
                    }
                    if (!resp.ok) {
                      const j = await resp.json().catch(() => ({}));
                      throw new Error(j.error || `Server error ${resp.status}`);
                    }
                    const { cv: parsedCV } = await resp.json();
                    // Normalize AI output to renderer-expected shape
                    const normalized = {
                      ...parsedCV,
                      experience: Array.isArray(parsedCV.experience) ? parsedCV.experience.map(r => ({
                        title: r.title || '', company: r.company || '', location: r.location || '', dates: r.dates || '',
                        bullets: Array.isArray(r.bullets) ? r.bullets : []
                      })) : [],
                      education: Array.isArray(parsedCV.education) ? parsedCV.education.map(e => ({
                        line: e.line || [e.degree, e.institution, e.dates].filter(Boolean).join(' · '),
                        detail: e.detail || ''
                      })) : [],
                      skills: Array.isArray(parsedCV.skills)
                        ? (parsedCV.skills.length && typeof parsedCV.skills[0] === 'object' && parsedCV.skills[0] !== null
                            ? parsedCV.skills.map(g => ({ label: g.label || '', items: Array.isArray(g.items) ? g.items : [] }))
                            : [{ label: '', items: parsedCV.skills.filter(s => typeof s === 'string') }])
                        : [],
                      certifications: Array.isArray(parsedCV.certifications) ? parsedCV.certifications : [],
                      awards: Array.isArray(parsedCV.awards) ? parsedCV.awards : [],
                      development: Array.isArray(parsedCV.development) ? parsedCV.development : [],
                      volunteer: Array.isArray(parsedCV.volunteer) ? parsedCV.volunteer : [],
                    };
                    setEditedCV(normalized);
                    setRaw(text);
                    setOverrideExperience(null);
                    setUploadStatus('Imported. Switch to Edit tab to refine.');
                    setTab('edit');
                  } catch (err) {
                    console.error(err);
                    setUploadStatus('Upload failed: ' + (err.message || err));
                  }
                  e.target.value = '';
                }} />
                <span>Choose file…</span>
              </label>
              {uploadStatus && <div className="hint">{uploadStatus}</div>}
              <div className="hint subtle">Your CV is sent to the AI parser and turned into structured fields. Nothing is stored.</div>
            </Field>

            <Field label="…or paste raw CV text">
              <textarea
                className="ta"
                value={raw}
                onChange={e => { setRaw(e.target.value); setOverrideExperience(null); setEditedCV(null); }}
                placeholder="Paste any messy CV text — bullets, paragraphs, LinkedIn export…"
              />
              <div className="metaRow">
                <span>{raw ? `${raw.split('\n').length} lines · ${wordCount} words extracted` : 'Empty resume — start blank, paste, or upload above.'}</span>
                {window.SAMPLE_NURSE_CV && <button className="ghost" onClick={() => { setRaw(window.SAMPLE_NURSE_CV); setOverrideExperience(null); setEditedCV(null); }}>Load sample</button>}
              </div>
            </Field>

            <Field label="Profession or field">
              <div className="chips">
                {PROFESSIONS.map(p => (
                  <button key={p} className={profession === p ? 'chip on' : 'chip'} onClick={() => setProfession(p)}>{p}</button>
                ))}
              </div>
              {profession === 'Other' && (
                <input className="input" placeholder="Describe field…" value={otherProfession} onChange={e => setOtherProfession(e.target.value)} />
              )}
              {recommendedStyle && (
                <div className="hint">
                  Recommended: <button className="link" onClick={() => setStyle(recommendedStyle)}>{STYLE_OPTIONS.find(s => s.id === recommendedStyle).name}</button>
                </div>
              )}
            </Field>

            <Field label="AI polish bullets">
              <div className="row">
                <button className="primary" onClick={polishBullets} disabled={polishing}>
                  {polishing ? 'Polishing…' : 'Rewrite with action verbs'}
                </button>
                {overrideExperience && <button className="ghost" onClick={resetPolish}>Revert</button>}
              </div>
              {polishStatus && <div className="hint">{polishStatus}</div>}
              <div className="hint subtle">Preserves your facts and numbers; tightens tone to match the chosen profession.</div>
            </Field>
          </div>
        )}

        {tab === 'edit' && (
          <div className="tabBody">
            <div className="hint subtle" style={{ marginBottom: 10 }}>
              Drag sections to reorder. Click ✓ to hide a section. Edit fields live below.
              {editedCV && <button className="link" style={{ marginLeft: 8 }} onClick={() => setEditedCV(null)}>Reset to parsed</button>}
            </div>
            <div className="secReorder" style={{ '--accent': accent }}>
              <div className="srHead">Section order</div>
              {sectionOrder.map(k => (
                <div
                  key={k}
                  draggable
                  onDragStart={(e) => { dragKey.current = k; e.currentTarget.classList.add('dragging'); }}
                  onDragEnd={(e) => { e.currentTarget.classList.remove('dragging'); document.querySelectorAll('.srItem.dragOver').forEach(el => el.classList.remove('dragOver')); }}
                  onDragOver={(e) => { e.preventDefault(); e.currentTarget.classList.add('dragOver'); onDragOver(k)(e); }}
                  onDragLeave={(e) => e.currentTarget.classList.remove('dragOver')}
                  onDrop={(e) => { e.currentTarget.classList.remove('dragOver'); }}
                  className={enabled.has(k) ? 'srItem on' : 'srItem'}
                >
                  <span className="srGrip">⋮⋮</span>
                  <span className="srTitle" onClick={() => toggleSection(k)}>{window.titleFor(k)}</span>
                  <button className="srToggle" onClick={() => toggleSection(k)} title={enabled.has(k) ? 'Hide section' : 'Show section'}>
                    {enabled.has(k) ? '✓' : ''}
                  </button>
                </div>
              ))}
            </div>
            <window.CVEditor
              cv={cv}
              onChange={(next) => setEditedCV(next)}
            />
          </div>
        )}

        {tab === 'design' && (
          <div className="tabBody">
            <Field label="Profile photo (optional)">
              <label className="filePick" style={{ display: 'block', width: '100%' }}>
                <input type="file" accept="image/*" onChange={onPhoto} />
                <span>{photo ? 'Replace photo' : 'Upload photo'}</span>
              </label>
              {photo && (
                <button className="ghost" style={{ marginTop: 6, width: '100%' }} onClick={() => setPhoto(null)}>Remove photo</button>
              )}
              {photo && (
                <div style={{ marginTop: 10 }}>
                  <div className="miniLabel">Placement</div>
                  <div className="row" style={{ flexWrap: 'wrap', gap: 6 }}>
                    {[['header','Header'],['sidebar','Sidebar'],['corner','Corner']].map(([v,l]) => (
                      <button key={v} className={photoPlacement === v ? 'chip on' : 'chip'} onClick={() => setPhotoPlacement(v)}>{l}</button>
                    ))}
                  </div>
                  <div className="miniLabel" style={{ marginTop: 8 }}>Shape</div>
                  <div className="row" style={{ flexWrap: 'wrap', gap: 6 }}>
                    {[['circle','Circle'],['square','Square'],['portrait','Portrait']].map(([v,l]) => (
                      <button key={v} className={photoShape === v ? 'chip on' : 'chip'} onClick={() => setPhotoShape(v)}>{l}</button>
                    ))}
                  </div>
                  <div style={{ marginTop: 10, display: 'flex', alignItems: 'center', gap: 10 }}>
                    <div style={{
                      width: photoShape === 'portrait' ? 36 : 48,
                      height: photoShape === 'portrait' ? 56 : 48,
                      borderRadius: photoShape === 'circle' ? '50%' : photoShape === 'square' ? 4 : 2,
                      backgroundImage: `url(${photo})`,
                      backgroundSize: 'cover',
                      backgroundPosition: 'center',
                      border: '1px solid #ddd'
                    }} />
                    <div style={{ fontSize: 11, color: '#666' }}>Preview</div>
                  </div>
                </div>
              )}
              <div className="hint subtle">Works on every template. Choose Header, Sidebar or Corner placement.</div>
            </Field>

            <Field label="Style template">
              <div className="styles">
                {STYLE_OPTIONS.map(s => (
                  <button key={s.id} className={style === s.id ? 'styleCard on' : 'styleCard'} onClick={() => setStyle(s.id)}>
                    <StyleSwatch id={s.id} accent={accent} />
                    <div className="styleName">{s.name}</div>
                    <div className="styleDesc">{s.desc}</div>
                  </button>
                ))}
              </div>
            </Field>

            <Field label="Accent colour">
              <div className="swatches">
                {ACCENT_PRESETS.map(p => (
                  <button key={p.hex} className={accent === p.hex ? 'sw on' : 'sw'} style={{ background: p.hex }} onClick={() => setAccent(p.hex)} title={p.name} />
                ))}
              </div>
              <div className="row">
                <label className="hexLabel">
                  <span>HEX</span>
                  <input className="input mono" value={accent} onChange={e => setAccent(e.target.value)} />
                </label>
                <input type="color" value={accent} onChange={e => setAccent(e.target.value)} className="colorPicker" />
              </div>
            </Field>

            <Field label="Font pairing">
              <div className="fonts">
                {FONT_OPTIONS.map(f => (
                  <button key={f.id} className={font === f.id ? 'fontCard on' : 'fontCard'} onClick={() => setFont(f.id)}>
                    <div className="fontName" style={{ fontFamily: window.FONT_STACKS[f.id].display }}>{f.name}</div>
                    <div className="fontSample" style={{ fontFamily: window.FONT_STACKS[f.id].body }}>{f.sample}</div>
                  </button>
                ))}
              </div>
            </Field>

            <Field label="Decorative motif">
              <div className="motifs">
                {window.MOTIF_OPTIONS.map(m => (
                  <button
                    key={m.id}
                    className={motif === m.id ? 'motifCard on' : 'motifCard'}
                    onClick={() => setMotif(m.id)}
                    title={m.desc}
                  >
                    <div className="motifPreview">
                      <window.Motif kind={m.id} color={accent} height={20} />
                    </div>
                    <div className="motifName">{m.name}</div>
                  </button>
                ))}
              </div>
              <div className="hint subtle">Appears as the rule under the name and in section dividers. Pulse, ECG, cross, caduceus and stethoscope read clearly at print scale.</div>
            </Field>

            <Field label="Motif width">
              <div className="seg">
                <button className={motifWidth === 'compact' ? 'on' : ''} onClick={() => setMotifWidth('compact')}>Compact</button>
                <button className={motifWidth === 'wide' ? 'on' : ''} onClick={() => setMotifWidth('wide')}>Wide</button>
                <button className={motifWidth === 'full' ? 'on' : ''} onClick={() => setMotifWidth('full')}>Full bleed</button>
              </div>
              <div className="hint subtle">Compact sits under the name; Wide spans 60%; Full bleed runs edge to edge.</div>
            </Field>

            <Field label="Page size">
              <div className="seg">
                <button className={pageSize === 'A4' ? 'on' : ''} onClick={() => setPageSize('A4')}>A4</button>
                <button className={pageSize === 'Letter' ? 'on' : ''} onClick={() => setPageSize('Letter')}>US Letter</button>
              </div>
            </Field>

            <Field label={`Print bleed / margin (${printBleed}mm)`}>
              <div className="row" style={{ flexWrap: 'wrap', gap: 6, alignItems: 'center' }}>
                {[0, 3, 5, 10, 15].map(n => (
                  <button key={n} className={printBleed === n ? 'chip on' : 'chip'} onClick={() => setPrintBleed(n)}>{n === 0 ? 'None' : n + 'mm'}</button>
                ))}
                <input
                  type="number"
                  min="0"
                  max="20"
                  step="0.5"
                  value={printBleed}
                  onChange={(e) => {
                    const v = parseFloat(e.target.value);
                    if (Number.isFinite(v)) setPrintBleed(Math.max(0, Math.min(20, v)));
                  }}
                  style={{ width: 70, padding: '4px 6px', border: '1px solid #2e2a25', background: '#100e0c', color: '#ece5d6', borderRadius: 4, fontSize: 12 }}
                />
              </div>
              <div className="row" style={{ marginTop: 8, gap: 6, flexWrap: 'wrap' }}>
                <button
                  className={bleedApplyToExport ? 'chip on' : 'chip'}
                  onClick={() => setBleedApplyToExport(true)}
                  title="Apply the bleed/margin to the printed PDF as well as the on-screen preview"
                >Apply on export</button>
                <button
                  className={!bleedApplyToExport ? 'chip on' : 'chip'}
                  onClick={() => setBleedApplyToExport(false)}
                  title="Use the bleed/margin only to visualise spacing — printed/exported PDF stays at 0mm margin"
                >Preview only</button>
              </div>
              <div className="hint subtle">Adds an extra blank margin around each printed page so edge-aligned content survives your printer's non-printable zone. Use 3–5mm for most home printers, 0mm for true full-bleed press output. Switch to <em>Preview only</em> if your printer driver already adds its own margin.</div>
            </Field>

            <Field label="Free-section drag bounds">
              <div className="row" style={{ flexWrap: 'wrap', gap: 6 }}>
                <button className={freeEdgeMode === 'corners' ? 'chip on' : 'chip'} onClick={() => setFreeEdgeMode('corners')} title="Drag free-positioned text all the way into the page corners">Corners (free)</button>
                <button className={freeEdgeMode === 'safe' ? 'chip on' : 'chip'} onClick={() => setFreeEdgeMode('safe')} title="Keep free-positioned text inside the printable safe area">Safe area</button>
              </div>
              <div className="hint subtle">Pick <em>Corners</em> to drag text flush against the paper edges, or <em>Safe area</em> to stay inside the printable zone.</div>
            </Field>

            <Field label="Free-section collisions">
              <div className="row" style={{ flexWrap: 'wrap', gap: 6 }}>
                <button className={collisionMode === 'warn' ? 'chip on' : 'chip'} onClick={() => setCollisionMode('warn')} title="Highlight overlapping free-positioned sections in red">Warn</button>
                <button className={collisionMode === 'push' ? 'chip on' : 'chip'} onClick={() => setCollisionMode('push')} title="Push other free-positioned sections out of the way while dragging">Push</button>
                <button className={collisionMode === 'off' ? 'chip on' : 'chip'} onClick={() => setCollisionMode('off')} title="Allow free-positioned sections to overlap freely">Off</button>
              </div>
              <div className="hint subtle">Controls what happens when a free-positioned section is dragged on top of another. <em>Warn</em> outlines collisions, <em>Push</em> nudges the other block away.</div>
            </Field>

            <Field label="Page footer & continuation header">
              <div className="row" style={{ flexWrap: 'wrap', gap: 6 }}>
                <button
                  className={showPageFooter ? 'chip on' : 'chip'}
                  onClick={() => setShowPageFooter(v => !v)}
                  title="Show or hide the small name + 'Page X of Y' line at the bottom of every page"
                >{showPageFooter ? '✓ Page footer' : 'Page footer'}</button>
                <button
                  className={showContinuationHeader ? 'chip on' : 'chip'}
                  onClick={() => setShowContinuationHeader(v => !v)}
                  title="Show or hide the slim header at the top of pages 2+ (name + page count)"
                >{showContinuationHeader ? '✓ Continuation header' : 'Continuation header'}</button>
              </div>
              <div className="hint subtle">Toggle the small name and "Page X of Y" markers. Applies to every template, both on screen and in export.</div>
            </Field>

            <Field label={`Pages (${pages})`}>
              <div className="row" style={{ flexWrap: 'wrap', gap: 6 }}>
                {[1,2,3,4].map(n => (
                  <button key={n} className={pages === n ? 'chip on' : 'chip'} onClick={() => { setPages(n); if (activePage > n) setActivePage(1); }}>{n}</button>
                ))}
              </div>
              {pages > 1 && (
                <>
                  <div className="miniLabel" style={{ marginTop: 8 }}>Viewing page</div>
                  <div className="row" style={{ flexWrap: 'wrap', gap: 6 }}>
                    {Array.from({length: pages}, (_, i) => i+1).map(n => (
                      <button key={n} className={activePage === n ? 'chip on' : 'chip'} onClick={() => setActivePage(n)}>Page {n}</button>
                    ))}
                  </div>
                </>
              )}
            </Field>
          </div>
        )}

        {tab === 'sections' && (
          <div className="tabBody">
            <Field label="Drag to reorder · click to toggle">
              <ul className="secList">
                {sectionOrder.map(k => (
                  <li
                    key={k}
                    draggable
                    onDragStart={onDragStart(k)}
                    onDragOver={onDragOver(k)}
                    className={enabled.has(k) ? 'secItem on' : 'secItem'}
                  >
                    <span className="grip">⋮⋮</span>
                    <button className="secToggle" onClick={() => toggleSection(k)}>
                      <span className={enabled.has(k) ? 'check on' : 'check'} />
                      {window.titleFor(k)}
                    </button>
                  </li>
                ))}
              </ul>
            </Field>
          </div>
        )}

        <div className="footer">
          {polishStatus && <div className="hint" style={{ marginBottom: 6 }}>{polishStatus}</div>}
          <div className="hint subtle">Cmd / Ctrl + P also exports.</div>
        </div>
      </aside>

      <div className="resizer" onMouseDown={onResizerDown} title="Drag to resize panel" />

      <main className={"canvas" + (editMode ? ' editMode' : '')} ref={pageRef}>
        <div className="canvasFloatBar">
          <button
            className={"editToggle" + (editMode ? ' on' : '')}
            onClick={() => setEditMode(v => !v)}
            title="Toggle on-page editor: drag to reorder, resize, delete"
          >
            {editMode ? '✓ Editing page' : '✎ Edit on page'}
          </button>
          {editMode && (
            <span className="editHint">Drag the grip to reorder · ⊕ ⊖ to resize · × to remove</span>
          )}
          <label className="editHint" style={{ display: 'inline-flex', alignItems: 'center', gap: 6, cursor: 'pointer' }} title="Toggle edge clamping for dragged content">
            <input
              type="checkbox"
              checked={flushEdges}
              onChange={(e) => setFlushEdges(e.target.checked)}
              style={{ accentColor: '#b14a2e' }}
            />
            <span>{flushEdges ? 'Flush to edge' : 'Safe zone'}</span>
          </label>
        </div>
        <div className="canvasInner" style={{ flexDirection: 'column', gap: '12mm', alignItems: 'center' }}>
          <ResumeRenderer cv={cv} opts={opts} pageSize={pageSize} activePage={viewAll ? null : activePage} />
        </div>
        <CanvasDragLayer
          pageRef={pageRef}
          editMode={editMode}
          flushEdges={flushEdges}
          sectionOrder={sectionOrder}
          setSectionOrder={setSectionOrder}
          enabled={enabled}
          setEnabled={setEnabled}
          sectionScale={sectionScale}
          setSectionScale={setSectionScale}
          pagePlan={pagePlan}
          setPagePlan={setPagePlan}
          freePos={freePos}
          setFreePos={setFreePos}
          totalPages={pages}
          collisionMode={collisionMode}
          freeEdgeMode={freeEdgeMode}
        />
      </main>
    </div>
  );
}

// Canvas drag layer: gives every rendered section block (data-section) a draggable
// handle, resize controls, and a delete button when "Edit on page" is on.
// Outside edit mode, sections are still draggable (lightweight grab anywhere)
// for quick reorder.
function CanvasDragLayer({ pageRef, editMode, flushEdges, sectionOrder, setSectionOrder, enabled, setEnabled, sectionScale, setSectionScale, pagePlan, setPagePlan, freePos, setFreePos, totalPages, collisionMode, freeEdgeMode }) {
  useEffect(() => {
    const root = pageRef.current;
    if (!root) return;
    let dragKey = null;
    let indicator = null;
    let lastTarget = null;
    let guides = null; // { left, right, top, bottom }

    // When flushEdges is on, drop the safety inset so the indicator and guides
    // sit exactly at the page edge. Otherwise keep a tiny visible buffer.
    const SAFE_INSET_PX = flushEdges ? 0 : 2;
    const SNAP_TOLERANCE = flushEdges ? 0 : 8; // px: snap-to-edge when within this distance (disabled in flush mode)

    const ensureIndicator = () => {
      if (indicator) return indicator;
      indicator = document.createElement('div');
      indicator.className = 'sectionDropIndicator';
      document.body.appendChild(indicator);
      return indicator;
    };
    const ensureGuides = () => {
      if (guides) return guides;
      const make = (cls) => { const d = document.createElement('div'); d.className = cls; document.body.appendChild(d); return d; };
      guides = {
        left: make('alignGuide vertical'),
        right: make('alignGuide vertical'),
        top: make('alignGuide horizontal'),
        bottom: make('alignGuide horizontal'),
      };
      return guides;
    };
    // Find the page (sheet) the pointer is over so we can show its safe edges.
    const findSheet = (clientY, clientX) => {
      const sheets = root.querySelectorAll('.sheet');
      for (const s of sheets) {
        const r = s.getBoundingClientRect();
        if (clientY >= r.top - 4 && clientY <= r.bottom + 4 && clientX >= r.left - 4 && clientX <= r.right + 4) return r;
      }
      return null;
    };
    // Same as findSheet but also returns the sheet element + page index.
    const findSheetEl = (clientY, clientX) => {
      const sheets = root.querySelectorAll('.sheet');
      for (const s of sheets) {
        if (s.style.display === 'none') continue;
        const r = s.getBoundingClientRect();
        if (clientY >= r.top - 4 && clientY <= r.bottom + 4 && clientX >= r.left - 4 && clientX <= r.right + 4) {
          const pageNum = parseInt(s.dataset.page || '1', 10);
          return { rect: r, el: s, page: pageNum };
        }
      }
      return null;
    };
    const positionIndicator = (el, before, sheetRect) => {
      const ind = ensureIndicator();
      const r = el.getBoundingClientRect();
      // Constrain the drop bar to the sheet's safe zone so it never overhangs the page edge.
      const safeLeft = sheetRect ? sheetRect.left + SAFE_INSET_PX : r.left;
      const safeRight = sheetRect ? sheetRect.right - SAFE_INSET_PX : r.right;
      const left = Math.max(safeLeft, r.left);
      const right = Math.min(safeRight, r.right);
      ind.style.left = left + 'px';
      ind.style.width = Math.max(40, right - left) + 'px';
      ind.style.top = (before ? r.top - 3 : r.bottom + 1) + 'px';
      ind.style.opacity = '1';
    };
    const showGuides = (sheetRect) => {
      if (!sheetRect) return hideGuides();
      const g = ensureGuides();
      const safeL = sheetRect.left + SAFE_INSET_PX;
      const safeR = sheetRect.right - SAFE_INSET_PX;
      const safeT = sheetRect.top + SAFE_INSET_PX;
      const safeB = sheetRect.bottom - SAFE_INSET_PX;
      Object.assign(g.left.style,  { left: safeL + 'px', top: sheetRect.top + 'px', height: sheetRect.height + 'px', opacity: 1 });
      Object.assign(g.right.style, { left: safeR + 'px', top: sheetRect.top + 'px', height: sheetRect.height + 'px', opacity: 1 });
      Object.assign(g.top.style,    { top: safeT + 'px', left: sheetRect.left + 'px', width: sheetRect.width + 'px', opacity: 1 });
      Object.assign(g.bottom.style, { top: safeB + 'px', left: sheetRect.left + 'px', width: sheetRect.width + 'px', opacity: 1 });
    };
    const hideGuides = () => { if (guides) Object.values(guides).forEach(d => d.style.opacity = 0); };
    const clearIndicator = () => { if (indicator) indicator.style.opacity = '0'; hideGuides(); };

    const decorate = () => {
      // Free-positioned sections are anchored to the outer .sheet (not the
      // padded .rsx-root), so they can reach the true page corners regardless
      // of the print-bleed inset. Force rsx-root to be a non-positioning
      // ancestor so absolute children resolve against .sheet instead.
      root.querySelectorAll('.sheet').forEach(s => {
        if (getComputedStyle(s).position === 'static') s.style.position = 'relative';
      });
      root.querySelectorAll('.sheet .rsx-root').forEach(rs => {
        rs.style.position = 'static';
      });
      root.querySelectorAll('[data-section]').forEach(el => {
        const key = el.dataset.section;
        // Apply per-section scale
        const sc = sectionScale[key];
        if (sc && sc !== 1) {
          el.style.fontSize = sc + 'em';
        } else {
          el.style.fontSize = '';
        }
        if (!el.dataset.dragWired) {
          el.dataset.dragWired = '1';
          el.classList.add('canvasSection');
        }
        // Apply free-position: absolute placement on its sheet, expressed as
        // percentages so it stays put when the sheet resizes.
        const fp = freePos[key];
        const sheet = el.closest('.sheet');
        if (fp && sheet) {
          el.style.position = 'absolute';
          el.style.left = fp.leftPct + '%';
          el.style.top = fp.topPct + '%';
          el.style.width = (fp.widthPct || 60) + '%';
          el.style.zIndex = '5';
          el.classList.add('isFreePos');
        } else if (el.classList.contains('isFreePos')) {
          el.style.position = '';
          el.style.left = '';
          el.style.top = '';
          el.style.width = '';
          el.style.zIndex = '';
          el.classList.remove('isFreePos');
        }
        // Toolbar (only present in edit mode)
        let bar = el.querySelector(':scope > .secEditBar');
        if (editMode) {
          el.setAttribute('draggable', 'false'); // dragging via grip only
          if (!bar) {
            bar = document.createElement('div');
            bar.className = 'secEditBar';
            bar.setAttribute('contenteditable', 'false');
            bar.innerHTML = `
              <button class="seBtn seGrip" data-act="grip" title="Drag to reorder" draggable="true">⋮⋮</button>
              <button class="seBtn seFree" data-act="free" title="Free move: drag anywhere on the page">✥</button>
              <span class="seLabel">${key}</span>
              <button class="seBtn" data-act="shrink" title="Smaller text">−</button>
              <button class="seBtn" data-act="grow" title="Larger text">+</button>
              <button class="seBtn" data-act="reset" title="Reset size">↺</button>
              <button class="seBtn" data-act="dock" title="Snap back into the page flow">⤓</button>
              <button class="seBtn seDel" data-act="del" title="Remove section">×</button>
            `;
            el.appendChild(bar);
          }
          // Drag-to-resize handle (bottom-right corner)
          if (!el.querySelector(':scope > .secResize')) {
            const h = document.createElement('div');
            h.className = 'secResize';
            h.title = 'Drag to resize text';
            h.dataset.act = 'resize';
            el.appendChild(h);
          }
        } else {
          if (bar) bar.remove();
          const rh = el.querySelector(':scope > .secResize');
          if (rh) rh.remove();
          el.setAttribute('draggable', 'true');
        }
      });
      // Collision pass: highlight any free-positioned section whose box
      // overlaps another section on the same sheet.
      if (collisionMode !== 'off') {
        root.querySelectorAll('.canvasSection.isOverlapping').forEach(n => n.classList.remove('isOverlapping'));
        root.querySelectorAll('.sheet').forEach(sheet => {
          const blocks = Array.from(sheet.querySelectorAll('[data-section]'));
          const rects = blocks.map(b => ({ el: b, r: b.getBoundingClientRect() }));
          for (let i = 0; i < rects.length; i++) {
            for (let j = i + 1; j < rects.length; j++) {
              const a = rects[i], b = rects[j];
              if (!(a.el.classList.contains('isFreePos') || b.el.classList.contains('isFreePos'))) continue;
              const overlap = !(a.r.right <= b.r.left || a.r.left >= b.r.right || a.r.bottom <= b.r.top || a.r.top >= b.r.bottom);
              if (overlap) {
                if (a.el.classList.contains('isFreePos')) a.el.classList.add('isOverlapping');
                if (b.el.classList.contains('isFreePos')) b.el.classList.add('isOverlapping');
              }
            }
          }
        });
      }
    };
    decorate();
    const mo = new MutationObserver(decorate);
    mo.observe(root, { childList: true, subtree: true });

    const startDrag = (el, e) => {
      dragKey = el.dataset.section;
      el.classList.add('isDragging');
      try { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', dragKey); } catch (_) {}
      // Lock heights of every sheet for the duration of the drag so the
      // pagination effect (or browser reflow) can't pop a blank page in
      // or shift sections around mid-drag. Released in cleanup().
      root.querySelectorAll('.sheet').forEach(s => {
        const h = s.getBoundingClientRect().height;
        s.dataset.lockedH = String(h);
        s.style.minHeight = h + 'px';
      });
      window.dispatchEvent(new Event('rsx:dragstart'));
    };
    const onStart = (e) => {
      const grip = e.target.closest('[data-act="grip"]');
      const el = e.target.closest('[data-section]');
      if (!el || !root.contains(el)) return;
      if (editMode && !grip) { e.preventDefault(); return; }
      startDrag(el, e);
    };
    const onOver = (e) => {
      if (!dragKey) return;
      e.preventDefault();
      const sheetInfo = findSheetEl(e.clientY, e.clientX);
      const sheetRect = sheetInfo?.rect || null;
      const targetPage = sheetInfo?.page || null;
      const el = e.target.closest('[data-section]');
      if (el && root.contains(el)) {
        const r = el.getBoundingClientRect();
        const distTop = Math.abs(e.clientY - r.top);
        const distBot = Math.abs(e.clientY - r.bottom);
        let before;
        if (distTop < SNAP_TOLERANCE && distTop <= distBot) before = true;
        else if (distBot < SNAP_TOLERANCE) before = false;
        else before = (e.clientY - r.top) < r.height / 2;
        lastTarget = { key: el.dataset.section, before, page: targetPage };
        positionIndicator(el, before, sheetRect);
      } else if (sheetInfo) {
        // Hovering empty area of a sheet — drop will append to that page.
        const sections = sheetInfo.el.querySelectorAll('[data-section]');
        const last = sections[sections.length - 1];
        if (last) {
          lastTarget = { key: last.dataset.section, before: false, page: targetPage };
          positionIndicator(last, false, sheetRect);
        } else {
          // Empty sheet — pin indicator to the top of the sheet content area.
          const ind = ensureIndicator();
          ind.style.left = (sheetRect.left + 16) + 'px';
          ind.style.width = (sheetRect.width - 32) + 'px';
          ind.style.top = (sheetRect.top + 24) + 'px';
          ind.style.opacity = '1';
          lastTarget = { key: null, before: true, page: targetPage, emptyPage: true };
        }
      }
      showGuides(sheetRect);
    };
    const onDrop = (e) => {
      if (!dragKey) { cleanup(); return; }
      e.preventDefault();
      const target = lastTarget;
      const movedKey = dragKey;
      // 1) Update page assignment if dropped on a different page.
      if (target?.page && totalPages > 1) {
        setPagePlan(prev => {
          const next = { ...prev };
          // If no plan exists yet, seed with current visible distribution
          // so unrelated sections keep their pages.
          if (Object.keys(next).length === 0) {
            root.querySelectorAll('.sheet').forEach(s => {
              const p = parseInt(s.dataset.page || '1', 10);
              s.querySelectorAll('[data-section]').forEach(sec => { next[sec.dataset.section] = p; });
            });
          }
          next[movedKey] = target.page;
          return next;
        });
      }
      // 2) Reorder within sectionOrder so the dragged item lands next to its target.
      if (target && target.key && target.key !== movedKey) {
        setSectionOrder(prev => {
          const a = prev.filter(k => k !== movedKey);
          let to = a.indexOf(target.key);
          if (to < 0) return prev;
          if (!target.before) to += 1;
          a.splice(to, 0, movedKey);
          return a;
        });
      }
      cleanup();
    };
    const onEnd = () => cleanup();
    const cleanup = () => {
      root.querySelectorAll('.isDragging').forEach(n => n.classList.remove('isDragging'));
      clearIndicator();
      dragKey = null;
      lastTarget = null;
      // Release the per-sheet height lock on the next frame so the
      // re-rendered DOM is in place before pagination measures again.
      requestAnimationFrame(() => {
        root.querySelectorAll('.sheet').forEach(s => {
          if (s.dataset.lockedH) { s.style.minHeight = ''; delete s.dataset.lockedH; }
        });
        window.dispatchEvent(new Event('rsx:dragend'));
      });
    };

    const onClick = (e) => {
      // Selection for keyboard nudging: clicking inside a free-positioned section
      // (but not on a button or editable text) marks it as the keyboard target.
      const freeEl = e.target.closest('.isFreePos');
      if (freeEl && !e.target.closest('.seBtn') && !e.target.closest('[contenteditable="true"]')) {
        root.querySelectorAll('.isFreeSelected').forEach(n => n.classList.remove('isFreeSelected'));
        freeEl.classList.add('isFreeSelected');
      }
      const btn = e.target.closest('.seBtn');
      if (!btn) return;
      const el = btn.closest('[data-section]');
      if (!el) return;
      const key = el.dataset.section;
      const act = btn.dataset.act;
      if (act === 'del') {
        setEnabled(prev => { const n = new Set(prev); n.delete(key); return n; });
      } else if (act === 'grow') {
        setSectionScale(prev => ({ ...prev, [key]: Math.min(1.6, (prev[key] || 1) + 0.1) }));
      } else if (act === 'shrink') {
        setSectionScale(prev => ({ ...prev, [key]: Math.max(0.7, (prev[key] || 1) - 0.1) }));
      } else if (act === 'reset') {
        setSectionScale(prev => { const n = { ...prev }; delete n[key]; return n; });
      } else if (act === 'dock') {
        // Snap free-positioned section back into the page flow.
        setFreePos(prev => { const n = { ...prev }; delete n[key]; return n; });
      }
    };

    // Drag-to-resize: pointer down on the corner handle, then we track movement
    // and translate net displacement into a font-size scale (0.7–1.8).
    const onPointerDown = (e) => {
      // Free-move: drag the ✥ handle to absolutely position the section anywhere
      // on the current sheet, including into the wide outside margins.
      const moveHandle = e.target.closest('[data-act="free"]');
      if (moveHandle) {
        const el = moveHandle.closest('[data-section]');
        const sheet = el?.closest('.sheet');
        const rsRoot = sheet?.querySelector('.rsx-root');
        if (!el || !sheet || !rsRoot) return;
        e.preventDefault(); e.stopPropagation();
        const key = el.dataset.section;
        // Anchor coordinates to the outer .sheet so the section can reach
        // the true paper corners (the rsx-root is inset by the print bleed).
        const sheetRect = sheet.getBoundingClientRect();
        const elRect = el.getBoundingClientRect();
        // Pointer offset inside the section so the handle stays under the cursor.
        const grabDx = e.clientX - elRect.left;
        const grabDy = e.clientY - elRect.top;
        const widthPct = Math.max(15, Math.min(100, (elRect.width / sheetRect.width) * 100));
        const pageNum = parseInt(sheet.dataset.page || '1', 10);
        el.classList.add('isFreeMoving');
        root.querySelectorAll('.isFreeSelected').forEach(n => n.classList.remove('isFreeSelected'));
        el.classList.add('isFreeSelected');
        document.body.classList.add('rsx-dragging');
        const move = (ev) => {
          const x = ev.clientX - sheetRect.left - grabDx;
          const y = ev.clientY - sheetRect.top - grabDy;
          // Allow the section to reach the actual page corners. We only
          // prevent it from sliding fully off the sheet by keeping a tiny
          // visible sliver (8px) on the far side.
          const wPx = elRect.width;
          const hPx = elRect.height;
          // 'corners' mode: only keep an 8px sliver visible so users can drag
          // flush to any edge. 'safe' mode: clamp inside the printable area
          // so nothing crosses the page boundary.
          const safe = freeEdgeMode === 'safe';
          const minX = safe ? 0 : -(wPx - 8);
          const minY = safe ? 0 : -(hPx - 8);
          const maxX = safe ? Math.max(0, sheetRect.width - wPx) : sheetRect.width - 8;
          const maxY = safe ? Math.max(0, sheetRect.height - hPx) : sheetRect.height - 8;
          const cx = Math.max(minX, Math.min(maxX, x));
          const cy = Math.max(minY, Math.min(maxY, y));
          const leftPct = cx / sheetRect.width * 100;
          const topPct = cy / sheetRect.height * 100;
          setFreePos(prev => {
            const wPct = prev[key]?.widthPct || widthPct;
            const next = { ...prev, [key]: { page: pageNum, leftPct, topPct, widthPct: wPct } };
            if (collisionMode === 'push') {
              // Compute moving section's box in sheet pixels, then push any
              // other free-positioned section on the same page out vertically.
              const movRect = el.getBoundingClientRect();
              const movHpct = (movRect.height / sheetRect.height) * 100;
              const movBottom = topPct + movHpct;
              Object.keys(next).forEach(k => {
                if (k === key) return;
                const o = next[k];
                if (!o || o.page !== pageNum) return;
                const oEl = root.querySelector(`[data-section="${k}"]`);
                const oRect = oEl?.getBoundingClientRect();
                if (!oRect) return;
                const oHpct = (oRect.height / sheetRect.height) * 100;
                const oWpct = o.widthPct || 60;
                const horiz = !(leftPct + wPct <= o.leftPct || leftPct >= o.leftPct + oWpct);
                const vert = !(movBottom <= o.topPct || topPct >= o.topPct + oHpct);
                if (horiz && vert) {
                  // Push down if the moving block's center is above other's center, else push up.
                  const movCenter = topPct + movHpct / 2;
                  const oCenter = o.topPct + oHpct / 2;
                  let newTop = movCenter < oCenter ? movBottom + 0.5 : topPct - oHpct - 0.5;
                  newTop = Math.max(0, Math.min(98, newTop));
                  next[k] = { ...o, topPct: newTop };
                }
              });
            }
            return next;
          });
        };
        const up = () => {
          el.classList.remove('isFreeMoving');
          document.body.classList.remove('rsx-dragging');
          window.removeEventListener('pointermove', move);
          window.removeEventListener('pointerup', up);
        };
        window.addEventListener('pointermove', move);
        window.addEventListener('pointerup', up);
        return;
      }
      const handle = e.target.closest('.secResize');
      if (!handle) return;
      const el = handle.closest('[data-section]');
      if (!el) return;
      e.preventDefault(); e.stopPropagation();
      const key = el.dataset.section;
      const startX = e.clientX, startY = e.clientY;
      const startScale = (sectionScale[key] || 1);
      const startW = el.getBoundingClientRect().width || 300;
      // If section is free-positioned, dragging the corner adjusts its width
      // (and keeps font scale untouched). Otherwise behave as before.
      const fp = freePos[key];
      if (fp) {
        const sheet = el.closest('.sheet');
        const rsRoot = sheet?.querySelector('.rsx-root');
        const sheetRect = rsRoot?.getBoundingClientRect();
        const startWidthPct = fp.widthPct || ((startW / (sheetRect?.width || 1)) * 100);
        el.classList.add('isResizing');
        const move = (ev) => {
          const dx = ev.clientX - startX;
          const newPx = startW + dx;
          const widthPct = Math.max(12, Math.min(100, (newPx / (sheetRect.width || 1)) * 100));
          setFreePos(prev => ({ ...prev, [key]: { ...prev[key], widthPct } }));
        };
        const up = () => {
          el.classList.remove('isResizing');
          window.removeEventListener('pointermove', move);
          window.removeEventListener('pointerup', up);
        };
        window.addEventListener('pointermove', move);
        window.addEventListener('pointerup', up);
        return;
      }
      el.classList.add('isResizing');
      const move = (ev) => {
        // Use the larger of horizontal / vertical drag so users can pull either way.
        const dx = ev.clientX - startX;
        const dy = ev.clientY - startY;
        const delta = Math.abs(dx) >= Math.abs(dy) ? dx : dy;
        // 1px per ~0.4% scale; clamp to a sensible range.
        const next = Math.max(0.7, Math.min(1.8, startScale + delta / (startW * 0.6)));
        setSectionScale(prev => ({ ...prev, [key]: Math.round(next * 100) / 100 }));
      };
      const up = () => {
        el.classList.remove('isResizing');
        window.removeEventListener('pointermove', move);
        window.removeEventListener('pointerup', up);
      };
      window.addEventListener('pointermove', move);
      window.addEventListener('pointerup', up);
    };

    // Keyboard nudging: arrow keys move the currently selected free-positioned
    // section by ~0.2% per press (Shift = ~1%). Esc clears selection.
    const onKeyDown = (e) => {
      const sel = root.querySelector('.isFreeSelected');
      if (!sel) return;
      // Don't hijack typing inside editable text.
      const ae = document.activeElement;
      if (ae && (ae.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName))) return;
      if (e.key === 'Escape') { sel.classList.remove('isFreeSelected'); return; }
      const arrows = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] };
      const v = arrows[e.key];
      if (!v) return;
      e.preventDefault();
      const step = e.shiftKey ? 1 : 0.2;
      const key = sel.dataset.section;
      const sheet = sel.closest('.sheet');
      const pageNum = parseInt(sheet?.dataset.page || '1', 10);
      setFreePos(prev => {
        const cur = prev[key] || { page: pageNum, leftPct: 10, topPct: 10, widthPct: 60 };
        const safe = freeEdgeMode === 'safe';
        const lo = safe ? 0 : -50;
        const hi = safe ? 95 : 100;
        const leftPct = Math.max(lo, Math.min(hi, (cur.leftPct || 0) + v[0] * step));
        const topPct = Math.max(lo, Math.min(hi, (cur.topPct || 0) + v[1] * step));
        return { ...prev, [key]: { ...cur, leftPct, topPct } };
      });
    };

    root.addEventListener('dragstart', onStart);
    root.addEventListener('dragover', onOver);
    root.addEventListener('drop', onDrop);
    root.addEventListener('dragend', onEnd);
    root.addEventListener('click', onClick);
    root.addEventListener('pointerdown', onPointerDown);
    window.addEventListener('keydown', onKeyDown);
    return () => {
      mo.disconnect();
      root.removeEventListener('dragstart', onStart);
      root.removeEventListener('dragover', onOver);
      root.removeEventListener('drop', onDrop);
      root.removeEventListener('dragend', onEnd);
      root.removeEventListener('click', onClick);
      root.removeEventListener('pointerdown', onPointerDown);
      window.removeEventListener('keydown', onKeyDown);
      if (indicator) { indicator.remove(); indicator = null; }
      if (guides) { Object.values(guides).forEach(d => d.remove()); guides = null; }
      // strip any toolbars and resize handles
      root.querySelectorAll('.secEditBar, .secResize, .isFreeSelected').forEach(n => n.classList && n.classList.remove('isFreeSelected'));
      root.querySelectorAll('.secEditBar, .secResize').forEach(n => n.remove());
    };
  }, [pageRef, editMode, flushEdges, sectionOrder, sectionScale, setSectionOrder, setEnabled, setSectionScale, pagePlan, setPagePlan, freePos, setFreePos, totalPages, collisionMode, freeEdgeMode]);
  return null;
}

function Field({ label, children }) {
  return (
    <div className="field">
      <div className="fieldLabel">{label}</div>
      {children}
    </div>
  );
}

function StyleSwatch({ id, accent }) {
  if (id === 'editorial-minimal') {
    return (
      <svg viewBox="0 0 60 36" className="styleSwatch">
        <rect width="60" height="36" fill="#fbf8f1" />
        <text x="30" y="14" fontSize="6" fontFamily="serif" textAnchor="middle" fill="#2a2722" letterSpacing="1">NAME</text>
        <line x1="20" y1="18" x2="40" y2="18" stroke={accent} strokeWidth="0.5" />
        <rect x="6" y="22" width="14" height="1" fill="#888" />
        <rect x="6" y="25" width="10" height="1" fill="#888" />
        <rect x="24" y="22" width="30" height="1" fill="#888" />
        <rect x="24" y="25" width="26" height="1" fill="#888" />
        <rect x="24" y="28" width="28" height="1" fill="#888" />
      </svg>
    );
  }
  if (id === 'structured-corporate') {
    return (
      <svg viewBox="0 0 60 36" className="styleSwatch">
        <rect width="60" height="36" fill="#fff" />
        <rect width="60" height="12" fill="#101521" />
        <rect x="2" y="3" width="20" height="2" fill="#fff" />
        <rect x="2" y="7" width="10" height="1" fill={accent} />
        <rect x="0" y="11.5" width="24" height="1" fill={accent} />
        <rect x="3" y="16" width="14" height="1" fill="#101521" />
        <rect x="3" y="19" width="14" height="1" fill="#888" />
        <rect x="22" y="16" width="34" height="1" fill="#101521" />
        <rect x="22" y="19" width="32" height="1" fill="#888" />
        <rect x="22" y="22" width="30" height="1" fill="#888" />
      </svg>
    );
  }
  if (id === 'technical-clean') {
    return (
      <svg viewBox="0 0 60 36" className="styleSwatch">
        <rect width="60" height="36" fill="#fff" />
        <rect width="22" height="36" fill="#f3f1ec" />
        <text x="3" y="10" fontSize="3" fontFamily="monospace" fill={accent}>// resume</text>
        <rect x="3" y="13" width="14" height="2" fill="#1a1f24" />
        <rect x="3" y="20" width="6" height="2" fill="#fff" stroke={accent} strokeWidth="0.3" />
        <rect x="10" y="20" width="8" height="2" fill="#fff" stroke={accent} strokeWidth="0.3" />
        <rect x="3" y="23" width="10" height="2" fill="#fff" stroke={accent} strokeWidth="0.3" />
        <rect x="26" y="6" width="30" height="1.5" fill="#1a1f24" />
        <rect x="26" y="11" width="20" height="1" fill="#888" />
        <rect x="26" y="14" width="28" height="1" fill="#888" />
        <rect x="26" y="20" width="26" height="1" fill="#888" />
      </svg>
    );
  }
  if (id === 'bold-creative') {
    return (
      <svg viewBox="0 0 60 36" className="styleSwatch">
        <rect width="60" height="36" fill="#fff" />
        <rect width="60" height="18" fill={accent} />
        <text x="3" y="12" fontSize="7" fontWeight="800" fill="#fff" letterSpacing="-0.3">NAME</text>
        <rect x="0" y="18" width="60" height="3" fill="#1a1a1a" />
        <circle cx="4" cy="25" r="1" fill={accent} />
        <rect x="7" y="24" width="14" height="1" fill="#1a1a1a" />
        <rect x="7" y="27" width="12" height="1" fill="#888" />
        <rect x="24" y="24" width="32" height="1" fill="#1a1a1a" />
        <rect x="24" y="27" width="28" height="1" fill="#888" />
        <rect x="24" y="30" width="30" height="1" fill="#888" />
      </svg>
    );
  }
  return (
    <svg viewBox="0 0 60 36" className="styleSwatch">
      <rect width="60" height="36" fill="#fff" />
      <text x="30" y="9" fontSize="5" fontFamily="serif" textAnchor="middle" fill="#222">Name, Cred</text>
      <text x="30" y="13" fontSize="2.5" textAnchor="middle" fill={accent} letterSpacing="0.6">TITLE HERE</text>
      <line x1="6" y1="15" x2="54" y2="15" stroke="#555" strokeWidth="0.4" />
      <line x1="22" y1="17" x2="22" y2="34" stroke="#bbb" strokeWidth="0.3" />
      <rect x="6" y="19" width="12" height="1" fill="#888" />
      <rect x="6" y="22" width="10" height="1" fill="#888" />
      <rect x="24" y="19" width="30" height="1" fill="#222" />
      <rect x="24" y="22" width="28" height="1" fill="#888" />
      <rect x="24" y="25" width="26" height="1" fill="#888" />
    </svg>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
