#!/usr/bin/env python3
"""Build the Frontier Ideas bundle: standalone site + WordPress fragments + manifest."""
import html
import json
import hashlib
import re
import shutil
from datetime import datetime, timezone
from pathlib import Path

from data_1 import IDEAS as A
from data_2 import IDEAS as B
from data_3 import IDEAS as C

IDEAS = A + B + C
SITE_BASE = "https://incoprea.com/frontier"
BUILT = "30 October 2026"
ROOT = Path(__file__).resolve().parent
OUT = ROOT.parent / "out"

esc = html.escape


# ---------------------------------------------------------------- validation
def validate():
    assert len(IDEAS) == 24, f"expected 24 ideas, got {len(IDEAS)}"
    slugs = [i["slug"] for i in IDEAS]
    assert len(set(slugs)) == 24, "duplicate slug"
    assert [i["n"] for i in IDEAS] == list(range(1, 25)), "numbering broken"
    for i in IDEAS:
        for key in ("title", "dek", "thesis", "gap", "why_now", "do", "success",
                    "kill", "ifworks", "changed", "idea", "risks", "sources", "domain", "kind"):
            assert i.get(key), f"{i['slug']} missing {key}"
        assert len(i["changed"]) >= 2, f"{i['slug']} needs 2+ frontier facts"
        assert len(i["idea"]) >= 2, f"{i['slug']} needs 2+ idea paragraphs"


DOMAINS = []
for i in IDEAS:
    if i["domain"] not in DOMAINS:
        DOMAINS.append(i["domain"])


# ---------------------------------------------------------------- helpers
def words(idea):
    parts = [idea["dek"], idea["thesis"], idea["gap"], idea["why_now"],
             idea["do"], idea["success"], idea["kill"], idea["ifworks"]]
    parts += idea["idea"] + idea["risks"] + [c[0] for c in idea["changed"]]
    parts += [t for t, _ in idea["sources"]]
    return sum(len(p.split()) for p in parts)


def refs(idea):
    """Ordered reference list plus url -> number map."""
    ordered, seen = [], {}
    for title, url in idea["sources"]:
        if url not in seen:
            ordered.append((title, url))
            seen[url] = len(ordered)
    for text, url, label in idea["changed"]:
        if url not in seen:
            ordered.append((label, url))
            seen[url] = len(ordered)
    return ordered, seen


def cite(idea, url):
    _, seen = refs(idea)
    return seen.get(url)


def reading_time(idea):
    return max(2, round(words(idea) / 200))


def meta_desc(idea):
    d = re.sub(r"\s+", " ", idea["thesis"])
    return d if len(d) <= 158 else d[:155].rsplit(" ", 1)[0] + "..."


def sha256(path):
    h = hashlib.sha256()
    h.update(path.read_bytes())
    return h.hexdigest()


