// Generează public/ din content/copy.mjs și content/data/*.json.
//
// Nicio cifră din text nu e scrisă de mână: copy.mjs poartă jetoane {nume}, iar
// valorile se calculează aici, o singură dată, din date. qa_assertions.mjs reface
// fiecare calcul pe pagina servită și cade dacă o cifră nu mai iese.

import { readFileSync, writeFileSync, mkdirSync, readdirSync, copyFileSync, rmSync }
  from "node:fs";
import { execFileSync } from "node:child_process";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { copy, strataOrder } from "./content/copy.mjs";

const HERE = dirname(fileURLToPath(import.meta.url));
const DATA = join(HERE, "content", "data");
const OUT = join(HERE, "public");
const BUILT = new Date("2026-07-30T00:00:00Z");

execFileSync("python3", [join(HERE, "tools", "package_repro.py")], {
  cwd: HERE,
  stdio: "inherit",
});
const SOURCE_VERSION = readFileSync(
  join(OUT, "downloads", "straturi-source.sha256"), "utf8",
).trim().split(/\s+/)[0].slice(0, 12);

const j = (n) => JSON.parse(readFileSync(join(DATA, n), "utf8"));
const meta = j("meta.json");
const strata = j("strata.json");
const agreement = j("agreement.json");
const index = j("index.json");

// ——— structura celor două ediții trebuie să fie identică ———————————————————
function shape(o, path = "") {
  if (Array.isArray(o)) return `${path}[]:${o.length}`;
  if (o && typeof o === "object")
    return Object.keys(o).sort().map((k) => shape(o[k], `${path}.${k}`)).join("|");
  return path;
}
{
  const a = shape(copy.ro), b = shape(copy.en);
  if (a !== b) {
    const A = new Set(a.split("|")), B = new Set(b.split("|"));
    const only = (x, y) => [...x].filter((k) => !y.has(k)).slice(0, 12);
    console.error("ro fără en:", only(A, B));
    console.error("en fără ro:", only(B, A));
    throw new Error("copy.mjs: ramurile ro și en nu au aceeași structură");
  }
}

// ——— cifrele ————————————————————————————————————————————————————————————————
const S = Object.fromEntries(strata.map((s) => [s.id, s]));
const ser = meta.series;
const spoken = ser.vorbit, written = ser.scris;
const typesTotal = strata.reduce((a, s) => a + s.types_all, 0);

const pctOf = (st, s) => (s.per[st] || 0) / s.assigned * 100;
const typePct = (st) => S[st].types_all / typesTotal * 100;
const officialYears = Object.values(ser).filter((s) => s.kind === "oficial")
  .sort((a, b) => a.year - b.year);
const histBins = Object.values(ser).filter((s) => s.kind === "istoric")
  .sort((a, b) => a.from - b.from);
const off = (y) => officialYears.find((s) => s.year === y);
const frMaxDoc = officialYears.reduce(
  (a, b) => (pctOf("franceza", b) > pctOf("franceza", a) ? b : a));

const substratDisputed = (agreement.pairs
  .find((p) => p.wikt === "substrat" && p.dex === "necunoscut") || { n: 0 }).n;

function fmtInt(n, lang) {
  const s = Math.round(n).toString();
  const grouped = s.replace(/\B(?=(\d{3})+(?!\d))/g, lang === "ro" ? "." : ",");
  return grouped;
}
function fmtDec(n, lang, digits = 1) {
  const s = n.toFixed(digits);
  return lang === "ro" ? s.replace(".", ",") : s;
}
const pctProse = (n, lang) =>
  lang === "ro" ? `${fmtDec(n, lang)} la sută` : `${fmtDec(n, lang)} per cent`;
const pctShort = (n, lang) => `${fmtDec(n, lang)}%`;

