// religion — read-only vault explorer. bun only, zero deps, zero external requests. // systemd --user unit: religion.service → http://127.0.0.1:4870 import { readdirSync, readFileSync, statSync, existsSync } from "fs"; import { join, normalize } from "path"; import { TREES, type LineageTree, type TreeNode } from "./tree-data.ts"; import { P_ROLES, P_TRADS, P_DEITIES, P_GAP_HINTS, P_EDGES } from "./pantheon-data.ts"; const ROOT = normalize(join(import.meta.dir, "..")); const PORT = 4870; const esc = (s: string) => s.replace(/&/g, "&").replace(//g, ">"); const slug = (s: string) => s.toLowerCase().replace(/[^a-z0-9áðéíóúýþæö]+/gi, "-").replace(/^-|-$/g, ""); function parseFrontmatter(src: string): { fm: Record; body: string } { const m = src.match(/^---\n([\s\S]*?)\n---\n?/); if (!m) return { fm: {}, body: src }; const fm: Record = {}; // Block-style YAML lists are real in this vault (e.g. claim-qafzeh… has `sources:` followed by // ` - "[[…]]"` lines) — collect them under the preceding empty-valued key, comma-joined. // Inline comments after values stay stripped (e.g. `transmission: contact # verdict scoped…`). let listKey = ""; for (const line of m[1].split("\n")) { const kv = line.match(/^(\w[\w_]*):\s*(.*)$/); if (kv) { fm[kv[1]] = kv[2].replace(/^["']|["']$/g, "").replace(/\s+#.*$/, ""); listKey = fm[kv[1]] === "" ? kv[1] : ""; continue; } const li = line.match(/^\s+-\s+(.*)$/); if (li && listKey) { const v = li[1].trim().replace(/^["']|["']$/g, ""); fm[listKey] = fm[listKey] ? fm[listKey] + ", " + v : v; } } return { fm, body: src.slice(m[0].length) }; } function inline(s: string): string { return esc(s) .replace(/\[\[([^\]]+)\]\]/g, (_, n) => `${n}`) .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, t, h) => `${t}`) .replace(/`([^`]+)`/g, "$1") .replace(/\*\*([^*]+)\*\*/g, "$1") .replace(/\*([^*]+)\*/g, "$1"); } function mdToHtml(md: string): { html: string; toc: { id: string; text: string; level: number }[] } { const lines = md.split("\n"); const out: string[] = []; const toc: { id: string; text: string; level: number }[] = []; let i = 0, inCode = false, inList = false, inQuote = false, inTable = false; const close = () => { if (inList) { out.push(""); inList = false; } if (inQuote) { out.push(""); inQuote = false; } if (inTable) { out.push(""); inTable = false; } }; while (i < lines.length) { const l = lines[i]; if (l.startsWith("```")) { close(); inCode = !inCode; out.push(inCode ? "
" : "
"); i++; continue; } if (inCode) { out.push(esc(l)); i++; continue; } const h = l.match(/^(#{1,6})\s+(.*)/); if (h) { close(); const n = h[1].length, text = h[2], id = slug(text); if (n === 2 || n === 3) toc.push({ id, text: text.replace(/[*`_]/g, ""), level: n }); out.push(`${inline(text)}`); i++; continue; } if (/^\s*\|/.test(l)) { if (!inTable) { close(); inTable = true; const heads = l.split("|").slice(1, -1).map(c => `${inline(c.trim())}`).join(""); out.push(`
${heads}`); if (/^\s*\|[\s:-]+\|/.test(lines[i + 1] ?? "")) i++; } else { const cells = l.split("|").slice(1, -1).map(c => ``).join(""); out.push(`${cells}`); } i++; continue; } else if (inTable) { out.push("
${inline(c.trim())}
"); inTable = false; } if (/^>\s?/.test(l)) { if (!inQuote) { close(); inQuote = true; out.push("
"); } out.push(`

${inline(l.replace(/^>\s?/, ""))}

`); i++; continue; } else if (inQuote) { out.push("
"); inQuote = false; } if (/^\s*[-*]\s+/.test(l)) { if (!inList) { close(); inList = true; out.push(""); inList = false; } if (/^---+\s*$/.test(l)) { close(); out.push("
"); i++; continue; } if (l.trim() === "") { close(); i++; continue; } out.push(`

${inline(l)}

