/* ===================================================================
   KabelFlux — AI Copilot sidebar (views-ai.jsx)
   Exposes window.AICopilot. ES2015-safe (no object-rest, no {...} literals).
   =================================================================== */
const AICopilot = ({ pid, me, onClose }) => {
  const { useState, useRef, useEffect } = React;
  const [messages, setMessages] = useState([]);   // [{role, text}] for display
  const [history, setHistory]   = useState([]);    // Anthropic-format message list
  const [input, setInput]       = useState('');
  const [busy, setBusy]         = useState(false);
  const [pending, setPending]   = useState(null);
  const [autoKinds, setAutoKinds] = useState({});  // { create_wire: true, ... }
  const [toolsOpen, setToolsOpen] = useState(false);  // AI-tools chip tray
  const scroller = useRef(null);

  useEffect(() => {
    if (scroller.current) scroller.current.scrollTop = scroller.current.scrollHeight;
  }, [messages, pending]);

  function pushMsg(role, text) {
    setMessages(prev => prev.concat([{ role: role, text: text }]));
  }

  async function runTurn(convo) {
    setBusy(true);
    let streamed = '';
    let done = null;
    setMessages(prev => prev.concat([{ role: 'assistant', text: '' }]));
    await kfxAiChatStream({ project_id: pid, messages: convo }, function (ev) {
      if (ev.type === 'text') {
        streamed += ev.text;
        setMessages(prev => {
          const copy = prev.slice();
          copy[copy.length - 1] = { role: 'assistant', text: streamed };
          return copy;
        });
      } else if (ev.type === 'error') {
        pushMsg('error', ev.message || 'AI error');
      } else if (ev.type === 'done') {
        done = ev;
      }
    });
    setBusy(false);
    if (done) await handleDone(convo, done);
  }

  async function handleDone(convo, done, noResume) {
    const writes = done.pending_writes || [];
    const baseHistory = convo.concat([done.assistant_message]);
    if (!writes.length) {
      setHistory(baseHistory);
      return;
    }
    const toolResults = [];
    const rr = done.read_results || {};
    Object.keys(rr).forEach(function (rid) {
      toolResults.push({ type: 'tool_result', tool_use_id: rid,
                         content: JSON.stringify(rr[rid]) });
    });
    for (let i = 0; i < writes.length; i++) {
      const w = writes[i];
      let approved = !!autoKinds[w.name];
      if (!approved) {
        const decision = await askConfirm(w);
        if (decision.auto) setAutoKinds(prev => Object.assign({}, prev, makeKind(w.name)));
        approved = decision.approve || decision.auto;
        if (!approved) {
          toolResults.push({ type: 'tool_result', tool_use_id: w.id, is_error: true,
            content: 'User rejected this action.' +
                     (decision.feedback ? ' Feedback: ' + decision.feedback : '') });
          continue;
        }
      }
      try {
        const res = await kfxAiExecute(pid, w.name, w.input);
        pushMsg('system', '✓ ' + w.preview);
        toolResults.push({ type: 'tool_result', tool_use_id: w.id,
                           content: JSON.stringify(res.result || res) });
      } catch (e) {
        toolResults.push({ type: 'tool_result', tool_use_id: w.id, is_error: true,
                           content: 'Execution failed: ' + (e && e.message ? e.message : e) });
      }
    }
    const nextHistory = baseHistory.concat([{ role: 'user', content: toolResults }]);
    setHistory(nextHistory);
    if (!noResume) await runTurn(nextHistory);
  }

  const fileRef = useRef(null);
  async function uploadSketch(file) {
    if (busy || !file) return;
    pushMsg('user', '🖼 Uploaded a harness sketch');
    setBusy(true);
    let streamed = '';
    let done = null;
    setMessages(prev => prev.concat([{ role: 'assistant', text: '' }]));
    await window.kfxAiSketchStream(pid, file, function (ev) {
      if (ev.type === 'text') {
        streamed += ev.text;
        setMessages(prev => { const c = prev.slice(); c[c.length - 1] = { role: 'assistant', text: streamed }; return c; });
      } else if (ev.type === 'error') {
        pushMsg('error', ev.message || 'Sketch import failed.');
      } else if (ev.type === 'done') {
        done = ev;
      }
    });
    setBusy(false);
    // Confirm + apply proposed writes; don't resume chat (image isn't in client history).
    if (done) await handleDone([{ role: 'user', content: '[harness sketch]' }], done, true);
  }

  // CSV / Netlist-KBL import: pick a file, map it, show the proposal, then
  // confirm to write the wires. importKindRef tracks which mapper to use.
  const importRef = useRef(null);
  const importKindRef = useRef('import');
  async function onImportFile(file) {
    if (busy || !file) return;
    const netlist = importKindRef.current === 'import-netlist';
    pushMsg('user', (netlist ? '🗂 Netlist/KBL import: ' : '📥 CSV import: ') + file.name);
    setBusy(true);
    try {
      const mapper = netlist ? window.kfxAiImportNetlist : window.kfxAiImportMap;
      const d = await mapper(pid, file);
      const wires = d.wires || [];
      const preview = JSON.stringify(wires, null, 2);
      pushMsg('assistant', (d.notes ? d.notes + '\n\n' : '') + 'Proposed **' + (d.count || wires.length) +
        '** wires:\n\n```json\n' + (preview.length > 2000 ? preview.slice(0, 2000) + '\n…' : preview) + '\n```');
      if (wires.length && window.confirm('Import ' + wires.length + ' wires into this project?')) {
        const n = await window.kfxImportWires(pid, wires);
        pushMsg('assistant', 'Imported **' + n + '** wires — open the Wires tab to review.');
      }
    } catch (e) { pushMsg('error', (e && e.message) || 'Import failed'); }
    setBusy(false);
  }

  function makeKind(name) { const o = {}; o[name] = true; return o; }

  const [confirm, setConfirm] = useState(null); // {write, resolve}
  function askConfirm(write) {
    return new Promise(function (resolve) { setConfirm({ write: write, resolve: resolve }); });
  }
  function decide(d) {
    if (confirm) { confirm.resolve(d); setConfirm(null); }
  }

  // One-tap read-only analyses (Feature B = review, N = deep review).
  // These never propose writes, so we just stream the answer into the thread.
  async function runQuick(label, streamFn) {
    if (busy) return;
    pushMsg('user', label);
    setBusy(true);
    let streamed = '';
    setMessages(prev => prev.concat([{ role: 'assistant', text: '' }]));
    await streamFn(pid, function (ev) {
      if (ev.type === 'text') {
        streamed += ev.text;
        setMessages(prev => { const c = prev.slice(); c[c.length - 1] = { role: 'assistant', text: streamed }; return c; });
      } else if (ev.type === 'error') {
        pushMsg('error', ev.message || 'AI error');
      }
    });
    setBusy(false);
  }

  // AI Tools, run from the copilot. Streams the answer straight into the
  // thread (same as runQuick). Tools needing free-text input prompt for it;
  // file-based importers stay in the AI Tools panel.
  async function runTool(t) {
    if (busy || !pid) return;
    if (t.k === 'import' || t.k === 'import-netlist') {
      importKindRef.current = t.k; setToolsOpen(false);
      if (importRef.current) importRef.current.click();
      return;
    }
    let arg = '';
    if (t.k === 'continuity') { arg = (window.prompt('Failure to diagnose (e.g. pins 4-7 open on J3):') || '').trim(); if (!arg) return; }
    else if (t.k === 'change') { arg = (window.prompt('Change to assess (e.g. swap J5 from DB9 to DB15):') || '').trim(); if (!arg) return; }
    else if (t.k === 'standards') { arg = (window.prompt('Standards question (e.g. shield termination, IPC-620 Class 3?):') || '').trim(); if (!arg) return; }
    else if (t.k === 'rfq') { arg = (window.prompt('Paste the customer RFQ email text:') || '').trim(); if (!arg) return; }
    setToolsOpen(false);
    pushMsg('user', t.icon + ' ' + t.l + (arg ? ': ' + arg : ''));
    setBusy(true);
    let streamed = '';
    setMessages(prev => prev.concat([{ role: 'assistant', text: '' }]));
    const put = function (s) { setMessages(prev => { const c = prev.slice(); c[c.length - 1] = { role: 'assistant', text: s }; return c; }); };
    const onEv = function (ev) {
      if (ev.type === 'text') { streamed += ev.text; put(streamed); }
      else if (ev.type === 'error') { pushMsg('error', ev.message || 'AI error'); }
    };
    try {
      if (t.k === 'narrate') await window.kfxAiNarrateStream(pid, onEv);
      else if (t.k === 'parts') await window.kfxAiPartsStream(pid, onEv);
      else if (t.k === 'continuity') await window.kfxAiContinuityStream(pid, arg, onEv);
      else if (t.k === 'change') await window.kfxAiChangeImpactStream(pid, arg, onEv);
      else if (t.k === 'standards') await window.kfxAiStandardsStream(pid, arg, '', onEv);
      else if (t.k === 'wire-sizing') await window.kfxAiWireSizingStream(pid, onEv);
      else if (t.k === 'quote') await window.kfxAiQuoteStream(pid, onEv);
      else if (t.k === 'build') await window.kfxAiBuildInstructionsStream(pid, onEv);
      else if (t.k === 'rfq') await window.kfxAiRfqStream(pid, arg, onEv);
      else if (t.k === 'anomaly') await window.kfxAiAnomalyStream(pid, onEv);
      else if (t.k === 'estimates') {
        const d = await window.kfxGetEstimates(pid);
        const lines = ['**Wire lengths (estimated, mm):**'];
        (d.lengths || []).forEach(function (l) { lines.push('- #' + l.id + ': ' + l.length_mm + ' mm'); });
        lines.push(''); lines.push('**Bundles (estimated OD):**');
        (d.bundles || []).forEach(function (b) { lines.push('- ' + b.group + ': ' + b.wire_count + ' wires, ~' + b.bundle_od_mm + ' mm'); });
        if (d.cost) {
          lines.push(''); lines.push('**Cost estimate:**');
          lines.push('- Labour: ' + d.cost.labour.total_minutes + ' min @ ' + d.cost.labour_rate + '/hr = ' + d.cost.labour_cost);
          lines.push('- Material: ' + d.cost.material.total + ((d.cost.material.unpriced_parts || []).length ? ' (unpriced: ' + d.cost.material.unpriced_parts.join(', ') + ')' : ''));
          lines.push('- Overhead (' + d.cost.overhead_pct + '%): ' + d.cost.overhead_cost);
          lines.push('- **Total: ' + d.cost.total + '**');
        }
        put(lines.join('\n'));
      }
    } catch (e) { pushMsg('error', (e && e.message) || 'Tool failed'); }
    setBusy(false);
  }

  function exportPdf() {
    if (!messages.length) return;
    const md = messages.map(function (m) {
      const who = m.role === 'user' ? '**You**' : m.role === 'assistant' ? '**Flux AI**'
        : m.role === 'system' ? '_system_' : '_error_';
      return who + '\n\n' + (m.text || '');
    }).join('\n\n---\n\n');
    window.kfxExportMarkdownPdf('Flux AI — conversation', md);
  }

  // F = NL -> draft harness. Proposes writes, so it routes through handleDone
  // (the same per-action confirm flow as chat). noResume: image/draft prompt
  // isn't replayed as client history.
  async function runDraft() {
    if (busy) return;
    const desc = (window.prompt('Describe the harness to draft (connectors, wires, pins):') || '').trim();
    if (!desc) return;
    pushMsg('user', '✍ Draft: ' + desc);
    setBusy(true);
    let streamed = '';
    let done = null;
    setMessages(prev => prev.concat([{ role: 'assistant', text: '' }]));
    await kfxAiDraftStream(pid, desc, function (ev) {
      if (ev.type === 'text') {
        streamed += ev.text;
        setMessages(prev => { const c = prev.slice(); c[c.length - 1] = { role: 'assistant', text: streamed }; return c; });
      } else if (ev.type === 'error') {
        pushMsg('error', ev.message || 'Draft failed.');
      } else if (ev.type === 'done') {
        done = ev;
      }
    });
    setBusy(false);
    if (done) await handleDone([{ role: 'user', content: desc }], done, true);
  }

  async function send() {
    const text = input.trim();
    if (!text || busy) return;
    setInput('');
    pushMsg('user', text);
    const convo = history.concat([{ role: 'user', content: text }]);
    setHistory(convo);
    await runTurn(convo);
  }

  return (
    <div style={{ position: 'fixed', top: 0, right: 0, width: 380, height: '100vh',
      background: 'var(--bg-1)', borderLeft: '1px solid var(--line)', zIndex: 9000,
      display: 'flex', flexDirection: 'column', boxShadow: '-4px 0 24px rgba(0,0,0,.25)' }}>
      <div style={{ padding: '10px 12px', borderBottom: '1px solid var(--line)',
        display: 'flex', alignItems: 'center', gap: 8 }}>
        <strong style={{ flex: 1, fontSize: 13 }}>Flux AI</strong>
        <input ref={fileRef} type="file" accept="image/*" style={{ display: 'none' }}
          onChange={function (e) { const f = e.target.files && e.target.files[0]; e.target.value = ''; if (f) uploadSketch(f); }} />
        <input ref={importRef} type="file" accept=".csv,.txt,.net,.xml,.kbl,text/csv,text/plain" style={{ display: 'none' }}
          onChange={function (e) { const f = e.target.files && e.target.files[0]; e.target.value = ''; if (f) onImportFile(f); }} />
        <button onClick={function () { if (fileRef.current) fileRef.current.click(); }} disabled={busy}
          title="Import a harness sketch / photo (vision)"
          style={{ background: 'none', border: 'none', color: 'var(--t-3)', cursor: 'pointer', fontSize: 15 }}>{'🖼'}</button>
        <button onClick={exportPdf} disabled={!messages.length}
          title="Export this conversation to PDF"
          style={{ background: 'none', border: 'none', color: 'var(--t-3)', cursor: messages.length ? 'pointer' : 'default', fontSize: 14 }}>{'⤓'}</button>
        <button onClick={onClose} style={{ background: 'none', border: 'none',
          color: 'var(--t-3)', cursor: 'pointer', fontSize: 16 }}>{'✕'}</button>
      </div>

      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, padding: '6px 8px', borderBottom: '1px solid var(--line)' }}>
        <button disabled={busy} onClick={function () { runQuick('🔍 Design review', kfxAiReviewStream); }}
          title="Quick design review (Feature B)" style={quickStyle()}>Review</button>
        <button disabled={busy} onClick={function () { runQuick('🧠 Deep review', kfxAiDeepReviewStream); }}
          title="Multi-agent deep review (Feature N)" style={quickStyle()}>Deep review</button>
        <button disabled={busy} onClick={function () { runQuick('📝 Summarize changes', kfxAiDocsStream); }}
          title="Auto-docs / summarize changes (Feature E)" style={quickStyle()}>Summarize</button>
        <button disabled={busy} onClick={runDraft}
          title="Draft a harness from a description (Feature F)" style={quickStyle()}>Draft</button>
        <button disabled={busy} onClick={function () { if (fileRef.current) fileRef.current.click(); }}
          title="Draft from a sketch / photo (Feature G)" style={quickStyle()}>Sketch</button>
        <button disabled={busy} onClick={function () { setToolsOpen(function (v) { return !v; }); }}
          title="AI tools" style={quickStyle(toolsOpen)}>{'✦ Tools ' + (toolsOpen ? '▴' : '▾')}</button>
      </div>

      {toolsOpen && (
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, padding: '6px 8px', borderBottom: '1px solid var(--line)', background: 'var(--bg-2)' }}>
          {COPILOT_TOOLS.map(function (t) {
            return (
              <button key={t.k} disabled={busy} onClick={function () { runTool(t); }}
                title={t.l} style={quickStyle()}>{t.icon + ' ' + t.l}</button>
            );
          })}
        </div>
      )}

      <div ref={scroller} style={{ flex: 1, overflowY: 'auto', padding: 12 }}>
        {messages.map(function (m, i) {
          const isUser = m.role === 'user';
          const isErr = m.role === 'error';
          return (
            <div key={i} style={{ marginBottom: 8, textAlign: isUser ? 'right' : 'left' }}>
              <span style={{ display: 'inline-block', maxWidth: '90%', padding: '6px 10px',
                borderRadius: 8, fontSize: 12, whiteSpace: m.role === 'assistant' ? 'normal' : 'pre-wrap',
                textAlign: 'left', lineHeight: 1.45, wordBreak: 'break-word',
                background: isUser ? 'var(--accent)' : (isErr ? 'rgba(239,78,78,.15)' : 'var(--bg-2)'),
                color: isUser ? '#fff' : (isErr ? 'var(--danger)' : 'var(--t-1)') }}
                {...(m.role === 'assistant'
                  ? { dangerouslySetInnerHTML: { __html: mdToHtml(m.text) } }
                  : { children: m.text })} />
            </div>
          );
        })}

        {confirm && (
          <div style={{ border: '1px solid var(--warning)', borderRadius: 8, padding: 10,
            background: 'rgba(245,166,35,.10)', marginBottom: 8 }}>
            <div style={{ fontSize: 12, fontWeight: 700, marginBottom: 6 }}>Confirm action</div>
            <div style={{ fontSize: 12, marginBottom: 8 }}>{confirm.write.preview}</div>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
              <button onClick={function () { decide({ approve: true }); }}
                style={btnStyle('var(--success)')}>Approve</button>
              <button onClick={function () { decide({ auto: true }); }}
                style={btnStyle('var(--accent)')}>Approve + auto this type</button>
              <button onClick={function () { decide({ approve: false }); }}
                style={btnStyle('var(--danger)')}>Reject</button>
            </div>
          </div>
        )}
      </div>

      <div style={{ borderTop: '1px solid var(--line)', padding: 8, display: 'flex', gap: 6 }}>
        <input value={input} disabled={busy}
          onChange={function (e) { setInput(e.target.value); }}
          onKeyDown={function (e) { if (e.key === 'Enter') send(); }}
          placeholder={busy ? 'Thinking…' : 'Ask or instruct…'}
          style={{ flex: 1, padding: '8px 10px', borderRadius: 6, fontSize: 12,
            background: 'var(--bg-2)', border: '1px solid var(--line)', color: 'var(--t-1)' }} />
        <button onClick={send} disabled={busy} style={btnStyle('var(--accent)')}>Send</button>
      </div>
    </div>
  );
};