function tokens(lang) {
  const I = (n) => fmtInt(n, lang);
  const P = (n) => pctProse(n, lang);
  const D = (n, d = 1) => fmtDec(n, lang, d);
  const b0 = meta.hist.bins[0];
  return {
    words: I(meta.vocab.shipped),
    layered: I(meta.vocab.layered_lemmas),
    plotted: I(meta.vocab.plotted),
    overrides: I(Object.keys(meta.overrides).length),

    frTypesPct: P(typePct("franceza")),
    latTypesPct: P(typePct("latina")),
    grTypesPct: P(typePct("greaca")),
    enTypes: I(S.engleza.types_all),
    enTypesPct: P(typePct("engleza")),
    unkTypes: I(S.necunoscut.types_all),
    substratDisputed: I(substratDisputed),

    latSpokenPct: P(pctOf("latina", spoken)),
    latWrittenPct: P(pctOf("latina", written)),
    frSpokenPct: P(pctOf("franceza", spoken)),
    frWrittenPct: P(pctOf("franceza", written)),

    slavaRatio: D(pctOf("slava", spoken) / pctOf("slava", written)),
    turcaRatio: D(pctOf("turca", spoken) / pctOf("turca", written)),
    frRatioWritten: D(pctOf("franceza", written) / pctOf("franceza", spoken)),

    slavona1848: P(pctOf("slavona", off(1848))),
    slavona1866: P(pctOf("slavona", off(1866))),
    fr1848: P(pctOf("franceza", off(1848))),
    fr1866: P(pctOf("franceza", off(1866))),
    fr2003: P(pctOf("franceza", off(2003))),
    frMax: P(pctOf("franceza", frMaxDoc)),
    vecini1938: P(pctOf("vecini", off(1938))),
    vecini1965: P(pctOf("vecini", off(1965))),

    histTexts: I(meta.hist.bins.reduce((a, b) => a + b.texts, 0)),
    histTokens: I(meta.hist.total_tokens),
    cap: I(meta.hist.cap_per_author),
    firstBinTokens: I(b0.tokens),
    firstBinAuthors: I(b0.authors),
    firstBinTop: P(b0.top_author_share * 100),

    spokenTokens: I(meta.now.vorbit.tokens),
    writtenTokens: I(meta.now.scris.tokens),
    wikiArticles: I(meta.now.scris.articles),

    dexForms: I(meta.dex_forms),
    dexCompared: I(agreement.compared),
    dexAgree: P(agreement.rate * 100),
    dexFamilyAgree: P(agreement.family_rate * 100),

    assignedSpoken: P(spoken.assigned / spoken.tokens * 100),
    assignedWritten: P(written.assigned / written.tokens * 100),

    date: BUILT.toLocaleDateString(lang === "ro" ? "ro-RO" : "en-GB",
      { day: "numeric", month: "long", year: "numeric" }),
  };
}

function fill(s, tok) {
  if (typeof s !== "string") return s;
  return s.replace(/\{(\w+)\}/g, (m, k) => (k in tok ? tok[k] : m));
}
function fillDeep(o, tok) {
  if (typeof o === "string") return fill(o, tok);
  if (Array.isArray(o)) return o.map((x) => fillDeep(x, tok));
  if (o && typeof o === "object")
    return Object.fromEntries(Object.entries(o).map(([k, v]) => [k, fillDeep(v, tok)]));
  return o;
}

// jetoanele nefolosite sunt o greșeală de scriere, nu o opțiune
{
  const used = new Set();
  const walk = (o) => {
    if (typeof o === "string") for (const m of o.matchAll(/\{(\w+)\}/g)) used.add(m[1]);
    else if (o && typeof o === "object") Object.values(o).forEach(walk);
  };
  walk(copy.ro); walk(copy.en);
  const known = new Set(Object.keys(tokens("ro")));
  const runtime = new Set(["n", "layer", "word", "parts", "date"]);
  const unknown = [...used].filter((k) => !known.has(k) && !runtime.has(k));
  if (unknown.length) throw new Error(`jetoane necunoscute în copy.mjs: ${unknown}`);
}