# ---------------------------------------------------------------- styles
CSS = """
:root{
  --paper:#f6f3ec; --ink:#191811; --muted:#6f6857; --rule:#dcd5c4;
  --accent:#a8412a; --accent-soft:#efe1da; --card:#fffdf8;
}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{margin:0;background:var(--paper);color:var(--ink);
  font-family:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
  font-size:18px;line-height:1.62;text-rendering:optimizeLegibility}
a{color:var(--accent);text-decoration:none;border-bottom:1px solid rgba(168,65,42,.32)}
a:hover{border-bottom-color:var(--accent)}
.wrap{max-width:46rem;margin:0 auto;padding:0 1.35rem}
body.index .wrap{max-width:68rem}
.masthead{border-bottom:1px solid var(--rule);background:rgba(246,243,236,.94);
  position:sticky;top:0;z-index:5;backdrop-filter:saturate(1.1) blur(6px)}
.masthead .wrap{display:flex;align-items:baseline;justify-content:space-between;
  gap:1rem;padding-top:.85rem;padding-bottom:.8rem;max-width:70rem}
.wordmark{font-family:ui-sans-serif,-apple-system,"Helvetica Neue",Arial,sans-serif;
  font-size:.72rem;letter-spacing:.22em;text-transform:uppercase;font-weight:600;
  border:0;color:var(--ink)}
.wordmark span{color:var(--accent)}
.masthead .meta{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:.66rem;letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}
.kicker{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.68rem;
  letter-spacing:.14em;text-transform:uppercase;color:var(--muted);
  margin:2.6rem 0 .9rem;display:flex;flex-wrap:wrap;gap:.55rem;align-items:center}
.kicker .n{color:var(--accent);font-weight:700}
.kicker .dot{color:var(--rule)}
h1{font-size:clamp(2.1rem,5.2vw,3.15rem);line-height:1.06;letter-spacing:-.018em;
  font-weight:600;margin:.2rem 0 .7rem}
.dek{font-size:1.22rem;line-height:1.45;color:#3c382e;margin:0 0 1.9rem;
  font-style:italic;max-width:38rem}
.thesis{border-left:2px solid var(--accent);margin:0 0 2.6rem;padding:.15rem 0 .15rem 1.15rem;
  font-size:1.1rem;line-height:1.55;color:#2b2820}
.thesis em{font-style:normal;background:var(--accent-soft);padding:0 .15em}
h2{font-family:ui-sans-serif,-apple-system,"Helvetica Neue",Arial,sans-serif;
  font-size:.72rem;letter-spacing:.18em;text-transform:uppercase;font-weight:700;
  color:var(--ink);margin:2.9rem 0 .9rem;display:flex;gap:.7rem;align-items:baseline}
h2 .num{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--accent);
  font-weight:400;font-size:.7rem}
h2:after{content:"";flex:1;height:1px;background:var(--rule);transform:translateY(-.2em)}
section p{margin:0 0 1.05rem}
.changed{list-style:none;padding:0;margin:0;display:grid;gap:1.15rem}
.changed li{position:relative;padding-left:1.9rem;font-size:.97rem;line-height:1.55;
  color:#353128}
.changed li:before{content:"";position:absolute;left:.15rem;top:.62em;width:.45rem;
  height:.45rem;border-radius:50%;background:var(--accent)}
.changed .src{display:block;margin-top:.28rem;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:.63rem;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);border:0}
.ref{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.62rem;
  vertical-align:super;line-height:0;padding-left:.14em;border:0;color:var(--accent)}
.grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:0;border-top:1px solid var(--rule);
  border-bottom:1px solid var(--rule);margin:1.2rem 0 0}
.grid3>div{padding:1rem 1.05rem 1.15rem;border-right:1px solid var(--rule)}
.grid3>div:last-child{border-right:0}
.grid3 h3{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.62rem;
  letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin:0 0 .45rem;font-weight:600}
.grid3 p{margin:0;font-size:.9rem;line-height:1.5}
.grid3 .kill h3{color:var(--accent)}
ul.plain{list-style:none;padding:0;margin:0}
ul.plain li{position:relative;padding-left:1.15rem;margin-bottom:.65rem;font-size:.96rem;
  line-height:1.55;color:#353128}
ul.plain li:before{content:"—";position:absolute;left:0;color:var(--accent)}
.refs ol{padding-left:1.35rem;margin:0;font-size:.82rem;line-height:1.5;color:#4a4538}
.refs li{margin-bottom:.6rem;padding-left:.2rem}
.refs a{word-break:break-word}
.refs .u{display:block;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:.64rem;color:var(--muted);border:0;margin-top:.1rem}
.pager{display:flex;justify-content:space-between;gap:1rem;border-top:1px solid var(--rule);
  margin-top:3.4rem;padding:1.3rem 0 1.1rem;font-size:.86rem}
.pager a{border:0}
.pager .mid{color:var(--muted);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:.66rem;letter-spacing:.1em;text-transform:uppercase}
footer{border-top:1px solid var(--rule);margin-top:0;padding:1.6rem 0 3rem;
  font-size:.78rem;color:var(--muted);line-height:1.55}
footer strong{color:var(--ink);font-weight:600}
.rule{height:1px;background:var(--rule);border:0;margin:0}

/* index */
.hero{padding:3.4rem 0 2.2rem;border-bottom:1px solid var(--rule)}
.hero .eyebrow{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.68rem;
  letter-spacing:.18em;text-transform:uppercase;color:var(--accent);margin:0 0 1.1rem}
.hero h1{font-size:clamp(2.5rem,6.4vw,4.4rem);max-width:22ch;margin:0 0 1.2rem}
.hero p.lede{font-size:1.2rem;line-height:1.5;color:#3c382e;max-width:40rem;margin:0 0 1.5rem}
.hero .stats{display:flex;flex-wrap:wrap;gap:1.8rem;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:.68rem;letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}
.hero .stats b{display:block;font-family:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
  font-size:1.7rem;letter-spacing:0;color:var(--ink);font-weight:600}
.filters{display:flex;flex-wrap:wrap;gap:.45rem;padding:1.6rem 0 1.2rem;position:sticky;top:2.9rem;
  background:var(--paper);z-index:4;border-bottom:1px solid var(--rule)}
.filters button{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.66rem;
  letter-spacing:.08em;text-transform:uppercase;background:transparent;color:var(--muted);
  border:1px solid var(--rule);border-radius:999px;padding:.4rem .75rem;cursor:pointer}
.filters button[aria-pressed="true"]{background:var(--ink);color:var(--paper);border-color:var(--ink)}
.filters button:hover{border-color:var(--muted);color:var(--ink)}
.filters button[aria-pressed="true"]:hover{color:var(--paper)}
.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(19rem,1fr));gap:1px;
  background:var(--rule);border:1px solid var(--rule);margin:1.8rem 0 0}
.card{background:var(--card);padding:1.5rem 1.35rem 1.35rem;display:flex;flex-direction:column;
  min-height:12.5rem;transition:background .15s ease}
.card:hover{background:#fff}
.card .top{display:flex;justify-content:space-between;gap:.6rem;align-items:baseline;
  font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.6rem;letter-spacing:.1em;
  text-transform:uppercase;color:var(--muted);margin-bottom:1.2rem}
.card .top .idx{color:var(--accent)}
.card h3{font-size:1.28rem;line-height:1.16;letter-spacing:-.01em;margin:0 0 .55rem;font-weight:600}
.card h3 a{border:0;color:inherit}
.card p{margin:0;font-size:.88rem;line-height:1.5;color:#4d483b;flex:1}
.card .go{margin-top:1.2rem;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:.62rem;letter-spacing:.12em;text-transform:uppercase;color:var(--accent)}
.colophon{border-top:1px solid var(--rule);margin-top:3rem;padding:2.2rem 0 3.5rem;
  font-size:.82rem;color:var(--muted);line-height:1.6}
.colophon h2{margin-top:0}
.colophon p{max-width:42rem}
.count{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.66rem;
  color:var(--muted);letter-spacing:.08em;padding:1.1rem 0 .2rem}
@media (max-width:640px){
  body{font-size:17px}
  .grid3{grid-template-columns:1fr}
  .grid3>div{border-right:0;border-bottom:1px solid var(--rule)}
  .grid3>div:last-child{border-bottom:0}
  .filters{position:static}
  .masthead .meta{display:none}
}
@media print{
  .masthead,.filters,.pager,.hero .stats{display:none}
  body{background:#fff}
  .card{min-height:0}
  a{color:#000;border:0}
}
"""