function btnStyle(bg) {
  return { padding: '6px 10px', borderRadius: 6, border: 'none', cursor: 'pointer',
    fontSize: 11, fontWeight: 700, color: '#fff', background: bg };
}

function quickStyle(active) {
  return { flex: '1 1 auto', padding: '5px 8px', borderRadius: 6, cursor: 'pointer',
    fontSize: 11, fontWeight: 600,
    color: active ? '#fff' : 'var(--t-1)',
    background: active ? 'var(--accent)' : 'var(--bg-2)',
    border: '1px solid ' + (active ? 'var(--accent)' : 'var(--line)') };
}

// AI Tools surfaced inside the copilot (file-based importers stay in the AI
// Tools panel). Keys mirror views-ai-tools.jsx TOOLS.
const COPILOT_TOOLS = [
  { k: 'narrate',     l: 'Narrate',       icon: '📖' },
  { k: 'parts',       l: 'Parts',         icon: '🔩' },
  { k: 'continuity',  l: 'Continuity',    icon: '🔌' },
  { k: 'change',      l: 'Change impact', icon: '🔀' },
  { k: 'standards',   l: 'Standards Q&A', icon: '📐' },
  { k: 'wire-sizing', l: 'Wire sizing',   icon: '📏' },
  { k: 'quote',       l: 'Quote',         icon: '💲' },
  { k: 'build',       l: 'Build steps',   icon: '🛠' },
  { k: 'rfq',         l: 'RFQ triage',    icon: '📨' },
  { k: 'anomaly',     l: 'Anomaly',       icon: '⚠️' },
  { k: 'estimates',   l: 'Estimates',     icon: '📦' },
  { k: 'import',         l: 'Import CSV',  icon: '📥' },
  { k: 'import-netlist', l: 'Netlist/KBL', icon: '🗂' },
];