// ——— unelte de randare ——————————————————————————————————————————————————————
const esc = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;")
  .replace(/>/g, "&gt;").replace(/"/g, "&quot;");
const attr = (s) => esc(s).replace(/'/g, "&#39;");
function richText(value) {
  if (typeof value === "string") return esc(value);
  if (!value || !Array.isArray(value.parts))
    throw new Error("copy.mjs: paragraf rich-text invalid");
  return value.parts.map((part) => {
    if (typeof part === "string") return esc(part);
    return `<a href="${attr(part.href)}">${esc(part.text)}</a>`;
  }).join("");
}

// Numele de limbă ale etimoanelor, pe codurile Wiktionary.
const LANGS = {
  ro: { la: "latină", "la-vul": "latină vulgară", "la-lat": "latină târzie",
    "la-med": "latină medievală", "la-new": "latină nouă", "la-cla": "latină clasică",
    "la-eme": "latină modernă timpurie", "la-pro": "proto-latină",
    "itc-pro": "proto-italică", "itc-ola": "latină arhaică",
    "ine-pro": "indo-europeană comună", "qfa-sub": "substrat preroman",
    sq: "albaneză", aln: "albaneză gegă", cu: "slavonă bisericească",
    chu: "slavonă bisericească", "sla-pro": "protoslavă", sla: "slavă",
    bg: "bulgară", sh: "serbocroată", sr: "sârbă", hr: "croată", uk: "ucraineană",
    ru: "rusă", pl: "poloneză", sk: "slovacă", cs: "cehă", be: "belarusă",
    hu: "maghiară", ohu: "maghiară veche", fi: "finlandeză",
    el: "greacă modernă", grc: "greacă veche", gkm: "greacă bizantină",
    "grc-koi": "greacă koine", ota: "turcă osmanlie", tr: "turcă",
    crh: "tătară crimeeană", qwm: "cumană", trk: "turcică", az: "azeră",
    fa: "persană", ar: "arabă", he: "ebraică", arc: "aramaică", syc: "siriacă",
    sa: "sanscrită", hi: "hindi", ur: "urdu", tt: "tătară",
    de: "germană", gmh: "germană medie", goh: "germană veche", bar: "bavareză",
    nl: "neerlandeză", dum: "neerlandeză medie", yi: "idiș", gem: "germanică",
    "gem-pro": "protogermanică", got: "gotică", non: "nordică veche",
    sv: "suedeză", da: "daneză", no: "norvegiană", is: "islandeză",
    fr: "franceză", frm: "franceză medie", fro: "franceză veche",
    oc: "occitană", ca: "catalană", es: "spaniolă", pt: "portugheză",
    it: "italiană", vec: "venetă", nap: "napolitană", scn: "siciliană",
    en: "engleză", enm: "engleză medie", ang: "engleză veche",
    ja: "japoneză", zh: "chineză", ko: "coreeană", ms: "malaeză",
    ta: "tamilă", sw: "swahili", tpi: "tok pisin", qu: "quechua",
    nah: "nahuatlă", "ase-": "altă limbă", rup: "aromână", ruq: "meglenoromână",
    ruo: "istroromână", roa: "romanică", "roa-opt": "portugheză veche",
    la_: "latină", mul: "internațională", und: "necunoscută",
  },
  en: { la: "Latin", "la-vul": "Vulgar Latin", "la-lat": "Late Latin",
    "la-med": "Medieval Latin", "la-new": "New Latin", "la-cla": "Classical Latin",
    "la-eme": "Early Modern Latin", "la-pro": "Proto-Latin",
    "itc-pro": "Proto-Italic", "itc-ola": "Old Latin",
    "ine-pro": "Proto-Indo-European", "qfa-sub": "pre-Roman substrate",
    sq: "Albanian", aln: "Gheg Albanian", cu: "Old Church Slavonic",
    chu: "Church Slavonic", "sla-pro": "Proto-Slavic", sla: "Slavic",
    bg: "Bulgarian", sh: "Serbo-Croatian", sr: "Serbian", hr: "Croatian",
    uk: "Ukrainian", ru: "Russian", pl: "Polish", sk: "Slovak", cs: "Czech",
    be: "Belarusian", hu: "Hungarian", ohu: "Old Hungarian", fi: "Finnish",
    el: "Modern Greek", grc: "Ancient Greek", gkm: "Byzantine Greek",
    "grc-koi": "Koine Greek", ota: "Ottoman Turkish", tr: "Turkish",
    crh: "Crimean Tatar", qwm: "Cuman", trk: "Turkic", az: "Azerbaijani",
    fa: "Persian", ar: "Arabic", he: "Hebrew", arc: "Aramaic", syc: "Syriac",
    sa: "Sanskrit", hi: "Hindi", ur: "Urdu", tt: "Tatar",
    de: "German", gmh: "Middle High German", goh: "Old High German",
    bar: "Bavarian", nl: "Dutch", dum: "Middle Dutch", yi: "Yiddish",
    gem: "Germanic", "gem-pro": "Proto-Germanic", got: "Gothic",
    non: "Old Norse", sv: "Swedish", da: "Danish", no: "Norwegian",
    is: "Icelandic", fr: "French", frm: "Middle French", fro: "Old French",
    oc: "Occitan", ca: "Catalan", es: "Spanish", pt: "Portuguese",
    it: "Italian", vec: "Venetian", nap: "Neapolitan", scn: "Sicilian",
    en: "English", enm: "Middle English", ang: "Old English",
    ja: "Japanese", zh: "Chinese", ko: "Korean", ms: "Malay",
    ta: "Tamil", sw: "Swahili", tpi: "Tok Pisin", qu: "Quechua",
    nah: "Nahuatl", rup: "Aromanian", ruq: "Megleno-Romanian",
    ruo: "Istro-Romanian", roa: "Romance", "roa-opt": "Old Portuguese",
    la_: "Latin", mul: "international", und: "unknown",
  },
};

function portfolioFor(c) {
  return c.lang === "ro"
    ? { href: "https://mariuscomper.uk/", label: "Toate proiectele" }
    : { href: "https://mariuscomper.uk/en/", label: "All projects" };
}

function shell({ c, tok, pageTitle, desc, canonical, body, extraHead = "", cls = "" }) {
  const t = c.ui, n = c.nav;
  const portfolio = portfolioFor(c);
  const markdown = canonical === "/" ? "/index.md" : `${canonical}index.md`;
  return `<!doctype html>
<html lang="${c.lang}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${esc(pageTitle)}</title>
<meta name="description" content="${attr(desc)}">
<link rel="canonical" href="https://straturi.mariuscomper.uk${canonical}">
<link rel="alternate" hreflang="ro" href="https://straturi.mariuscomper.uk/">
<link rel="alternate" hreflang="en" href="https://straturi.mariuscomper.uk/en/">
<link rel="alternate" type="text/markdown" href="${markdown}">
<meta property="og:type" content="article">
<meta property="og:title" content="${attr(pageTitle)}">
<meta property="og:description" content="${attr(desc)}">
<meta property="og:url" content="https://straturi.mariuscomper.uk${canonical}">
<meta property="og:image" content="https://straturi.mariuscomper.uk/assets/og${c.lang === "en" ? "-en" : ""}.png">
<meta property="og:image:alt" content="${attr(c.ogAlt)}">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/assets/site.css">
${extraHead}
</head>
<body class="${cls}" data-lang="${c.lang}">
<span id="top" class="top-target" tabindex="-1"></span>
<a class="skip" href="#main">${c.lang === "ro" ? "Sari la conținut" : "Skip to content"}</a>
<header class="masthead">
  <a class="wordmark" href="${c.dir}">${esc(c.title)}</a>
  <nav aria-label="${c.lang === "ro" ? "Secțiuni" : "Sections"}">
    <a href="${c.dir}${c.lang === "ro" ? "metoda/" : "method/"}">${esc(n.method)}</a>
    <a href="${c.dir}${c.lang === "ro" ? "surse/" : "sources/"}">${esc(n.sources)}</a>
    <a class="lang" href="${c.other.href}" hreflang="${c.lang === "ro" ? "en" : "ro"}">${esc(c.other.label)}</a>
  </nav>
</header>
<main id="main">
${body}
</main>
<footer class="foot">
  <p>${esc(fill(c.footer.updated, tok))} <a href="${c.other.href}">${esc(c.other.full)}</a></p>
  <p class="thin">${esc(c.footer.byline)} · <a href="${portfolio.href}">${portfolio.label}</a></p>
  <a class="back-to-top" href="#top">${esc(n.top)}</a>
</footer>
<script src="/assets/site.js" type="module"></script>
</body>
</html>
`;
}

// ——— coloana stratigrafică ————————————————————————————————————————————————
function columnHtml(c, tok) {
  const order = [...strataOrder].reverse();  // cel mai nou sus
  const modes = {
    types: (st) => typePct(st),
    spoken: (st) => pctOf(st, spoken),
    written: (st) => pctOf(st, written),
  };
  const vals = Object.fromEntries(order.map((st) => [st,
    Object.fromEntries(Object.entries(modes).map(([k, f]) => [k, f(st)]))]));
  const data = (st) => `data-stratum="${st}"
    data-types="${vals[st].types.toFixed(3)}"
    data-spoken="${vals[st].spoken.toFixed(3)}"
    data-written="${vals[st].written.toFixed(3)}"`;
  // Banda poartă proporția și nimic altceva; numele stă alături, ca stratul de
  // trei zecimi de procent să rămână o dungă adevărată, nu una rotunjită în sus
  // ca să încapă un cuvânt în ea.
  const bands = order.map((st) => `    <div class="band" ${data(st)}
      style="--h:${vals[st].types.toFixed(3)}"
      role="img" aria-label="${attr(`${c.strata[st].name}: ${pctShort(vals[st].types, c.lang)}`)}"></div>`).join("\n");
  const legend = order.map((st) => `    <li class="leg" ${data(st)}>
      <span class="leg-name">${esc(c.strata[st].name)}</span>
      <span class="leg-val">${pctShort(vals[st].types, c.lang)}</span>
    </li>`).join("\n");
  return `<figure class="column-figure">
  <div class="col-controls" role="group" aria-label="${attr(c.column.title)}">
    <button type="button" data-mode="types" class="on" aria-pressed="true">${esc(c.column.modeTypes)}</button>
    <button type="button" data-mode="spoken" aria-pressed="false">${esc(c.column.modeTokens)}</button>
    <button type="button" data-mode="written" aria-pressed="false">${esc(c.column.modeWritten)}</button>
  </div>
  <div class="column-wrap">
    <div class="column" id="column">
${bands}
    </div>
    <ol class="col-legend">
${legend}
    </ol>
  </div>
  <figcaption>${esc(c.column.note)}</figcaption>
</figure>`;
}

// ——— grafic: raportul vorbit/scris pe straturi —————————————————————————————
function registerChart(c) {
  const rows = strataOrder.map((st) => {
    const sp = pctOf(st, spoken), wr = pctOf(st, written);
    const ratio = wr > 0 ? sp / wr : 1;
    return { st, sp, wr, ratio, l: Math.log2(ratio) };
  });
  const max = Math.max(...rows.map((r) => Math.abs(r.l))) * 1.05;
  const mid = 50;
  const bars = rows.map((r) => {
    const w = Math.abs(r.l) / max * 46;
    const x = r.l >= 0 ? mid : mid - w;
    const cls = r.l >= 0 ? "sp" : "wr";
    const label = `${c.strata[r.st].name}: ${fmtDec(r.ratio, c.lang, 2)}×`;
    return `<div class="rrow">
  <span class="rlabel">${esc(c.strata[r.st].name)}</span>
  <span class="rtrack" role="img" aria-label="${attr(label)}">
    <span class="rzero"></span>
    <span class="rbar ${cls}" style="left:${x.toFixed(2)}%;width:${w.toFixed(2)}%"></span>
    <span class="rnum ${r.l >= 0 ? "right" : "left"}">${fmtDec(r.ratio, c.lang, 2)}×</span>
  </span>
</div>`;
  }).join("\n");
  return `<figure class="reg-figure">
  <div class="reg-legend"><span><i class="sw sp"></i>${esc(c.findings.register.spokenMore)}</span><span><i class="sw wr"></i>${esc(c.findings.register.writtenMore)}</span></div>
  <div class="rchart">
${bars}
  </div>
</figure>`;
}

// ——— grafic: textul oficial, mici multipli ————————————————————————————————
function officialChart(c) {
  const layers = ["franceza", "latina", "slavona", "vecini"];
  const years = officialYears.map((d) => d.year);
  const x0 = Math.min(...years), x1 = Math.max(...years);
  const W = 260, H = 120, PL = 34, PR = 8, PT = 10, PB = 22;
  const panels = layers.map((st) => {
    const pts = officialYears.map((d) => ({ y: d.year, v: pctOf(st, d) }));
    const vmax = Math.max(...pts.map((p) => p.v)) || 1;
    const top = vmax <= 5 ? Math.ceil(vmax) : Math.ceil(vmax / 10) * 10;
    const sx = (y) => PL + (y - x0) / (x1 - x0) * (W - PL - PR);
    const sy = (v) => H - PB - v / top * (H - PT - PB);
    const d = pts.map((p, i) => `${i ? "L" : "M"}${sx(p.y).toFixed(1)},${sy(p.v).toFixed(1)}`).join("");
    const dots = pts.map((p) => `<circle cx="${sx(p.y).toFixed(1)}" cy="${sy(p.v).toFixed(1)}" r="3.2"><title>${p.y}: ${pctShort(p.v, c.lang)}</title></circle>`).join("");
    const ticks = [x0, 1923, 1965, x1].map((y) =>
      `<text x="${sx(y).toFixed(1)}" y="${H - 6}" class="tick" text-anchor="middle">${y}</text>`).join("");
    return `<figure class="panel">
  <figcaption>${esc(c.strata[st].name)}</figcaption>
  <svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${attr(c.strata[st].name)}">
    <line x1="${PL}" y1="${H - PB}" x2="${W - PR}" y2="${H - PB}" class="axis"/>
    <line x1="${PL}" y1="${PT}" x2="${PL}" y2="${H - PB}" class="axis"/>
    <text x="${PL - 5}" y="${sy(top) + 4}" class="tick" text-anchor="end">${pctShort(top, c.lang)}</text>
    <text x="${PL - 5}" y="${H - PB + 4}" class="tick" text-anchor="end">0</text>
    <path d="${d}" class="line"/>
    <g class="dots">${dots}</g>
    ${ticks}
  </svg>
</figure>`;
  }).join("\n");
  const table = `<details class="tbl"><summary>${esc(c.ui.tableView)}</summary>
<table><thead><tr><th>${esc(c.ui.year)}</th><th>${esc(c.ui.document)}</th>${layers.map((st) => `<th>${esc(c.strata[st].name)}</th>`).join("")}</tr></thead><tbody>
${officialYears.map((d) => `<tr><td>${d.year}</td><td>${esc(d.label)}</td>${layers.map((st) => `<td>${pctShort(pctOf(st, d), c.lang)}</td>`).join("")}</tr>`).join("\n")}
</tbody></table></details>`;
  return `<div class="panels">
${panels}
</div>
<p class="axis-note">${esc(c.findings.official.axis)}</p>
${table}`;
}

// ——— grafic: inversiunea ————————————————————————————————————————————————
function inversionChart(c) {
  const picked = ["franceza", "latina", "slavona", "maghiara", "vecini", "germana",
    "turca", "greaca"];
  const rows = picked.map((st) => ({
    st, t: typePct(st), s: pctOf(st, spoken),
  })).sort((a, b) => b.t - a.t);
  const max = Math.max(...rows.flatMap((r) => [r.t, r.s]));
  const bar = (v, cls) =>
    `<span class="ibar ${cls}" style="width:${(v / max * 100).toFixed(2)}%"></span>`;
  return `<figure class="inv-figure">
  <div class="inv-head"><span></span><span>${esc(c.findings.inversion.typesLabel)}</span><span>${esc(c.findings.inversion.tokensLabel)}</span></div>
${rows.map((r) => `  <div class="irow">
    <span class="ilabel">${esc(c.strata[r.st].name)}</span>
    <span class="itrack">${bar(r.t, "t")}<b>${pctShort(r.t, c.lang)}</b></span>
    <span class="itrack">${bar(r.s, "s")}<b>${pctShort(r.s, c.lang)}</b></span>
  </div>`).join("\n")}
</figure>`;
}

// ——— cardurile straturilor ————————————————————————————————————————————————
function strataCards(c, tok) {
  return strataOrder.map((st) => {
    const s = c.strata[st], d = S[st];
    const ex = d.examples.slice(0, 10);
    return `<article class="stratum" id="strat-${st}">
  <header>
    <h3>${esc(s.name)}</h3>
    <p class="when">${esc(s.when)} · ${esc(s.who)}</p>
  </header>
  <p>${esc(s.body)}</p>
  <dl class="figures">
    <div><dt>${esc(c.ui.types)}</dt><dd>${fmtInt(d.types_all, c.lang)}</dd></div>
    <div><dt>${esc(c.ui.spokenShare)}</dt><dd>${pctShort(pctOf(st, spoken), c.lang)}</dd></div>
    <div><dt>${esc(c.ui.writtenShare)}</dt><dd>${pctShort(pctOf(st, written), c.lang)}</dd></div>
  </dl>
  <p class="examples"><span>${esc(c.ui.examplesLabel)}</span> ${ex.map((w) =>
      `<button type="button" class="chip" data-word="${attr(w)}">${esc(w)}</button>`).join(" ")}</p>
</article>`;
  }).join("\n");
}

// ——— pagina principală ————————————————————————————————————————————————————
function homePage(lang) {
  const tok = tokens(lang);
  const c = fillDeep(copy[lang], tok);
  const portfolio = portfolioFor(c);
  const body = `
<section class="hero">
  <p class="kicker">${esc(c.hero.kicker)}</p>
  <h1>${esc(c.title)}</h1>
  <p class="tagline">${esc(c.tagline)}</p>
  <p class="lede">${esc(c.hero.lede)}</p>
</section>

<section class="lookup" id="lookup">
  <form class="searchbox" role="search" autocomplete="off">
    <label for="q">${esc(c.ui.searchLabel)}</label>
    <div class="field">
      <input id="q" name="q" type="search" placeholder="${attr(c.hero.placeholder)}"
        spellcheck="false" autocapitalize="off" enterkeyhint="search"
        aria-describedby="qhint" aria-expanded="false" aria-controls="ac"
        role="combobox">
      <button type="button" class="dice" data-random title="${attr(c.hero.randomLabel)}"
        aria-label="${attr(c.hero.randomLabel)}">↻</button>
    </div>
    <ul id="ac" class="ac" role="listbox" hidden></ul>
    <p id="qhint" class="hint">${esc(c.hero.hint)}</p>
  </form>
  <div id="result" class="result" aria-live="polite"></div>
</section>

<section class="prose">
  <h2>${esc(c.intro.title)}</h2>
  ${c.intro.body.map((p) => `<p>${esc(p)}</p>`).join("\n  ")}
  ${inversionChart(c)}
  <p class="figure-note">${esc(c.findings.inversion.body)}</p>
</section>

<section class="prose wide">
  <h2>${esc(c.column.title)}</h2>
  <p>${esc(c.column.body)}</p>
  ${columnHtml(c, tok)}
</section>

<section class="prose">
  <h2>${esc(c.findings.register.title)}</h2>
  ${c.findings.register.body.map((p) => `<p>${esc(p)}</p>`).join("\n  ")}
  ${registerChart(c)}
</section>

<section class="prose wide">
  <h2>${esc(c.findings.official.title)}</h2>
  ${c.findings.official.body.map((p) => `<p>${esc(p)}</p>`).join("\n  ")}
  ${officialChart(c)}
</section>

<section class="prose strata-list">
  <h2>${esc(c.ui.strataTitle)}</h2>
  ${strataCards(c, tok)}
</section>
`;
  return shell({
    c, tok, pageTitle: `${c.title} — ${c.tagline}`, desc: c.metaDescription,
    canonical: c.dir, body, cls: "home",
    extraHead: `<link rel="preload" href="/data/index.json" as="fetch" crossorigin>
<script type="application/ld+json">${JSON.stringify({
      "@context": "https://schema.org", "@type": "Article",
      headline: c.title, description: c.metaDescription,
      inLanguage: c.lang, datePublished: BUILT.toISOString().slice(0, 10),
      author: { "@type": "Person", name: c.footer.byline, url: portfolio.href },
    })}</script>`,
  });
}

// ——— metodă ————————————————————————————————————————————————————————————————
function methodPage(lang) {
  const tok = tokens(lang);
  const c = fillDeep(copy[lang], tok);
  const ov = Object.entries(meta.overrides).map(([w, o]) =>
    `<tr><td><b>${esc(w)}</b></td><td>${esc(c.strata[o.layer].name)}</td><td>${esc(o.why)}</td></tr>`).join("\n");
  const steps = [
    "python3 tools/fetch_categories.py", "python3 tools/dex_extract.py",
    "python3 tools/ws_index.py", "python3 tools/corpus_hist.py",
    "python3 tools/corpus_now.py", "python3 tools/official.py",
    "python3 tools/shortlist.py", "python3 tools/fetch_entries.py",
    "python3 tools/dex_check.py", "python3 tools/audit_top.py",
    "python3 tools/analyse.py", "node build.mjs", "node qa_assertions.mjs",
  ];
  const body = `
<section class="prose narrow">
  <h1>${esc(c.method.title)}</h1>
  <p class="lede">${esc(c.method.lede)}</p>
  ${c.method.sections.map((s) => `<h2>${esc(s.h)}</h2>\n  ${s.p.map((p) => `<p>${richText(p)}</p>`).join("\n  ")}`).join("\n  ")}

  <h2>${esc(c.method.overridesTitle)}</h2>
  <p>${esc(c.method.overridesIntro)}</p>
  <table class="ov"><thead><tr><th>${esc(lang === "ro" ? "Cuvânt" : "Word")}</th><th>${esc(c.ui.layer)}</th><th>${esc(lang === "ro" ? "Motiv" : "Reason")}</th></tr></thead><tbody>
${ov}
  </tbody></table>

  <h2>${esc(c.method.reproTitle)}</h2>
  <p>${esc(c.method.reproIntro)}</p>
  <p><a class="source-download" href="/downloads/straturi-source-${SOURCE_VERSION}.zip"
    download>${esc(c.method.reproDownload)}</a>
    <a class="checksum-link" href="/downloads/straturi-source-${SOURCE_VERSION}.sha256">SHA-256</a></p>
  <p class="repro-note">${esc(c.method.reproNote)}</p>
  <pre class="steps">${steps.map(esc).join("\n")}</pre>
</section>`;
  return shell({
    c, tok, pageTitle: `${c.method.title} — ${c.title}`, desc: c.method.lede,
    canonical: `${c.dir}${lang === "ro" ? "metoda/" : "method/"}`, body,
  });
}

// ——— surse ——————————————————————————————————————————————————————————————————
function sourcesPage(lang) {
  const tok = tokens(lang);
  const c = fillDeep(copy[lang], tok);
  const body = `
<section class="prose narrow">
  <h1>${esc(c.sources.title)}</h1>
  <p class="lede">${esc(c.sources.lede)}</p>
  ${c.sources.groups.map((g) => `<h2>${esc(g.h)}</h2>
  <ul class="srcs">
${g.items.map((i) => `    <li><p class="t">${i.u ? `<a href="${attr(i.u)}" rel="noopener">${esc(i.t)}</a>` : esc(i.t)}</p><p class="d">${esc(i.d)}</p></li>`).join("\n")}
  </ul>`).join("\n  ")}
  <h2>${esc(c.sources.creditsTitle)}</h2>
  <p>${esc(c.sources.credits)}</p>
</section>`;
  return shell({
    c, tok, pageTitle: `${c.sources.title} — ${c.title}`, desc: c.sources.lede,
    canonical: `${c.dir}${lang === "ro" ? "surse/" : "sources/"}`, body,
  });
}

function notFoundPage() {
  const tok = tokens("ro");
  const c = fillDeep(copy.ro, tok);
  return shell({
    c, tok, pageTitle: `404 — ${c.title}`, desc: c.metaDescription,
    canonical: "/", body: `<section class="prose narrow"><h1>404</h1>
<p>Pagina nu există. <a href="/">Înapoi la straturi</a>.</p></section>`,
  });
}

// ——— scriere ————————————————————————————————————————————————————————————————
function write(rel, s) {
  const p = join(OUT, rel);
  mkdirSync(dirname(p), { recursive: true });
  writeFileSync(p, s);
}

function decodeEntities(s) {
  return s
    .replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
}

function markdownFromHtml(html, title, canonical, lang) {
  const main = (html.match(/<main[^>]*>([\s\S]*?)<\/main>/i) || [null, ""])[1];
  const text = main
    .replace(/<svg[\s\S]*?<\/svg>/gi, "")
    .replace(/<script[\s\S]*?<\/script>/gi, "")
    .replace(/<style[\s\S]*?<\/style>/gi, "")
    .replace(/<a[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi, "[$2]($1)")
    .replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, "\n# $1\n")
    .replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, "\n## $1\n")
    .replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, "\n### $1\n")
    .replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, "\n- $1")
    .replace(/<(p|figcaption|summary)[^>]*>([\s\S]*?)<\/\1>/gi, "\n$2\n")
    .replace(/<\/(tr|article|section|figure|div|dl)>/gi, "\n")
    .replace(/<\/(td|th)>/gi, " | ")
    .replace(/<br\s*\/?>/gi, "\n")
    .replace(/<[^>]+>/g, "")
    .replace(/[ \t]+\n/g, "\n")
    .replace(/\n{3,}/g, "\n\n")
    .trim();
  return `---\ntitle: "${title.replace(/"/g, '\\"')}"\nlang: ${lang}\ncanonical: https://straturi.mariuscomper.uk${canonical}\nupdated: 2026-07-30\n---\n\n${decodeEntities(text)}\n`;
}