# ---------------------------------------------------------------- article
def render_article(idea, prev_idea, next_idea, base, standalone=True):
    ordered, seen = refs(idea)

    kick = (
        f'<p class="kicker"><span class="n">Idea {idea["n"]:02d}</span>'
        f'<span class="dot">/</span><span>{esc(idea["domain"])}</span>'
        f'<span class="dot">/</span><span>{esc(idea["kind"])}</span>'
        f'<span class="dot">/</span><span>{reading_time(idea)} min</span></p>'
    )

    changed = "".join(
        f'<li>{esc(text)}<a class="ref" href="#r{seen[url]}">{seen[url]}</a>'
        f'<a class="src" href="{esc(url)}" rel="noopener">{esc(label)}</a></li>'
        for text, url, label in idea["changed"]
    )

    paras = "".join(f"<p>{esc(p)}</p>" for p in idea["idea"])
    risks = "".join(f"<li>{esc(r)}</li>" for r in idea["risks"])
    ref_items = "".join(
        f'<li id="r{n}"><a href="{esc(url)}" rel="noopener">{esc(title)}</a>'
        f'<a class="u" href="{esc(url)}" rel="noopener">{esc(url)}</a></li>'
        for n, (title, url) in enumerate(ordered, 1)
    )

    def href(slug):
        return f'{base}/{slug}/' if base else f'{slug}.html'

    pager = ['<nav class="pager">']
    pager.append(
        f'<a href="{href(prev_idea["slug"])}">&larr; {esc(prev_idea["title"])}</a>'
        if prev_idea else '<span></span>'
    )
    pager.append('<a class="mid" href="../index.html">All ideas</a>' if not base else f'<a class="mid" href="{base}/">All ideas</a>')
    pager.append(
        f'<a href="{href(next_idea["slug"])}">{esc(next_idea["title"])} &rarr;</a>'
        if next_idea else '<span></span>'
    )
    pager.append("</nav>")

    body = f"""<main class="wrap">
<article>
{kick}
<h1>{esc(idea["title"])}</h1>
<p class="dek">{esc(idea["dek"])}</p>
<blockquote class="thesis">{esc(idea["thesis"])}</blockquote>

<section>
<h2><span class="num">01</span>What changed</h2>
<ul class="changed">{changed}</ul>
</section>

<section>
<h2><span class="num">02</span>The gap</h2>
<p>{esc(idea["gap"])}</p>
</section>

<section>
<h2><span class="num">03</span>The idea</h2>
{paras}
</section>

<section>
<h2><span class="num">04</span>Why now</h2>
<p>{esc(idea["why_now"])}</p>
</section>

<section>
<h2><span class="num">05</span>The decisive test</h2>
<div class="grid3">
<div><h3>Do this</h3><p>{esc(idea["do"])}</p></div>
<div><h3>Success looks like</h3><p>{esc(idea["success"])}</p></div>
<div class="kill"><h3>Kills it</h3><p>{esc(idea["kill"])}</p></div>
</div>
</section>

<section>
<h2><span class="num">06</span>What would break it</h2>
<ul class="plain">{risks}</ul>
</section>

<section>
<h2><span class="num">07</span>If it works</h2>
<p>{esc(idea["ifworks"])}</p>
</section>

<section class="refs">
<h2><span class="num">08</span>Read the frontier</h2>
<ol>{ref_items}</ol>
</section>
</article>
{''.join(pager)}
<footer>
<strong>Incoprea Frontier &middot; Idea {idea["n"]:02d} of 24.</strong>
A proposal written {BUILT}. This is an argued idea, not a peer-reviewed result:
the cited work is real, the synthesis is the author's, and every claim about
future experiments is a prediction. Check the sources before quoting anything here.
</footer>
</main>"""

    if not standalone:
        return body

    return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{esc(idea["title"])} &middot; Incoprea Frontier</title>