`); i++; } close(); return { html: out.join("\n"), toc }; } // ---------- deep-time date parsing ---------- const NOWY = new Date().getFullYear(); const YMIN = 1950 - 100000; // 100,000 BP on the astronomical axis // Frontmatter date string → signed astronomical year (2350 BCE → -2349 · 92,000 BP → -90050 · 712 CE → 712). // The FIRST parseable anchor in the string wins — vault convention puts the headline attestation first // (later anchors are often explicitly rejected, e.g. "Yayoi … is inference only, not attested"). // Within one anchor, ranges resolve to the older endpoint. Strings led by "speculative" → null — // undated rather than guessed, per AGENTS.md §1.6. Unparseable → null. function earliestYear(s: string): number | null { if (!s) return null; if (/speculative/i.test(s.slice(0, 80))) return null; const num = (x: string) => parseFloat(x.replace(/,/g, "")); const cands: { i: number; y: number }[] = []; for (const m of s.matchAll(/([\d,.]+)\s*ka\s+BP\b/gi)) cands.push({ i: m.index!, y: 1950 - num(m[1]) * 1000 }); for (const m of s.matchAll(/([\d,]+)(?:\s*[–—-]\s*([\d,]+))?\s*(?:cal\.?\s+)?BP\b/g)) cands.push({ i: m.index!, y: 1950 - Math.max(num(m[1]), m[2] ? num(m[2]) : -Infinity) }); for (const m of s.matchAll(/([\d,]+)(?:\s*[–—-]\s*([\d,]+))?\s*BCE\b/g)) cands.push({ i: m.index!, y: 1 - Math.max(num(m[1]), m[2] ? num(m[2]) : -Infinity) }); for (const m of s.matchAll(/([\d,]+)(?:\s*[–—-]\s*([\d,]+))?\s*CE\b/g)) cands.push({ i: m.index!, y: Math.min(num(m[1]), m[2] ? num(m[2]) : Infinity) }); for (const m of s.matchAll(/(\d{1,2})(?:st|nd|rd|th)(?:\s*[–—-]\s*(\d{1,2})(?:st|nd|rd|th)?)?\s*c(?:ent)?(?:ury|uries)?\.?\s*(BCE|CE)\b/gi)) { const cs = [m[1], m[2]].filter(Boolean).map(Number); cands.push({ i: m.index!, y: m[3].toUpperCase() === "BCE" ? 1 - Math.max(...cs) * 100 : (Math.min(...cs) - 1) * 100 + 1, }); } for (const m of s.matchAll(/(\d{1,2})(?:st|nd|rd|th)?\s*millennium\s*(BCE|CE)\b/gi)) cands.push({ i: m.index!, y: m[2].toUpperCase() === "BCE" ? 1 - +m[1] * 1000 : (+m[1] - 1) * 1000 + 1 }); if (!cands.length) // bare modern year fallback ("book published 1995") for (const m of s.matchAll(/\b(1[5-9]\d{2}|20[0-2]\d)\b/g)) cands.push({ i: m.index!, y: +m[1] }); if (!cands.length) return null; const y = cands.sort((a, b) => a.i - b.i)[0].y; return y < YMIN ? YMIN : y > NOWY ? NOWY : y; } // evidence class: explicit frontmatter field wins; else the first class marker named in the date string function evClass(fm: Record, ds: string): string { if (fm.evidence_class) return fm.evidence_class; const a = ds.search(/1-archaeology/), t = ds.search(/2-text/); if (a >= 0 && (t < 0 || a < t)) return "1-archaeology"; if (t >= 0) return "2-text"; if (/3-reconstruction/.test(ds)) return "3-reconstruction"; if (/4-ethnography/.test(ds)) return "4-ethnography"; return ""; } const dateStrOf = (fm: Record) => { const ds = (fm.attestation_earliest || fm.date_of_evidence || "").trim(); return /^["']+$/.test(ds) ? "" : ds; }; // /api/timeline — every dated note in the vault as one cached JSON payload let tlCache = { v: "", body: "" }; function timelineJson(): string { const v = vaultVersion(); if (tlCache.v === v && tlCache.body) return tlCache.body; const events: object[] = []; const walk = (dir: string, r: string) => { for (const e of readdirSync(dir).sort()) { if (e.startsWith(".") || e === "node_modules") continue; const a = join(dir, e); if (statSync(a).isDirectory()) { walk(a, r + "/" + e); continue; } if (!e.endsWith(".md")) continue; let fm: Record; try { fm = parseFrontmatter(readFileSync(a, "utf8")).fm; } catch { continue; } const ds = dateStrOf(fm); if (!ds) continue; events.push({ p: r + "/" + e, t: fm.title || e.replace(/\.md$/, "").replace(/-/g, " "), d: r.split("/")[1] ?? "", type: fm.type || "note", y: earliestYear(ds), conf: fm.confidence || "", trans: fm.transmission || "", ev: evClass(fm, ds), ds: ds.length > 160 ? ds.slice(0, 157) + "…" : ds, }); } }; for (const e of readdirSync(ROOT).sort()) { if (e.startsWith(".") || e === "node_modules" || e === "templates" || e === "tools") continue; const a = join(ROOT, e); if (statSync(a).isDirectory()) walk(a, "/" + e); } events.sort((a: any, b: any) => (a.y ?? Infinity) - (b.y ?? Infinity)); tlCache = { v, body: JSON.stringify(events) }; return tlCache.body; } const BADGE_KEYS = ["type", "tier", "domain", "status", "confidence", "transmission", "evidence_class"]; function fmCard(fm: Record): string { const keys = Object.keys(fm).filter(k => fm[k] && fm[k] !== "[]" && fm[k] !== '""'); if (!keys.length) return ""; const badges = BADGE_KEYS.filter(k => fm[k]).map(k => `${esc(k.replace("_", " "))} · ${esc(fm[k])}`).join(""); const facts = ["citation", "attestation_earliest", "counter_evidence", "thesis", "region_origin", "url_verified", "date_of_evidence", "source_diversity"] .filter(k => fm[k]).map(k => `
${esc(k.replace(/_/g, " "))}${esc(fm[k])}
`).join(""); return `
${badges}
${facts}
`; } const DOMAINS: [string, string][] = [ ["01_prehistoric", "Prehistoric"], ["02_mesopotamian", "Mesopotamia"], ["03_egyptian", "Egypt"], ["04_indo_european", "Indo-European"], ["05_abrahamic", "Abrahamic"], ["06_dharmic", "Dharmic"], ["07_east_asian", "East Asia"], ["08_indigenous", "Indigenous"], ["09_comparative", "Comparative"], ]; function progressBar(): string { let pct = 0, label = "", step = "", iter = ""; try { const fm = parseFrontmatter(readFileSync(join(ROOT, "ISA.md"), "utf8")).fm; const m = (fm.progress || "").match(/(\d+)\/(\d+)/); if (m) { pct = Math.round((+m[1] / +m[2]) * 100); label = `${m[1]}/${m[2]} criteria · ${pct}%`; } } catch {} try { step = readFileSync(join(ROOT, "00_meta/now.txt"), "utf8").trim(); } catch {} try { const ls = JSON.parse(readFileSync(join(ROOT, "00_meta/loop-state.json"), "utf8")); iter = `iteration ${ls.iteration}/${ls.total}`; } catch {} if (!label && !step) return ""; return `
${esc(iter)} ${esc(step)} ${esc(label)}
`; } function nav(active: string): string { const EXPERIENCES: [string, string][] = [ ["/tree", "Tree"], ["/voices", "Voices"], ["/map", "Map"], ["/scenes", "Scenes"], ["/observatory", "Observatory"], ["/pantheon", "Pantheon"], ["/whisper", "Whisper"], ["/rituals", "Rituals"], ]; const tail: [string, string][] = [ ["/00_meta/STATUS.md", "Status"], ["/00_meta/OPEN_QUESTIONS.md", "Questions"], ["/00_meta/Methodology.md", "Method"], ["/AGENTS.md", "Ethics"], ["/ISA.md", "ISA"], ]; const a = ([h, t]: [string, string]) => `${t}`; const dd = DOMAINS.map(([d, n]) => `${n}`).join(""); const exOn = EXPERIENCES.some(([h]) => h === active); return `
origins//research
`; } const CSS = ` :root{--bg:#0f1117;--panel:#161922;--card:#1c1f27;--line:#272b38;--fg:#d8dee9;--dim:#8a93a5; --blue:#7aa2f7;--green:#9ece6a;--purple:#bb9af7;--pink:#f7768e;--amber:#e0af68} *{box-sizing:border-box}html{scroll-behavior:smooth} body{margin:0;background:var(--bg);color:var(--fg);font:16px/1.7 ui-sans-serif,system-ui,"Segoe UI",Inter,Roboto,sans-serif; text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased} a{color:var(--blue);text-decoration:none}a:hover{color:#a8c3ff} .wiki{color:var(--green);border-bottom:1px dashed #9ece6a44} code{background:#22263250;border:1px solid var(--line);padding:.08em .4em;border-radius:6px;font-size:.88em;font-family:inherit;color:var(--amber)} pre{background:var(--card);border:1px solid var(--line);padding:1rem 1.2rem;border-radius:12px;overflow-x:auto} pre code{border:0;background:none;color:var(--fg)} .top{position:sticky;top:0;z-index:10;background:#0f1117e6;backdrop-filter:blur(10px);border-bottom:1px solid var(--line)} .topin{max-width:72rem;margin:0 auto;padding:.7rem 1.2rem;display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap} .wordmark{font-weight:800;font-size:1.05rem;letter-spacing:-.02em;color:var(--fg)} .wordmark span{color:var(--pink);font-weight:800} .top nav{display:flex;gap:.2rem;align-items:center;flex-wrap:wrap} .top nav a{padding:.35rem .7rem;border-radius:8px;font-size:.88rem;font-weight:600;color:var(--dim)} .top nav a:hover{color:var(--fg);background:var(--card)}.top nav a.on{color:var(--fg);background:var(--card)} .dd{position:relative}.ddm{display:none;position:absolute;left:0;top:100%;background:var(--card);border:1px solid var(--line); border-radius:12px;padding:.4rem;min-width:13rem;box-shadow:0 12px 40px #0009} .dd:hover .ddm{display:block}.ddm a{display:block} main{max-width:72rem;margin:0 auto;padding:1.4rem 1.2rem 4rem} article{max-width:50rem;margin:0 auto} h1{font-size:2.1rem;line-height:1.15;letter-spacing:-.03em;font-weight:800;margin:1.2rem 0 .8rem} h2{font-size:1.35rem;letter-spacing:-.02em;font-weight:750;margin-top:2.4rem;padding-bottom:.35rem;border-bottom:1px solid var(--line)} h3{font-size:1.05rem;font-weight:700;color:var(--fg);margin-top:1.8rem} h2 a,h3 a{color:inherit} blockquote{border-left:3px solid var(--purple);margin:1.2rem 0;padding:.4rem 1.2rem;color:#b6bdcc;background:var(--panel);border-radius:0 12px 12px 0} blockquote p{margin:.4rem 0} .tablewrap{overflow-x:auto;margin:1.2rem 0;border:1px solid var(--line);border-radius:12px} table{border-collapse:collapse;width:100%;font-size:.92rem} th,td{border-bottom:1px solid var(--line);padding:.55rem .8rem;text-align:left;vertical-align:top} th{background:var(--panel);font-weight:700;font-size:.8rem;text-transform:uppercase;letter-spacing:.06em;color:var(--dim)} tr:last-child td{border-bottom:0} hr{border:0;border-top:1px solid var(--line);margin:2rem 0} .crumb{font-size:.82rem;color:var(--dim);margin:0 0 1rem}.crumb a{color:var(--dim)}.crumb a:hover{color:var(--fg)} .meta{background:linear-gradient(180deg,var(--panel),var(--card));border:1px solid var(--line);border-radius:16px;padding:1rem 1.2rem;margin:0 0 1.8rem} .badges{display:flex;flex-wrap:wrap;gap:.35rem;margin-bottom:.4rem} .badge{font-size:.72rem;font-weight:700;letter-spacing:.04em;padding:.2em .7em;border-radius:999px;background:#272b38;color:var(--fg)} .b-tier{background:#3d59a155}.b-type{background:#2f5e4455;color:var(--green)}.b-domain{background:#272b38;color:var(--dim)} .c-high{background:#9ece6a22;color:var(--green)}.c-medium{background:#e0af6822;color:var(--amber)} .c-low{background:#f7768e22;color:var(--pink)}.c-speculative{background:#f7768e33;color:var(--pink)} .t-descent{background:#9ece6a22;color:var(--green)}.t-contact{background:#7aa2f722;color:var(--blue)} .t-convergence{background:#bb9af722;color:var(--purple)}.t-unresolved{background:#272b38;color:var(--dim)} .fact{font-size:.86rem;color:#b6bdcc;padding:.25rem 0;border-top:1px dashed var(--line)} .fact span{display:inline-block;min-width:11rem;color:var(--dim);font-weight:600;font-size:.74rem;text-transform:uppercase;letter-spacing:.05em} .toc{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:.9rem 1.2rem;margin:0 0 1.6rem;font-size:.9rem} .toc b{font-size:.74rem;text-transform:uppercase;letter-spacing:.08em;color:var(--dim)} .toc a{display:block;padding:.12rem 0;color:#b6bdcc}.toc a.l3{padding-left:1rem;color:var(--dim)} .hero{position:relative;border:1px solid var(--line);border-radius:22px;overflow:hidden;margin:.4rem 0 2rem; background:#0c0e13 url(/assets/bg-braided-river.svg) center/cover no-repeat} .heroin{padding:3.6rem 2.4rem;background:linear-gradient(180deg,#0f111766,#0f1117ee)} .hero h1{font-size:2.7rem;margin:.2rem 0 .6rem} .hero p{max-width:44rem;color:#b6bdcc;font-size:1.06rem} .kicker{font-size:.78rem;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:var(--pink)} .stats{display:flex;gap:.6rem;flex-wrap:wrap;margin-top:1.4rem} .stat{background:#161922cc;border:1px solid var(--line);border-radius:12px;padding:.5rem 1rem;font-size:.85rem;color:var(--dim)} .stat b{display:block;font-size:1.3rem;color:var(--fg);letter-spacing:-.02em} .figgrid{display:grid;gap:1.2rem;margin:1.6rem 0 2.2rem} figure{margin:0;background:var(--panel);border:1px solid var(--line);border-radius:18px;overflow:hidden} figure img{display:block;width:100%;height:auto} figcaption{padding:.7rem 1.1rem;font-size:.85rem;color:var(--dim);border-top:1px solid var(--line)} .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(15rem,1fr));gap:.9rem;margin:1.4rem 0} .cardlink{display:block;background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:1rem 1.1rem;color:var(--fg);transition:border-color .15s,transform .15s} .cardlink:hover{border-color:#3d59a1;transform:translateY(-2px);color:var(--fg)} .cardlink .t{font-weight:700;letter-spacing:-.01em;line-height:1.3} .cardlink .k{font-size:.72rem;font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:var(--dim);margin-bottom:.3rem} .cardlink .b{margin-top:.5rem;display:flex;gap:.3rem;flex-wrap:wrap} footer{border-top:1px solid var(--line);margin-top:3rem;padding-top:1.2rem;font-size:.82rem;color:var(--dim);display:flex;gap:1rem;flex-wrap:wrap} .prog{position:sticky;top:0;z-index:9;background:#0c0e13f2;border-bottom:1px solid var(--line)} .progbar{height:3px;background:#272b38} .progfill{height:100%;background:linear-gradient(90deg,var(--green),var(--blue),var(--purple));transition:width .6s} .progtxt{max-width:72rem;margin:0 auto;padding:.4rem 1.2rem;font-size:.78rem;color:var(--dim);display:flex;gap:.5rem;align-items:center} .progtxt b{color:var(--fg);white-space:nowrap} .progpct{margin-left:auto;color:var(--fg);font-weight:700;white-space:nowrap} .pulse{width:8px;height:8px;border-radius:50%;background:var(--green);flex:none;animation:pu 1.6s infinite} @keyframes pu{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.75)}} .notehero{padding:1rem 0 .4rem}.notehero h1{margin:.3rem 0 .7rem;font-size:2.3rem} .notegrid{display:grid;grid-template-columns:minmax(0,1fr) 16rem;gap:1.6rem;align-items:start} @media(max-width:860px){.notegrid{grid-template-columns:1fr}.aside{order:2}} .notemain{min-width:0} .sect{background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:.4rem 1.3rem 1rem;margin:0 0 1.1rem} .sect h2,.sect h3{border:0;margin-top:1rem;font-size:1.02rem} .secthead{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;padding:.85rem 0 .3rem;font-weight:750;letter-spacing:-.01em;font-size:1.06rem} .secthead i{font-style:normal;font-size:.68rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--dim);white-space:nowrap} .s-lede{background:linear-gradient(180deg,#1c2030,var(--panel));border-color:#3d59a155} .s-lede p{font-size:1.13rem;line-height:1.75} .s-green{border-left:3px solid var(--green)}.s-green .secthead i{color:var(--green)} .s-pink{border-left:3px solid var(--pink)}.s-pink .secthead i{color:var(--pink)} .s-blue{border-left:3px solid var(--blue)}.s-blue .secthead i{color:var(--blue)} .s-purple{border-left:3px solid var(--purple)}.s-purple .secthead i{color:var(--purple)} .s-amber{border-left:3px solid var(--amber)}.s-amber .secthead i{color:var(--amber)} .s-dashed{border-style:dashed} .s-verdict{border-width:2px} .s-verdict.v-descent{border-color:#9ece6a66;background:linear-gradient(180deg,#16201577,var(--panel))} .s-verdict.v-contact{border-color:#7aa2f766;background:linear-gradient(180deg,#151b2877,var(--panel))} .s-verdict.v-convergence{border-color:#bb9af766;background:linear-gradient(180deg,#1c172877,var(--panel))} .s-verdict.v-unresolved{border-color:#8a93a566} .vword{font-size:1rem;text-transform:uppercase;letter-spacing:.14em;padding:.2em .8em;border-radius:999px} .aside{position:sticky;top:4.6rem;display:flex;flex-direction:column;gap:.9rem} .apanel{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:.8rem 1rem;font-size:.86rem} .apanel>b{display:block;font-size:.68rem;font-weight:800;text-transform:uppercase;letter-spacing:.12em;color:var(--dim);margin-bottom:.45rem} .apanel a{display:block;padding:.16rem 0;color:#b6bdcc}.apanel a:hover{color:var(--fg)} .afact{padding:.3rem 0;border-top:1px dashed var(--line);color:#b6bdcc;overflow-wrap:break-word} .afact:first-of-type{border-top:0} .afact span{display:block;font-size:.66rem;font-weight:800;text-transform:uppercase;letter-spacing:.1em;color:var(--dim)} @media(max-width:640px){.hero h1{font-size:1.9rem}.heroin{padding:2rem 1.2rem}} .trail{background:#0c0e13;border-bottom:1px solid var(--line)} .trailin{max-width:72rem;margin:0 auto;padding:.4rem 1.2rem .15rem;display:flex;gap:.7rem;align-items:center} .tmlab{flex:none;font-size:.8rem;opacity:.85} #tm{flex:1;height:14px;margin:0;accent-color:var(--amber);background:transparent;cursor:ew-resize} #tmyear{flex:none;font-size:.78rem;font-weight:800;color:var(--amber);min-width:6.5rem;text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums} .tstrip{max-width:72rem;margin:0 auto;padding:.05rem 1.2rem .5rem;font-size:.78rem;color:var(--dim);line-height:1.55} .tstrip b{color:var(--fg)}.tstrip a{color:#b6bdcc}.tstrip a:hover{color:var(--fg)}.tstrip .tdim{opacity:.6} [data-y]{transition:opacity .45s} [data-y].era-dim{opacity:.25} #tlwrap{position:relative;border:1px solid var(--line);border-radius:18px;overflow:hidden;background:#0c0e13;margin:1.2rem 0 .5rem} #tl{display:block;touch-action:none} #tltip{position:absolute;pointer-events:none;background:var(--card);border:1px solid var(--line);border-radius:10px;padding:.45rem .75rem;font-size:.78rem;line-height:1.45;max-width:24rem;box-shadow:0 10px 34px #000b} #tltip b{display:block;color:var(--fg)} .tlegend{display:flex;gap:1.1rem;flex-wrap:wrap;align-items:center;font-size:.8rem;color:var(--dim)} .tlegend i{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.4rem;font-style:normal;vertical-align:-1px} `; // ---------- designed note layouts ---------- const NOTE_TYPES = ["claim", "motif", "source", "tradition", "synthesis", "question"]; const TYPE_LABEL: Record = { claim: ["⚖", "Atomic claim"], motif: ["🜄", "Shared motif"], source: ["✦", "Primary source record"], tradition: ["⛩", "Tradition profile"], synthesis: ["∴", "Synthesis essay"], question: ["?", "Open question"], }; function splitSections(md: string): { title: string; md: string }[] { const out: { title: string; md: string }[] = []; let cur = { title: "", md: "" }; for (const l of md.split("\n")) { const m = l.match(/^##\s+(.*)/); if (m) { if (cur.md.trim() || cur.title) out.push(cur); cur = { title: m[1].trim(), md: "" }; } else if (/^#\s+/.test(l)) continue; // h1 replaced by hero else cur.md += l + "\n"; } if (cur.md.trim() || cur.title) out.push(cur); return out; } function sectKind(t: string): { cls: string; tag: string } { const s = t.toLowerCase(); if (/counter|steelman/.test(s)) return { cls: "s-pink", tag: "against" }; if (/support|evidence for/.test(s)) return { cls: "s-green", tag: "for" }; if (/verdict/.test(s)) return { cls: "s-verdict", tag: "verdict" }; if (/transmission/.test(s)) return { cls: "s-blue", tag: "analysis" }; if (/feynman/.test(s)) return { cls: "s-purple", tag: "feynman check" }; if (/open question/.test(s)) return { cls: "s-amber", tag: "gaps" }; if (/emic|self-account/.test(s)) return { cls: "s-purple", tag: "emic · their voice" }; if (/change my mind|reliability/.test(s)) return { cls: "s-dashed", tag: "epistemics" }; if (/claim$|^the claim|thesis|story pattern|what this source/.test(s)) return { cls: "s-lede", tag: "the core" }; if (/occurrence|extraction/.test(s)) return { cls: "s-plain", tag: "data" }; return { cls: "s-plain", tag: "" }; } function renderNote(fm: Record, body: string, rel: string): string { const [icon, label] = TYPE_LABEL[fm.type] ?? ["·", fm.type || "note"]; const name = rel.split("/").pop()!.replace(/\.md$/, ""); const banner = illo(name); const badges = BADGE_KEYS.filter(k => fm[k]).map(k => `${esc(k.replace("_", " "))} · ${esc(fm[k])}`).join(""); const secs = splitSections(body); const main = secs.map(s => { const { html } = mdToHtml(s.md); if (!s.title) return html.trim() ? `
${html}
` : ""; const k = sectKind(s.title); if (k.cls === "s-verdict") { const t = fm.transmission ?? ""; return `
${esc(s.title)}${t ? `${esc(t)}` : ""}
${html}
`; } return `
${esc(s.title)}${k.tag ? `${k.tag}` : ""}
${html}
`; }).join(""); const factKeys = Object.keys(fm).filter(k => !BADGE_KEYS.includes(k) && !["title", "created", "updated", "tags"].includes(k) && fm[k] && fm[k] !== "[]" && fm[k] !== '""'); const facts = factKeys.map(k => `
${esc(k.replace(/_/g, " "))}${esc(fm[k])}
`).join(""); const tags = (fm.tags ?? "").replace(/[\[\]"]/g, "").split(",").map(t => t.trim()).filter(Boolean) .map(t => `${esc(t)}`).join(""); const toc = secs.filter(s => s.title).map(s => `${esc(s.title)}`).join(""); const dy = earliestYear(dateStrOf(fm)); return `${banner}
${icon} ${esc(label)}

${esc(fm.title || name.replace(/-/g, " "))}

${badges}
${main}
`; } // ---------- time-machine mode: header scrubber + context strip ---------- function timeRail(): string { return `
today
`; } // log-scale scrubber: slider p∈[0,1] → year, 100,000 BP at p=0, today at p=1. // Dims every [data-y] newer than the scrub year; context strip shows the 3 nearest attestations before the moment. const TIMEJS = ` (()=>{ const tm=document.getElementById('tm');if(!tm)return; const out=document.getElementById('tmyear'),strip=document.getElementById('tstrip'); const YMAX=${NOWY},YMIN=${YMIN},K=(YMAX-YMIN)/99999; const yOf=function(p){return p>=.999?YMAX:Math.round(YMAX-(Math.pow(10,5*(1-p))-1)*K)}; const pOf=function(y){return y>=YMAX?1:1-Math.log10(1+(YMAX-y)/K)/5}; const com=function(x){return String(x).replace(/\\B(?=(\\d{3})+(?!\\d))/g,',')}; const fmt=function(y){return y>=YMAX?'today':y<-10050?com(1950-y)+' BP':y<=0?com(1-y)+' BCE':com(y)+' CE'}; let data=null,loading=false,cur=YMAX; function load(){if(data||loading)return;loading=true; fetch('/api/timeline').then(function(r){return r.json()}).then(function(j){data=j;render(cur)}).catch(function(){loading=false});} function render(y){ out.textContent=fmt(y); const today=y>=YMAX,els=document.querySelectorAll('[data-y]'); for(let i=0;iy); } if(today){strip.hidden=true;return} if(!data){load();return} const near=data.filter(function(e){return e.y!==null&&e.y!==undefined&&e.y<=y}).sort(function(a,b){return b.y-a.y}).slice(0,3); strip.textContent=''; const b=document.createElement('b');b.textContent='\\u231B You are in '+fmt(y);strip.appendChild(b); strip.appendChild(document.createTextNode(near.length?' \\u2014 nearest attestations before this moment: ':' \\u2014 nothing in the vault is attested yet.')); for(let i=0;i=YMAX){history.replaceState(null,'',location.pathname+location.search);try{localStorage.removeItem('tmyear')}catch(e){}} else{history.replaceState(null,'','#t='+y);try{localStorage.setItem('tmyear',String(y))}catch(e){}} render(y); } tm.addEventListener('input',function(){set(yOf(+tm.value/1000),true)}); let saved=null; const h=location.hash.match(/^#t=(-?\\d+)$/); if(h)saved=+h[1]; else{try{const ls=localStorage.getItem('tmyear');if(ls!==null)saved=+ls}catch(e){}} if(saved!==null&&isFinite(saved)&&savedvault`]; for (const p of parts) { acc += "/" + p; crumbs.push(`${esc(p)}`); } const crumb = parts.length ? `` : ""; const html = ` ${esc(title)} ${nav(active)}${progressBar()}${timeRail()}
${crumb}${body}
`; return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } }); } function noteCard(href: string, name: string, abs: string): string { let fm: Record = {}; try { fm = parseFrontmatter(readFileSync(abs, "utf8")).fm; } catch {} const title = fm.title && fm.title !== "" ? fm.title : name.replace(/\.md$/, "").replace(/-/g, " "); const kind = fm.type || "note"; const b: string[] = []; if (fm.confidence) b.push(`${esc(fm.confidence)}`); if (fm.transmission) b.push(`${esc(fm.transmission)}`); if (fm.tier) b.push(`tier ${esc(fm.tier)}`); const dy = earliestYear(dateStrOf(fm)); return `
${esc(kind)}
${esc(title)}
${b.length ? `
${b.join("")}
` : ""}
`; } function listDir(abs: string, rel: string): Response { const entries = readdirSync(abs).filter(n => !n.startsWith(".") && n !== "node_modules").sort(); const dirs = entries.filter(n => statSync(join(abs, n)).isDirectory()); const files = entries.filter(n => !statSync(join(abs, n)).isDirectory()); const base = rel === "/" ? "" : rel; const dirCards = dirs.map(n => `
directory
📁 ${esc(n)}
`).join(""); const fileCards = files.map(n => n.endsWith(".md") ? noteCard(`${base}/${n}`, n, join(abs, n)) : `
file
${esc(n)}
`).join(""); const dn = DOMAINS.find(([d]) => rel === "/" + d); const h = dn ? `${dn[1]} · ${esc(dn[0])}` : esc(rel); const banner = illo(rel.split("/").filter(Boolean).pop() ?? ""); return page(rel, rel, `${banner}

${h}

${dirCards || fileCards ? `
${dirCards}${fileCards}
` : "

empty

"}`, "/" + (parts0(rel))); } const parts0 = (rel: string) => rel.split("/").filter(Boolean)[0] ?? ""; // auto-banner: any page whose basename matches assets/illustrations/.svg gets it as a header figure function illo(name: string): string { const base = `illustrations/${name.replace(/\.md$/, "")}`; const f = existsSync(join(ROOT, "assets", base + ".png")) ? base + ".png" : existsSync(join(ROOT, "assets", base + ".svg")) ? base + ".svg" : ""; return f ? `
${esc(name)} illustration
` : ""; } // vault version = newest mtime across the tree; viewers poll and self-refresh on change let verCache = { t: 0, v: "" }; function vaultVersion(): string { if (Date.now() - verCache.t < 1000) return verCache.v; let max = 0; const walk = (d: string) => { for (const e of readdirSync(d)) { if (e.startsWith(".") || e === "node_modules") continue; const a = join(d, e); const st = statSync(a); if (st.isDirectory()) walk(a); else if (st.mtimeMs > max) max = st.mtimeMs; } }; try { walk(ROOT); } catch {} verCache = { t: Date.now(), v: String(Math.floor(max)) }; return verCache.v; } function countFiles(dir: string): number { let n = 0; for (const e of readdirSync(dir)) { if (e.startsWith(".")) continue; const a = join(dir, e); if (statSync(a).isDirectory()) n += countFiles(a); else if (e.endsWith(".md")) n++; } return n; } function home(): Response { const idx = readFileSync(join(ROOT, "INDEX.md"), "utf8"); const { body } = parseFrontmatter(idx); const { html } = mdToHtml(body.replace(/^# .*\n/, "")); let notes = 0; for (const [d] of DOMAINS) try { notes += countFiles(join(ROOT, d)); } catch {} let openQ = 0; try { openQ = (readFileSync(join(ROOT, "00_meta/OPEN_QUESTIONS.md"), "utf8").match(/^\| Q\d+/gm) || []).length; } catch {} const figs = [ ["diagram-transmission.svg", "Why religions share stories: descent, contact, or convergence — every motif gets one falsifiable verdict."], ["diagram-tiers.svg", "The three-tier engine: material moves up through templates; open questions flow back down and re-task gathering."], ["chart-attestation-timeline.svg", "Earliest dated evidence per tradition — archaeology (amber) vs earliest text (blue)."], ].filter(([f]) => existsSync(join(ROOT, "assets", f))) .map(([f, c]) => `
${esc(c)}
${esc(c)}
`).join(""); const hero = `
a second brain on the origins of religion

Where do the gods come from?

Every religion of human history, traced to its earliest dated evidence. Every shared story — floods, dying gods, sky fathers — given a falsifiable verdict: descent, contact, or convergence.

Read the answer →

${DOMAINS.length}domains
${notes}research notes
${openQ}open questions
3tiers · sources → notes → synthesis
`; return page("origins//research — religion origins vault", "/", `${hero}${figs ? `
${figs}
` : ""}
${html}
`, "/"); } // ---------- /timeline — deep-time zoom timeline (canvas, arcsinh-compressed axis) ---------- const TLJS = ` (()=>{ const wrap=document.getElementById('tlwrap'),cv=document.getElementById('tl'),tip=document.getElementById('tltip'); if(!cv)return; const ctx=cv.getContext('2d'); const YMAX=${NOWY},YMIN=${YMIN},S=60; const ua=function(y){return Math.asinh((YMAX-y)/S)}; const UMAX=ua(YMIN); let va=0,vb=UMAX,W=900,H=470,dpr=1; let events=[],dots=[],hov=null; const LANES=TL_LANES,laneIdx={}; LANES.forEach(function(l,i){laneIdx[l[0]]=i}); const ERAS=[[YMIN,-10050,'Paleolithic'],[-10050,-3300,'Neolithic'],[-3300,-1200,'Bronze Age'],[-1200,1,'Iron Age'],[1,YMAX,'Common Era']]; const com=function(x){return String(x).replace(/\\B(?=(\\d{3})+(?!\\d))/g,',')}; const fmt=function(y){y=Math.round(y);return y<-10050?com(1950-y)+' BP':y<=0?com(1-y)+' BCE':com(y)+' CE'}; const xOf=function(y){return W*(vb-ua(y))/(vb-va)}; const yAt=function(x){return YMAX-S*Math.sinh(vb-(x/W)*(vb-va))}; function clampView(){ const span=vb-va; if(vb>UMAX+.4){vb=UMAX+.4;va=vb-span} if(va<-.4){va=-.4;vb=Math.min(va+span,UMAX+.4)} } function colorOf(e){ const ev=e.ev||''; if(ev.indexOf('1-archaeology')>=0)return '#e0af68'; if(ev.indexOf('2-text')>=0)return '#7aa2f7'; return '#bb9af7'; } function draw(){ ctx.clearRect(0,0,W,H); const laneTop=26,laneH=Math.floor((H-92)/LANES.length),axisY=H-58; for(let i=0;i=W)continue; ctx.fillStyle=i%2?'#151823':'#10131b'; ctx.fillRect(x1,laneTop,x2-x1,axisY-laneTop); if(x2-x1>84){ ctx.fillStyle='#8a93a5';ctx.font='700 11px ui-sans-serif,system-ui,sans-serif';ctx.textAlign='center'; ctx.fillText(ERAS[i][2].toUpperCase(),(x1+x2)/2,axisY+34); } } ctx.textAlign='left'; for(let i=0;i2500)continue; for(let y=Math.ceil(yL/st)*st;y<=yR;y+=st){ const x=xOf(y);if(x<16||x>W-16)continue; let ok=true;for(const t of taken)if(Math.abs(t-x)<64){ok=false;break} if(!ok)continue; taken.push(x); ctx.strokeStyle='#3a4154';ctx.beginPath();ctx.moveTo(x,axisY);ctx.lineTo(x,axisY+5);ctx.stroke(); ctx.fillStyle='#8a93a5';ctx.fillText(fmt(y),x,axisY+17); } } dots=[];const occ=[]; for(const e of events){ const x=xOf(e.y);if(x<-20||x>W+20)continue; const li=laneIdx[e.d]!==undefined?laneIdx[e.d]:LANES.length-1; let lvl=0; for(const l of [0,1,-1,2,-2,3,-3]){lvl=l;let clash=false; for(const o of occ)if(o.li===li&&o.lvl===l&&Math.abs(o.x-x)<11){clash=true;break} if(!clash)break} occ.push({li:li,lvl:lvl,x:x}); dots.push({x:x,y:laneTop+li*laneH+Math.floor(laneH/2)+lvl*9,e:e}); } for(const d of dots){ ctx.beginPath();ctx.arc(d.x,d.y,4.5,0,6.2832); ctx.globalAlpha=d.e.conf==='speculative'?.45:1; ctx.fillStyle=colorOf(d.e);ctx.fill();ctx.globalAlpha=1; ctx.strokeStyle='#0c0e13';ctx.stroke(); } if(hov){ctx.beginPath();ctx.arc(hov.x,hov.y,7,0,6.2832);ctx.strokeStyle='#d8dee9';ctx.stroke()} } function resize(){ dpr=window.devicePixelRatio||1;W=wrap.clientWidth;H=470; cv.width=Math.round(W*dpr);cv.height=Math.round(H*dpr); cv.style.width=W+'px';cv.style.height=H+'px'; ctx.setTransform(dpr,0,0,dpr,0,0);draw(); } let dragging=false,lastX=0,moved=0; cv.addEventListener('mousedown',function(e){dragging=true;lastX=e.clientX;moved=0;cv.style.cursor='grabbing'}); window.addEventListener('mouseup',function(){ if(dragging&&moved<5&&hov)location.href=hov.e.p; dragging=false;cv.style.cursor='grab'; }); cv.addEventListener('mousemove',function(e){ const r=cv.getBoundingClientRect(),mx=e.clientX-r.left,my=e.clientY-r.top; if(dragging){ const dx=e.clientX-lastX;lastX=e.clientX;moved+=Math.abs(dx); const s=dx*(vb-va)/W;va+=s;vb+=s;clampView();hov=null;tip.hidden=true;draw();return; } let best=null,bd=121; for(const d of dots){const q=(d.x-mx)*(d.x-mx)+(d.y-my)*(d.y-my);if(qUMAX+.8)return; va=na;vb=nb;clampView();draw(); },{passive:false}); window.addEventListener('resize',resize); cv.style.cursor='grab'; resize(); fetch('/api/timeline').then(function(r){return r.json()}).then(function(j){ events=j.filter(function(e){return e.y!==null&&e.y!==undefined}); draw(); }); })();`; function timelinePage(): Response { const body = `

Deep-Time Timeline

Everything this vault has dated, in one place — 100,000 BP to today on an arcsinh-compressed axis. Scroll to zoom · drag to travel · click a dot to open its note. The dark is real — that's how little we can date.

archaeology (class 1) text (class 2) reconstruction · ethnography · other faded dot = speculative confidence
`; return page("Deep-Time Timeline — origins//research", "/timeline", body, "/timeline"); } // ---------- /tree — descent-tree explorer (static-first slice, SVG, server-rendered) ---------- const EV_COLOR: Record = { "1-archaeology": "#e0af68", "2-text": "#7aa2f7", "3-reconstruction": "#bb9af7", "4-ethnography": "#9ece6a", }; const TREE_H = 660; function treeSvg(t: LineageTree): string { const padT = 28, padB = 44; const yPix = (y: number) => padT + ((t.yTop - y) / (t.yTop - t.yBot)) * (TREE_H - padT - padB); const byId: Record = {}; for (const n of t.nodes) byId[n.id] = n; const parts: string[] = []; // era depth gridlines — vertical axis is loosely time-ordered, root at the bottom const step = t.yTop - t.yBot > 4000 ? 1000 : 500; for (let y = Math.ceil(t.yBot / step) * step; y <= t.yTop; y += step) { const py = yPix(y); const lab = y <= 0 ? `${1 - y} BCE` : `${y} CE`; parts.push(`` + `${lab}`); } // branches (edges drawn under nodes) for (const n of t.nodes) { for (const pid of [n.parent, n.parent2]) { if (!pid || !byId[pid]) continue; const p = byId[pid]; const x1 = p.x, y1 = yPix(p.y), x2 = n.x, y2 = yPix(n.y), my = (y1 + y2) / 2; const cog = n.cognate && p.cognate ? ` data-cognate="${esc(n.cognate)}"` : ""; parts.push(``); } } // honesty label in the root zone if (t.reconLabel) parts.push(`${esc(t.reconLabel)}`); // nodes (fruit = dated attestation dot, colored by evidence class) for (const n of t.nodes) { const py = yPix(n.y); const anchor = n.x < t.w * 0.14 ? "start" : n.x > t.w * 0.86 ? "end" : "middle"; const tx = anchor === "start" ? n.x - 8 : anchor === "end" ? n.x + 8 : n.x; const color = EV_COLOR[n.cls ?? ""] ?? "#8a93a5"; const shape = n.kind === "fruit" ? `` : n.kind === "context" ? `` : ``; const lines: string[] = [`${esc(n.label)}`]; if (n.date) lines.push(`${esc(n.date)}`); if (n.sub) lines.push(`${esc(n.sub)}`); const cog = n.cognate ? ` data-cognate="${esc(n.cognate)}"` : ""; parts.push(`${shape}${lines.join("")}`); } return `${parts.join("")}`; } const TREE_CSS = ` .treesec{margin:1.8rem 0} .treesec h2{margin-top:0} .tblurb{color:var(--dim);max-width:48rem;margin:.3rem 0 .9rem;font-size:.92rem} .treewrap{border:1px solid var(--line);border-radius:18px;background:#0c0e13;overflow:hidden} .treewrap svg{display:block;width:100%;height:auto} .treecols{display:grid;grid-template-columns:1fr 1fr;gap:1.4rem} @media(max-width:860px){.treecols{grid-template-columns:1fr}} .tedge{fill:none;stroke:#5a6378;stroke-width:2.5;transition:opacity .25s} .e-dashed{stroke-dasharray:7 6;opacity:.4;stroke:#bb9af7} .e-context{stroke-dasharray:2 5;opacity:.35} .tnode text{font:600 13px ui-sans-serif,system-ui,sans-serif;fill:var(--fg)} .tnode .tsub{font-size:10px;fill:var(--dim);font-weight:500} .tnode .tdate{font-size:10.5px;fill:var(--amber);font-weight:700;font-variant-numeric:tabular-nums} .k-context text{fill:var(--dim)}.k-context .tdate{fill:#5a6378} .tnode.recon{opacity:.7} .tgrid{stroke:#1a1e2a;stroke-width:1}.tgridlab{font-size:9px;fill:#5a6378} .treclab{font-size:11px;fill:#bb9af7;opacity:.85;font-style:italic} [data-cognate]{cursor:pointer} g.glow circle{stroke:#fff;stroke-width:2.5;filter:drop-shadow(0 0 7px #e0af68)} g.glow text{fill:#fff}g.glow .tsub{fill:#d8dee9} path.glow{stroke:#e0af68 !important;opacity:1 !important;filter:drop-shadow(0 0 5px #e0af6888)} `; // cognate glow: hovering/tapping any node with data-cognate lights the whole set + connecting wood const TREE_JS = ` (()=>{ const els=document.querySelectorAll('[data-cognate]'); function set(id,on){for(const e of els)if(e.getAttribute('data-cognate')===id)e.classList.toggle('glow',on)} for(const e of els){ const id=e.getAttribute('data-cognate'); e.addEventListener('pointerenter',function(){set(id,true)}); e.addEventListener('pointerleave',function(){set(id,false)}); } })();`; function treePage(): Response { const [pie, ...rest] = TREES; const sect = (t: LineageTree) => `

${esc(t.title)}

${esc(t.blurb)}

${treeSvg(t)}
`; const body = `

Descent Tree Explorer

Three lineage trees grown from vault data — roots in the deep past at the bottom, the present at the top. Hover a deity to ignite its cognate set across branches · click any node to open its vault note. Dashed, translucent wood is reconstruction or inference — honestly marked, never solid.

archaeology (class 1) text (class 2) reconstruction (class 3) dashed branch = reconstructed / inferred / contested segment
${sect(pie)}
${rest.map(sect).join("")}
`; return page("Descent Tree Explorer — origins//research", "/tree", body, "/tree"); } // ---------- /voices — First Voices gallery (two rooms; pure-CSS reveal) ---------- // Honesty rule: UNVERIFIED QUOTES ARE FORBIDDEN. Only wording actually quoted in a vault // note is rendered as a quotation; everything else is explicitly marked as paraphrase. const VOICES_CSS = ` .vhall{display:grid;grid-template-columns:1fr 1fr;gap:1.4rem;margin:1.6rem 0} @media(max-width:760px){.vhall{grid-template-columns:1fr}} .vcard{position:relative;display:block;border-radius:20px;overflow:hidden;border:1px solid var(--line);min-height:24rem;background-size:cover;background-position:center;transition:transform .2s,border-color .2s} .vcard:hover{transform:translateY(-3px);border-color:#e0af6866} .vcard .vin{position:absolute;inset:0;background:linear-gradient(180deg,#0003 30%,#000d 85%);display:flex;flex-direction:column;justify-content:flex-end;padding:1.6rem;color:var(--fg)} .vcard .vd{font-size:1.7rem;font-weight:800;letter-spacing:-.02em;color:#e0af68;font-variant-numeric:tabular-nums} .vcard .vp{color:var(--dim);font-size:.9rem} .vcard .vgo{margin-top:.5rem;font-size:.82rem;font-weight:700;color:var(--fg)} .vroom{position:relative;border-radius:22px;overflow:hidden;border:1px solid var(--line);background-size:cover;background-position:center;margin:1rem 0} .vshade{background:linear-gradient(180deg,#000000a8,#000000d4 35%,#000000ef);padding:3.4rem 2.6rem 2.4rem;min-height:76vh} @media(max-width:640px){.vshade{padding:2rem 1.2rem}} .vroomin{max-width:46rem;margin:0 auto} .vkick{font-size:.76rem;font-weight:800;letter-spacing:.18em;text-transform:uppercase;color:#e0af68} .vroom h1{font-size:2rem;margin:.5rem 0 1.6rem;color:#fff} .vlabel{font-size:.72rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--dim);margin:2rem 0 .6rem} .vquote p{font-size:1.45rem;line-height:1.65;color:#eef1f6;margin:.35rem 0;font-weight:500} .vquote .vq{color:#fff;font-weight:600} .vsrc{font-size:.85rem;color:var(--dim);margin-top:.5rem} .vpara p{font-size:1.05rem;line-height:1.8;color:#c4cad6} .vemic{border:1px solid #bb9af755;background:#bb9af714;border-radius:16px;padding:1.1rem 1.5rem 1.2rem;margin-top:2.6rem} .vemic b{display:block;font-size:.7rem;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--purple);margin-bottom:.5rem} .vemic p{margin:.3rem 0;color:#d6dae3;line-height:1.75} .vetic{border-top:1px solid #272b38aa;margin-top:2.6rem;padding-top:1rem;font-size:.82rem;color:var(--dim);line-height:1.7} .vetic b{color:#b6bdcc} .vnext{display:inline-block;margin-top:2rem;font-weight:700;color:#e0af68;font-size:1rem} .vline{opacity:0;animation:vfade 1.3s ease forwards} @keyframes vfade{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}} @media(prefers-reduced-motion:reduce){.vline{animation:none;opacity:1}} `; const vl = (i: number, html: string, cls = "") => `
${html}
`; function voicesHall(): Response { const cards = [ { href: "/voices/unas", img: "/assets/illustrations/03_egyptian.png", d: "c. 2350 BCE", p: "a burial chamber at Saqqara, Egypt" }, { href: "/voices/baal", img: "/assets/illustrations/02_mesopotamian.png", d: "c. 1400–1200 BCE", p: "a scribe's room at Ugarit, Syria" }, ].map(c => `
${esc(c.d)}
${esc(c.p)}
enter the room →
`).join(""); const body = `
First Voices

Humanity's earliest surviving religious words

Everything else in this vault is about ancient religion. These rooms are the only place where you are addressed by it. Two rooms are open; more open as their passages are verified against the cited editions — unverified quotes are forbidden here.

${cards}

Backdrops are the vault's domain illustrations, standing in until room-specific scenes are generated. Proposal: first-voices.

`; return page("First Voices — origins//research", "/voices", body, "/voices"); } function voiceRoomUnas(): string { let i = 0; return `
${vl(i++, `
First Voices · Room I · Saqqara, Egypt
`)} ${vl(i++, `

c. 2350 BCE — the pyramid of Unas

`)} ${vl(i++, `
The passage — paraphrase after Faulkner (1969)

The vault carries no verified verbatim utterance from this corpus yet, so nothing below is presented as quotation. This is a paraphrase of what the vault's source record attests the utterances say.

`)}
${vl(i++, `

The utterances carved for Unas insist that the king lives:

`)} ${vl(i++, `

that he rises and is gathered together,

`)} ${vl(i++, `

that he ascends to the sky and takes his place among the gods,

`)} ${vl(i++, `

that he is Osiris, and he is Re.

`)}
${vl(i++, `

Original script: Egyptian hieroglyphs, carved in vertical columns on the chamber walls — the inscribed copy itself is the datum. No transliteration is carried in the vault record.

`)} ${vl(i++, `
Verbatim — from the edition's introduction (as quoted in the vault source record)

“The texts preserved in the pyramid of Unas constitute the earliest known collection of religious literature in the world.”

— Faulkner, introduction, Vol. I

`)} ${vl(i++, `
What this person was asking for — emic · interpretive

That the king's death not be a death. The priests who composed these spells, and the craftsmen who cut them into the chamber walls, are not describing a funeral — they are insisting, in stone, that Unas rises, crosses the sky, and sits among the gods. The carving itself is the request: words made permanent so that the outcome would be too.

`)} ${vl(i++, `
Etic record. Edition: Faulkner, R. O. (1969). The Ancient Egyptian Pyramid Texts. 2 vols. Oxford: Clarendon Press · earliest attestation c. 2350 BCE (pyramid of Unas, last king of the 5th Dynasty, Saqqara; 283 utterances) · evidence class 2-text · caveat: the inscribed copy is the datum — composition is estimated c. 3000–2700 BCE and the two must not be conflated · backdrop: placeholder vault illustration, not the chamber itself.
source record · claim: oldest large written corpus
`)} ${vl(i++, `next voice → Ugarit, c. 1400–1200 BCE`)}
`; } function voiceRoomBaal(): string { let i = 0; return `
${vl(i++, `
First Voices · Room II · Ugarit (Ras Shamra), Syria
`)} ${vl(i++, `

c. 1400–1200 BCE — the Baal Cycle tablets

`)} ${vl(i++, `
The passage — verbatim lines as carried in the vault source record (trans. Smith & Pitard)
`)}
${vl(i++, `

“Baal is dead!”

`)} ${vl(i++, `

— the mourning cry of El and Anat, KTU 1.5–1.6

`)} ${vl(i++, `

“mightiest Baal lives, the prince, lord of the earth, exists”

`)} ${vl(i++, `

— the proclamation after El's dream-vision, KTU 1.6 iii

`)}
${vl(i++, `

Between the two cries — paraphrase after Smith & Pitard: Baal descends into the gullet of Mot, Death, and dies. El and Anat perform the mourning rites. Anat seizes Mot and splits, winnows, burns, grinds, and sows him like grain. Then El dreams the heavens raining oil and the wadis running with honey — and knows that Baal lives.

`)} ${vl(i++, `

Original script: Ugaritic alphabetic cuneiform. No transliteration is carried in the vault record, so none is shown.

`)} ${vl(i++, `
What these people were asking for — emic · interpretive

That Death not keep him. On that coast, drought is Mot winning — and the mourning of El and Anat, then the shout that the storm-rider lives, ask the same thing every farmer below the citadel asked: that the rains come back, that the dry season not be forever.

`)} ${vl(i++, `
Etic record. Edition: Mark S. Smith, The Ugaritic Baal Cycle, Vol. I (Brill, 1994); Smith & Pitard, Vol. II (Brill, 2009) · tablets c. 1400–1200 BCE, Ugarit — the city's destruction c. 1185 BCE gives a hard terminus · evidence class 2-text · caveat: tablet order is partly reconstructed and key passages around Baal's revival are broken — the mechanism of his return is lost · colophons name the scribe Ilimilku under King Niqmaddu · backdrop: placeholder vault illustration.
source record · motif: the dying-rising god
`)} ${vl(i++, `← back to the gallery`)}
`; } function voiceRoom(slug: string): Response | null { const rooms: Record string]> = { unas: ["First Voices — the pyramid of Unas (c. 2350 BCE)", voiceRoomUnas], baal: ["First Voices — the Baal Cycle (c. 1400–1200 BCE)", voiceRoomBaal], }; const r = rooms[slug]; return r ? page(r[0], "/voices/" + slug, r[1](), "/voices") : null; } // ---------- /map — motif diffusion map, great-flood slice (stylized SVG world, millennium scrubber) ---------- // Honesty contract: only the documented Near Eastern lineage animates as a journey (per great-flood.md // verdict scope). Everything else renders as toggleable ghost markers labeled "unresolved — Q5". const MAPW = 1000, MAPH = 389; const pj = (lon: number, lat: number): [number, number] => [((lon + 180) / 360) * MAPW, ((80 - lat) / 140) * MAPH]; // hand-drawn simplified continents (lon,lat rings) — stylized on purpose; no external files const MAP_LANDS: number[][][] = [ // Americas [[-168,68],[-160,71],[-140,70],[-125,71],[-110,73],[-95,72],[-85,69],[-80,67],[-75,62],[-60,57],[-55,52],[-65,47],[-70,43],[-74,40],[-76,35],[-80,32],[-81,26],[-84,30],[-89,30],[-94,29],[-97,26],[-97,21],[-94,18],[-88,16],[-83,10],[-79,9],[-77,8],[-72,12],[-64,11],[-60,8],[-52,5],[-50,0],[-44,-3],[-35,-8],[-37,-12],[-40,-22],[-48,-28],[-55,-35],[-62,-40],[-65,-47],[-69,-54],[-72,-52],[-74,-45],[-73,-37],[-71,-30],[-70,-20],[-75,-15],[-79,-7],[-81,-3],[-78,2],[-78,7],[-83,8],[-87,13],[-92,15],[-96,16],[-105,20],[-110,24],[-114,30],[-118,34],[-122,38],[-124,42],[-124,48],[-128,52],[-133,56],[-140,60],[-150,60],[-158,57],[-166,60]], // Eurasia (Med, Black Sea, Red Sea, Persian Gulf carved by the coast ring) [[-9,36],[-9,43],[-2,46],[-5,48],[0,50],[4,52],[8,55],[8,57],[11,56],[10,59],[5,61],[12,66],[18,70],[28,71],[40,67],[45,68],[55,69],[68,72],[80,73],[95,77],[105,78],[120,74],[135,72],[150,70],[170,66],[178,65],[170,60],[160,55],[150,47],[140,40],[129,35],[122,38],[118,39],[122,33],[121,28],[114,21],[108,15],[105,9],[101,3],[98,8],[95,16],[91,22],[86,21],[81,15],[77,7],[72,18],[67,24],[61,25],[57,27],[52,30],[48,30],[49,27],[53,25],[56,24.5],[58,23],[59,21],[55,17],[48,13],[44,12],[39,20],[35,28],[34.5,29.5],[34.5,31.5],[35,33],[35.5,34.5],[35.8,35.7],[36.2,36.6],[34,36.8],[31,36.4],[29,36.6],[27,36.9],[26,38],[26.5,39.5],[29,41],[34,42],[41,42],[41.5,44],[36,45.5],[33,45],[31,46],[28,44],[27,42],[26.5,41],[24,40],[22.8,37.2],[21.5,38.4],[19.5,40.3],[16,43],[13.5,45.5],[13.8,44.5],[15.5,42],[16.5,41],[18.5,40.2],[16.8,39.6],[16.2,38],[14.5,40.2],[11.5,42.4],[10,44],[8.5,44.3],[6,43.2],[3,42.4],[0.5,39.5],[-2,36.8],[-5,36]], // Africa [[-6,35],[-2,35],[3,36.5],[10,37],[11,33.5],[15,32.3],[20,32.5],[25,31.8],[31,31.2],[34,28],[36,24],[37.5,18],[40,15],[43,11.5],[48,11],[51.2,11.8],[46,1],[41,-5],[37,-12],[35,-20],[33,-26],[27.5,-33.5],[20,-34.8],[16,-29],[12,-18],[9.5,-7],[6,-1],[8.5,4.3],[3,6.3],[-4,5.2],[-8,4.3],[-13,9],[-17,14.7],[-17,21],[-15,27],[-10,31]], // Australia [[114,-22],[122,-17],[130,-12],[136,-12],[139,-17],[142,-11],[146,-19],[153,-27],[150,-37],[144,-38],[138,-35],[132,-32],[124,-33],[115,-34],[113,-26]], // Greenland [[-45,60],[-53,65],[-58,72],[-50,78],[-38,79],[-25,71],[-35,66]], // British Isles [[-5,50],[-6,54],[-4,58],[-2,57],[1,52]], // Japan [[130,32],[136,35.5],[140,40],[142,44],[139,41],[134,34.5]], // New Guinea [[131,-1.5],[141,-2.5],[148,-7],[143,-9],[135,-4]], // New Zealand [[172,-34.5],[178,-37.5],[173,-42],[167,-46.5],[171,-41]], // Madagascar [[44,-12],[50,-16],[47,-25],[44,-20]], // Sumatra · Borneo (stylized blobs) [[95.5,5],[103,-2],[106,-6],[98,2]], [[109,1],[114,4.5],[117,1],[113,-3.5],[109,-1.5]], ]; const MAP_LAND_D = MAP_LANDS.map(p => "M" + p.map(([o, a]) => pj(o, a).map(v => v.toFixed(1)).join(" ")).join("L") + "Z").join(""); // detail viewport (the Near East corridor) in projected coordinates const DET = (() => { const [x0, y0] = pj(18, 41.5), [x1, y1] = pj(50, 26.5); return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 }; })(); type MapNode = { id: string; label: string; ylab: string; year: number; lon: number; lat: number; mech: "descent" | "contact"; note: string; title: string; dx: number; dy: number; anchor: "start" | "end" | "middle"; }; const MAP_NODES: MapNode[] = [ { id: "nippur", label: "Nippur", ylab: "c. 1900–1800 BCE", year: -1900, lon: 45.18, lat: 32.13, mech: "descent", note: "/09_comparative/1_sources/eridu-genesis.md", dx: 2, dy: 3.4, anchor: "start", title: "Descent root — ‘the Flood’ as epoch divider in Old Babylonian Sumerian King List recensions (c. 1900–1800 BCE, 2-text); Eridu Genesis narrative (Ziusudra), tablet CBS 10673, c. 1600 BCE. Click → vault note." }, { id: "babylon", label: "Babylon · Sippar", ylab: "c. 1635 BCE", year: -1635, lon: 44.42, lat: 32.54, mech: "descent", note: "/09_comparative/1_sources/atrahasis-epic.md", dx: 1.8, dy: -2, anchor: "start", title: "Old Babylonian Atra-ḫasīs tablets, colophon-dated to the reign of Ammi-ṣaduqa, c. 1635 BCE (2-text). Click → vault note." }, { id: "megiddo", label: "Megiddo", ylab: "14th c. BCE", year: -1350, lon: 35.18, lat: 32.58, mech: "contact", note: "/09_comparative/1_sources/epic-of-gilgamesh-tablet-xi.md", dx: -2, dy: 0.5, anchor: "end", title: "Contact — Gilgamesh fragment found at Megiddo, 14th c. BCE: cuneiform scribal training in Canaan (2-text). Click → vault note." }, { id: "ugarit", label: "Ugarit", ylab: "13th c. BCE", year: -1250, lon: 35.78, lat: 35.6, mech: "contact", note: "/09_comparative/1_sources/atrahasis-epic.md", dx: -2, dy: -1.4, anchor: "end", title: "Contact — Atra-ḫasīs fragment RS 22.421 at Ugarit, 13th c. BCE: the Babylonian text physically circulating on the Levantine coast (2-text). Click → vault note." }, { id: "nineveh", label: "Nineveh", ylab: "c. 1200–1100 BCE", year: -1200, lon: 43.15, lat: 36.36, mech: "descent", note: "/09_comparative/1_sources/epic-of-gilgamesh-tablet-xi.md", dx: 2, dy: 0.5, anchor: "start", title: "Within-tradition descent — Standard Babylonian Gilgamesh XI redaction (hero Ūta-napišti) c. 1200–1100 BCE; extant copies 7th c. BCE, Nineveh libraries. Gilgamesh XI demonstrably incorporates Atra-ḫasīs. Click → vault note." }, { id: "jerusalem", label: "Jerusalem", ylab: "7th–5th c. BCE", year: -600, lon: 35.23, lat: 31.78, mech: "contact", note: "/09_comparative/1_sources/genesis-flood-narrative.md", dx: -2, dy: 3, anchor: "end", title: "Contact — Genesis 6–9 (Noah), composition c. 7th–5th c. BCE; Judah’s literate elite in Babylonia 597–539 BCE. Borrowing timing open: Late Bronze vs exilic (Q4). Click → vault note." }, { id: "greece", label: "Greece", ylab: "466 BCE", year: -466, lon: 22.5, lat: 38.48, mech: "contact", note: "/09_comparative/2_notes/great-flood.md", dx: 1.8, dy: -1.6, anchor: "start", title: "Contact, medium confidence — Deucalion: securely Pindar, Olympian 9.42–53 (466 BCE); plausibly the same contact radiating west via Levantine–Anatolian channels. Click → vault note." }, ]; const mnodeById: Record = {}; for (const n of MAP_NODES) mnodeById[n.id] = n; type MapEdge = { a: string; b: string; year: number; mech: "descent" | "contact"; k: number; dashed?: boolean; title: string }; const MAP_EDGES: MapEdge[] = [ { a: "nippur", b: "babylon", year: -1635, mech: "descent", k: 2, title: "Descent within Mesopotamia — one continuous scribal tradition: the Ziusudra → Ūta-napišti onomastic chain." }, { a: "babylon", b: "nineveh", year: -1200, mech: "descent", k: 3, title: "Descent within Mesopotamia — SB Gilgamesh XI incorporates Atra-ḫasīs (keeps the hero’s name in two lines)." }, { a: "babylon", b: "megiddo", year: -1350, mech: "contact", k: 7, title: "Contact corridor — Gilgamesh fragment at Megiddo, 14th c. BCE." }, { a: "babylon", b: "ugarit", year: -1250, mech: "contact", k: 6, title: "Contact corridor — Atra-ḫasīs fragment RS 22.421 sails up the Levantine coast to Ugarit, 13th c. BCE." }, { a: "babylon", b: "jerusalem", year: -600, mech: "contact", k: 13, title: "Contact — exilic channel, 597–539 BCE. Timing open (Q4): Late Bronze inheritance is the live alternative." }, { a: "megiddo", b: "jerusalem", year: -600, mech: "contact", k: 1.5, dashed: true, title: "Q4 alternative route — Late Bronze Levantine circulation instead of (or before) the exilic borrowing. Both render because the question is open." }, { a: "ugarit", b: "greece", year: -466, mech: "contact", k: 8, dashed: true, title: "Contact, medium confidence — the same story radiating west to Greek Deucalion via Levantine–Anatolian channels." }, ]; type MapGhost = { label: string; ylab: string; lon: number; lat: number; note: string; anchor?: "start" | "end" }; const MAP_GHOSTS: MapGhost[] = [ { label: "Gun-Yu flood epic", ylab: "Suigongxu bronze c. 900 BCE", lon: 112, lat: 34, note: "/09_comparative/2_notes/great-flood.md" }, { label: "Manu and the fish", ylab: "Śatapatha Brāhmaṇa c. 700–600 BCE", lon: 78, lat: 27, note: "/09_comparative/2_notes/great-flood.md" }, { label: "North America", ylab: "recorded 16th c. CE+ (post-contact)", lon: -95, lat: 38, note: "/09_comparative/1_sources/berezkin-motif-database.md" }, { label: "Mesoamerica", ylab: "recorded 16th c. CE+ (post-contact)", lon: -99, lat: 19.4, note: "/09_comparative/1_sources/berezkin-motif-database.md" }, { label: "Andes", ylab: "recorded 16th c. CE+ (post-contact)", lon: -72.5, lat: -13.5, note: "/09_comparative/1_sources/berezkin-motif-database.md" }, { label: "Oceania · Australia", ylab: "recorded 16th c. CE+ (post-contact)", lon: 158, lat: -14, note: "/09_comparative/1_sources/berezkin-motif-database.md", anchor: "end" }, ]; function mapQPath(a: MapNode, b: MapNode, k: number): string { const [x1, y1] = pj(a.lon, a.lat), [x2, y2] = pj(b.lon, b.lat); const mx0 = (x1 + x2) / 2, my0 = (y1 + y2) / 2; const dx = x2 - x1, dy = y2 - y1, L = Math.hypot(dx, dy) || 1; const ux = -dy / L, uy = dx / L; return `M ${x1.toFixed(1)} ${y1.toFixed(1)} Q ${(mx0 + ux * k).toFixed(1)} ${(my0 + uy * k).toFixed(1)} ${x2.toFixed(1)} ${y2.toFixed(1)}`; } // edge + node markup; `det` adds labels/date chips (the world view keeps glyphs tiny) function mapGlyphs(det: boolean, pre: string): string { const parts: string[] = []; for (const e of MAP_EDGES) { const cls = e.dashed ? "dashflow" : "flow"; parts.push(`${esc(e.title)}`); } for (const n of MAP_NODES) { const [x, y] = pj(n.lon, n.lat); const r = det ? 1.2 : 3; const lab = det ? `${esc(n.label)} ${esc(n.ylab)}` : ""; parts.push(`${esc(n.label)} — ${esc(n.title)}${lab}`); } return parts.join("\n"); } function mapDefs(pre: string): string { return ` `; } function mapWorldSvg(): string { const [cx, cy] = pj(50.5, 41.5); // Caspian patch const ghosts = MAP_GHOSTS.map(g => { const [x, y] = pj(g.lon, g.lat); const an = g.anchor ?? "start"; const tx = an === "end" ? x - 9 : x + 9; return `unresolved — Q5. ${esc(g.label)} — ${esc(g.ylab)}. Independent occurrence: cannot derive from Mesopotamia by any documented route. Click → vault note. ${esc(g.label)} ${esc(g.ylab)} · unresolved — Q5`; }).join("\n"); return ` ${mapDefs("w")} the documented corridor — detail below ↓ ${mapGlyphs(false, "w")} ${ghosts} `; } function mapDetailSvg(): string { return ` ${mapDefs("d")} ${mapGlyphs(true, "d")} `; } const MAP_CSS = ` .mapwrap{border:1px solid var(--line);border-radius:18px;overflow:hidden;background:#0a0d14;margin:.9rem 0} .mapwrap svg{display:block;width:100%;height:auto} .land{fill:#1d2433;stroke:#2d3650;stroke-width:.6} #md .land{stroke-width:.15} .seapatch{fill:#0a0d14} .dbox{fill:none;stroke:#8a93a5;stroke-width:.8;stroke-dasharray:4 4;opacity:.45} .dboxlab{font:600 8.5px ui-sans-serif,system-ui,sans-serif;fill:#8a93a5;opacity:.8} .flow{fill:none;stroke-width:1.6} #md .flow{stroke-width:.5} .f-contact{stroke:#7aa2f7}.f-descent{stroke:#9ece6a} .dashflow{fill:none;stroke-dasharray:3 3;stroke-width:1.4;opacity:0;transition:opacity 1.1s} #md .dashflow{stroke-width:.42;stroke-dasharray:1 1} .dashflow.on{opacity:.7} path.flow{transition:stroke-dashoffset 1.4s ease} .mn{opacity:0;transition:opacity .9s} .mn.on{opacity:1} .mn circle{stroke:#0c0e13;stroke-width:.4} .m-descent circle{fill:#9ece6a}.m-contact circle{fill:#7aa2f7} #md text.nl{font:700 1.9px ui-sans-serif,system-ui,sans-serif;fill:#e8ecf3} #md text.nd{font:700 1.35px ui-sans-serif,system-ui,sans-serif;fill:var(--amber);font-variant-numeric:tabular-nums} .ghost{opacity:0;transition:opacity .8s;pointer-events:none} #mapbox.ghosts .ghost{opacity:.8;pointer-events:auto} .ghost .go{fill:none;stroke:#bb9af7;stroke-width:1;stroke-dasharray:2.5 2.5} .ghost .gi{fill:#bb9af7} .ghost text{font:700 8.5px ui-sans-serif,system-ui,sans-serif;fill:#bb9af7} .ghost .gq{font-weight:500;font-size:7.5px;fill:#8a93a5} .honesty{border:1px solid #7aa2f755;background:#7aa2f712;border-radius:14px;padding:.75rem 1.2rem;color:#c8d2e8;font-size:.93rem;margin:.9rem 0;line-height:1.6} .mctl{display:flex;gap:.9rem;align-items:center;flex-wrap:wrap;margin:.7rem 0;background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:.65rem 1rem} #my{flex:1;min-width:13rem;accent-color:var(--blue);cursor:ew-resize} #myy{font-weight:800;color:var(--amber);min-width:5.8rem;text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums} .mghlab{display:flex;gap:.45rem;align-items:center;font-size:.84rem;color:var(--dim);cursor:pointer;user-select:none} .mhint{font-size:.84rem;color:var(--dim)} .mlog{list-style:none;padding:0;margin:1rem 0 0} .mlog li{opacity:.25;transition:opacity .5s;padding:.38rem 0;border-top:1px dashed var(--line);font-size:.92rem;color:#c4cad6} .mlog li.on{opacity:1} .lyr{display:inline-block;min-width:9.5rem;color:var(--amber);font-weight:700;font-variant-numeric:tabular-nums;font-size:.84rem} .mlog .badge{margin-right:.4rem} `; const MAP_JS = ` (()=>{ const sl=document.getElementById('my'),out=document.getElementById('myy'),gh=document.getElementById('gh'),box=document.getElementById('mapbox'); if(!sl)return; const flows=document.querySelectorAll('path.flow'); for(let i=0;i `
  • ${esc(y <= 0 ? (1 - y) + " BCE" : y + " CE")}${m}${esc(w)} — ${esc(t)} note →
  • `).join("\n"); const body = `

    Motif Diffusion Map — The Great Flood

    One story, five millennia, rendered from the vault's dated occurrence table. Drag time forward. Routes are colored by transmission verdict: contact · descent · convergence / unresolved. Every glyph clicks through to the vault note that justifies it.

    ⚖ This map shows the documented Near Eastern lineage (high confidence). The global distribution is a separate, open question (Q5).
    3000 BCE 1000 CE 3001 BCE
    ${mapWorldSvg()}

    The corridor, up close

    Babylonia → Ugarit (RS 22.421) → Megiddo → exilic Jerusalem → Greece. Dashed = medium confidence or open alternative (Q4).

    ${mapDetailSvg()}
    descent (within-tradition lineage) contact (attested corridor) convergence / unresolved (ghost layer) dashed = medium confidence or open question

    The journey, as a log

      ${logHtml}

    Data: motif: the great flood (occurrence table + transmission analysis). Coordinates are an editorial geo layer over the vault's dated attestations — the notes stay the single source of truth for every claim. Proposal: motif-diffusion-map.

    `; return page("Motif Diffusion Map — origins//research", "/map", body, "/map"); } // ---------- /scenes — stand-there scenes (Eridu room first) ---------- // Honesty contract: every narration sentence carries data-grade from how the licensing notes ground it: // attested = physical/textual evidence exists · inferred = reasonable reconstruction · imagined = atmosphere. // The licensing notes (and only these) authorize the Eridu narration: const SCENE_NOTES: Record = { SEQ: { href: "/02_mesopotamian/1_sources/eridu-temple-sequence-archaeology.md", label: "Eridu Temple Sequence — Safar/Lloyd/Mustafa 1981 record (tier 1)" }, CLAIM: { href: "/02_mesopotamian/2_notes/eridu-temple-level-xviii-earliest-cult-building.md", label: "Claim: Level XVIII earliest cult building in Mesopotamia (tier 2)" }, TRAD: { href: "/02_mesopotamian/2_notes/sumerian-religion.md", label: "Tradition: Sumerian religion (tier 2)" }, }; type SceneLine = { g: "attested" | "inferred" | "imagined"; t: string; lic: string[]; why: string }; const ERIDU_LINES: SceneLine[] = [ { g: "attested", t: "You are standing at the edge of a freshwater marsh in southern Mesopotamia, around 5500 BCE.", lic: ["SEQ", "CLAIM"], why: "Eridu founded c. 5500 BCE (Ubaid I); the site sits at the edge of what was then a marshy estuary." }, { g: "imagined", t: "It is dusk. The heat is going out of the day, and the reed beds have gone quiet.", lic: [], why: "Atmosphere — no source licenses the hour, the temperature, or the quiet." }, { g: "attested", t: "Under your feet is a dune of clean sand. No one has ever lived here before you.", lic: ["SEQ"], why: "“Eridu was founded on a virgin sand dune — no earlier habitation layer.”" }, { g: "attested", t: "Ahead of you stands a single small room of mud brick — about twelve feet by fifteen. You could cross it in five steps.", lic: ["SEQ"], why: "Bertman (p. 108, citing the excavation reports): ~12 × 15 ft, mud brick." }, { g: "attested", t: "Inside, against the wall: a low podium of mud brick. An altar.", lic: ["SEQ"], why: "“A simple podium or altar for sacrifices.”" }, { g: "attested", t: "Behind it, a niche recessed into the wall.", lic: ["SEQ"], why: "The niche is excavated architecture." }, { g: "inferred", t: "The niche was made to hold an image — a statue of whoever this room belongs to.", lic: ["SEQ", "CLAIM"], why: "“Meant to hold a statue of the god” is Bertman’s reading; the cult-image identification is interpretive (the source’s own reliability notes say so)." }, { g: "attested", t: "On and around the altar there are fish bones, and ash.", lic: ["SEQ", "CLAIM"], why: "Fish bones and ashes scattered around the altar in multiple early levels — excavated deposits." }, { g: "inferred", t: "Someone has burned offerings of fish here — and not once: again and again, floor above floor.", lic: ["CLAIM", "SEQ"], why: "“Material evidence of repeated ritual behaviour” — the vault’s inference from deposits in multiple levels; ‘offering’ is already interpretation." }, { g: "inferred", t: "The fish came out of this marsh — the sweet water at the settlement’s edge.", lic: ["CLAIM"], why: "Fish offerings are ‘coherent with’ the estuary setting — coherence, not demonstration." }, { g: "imagined", t: "Smoke is rising off the altar now, thin and steady, drifting through the doorway past you toward the first stars.", lic: [], why: "Atmosphere — the ash is real; tonight’s smoke, doorway draft, and stars are not in any source." }, { g: "attested", t: "Nobody here can write. The first written words are still some two thousand years away.", lic: ["TRAD", "CLAIM"], why: "Pre-literate Ubaid; earliest proto-cuneiform tablets c. 3200–3000 BCE." }, { g: "attested", t: "So you cannot ask them what they call this building, or who the niche is for. Neither can we.", lic: ["CLAIM"], why: "The claim’s own counter-evidence: we cannot know what they called the building or what deity, if any, the offerings addressed." }, { g: "attested", t: "Much later, the people of this land will say a god lived here — Enki, lord of the sweet water under the earth — and that this was the first city, founded by the gods before the flood.", lic: ["TRAD", "CLAIM"], why: "The later Sumerian tradition (emic) is textually attested; its truth about THIS room is not. The narration must not name Enki as this room’s god — sumerian-religion flags that as back-projection." }, { g: "inferred", t: "Maybe they remembered this place. Maybe they invented a memory.", lic: ["SEQ", "CLAIM"], why: "“Plausible long-term cultural memory” vs back-projection — the etic hedge, verbatim from the notes." }, { g: "attested", t: "What is certain is the ground itself: people will rebuild on this exact spot — room over room, temple over temple, eighteen times, for the next two thousand years.", lic: ["SEQ", "CLAIM"], why: "Eighteen superimposed temple levels, unbroken c. 5500 → 3500 BCE." }, { g: "inferred", t: "You are standing in the earliest building in Mesopotamia that archaeology is willing to call a temple.", lic: ["CLAIM"], why: "The claim itself — confidence medium, because ‘temple’ is an interpretive label resting on the podium + offering debris." }, { g: "imagined", t: "Behind you the marsh goes dark. Smoke, fish, river mud, stars. Whatever has begun here is not going to stop.", lic: [], why: "Closing atmosphere — mood, not evidence." }, ]; const SCENE_CSS = ` .scn{position:relative;border-radius:22px;overflow:hidden;border:1px solid var(--line);margin:1rem 0;background:#000} .scimgwrap{position:absolute;inset:-5%;will-change:transform} .scimg{width:100%;height:100%;object-fit:cover;animation:kb 75s ease-in-out infinite alternate;transition:filter 1.2s} @keyframes kb{from{transform:scale(1.06)}to{transform:scale(1.18) translate(-2%,1.5%)}} @media(prefers-reduced-motion:reduce){.scimg{animation:none}} .scshade{position:absolute;inset:0;background:linear-gradient(180deg,#00000066,#000000b0 50%,#000000ec 85%)} .scui{position:relative;max-width:48rem;margin:0 auto;min-height:86vh;display:flex;flex-direction:column;justify-content:space-between;padding:2.2rem 2rem;z-index:1} @media(max-width:640px){.scui{padding:1.4rem 1rem}} .schead{display:flex;justify-content:space-between;gap:1rem;align-items:flex-start;flex-wrap:wrap} .scn h1{font-size:1.9rem;margin:.3rem 0 .5rem;color:#fff} .scpill{display:inline-block;font-size:.74rem;font-weight:800;letter-spacing:.06em;padding:.25em .8em;border-radius:999px;background:#000000aa;border:1px solid #ffffff33;color:#e8ecf3;margin-right:.4rem} .sctools{display:flex;gap:.6rem;align-items:center;flex-wrap:wrap} .scswitch{display:flex;gap:.5rem;align-items:center;font-size:.84rem;font-weight:700;color:#e8ecf3;background:#000000aa;border:1px solid #ffffff33;border-radius:999px;padding:.4rem .9rem;cursor:pointer;user-select:none} #srcbtn{font:700 .84rem ui-sans-serif,system-ui,sans-serif;color:#e8ecf3;background:#000000aa;border:1px solid #ffffff33;border-radius:999px;padding:.4rem .9rem;cursor:pointer} #srcbtn:hover,.scswitch:hover{border-color:#ffffff77} .scbody{margin-top:2.2rem} .sline{font-size:1.18rem;line-height:1.85;color:#e9edf4;margin:.5rem 0;opacity:0;transform:translateY(10px);transition:opacity 1s ease,transform 1s ease,color .8s} .sline.in{opacity:1;transform:none} .schfoot{display:none;margin-top:1.6rem;font-size:.9rem;color:var(--amber);border-top:1px solid #ffffff2a;padding-top:.9rem;font-style:italic} .scn.honest .scimg{filter:saturate(.15) brightness(.6) contrast(1.05)} .scn.honest .sline::before{content:attr(data-grade);display:inline-block;font-size:.6rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase;margin-right:.65rem;vertical-align:.15em;opacity:.9} .scn.honest .sline[data-grade=attested]{color:#fff} .scn.honest .sline[data-grade=attested]::before{color:var(--green)} .scn.honest .sline.in[data-grade=inferred]{opacity:.72;color:var(--amber)} .scn.honest .sline[data-grade=inferred]::before{color:var(--amber)} .scn.honest .sline.in[data-grade=imagined]{opacity:.13} .scn.honest .schfoot{display:block} .scsrc{position:absolute;top:0;right:0;bottom:0;width:min(28rem,94%);background:#0c0e13f5;border-left:1px solid var(--line);transform:translateX(103%);transition:transform .5s ease;overflow-y:auto;padding:1.3rem 1.4rem;z-index:4} .scn.showsrc .scsrc{transform:none} .scsrc h2{margin-top:.4rem;font-size:1.05rem;border:0} .scsrc .srow{border-top:1px dashed var(--line);padding:.55rem 0;font-size:.84rem;color:#c4cad6} .scsrc .srow b{color:var(--fg)} .scsrc .srow .g-attested{color:var(--green)}.scsrc .srow .g-inferred{color:var(--amber)}.scsrc .srow .g-imagined{color:var(--dim)} .scsrc .why{color:var(--dim);margin:.15rem 0} .scsrc .lics a{display:block;color:#b6bdcc;font-size:.8rem} #srcclose{float:right;background:none;border:1px solid var(--line);color:var(--dim);border-radius:8px;padding:.2rem .6rem;cursor:pointer;font-size:.8rem} .schall{display:grid;grid-template-columns:repeat(auto-fill,minmax(17rem,1fr));gap:1.2rem;margin:1.6rem 0} .sccard{position:relative;display:block;border-radius:20px;overflow:hidden;border:1px solid var(--line);min-height:20rem;background-size:cover;background-position:center;transition:transform .2s,border-color .2s;color:var(--fg)} a.sccard:hover{transform:translateY(-3px);border-color:#e0af6866;color:var(--fg)} .sccard .scin{position:absolute;inset:0;background:linear-gradient(180deg,#0003 30%,#000d 85%);display:flex;flex-direction:column;justify-content:flex-end;padding:1.4rem} .sccard .scd{font-size:1.5rem;font-weight:800;letter-spacing:-.02em;color:#e0af68;font-variant-numeric:tabular-nums} .sccard .scp{color:#c4cad6;font-size:.9rem} .sccard .scgo{margin-top:.5rem;font-size:.82rem;font-weight:700} .sccard.soon{opacity:.45;filter:saturate(.4)} .sccard.soon .scgo{color:var(--dim)} `; const SCENE_JS = ` (()=>{ const scn=document.getElementById('scn');if(!scn)return; const reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches; const lines=scn.querySelectorAll('.sline'); for(let i=0;i.2||Math.abs(ty-cy)>.2)requestAnimationFrame(tick);else raf=false; } scn.addEventListener('pointermove',function(e){ const r=scn.getBoundingClientRect(); tx=((e.clientX-r.left)/r.width-.5)*-14; ty=((e.clientY-r.top)/r.height-.5)*-9; if(!raf){raf=true;requestAnimationFrame(tick)} }); } })();`; function scenesHall(): Response { const cards = `
    c. 5500 BCE
    the Level XVIII temple platform, Eridu — the marsh, the altar, the fish bones
    stand there →
    ~92,000 BP
    a burial at Qafzeh Cave, Levant — ochre and shell
    in preparation — narration must be licensed line-by-line first
    c. 9600 BCE
    dawn at Enclosure D, Göbekli Tepe
    in preparation — narration must be licensed line-by-line first
    c. 1250 BCE
    Wu Ding’s divination chamber, Anyang
    in preparation — narration must be licensed line-by-line first
    `; const body = `
    Stand-There Scenes

    Be moved to the evidence’s own moment

    Second person, present tense — and every sentence licensed by a vault note. The signature move is the “what we actually know” toggle: flip it and the imagined sentences fade to ghosts, the inferred ones go amber, and only what is in the ground or in a text stays bright. One scene is open; the rest open as their narrations pass line-by-line licensing.

    ${cards}

    Backdrop: the vault’s Mesopotamia domain illustration, standing in until a scene-specific render is generated (image pipeline per AGENTS.md §5). Proposal: stand-there-scenes.

    `; return page("Stand-There Scenes — origins//research", "/scenes", body, "/scenes"); } function sceneEridu(): Response { const lines = ERIDU_LINES.map((l, i) => `

    ${esc(l.t)}

    `).join("\n"); const rows = ERIDU_LINES.map((l, i) => { const lics = l.lic.length ? l.lic.map(k => `→ ${esc(SCENE_NOTES[k].label)}`).join("") : `unlicensed — atmosphere only`; return `
    L${i + 1} · ${l.g}
    “${esc(l.t)}”
    ${esc(l.why)}
    ${lics}
    `; }).join("\n"); const noteList = Object.values(SCENE_NOTES).map(n => `→ ${esc(n.label)}`).join(""); const body = `
    Dusk over a Mesopotamian marsh — the vault's Mesopotamia illustration standing in for the Eridu shrine
    Stand-There Scenes · Room I · Eridu

    The Level XVIII temple platform

    c. 5500 BCE · Ubaid ITell Abu Shahrain, southern Iraqevidence: 1-archaeology
    ${lines}
    Everything still bright is in the ground or in a text. Everything faded is us.
    scene · stand-thereconfidence · medium1-archaeology
    counter-evidenceThe “temple” identification rests on the altar/podium and offering debris — an interpretive inference, not an unambiguous architectural signature. Pre-literate Ubaid: no text labels the building or its deity, and the Enki identification is later back-projection. Göbekli Tepe’s ritual architecture is millennia older (different cultural sphere). Full steelman: the claim note.
    backdropVault domain illustration standing in — not a render of the Level XVIII shrine itself. The honesty toggle desaturates it to underline that the image, too, is reconstruction.

    ← all scenes · next rooms (Qafzeh, Göbekli Tepe, Anyang) open once their narrations pass line-by-line licensing.

    `; return page("Stand there — Eridu, c. 5500 BCE — origins//research", "/scenes/eridu", body, "/scenes"); } // ---------- /observatory — evidence observatory (epistemic mission control) ---------- // Server-side aggregation over ALL vault notes' frontmatter; every glyph is an to its note. // Parsing honors the three verified gotchas (00_meta/ideas/evidence-observatory.md): // block-style YAML lists (parseFrontmatter above), inline comments (existing strip), prose dates (earliestYear). type ObsNote = { p: string; t: string; type: string; d: string; conf: string; ev: string; trans: string; status: string; y: number | null; ds: string; occ: number; counterBad: boolean; urlv: string; }; let obsCache = { v: "", notes: [] as ObsNote[] }; function obsScan(): ObsNote[] { const v = vaultVersion(); if (obsCache.v === v && obsCache.notes.length) return obsCache.notes; const notes: ObsNote[] = []; const walk = (dir: string, r: string) => { for (const e of readdirSync(dir).sort()) { if (e.startsWith(".")) continue; const a = join(dir, e); if (statSync(a).isDirectory()) { walk(a, r + "/" + e); continue; } if (!e.endsWith(".md")) continue; let fm: Record; try { fm = parseFrontmatter(readFileSync(a, "utf8")).fm; } catch { continue; } if (!fm.type) continue; const ds = dateStrOf(fm); const ce = (fm.counter_evidence ?? "").trim(); notes.push({ p: r + "/" + e, t: fm.title || e.replace(/\.md$/, "").replace(/-/g, " "), type: fm.type, d: r.split("/")[1] ?? "", conf: fm.confidence || "", ev: evClass(fm, ds), trans: fm.transmission || "", status: fm.status || "", y: earliestYear(ds), ds: ds.length > 200 ? ds.slice(0, 197) + "…" : ds, occ: fm.occurrences ? fm.occurrences.replace(/[\[\]]/g, "").split(",").map(s => s.trim()).filter(Boolean).length : 0, counterBad: ["claim", "motif", "synthesis", "tradition"].includes(fm.type) && (!ce || /^none found/i.test(ce)), urlv: fm.url_verified || "", }); } }; for (const [d] of DOMAINS) { try { walk(join(ROOT, d), "/" + d); } catch {} } obsCache = { v, notes }; return notes; } type ObsQ = { id: string; q: string; targets: string[]; open: boolean }; function obsQuestions(): ObsQ[] { const out: ObsQ[] = []; let src = ""; try { src = readFileSync(join(ROOT, "00_meta/OPEN_QUESTIONS.md"), "utf8"); } catch { return out; } for (const line of src.split("\n")) { const m = line.match(/^\| (Q\d+) \|/); if (!m) continue; // Q-backlog row deliberately excluded — shown as parked footnote const cells = line.split("|").map(c => c.trim()); const targets = (cells[3] ?? "").split("·").map(s => s.trim()).filter(s => /^0\d_/.test(s)); out.push({ id: m[1], q: (cells[2] ?? "").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").slice(0, 180), targets, open: !/answered/i.test(cells[4] ?? "") }); } return out; } function backlogCount(): number { try { return (readFileSync(join(ROOT, "00_meta/QUESTION_BACKLOG.md"), "utf8").match(/^\| B\d+ \|/gm) || []).length; } catch { return 0; } } function observatoryJson(): string { const notes = obsScan(); return JSON.stringify({ claims: notes.filter(n => n.type === "claim"), motifs: notes.filter(n => n.type === "motif"), traditions: notes.filter(n => n.type === "tradition"), sources: notes.filter(n => n.type === "source"), questions: obsQuestions(), backlog_parked: backlogCount(), generated: vaultVersion(), }); } const OBS_TCOLOR: Record = { descent: "#9ece6a", contact: "#7aa2f7", convergence: "#bb9af7", unresolved: "#8a93a5" }; const OBS_CONF_OP: Record = { high: 1, medium: 0.8, low: 0.55, speculative: 0.4 }; const OBS_EVC: Record = { ...EV_COLOR, "5-cognitive": "#f7768e", "": "#5a6378" }; const hash32 = (s: string) => { let x = 9; for (let i = 0; i < s.length; i++) x = Math.imul(x ^ s.charCodeAt(i), 0x9e3779b1) >>> 0; return x; }; // (b) motif constellation — motifs as glowing verdict-colored stars on a deep-time axis; // tradition profiles as faint hollow planets; undated/speculative items live in the horizon zone. function obsConstellation(notes: ObsNote[]): string { const W = 1000, H = 350, HX = 152, AX0 = HX + 28, AX1 = W - 24, axisY = H - 34; const S = 60, ua = (y: number) => Math.asinh((NOWY - y) / S), UMAX = ua(YMIN); const xOf = (y: number) => AX0 + (1 - ua(y) / UMAX) * (AX1 - AX0); const parts: string[] = [ ``, ``, ``, `BEYOND THE EVIDENCE`, `HORIZON`, `undated · reconstruction-only`, ]; const ticks: [number, string][] = [[YMIN, "100,000 BP"], [-8050, "10,000 BP"], [-2999, "3000 BCE"], [-999, "1000 BCE"], [1, "1 CE"], [NOWY, "today"]]; for (const [y, lab] of ticks) { const x = xOf(y); parts.push(`${lab}`); } const stars = notes.filter(n => n.type === "motif"); // sort traditions by date so x-adjacent glyphs land in different label bands const trads = notes.filter(n => n.type === "tradition").sort((a, b) => (a.y ?? -Infinity) - (b.y ?? -Infinity)); let hzY = 70; const glyph = (n: ObsNote, star: boolean, i: number) => { const undated = n.y === null; const cx = undated ? HX / 2 : xOf(n.y!); const cy = undated ? (hzY += 52) - 52 : star ? 84 + (i % 3) * 56 : 226 + (i % 3) * 30; const r = star ? Math.min(19, 6 + n.occ) : 4.5; const col = star ? (OBS_TCOLOR[n.trans] ?? "#8a93a5") : "#8a93a5"; const op = (OBS_CONF_OP[n.conf] ?? 0.7) * (undated ? 0.75 : 1); const shape = star ? `` : ``; let lab = n.t.replace(/^(Motif|Tradition):\s*/i, "").replace(/\s*\(.*$/, ""); if (!star) lab = lab.replace(/\s+religion.*$/i, ""); // "Sumerian Religion" → "Sumerian" (de-clutter) return `${shape} ${esc(lab.length > 26 ? lab.slice(0, 24) + "…" : lab)} ${esc(n.t)} — ${star ? `verdict: ${n.trans || "—"} · ` : ""}confidence: ${n.conf || "—"} · ${esc(n.ds || "undated — beyond the evidence horizon")}`; }; stars.forEach((n, i) => parts.push(glyph(n, true, i))); trads.forEach((n, i) => parts.push(glyph(n, false, i))); return `${parts.join("\n")}`; } // (a) claim field — confidence × evidence-class density grid function obsClaimField(notes: ObsNote[]): string { const COLS = ["1-archaeology", "2-text", "3-reconstruction", "4-ethnography", "5-cognitive", ""]; const COLAB = ["1 · archaeology", "2 · text", "3 · reconstr.", "4 · ethnogr.", "5 · cognitive", "unclassed"]; const ROWS = ["high", "medium", "low", "speculative"]; const LX = 86, TY = 26, CW = 88, CH = 64; const W = LX + COLS.length * CW + 8, H = TY + ROWS.length * CH + 8; const parts: string[] = []; for (let ri = 0; ri < ROWS.length; ri++) parts.push(`${ROWS[ri]}`); for (let ci = 0; ci < COLS.length; ci++) parts.push(`${COLAB[ci]}`); for (let ri = 0; ri < ROWS.length; ri++) for (let ci = 0; ci < COLS.length; ci++) { const healthy = ri <= 1 && ci <= 1, alarm = ri === 0 && ci >= 2; parts.push(``); } let unrated = 0; for (const n of notes.filter(n => n.type === "claim" || n.type === "tradition")) { const ri = ROWS.indexOf(n.conf), ci = COLS.indexOf(n.ev); if (ri < 0) { unrated++; continue; } const c = ci < 0 ? COLS.length - 1 : ci; const jx = (hash32(n.p) % 1000) / 1000, jy = (hash32(n.p + "y") % 1000) / 1000; const x = LX + c * CW + 11 + jx * (CW - 22), y = TY + ri * CH + 11 + jy * (CH - 22); const col = OBS_EVC[n.ev] ?? "#5a6378"; const alarm = ri === 0 && c >= 2; const dot = n.type === "tradition" ? `` : ``; parts.push(`${dot}${alarm ? `` : ""} ${esc(n.t)} — ${n.type} · ${n.conf} × ${n.ev || "unclassed"} · ${n.d}${alarm ? " ⚠ high confidence on class-3+ evidence" : ""}`); } return `${parts.join("\n")}` + (unrated ? `

    ${unrated} typed note(s) carry no confidence rating and are not plotted.

    ` : ""); } // (c) per-domain source-diversity bars (HTML) + counter-evidence health ticks function obsSpectra(notes: ObsNote[]): string { const rows = DOMAINS.map(([d, name]) => { const srcs = notes.filter(n => n.type === "source" && n.d === d); const classes = ["1-archaeology", "2-text", "3-reconstruction", "4-ethnography", "5-cognitive", ""]; const segs = classes.map(c => [c, srcs.filter(s => s.ev === c).length] as [string, number]).filter(([, n]) => n > 0); const bar = segs.map(([c, n]) => ``).join(""); const online = srcs.filter(s => s.urlv && s.urlv !== "not-online"); const okUrl = online.filter(s => s.urlv === "yes").length; const chip = srcs.length ? `${okUrl}/${online.length} URLs verified · ${srcs.length - online.length} offline` : ""; const flagged = notes.filter(n => n.d === d && n.counterBad); const ticks = flagged.map(f => `!`).join(""); return `
    ${esc(name)}
    ${bar || `no tier-1 sources yet`}
    ${srcs.length || ""}${chip}${ticks}
    `; }).join("\n"); return rows; } // (d) open-question pressure gauges per target domain function obsGauges(qs: ObsQ[], parked: number): string { const per: Record = {}; for (const [d] of DOMAINS) per[d] = { open: [], ans: [] }; for (const q of qs) for (const t of q.targets) if (per[t]) (q.open ? per[t].open : per[t].ans).push(q); const maxOpen = Math.max(1, ...DOMAINS.map(([d]) => per[d].open.length)); const cards = DOMAINS.map(([d, name]) => { const { open, ans } = per[d]; const frac = open.length / maxOpen; const ang = -90 + frac * 180; const heat = open.length >= 4 ? "#f7768e" : open.length >= 2 ? "#e0af68" : "#9ece6a"; const chips = [...open.map(q => `${q.id}`), ...ans.map(q => `${q.id}`)].join(""); return `
    ${esc(name)}
    ${open.length} open · ${ans.length} answered
    ${chips || 'idle'}
    `; }).join("\n"); return cards + `

    + ${parked} questions parked in the backlog (excluded from gauges by design — they don't block verdicts).

    `; } const OBS_CSS = ` .obox{border:1px solid var(--line);border-radius:18px;background:#0c0e13;overflow:hidden;margin:.9rem 0;padding:.4rem} .obox svg{display:block;width:100%;height:auto} .ohz{font:800 10px ui-sans-serif,system-ui,sans-serif;fill:#bb9af7;letter-spacing:.14em} .ohz2{font:600 9px ui-sans-serif,system-ui,sans-serif;fill:#8a93a5} .oax{font:600 10px ui-sans-serif,system-ui,sans-serif;fill:#8a93a5} .ostar{font:700 11px ui-sans-serif,system-ui,sans-serif;fill:#d8dee9} .otrad{font:500 9px ui-sans-serif,system-ui,sans-serif;fill:#8a93a5} .ofl{font:700 10px ui-sans-serif,system-ui,sans-serif;fill:#8a93a5} .ocols{display:grid;grid-template-columns:3fr 2fr;gap:1.4rem;align-items:start} @media(max-width:960px){.ocols{grid-template-columns:1fr}} .osec h2{margin-top:1.6rem} .osec p.lede{color:var(--dim);margin:.2rem 0 .6rem;font-size:.9rem;max-width:48rem} .osrow{display:flex;align-items:center;gap:.6rem;padding:.42rem 0;border-top:1px dashed var(--line);font-size:.86rem} .osd{min-width:7.5rem;font-weight:700;color:#b6bdcc} .osbar{flex:1;display:flex;height:14px;border-radius:7px;overflow:hidden;background:#161922;min-width:8rem} .osbar i{display:block;min-width:8px} .osn{color:var(--dim);font-variant-numeric:tabular-nums;min-width:1.4rem;text-align:right} .ochip{font-size:.68rem;font-weight:700;color:var(--dim);background:#161922;border:1px solid var(--line);border-radius:999px;padding:.1em .6em;white-space:nowrap} .otick{display:inline-flex;align-items:center;justify-content:center;width:15px;height:15px;border-radius:50%;background:#f7768e22;color:var(--pink);font-size:.68rem;font-weight:800} .ogrid{display:flex;flex-wrap:wrap;gap:.9rem} .ogauge{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:.7rem .8rem .6rem;width:11.4rem} .ogauge svg{width:100%;height:auto} .ogd{display:block;font-weight:800;font-size:.86rem;color:var(--fg);margin-top:.2rem} .ogn{font-size:.76rem;color:var(--dim)} .ogc{display:flex;flex-wrap:wrap;gap:.25rem;margin-top:.35rem} .qchip{font-size:.66rem;font-weight:800;border-radius:999px;padding:.08em .55em;border:1px solid var(--line)} .qchip.qo{background:#e0af6822;color:var(--amber)} .qchip.qa{background:#9ece6a18;color:var(--green);opacity:.75} .odim{color:var(--dim);font-size:.8rem} .oleg{display:flex;gap:1rem;flex-wrap:wrap;font-size:.78rem;color:var(--dim);margin:.2rem 0 .6rem} .oleg i{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.35rem} `; function observatoryPage(): Response { const notes = obsScan(); const qs = obsQuestions(), parked = backlogCount(); const nMotif = notes.filter(n => n.type === "motif").length; const nClaim = notes.filter(n => n.type === "claim").length; const nSrc = notes.filter(n => n.type === "source").length; const body = `

    Evidence Observatory

    The vault knowing what it knows — ${nMotif} motifs, ${nClaim} claims, ${nSrc} sources, ${qs.length} register questions, aggregated live from frontmatter. Every glyph opens the note behind it. Items the vault cannot date sit in the “beyond the evidence horizon” zone — beyond that line we are reconstructing, not observing.

    The Motif Constellation

    Motifs as stars on the deep-time axis, positioned at earliest attestation, sized by occurrence count, colored by transmission verdict. Tradition profiles orbit below as faint hollow points.

    descentcontactconvergenceunresolvedopacity = confidence · hollow = tradition profile
    ${obsConstellation(notes)}

    The Claim Field

    Every claim (and tradition profile, hollow) on a confidence × evidence-class grid. The healthy corner (high × class 1–2) glows green; a dot in the amber-dashed corner — high confidence on reconstruction-or-worse — wears a calibration-alarm ring.

    ${obsClaimField(notes)}

    Open-Question Pressure

    Register questions by target domain: hot dials are where synthesis is re-tasking the gathering. Chips link to the register.

    ${obsGauges(qs, parked)}

    Source Diversity Spectra

    Tier-1 sources per domain, stacked by evidence class — archaeology · text · reconstruction · ethnography · cognitive — with URL-verification ratios. A pink ! marks a note whose counter-evidence is empty or “none found”.

    ${obsSpectra(notes)}

    Raw aggregate: /api/observatory · proposal: evidence-observatory. The page re-renders itself when the vault changes (the standard 3-second version poll) — leave it open and watch the vault think.

    `; return page("Evidence Observatory — origins//research", "/observatory", body, "/observatory"); } // ---------- /pantheon — pantheon correspondence grid ---------- // Typographic tiles only (no portraits in v1); rows = divine roles, columns = traditions. // Selecting a row draws its verdict-colored evidence edges; empty cells are honest gaps. const PAN_CSS = ` .pctl{display:flex;gap:.5rem;flex-wrap:wrap;margin:.8rem 0} .prole{font:700 .84rem ui-sans-serif,system-ui,sans-serif;color:var(--dim);background:var(--panel);border:1px solid var(--line);border-radius:999px;padding:.42rem 1rem;cursor:pointer} .prole:hover{color:var(--fg);border-color:#3d59a1} .prole.on{color:var(--fg);background:#3d59a133;border-color:#3d59a1} .pscroll{overflow-x:auto;border:1px solid var(--line);border-radius:18px;background:#0c0e13} #pgrid{position:relative;display:grid;grid-template-columns:9.5rem repeat(${P_TRADS.length},minmax(10.5rem,1fr));gap:.45rem;padding:.7rem;min-width:${9.5 + P_TRADS.length * 11}rem} .phead{padding:.5rem .6rem .4rem} .phead b{display:block;font-size:.84rem;letter-spacing:-.01em} .phead i{font-style:normal;font-size:.66rem;color:var(--amber);font-weight:700;font-variant-numeric:tabular-nums} .prowlab{display:flex;flex-direction:column;justify-content:center;padding:.5rem .6rem;border:0;background:none;text-align:left;cursor:pointer;border-radius:12px;color:var(--fg)} .prowlab:hover{background:var(--card)} .prowlab b{font-size:.88rem;letter-spacing:-.01em} .prowlab span{font-size:.68rem;color:var(--dim);line-height:1.45} .ptile{position:relative;display:block;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:.55rem .65rem;color:var(--fg);transition:opacity .3s,border-color .15s;min-height:5.2rem} .ptile:hover{border-color:#3d59a1;color:var(--fg)} .ptile .pn{font-weight:800;letter-spacing:-.01em;line-height:1.25} .ptile .pd{font-size:.68rem;color:var(--amber);font-weight:700;margin-top:.25rem;line-height:1.45;font-variant-numeric:tabular-nums} .ptile .pc{font-size:.62rem;color:var(--dim);margin-top:.2rem} .pgap{display:flex;align-items:center;justify-content:center;border:1px dashed #272b38;border-radius:12px;padding:.55rem .65rem;font-size:.7rem;color:#5a6378;text-align:center;line-height:1.5;min-height:5.2rem;transition:opacity .3s} .pbadge{display:inline-block;font-size:.6rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase;border-radius:999px;padding:.12em .55em;margin-top:.3rem} .ptag{display:none;font-size:.66rem;line-height:1.5;color:var(--fg);background:#e0af6818;border:1px solid #e0af6855;border-radius:8px;padding:.3rem .45rem;margin-top:.35rem} #pgrid[data-sel] .ptile,#pgrid[data-sel] .pgap{opacity:.22} #pgrid[data-sel] .prowlab{opacity:.45} ${P_ROLES.map(r => `#pgrid[data-sel="${r.id}"] [data-row="${r.id}"]{opacity:1} #pgrid[data-sel="${r.id}"] .xlit-${r.id}{opacity:1} #pgrid[data-sel="${r.id}"] .prowlab[data-rolebtn="${r.id}"]{opacity:1} #pgrid[data-sel="${r.id}"] .ptag[data-showrow="${r.id}"]{display:block}`).join("\n")} #pedges{position:absolute;inset:0;pointer-events:none;z-index:3} #pedges path.pe{fill:none;stroke-width:3;transition:stroke-dashoffset .9s ease;pointer-events:stroke;cursor:pointer} #pedges path.phit{fill:none !important;stroke:transparent;stroke-width:16;pointer-events:stroke;cursor:pointer} .pe-descent{stroke:#9ece6a}.pe-contact{stroke:#7aa2f7}.pe-convergence{stroke:#bb9af7}.pe-unresolved{stroke:#8a93a5;stroke-dasharray:6 5} #pevid{border:1px solid var(--line);border-radius:14px;background:var(--panel);padding:.8rem 1.1rem;margin:.9rem 0;font-size:.9rem;min-height:3.2rem;color:#c4cad6} #pevid b.pk{text-transform:uppercase;letter-spacing:.1em;font-size:.72rem;padding:.15em .7em;border-radius:999px;margin-right:.5rem} #pevid .pvd{color:var(--fg);font-weight:700} `; function pantheonPage(): Response { const byCell: Record = {}; for (const d of P_DEITIES) (byCell[d.role + ":" + d.trad] ??= [] as any).push(d); const heads = `
    ` + P_TRADS.map(t => `
    ${esc(t.label)}${esc(t.sub)}
    `).join(""); const rows = P_ROLES.map(r => { const lab = ``; const cells = P_TRADS.map(t => { const ds = byCell[r.id + ":" + t.id] ?? []; if (!ds.length) { const hint = P_GAP_HINTS[r.id + ":" + t.id]; return `
    ${esc(hint ?? "no vault note yet")}
    `; } return ds.map(d => { const xlit = d.rowTag && d.rowTag.row !== r.id ? ` xlit-${d.rowTag.row}` : ""; const badge = d.badge ? `${d.badge}` : ""; const tag = d.rowTag ? `
    ${esc(d.rowTag.text)}
    ` : ""; return `
    ${esc(d.name)}
    ${esc(d.date)}
    ${esc(d.cls)}
    ${badge}${tag}
    `; }).join(""); }).join(""); return lab + cells; }).join("\n"); const edgesJson = JSON.stringify(P_EDGES); const body = `

    Pantheon Correspondence Grid

    Which of these gods are actually related — and how would we know? Select a role row to draw the connections the vault can defend, colored by verdict: descent (sound-law name cognacy) · contact (documented borrowing) · convergence (same idea, no shared word). Unconnected tiles are unconnected on purpose. Every tile links to its vault note; dashed cells are honest gaps.

    ${P_ROLES.map(r => ``).join("")}
    ${heads} ${rows}
    Hover or click a connection line to see the evidence behind it.

    Curated from vault notes only — no edge without a tier-1/2 note behind it. The sky-father row is the benchmark: Dyaus–Zeus–Jupiter–Týr share one inherited word (*dyḗus) transformed by each branch's own sound laws, while Thor — the storm-wielder in Zeus's role — shares none of it: names are inherited; jobs are reassigned (Q10). Data: sky-father · dying-rising-god · proposal: pantheon-correspondence-grid.

    `; return page("Pantheon Correspondence Grid — origins//research", "/pantheon", body, "/pantheon"); } // ---------- /whisper — Whisper Chain: the oral-transmission chasm, walked ---------- // Honesty contract: every date and claim on this page comes from the two Rigveda gap notes // (06_dharmic + 04_indo_european), the Aboriginal time-depth note (08_indigenous), and the // Henige 2009 source record. The drift lane is an explicitly-labeled SIMULATION (deterministic, // seeded — every visitor sees the same decay), the honest control condition, not data. // Confidence inheritance: the Aboriginal coda renders dim because its note rates the claim // `low`; the Vedic chain renders bright. prefers-reduced-motion: flicker/wobble disabled. const WH_NOTES = { D6: "/06_dharmic/2_notes/rigveda-oral-composition-attestation-gap.md", D4: "/04_indo_european/2_notes/rigveda-oral-gap-composition-vs-attestation.md", AB: "/08_indigenous/2_notes/aboriginal-oral-tradition-coastal-flooding-time-depth-claim.md", HEN: "/08_indigenous/1_sources/henige-2009-deep-time-oral-tradition.md", }; // deterministic "telephone game" — RV 1.1.1 in English, one mutation per generation const WH_SYN: Record = { praise: ["honor", "thank", "greet", "sing to"], agni: ["the fire", "the flame", "the bright one"], fire: ["flame", "light", "blaze"], flame: ["light", "spark"], household: ["house", "home", "hearth"], priest: ["elder", "holy man", "keeper"], divine: ["holy", "great", "heavenly"], minister: ["servant", "master", "helper"], sacrifice: ["offering", "feast", "rite"], invoker: ["caller", "summoner", "speaker"], bestows: ["gives", "brings", "hands out"], greatest: ["best", "richest", "finest"], treasure: ["wealth", "riches", "gifts", "gold"], wealth: ["money", "fortune"], }; function whisperDrift(): string[] { let s = 0x2b992d39; // fixed seed const rnd = () => { s = (Math.imul(s, 1664525) + 1013904223) >>> 0; return s / 4294967296; }; let w = "I praise Agni the household priest the divine minister of the sacrifice the invoker who bestows the greatest treasure".split(" "); const out = [w.join(" ")]; for (let g = 1; g <= 100; g++) { const q = rnd(); if (q < 0.5) { // synonym swap const cand = w.map((x, i) => [x.toLowerCase().replace(/[^a-z]/g, ""), i] as [string, number]).filter(([x]) => WH_SYN[x]); if (cand.length) { const [word, i] = cand[Math.floor(rnd() * cand.length)]; const alts = WH_SYN[word]; w = w.slice(0, i).concat(alts[Math.floor(rnd() * alts.length)].split(" "), w.slice(i + 1)); } } else if (q < 0.72 && w.length > 6) { // dropped clause/word const i = 1 + Math.floor(rnd() * (w.length - 1)); w = w.slice(0, i).concat(w.slice(i + 1)); } else if (q < 0.9 && w.length > 3) { // smoothed grammar (adjacent transposition) const i = 1 + Math.floor(rnd() * (w.length - 2)); const t2 = w[i]; w[i] = w[i + 1]; w[i + 1] = t2; } // else: this generation happens to transmit cleanly out.push(w.join(" ")); } return out; } const WH_DRIFT = whisperDrift(); const WH_CSS = ` #wwrap{height:2400vh;position:relative} @media(max-width:700px){#wwrap{height:780vh}} #wstage{position:sticky;top:0;height:100vh;overflow:hidden;background:#06070b;border:1px solid var(--line);border-radius:18px} #wc{position:absolute;inset:0;width:100%;height:100%} .wlay{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;gap:.8rem;padding:4.4rem 1.6rem 2.2rem;opacity:0;visibility:hidden;pointer-events:none;z-index:2} .wlay a,.wlay .wplank{pointer-events:auto} .wkick{font-size:.74rem;font-weight:800;letter-spacing:.2em;text-transform:uppercase;color:#e0af68} .wdev{font-size:1.65rem;color:#fff;line-height:1.65} .wiast{font-size:1.1rem;color:#e9d9b8;font-style:italic} .wtrans{font-size:.94rem;color:var(--dim);max-width:34rem;margin:0} .wscroll{margin-top:1.2rem;font-size:.8rem;color:var(--dim);animation:wbob 2.2s ease-in-out infinite} @keyframes wbob{0%,100%{transform:none}50%{transform:translateY(6px)}} @media(prefers-reduced-motion:reduce){.wscroll{animation:none}.wplank{animation:none !important}} #whud{position:absolute;top:3.4rem;left:0;right:0;display:flex;justify-content:center;align-items:baseline;gap:1.1rem;font-variant-numeric:tabular-nums;font-size:.84rem;color:var(--dim);flex-wrap:wrap} #whud b{color:#e0af68;font-size:1.04rem} .wlanes{display:flex;flex-direction:column;gap:.8rem;width:min(46rem,94%)} .wlane{background:#0c0e13d8;border:1px solid var(--line);border-radius:14px;padding:.7rem 1.1rem;text-align:left} .wlane .wl{font-size:.64rem;font-weight:800;letter-spacing:.14em;text-transform:uppercase;margin-bottom:.3rem} .wlane .wt{margin:0;line-height:1.7} .wlane.lock{border-color:#9ece6a44}.wlane.lock .wl{color:var(--green)} .wlane.lock .wt{color:#eef3e2;font-style:italic} .wlane.drift{border-color:#f7768e33}.wlane.drift .wl{color:var(--pink)} .wlane.drift .wt{color:#cfa6ad} .wpat{font-size:.78rem;color:var(--amber);margin-top:.45rem;line-height:1.6} .wpat i{color:var(--dim);font-style:normal} #wmit{position:absolute;right:4%;top:16%;width:min(20rem,80%);text-align:left;background:#10131bee;border:1px solid #7aa2f755;border-radius:14px;padding:.8rem 1rem;font-size:.82rem;color:#c8d2e8;opacity:0;visibility:hidden;line-height:1.6} #wmit b{color:var(--fg)} .wbadge{display:inline-block;font-size:.62rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase;border-radius:999px;padding:.14em .6em;background:#7aa2f722;color:var(--blue);margin-right:.3rem} .wbadge.b3{background:#bb9af722;color:var(--purple)} .wbadge.bp{background:#f7768e22;color:var(--pink)} .wbadge.b1{background:#e0af6822;color:var(--amber)} .wbig{font-size:1.7rem;font-weight:800;letter-spacing:-.02em;color:#fff;line-height:1.3} .wsub{color:var(--dim);font-size:.92rem;max-width:42rem;margin:0;line-height:1.7} #wbridge{position:relative;width:min(52rem,96%);margin-top:.8rem} #wrope{position:absolute;left:0;right:0;top:1.1rem;height:3px;background:linear-gradient(90deg,#6b5a3a,#8a7044,#6b5a3a);border-radius:2px} #wwalk{position:absolute;top:.42rem;width:14px;height:14px;border-radius:50%;background:radial-gradient(circle,#ffd68c,#e08c3c);box-shadow:0 0 14px #e0af68aa;transform:translateX(-50%);left:8%} .wplanks{position:relative;display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem;padding-top:2.3rem} @media(max-width:700px){.wplanks{grid-template-columns:1fr;gap:.5rem}} a.wplank{display:block;text-align:left;background:linear-gradient(180deg,#2b2317,#1c1812);border:1px solid #6b5a3a88;border-radius:10px;padding:.7rem .9rem;color:var(--fg);font-size:.8rem;animation:wcreak 5s ease-in-out infinite;line-height:1.55} a.wplank:nth-child(2){animation-delay:1.6s}a.wplank:nth-child(3){animation-delay:3.1s} @keyframes wcreak{0%,100%{transform:rotate(0)}50%{transform:rotate(.6deg) translateY(1px)}} a.wplank b{display:block;color:#ffd9a0;letter-spacing:-.01em;margin-bottom:.2rem} a.wplank .wd{color:var(--dim);margin:.25rem 0 .1rem} .wms{display:flex;gap:1rem;margin-top:1.4rem;flex-wrap:wrap;justify-content:center} .wm{width:16rem;text-align:left;background:#0e1016f2;border:1px solid #3a4154;border-radius:12px;padding:.7rem .9rem;font-size:.8rem;color:#c4cad6;box-shadow:0 0 30px #7aa2f71c;line-height:1.6} .wm b{color:#fff} .wdimwrap{opacity:.78;filter:saturate(.55);width:min(46rem,94%);display:flex;flex-direction:column;gap:.7rem;align-items:center} .wcols{display:grid;grid-template-columns:1fr 1fr;gap:1.2rem;width:min(54rem,96%);text-align:left} @media(max-width:700px){.wcols{grid-template-columns:1fr}} .wcol{background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:.9rem 1.2rem;font-size:.86rem;color:#c4cad6} .wcol.against{border-left:3px solid var(--pink)} .wcol.counter{border-left:3px solid var(--green)} .wcol h3{margin:.2rem 0 .5rem;font-size:.94rem} .wcol ul{margin:.3rem 0;padding-left:1.1rem} .wcol li{margin:.3rem 0;line-height:1.6} .wq{font-style:italic;color:#e8ccd2;margin:.5rem 0 0} .wfinal{font-size:1.45rem;font-weight:800;letter-spacing:-.02em;color:#fff;max-width:40rem;line-height:1.55} .wlinks{font-size:.83rem;color:var(--dim)} .wlinks a{display:inline-block;margin:0 .45rem} `; const WH_JS = ` (()=>{ const wrap=document.getElementById('wwrap');if(!wrap)return; const stage=document.getElementById('wstage'),cv=document.getElementById('wc'),ctx=cv.getContext('2d'); const reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches; const $=function(id){return document.getElementById(id)}; const lays={open:$('w-open'),chain:$('w-chain'),chasm:$('w-chasm'),coda:$('w-coda'),end:$('w-end')}; const hudY=$('whyear'),hudG=$('whgen'),hudP=$('whphase'),driftEl=$('wdrift'),pmode=$('wpmode'),pdemo=$('wpdemo'),mit=$('wmit'),walk=$('wwalk'); const W8=['agním','īḷe','puróhitaṃ','yajñásya','devám','ṛtvíjam','hótāraṃ','ratnadhā́tamam']; const MODES=['saṃhitā — the continuous text','pada — word by word, sandhi dissolved','krama — overlapping pairs (ab · bc · cd)','jaṭā — forward, back, forward (ab·ba·ab)','ghana — the full braid (ab·ba·ab·abc·cba·abc)']; function demo(m){ const w=W8; if(m===0)return w.slice(0,4).join(' ')+' …'; if(m===1)return w.slice(0,4).join(' | ')+' …'; if(m===2)return w[0]+' '+w[1]+' · '+w[1]+' '+w[2]+' · '+w[2]+' '+w[3]+' …'; if(m===3)return w[0]+' '+w[1]+' / '+w[1]+' '+w[0]+' / '+w[0]+' '+w[1]+' · '+w[1]+' '+w[2]+' / '+w[2]+' '+w[1]+' / '+w[1]+' '+w[2]+' …'; return w[0]+' '+w[1]+' / '+w[1]+' '+w[0]+' / '+w[0]+' '+w[1]+' / '+w[0]+' '+w[1]+' '+w[2]+' / '+w[2]+' '+w[1]+' '+w[0]+' / '+w[0]+' '+w[1]+' '+w[2]+' …'; } const clamp=function(x,a,b){return xb?b:x}; const fade=function(p,a,b,c,d){return p<=a||p>=d?0:p>>0;return ss/4294967296}; const STARS=[];for(let i=0;i<34;i++)STARS.push([srnd(),srnd()*.55+.1,srnd()*.9+.4]); function vis(el,w){el.style.opacity=w.toFixed(3);el.style.visibility=w>.02?'visible':'hidden'} function figure(x,y,s,a){ if(a<=0||x<-60||x>Wd+60)return; const g=ctx.createRadialGradient(x,y-30*s,2,x,y-30*s,46*s); g.addColorStop(0,'rgba(255,196,110,'+(.5*a).toFixed(3)+')'); g.addColorStop(.35,'rgba(224,140,60,'+(.18*a).toFixed(3)+')'); g.addColorStop(1,'rgba(224,140,60,0)'); ctx.fillStyle=g;ctx.fillRect(x-50*s,y-80*s,100*s,100*s); ctx.fillStyle='rgba(10,10,15,'+(.95*a).toFixed(3)+')'; ctx.beginPath();ctx.arc(x,y-21*s,5.5*s,0,6.2832);ctx.fill(); ctx.beginPath();ctx.moveTo(x-11*s,y);ctx.quadraticCurveTo(x,y-19*s,x+11*s,y);ctx.closePath();ctx.fill(); ctx.fillStyle='rgba(255,214,140,'+(.85*a).toFixed(3)+')'; ctx.beginPath();ctx.arc(x,y-30*s,(1.4+flick)*s,0,6.2832);ctx.fill(); } function chainRow(genF,count,yBase,s,a,sp){ const lo=Math.max(0,Math.ceil(genF-(Wd*.55)/sp)),hi=Math.min(count,Math.floor(genF+(Wd*.55)/sp)); for(let i=lo;i<=hi;i++)figure(Wd/2+(i-genF)*sp,yBase,s,a*(i<=genF+.5?1:.4)); } function ticks(y,n,col,a){ if(a<=0)return; ctx.strokeStyle=col;ctx.globalAlpha=a;ctx.lineWidth=1; const pad=Wd*.06,w=Wd-2*pad; ctx.beginPath(); for(let i=0;i<=n;i++){const x=pad+(i/n)*w;ctx.moveTo(x,y-4);ctx.lineTo(x,y+4)} ctx.stroke();ctx.globalAlpha=1; } function draw(){ ctx.clearRect(0,0,Wd,Hd); ctx.fillStyle='#06070b';ctx.fillRect(0,0,Wd,Hd); const openW=fade(t,-.01,0,.025,.05); const chainW=fade(t,.025,.05,.50,.545); const chasmW=fade(t,.51,.555,.715,.755); const codaW=fade(t,.725,.765,.865,.895); const endW=fade(t,.875,.92,1,1.02); const chainP=clamp((t-.05)/.45,0,1); const gy=Hd*.78; if(openW>0)figure(Wd/2,gy,1.4,openW); if(chainW>0){ ctx.fillStyle='rgba(9,10,14,'+(.9*chainW).toFixed(3)+')';ctx.fillRect(0,gy,Wd,Hd-gy); chainRow(chainP*GENS,GENS,gy,1,chainW,Math.max(80,Wd/10)); } if(chasmW>0){ const rimL=Wd*.16,rimR=Wd*.84; ctx.fillStyle='rgba(9,10,14,'+(.9*chasmW).toFixed(3)+')'; ctx.fillRect(0,gy,rimL,Hd-gy);ctx.fillRect(rimR,gy,Wd-rimR,Hd-gy); const vg=ctx.createLinearGradient(0,gy,0,Hd); vg.addColorStop(0,'rgba(11,13,20,'+chasmW.toFixed(3)+')');vg.addColorStop(1,'rgba(0,0,0,'+chasmW.toFixed(3)+')'); ctx.fillStyle=vg;ctx.fillRect(rimL,gy,rimR-rimL,Hd-gy); ctx.fillStyle='rgba(216,222,233,'+(.8*chasmW).toFixed(3)+')'; for(const st of STARS){ctx.globalAlpha=chasmW*.7;ctx.beginPath();ctx.arc(st[0]*Wd,st[1]*Hd,st[2],0,6.2832);ctx.fill()} ctx.globalAlpha=1; figure(rimL*.45,gy,.9,chasmW);figure(rimL*.8,gy,.9,chasmW); figure(rimR+(Wd-rimR)*.35,gy,.45,chasmW*.8);figure(rimR+(Wd-rimR)*.7,gy,.45,chasmW*.8); } if(codaW>0){ ticks(Hd*.3,GENS,'#e0af68',.55*codaW); ticks(Hd*.42,400,'#8a93a5',.2*codaW); } if(endW>0){ctx.fillStyle='rgba(23,26,35,'+(.96*endW).toFixed(3)+')';ctx.fillRect(0,0,Wd,Hd)} } function update(){ const openW=fade(t,-.01,0,.025,.05); const chainW=fade(t,.025,.05,.50,.545); const chasmW=fade(t,.51,.555,.715,.755); const codaW=fade(t,.725,.765,.865,.895); const endW=fade(t,.875,.92,1,1.02); vis(lays.open,openW);vis(lays.chain,chainW);vis(lays.chasm,chasmW);vis(lays.coda,codaW);vis(lays.end,endW); const chainP=clamp((t-.05)/.45,0,1); const gen=Math.round(chainP*GENS),year=Y0+chainP*(Y1-Y0); hudY.textContent=fmt(year); hudG.textContent='generation '+gen+' of ~'+GENS; hudP.textContent=year<-1200?'composition window — inferred (3-reconstruction)':'oral transmission — no manuscript exists'; driftEl.textContent='“'+WDRIFT[Math.min(gen,WDRIFT.length-1)]+'”'; const m=Math.min(4,Math.floor(chainP*6)); pmode.textContent=MODES[m];pdemo.textContent=demo(m); vis(mit,fade(chainP,.02,.05,.13,.18)*Math.max(chainW,0)); const chasmP=clamp((t-.555)/.16,0,1); walk.style.left=(8+84*chasmP).toFixed(1)+'%'; draw(); } function resize(){ const dpr=window.devicePixelRatio||1; Wd=stage.clientWidth;Hd=stage.clientHeight; cv.width=Math.round(Wd*dpr);cv.height=Math.round(Hd*dpr); ctx.setTransform(dpr,0,0,dpr,0,0);update(); } function onScroll(){ const total=wrap.offsetHeight-window.innerHeight; t=clamp(-wrap.getBoundingClientRect().top/Math.max(1,total),0,1); update(); } window.addEventListener('scroll',onScroll,{passive:true}); window.addEventListener('resize',resize); resize();onScroll(); if(!reduce){ let last=0; (function loop(ts){ if(ts-last>90){last=ts;flick=.3+Math.random()*1.1;draw()} requestAnimationFrame(loop); })(0); } })();`; function whisperPage(): Response { const body = `
    Whisper Chain
    This text has no original.

    For roughly 2,300–2,500 years — about 100 human generations — the Rigveda existed only as sound passing from one mouth to the next ear. Composition c. 1500–1200 BCE is an inference; the oldest reported manuscript is c. 1040 CE.

    अग्निमीळे पुरोहितं यज्ञस्य देवमृत्विजम् । होतारं रत्नधातमम् ॥
    agním īḷe puróhitaṃ yajñásya devám ṛtvíjam · hótāraṃ ratnadhā́tamam

    “I praise Agni, the household priest, divine minister of the sacrifice, the invoker, greatest bestower of treasure.” — Rigveda 1.1.1

    scroll ↓ — the page is the chain, ~100 generations long
    year 1500 BCEgeneration 0 of ~100
    The pāṭha-locked lane — byte-identical, generation after generation

    agním īḷe puróhitaṃ yajñásya devám ṛtvíjam · hótāraṃ ratnadhā́tamam

    saṃhitā

    Four interlocking recitation modes: corrupt one syllable and the modes disagree — the error is caught.
    Control lane — an ordinary telephone game (deterministic simulation, not data)

    One small mutation per generation: synonym swaps, dropped words, smoothed grammar. This is what unsafeguarded transmission does.
    2-text · external anchor
    c. 1380 BCE — the Mitanni treaty. A Hittite–Mitanni treaty from the Hattusa archive invokes Mitra, Varuṇa, Indra, Nāsatyā as divine witnesses — the first time anyone outside the chain wrote down anything from inside it: four god-names, in someone else's script, far from the Punjab. The lone external anchor. note →
    The chasm
    COMPOSITION → ATTESTATION

    ~2,300–2,500 years · zero manuscripts. Everything before the manuscript is a bridge built of inference — and this is every plank it has.

    c. 1040 CE — Nepal. The oldest reported Rigveda manuscript (Witzel 1997) — a single scholarly report, not fully published. Genuine residual uncertainty, recorded as such. note →
    1464 CE — BORI, Pune. The oldest securely documented manuscript: birch bark, Sharada script, UNESCO Memory of the World 2007. The first moment the sound becomes a physically dated object. note →
    Coda — the longer chain
    ~400 generations, claimed

    Australian Aboriginal coastal-flooding traditions: stories at 21 coastal sites describe land now under the sea, and the matching sea-level events date to 7,250–13,070 cal BP (Nunn & Reid 2016).

    confidence · lowevents · 1-archaeologytransmission · 4-ethnography / speculative

    No pāṭha. No treaty plank. No manuscript wall. The only bridge offered is the landscape itself — story geography matching the drowned-land contours — and whether that bridge holds is exactly what is contested. This chain renders dim because the vault rates the time-depth claim low: the geological events are real (class 1); the transmission chain is undated and unverified.

    House lights

    Henige · Hiscock — the steelman against deep time

    • The dating is circular: it assumes the very fidelity it claims to prove. There is no independent clock on oral tradition.
    • Every well-documented case — the same tradition recorded at two dates — shows narrative change within decades to a few centuries of contact.
    • Communities were not isolated: language replacement, migration, and inter-group contact are corruption vectors over millennia.
    • Flood and coastal-change stories arise globally — convergence needs no deep memory.
    • Post-hoc sampling: stories were selected because they match the geology.

    “Impossible to disprove yet impossible to believe.” — Henige 2009, the standing critique (it predates Nunn & Reid 2016). source record →

    The Vedic counterexample

    • Four interlocking recitation modes — pada, krama, jaṭā, ghana — make undetected insertion or deletion extremely difficult.
    • Phonological preservation is empirically demonstrable: regional schools separated for centuries recite with near-identical phonology.
    • The text came down “almost entirely without corruptions.”
    • So high-fidelity deep transmission is possible — but it required a dedicated, engineered mnemonic institution.
    • And even this chain cannot date its own origin: the safeguards are post-composition innovations, semantic drift is not directly verifiable, and composition c. 1500–1200 BCE remains inference (3-reconstruction), not attestation.
    Oral tradition can carry a text farther than any manuscript. It just can't carry a date.

    Every date and claim above comes from the linked vault notes; the drift lane is a labeled, deterministic simulation (the honest control condition). The verse is Rigveda 1.1.1 per the whisper-chain proposal. Reduced-motion preferences disable the flicker and plank wobble.

    `; return page("Whisper Chain — 100 generations of mouths — origins//research", "/whisper", body, "/whisper"); } // ---------- /rituals — Ritual Walkthroughs (scroll-driven, emic/etic split per AGENTS.md §1.2) ---------- // Discipline: each step shows LEFT "the diviner believes…" (emic — grounded in the inscriptions // as carried by the vault notes) and RIGHT "historians infer…" (etic — scholar-named), never // blended. Only steps the Keightley / shang-religion / oracle-bones notes license are shown. // Backdrop: the vault's East Asia illustration, re-treated per step via CSS filters — no image // generation. Three further walkthroughs are stubs blocked on gathering (register Q31–Q33). const R_NOTES: Record = { K: ["/07_east_asian/1_sources/keightley-sources-of-shang-history.md", "Keightley 1978, Sources of Shang History (tier 1)"], T: ["/07_east_asian/2_notes/shang-religion.md", "Tradition: Shang religion (tier 2)"], C: ["/07_east_asian/2_notes/oracle-bones-earliest-dated-chinese-religious-writing.md", "Claim: earliest dated Chinese religious writing (tier 2)"], E: ["/07_east_asian/1_sources/eno-shang-state-religion-pantheon.md", "Eno, Shang state religion and the pantheon (tier 1)"], }; type RitStep = { title: string; emic: string; emicSrc: string[]; etic: string; eticSrc: string[] }; const SHANG_STEPS: RitStep[] = [ { title: "The bone is prepared", emic: "The bone must be made ready before the powers can be addressed. Ox scapulae and turtle plastrons are prepared for the rite — the surface through which the ancestors and Di, real and causally effective powers, will answer the court.", emicSrc: ["T", "K"], etic: "Keightley (1978): the inscriptions are divination records on prepared ox scapulae and turtle plastrons from the royal capital Yinxu (Anyang). An estimated 150,000+ inscribed fragments have been recovered — a systematic, large-scale corpus, but an exclusively royal one: it attests elite state religion, not what most Shang people practiced.", eticSrc: ["K", "C"], }, { title: "The charge is posed", emic: "Before the king, the diviner poses the charge in formal terms — “On day X, crack-making. The king should hunt at Y?” The matter is put directly to the ancestral powers, whose favor determines outcomes in war, harvest, weather, royal health, childbirth, and hunting.", emicSrc: ["K", "T"], etic: "Keightley: the formal charge structure provides dated, sequential records of royal religious activity. Historians read the procedure as a structured mechanism for decision under uncertainty (anxiety-reduction) and as divine authorization for collective royal action (cooperation-enforcement) — functions tagged in the vault's tradition profile.", eticSrc: ["K", "T"], }, { title: "Heat — and the crack", emic: "Heat is applied to the prepared bone until it cracks. The crack is the answer arriving: the inscriptions treat the cracks as divine responses to the charge that was posed. Divination, from inside the Shang ritual frame, is efficacious technology — not symbolic expression.", emicSrc: ["T", "C"], etic: "What survives is physical: burn and crack on bones recovered from stratified deposits at Yinxu, excavated systematically from 1928 onwards. Twenty-six bones from Wu Ding's reign are radiocarbon-dated to 1254–1197 BCE ±10 years (Zhichun Jing et al., Radiocarbon) — class 1-archaeology, independent of traditional Chinese chronology.", eticSrc: ["C", "K"], }, { title: "The king reads the crack", emic: "The crack is read as auspicious or inauspicious. Professional diviners (bu) execute the technical work, but the king is the ultimate divination authority — his unique ritual access to Di and the ancestors is the ontological basis of his right to rule.", emicSrc: ["T", "K"], etic: "Keightley and Eno: the king's exclusive ritual access grounded political authority — legitimation is the most clearly attested function across the corpus. What Di actually is remains genuinely contested: separate high deity (Keightley) or apex of the ancestral hierarchy (Eno)? The oracle-bone syntax cannot settle it (register Q25).", eticSrc: ["K", "E", "T"], }, { title: "The record — and the verification", emic: "The divination is carved into the bone: the charge, the reading, and — sometimes — what really happened. The inscriptions are not records of something believed; they are records of something done: charges made, cracks read, outcomes noted.", emicSrc: ["C", "K"], etic: "The verification records are analytically decisive: they show a pragmatic, outcome-tracking relationship with the supernatural, not a purely symbolic or commemorative one. These inscriptions are simultaneously the earliest dated corpus of Chinese writing and the earliest direct textual evidence for Chinese religious practice.", eticSrc: ["C", "K"], }, ]; const RIT_CSS = ` .ropen{position:relative;border:1px solid var(--line);border-radius:20px;overflow:hidden;background:#07080c;padding:3.2rem 2rem;text-align:center;margin:.6rem 0 0} .ropen h1{color:#fff;margin:.4rem 0 .8rem} .rkick{font-size:.72rem;font-weight:800;letter-spacing:.18em;text-transform:uppercase;color:#e0af68} .rdate{color:var(--dim);font-style:italic} .rhonesty{max-width:44rem;margin:1rem auto 0;border:1px solid #bb9af755;background:#bb9af712;border-radius:14px;padding:.7rem 1.1rem;font-size:.86rem;color:#d6dae3;text-align:left;line-height:1.65} .rrail{display:flex;gap:.4rem;flex-wrap:wrap;justify-content:center;margin-top:1rem} .rrail a{font-size:.76rem;font-weight:700;color:var(--dim);background:var(--panel);border:1px solid var(--line);border-radius:999px;padding:.3rem .8rem} .rrail a:hover{color:var(--fg)} .rstep{position:relative;min-height:165vh;margin:.8rem 0} .rstep.rlast{min-height:120vh} .rsticky{position:sticky;top:0;min-height:100vh;display:flex;align-items:center;justify-content:center;overflow:hidden;border-radius:20px;border:1px solid var(--line);background:#0c0e13} .rbg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover} .rshade{position:absolute;inset:0;background:linear-gradient(180deg,#000000a6,#000000cc 55%,#000000ea)} .f1 .rbg{filter:sepia(.45) brightness(.5) contrast(1.1)} .f2 .rbg{filter:brightness(.45) saturate(1.25) hue-rotate(-8deg)} .f3 .rbg{filter:brightness(.62) saturate(1.7) contrast(1.25) hue-rotate(-24deg)} .f4 .rbg{filter:brightness(.38) saturate(.55) contrast(1.1)} .f5 .rbg{filter:grayscale(.55) brightness(.55) contrast(1.15)} .fend .rbg{filter:grayscale(.92) brightness(.4) contrast(1.35)} .rin{position:relative;z-index:1;width:min(58rem,94%);padding:4.6rem 0 2.6rem} .rin h2{border:0;color:#fff;font-size:1.65rem;margin:.35rem 0 1rem;letter-spacing:-.02em} .rsplit{display:grid;grid-template-columns:1fr 1fr;gap:1rem;align-items:start} @media(max-width:760px){.rsplit{grid-template-columns:1fr}} .rpanel{background:#0c0e13e8;border:1px solid var(--line);border-radius:16px;padding:.9rem 1.1rem;font-size:.92rem;line-height:1.75;color:#d6dae3} .rpanel>b{display:block;font-size:.68rem;font-weight:800;letter-spacing:.14em;text-transform:uppercase;margin-bottom:.45rem} .rpanel p{margin:.2rem 0} .remic{border-left:3px solid var(--purple)}.remic>b{color:var(--purple)} .retic{border-left:3px solid var(--blue)}.retic>b{color:var(--blue)} .rsrc{margin-top:.55rem;padding-top:.45rem;border-top:1px dashed var(--line);font-size:.76rem;color:var(--dim)} .rsrc a{display:block;color:#b6bdcc}.rsrc a:hover{color:var(--fg)} html.js .rpanel{opacity:0;transform:translateY(16px);transition:opacity .8s ease,transform .8s ease} html.js .retic{transition-delay:.22s} html.js .rpanel.live{opacity:1;transform:none} @media(prefers-reduced-motion:reduce){html.js .rpanel{opacity:1;transform:none;transition:none}} .rclose{text-align:center} .rclose .rfinal{font-size:1.4rem;font-weight:800;letter-spacing:-.02em;color:#fff;max-width:38rem;margin:0 auto 1rem;line-height:1.55} .rhall{display:grid;grid-template-columns:repeat(auto-fill,minmax(17rem,1fr));gap:1.2rem;margin:1.6rem 0} .rcard{position:relative;display:block;border-radius:20px;overflow:hidden;border:1px solid var(--line);min-height:19rem;background-size:cover;background-position:center;transition:transform .2s,border-color .2s;color:var(--fg)} a.rcard:hover{transform:translateY(-3px);border-color:#e0af6866;color:var(--fg)} .rcard .rcin{position:absolute;inset:0;background:linear-gradient(180deg,#0003 25%,#000d 85%);display:flex;flex-direction:column;justify-content:flex-end;padding:1.4rem} .rcard .rcd{font-size:1.4rem;font-weight:800;letter-spacing:-.02em;color:#e0af68;font-variant-numeric:tabular-nums} .rcard .rcp{color:#c4cad6;font-size:.9rem} .rcard .rcgo{margin-top:.5rem;font-size:.8rem;font-weight:700;line-height:1.5} .rcard.soon{opacity:.55;filter:saturate(.4)} .rcard.soon .rcgo{color:var(--amber)} `; const RIT_JS = ` (()=>{ document.documentElement.classList.add('js'); const io=new IntersectionObserver(function(es){for(const e of es)if(e.isIntersecting)e.target.classList.add('live')},{threshold:.25}); const ps=document.querySelectorAll('.rpanel');for(let i=0;i `
    ${esc(d)}
    ${esc(p)}
    ${esc(go)}
    `; const body = `
    Ritual Walkthroughs

    Step inside a documented ancient rite

    Scroll-driven reconstructions of documented rituals, one step at a time — every frame split into an emic pane (what the participant believed, sourced to inscriptions and ritual texts) and an etic pane (what historians infer, scholar-named), never blended. A walkthrough ships only when every caption is licensed by a tier-1/2 vault note. One is ready; three are blocked on gathering, and their blocking questions sit in the register (Q31–Q33).

    c. 1250 BCE
    A Shang oracle-bone divination — Yinxu, reign of Wu Ding. Charge · crack · reading · record · verification.
    witness it → (the procedure is literally written on the surviving bones)
    ${stub("/assets/illustrations/02_mesopotamian.png", "1st millennium BCE", "Akitu festival, day 4 — Babylon. The Enūma Eliš recited before Marduk.", "gathering required — blocked on a tier-1 note on the Akitu ritual tablets (Linssen 2004) · register Q31 →")} ${stub("/assets/illustrations/06_dharmic.png", "c. 1200–900 BCE", "A Vedic fire offering (agnihotra/yajña) — kindling Agni, ghee oblations, priestly roles.", "gathering required — blocked on a tier-1 śrauta-procedure note; every frame must carry the composition-vs-attestation caveat · register Q32 →")} ${stub("/assets/illustrations/03_egyptian.png", "New Kingdom", "An Opening of the Mouth ceremony — the adze touching the mummy's mouth.", "gathering required — blocked on a tier-1 note on Otto's 1960 edition of the scene-sequence · register Q33 →")}

    Backdrops are the vault's domain illustrations, re-treated per step via CSS filters — no scene renders are generated until a walkthrough's captions are fully licensed. Proposal: ritual-walkthroughs.

    `; return page("Ritual Walkthroughs — origins//research", "/rituals", body, "/rituals"); } function ritualShang(): Response { const src = (keys: string[]) => keys.map(k => `→ ${esc(R_NOTES[k][1])}`).join(""); const steps = SHANG_STEPS.map((s, i) => `
    Step ${i + 1} of ${SHANG_STEPS.length}

    ${esc(s.title)}

    The diviner believes — emic, sourced to the inscriptions

    ${s.emic}

    ${src(s.emicSrc)}
    Historians infer — etic, scholar-named

    ${s.etic}

    ${src(s.eticSrc)}
    `).join("\n"); const body = `
    Ritual Walkthroughs · I · Shang divination

    Witness a divination — Anyang, c. 1250 BCE

    Yinxu, royal precinct, reign of Wu Ding. A question is put to the ancestors.

    The split-frame rule: on every step, the left pane is emic — what the participants believed, in their terms, as the inscriptions carry it — and the right pane is etic — what historians infer, with the scholar named and the evidence class stated. They never blend (AGENTS.md §1.2). Only steps licensed by the vault's Keightley / Shang-religion / oracle-bones notes are shown; the procedure itself — charge, crack, reading, record, verification — is written on the surviving bones. The backdrop is the vault's East Asia illustration re-treated per step, not a reconstruction render.
    ${SHANG_STEPS.map((s, i) => `${i + 1} · ${esc(s.title)}`).join("")}
    ${steps}
    3,200 years later

    The same bones, on black

    Every caption on this page is verifiable against the inscriptions — that is why this ritual shipped first.

    What the corpus is

    150,000+ inscribed fragments from Yinxu; 26 bones radiocarbon-dated to 1254–1197 BCE ±10 years. The earliest dated corpus of Chinese writing, and the earliest direct textual evidence for Chinese religious practice.

    What it is not

    It is exclusively royal — commoner religion is an evidential blank (register B15) — and it covers only the last nine Shang kings, roughly the dynasty's final 200 years. “Earliest dated religious writing” does not mean earliest religion: the religion precedes the writing.

    ${src(["K", "T", "C", "E"])}

    ← all walkthroughs · the next three rituals open when their tier-1 gathering lands (register Q31–Q33)

    `; return page("Witness a divination — Anyang, c. 1250 BCE — origins//research", "/rituals/shang-divination", body, "/rituals"); } Bun.serve({ hostname: "0.0.0.0", // principal directive 2026-06-11: LAN-accessible for remote exploring port: PORT, fetch(req) { const url = new URL(req.url); let rel = decodeURIComponent(url.pathname); if (rel.includes("..") || rel.split("/").some(p => p.startsWith("."))) return new Response("forbidden", { status: 403 }); if (rel === "/sitemap.xml") { const base = "https://god.olibuijr.com"; const urls: string[] = [`${base}/${new Date(statSync(join(ROOT, "INDEX.md")).mtimeMs).toISOString().slice(0, 10)}`]; const walk = (d: string, r: string) => { for (const e of readdirSync(d).sort()) { if (e.startsWith(".") || e === "node_modules" || e === "INDEX.md") continue; const a = join(d, e); const st = statSync(a); if (st.isDirectory()) { urls.push(`${base}${r}/${encodeURIComponent(e)}`); walk(a, `${r}/${e}`); } else if (e.endsWith(".md")) urls.push(`${base}${r}/${encodeURIComponent(e)}${new Date(st.mtimeMs).toISOString().slice(0, 10)}`); } }; walk(ROOT, ""); return new Response(`\n\n${urls.join("\n")}\n`, { headers: { "content-type": "application/xml; charset=utf-8" } }); } if (rel === "/api/version") return new Response(JSON.stringify({ v: vaultVersion() }), { headers: { "content-type": "application/json" } }); if (rel === "/api/timeline") return new Response(timelineJson(), { headers: { "content-type": "application/json; charset=utf-8" } }); if (rel === "/api/observatory") return new Response(observatoryJson(), { headers: { "content-type": "application/json; charset=utf-8" } }); if (rel === "/observatory") return observatoryPage(); if (rel === "/pantheon") return pantheonPage(); if (rel === "/timeline") return timelinePage(); if (rel === "/tree") return treePage(); if (rel === "/map") return mapPage(); if (rel === "/scenes" || rel === "/scenes/") return scenesHall(); if (rel === "/scenes/eridu") return sceneEridu(); const sm = rel.match(/^\/scenes\/([a-z-]+)$/); if (sm) return page("404", rel, `

    No such scene

    ${esc(rel)} — only one scene has passed line-by-line licensing so far.

    `); if (rel === "/whisper" || rel === "/whisper/") return whisperPage(); if (rel === "/rituals" || rel === "/rituals/") return ritualsHall(); if (rel === "/rituals/shang-divination") return ritualShang(); const rm = rel.match(/^\/rituals\/([a-z-]+)$/); if (rm) return page("404", rel, `

    No such walkthrough

    ${esc(rel)} — only the Shang divination has passed caption licensing so far; the rest are blocked on gathering (register Q31–Q33).

    `); if (rel === "/voices" || rel === "/voices/") return voicesHall(); const vm = rel.match(/^\/voices\/([a-z-]+)$/); if (vm) { const r = voiceRoom(vm[1]); if (r) return r; return page("404", rel, `

    No such room

    ${esc(rel)} — only two voices are verified so far.

    `); } if (rel === "/" || rel === "/INDEX.md") return home(); const abs = normalize(join(ROOT, rel)); if (!abs.startsWith(ROOT) || !existsSync(abs)) return page("404", rel, `

    Not found

    ${esc(rel)}

    `); if (statSync(abs).isDirectory()) return listDir(abs, rel.replace(/\/$/, "")); if (abs.endsWith(".md")) { const { fm, body } = parseFrontmatter(readFileSync(abs, "utf8")); if (NOTE_TYPES.includes(fm.type)) { return page(fm.title || rel, rel, renderNote(fm, body, rel), "/" + parts0(rel)); } const { html, toc } = mdToHtml(body); const tocHtml = toc.length >= 4 ? `
    On this page${toc.map(t => `${esc(t.text)}`).join("")}
    ` : ""; const banner = illo(rel.split("/").filter(Boolean).pop() ?? ""); return page(fm.title || rel, rel, `
    ${banner}${fmCard(fm)}${tocHtml}${html}
    `, "/" + parts0(rel)); } if (/\.(ts|txt|json|yaml|yml|service)$/.test(abs)) return new Response(readFileSync(abs, "utf8"), { headers: { "content-type": "text/plain; charset=utf-8" } }); return new Response(Bun.file(abs)); }, }); console.log(`religion explorer → http://127.0.0.1:${PORT}`);