for (const d of ["en", "metoda", "surse", "data"]) {
  try { rmSync(join(OUT, d), { recursive: true }); } catch {}
}
try { rmSync(join(OUT, "index.html")); } catch {}

const generatedPages = [
  ["index", "ro", "/", copy.ro.title, homePage("ro")],
  ["en/index", "en", "/en/", copy.en.title, homePage("en")],
  ["metoda/index", "ro", "/metoda/", copy.ro.method.title, methodPage("ro")],
  ["en/method/index", "en", "/en/method/", copy.en.method.title, methodPage("en")],
  ["surse/index", "ro", "/surse/", copy.ro.sources.title, sourcesPage("ro")],
  ["en/sources/index", "en", "/en/sources/", copy.en.sources.title, sourcesPage("en")],
];
for (const [rel, lang, canonical, title, html] of generatedPages) {
  write(`${rel}.html`, html);
  write(`${rel}.md`, markdownFromHtml(html, title, canonical, lang));
}
write("404.html", notFoundPage());

// datele publicate: dicționarul pe felii, indexul, agregatele
for (const dir of ["words", "forms"]) {
  mkdirSync(join(OUT, "data", dir), { recursive: true });
  for (const f of readdirSync(join(DATA, dir)))
    copyFileSync(join(DATA, dir, f), join(OUT, "data", dir, f));
}
for (const f of ["index.json", "strata.json", "meta.json", "agreement.json"])
  copyFileSync(join(DATA, f), join(OUT, "data", f));