<meta name="description" content="{esc(meta_desc(idea))}">
<link rel="canonical" href="{SITE_BASE}/{idea["slug"]}/">
<meta property="og:type" content="article">
<meta property="og:title" content="{esc(idea["title"])}">
<meta property="og:description" content="{esc(idea["dek"])}">
<style>{CSS}</style>
<script type="application/ld+json">{{
"@context":"https://schema.org",
"@type":"Article",
"headline":{json.dumps(idea["title"])},
"description":{json.dumps(idea["thesis"])},
"datePublished":"2026-10-30",
"author":{{"@type":"Organization","name":"Incoprea"}},
"publisher":{{"@type":"Organization","name":"Incoprea"}},
"isPartOf":{{"@type":"CreativeWorkSeries","name":"Incoprea Frontier"}}
}}</script>
</head>
<body>
<header class="masthead"><div class="wrap">
<a class="wordmark" href="../index.html">Incoprea <span>Frontier</span></a>
<div class="meta">24 ideas &middot; 2026</div>
</div></header>
{body}
</body>
</html>"""


# ---------------------------------------------------------------- index
def render_index():
    cards = []
    for i in IDEAS:
        cards.append(
            f'<article class="card" data-domain="{esc(i["domain"])}">'
            f'<div class="top"><span class="idx">{i["n"]:02d}</span><span>{esc(i["domain"])}</span></div>'
            f'<h3><a href="ideas/{i["slug"]}.html">{esc(i["title"])}</a></h3>'
            f'<p>{esc(i["dek"])}</p>'
            f'<div class="go">Read &rarr;</div></article>'
        )
    dots = [f'<button data-domain="all" aria-pressed="true">All <span>{len(IDEAS)}</span></button>']
    for d in DOMAINS:
        c = sum(1 for i in IDEAS if i["domain"] == d)
        dots.append(f'<button data-domain="{esc(d)}" aria-pressed="false">{esc(d)} <span>{c}</span></button>')

    total_words = sum(words(i) for i in IDEAS)

    return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Incoprea Frontier &middot; 24 original ideas from 2026 frontier science</title>
<meta name="description" content="Twenty-four argued research proposals built on the newest published science and technology, each with a decisive test.">
<link rel="canonical" href="{SITE_BASE}/">
<style>{CSS}</style>
</head>
<body class="index">
<header class="masthead"><div class="wrap">
<a class="wordmark" href="index.html">Incoprea <span>Frontier</span></a>
<div class="meta">Built {BUILT}</div>
</div></header>

<section class="hero"><div class="wrap">
<p class="eyebrow">A working idea book &middot; not peer reviewed</p>
<h1>Twenty-four ideas the newest science makes possible</h1>
<p class="lede">Each entry starts from a result published in the last eighteen months,
names a gap that result opens, and proposes a falsifiable experiment. No idea here is a
summary. All of them are arguments, and each one says what would kill it.</p>
<div class="stats">
<div><b>24</b>ideas</div>
<div><b>8</b>fields</div>
<div><b>{total_words:,}</b>words</div>
<div><b>{BUILT.split()[0]} {BUILT.split()[1][:3]}</b>built</div>
</div>
</div></section>

<div class="wrap">
<nav class="filters" aria-label="Filter by field">{''.join(dots)}</nav>
<p class="count" id="count">Showing all 24 ideas</p>
<section class="cards" id="cards">{''.join(cards)}</section>

<div class="colophon">
<h2>How to read this</h2>
<p><strong>What changed</strong> is the published work, cited. <strong>The gap</strong> is what
that work leaves undone. <strong>The idea</strong> is the proposal. <strong>The decisive test</strong>
is the cheapest experiment that would settle it, with an explicit result that kills it.
If you only read one section, read the test.</p>
<p>Nothing here has been peer reviewed. The sources are real and linked; the synthesis and the
proposed experiments are the author's. Treat every entry as a hypothesis with a plan attached,
not as a finding.</p>
<p>Built {BUILT}. Standalone HTML, no external assets. WordPress fragments for every idea are in
<code>wordpress/</code>.</p>
</div>
</div>

<script>
const btns=[...document.querySelectorAll('.filters button')];
const cards=[...document.querySelectorAll('.card')];
const count=document.getElementById('count');
function apply(d){{
  let n=0;
  cards.forEach(c=>{{
    const show = d==='all' || c.dataset.domain===d;
    c.style.display = show ? '' : 'none';
    if(show) n++;
  }});
  btns.forEach(b=>b.setAttribute('aria-pressed', String(b.dataset.domain===d)));
  count.textContent = d==='all' ? 'Showing all '+n+' ideas' : 'Showing '+n+' ideas in '+d;
}}
btns.forEach(b=>b.addEventListener('click',()=>apply(b.dataset.domain)));
</script>
</body>
</html>"""


