#!/usr/bin/env node
// build-html.mjs — render norske kronikker som selvstendige, annoterbare HTML-filer.
// Bruk: node build-html.mjs utkast/01-open-source-edge-modeller.md [flere.md ...]
// Skriver til review/<samme-navn>.html. Ingen npm-avhengigheter, ingen nett.

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// ---------------------------------------------------------------------------
// YAML front matter (minimal: flate key: "value"-par mellom --- ... ---)
// ---------------------------------------------------------------------------
function parseFrontMatter(raw) {
  const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
  if (!m) return { meta: {}, body: raw };
  const meta = {};
  for (const line of m[1].split(/\r?\n/)) {
    const mm = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
    if (!mm) continue;
    let val = mm[2].trim();
    if (
      (val.startsWith('"') && val.endsWith('"')) ||
      (val.startsWith("'") && val.endsWith("'"))
    ) {
      val = val.slice(1, -1);
    }
    meta[mm[1]] = val;
  }
  return { meta, body: m[2].replace(/^\r?\n+/, "") };
}

// ---------------------------------------------------------------------------
// HTML-escape for tekstinnhold
// ---------------------------------------------------------------------------
function esc(s) {
  return s
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

// ---------------------------------------------------------------------------
// Inline markdown: `kode`, [lenke](url), **fet**, *kursiv*. «» og — beholdes.
// Tar uescapet tekst, returnerer escaped HTML med inline-tagger.
// ---------------------------------------------------------------------------
export function inline(text) {
  let out = esc(text);
  // `kode` først, slik at * og ** inni en kode-span ikke tolkes som fet/kursiv
  out = out.replace(/`([^`]+)`/g, (_, c) => `<code>${c}</code>`);
  // [tekst](url) — kun http(s)/mailto får href; andre skjemaer beholder teksten
  out = out.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, url) =>
    /^(https?:|mailto:)/i.test(url)
      ? `<a href="${url}" target="_blank" rel="noopener">${label}</a>`
      : label
  );
  // **fet** før *kursiv* for å unngå konflikt
  out = out.replace(/\*\*([^*]+)\*\*/g, (_, c) => `<strong>${c}</strong>`);
  out = out.replace(/\*([^*]+)\*/g, (_, c) => `<em>${c}</em>`);
  return out;
}

// ---------------------------------------------------------------------------
// Overskrift: # -> <h1> ... #### -> <h4> (klemmes til h4; dypere nivåer
// kollapser til <h4>). Tar antall #-tegn + raw heading-tekst.
// ---------------------------------------------------------------------------
export function renderHeading(hashes, text) {
  const level = Math.min(hashes, 4); // # .. #### -> <h1> .. <h4>
  return `<h${level}>${inline(text.trim())}</h${level}>`;
}

// ---------------------------------------------------------------------------
// Tabell: en sammenhengende blokk av `| a | b |`-rader -> <table>.
// Rad 1 = header (<th>). En påfølgende separator-rad (| --- | --- |) hoppes
// over. Øvrige rader = <td>. Ujevn celletall tolereres (ingen kast).
// ---------------------------------------------------------------------------
function splitRow(line) {
  // Fjern ledende/avsluttende pipe, del på resten.
  return line
    .trim()
    .replace(/^\|/, "")
    .replace(/\|$/, "")
    .split("|")
    .map((c) => c.trim());
}
function isSeparatorRow(line) {
  // | --- | :---: | ---: | osv.
  return /^\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)*\|?$/.test(line.trim());
}
export function renderTable(rows) {
  if (!rows.length) return "";
  let bodyRows = rows;
  const header = splitRow(rows[0]);
  let startIdx = 1;
  if (rows.length > 1 && isSeparatorRow(rows[1])) startIdx = 2;
  bodyRows = rows.slice(startIdx);

  const thead =
    "<thead><tr>" +
    header.map((c) => `<th>${inline(c)}</th>`).join("") +
    "</tr></thead>";
  const tbody =
    "<tbody>" +
    bodyRows
      .map(
        (r) =>
          "<tr>" +
          splitRow(r)
            .map((c) => `<td>${inline(c)}</td>`)
            .join("") +
          "</tr>"
      )
      .join("") +
    "</tbody>";
  return `<table>${thead}${tbody}</table>`;
}

// A markdown table line: starts with `|` and has at least one more `|`.
function isTableLine(t) {
  return /^\|.*\|/.test(t);
}

// ---------------------------------------------------------------------------
// Kompakt markdown -> HTML for body.
// Håndterer: # .. #### overskrifter, | tabeller |, - punktlister,
// 1. nummererte lister, > blockquote (flerlinjers, med FIGUR-markør),
// [lenker](url), --- horisontal linje, `kode`, og avsnitt (blanklinje-separert).
// Første avsnitt får drop-cap-klasse. Avsnitt etter det første: .indent.
// ---------------------------------------------------------------------------

// Figur-markør: les figurer/figN*.svg fra serie-mappa (process.cwd()).
// Returnerer rå SVG-streng, eller null hvis ingen matchende fil finnes.
function readFigureSvg(n) {
  const figDir = path.join(process.cwd(), "figurer");
  if (!fs.existsSync(figDir)) return null;
  const re = new RegExp("^fig" + n + "(?:[-.].*)?\\.svg$", "i");
  const match = fs.readdirSync(figDir).filter((f) => re.test(f)).sort()[0];
  if (!match) return null;
  return fs
    .readFileSync(path.join(figDir, match), "utf8")
    .replace(/<\?xml[^>]*\?>\s*/i, "")
    .trim();
}

export function markdownToHtml(body) {
  const lines = body.replace(/\r\n/g, "\n").split("\n");
  const blocks = [];
  let i = 0;
  let paraCount = 0;

  function flushPara(buf) {
    if (!buf.length) return;
    const text = buf.join(" ").trim();
    if (!text) return;
    paraCount++;
    const cls = paraCount === 1 ? "lede" : "indent";
    blocks.push(`<p class="${cls}">${inline(text)}</p>`);
  }

  while (i < lines.length) {
    const line = lines[i];
    const trimmed = line.trim();

    // Horisontal linje
    if (/^---+$/.test(trimmed)) {
      blocks.push("<hr>");
      i++;
      continue;
    }

    // Overskrifter (# .. ###### -> <h1> .. <h4>, dypere klemmes til h4)
    let hm = trimmed.match(/^(#{1,6})\s+(.*)$/);
    if (hm) {
      blocks.push(renderHeading(hm[1].length, hm[2]));
      i++;
      continue;
    }

    // Tabell (sammenhengende | a | b | -rader)
    if (isTableLine(trimmed)) {
      const rows = [];
      while (i < lines.length && isTableLine(lines[i].trim())) {
        rows.push(lines[i].trim());
        i++;
      }
      blocks.push(renderTable(rows));
      continue;
    }

    // Blockquote (sammenhengende > -linjer; tom > -linje = nytt avsnitt i samme quote)
    if (/^>\s?/.test(trimmed)) {
      const qlines = [];
      while (i < lines.length && /^>\s?/.test(lines[i].trim())) {
        qlines.push(lines[i].trim().replace(/^>\s?/, ""));
        i++;
      }
      const paras = [];
      let cur = [];
      for (const ql of qlines) {
        if (ql.trim() === "") {
          if (cur.length) { paras.push(cur.join(" ").trim()); cur = []; }
        } else cur.push(ql);
      }
      if (cur.length) paras.push(cur.join(" ").trim());

      // Figur-markør: blockquote som starter med **[FIGUR N — ...]**.
      // Finnes figurer/figN*.svg → inline SVG (rendres i nettleser).
      // Ellers: vis spec'en som vanlig blockquote (uendret oppførsel).
      const figm = paras.length && paras[0].match(/^\*\*\[FIGUR\s+(\d+)\b/);
      if (figm) {
        const svg = readFigureSvg(figm[1]);
        if (svg) {
          const capm = paras[0].match(/«([^»]+)»/);
          const caption = capm ? `Figur ${figm[1]} — ${capm[1]}` : "";
          blocks.push(
            `<figure class="fig">${svg}` +
              (caption ? `<figcaption>${inline(caption)}</figcaption>` : "") +
              "</figure>"
          );
          continue;
        }
      }

      blocks.push("<blockquote>" + paras.map((p) => `<p>${inline(p)}</p>`).join("") + "</blockquote>");
      continue;
    }

    // Punktliste
    if (/^[-*]\s+/.test(trimmed)) {
      const items = [];
      while (i < lines.length && /^[-*]\s+/.test(lines[i].trim())) {
        items.push(lines[i].trim().replace(/^[-*]\s+/, ""));
        i++;
      }
      blocks.push(
        "<ul>" + items.map((it) => `<li>${inline(it)}</li>`).join("") + "</ul>"
      );
      continue;
    }

    // Nummerert liste
    if (/^\d+\.\s+/.test(trimmed)) {
      const items = [];
      while (i < lines.length && /^\d+\.\s+/.test(lines[i].trim())) {
        items.push(lines[i].trim().replace(/^\d+\.\s+/, ""));
        i++;
      }
      blocks.push(
        "<ol>" + items.map((it) => `<li>${inline(it)}</li>`).join("") + "</ol>"
      );
      continue;
    }

    // Blank linje -> avsnittsgrense
    if (trimmed === "") {
      i++;
      continue;
    }

    // Vanlig avsnitt: samle til blank/strukturlinje
    const pbuf = [];
    while (i < lines.length) {
      const t = lines[i].trim();
      if (
        t === "" ||
        /^---+$/.test(t) ||
        /^(#{1,6})\s+/.test(t) ||
        isTableLine(t) ||
        /^>\s?/.test(t) ||
        /^[-*]\s+/.test(t) ||
        /^\d+\.\s+/.test(t)
      ) {
        break;
      }
      pbuf.push(t);
      i++;
    }
    flushPara(pbuf);
  }

  return blocks.join("\n");
}

// ---------------------------------------------------------------------------
// JS-streng-escape (for ordrett innbygging av body-markdown og meta)
// ---------------------------------------------------------------------------
function toJsString(s) {
  return JSON.stringify(s);
}

// ---------------------------------------------------------------------------
// CSS — redaksjonell avis-stil, self-contained
// ---------------------------------------------------------------------------
const CSS = `
:root {
  --bg: #FBFAF7;
  --ink: #1A1A1A;
  --muted: #555555;
  --accent: #9A3324;
  --rule: #d8d4cb;
  --serif: "Iowan Old Style","Palatino Linotype",Palatino,"Book Antiqua",Georgia,serif;
  --sans: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --c-change: #e6a817;
  --c-add: #2e8b57;
  --c-remove: #c0392b;
  --c-clarify: #2d6cdf;
  --c-risk: #7d3c98;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--serif);
  font-size: 20px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}
.page {
  display: flex;
  justify-content: center;
  gap: 2rem;
  padding: 4rem 1.5rem 8rem;
}
article {
  max-width: 34em;
  width: 100%;
}
.kicker {
  font-family: var(--sans);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--accent);
  margin: 0 0 0.9rem;
}
h1.title {
  font-family: var(--serif);
  font-size: 2.6rem;
  line-height: 1.1;
  font-weight: 700;
  margin: 0 0 1rem;
  letter-spacing: -0.01em;
}
.subtitle {
  font-family: var(--serif);
  font-style: italic;
  font-size: 1.25rem;
  line-height: 1.4;
  color: var(--muted);
  margin: 0 0 1.6rem;
}
.byline-wrap {
  border-top: 1px solid var(--rule);
  padding-top: 0.9rem;
  margin-bottom: 2.4rem;
}
.byline {
  font-family: var(--sans);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.7rem;
  color: var(--muted);
  margin: 0 0 0.3rem;
}
.meta {
  font-family: var(--sans);
  font-size: 0.7rem;
  color: var(--muted);
  letter-spacing: 0.04em;
}
.body p { margin: 0; text-align: left; }
.body p.indent { text-indent: 1.4em; }
.body p.lede::first-letter {
  float: left;
  font-size: 3.1em;
  line-height: 0.82;
  padding: 0.05em 0.08em 0 0;
  color: var(--accent);
  font-weight: 700;
}
.body h1 { font-size: 1.8rem; font-weight: 700; margin: 2.2rem 0 0.7rem; line-height: 1.15; }
.body h2 { font-size: 1.5rem; font-weight: 700; margin: 2rem 0 0.6rem; line-height: 1.2; }
.body h3 { font-size: 1.2rem; font-weight: 700; margin: 1.6rem 0 0.5rem; line-height: 1.25; }
.body h4 { font-size: 1.05rem; font-weight: 700; margin: 1.3rem 0 0.4rem; line-height: 1.3; }
.body a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.body code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.86em;
  background: #efece4;
  padding: 0.08em 0.35em;
  border-radius: 3px;
}
.body table {
  border-collapse: collapse;
  width: 100%;
  margin: 1.4rem 0;
  font-family: var(--sans);
  font-size: 0.86rem;
}
.body th, .body td {
  border: 1px solid var(--rule);
  padding: 0.5em 0.7em;
  text-align: left;
  vertical-align: top;
}
.body th { background: #efece4; font-weight: 600; }
.body ul, .body ol { margin: 0.8rem 0; padding-left: 1.5em; }
.body li { margin: 0.3rem 0; }
.body blockquote {
  margin: 1.4rem 0;
  padding-left: 1.1em;
  border-left: 3px solid var(--accent);
  font-style: italic;
  color: #333;
}
.body blockquote p { margin: 0 0 0.7em; }
.body blockquote p:last-child { margin-bottom: 0; }
.body figure.fig { margin: 1.9rem 0; text-align: center; }
.body figure.fig svg {
  max-width: 100%;
  height: auto;
  border: 1px solid var(--rule);
  background: #fff;
  border-radius: 4px;
}
.body figure.fig figcaption {
  font-family: var(--sans);
  font-size: 0.8rem;
  color: var(--muted);
  font-style: italic;
  margin-top: 0.5rem;
}
.body hr {
  border: 0;
  border-top: 1px solid var(--rule);
  margin: 2rem auto;
  width: 40%;
}

/* Annoterte markeringer */
.anno {
  border-radius: 2px;
  padding: 0 1px;
  cursor: pointer;
}
.anno-change { background: rgba(230,168,23,0.30); }
.anno-add    { background: rgba(46,139,87,0.25); }
.anno-remove { background: rgba(192,57,43,0.22); }
.anno-clarify{ background: rgba(45,108,223,0.20); }
.anno-risk   { background: rgba(125,60,152,0.20); }
.anno-num {
  font-family: var(--sans);
  font-size: 0.6em;
  font-weight: 700;
  vertical-align: super;
  margin-left: 1px;
  color: var(--accent);
}

/* Flytende verktøylinje */
.anno-toolbar {
  position: absolute;
  z-index: 1000;
  display: none;
  background: #1A1A1A;
  border-radius: 8px;
  padding: 4px;
  box-shadow: 0 6px 24px rgba(0,0,0,0.28);
  gap: 2px;
}
.anno-toolbar.show { display: flex; }
.anno-toolbar button {
  font-family: var(--sans);
  font-size: 0.72rem;
  border: 0;
  background: transparent;
  color: #fff;
  padding: 6px 9px;
  border-radius: 5px;
  cursor: pointer;
}
.anno-toolbar button:hover { background: rgba(255,255,255,0.16); }
.anno-toolbar .swatch {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-right: 5px; vertical-align: middle;
}

/* Kommentarboks (popover) */
.anno-popover {
  position: absolute;
  z-index: 1001;
  display: none;
  background: #fff;
  border: 1px solid var(--rule);
  border-radius: 8px;
  padding: 10px;
  width: 280px;
  box-shadow: 0 8px 30px rgba(0,0,0,0.22);
  font-family: var(--sans);
}
.anno-popover.show { display: block; }
.anno-popover .ph {
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted); margin-bottom: 6px; font-weight: 600;
}
.anno-popover textarea {
  width: 100%; min-height: 70px; font-family: var(--sans); font-size: 0.85rem;
  border: 1px solid var(--rule); border-radius: 5px; padding: 6px; resize: vertical;
}
.anno-popover .row { display: flex; justify-content: flex-end; gap: 6px; margin-top: 8px; }
.anno-popover button {
  font-family: var(--sans); font-size: 0.78rem; padding: 5px 12px;
  border-radius: 5px; border: 1px solid var(--rule); background: #f4f2ec; cursor: pointer;
}
.anno-popover button.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
.anno-popover .intent-pick { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 8px; }
.anno-popover .intent-pick button {
  font-family: var(--sans); font-size: 0.72rem; padding: 4px 8px;
  border-radius: 5px; border: 1px solid var(--rule); background: #f4f2ec; cursor: pointer;
}
.anno-popover .intent-pick button.active { background: #1A1A1A; color: #fff; border-color: #1A1A1A; }
.anno-popover .del-edit { margin-right: auto; color: var(--c-remove); background: #fff; }

/* Sidepanel */
.sidebar {
  width: 320px;
  flex: 0 0 320px;
  font-family: var(--sans);
  position: sticky;
  top: 2rem;
  align-self: flex-start;
  max-height: calc(100vh - 4rem);
  overflow: auto;
}
.sidebar h2 {
  font-family: var(--sans); font-size: 0.8rem; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--muted); margin: 0 0 0.8rem;
}
.sidebar .actions { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 1rem; }
.sidebar .actions button {
  font-family: var(--sans); font-size: 0.74rem; padding: 6px 10px; border-radius: 6px;
  border: 1px solid var(--rule); background: #fff; cursor: pointer;
}
.sidebar .actions button.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
.anno-item {
  border: 1px solid var(--rule); border-radius: 8px; padding: 9px 10px;
  margin-bottom: 8px; background: #fff;
}
.anno-item .top { display: flex; align-items: center; gap: 6px; margin-bottom: 5px; }
.anno-item .num {
  font-weight: 700; font-size: 0.72rem; background: #efece4;
  width: 20px; height: 20px; border-radius: 50%; display: inline-flex;
  align-items: center; justify-content: center;
}
.anno-item .badge {
  font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.05em;
  font-weight: 700; padding: 2px 7px; border-radius: 10px; color: #fff;
}
.badge-change { background: var(--c-change); color:#3a2a00; }
.badge-add    { background: var(--c-add); }
.badge-remove { background: var(--c-remove); }
.badge-clarify{ background: var(--c-clarify); }
.badge-risk   { background: var(--c-risk); }
.anno-item .quote { font-size: 0.78rem; color:#444; font-style: italic; margin: 4px 0; }
.anno-item .cmt { font-size: 0.84rem; color: var(--ink); }
.anno-item .edit {
  margin-left: auto; border: 0; background: transparent; color: var(--c-clarify);
  cursor: pointer; font-size: 0.74rem;
}
.anno-item .del {
  margin-left: 4px; border: 0; background: transparent; color: var(--c-remove);
  cursor: pointer; font-size: 0.74rem;
}
.sidebar .empty { font-size: 0.8rem; color: var(--muted); font-style: italic; }
.sidebar-toggle {
  position: fixed; top: 1rem; right: 1rem; z-index: 900;
  font-family: var(--sans); font-size: 0.74rem; padding: 7px 12px;
  border-radius: 6px; border: 1px solid var(--rule); background: #fff; cursor: pointer;
  box-shadow: 0 2px 10px rgba(0,0,0,0.08);
}

/* Eksport-overlay */
.export-overlay {
  position: fixed; inset: 0; z-index: 2000; display: none;
  background: rgba(0,0,0,0.45); align-items: center; justify-content: center;
}
.export-overlay.show { display: flex; }
.export-box {
  background: #fff; border-radius: 10px; padding: 16px; width: min(720px, 92vw);
  font-family: var(--sans); box-shadow: 0 20px 60px rgba(0,0,0,0.35);
}
.export-box h3 { margin: 0 0 8px; font-size: 0.95rem; }
.export-box textarea {
  width: 100%; height: 50vh; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8rem; border: 1px solid var(--rule); border-radius: 6px; padding: 10px;
}
.export-box .row { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; }
.export-box button {
  font-family: var(--sans); font-size: 0.82rem; padding: 7px 14px; border-radius: 6px;
  border: 1px solid var(--rule); background: #f4f2ec; cursor: pointer;
}
.export-box button.primary { background: var(--accent); color: #fff; border-color: var(--accent); }

@media (max-width: 1100px) {
  .sidebar { display: none; }
  .sidebar.mobile-show {
    display: block; position: fixed; right: 0; top: 0; bottom: 0; z-index: 950;
    width: 86vw; max-width: 360px; background: var(--bg); padding: 1.2rem;
    box-shadow: -6px 0 30px rgba(0,0,0,0.2); max-height: 100vh;
  }
}

@media print {
  .sidebar, .sidebar-toggle, .anno-toolbar, .anno-popover, .export-overlay { display: none !important; }
  .anno-num { display: none !important; }
  .anno { background: transparent !important; }
  body { font-size: 12pt; background: #fff; }
  .page { padding: 0; }
  article { max-width: 100%; }
}
`;

// ---------------------------------------------------------------------------
// Klient-JS (annoteringsverktøy). Bygges inn ordrett.
// ---------------------------------------------------------------------------
const CLIENT_JS = String.raw`
(function () {
  "use strict";
  var STORE_KEY = "anno:" + window.__ARTICLE_KEY__;
  var BODY_MD = window.__BODY_MD__;
  var SOURCE_FILE = window.__SOURCE_FILE__;
  var TITLE = window.__TITLE__;
  var INTENTS = {
    change:  { label: "Endre",     cls: "change",  upper: "CHANGE" },
    add:     { label: "Legg til",  cls: "add",     upper: "ADD" },
    remove:  { label: "Fjern",     cls: "remove",  upper: "REMOVE" },
    clarify: { label: "Avklar",    cls: "clarify", upper: "CLARIFY" },
    risk:    { label: "Risiko",    cls: "risk",    upper: "RISK" }
  };
  var SWATCH = {
    change: "#e6a817", add: "#2e8b57", remove: "#c0392b", clarify: "#2d6cdf", risk: "#7d3c98"
  };

  // Hele <article> er markerbart (tittel + ingress + brødtekst), ikke bare .body.
  var article = document.querySelector("article");
  var toolbar = document.getElementById("annoToolbar");
  var popover = document.getElementById("annoPopover");
  var sidebar = document.getElementById("annoSidebar");
  var listEl  = document.getElementById("annoList");

  var annotations = load();
  var pendingRange = null; // {text} for ny annotering
  var editingId = null;    // id når en eksisterende annotering redigeres

  function load() {
    try {
      var raw = localStorage.getItem(STORE_KEY);
      return raw ? JSON.parse(raw) : [];
    } catch (e) { return []; }
  }
  function save() {
    try { localStorage.setItem(STORE_KEY, JSON.stringify(annotations)); } catch (e) {}
  }

  // --- Markering -> verktøylinje -------------------------------------------
  document.addEventListener("mouseup", function (e) {
    if (toolbar.contains(e.target) || popover.contains(e.target)) return;
    setTimeout(handleSelection, 0);
  });

  // Hent kontekst rundt markeringen (ordene foran/bak i samme avsnitt),
  // slik at korte markeringer (ett ord) kan plasseres entydig i eksporten.
  function getContext(range, selText) {
    var BLOCK = /^(P|LI|H1|H2|H3|H4|BLOCKQUOTE|TD)$/;
    var block = range.commonAncestorContainer;
    if (block.nodeType === 3) block = block.parentElement;
    while (block && block !== article && !BLOCK.test(block.tagName)) block = block.parentElement;
    if (!block || block === article) return "";
    try {
      var beforeR = document.createRange();
      beforeR.selectNodeContents(block);
      beforeR.setEnd(range.startContainer, range.startOffset);
      var afterR = document.createRange();
      afterR.selectNodeContents(block);
      afterR.setStart(range.endContainer, range.endOffset);
      var before = beforeR.toString().replace(/\s+/g, " ");
      var after = afterR.toString().replace(/\s+/g, " ");
      var W = 55;
      if (before.length > W) before = "…" + before.slice(-W);
      if (after.length > W) after = after.slice(0, W) + "…";
      return (before + "〈" + selText + "〉" + after).trim();
    } catch (e) { return ""; }
  }

  function handleSelection() {
    var sel = window.getSelection();
    if (!sel || sel.isCollapsed) { hideToolbar(); return; }
    var text = sel.toString().replace(/\s+/g, " ").trim();
    if (text.length < 2) { hideToolbar(); return; }
    var range = sel.getRangeAt(0);
    if (!article.contains(range.commonAncestorContainer)) { hideToolbar(); return; }
    var rect = range.getBoundingClientRect();
    pendingRange = { text: text, context: getContext(range, text) };
    showToolbar(rect);
  }

  function showToolbar(rect) {
    toolbar.classList.add("show");
    var tw = toolbar.offsetWidth;
    var x = window.scrollX + rect.left + rect.width / 2 - tw / 2;
    var y = window.scrollY + rect.top - toolbar.offsetHeight - 8;
    x = Math.max(8, x);
    if (y < window.scrollY + 4) y = window.scrollY + rect.bottom + 8;
    toolbar.style.left = x + "px";
    toolbar.style.top = y + "px";
    popover.classList.remove("show");
  }
  function hideToolbar() { toolbar.classList.remove("show"); }

  // Bygg verktøylinje-knapper
  Object.keys(INTENTS).forEach(function (key) {
    var b = document.createElement("button");
    b.innerHTML = '<span class="swatch" style="background:' + SWATCH[key] + '"></span>' + INTENTS[key].label;
    b.addEventListener("mousedown", function (ev) { ev.preventDefault(); });
    b.addEventListener("click", function () { openPopover(key); });
    toolbar.appendChild(b);
  });

  // Intent-velger i popoveren (brukes både ved ny og ved redigering)
  var pick = popover.querySelector(".intent-pick");
  Object.keys(INTENTS).forEach(function (key) {
    var b = document.createElement("button");
    b.type = "button";
    b.dataset.intent = key;
    b.innerHTML = '<span class="swatch" style="background:' + SWATCH[key] + '"></span>' + INTENTS[key].label;
    b.addEventListener("mousedown", function (ev) { ev.preventDefault(); });
    b.addEventListener("click", function () {
      popover.dataset.intent = key;
      updateIntentPick(key);
    });
    pick.appendChild(b);
  });
  function updateIntentPick(sel) {
    Array.prototype.forEach.call(pick.children, function (b) {
      b.classList.toggle("active", b.dataset.intent === sel);
    });
  }
  function positionPopover(rect) {
    popover.style.left = Math.max(8, window.scrollX + rect.left) + "px";
    popover.style.top = (window.scrollY + rect.top) + "px";
  }

  function openPopover(intentKey) {
    if (!pendingRange) return;
    editingId = null;
    popover.querySelector(".ph").textContent =
      INTENTS[intentKey].label + " — «" + truncate(pendingRange.text, 60) + "»";
    var ta = popover.querySelector("textarea");
    ta.value = "";
    popover.dataset.intent = intentKey;
    updateIntentPick(intentKey);
    popover.querySelector(".del-edit").style.display = "none";
    popover.classList.add("show");
    positionPopover(toolbar.getBoundingClientRect());
    toolbar.classList.remove("show");
    setTimeout(function () { ta.focus(); }, 10);
  }

  // Åpne popover for å REDIGERE en eksisterende annotering
  function openEdit(id, rect) {
    var a = annotations.filter(function (x) { return x.id === id; })[0];
    if (!a) return;
    editingId = id;
    pendingRange = null;
    popover.querySelector(".ph").textContent = "Rediger — «" + truncate(a.text, 60) + "»";
    var ta = popover.querySelector("textarea");
    ta.value = a.comment || "";
    popover.dataset.intent = a.intent;
    updateIntentPick(a.intent);
    popover.querySelector(".del-edit").style.display = "";
    popover.classList.add("show");
    positionPopover(rect);
    toolbar.classList.remove("show");
    window.getSelection().removeAllRanges();
    setTimeout(function () { ta.focus(); }, 10);
  }

  // Klikk på en markering i artikkelen -> rediger den
  article.addEventListener("click", function (e) {
    var span = e.target.closest ? e.target.closest("span.anno") : null;
    if (!span || !span.dataset.id) return;
    e.preventDefault();
    openEdit(span.dataset.id, span.getBoundingClientRect());
  });

  popover.querySelector(".cancel").addEventListener("click", function () {
    popover.classList.remove("show"); pendingRange = null; editingId = null;
  });
  popover.querySelector(".del-edit").addEventListener("click", function () {
    if (!editingId) return;
    annotations = annotations.filter(function (x) { return x.id !== editingId; });
    editingId = null;
    save();
    popover.classList.remove("show");
    render();
  });
  popover.querySelector(".primary").addEventListener("click", function () {
    var intent = popover.dataset.intent;
    var cmt = popover.querySelector("textarea").value.trim();
    if (editingId) {
      annotations.forEach(function (a) {
        if (a.id === editingId) { a.intent = intent; a.comment = cmt; }
      });
      editingId = null;
      save();
      popover.classList.remove("show");
      render();
      return;
    }
    if (!pendingRange) return;
    annotations.push({
      id: Date.now() + "-" + Math.floor(Math.random() * 1e6),
      intent: intent,
      text: pendingRange.text,
      context: pendingRange.context || "",
      comment: cmt
    });
    save();
    popover.classList.remove("show");
    pendingRange = null;
    window.getSelection().removeAllRanges();
    render();
  });

  // --- Rendering: marker tekst i artikkelen og bygg sidepanel --------------
  function render() {
    clearMarks();
    annotations.forEach(function (a, idx) { markInArticle(a, idx + 1); });
    renderList();
  }

  function clearMarks() {
    var marks = article.querySelectorAll("span.anno");
    marks.forEach(function (m) {
      var parent = m.parentNode;
      while (m.firstChild) {
        if (m.firstChild.classList && m.firstChild.classList.contains("anno-num")) {
          m.removeChild(m.firstChild);
        } else {
          parent.insertBefore(m.firstChild, m);
        }
      }
      parent.removeChild(m);
      parent.normalize();
    });
  }

  // Finn første tekst-treff i artikkelen og pakk det inn. Teller per-tekst
  // forekomster slik at like sitater markeres i rekkefølge.
  var occCounters = {};
  function markInArticle(a, num) {
    var needle = a.text;
    if (!occCounters[needle]) occCounters[needle] = 0;
    var skip = occCounters[needle];
    occCounters[needle]++;

    var walker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT, null);
    var node, found = 0;
    while ((node = walker.nextNode())) {
      var nv = node.nodeValue;
      var pos = nv.indexOf(needle);
      while (pos !== -1) {
        if (found === skip) {
          wrapTextNode(node, pos, needle.length, a, num);
          return;
        }
        found++;
        pos = nv.indexOf(needle, pos + 1);
      }
    }
  }

  function wrapTextNode(node, start, len, a, num) {
    var range = document.createRange();
    range.setStart(node, start);
    range.setEnd(node, start + len);
    var span = document.createElement("span");
    span.className = "anno anno-" + INTENTS[a.intent].cls;
    span.dataset.id = a.id;
    span.title = INTENTS[a.intent].label + (a.comment ? ": " + a.comment : "");
    try {
      range.surroundContents(span);
      var sup = document.createElement("sup");
      sup.className = "anno-num";
      sup.textContent = num;
      span.appendChild(sup);
    } catch (e) { /* range spente over elementgrense — hopp over markering */ }
  }

  function renderList() {
    occCounters = {}; // nullstill for neste render
    listEl.innerHTML = "";
    if (!annotations.length) {
      var em = document.createElement("div");
      em.className = "empty";
      em.textContent = "Ingen annoteringer ennå. Marker tekst i artikkelen for å begynne.";
      listEl.appendChild(em);
      return;
    }
    annotations.forEach(function (a, idx) {
      var item = document.createElement("div");
      item.className = "anno-item";
      var cls = INTENTS[a.intent].cls;
      item.innerHTML =
        '<div class="top">' +
          '<span class="num">' + (idx + 1) + '</span>' +
          '<span class="badge badge-' + cls + '">' + INTENTS[a.intent].label + '</span>' +
          '<button class="edit" title="Endre">Endre</button>' +
          '<button class="del" title="Slett">Slett</button>' +
        '</div>' +
        '<div class="quote">«' + escHtml(truncate(a.text, 90)) + '»</div>' +
        (a.comment ? '<div class="cmt">' + escHtml(a.comment) + '</div>' : '');
      item.querySelector(".edit").addEventListener("click", function () {
        openEdit(a.id, item.getBoundingClientRect());
      });
      item.querySelector(".del").addEventListener("click", function () {
        annotations = annotations.filter(function (x) { return x.id !== a.id; });
        save(); render();
      });
      listEl.appendChild(item);
    });
  }

  // --- Eksport: kompakt annoteringsliste (kun annoteringer, ikke brødtekst) -
  function buildAnnotatedMarkdown() {
    var header = "# Annoteringer — " + SOURCE_FILE + " · «" + TITLE + "»";
    if (!annotations.length) {
      return header + "\n\n(Ingen annoteringer.)\n";
    }
    function occurrences(s) {
      if (!s) return 0;
      var hay = article.textContent.replace(/\s+/g, " ");
      var n = 0, i = 0;
      while ((i = hay.indexOf(s, i)) !== -1) { n++; i += s.length; }
      return n;
    }
    var blocks = annotations.map(function (a, idx) {
      var lines = [(idx + 1) + ". [" + INTENTS[a.intent].upper + "] «" + a.text + "»"];
      // Ta med kontekst kun når markeringen er kort eller forekommer flere ganger
      // (ellers holder vi eksporten kompakt).
      if (a.context && (a.text.length < 30 || occurrences(a.text) > 1)) {
        lines.push("   ↳ i: «" + a.context + "»");
      }
      lines.push("   → " + (a.comment || ""));
      return lines.join("\n");
    });
    return header + "\n\n" + blocks.join("\n\n") + "\n";
  }

  function showExport() {
    var overlay = document.getElementById("exportOverlay");
    var ta = document.getElementById("exportText");
    ta.value = buildAnnotatedMarkdown();
    overlay.classList.add("show");
    ta.focus(); ta.select();
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(ta.value).catch(function () {});
    }
  }

  // --- Helpers --------------------------------------------------------------
  function truncate(s, n) { return s.length > n ? s.slice(0, n - 1) + "…" : s; }
  function escHtml(s) {
    return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  }

  // --- Bind topp-/panel-knapper --------------------------------------------
  document.getElementById("btnExport").addEventListener("click", showExport);
  document.getElementById("btnExportTop").addEventListener("click", showExport);
  document.getElementById("btnClear").addEventListener("click", function () {
    if (!annotations.length) return;
    if (confirm("Tøm alle annoteringer for denne artikkelen?")) {
      annotations = []; save(); render();
    }
  });
  document.getElementById("exportClose").addEventListener("click", function () {
    document.getElementById("exportOverlay").classList.remove("show");
  });
  document.getElementById("exportCopy").addEventListener("click", function () {
    var ta = document.getElementById("exportText");
    ta.select();
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(ta.value);
    } else { document.execCommand("copy"); }
  });
  document.getElementById("sidebarToggle").addEventListener("click", function () {
    sidebar.classList.toggle("mobile-show");
  });

  // Skjul verktøylinje ved klikk utenfor
  document.addEventListener("mousedown", function (e) {
    if (!toolbar.contains(e.target) && !popover.contains(e.target)) {
      if (!window.getSelection().toString().trim()) hideToolbar();
    }
  });

  render();
})();
`;

// ---------------------------------------------------------------------------
// HTML-shell
// ---------------------------------------------------------------------------
function buildPage(meta, body, articleKey, sourceFile) {
  const bodyHtml = markdownToHtml(body);
  const title = meta.title || "Kronikk";

  const metaLine = [meta.serie, meta.lesetid].filter(Boolean).join(" · ");

  return `<!DOCTYPE html>
<html lang="no">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${esc(title)}</title>
<style>${CSS}</style>
</head>
<body>
<button class="sidebar-toggle" id="sidebarToggle">Annoteringer</button>
<div class="page">
  <article>
    ${meta.kicker ? `<p class="kicker">${esc(meta.kicker)}</p>` : ""}
    <h1 class="title">${inline(title)}</h1>
    ${meta.subtitle ? `<p class="subtitle">${inline(meta.subtitle)}</p>` : ""}
    <div class="byline-wrap">
      ${meta.byline ? `<p class="byline">${esc(meta.byline)}</p>` : ""}
      ${metaLine ? `<p class="meta">${esc(metaLine)}</p>` : ""}
    </div>
    <div class="body">
${bodyHtml}
    </div>
  </article>

  <aside class="sidebar" id="annoSidebar">
    <h2>Annoteringer</h2>
    <div class="actions">
      <button class="primary" id="btnExport">Kopier annoteringer</button>
      <button id="btnClear">Tøm alle</button>
    </div>
    <div id="annoList"></div>
  </aside>
</div>

<div class="anno-toolbar" id="annoToolbar"></div>

<div class="anno-popover" id="annoPopover">
  <div class="ph"></div>
  <div class="intent-pick"></div>
  <textarea placeholder="Skriv kommentar (valgfritt)…"></textarea>
  <div class="row">
    <button class="del-edit" style="display:none;">Slett</button>
    <button class="cancel">Avbryt</button>
    <button class="primary">Lagre</button>
  </div>
</div>

<div class="export-overlay" id="exportOverlay">
  <div class="export-box">
    <h3>Annoteringer — kopier og lim tilbake</h3>
    <button class="primary" id="btnExportTop" style="margin-bottom:8px;">Kopier til utklippstavle</button>
    <textarea id="exportText" readonly></textarea>
    <div class="row">
      <button id="exportCopy">Kopier</button>
      <button class="primary" id="exportClose">Lukk</button>
    </div>
  </div>
</div>

<script>
window.__ARTICLE_KEY__ = ${toJsString(articleKey)};
window.__BODY_MD__ = ${toJsString(body)};
window.__SOURCE_FILE__ = ${toJsString(sourceFile)};
window.__TITLE__ = ${toJsString(title)};
</script>
<script>
${CLIENT_JS}
</script>
</body>
</html>
`;
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
// Returnerer antall HTML-filer skrevet. Eksitkoden settes av CLI-guarden under
// (S14/F7): main() kaller aldri process.exit() selv, slik at modulen kan
// importeres/testes uten å drepe prosessen.
export function main() {
  const args = process.argv.slice(2);
  if (!args.length) {
    console.error("Bruk: node build-html.mjs <fil.md> [flere.md ...]");
    return 0;
  }
  // Output følger serien (kjøres fra serie-mappa), ikke scriptet i tools/.
  const outDir = path.join(process.cwd(), "review");
  if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });

  let written = 0;
  for (const arg of args) {
    const inPath = path.isAbsolute(arg) ? arg : path.join(process.cwd(), arg);
    if (!fs.existsSync(inPath)) {
      console.error(`Fant ikke: ${inPath}`);
      continue;
    }
    const raw = fs.readFileSync(inPath, "utf8");
    const { meta, body } = parseFrontMatter(raw);
    const sourceFile = path.basename(inPath);
    const base = sourceFile.replace(/\.md$/i, "");
    const html = buildPage(meta, body, base, sourceFile);
    const outPath = path.join(outDir, base + ".html");
    fs.writeFileSync(outPath, html, "utf8");
    console.log(`Skrev ${outPath} (${(html.length / 1024).toFixed(1)} KB)`);
    written++;
  }

  // S14/F7: en typo'd/manglende input-fil ga tidligere exit 0 uten HTML (stille
  // footgun). Skrev vi ingenting, er det en feil — rapporter og la CLI-guarden
  // sette ikke-null exit.
  if (written === 0) {
    console.error(`Ingen HTML produsert (0 av ${args.length} input-fil(er) funnet) — sjekk filnavn og sti.`);
  }
  return written;
}

// CLI-guard: kjør kun når scriptet startes direkte, ikke ved import
// (mønster fra hooks/scripts/state-updater.mjs). Exit non-zero hvis ingen HTML.
if (import.meta.url === `file://${process.argv[1]}`) {
  process.exit(main() > 0 ? 0 : 1);
}