copyFileSync(
  join(HERE, "DEXONLINE-NOTICE.md"),
  join(OUT, "downloads", "DEXONLINE-NOTICE.md"),
);

// numele straturilor și etichetele de care are nevoie clientul
const client = {};
for (const lang of ["ro", "en"]) {
  const tok = tokens(lang);
  const c = fillDeep(copy[lang], tok);
  client[lang] = {
    strata: Object.fromEntries(strataOrder.map((st) => [st, {
      name: c.strata[st].name, when: c.strata[st].when, who: c.strata[st].who,
      curve: meta.hist.bins.map((b) => {
        const s = ser[`h${b.from}`];
        return +(pctOf(st, s)).toFixed(3);
      }),
      spoken: +pctOf(st, spoken).toFixed(3),
      written: +pctOf(st, written).toFixed(3),
    }])),
    langs: LANGS[lang],
    result: c.result,
    ui: c.ui,
    bins: meta.hist.bins.map((b) => [b.from, b.to]),
  };
}
write("data/client.json", JSON.stringify(client));

const pages = ["/", "/en/", "/metoda/", "/en/method/", "/surse/", "/en/sources/"];
write("sitemap.xml", `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${pages.map((p) => `  <url><loc>https://straturi.mariuscomper.uk${p}</loc><lastmod>${BUILT.toISOString().slice(0, 10)}</lastmod></url>`).join("\n")}
</urlset>
`);
write("robots.txt", `User-agent: *
Allow: /
Content-Signal: ai-train=no, search=yes, ai-input=yes
Sitemap: https://straturi.mariuscomper.uk/sitemap.xml
`);
write("_headers", `/assets/*
  Cache-Control: public, max-age=31536000, immutable
/data/*
  Cache-Control: public, max-age=3600
  Access-Control-Allow-Origin: *
/*
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
`);
write("llms.txt", `# Straturile unui cuvânt / The Layers of a Word

Stratigrafia lexicului românesc: pentru ${fmtInt(meta.vocab.shipped, "ro")} de cuvinte,
donatorul proxim, lanțul etimologic, prima apariție într-un corpus de proză datată
1825-1949 și frecvența în două registre contemporane.

- Ediția română: https://straturi.mariuscomper.uk/
- English edition: https://straturi.mariuscomper.uk/en/
- Metodă: https://straturi.mariuscomper.uk/metoda/
- Date: /data/index.json, /data/words/<literă>.json, /data/strata.json, /data/meta.json

Măsurători proprii, nu compilate din altă parte:
- ${pctShort(typePct("franceza"), "ro")} din lexemele cu donator cunoscut vin din franceză, dar
  numai ${pctShort(pctOf("franceza", spoken), "ro")} din cuvintele unei conversații; latina moștenită dă
  ${pctShort(typePct("latina"), "ro")} din lexeme și ${pctShort(pctOf("latina", spoken), "ro")} din conversație.
- Procentele pe corpus se raportează la cuvintele acoperite de clasificare:
  ${pctShort(spoken.assigned / spoken.tokens * 100, "ro")} în vorbire și
  ${pctShort(written.assigned / written.tokens * 100, "ro")} în scris.
- Cuvintele turcești sunt de ${fmtDec(pctOf("turca", spoken) / pctOf("turca", written), "ro", 2)} ori mai dese in vorbire decât în scris;
  cele franceze de ${fmtDec(pctOf("franceza", written) / pctOf("franceza", spoken), "ro", 2)} ori mai dese în scris decât în vorbire.
- În nouă texte constituționale, 1848-2003, franceza urcă de la ${pctShort(pctOf("franceza", off(1848)), "ro")}
  la ${pctShort(pctOf("franceza", off(2003)), "ro")}, iar turca e practic absentă la fiecare dată.

Surse: dexonline (set oficial de export; date redistribuite sub GPL v2+),
Wiktionary (categorii etimologice),
Wikisource RO (corpus istoric), Wikipedia RO, OpenSubtitles, Wikidata.
`);

console.log("scris public/ —",
  `${meta.vocab.shipped} cuvinte, ${readdirSync(join(OUT, "data", "words")).length} felii`);