# ---------------------------------------------------------------- wordpress
WP_CSS = """
.incoprea-idea{--ip-ink:#191811;--ip-muted:#6f6857;--ip-rule:#dcd5c4;--ip-accent:#a8412a;
 font-family:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;color:var(--ip-ink);
 font-size:18px;line-height:1.62;max-width:46rem;margin:0 auto}
.incoprea-idea a{color:var(--ip-accent);text-decoration:none;border-bottom:1px solid rgba(168,65,42,.32)}
.incoprea-idea .kicker{display:flex;flex-wrap:wrap;gap:.5rem;align-items:center;font-family:ui-monospace,Menlo,monospace;font-size:.68rem;letter-spacing:.14em;
 text-transform:uppercase;color:var(--ip-muted);margin:1.6rem 0 .8rem}
.incoprea-idea .kicker .n{color:var(--ip-accent);font-weight:700}
.incoprea-idea .kicker .dot{color:var(--ip-rule)}
.incoprea-idea h1{font-size:clamp(2rem,5vw,3rem);line-height:1.07;letter-spacing:-.018em;margin:.2rem 0 .7rem;font-weight:600}
.incoprea-idea .dek{font-size:1.2rem;line-height:1.45;font-style:italic;color:#3c382e;margin:0 0 1.8rem}
.incoprea-idea .thesis{border-left:2px solid var(--ip-accent);margin:0 0 2.4rem;padding:.1rem 0 .1rem 1.1rem;font-size:1.08rem}
.incoprea-idea h2{font-family:ui-sans-serif,-apple-system,Arial,sans-serif;font-size:.72rem;letter-spacing:.18em;
 text-transform:uppercase;margin:2.6rem 0 .8rem;font-weight:700}
.incoprea-idea h2 .num{font-family:ui-monospace,Menlo,monospace;color:var(--ip-accent);font-weight:400;margin-right:.6rem}
.incoprea-idea .changed{list-style:none;padding:0;margin:0}
.incoprea-idea .changed li{position:relative;padding-left:1.7rem;margin-bottom:1.1rem;font-size:.97rem}
.incoprea-idea .changed li:before{content:"";position:absolute;left:.1rem;top:.6em;width:.45rem;height:.45rem;
 border-radius:50%;background:var(--ip-accent)}
.incoprea-idea .src{display:block;margin-top:.2rem;font-family:ui-monospace,Menlo,monospace;font-size:.63rem;
 text-transform:uppercase;letter-spacing:.06em;color:var(--ip-muted);border:0}
.incoprea-idea .ref{font-family:ui-monospace,Menlo,monospace;font-size:.62rem;vertical-align:super;border:0}
.incoprea-idea .grid3{display:grid;grid-template-columns:repeat(3,1fr);border-top:1px solid var(--ip-rule);
 border-bottom:1px solid var(--ip-rule);margin:1.1rem 0}
.incoprea-idea .grid3>div{padding:1rem;border-right:1px solid var(--ip-rule)}
.incoprea-idea .grid3>div:last-child{border-right:0}
.incoprea-idea .grid3 h3{font-family:ui-monospace,Menlo,monospace;font-size:.62rem;letter-spacing:.14em;
 text-transform:uppercase;color:var(--ip-muted);margin:0 0 .4rem}
.incoprea-idea .grid3 .kill h3{color:var(--ip-accent)}
.incoprea-idea .grid3 p{margin:0;font-size:.9rem;line-height:1.5}
.incoprea-idea ul.plain{list-style:none;padding:0;margin:0}
.incoprea-idea ul.plain li{position:relative;padding-left:1.1rem;margin-bottom:.6rem;font-size:.96rem}
.incoprea-idea ul.plain li:before{content:"\\2014";position:absolute;left:0;color:var(--ip-accent)}
.incoprea-idea .refs ol{padding-left:1.3rem;font-size:.82rem;line-height:1.5}
.incoprea-idea .refs .u{display:block;font-family:ui-monospace,Menlo,monospace;font-size:.64rem;color:var(--ip-muted);border:0}
.incoprea-idea .ip-note{border-top:1px solid var(--ip-rule);margin-top:2.4rem;padding-top:1rem;font-size:.78rem;color:var(--ip-muted)}
@media(max-width:640px){.incoprea-idea .grid3{grid-template-columns:1fr}
 .incoprea-idea .grid3>div{border-right:0;border-bottom:1px solid var(--ip-rule)}
 .incoprea-idea .grid3>div:last-child{border-bottom:0}}
"""