/* Minimal, XSS-safe markdown -> HTML for assistant bubbles.
   Escapes ALL content first, then injects only our own tags. Subset:
   headings, bold, italic, inline code, links (http/https only), ul/ol, hr. */
function _mdEsc(s) {
  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function _mdInline(s) {
  s = s.replace(/`([^`]+)`/g,
    '<code style="background:var(--bg-1);padding:1px 4px;border-radius:3px">$1</code>');
  s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
  s = s.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<em>$2</em>');
  s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function (_m, txt, url) {
    return /^https?:\/\//i.test(url)
      ? '<a href="' + url + '" target="_blank" rel="noopener">' + txt + '</a>' : txt;
  });
  // light LaTeX cleanup the models emit ($\rightarrow$, $t55.1$ …)
  s = s.replace(/\$\\rightarrow\$/g, '→').replace(/\$\\leftarrow\$/g, '←');
  s = s.replace(/\$([^$]+)\$/g, '$1');
  return s;
}
function mdToHtml(text) {
  const lines = _mdEsc(text || '').split('\n');
  const out = [];
  let list = null;
  function closeList() { if (list) { out.push('</' + list + '>'); list = null; } }
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i]; let m;
    // GFM table: header row, a |---|---| separator, then data rows.
    if (/^\s*\|.*\|\s*$/.test(line) && i + 1 < lines.length
        && /^\s*\|?[\s:|-]*-[\s:|-]*\|?\s*$/.test(lines[i + 1])) {
      closeList();
      const cellsOf = function (r) { return r.trim().replace(/^\||\|$/g, '').split('|').map(function (c) { return c.trim(); }); };
      const headers = cellsOf(line);
      i++; // consume separator
      let t = '<table style="border-collapse:collapse;width:100%;margin:6px 0;font-size:11px"><thead><tr>';
      headers.forEach(function (h) { t += '<th style="border:1px solid var(--line);padding:4px 6px;text-align:left;background:var(--bg-2)">' + _mdInline(h) + '</th>'; });
      t += '</tr></thead><tbody>';
      while (i + 1 < lines.length && /^\s*\|.*\|\s*$/.test(lines[i + 1])) {
        i++;
        const cs = cellsOf(lines[i]);
        t += '<tr>';
        headers.forEach(function (_h, ci) { t += '<td style="border:1px solid var(--line);padding:4px 6px;vertical-align:top">' + _mdInline(cs[ci] || '') + '</td>'; });
        t += '</tr>';
      }
      t += '</tbody></table>';
      out.push(t); continue;
    }
    if (/^\s*---+\s*$/.test(line)) {
      closeList(); out.push('<hr style="border:none;border-top:1px solid var(--line);margin:8px 0"/>'); continue;
    }
    if ((m = line.match(/^(#{1,4})\s+(.*)$/))) {
      closeList();
      const sz = [0, 15, 14, 13, 12][m[1].length] || 12;
      out.push('<div style="font-weight:700;font-size:' + sz + 'px;margin:8px 0 4px">' + _mdInline(m[2]) + '</div>');
      continue;
    }
    if ((m = line.match(/^\s*[-*]\s+(.*)$/))) {
      if (list !== 'ul') { closeList(); out.push('<ul style="margin:4px 0;padding-left:18px">'); list = 'ul'; }
      out.push('<li>' + _mdInline(m[1]) + '</li>'); continue;
    }
    if ((m = line.match(/^\s*\d+\.\s+(.*)$/))) {
      if (list !== 'ol') { closeList(); out.push('<ol style="margin:4px 0;padding-left:18px">'); list = 'ol'; }
      out.push('<li>' + _mdInline(m[1]) + '</li>'); continue;
    }
    if (/^\s*$/.test(line)) { closeList(); out.push('<div style="height:6px"></div>'); continue; }
    closeList(); out.push('<div>' + _mdInline(line) + '</div>');
  }
  closeList();
  return out.join('');
}

window.AICopilot = AICopilot;
window.mdToHtml = mdToHtml;   // reused by the Revision-history AI-summary tab

/* Export any markdown (AI tools output, copilot transcript) to PDF via the
   browser's print dialog ("Save as PDF"). No backend / no dependency. */
function kfxExportMarkdownPdf(title, md) {
  const safe = String(title || 'KabelFlux AI').replace(/[<>&]/g, '');
  const body = mdToHtml(md || '');
  const w = window.open('', '_blank');
  if (!w) { alert('Allow pop-ups for this site to export the PDF.'); return; }
  w.document.write(
    '<!doctype html><html><head><meta charset="utf-8"><title>' + safe + '</title><style>' +
    /* map the app CSS vars the inline styles reference, to print-friendly colours */
    ':root{--line:#cbd2dc;--bg-1:#f3f4f6;--bg-2:#f3f4f6;--accent:#2563eb;--t-1:#111827}' +
    'body{font:13px/1.55 -apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#111827;max-width:780px;margin:0 auto;padding:36px}' +
    'h1.kfx-t{font-size:18px;margin:0 0 2px}.kfx-meta{color:#8a93a3;font-size:11px;margin:0 0 18px}' +
    'table{border-collapse:collapse;width:100%;margin:8px 0}th,td{border:1px solid #cbd2dc;padding:6px 8px;text-align:left;font-size:12px;vertical-align:top}th{background:#f3f4f6}' +
    'code{background:#f3f4f6;padding:1px 4px;border-radius:3px;font-family:ui-monospace,Consolas,monospace}' +
    'a{color:#2563eb}hr{border:none;border-top:1px solid #e2e6ed;margin:10px 0}' +
    '@media print{body{padding:0}}</style></head><body>' +
    '<h1 class="kfx-t">' + safe + '</h1><div class="kfx-meta">KabelFlux · ' + new Date().toLocaleString() + '</div>' +
    body + '</body></html>'
  );
  w.document.close(); w.focus();
  setTimeout(function () { try { w.print(); } catch (e) {} }, 350);
}
window.kfxExportMarkdownPdf = kfxExportMarkdownPdf;
