diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3a799fb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules/
+_site/
diff --git a/README.md b/README.md
index ec28047..a3bf5ea 100644
--- a/README.md
+++ b/README.md
@@ -86,6 +86,24 @@ locally:
node scripts/validate.mjs
```
+## Website
+
+The site at [aips.antseed.com](https://aips.antseed.com) is generated straight
+from `AIPS/*.md` — one page per proposal plus an index grouped by status. No
+CMS, no server: `scripts/build-site.mjs` renders the markdown with the
+templates and styles in `site/`, and Cloudflare Pages builds and deploys on
+every merge to `main`.
+
+Build locally:
+
+```bash
+npm install
+npm run build # validates, then renders to _site/
+```
+
+Cloudflare Pages settings: build command `npm run build`, output directory
+`_site`, production branch `main`.
+
## Copyright
All AIPs, and this repository's documentation, are released under
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..243fba6
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,157 @@
+{
+ "name": "antseed-aips",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "antseed-aips",
+ "dependencies": {
+ "js-yaml": "^4.1.0",
+ "markdown-it": "^14.1.0",
+ "markdown-it-anchor": "^9.2.0"
+ }
+ },
+ "node_modules/@types/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/markdown-it": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.2.0.tgz",
+ "integrity": "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/linkify-it": "^5",
+ "@types/mdurl": "^2"
+ }
+ },
+ "node_modules/@types/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
+ "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/linkify-it": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz",
+ "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/markdown-it"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "uc.micro": "^2.0.0"
+ }
+ },
+ "node_modules/markdown-it": {
+ "version": "14.3.0",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz",
+ "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/markdown-it"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "^4.5.0",
+ "linkify-it": "^5.0.2",
+ "mdurl": "^2.0.0",
+ "punycode.js": "^2.3.1",
+ "uc.micro": "^2.1.0"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.mjs"
+ }
+ },
+ "node_modules/markdown-it-anchor": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-9.2.1.tgz",
+ "integrity": "sha512-p6APiLJDFAW2GEvaavDvhIBn7jrX2jLv77NkBGgNacFTurbORYc4pyYySg/mI6mpR6cHQuAtzKtmqgQr4K8dsQ==",
+ "license": "Unlicense",
+ "peerDependencies": {
+ "@types/markdown-it": "*",
+ "markdown-it": "*"
+ }
+ },
+ "node_modules/mdurl": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz",
+ "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==",
+ "license": "MIT"
+ },
+ "node_modules/punycode.js": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/uc.micro": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+ "license": "MIT"
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..aa4fb2c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "antseed-aips",
+ "private": true,
+ "type": "module",
+ "description": "AntSeed Improvement Proposals — validator and static site generator",
+ "scripts": {
+ "validate": "node scripts/validate.mjs",
+ "build": "node scripts/validate.mjs && node scripts/build-site.mjs"
+ },
+ "dependencies": {
+ "js-yaml": "^4.1.0",
+ "markdown-it": "^14.1.0",
+ "markdown-it-anchor": "^9.2.0"
+ }
+}
diff --git a/scripts/build-site.mjs b/scripts/build-site.mjs
new file mode 100644
index 0000000..0d05458
--- /dev/null
+++ b/scripts/build-site.mjs
@@ -0,0 +1,251 @@
+// Static site generator for aips.antseed.com.
+// Reads AIPS/*.md, renders one page per AIP plus an index grouped by status.
+// Output: _site/ (deployed by Cloudflare Pages on merges to main).
+
+import { readdirSync, readFileSync, writeFileSync, mkdirSync, cpSync, existsSync } from 'node:fs';
+import { join, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import yaml from 'js-yaml';
+import MarkdownIt from 'markdown-it';
+import anchor from 'markdown-it-anchor';
+
+const root = join(dirname(fileURLToPath(import.meta.url)), '..');
+const out = join(root, '_site');
+const REPO = 'https://github.com/AntSeed/AIPs';
+
+const STATUS_ORDER = ['Living', 'Final', 'Last Call', 'Review', 'Draft', 'Stagnant', 'Withdrawn'];
+const md = new MarkdownIt({ html: true, linkify: true, typographer: true }).use(anchor, {
+ permalink: anchor.permalink.linkInsideHeader({ symbol: '#', placement: 'after', class: 'h-anchor' }),
+});
+
+// Rewrite intra-repo markdown links to site routes.
+const defaultLinkOpen =
+ md.renderer.rules.link_open ?? ((tokens, idx, options, env, self) => self.renderToken(tokens, idx, options));
+md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
+ const href = tokens[idx].attrGet('href');
+ if (href) {
+ const aip = href.match(/^(?:\.\/|\.\.\/AIPS\/|AIPS\/)?aip-(\d+)(?:\.md)?(#.*)?$/);
+ if (aip) tokens[idx].attrSet('href', `/aip-${aip[1]}${aip[2] ?? ''}`);
+ else if (/^(\.\.\/)?LICENSE$/.test(href)) tokens[idx].attrSet('href', `${REPO}/blob/main/LICENSE`);
+ else if (/^(\.\/|\.\.\/)?aip-template\.md$/.test(href)) tokens[idx].attrSet('href', `${REPO}/blob/main/aip-template.md`);
+ else if (/^(\.\.\/)?CONTRIBUTING\.md$/.test(href)) tokens[idx].attrSet('href', `${REPO}/blob/main/CONTRIBUTING.md`);
+ else if (href.startsWith('../assets/') || href.startsWith('assets/'))
+ tokens[idx].attrSet('href', `/${href.replace(/^\.\.\//, '')}`);
+ }
+ return defaultLinkOpen(tokens, idx, options, env, self);
+};
+
+const esc = (s) =>
+ String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
+
+function parse(file) {
+ const raw = readFileSync(join(root, 'AIPS', file), 'utf8');
+ const m = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
+ if (!m) throw new Error(`${file}: missing frontmatter`);
+ return { front: yaml.load(m[1]), body: m[2], file };
+}
+
+// Render "FirstName LastName (@user)" with the handle linked.
+const renderAuthors = (author) =>
+ esc(author).replace(/\(@([A-Za-z0-9-]+)\)/g, '(@$1 )');
+
+
+function tocFor(body) {
+ const items = [];
+ for (const t of md.parse(body, {})) {
+ if (t.type === 'heading_open' && t.tag === 'h2') items.push({ open: true });
+ else if (t.type === 'inline' && items.length && items[items.length - 1].open) {
+ const it = items[items.length - 1];
+ it.text = t.content;
+ it.id = t.content.toLowerCase().replace(/[^\w\s-]/g, '').trim().replace(/\s+/g, '-');
+ it.open = false;
+ }
+ }
+ return items.filter((i) => i.text);
+}
+
+const css = readFileSync(join(root, 'site', 'style.css'), 'utf8');
+
+const page = ({ title, description, canonicalPath, content, extraClass = '' }) => `
+
+
+
+
+${esc(title)}
+
+
+ '
+)}">
+
+
+
+
+
+
+
+
+${content}
+
+
+`;
+
+// ── collect ──────────────────────────────────────────────────────────
+const aips = readdirSync(join(root, 'AIPS'))
+ .filter((f) => /^aip-\d+\.md$/.test(f))
+ .map(parse)
+ .sort((a, b) => a.front.aip - b.front.aip);
+
+mkdirSync(out, { recursive: true });
+if (existsSync(join(root, 'assets'))) cpSync(join(root, 'assets'), join(out, 'assets'), { recursive: true });
+
+// ── per-AIP pages ────────────────────────────────────────────────────
+const PREAMBLE_ROWS = [
+ ['description', 'Description'],
+ ['author', 'Author'],
+ ['discussions-to', 'Discussion'],
+ ['status', 'Status'],
+ ['type', 'Type'],
+ ['category', 'Category'],
+ ['created', 'Created'],
+ ['requires', 'Requires'],
+];
+
+for (const { front, body, file } of aips) {
+ const n = front.aip;
+ const rows = PREAMBLE_ROWS.filter(([k]) => front[k] != null)
+ .map(([k, label]) => {
+ let v;
+ if (k === 'author') v = renderAuthors(front.author);
+ else if (k === 'discussions-to') v = `${esc(front[k])} `;
+ else if (k === 'status') v = esc(front.status);
+ else if (k === 'requires')
+ v = String(front.requires)
+ .split(/,\s*/)
+ .map((r) => `AIP-${r.trim()} `)
+ .join(', ');
+ else if (k === 'created') v = `${esc(front.created)} `;
+ else v = esc(front[k]);
+ return `
${label} ${v} `;
+ })
+ .join('\n');
+
+ const toc = tocFor(body);
+ const tocHtml = toc.length
+ ? `Contents ${toc
+ .map((i) => `${esc(i.text)} `)
+ .join('')} `
+ : '';
+
+ const content = `
+
+
+
+ ${esc(front.title)}
+ ${rows}
+ ${md.render(body)}
+ View source on GitHub
+
+ ${tocHtml}
+ `;
+
+ mkdirSync(join(out, `aip-${n}`), { recursive: true });
+ writeFileSync(
+ join(out, `aip-${n}`, 'index.html'),
+ page({
+ title: `AIP-${n}: ${front.title} — AntSeed Improvement Proposals`,
+ description: front.description,
+ canonicalPath: `/aip-${n}`,
+ content,
+ extraClass: 'is-doc',
+ })
+ );
+}
+
+// ── index ────────────────────────────────────────────────────────────
+const groups = STATUS_ORDER.map((status) => ({
+ status,
+ items: aips.filter((a) => a.front.status === status),
+})).filter((g) => g.items.length);
+
+const tables = groups
+ .map(
+ ({ status, items }) => `
+`
+ )
+ .join('\n');
+
+const indexContent = `
+
+
+
AntSeed Improvement Proposals
+
Design documents for the AntSeed peer-to-peer AI services network —
+ protocol standards, contract specifications, and process. Anyone may author one:
+ start from AIP-1 and open a pull request against
+ AntSeed/AIPs .
+
+
+
+${tables}
+ `;
+
+writeFileSync(
+ join(out, 'index.html'),
+ page({
+ title: 'AntSeed Improvement Proposals',
+ description: 'Design documents for the AntSeed peer-to-peer AI services network.',
+ canonicalPath: '/',
+ content: indexContent,
+ })
+);
+
+writeFileSync(
+ join(out, 'index.json'),
+ JSON.stringify(
+ aips.map(({ front }) => ({ ...front, url: `https://aips.antseed.com/aip-${front.aip}` })),
+ null,
+ 2
+ )
+);
+
+// Cloudflare Pages: clean-URL 404 fallback.
+writeFileSync(
+ join(out, '404.html'),
+ page({
+ title: 'Not found — AntSeed Improvement Proposals',
+ description: 'No such proposal.',
+ canonicalPath: '/404',
+ content: `Not found
+No proposal lives at this address. Browse the index .
`,
+ })
+);
+
+console.log(`built ${aips.length} AIP page(s) + index → _site/`);
diff --git a/site/style.css b/site/style.css
new file mode 100644
index 0000000..de5fa05
--- /dev/null
+++ b/site/style.css
@@ -0,0 +1,162 @@
+/* aips.antseed.com — plain paper-and-ink document site.
+ General Sans + Geist Mono, one emerald accent for links. */
+
+:root {
+ --paper: #fdfdfc;
+ --paper-raised: #f5f5f3;
+ --ink: #17181a;
+ --ink-soft: #63666b;
+ --hairline: #e2e2df;
+ --hairline-soft: #ededea;
+ --accent: #008359;
+ --code-bg: #f4f4f2;
+
+ --font-sans: 'General Sans', system-ui, sans-serif;
+ --font-mono: 'Geist Mono', ui-monospace, 'Fira Code', monospace;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --paper: #131416;
+ --paper-raised: #1b1c1f;
+ --ink: #e9e9e6;
+ --ink-soft: #9a9da3;
+ --hairline: #2b2d31;
+ --hairline-soft: #222327;
+ --accent: #2fbf8f;
+ --code-bg: #1c1d20;
+ }
+}
+
+* { box-sizing: border-box; }
+html { scroll-behavior: smooth; }
+body {
+ margin: 0;
+ background: var(--paper);
+ color: var(--ink);
+ font-family: var(--font-sans);
+ font-weight: 500;
+ line-height: 1.6;
+ -webkit-font-smoothing: antialiased;
+}
+a { color: var(--accent); text-decoration: none; }
+a:hover { text-decoration: underline; text-underline-offset: 3px; }
+.mono { font-family: var(--font-mono); font-size: 0.92em; }
+.shell { max-width: 68rem; margin: 0 auto; padding: 0 clamp(1.1rem, 4vw, 2rem); }
+
+/* masthead */
+.masthead { border-bottom: 1px solid var(--hairline); }
+.masthead-row {
+ display: flex; align-items: center; justify-content: space-between;
+ gap: 1.5rem; padding-top: 0.95rem; padding-bottom: 0.95rem;
+}
+.wordmark { color: var(--ink); font-weight: 600; font-size: 0.98rem; letter-spacing: -0.01em; }
+.wordmark em { font-style: normal; font-weight: 500; color: var(--ink-soft); }
+.wordmark:hover { text-decoration: none; }
+.mast-nav { display: flex; gap: 1.5rem; }
+.mast-nav a {
+ color: var(--ink-soft); font-family: var(--font-mono);
+ font-size: 0.76rem; text-transform: uppercase; letter-spacing: 0.08em;
+}
+.mast-nav a:hover { color: var(--ink); text-decoration: none; }
+
+/* hero */
+.hero { padding: 2.6rem 0 2.2rem; border-bottom: 1px solid var(--hairline); }
+.hero h1 { margin: 0; font-size: 1.9rem; font-weight: 700; letter-spacing: -0.02em; }
+.hero-sub { max-width: 42rem; margin: 0.8rem 0 0; color: var(--ink-soft); font-size: 0.98rem; }
+
+/* index */
+.status-group { padding: 2rem 0 0.5rem; }
+.status-group:last-child { padding-bottom: 3rem; }
+.group-title { margin: 0 0 0.7rem; font-size: 1.15rem; font-weight: 600; }
+.aip-table { width: 100%; border-collapse: collapse; }
+.aip-table th {
+ text-align: left; font-family: var(--font-mono); font-size: 0.68rem;
+ text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-soft);
+ font-weight: 500; padding: 0.4rem 0.8rem; border-bottom: 1px solid var(--ink);
+}
+.aip-table td { padding: 0.65rem 0.8rem; border-bottom: 1px solid var(--hairline); vertical-align: baseline; }
+.col-num { width: 5rem; }
+.col-auth { width: 17rem; font-size: 0.88rem; color: var(--ink-soft); }
+.row-title { font-weight: 500; color: var(--ink); }
+.row-title:hover { color: var(--accent); }
+@media (max-width: 640px) { .col-auth { display: none; } }
+
+/* document pages */
+.doc-shell {
+ display: grid; grid-template-columns: minmax(0, 1fr) 14rem;
+ gap: 3rem; padding-top: 2.2rem; padding-bottom: 4rem;
+}
+@media (max-width: 900px) { .doc-shell { grid-template-columns: minmax(0, 1fr); } .toc { display: none; } }
+.doc { min-width: 0; }
+.doc-kicker {
+ font-family: var(--font-mono); font-size: 0.74rem; letter-spacing: 0.05em;
+ text-transform: uppercase; color: var(--ink-soft); margin-bottom: 0.6rem;
+}
+.doc-kicker a { color: var(--ink-soft); }
+.doc-title { margin: 0 0 1.4rem; font-size: 1.9rem; line-height: 1.15; letter-spacing: -0.02em; font-weight: 700; }
+
+.preamble { margin: 0 0 2.2rem; padding: 0.3rem 0; border-top: 1px solid var(--ink); border-bottom: 1px solid var(--hairline); }
+.pre-row { display: grid; grid-template-columns: 8rem minmax(0, 1fr); gap: 1rem; padding: 0.38rem 0; }
+.pre-row + .pre-row { border-top: 1px solid var(--hairline-soft); }
+.preamble dt {
+ font-family: var(--font-mono); font-size: 0.68rem; text-transform: uppercase;
+ letter-spacing: 0.1em; color: var(--ink-soft); padding-top: 0.22rem;
+}
+.preamble dd { margin: 0; font-size: 0.92rem; overflow-wrap: anywhere; }
+
+/* prose */
+.prose { max-width: 44rem; font-size: 0.98rem; line-height: 1.7; }
+.prose h2, .prose h3, .prose h4 { letter-spacing: -0.01em; line-height: 1.25; margin: 2.2rem 0 0.8rem; font-weight: 600; }
+.prose h2 { font-size: 1.35rem; }
+.prose h3 { font-size: 1.1rem; }
+.prose h4 { font-size: 0.98rem; }
+.h-anchor { margin-left: 0.4rem; color: var(--hairline); font-family: var(--font-mono); font-size: 0.8em; }
+.prose h2:hover .h-anchor, .prose h3:hover .h-anchor, .prose h4:hover .h-anchor { color: var(--accent); }
+.prose p { margin: 0 0 1rem; }
+.prose strong { font-weight: 600; }
+.prose ul, .prose ol { padding-left: 1.4rem; margin: 0 0 1rem; }
+.prose li { margin: 0.3rem 0; }
+.prose blockquote {
+ margin: 1.3rem 0; padding: 0.8rem 1.1rem;
+ border-left: 3px solid var(--hairline); background: var(--paper-raised); font-size: 0.94rem;
+}
+.prose blockquote p:last-child { margin-bottom: 0; }
+.prose code {
+ font-family: var(--font-mono); font-size: 0.85em;
+ background: var(--code-bg); border-radius: 4px; padding: 0.08em 0.3em; overflow-wrap: anywhere;
+}
+.prose pre {
+ background: var(--code-bg); border: 1px solid var(--hairline-soft); border-radius: 6px;
+ padding: 1rem 1.2rem; overflow-x: auto; margin: 1.3rem 0; font-size: 0.82rem; line-height: 1.6;
+}
+.prose pre code { background: none; padding: 0; font-size: inherit; }
+.prose table { border-collapse: collapse; width: 100%; margin: 1.3rem 0; font-size: 0.9rem; display: block; overflow-x: auto; }
+.prose th, .prose td { border: 1px solid var(--hairline); padding: 0.45rem 0.75rem; text-align: left; }
+.prose th { font-size: 0.84rem; background: var(--paper-raised); }
+.prose hr { border: 0; border-top: 1px solid var(--hairline); margin: 2.2rem 0; }
+.prose img { max-width: 100%; }
+.doc-source { margin-top: 2.6rem; font-family: var(--font-mono); font-size: 0.78rem; }
+
+/* toc */
+.toc { position: sticky; top: 1.8rem; align-self: start; padding-top: 2.9rem; }
+.toc h2 {
+ margin: 0 0 0.6rem; font-family: var(--font-mono); font-size: 0.66rem;
+ text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-soft); font-weight: 500;
+}
+.toc ol { list-style: none; margin: 0; padding: 0; border-left: 1px solid var(--hairline); }
+.toc li a {
+ display: block; padding: 0.24rem 0 0.24rem 0.85rem; font-size: 0.82rem;
+ color: var(--ink-soft); border-left: 2px solid transparent; margin-left: -1px;
+}
+.toc li a:hover { color: var(--ink); border-left-color: var(--accent); text-decoration: none; }
+
+/* footer */
+.colophon { border-top: 1px solid var(--hairline); margin-top: 2rem; }
+.colophon-row {
+ display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
+ padding-top: 1.1rem; padding-bottom: 1.1rem;
+ font-family: var(--font-mono); font-size: 0.74rem; color: var(--ink-soft);
+}
+.colophon a { color: var(--ink-soft); text-decoration: underline; text-underline-offset: 3px; }
+.colophon a:hover { color: var(--accent); }