def render_wp(idea):
    body = render_article(idea, None, None, base=SITE_BASE + "/ideas", standalone=False)
    # drop the pager nav (site navigation does not belong in a post body)
    body = re.sub(r"<nav class=\"pager\">.*?</nav>", "", body, flags=re.S)
    body = body.replace('<main class="wrap">', "").replace("</main>", "")
    note = (
        f'<p class="ip-note"><strong>Incoprea Frontier, idea {idea["n"]:02d} of 24.</strong> '
        f'A proposal written {BUILT}; the cited work is real, the synthesis is the author\'s, '
        f'and every claim about future experiments is a prediction.</p>'
    )
    return f"""<!-- Incoprea Frontier · idea {idea["n"]:02d} · paste into a WordPress Custom HTML block -->
<style>{WP_CSS}</style>
<article class="incoprea-idea">
{body}
{note}
</article>
"""


def render_wp_index():
    rows = "".join(
        f'<li><a href="{SITE_BASE}/ideas/{i["slug"]}/">{esc(i["title"])}</a> '
        f'&mdash; {esc(i["dek"])} <em>({esc(i["domain"])})</em></li>'
        for i in IDEAS
    )
    return f"""<!-- Incoprea Frontier · index list · paste into a WordPress Custom HTML block -->
<div class="incoprea-frontier-index" style="font-family:'Iowan Old Style',Palatino,Georgia,serif;max-width:46rem;margin:0 auto">
<p style="font-size:1.05rem;font-style:italic;color:#3c382e">Twenty-four argued proposals built on
2026 frontier science, each with a decisive test. Nothing here is peer reviewed.</p>
<ol style="font-size:.95rem;line-height:1.55">
{rows}
</ol>
<p style="font-size:.78rem;color:#6f6857;border-top:1px solid #dcd5c4;padding-top:.8rem">
Change the base URL <code>{SITE_BASE}</code> to match your live routing.</p>
</div>
"""


# ---------------------------------------------------------------- docs
README = f"""# Incoprea Frontier — 24 original ideas from 2026 frontier science

Built {BUILT}. Self-contained: relative links, inline CSS, no external fonts, images or scripts.

**This is an idea book, not a research paper.** Each entry cites real published work from
2025-2026, then makes an argument that goes beyond it. Every entry ends with the experiment
that would settle it and the result that would kill it. Nothing here is peer reviewed.

## Start here

| File | What it is |
|---|---|
| `index.html` | The landing page — hero, field filter, 24 cards |
| `ideas/<slug>.html` | One standalone article per idea, a three-minute read |
| `wordpress/<slug>.html` | The same article as a WordPress Custom HTML fragment |
| `wordpress/frontier-index.html` | A single list block for the landing page |
| `MANIFEST.json` | SHA-256 for every file in the bundle |
| `PUBLISH-ORDER.md` | Step-by-step publishing instructions |

## The 24 ideas

| # | Field | Idea |
|---|---|---|
""" + "\n".join(f'| {i["n"]:02d} | {i["domain"]} | {i["title"]} |' for i in IDEAS) + f"""

## Structure of an entry

1. **What changed** — two or three dated 2026 results, each linked.
2. **The gap** — what those results leave undone.
3. **The idea** — the proposal, in enough detail to argue with.
4. **Why now** — the enabling conditions that did not exist two years ago.
5. **The decisive test** — do this / success looks like / this kills it.
6. **What would break it** — the three failure modes that worry me most.
7. **If it works** — the consequence.
8. **Read the frontier** — every source, numbered, with full URLs.

## Honest limitations

- Sources are linked but not independently replicated. Several are preprints or institutional
  announcements rather than peer-reviewed papers, and are labelled as reported.
- The proposed experiments are sketches. None has been costed, powered or ethically reviewed.
- The ideas are syntheses across fields; a specialist will find each one naive in places.
  That is what a cross-disciplinary idea book is for.
- No idea here is a recommendation to invest, publish, or act on. They are hypotheses.

## Rebuilding

`build/` contains the source: `data_1.py`, `data_2.py`, `data_3.py` (the 24 entries) and
`build.py` (the renderer). Run `python3 build.py` to regenerate `out/`.
"""

PUBLISH = f"""# Publish order

Two routes. Pick one.

## Route A — static site (everything already works)

1. Upload the contents of this folder to `{SITE_BASE}/` on your host.
2. `index.html` becomes the landing page; `ideas/*.html` are the articles.
3. Delete `wordpress/` — it is not needed for the static route.
4. Confirm the canonical URLs match your real paths. They are currently set to
   `{SITE_BASE}/...`. If your route differs, find and replace `{SITE_BASE}` across
   `ideas/*.html` and `index.html`.

Nothing else is required. No images, no fonts, no JavaScript beyond the 12-line
filter on the landing page.

## Route B — WordPress

The fragments are designed for a **Custom HTML block**, not the visual editor.

1. **Create the parent page** at `/frontier/`. Paste `wordpress/frontier-index.html`
   into a Custom HTML block. Set the slug to `frontier`.
2. **Create one page per idea**, child of `/frontier/`. Suggested slug is the filename:
   `ideas/circuit-diffing-release-gate.html` becomes `/frontier/circuit-diffing-release-gate/`.
   Paste `wordpress/<slug>.html` into a Custom HTML block.
3. **Check the base URL.** Every cross-link in the fragments is absolute and points at
   `{SITE_BASE}/ideas/<slug>/`. If your WordPress path is different, run one
   find-and-replace of `{SITE_BASE}` across `wordpress/*.html` before pasting.
4. **Hide the theme header if the design fights you.** The fragments carry their own
   typography and spacing. A theme with a narrow content column will constrain them;
   the fragment caps itself at 46rem and will not overflow.
5. **Keep the disclaimer.** Each fragment ends with a "proposal, not a finding" note.
   Do not remove it.

## Before you go live

- [ ] Set the canonical URL and `og:` tags if your path is not `{SITE_BASE}`.
- [ ] Decide whether the landing page should say "not peer reviewed". It currently does,
      twice. Leave it.
- [ ] Check every source link resolves. Several point at journals with paywalls; that is
      acceptable, but a dead link is not.
- [ ] If you publish to Blogger instead, use `wordpress/frontier-index.html` as the body of
      one post; the per-idea fragments also paste into Blogger's HTML view.

## What not to do

- Do not paste the standalone `ideas/*.html` into WordPress. They are full documents and
  will bring their own `<html>` and `<head>`.
- Do not claim any of this is peer reviewed, replicated, or complete.
- Do not merge two entries into one page; the 46rem measure is calibrated for one idea.
"""


# ---------------------------------------------------------------- build
def main():
    validate()
    if OUT.exists():
        shutil.rmtree(OUT)
    (OUT / "ideas").mkdir(parents=True)
    (OUT / "wordpress").mkdir(parents=True)

    written = []
    for idx, idea in enumerate(IDEAS):
        prev_idea = IDEAS[idx - 1] if idx > 0 else None
        next_idea = IDEAS[idx + 1] if idx < len(IDEAS) - 1 else None
        p = OUT / "ideas" / f'{idea["slug"]}.html'
        p.write_text(render_article(idea, prev_idea, next_idea, base=None), encoding="utf-8")
        written.append(p)

        w = OUT / "wordpress" / f'{idea["slug"]}.html'
        w.write_text(render_wp(idea), encoding="utf-8")
        written.append(w)

    index = OUT / "index.html"
    index.write_text(render_index(), encoding="utf-8")
    written.append(index)

    wpi = OUT / "wordpress" / "frontier-index.html"
    wpi.write_text(render_wp_index(), encoding="utf-8")
    written.append(wpi)

    src = OUT / "build"
    src.mkdir(exist_ok=True)
    for f in ("build.py", "data_1.py", "data_2.py", "data_3.py"):
        shutil.copy2(ROOT / f, src / f)

    (OUT / "README.md").write_text(README, encoding="utf-8")
    (OUT / "PUBLISH-ORDER.md").write_text(PUBLISH, encoding="utf-8")
    written += [OUT / "README.md", OUT / "PUBLISH-ORDER.md"]

    files = {}
    for p in sorted(OUT.rglob("*")):
        if p.is_file() and p.name != "MANIFEST.json":
            files[str(p.relative_to(OUT))] = {
                "sha256": sha256(p),
                "bytes": p.stat().st_size,
            }

    manifest = {
        "bundle": "Incoprea Frontier",
        "built": BUILT,
        "generated_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"),
        "site_base": SITE_BASE,
        "ideas": len(IDEAS),
        "words": sum(words(i) for i in IDEAS),
        "domains": {d: sum(1 for i in IDEAS if i["domain"] == d) for d in DOMAINS},
        "files": files,
        "file_count": len(files),
    }
    (OUT / "MANIFEST.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")

    print(f"ideas: {len(IDEAS)}  words: {manifest['words']:,}  files: {len(files)}")
    for d, c in manifest["domains"].items():
        print(f"  {c:>2}  {d}")
    print(f"out: {OUT}")


if __name__ == "__main__":
    main()
