#!/usr/bin/env python3
"""Builds the kit report suite: cover + atlas + scar codex + 3 kit command pages.
Light-first (documentation-review surface, per Wolf 2026-07-18); dark via media query.
Data: files.json / scars.json / _purposes.json (mechanical extracts). Regenerate: python3 _build.py"""
import os as _os; _os.chdir(_os.path.dirname(_os.path.abspath(__file__)))
import json, html, re
from collections import Counter

files=json.load(open('files.json')); purposes=json.load(open('_purposes.json')); scars=json.load(open('scars.json'))
try:
    rich=json.load(open('_rich.json'))
except (FileNotFoundError, json.JSONDecodeError):
    rich={}
try:
    enta_desc=json.load(open('_enta-desc.json'))
except (FileNotFoundError, json.JSONDecodeError):
    enta_desc={}
FILE_META={f['path']:f for f in files}
ENTA_META={}
scars.sort(key=lambda s:s['num'])
NR=sum(len(s['rules']) for s in scars); NT=sum(len(s['ties']) for s in scars)
# Counts are DERIVED from data — never hardcode a total that silently goes stale (the "119 vs 121" trap).
NS=len(scars); LAST=scars[-1]['num'] if scars else 0     # scars sorted by num above → [-1] is the highest id
NW=sum(1 for s in scars if s['num']>=114)                # this-window additions (L114+, matches the ERAS tail)
NF=len(files)
ERAS=[(40,'#77848e','L1–40 · foundations'),(80,'#5f87ad','L41–80 · fleets at scale'),(103,'#2f8a99','L81–103 · multi-vendor'),(113,'#0e7a63','L104–113 · observer era'),(10**9,'#c98f0a','L114+ · this window')]
era=lambda n:next((c for hi,c,_ in ERAS if n<=hi), ERAS[-1][1])
GRAD="linear-gradient(90deg,#77848e,#5f87ad,#2f8a99,#0e7a63,#e3a81c)"
ZONES=[('kernel','Kernel','canonical doctrine — machine-loaded YAML','#0e7a63'),
 ('skills','Skills','the entry commands a session invokes','#2f8a99'),
 ('tools','Tools','fleet apparatus copied into _build/','#c98f0a'),
 ('templates','Templates','skeletons instantiated per project','#5f87ad'),
 ('.','Root docs','narrative canon + NotebookLM sources','#8a7a4d'),
 ('examples','Examples','worked instances from live runs','#77848e'),
 ('build-fable-terminal','Sub-kit · build','the fable-terminal build program','#7a5fad'),
 ('pi-fable-terminal','Sub-kit · Pi','the Raspberry-Pi finish program','#ad5f7a')]
KIND={'yaml':('YAML','#0e7a63'),'md':('MD','#5f87ad'),'sh':('SH','#c98f0a'),'py':('PY','#7a5fad'),'json':('JSON','#ad5f7a'),'env':('ENV','#8a7a4d'),'sb':('SB','#77848e')}
kind=lambda p:KIND.get(p.rsplit('.',1)[-1],('FILE','#77848e'))
fsz=lambda n:f"{round(n/1024,1)}KB" if n>=1024 else f"{n}B"
def ampm(m):
    d,t=m.split(); h,mi=t.split(':'); h=int(h)
    return f"{d} · {h%12 or 12}:{mi} {'AM' if h<12 else 'PM'}"

CSS=f"""*{{box-sizing:border-box;margin:0}}
:root{{--bg:#f5f1e8;--sf:#fffdf8;--sf2:#efe9dc;--ink:#20272c;--mut:#66727b;--ln:#ddd3bf;--ac:#0e7a63;--hot:#b57e0a;--hotfill:#e3a81c}}
html[data-theme=dark]{{--bg:#0d1114;--sf:#161c21;--sf2:#1c242a;--ink:#e9e4d8;--mut:#93a0a8;--ln:#26313a;--ac:#17a884;--hot:#f0b429;--hotfill:#f0b429}}
html[data-theme=dark] .card:hover,html[data-theme=dark] .zone:hover,html[data-theme=dark] .ncard:hover,html[data-theme=dark] .cmd:hover{{box-shadow:0 10px 28px rgba(0,0,0,.4)}}
body{{font:15px/1.55 -apple-system,'SF Pro Text',Inter,sans-serif;background:var(--bg);color:var(--ink)}}
a{{color:var(--ac);text-decoration:none}}a:hover{{color:var(--hot)}}
.top{{position:sticky;top:0;z-index:20;background:color-mix(in srgb,var(--sf) 92%,transparent);backdrop-filter:blur(10px);border-bottom:1px solid var(--ln);padding:13px 30px;display:flex;gap:24px;align-items:baseline;flex-wrap:wrap}}
.top b{{font-size:17px}}.top a{{font-weight:650;font-size:14px;color:var(--mut)}}
.top a.here,.top a:hover{{color:var(--ink)}}.top a.here{{border-bottom:2px solid var(--hotfill);padding-bottom:3px}}
.wrap{{max-width:1560px;margin:0 auto;padding:34px 30px 90px}}
.eyebrow{{font:700 12px/1 ui-monospace,Menlo,monospace;letter-spacing:.22em;color:var(--hot);text-transform:uppercase}}
h1{{font-size:clamp(38px,5.5vw,68px);font-weight:800;letter-spacing:-.03em;line-height:1.04;margin:10px 0 4px}}
.rule{{height:5px;border-radius:3px;background:{GRAD};background-size:220% 100%;animation:drift 9s ease-in-out infinite alternate;margin:16px 0 8px}}
@keyframes drift{{to{{background-position:100% 0}}}}
.tag{{color:var(--mut);font-size:16.5px}}
.stats{{display:flex;gap:14px;flex-wrap:wrap;margin:26px 0}}
.stat{{background:var(--sf);border:1px solid var(--ln);border-radius:14px;padding:15px 22px;min-width:120px}}
.stat b{{display:block;font-size:32px;font-weight:800}}.stat span{{color:var(--mut);font-size:11.5px;text-transform:uppercase;letter-spacing:.1em}}
h2{{margin:38px 0 6px;font-size:22px}}h2 .sub{{font-weight:400;color:var(--mut);font-size:14px;margin-left:12px}}
.strip{{display:flex;flex-wrap:wrap;gap:3px;background:var(--sf);border:1px solid var(--ln);border-radius:14px;padding:18px}}
.tick{{width:9px;height:38px;border-radius:2px;cursor:pointer;transform-origin:bottom;transition:transform .18s}}
.tick:hover{{transform:scaleY(1.25)}}
.legend{{display:flex;gap:10px;flex-wrap:wrap;margin:12px 0 0}}
.lg{{display:inline-flex;gap:7px;align-items:center;background:var(--sf);border:1px solid var(--ln);border-radius:8px;padding:4px 12px;font-size:12.5px;color:var(--mut)}}
.lg i{{width:11px;height:11px;border-radius:3px;display:inline-block}}
.navcards{{display:grid;grid-template-columns:repeat(auto-fit,minmax(380px,1fr));gap:20px;margin:24px 0}}
.ncard{{background:var(--sf);border:1px solid var(--ln);border-radius:18px;padding:26px;display:block;color:var(--ink)}}
.ncard h3{{font-size:22px;margin-bottom:6px}}.ncard p{{color:var(--ink);opacity:.85;font-size:14px;line-height:1.6}}
.ncard code{{font:600 12px ui-monospace,monospace;background:color-mix(in srgb,var(--ink) 8%,transparent);border-radius:4px;padding:0 5px}}
.ncard .go{{display:inline-block;margin-top:14px;font:700 13px ui-monospace,monospace;color:var(--hot)}}
.card,.zone,.ncard,.stat,.cmd{{transition:transform .22s,box-shadow .22s,border-color .22s}}
.card:hover,.zone:hover,.ncard:hover,.cmd:hover{{transform:translateY(-3px);border-color:var(--ac);box-shadow:0 10px 28px rgba(60,50,20,.14)}}
.atlas{{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:18px;margin:20px 0}}
.zone{{background:var(--sf);border:1px solid var(--ln);border-left:5px solid var(--zc,var(--ac));border-radius:14px;padding:18px 20px}}
.zone h3{{font-size:15px;letter-spacing:.06em;text-transform:uppercase;color:var(--zc)}}
.zone .zn{{color:var(--mut);font-size:12.5px;margin:3px 0 12px}}
.chips{{display:flex;flex-wrap:wrap;gap:7px}}
.chip{{border-radius:7px;color:#fff;padding:3px 10px;font:700 11.5px/1.7 ui-monospace,monospace;cursor:pointer;white-space:nowrap}}
table{{border-collapse:collapse;width:100%;background:var(--sf);border:1px solid var(--ln);border-radius:12px;overflow:hidden;font-size:13.5px}}
th{{background:var(--sf2);text-align:left;padding:9px 14px;font-size:11px;text-transform:uppercase;letter-spacing:.09em;color:var(--mut)}}
td{{padding:9px 14px;border-top:1px solid var(--ln);vertical-align:top}}
td.path{{font:650 12.5px ui-monospace,monospace;white-space:nowrap}}
td.n{{text-align:right;white-space:nowrap;font:12.5px ui-monospace,monospace;color:var(--mut)}}
tr:target td{{background:color-mix(in srgb,var(--ac) 14%,transparent)}}
.tbl{{overflow-x:auto;margin-bottom:14px;border-radius:12px}}
.badge{{display:inline-block;border-radius:5px;color:#fff;padding:0 7px;font:800 10px/1.9 ui-monospace,monospace;margin-right:8px}}
.searchbar{{position:sticky;top:52px;z-index:10;background:var(--bg);padding:12px 0}}
.searchbar input{{width:100%;max-width:620px;padding:12px 18px;border:1.5px solid var(--ln);border-radius:12px;font-size:15px;background:var(--sf);color:var(--ink)}}
.searchbar input:focus{{outline:none;border-color:var(--ac)}}
.card{{background:var(--sf);border:1px solid var(--ln);border-left:5px solid var(--ec,var(--ac));border-radius:14px;padding:18px 22px;margin:14px 0}}
.card:target{{outline:2px solid var(--hotfill)}}
.card h3{{font-size:16px}}.card .lid{{font:800 13px ui-monospace,monospace;color:var(--ec);margin-right:9px}}
.card .note{{color:var(--mut);font-size:12.5px;margin:3px 0 8px}}
summary{{cursor:pointer;font-weight:650;color:var(--mut);font-size:13px}}
.inc{{color:var(--ink);opacity:.85;font-size:13.5px;margin-top:6px;border-left:3px solid var(--hotfill);padding-left:13px}}
.card ol{{margin:8px 0 4px 22px;font-size:13.5px}}.card ol li{{margin:6px 0}}
.ties{{margin-top:9px}}.tie{{display:inline-block;background:color-mix(in srgb,var(--ac) 14%,transparent);color:var(--ac);border-radius:6px;padding:1px 9px;font:700 11.5px ui-monospace,monospace;margin:2px 5px 0 0}}
.rise{{opacity:0;transform:translateY(14px)}}.rise.in{{opacity:1;transform:none;transition:opacity .5s,transform .5s}}
.tick{{opacity:0;transform:scaleY(0)}}.tick.in{{opacity:1;transform:scaleY(1);transition:opacity .3s,transform .35s}}
/* command pages */
.cmds{{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;margin:22px 0}}
.cmd{{background:var(--sf);border:1px solid var(--ln);border-radius:14px;padding:18px 20px;cursor:pointer}}
.cmd.sel{{border-color:var(--ac);box-shadow:0 0 0 2px color-mix(in srgb,var(--ac) 35%,transparent)}}
.cmd h3{{font:800 17px ui-monospace,monospace;color:var(--ac)}}
.cmd p{{color:var(--mut);font-size:13px;margin-top:5px}}
.seqpanel{{background:var(--sf);border:1px solid var(--ln);border-radius:16px;padding:24px;margin:10px 0 30px;min-height:170px}}
.seqtitle{{display:flex;align-items:baseline;gap:14px;margin-bottom:18px}}
.seqtitle b{{font:800 19px ui-monospace,monospace;color:var(--ac)}}
.seqtitle span{{color:var(--mut);font-size:13px}}
.replay{{margin-left:auto;font:700 12px ui-monospace,monospace;color:var(--hot);cursor:pointer;border:1px solid var(--ln);border-radius:8px;padding:5px 12px;background:var(--sf2)}}
.rail{{display:flex;align-items:stretch;gap:0;overflow-x:auto;padding:8px 2px 14px}}
.node{{min-width:168px;max-width:200px;background:var(--sf2);border:1.5px solid var(--ln);border-radius:12px;padding:12px 14px;opacity:0;transform:translateX(-14px) scale(.94)}}
.node.on{{opacity:1;transform:none;transition:opacity .34s,transform .34s;border-color:var(--nc,var(--ac))}}
.node.fire{{box-shadow:0 0 0 3px color-mix(in srgb,var(--nc,var(--ac)) 40%,transparent)}}
.node .st{{font:800 10.5px ui-monospace,monospace;color:var(--nc,var(--ac));letter-spacing:.08em}}
.node b{{display:block;font-size:13.5px;margin:3px 0 2px}}
.node code{{font:600 10.5px ui-monospace,monospace;color:var(--mut);word-break:break-all}}
.conn{{width:34px;flex:0 0 34px;display:flex;align-items:center}}
.conn i{{height:2.5px;background:var(--nc,var(--ac));width:100%;display:block;transform:scaleX(0);transform-origin:left;border-radius:2px}}
.conn i.on{{transform:scaleX(1);transition:transform .3s}}
.fileatlas{{display:grid;grid-template-columns:repeat(auto-fit,minmax(420px,1fr));gap:20px;margin:20px 0}}
.filezone{{background:var(--sf);border:1px solid var(--ln);border-left:5px solid var(--zc,var(--ac));border-radius:14px;padding:18px 20px}}
.filezone h3{{font-size:15px;letter-spacing:.06em;text-transform:uppercase;color:var(--zc)}}
.filezone .zn{{color:var(--mut);font-size:12.5px;margin:3px 0 12px}}
.filerich{{background:var(--sf2);border:1px solid var(--ln);border-left:3px solid var(--fc,var(--ac));border-radius:11px;padding:13px 15px;margin:10px 0;transition:transform .18s,border-color .18s,box-shadow .18s}}
.filerich:hover{{transform:translateY(-2px);border-color:var(--fc,var(--ac));box-shadow:0 6px 16px rgba(60,50,20,.1)}}
.filerich .fhead{{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}}
.filerich .fpath{{font:700 12.5px ui-monospace,monospace;overflow-wrap:anywhere;color:var(--ink)}}
.filerich .fmeta{{margin-left:auto;color:var(--mut);font:11px ui-monospace,monospace;white-space:nowrap}}
.filerich p{{font-size:13.5px;line-height:1.5;margin-top:8px}}
.filerich .fexample{{color:var(--mut);font-size:12.5px;font-style:italic;margin-top:6px}}
.facrs{{display:flex;flex-wrap:wrap;gap:5px;margin-top:9px}}
.facr{{display:inline-block;background:color-mix(in srgb,var(--ac) 13%,transparent);color:var(--ac);border:1px solid color-mix(in srgb,var(--ac) 25%,transparent);border-radius:6px;padding:3px 7px;font-size:11px;line-height:1.35}}
@media(prefers-reduced-motion:reduce){{.rise,.tick{{opacity:1!important;transform:none!important;transition:none!important}}.rule{{animation:none}}.card:hover,.zone:hover,.ncard:hover,.cmd:hover{{transform:none}}.node,.conn i{{opacity:1!important;transform:none!important;transition:none!important}}}}
footer{{margin-top:60px;color:var(--mut);font-size:12.5px;border-top:1px solid var(--ln);padding-top:18px;font-family:ui-monospace,monospace}}"""

JS="""const th=localStorage.getItem('kit-theme')||'light';document.documentElement.dataset.theme=th;
const tb=document.getElementById('themetoggle');const setIcon=()=>tb.textContent=document.documentElement.dataset.theme==='light'?'☾':'☀';setIcon();
tb.addEventListener('click',e=>{e.preventDefault();const n=document.documentElement.dataset.theme==='light'?'dark':'light';document.documentElement.dataset.theme=n;localStorage.setItem('kit-theme',n);setIcon()});const io=new IntersectionObserver(es=>es.forEach(e=>{if(e.isIntersecting){e.target.style.transitionDelay=(e.target.dataset.i%14*38)+'ms';e.target.classList.add('in');io.unobserve(e.target)}}),{threshold:.06});
document.querySelectorAll('.rise,.tick').forEach((el,i)=>{el.dataset.i=i;io.observe(el)});
const cnt=el=>{const t=+el.dataset.n,mr=matchMedia('(prefers-reduced-motion: reduce)').matches;if(mr){el.textContent=t.toLocaleString();return}
const t0=performance.now(),step=n=>{const p=Math.min(1,(n-t0)/900),e=1-Math.pow(1-p,3);el.textContent=Math.round(t*e).toLocaleString();if(p<1)requestAnimationFrame(step)};requestAnimationFrame(step)};
document.querySelectorAll('[data-n]').forEach(cnt);
const F=()=>{const q=(document.getElementById('q')||{}).value?.toLowerCase()||'';document.querySelectorAll('.card[data-t]').forEach(c=>c.style.display=!q||c.dataset.t.includes(q)?'':'none')};"""

SEQJS="""let TIMERS=[];
const play=cmd=>{
  TIMERS.forEach(clearTimeout);TIMERS=[];
  const seq=SEQS[cmd];if(!seq)return;
  document.querySelectorAll('.cmd').forEach(c=>c.classList.toggle('sel',c.dataset.cmd===cmd));
  const p=document.getElementById('seqpanel');
  p.querySelector('.seqtitle b').textContent=cmd;
  p.querySelector('.seqtitle span').textContent=seq.tag;
  const rail=document.getElementById('rail');
  rail.innerHTML=seq.steps.map((s,i)=>
    `<div class=node style="--nc:${s[2]||'var(--ac)'}"><span class=st>STEP ${i+1}</span><b>${s[0]}</b><code>${s[1]}</code></div>`+
    (i<seq.steps.length-1?`<div class=conn style="--nc:${s[2]||'var(--ac)'}"><i></i></div>`:'')
  ).join('');
  const mr=matchMedia('(prefers-reduced-motion: reduce)').matches;
  const nodes=[...rail.querySelectorAll('.node')],conns=[...rail.querySelectorAll('.conn i')];
  if(mr){nodes.forEach(n=>n.classList.add('on'));conns.forEach(c=>c.classList.add('on'));return}
  nodes.forEach((n,i)=>{
    TIMERS.push(setTimeout(()=>{n.classList.add('on','fire');n.scrollIntoView({inline:'nearest',block:'nearest',behavior:'smooth'});
      TIMERS.push(setTimeout(()=>n.classList.remove('fire'),340));
      if(conns[i])TIMERS.push(setTimeout(()=>conns[i].classList.add('on'),200));
    },i*380));
  });
};
document.querySelectorAll('.cmd').forEach(c=>c.addEventListener('click',()=>play(c.dataset.cmd)));
document.querySelector('.replay').addEventListener('click',()=>{const s=document.querySelector('.cmd.sel');if(s)play(s.dataset.cmd)});
const first=document.querySelector('.cmd');if(first)play(first.dataset.cmd);"""

K='#0e7a63';S='#2f8a99';T='#c98f0a';P='#5f87ad'  # kernel/skill/tool/template node colors
SEQ={
"/begin":{"tag":"boot a fleet — intake → apparatus → pilot → fan-out → audit","steps":[
 ["Locate + load kernel","kernel/principles.yaml → lessons.yaml → domains.yaml",K],
 ["Gauge context first","templates/ctx-check.sh · L113: never boot deep",P],
 ["Intake (blocking)","kernel/intake.yaml — batch or idea-stage dialogue",K],
 ["Executor prep","kernel/executors.yaml prep_checklist + auth proof",K],
 ["Pool preflight","ROUTING.md — rungs named, probe via tools/dispatch.sh (L115/L116)",T],
 ["Apparatus into _build/","tools/* + templates/worker-prompt.md instantiated",T],
 ["Pilot: one unit","tools/gate.sh — end-to-end before any fan-out (P4)",T],
 ["Fan-out in waves","tools/pool.sh — 3-4 workers, INTEGRITY clause",T],
 ["Watch instruments","tools/watch.sh — exception + heartbeat + progress",T],
 ["Done-audit","tools/audit-done.sh + dogfood-verify.sh — no marker unaudited (L1)",T],
 ["Complete + harvest","scars → PROV-* → canonical harvests (L34)",K]]},
"/continue":{"tag":"empirical re-entry — the disk is the source of truth","steps":[
 ["Classify re-entry","_build/project.env? fleet : ordinary",T],
 ["Activate kit (L97)","kernel/lessons.yaml → named ACTIVE guards, not just loaded",K],
 ["Reconstruct state","tools/assess.sh · progress.sh · delegated reads (CT1)",T],
 ["Diagnose buckets","RUNNING / DEAD / BLOCKED / DONE-CLAIMED / NOT-STARTED",K],
 ["Ask only unknowns","AskUserQuestion — batched, never what disk answered",S],
 ["Propose + execute","tools/inject.sh · pool.sh relaunch · audit-done.sh",T],
 ["Re-arm monitors","strict signatures + learned noise exclusions (L8/L17)",T]]},
"/handoff":{"tag":"scoped resumable handoff — re-entry becomes one word","steps":[
 ["Pick the scope token","one word — the project thread being parked",S],
 ["Write the card","_handoff/<token>.md — state, watchers, next moves",S],
 ["Pin the re-arm drill","detached processes die with the session (L100)",K],
 ["End with the resume line","'resume: /continue <token>' — literal",S]]},
"/learn":{"tag":"the standing observer — watch everything, harvest scars, ship","steps":[
 ["Arm watchers","tools/claude-all-watch.py --self <sid> · codex · macB · inbox",T],
 ["Observe the loop","HUMAN + OUTCOME + APIERROR lines from every other session",T],
 ["Detect a lesson","a correction, a failure class, a doctrine gap",K],
 ["Harvest to canonical","kernel/lessons.yaml — next L-id, single writer (L96)",K],
 ["Encode into apparatus","skills/ + tools/ + templates updated to enforce it",S],
 ["Ship fleet-wide","scripts/ship-kit.sh — L34 guard → 16 mirrors + 2 zips",T]]},
"/dogfood":{"tag":"a completion earns a human walk plan","steps":[
 ["Take the completion","gate green is not used (L12/L28)",K],
 ["Author DOGFOOD.md","a concrete self-contained manual walk",S],
 ["Human clicks through","defects surface where tests cannot see",S],
 ["Defects → tickets","fed back into the fleet's backlog",T]]},
"/dogfood-it":{"tag":"the agent drives the product itself, evidence-backed","steps":[
 ["Load the walk plan","DOGFOOD.md from /dogfood",S],
 ["Drive as a real user","browser / CLI / API — the actual product",T],
 ["Evidence per step","screenshots, outputs, timings — no vibes (L92/L95)",K],
 ["Verdicts + report","pass/fail per step; front-runs the human, never replaces",S]]},
"/small-window":{"tag":"tight-context discipline for small or fast-capping windows","steps":[
 ["Enter the mode","kernel/small-window.yaml — the discipline set",K],
 ["Thrift everything","delegate reads, cap outputs, scoped handoffs early",K],
 ["Exit or hand off","/small-window off — or park via /handoff",S]]},
"/skin-it":{"tag":"motion-forward design skins — safe / bold / wildcard","steps":[
 ["Parse subject + scope","whole site, or one surface (login, dashboard…)",S],
 ["Ground the product","purpose · domain · buyer · emotion — never the name",K],
 ["Consult the corpus","skin-library.yaml — 20 families · 16 motion patterns · 21 surfaces",S],
 ["Shortlist by domain","category_index — engagement-first domains lead motion-forward",S],
 ["Propose three skins","safe fit · bold · cross-pollinated wildcard + motion system",S],
 ["Hand off buildable","copy-paste :root tokens → wireframes / frontend-design",S]]},
"/pi":{"tag":"the hardware lane — Pi deploy, raspbian facts, on-device proof","steps":[
 ["Read the estate spec","pi-fable-terminal/PI_SPEC.yaml + SETUP.yaml",K],
 ["Check the fact bank","pi-fable-terminal/RASPBIAN_KNOWLEDGE.yaml — before any config change",K],
 ["Reach the device","kernel/machines.yaml — ssh wolf@100.79.97.3 (L93 host-grounding)",K],
 ["Probe on-device state","display/GPIO/storage/services — cheap probes before plans (L81)",T],
 ["Act in small steps","verify each on the DEVICE — config edits are claims",T],
 ["Bank what was hard-won","RASPBIAN_KNOWLEDGE.yaml += the new fact → /ship",K]]},
"/machines":{"tag":"reach, health, roles — resolve through the registry, never fish","steps":[
 ["Resolve via registry","kernel/machines.yaml — the resolution ladder",K],
 ["Verify reachability","ssh -o ConnectTimeout=8 — a timeout is a FINDING",T],
 ["Ground commands to hosts","preflight remote paths; non-interactive PATH differs (L93)",K],
 ["Report blind spots","unreachable ≠ clean — surface it",K],
 ["Registry drift → update","machines.yaml edited → /ship",K]]},
"/ship":{"tag":"canonical → 16 mirrors + 2 zips, L34-guarded","steps":[
 ["Validate the change","YAML parses · scar count/max as expected — a broken kernel ships fleet-wide",K],
 ["L34 harvest check","local scars in any mirror ABORT the ship — harvest first",K],
 ["Mirror everything","scripts/ship-kit.sh — auto-discovered copies, notes preserved",T],
 ["Rebuild both editions","v1 full · v2 orchestration (god-only skills excluded)",T],
 ["Verify independently","spot-check a mirror + grep the zips — never trust the run alone (P1)",K],
 ["Deploy live skills","cp → ~/.claude/skills — mirrors don't feed running sessions",S]]},
"/harvest":{"tag":"one scar, written right — id, incident, rules, ties, encode, ship","steps":[
 ["Single-writer check","L96 — only the standing harvester allocates ids",K],
 ["Verify the facts","L119 — every interface claim checked against the installed version",K],
 ["Allocate next L-id","max existing + 1 — never guess, never reuse",K],
 ["Write the scar","incident (the story) + rules (standalone imperatives) + ties (verified)",K],
 ["Encode into apparatus","a skill step / tool guard / template line — not just prose",S],
 ["Parse-check + /ship","count moved by exactly one → fleet-wide",T]]},
"/verdict":{"tag":"is this claim true per our canon? — the notebook decides, not the claimant","steps":[
 ["Freeze the claim","one decidable sentence, source named",K],
 ["Check notebook freshness","sources match the frozen kit state (NOTEBOOKLM_PROMPTS sequencing)",K],
 ["Ask the grounded reader","tools/verdict.sh / the notebook — answers ONLY from sources",T],
 ["Record verdict + passage","the canon line it rests on",K],
 ["Canon wrong or silent?","that is a finding → /harvest the correction",K]]},
"/guardrails":{"tag":"a rule that matters is a check that fails the build","steps":[
 ["Name the invariant","one falsifiable sentence",K],
 ["Pick the G-class","domains.yaml guardrail_candidates — or mint a new G-id",K],
 ["Write the failing check","exits nonzero on violation, wired into the gate",T],
 ["Prove RED then GREEN","violate it once deliberately — an unproven gate is decoration",T],
 ["Install in the project","templates/guardrails.yaml instantiated + registered",P],
 ["New class worth keeping?","/harvest + /ship",K]]},
"/drift":{"tag":"see divergence, classify it, act correctly — resync vs harvest-first vs promote","steps":[
 ["Run the scan","_drift-scan.py → drift.json → Kit Drift page",T],
 ["Classify each copy","CLEAN · BEHIND · LOCAL-SCARS · derived (entabeni)",K],
 ["BEHIND → resync","/ship — and ask why discovery missed it",T],
 ["LOCAL-SCARS → harvest FIRST","L34: the resync itself is the clobber — order is everything",K],
 ["Derived kits → review","EL changes watched; promotion EL→L is deliberate",K],
 ["Re-scan to confirm","drift found ≠ drift resolved",T]]},
"/deploy":{"tag":"read a project, then a deployment-strategy questionnaire → a plan","steps":[
 ["Read the project","<path> — stack, services, existing deploy config, envs, persistence (P1)",T],
 ["Map the topology","web · api · worker · db · redis — and their contracts",K],
 ["Confirm the shape","summarize back in 3-5 lines before any question",S],
 ["Strategy questionnaire","platform · services · domain/TLS · envs · rollback · gate+probes (P12)",S],
 ["Auth gate check","no auth layer on a privileged surface = a BLOCKER, not a footnote (L114)",K],
 ["Produce the plan","ordered steps + env matrix + rollback + post-deploy probe battery",T]]},
}
GOD_CMDS=["/begin","/continue","/handoff","/learn","/dogfood","/dogfood-it","/small-window","/skin-it","/pi","/machines","/ship","/harvest","/verdict","/guardrails","/drift","/deploy"]
ORCH_CMDS=["/begin","/continue","/handoff","/learn","/dogfood","/dogfood-it","/small-window","/skin-it"]
ENTA_CMDS=["/begin","/continue","/dogfood","/dogfood-it"]
# More derived counts (the lists exist now): zones, commands, skills. PRINCIPLES is external (not loaded here).
NZ=len(ZONES); NGC=len(GOD_CMDS); NOC=len(ORCH_CMDS)
NSKILL=len({f['dir'].split('/')[1] for f in files if f['dir'].startswith('skills/') and '/' in f['dir']})
NORCH=sum(1 for line in open('_orch-files.txt') if line.strip()) if _os.path.exists('_orch-files.txt') else 78
PRINCIPLES=12  # kernel/principles.yaml — not loaded here; bump on change

NAV=lambda here:f"""<div class=top><b>◈ think-like-fable</b>
<a class="{'here' if here=='i' else ''}" href="index.html">Cover</a>
<a class="{'here' if here=='a' else ''}" href="kit-atlas.html">Kit Atlas</a>
<a class="{'here' if here=='s' else ''}" href="scar-codex.html">Scar Codex</a>
<a class="{'here' if here=='g' else ''}" href="god-kit.html">God Kit</a>
<a class="{'here' if here=='o' else ''}" href="orchestration-kit.html">Orchestration</a>
<a class="{'here' if here=='e' else ''}" href="entabeni-kit.html">Entabeni</a>
<a class="{'here' if here=='d' else ''}" href="kit-drift.html">Drift</a>
<a href="/">← Observify</a>
<a href="#" id=themetoggle title="toggle light/dark">☾</a>
<span style="margin-left:auto;color:var(--mut);font:12px ui-monospace,monospace">frozen July 18, 2026 · {NS} scars</span></div>"""
H=lambda t:f"<meta charset=utf-8><meta name=viewport content='width=device-width,initial-scale=1'><title>{t}</title><style>{CSS}</style>"
stat=lambda n,l:f"<div class='stat rise'><b data-n={n}>0</b><span>{l}</span></div>"
FOOT="<footer>canonical: ~/Projects/holt-ecosystem-packets/think-like-fable · mirrors: 15 × ~/Projects · derived instances: entabeni + terminus · editions: v1 full / v2 orchestration · data: files.json · scars.json (mechanically extracted from kernel/lessons.yaml — zero paraphrase)</footer>"

def rich_of(path, enta=False):
    if enta and path in enta_desc:
        return enta_desc[path]
    if path in rich:
        return rich[path]
    meta=(ENTA_META if enta else FILE_META).get(path,{})
    return {'what':purposes.get(path,'') or meta.get('head',''),'example':'','acronyms':{}}

def file_atlas(paths, enta=False):
    zone_defs=ZONES + ([
        ('profiles','Profiles','per-repo engagement maps read before a worker acts','#c0392b'),
        ('reference','Reference','engagement knowledge base and grounded source material','#8a7a4d')
    ] if enta else [])
    grouped={z:[] for z,_,_,_ in zone_defs}
    for path in paths:
        if not path:
            continue
        zone='.' if '/' not in path else path.split('/',1)[0]
        grouped.setdefault(zone,[]).append(path)
    metadata=ENTA_META if enta else FILE_META
    sections=[]
    for zone,label,desc,color in zone_defs:
        zone_paths=sorted(grouped.get(zone,[]))
        if not zone_paths:
            continue
        cards=[]
        for path in zone_paths:
            info=rich_of(path,enta)
            what=html.escape(str(info.get('what','')))
            example=html.escape(str(info.get('example','')))
            acronyms=info.get('acronyms') or {}
            chips=''.join(f"<span class=facr>{html.escape(str(acr))} — {html.escape(str(definition))}</span>" for acr,definition in acronyms.items())
            meta=metadata.get(path,{})
            meta_bits=[]
            if meta.get('size') is not None:
                meta_bits.append(fsz(meta['size']))
            if meta.get('mtime'):
                meta_bits.append(ampm(meta['mtime']))
            meta_html=f"<span class=fmeta>{html.escape(' · '.join(meta_bits))}</span>" if meta_bits else ''
            example_html=f"<div class=fexample>e.g. {example}</div>" if example else ''
            acronyms_html=f"<div class=facrs>{chips}</div>" if chips else ''
            cards.append(f"""<article class=filerich style="--fc:{kind(path)[1]}">
<div class=fhead><span class=badge style="background:{kind(path)[1]}">{kind(path)[0]}</span><span class=fpath>{html.escape(path)}</span>{meta_html}</div>
<p>{what}</p>{example_html}{acronyms_html}</article>""")
        sections.append(f"<section class=filezone style='--zc:{color}'><h3>{label}</h3><div class=zn>{len(zone_paths)} files — {desc}</div>{''.join(cards)}</section>")
    return f"<div class=fileatlas>{''.join(sections)}</div>"

def kit_page(fname,here,title,eyebrow,desc,cmds,extra="",file_paths=None,enta=False):
    cards=''.join(f"""<div class="cmd rise" data-cmd="{c}"><h3>{c}</h3><p>{html.escape(SEQ[c]['tag'])}</p></div>""" for c in cmds)
    seqs=json.dumps({c:SEQ[c] for c in cmds})
    page=f"""{H(title)}{NAV(here)}<div class=wrap>
<div class=eyebrow>{eyebrow}</div><h1>{title}</h1><div class=rule></div>
<div class=tag>{desc}</div>{extra}
<h2>Commands<span class=sub>click a command to watch its firing sequence</span></h2>
<div class=cmds>{cards}</div>
<div class=seqpanel id=seqpanel><div class=seqtitle><b></b><span></span><span class=replay>↻ replay</span></div>
<div class=rail id=rail></div></div>
<h2>Every file<span class=sub>what it is · an example · acronyms defined</span></h2>
{file_atlas(file_paths or [],enta)}
{FOOT}</div><script>const SEQS={seqs};{JS}{SEQJS}</script>"""
    open(fname,'w').write(page); return len(page)

god_extra="""<div class=stats>""" + stat(NF,'files')+stat(NS,'L-scars')+stat(NGC,f'commands · {NGC-NOC} god-only')+stat(PRINCIPLES,'principles')+"""</div>
<p style="max-width:900px;color:var(--ink);opacity:.85;line-height:1.65"><b>The master.</b> Canonical at <code style="font:600 12px ui-monospace,monospace">~/Projects/holt-ecosystem-packets/think-like-fable</code>. Single-writer governance: only the standing harvester allocates L-ids and ships; Wolf greenlights revisions. Every mirror is an exact rsync via <code style="font:600 12px ui-monospace,monospace">ship-kit.sh</code> (L34-guarded). Truth accumulates here first.</p>"""
orch_extra="""<div class=stats>"""+stat(NORCH,'files in v2 zip')+stat(NS,'same scars')+stat(NOC,'run-a-fleet commands')+"""</div>
<p style="max-width:900px;color:var(--ink);opacity:.85;line-height:1.65"><b>The deployable edition — not a fork.</b> Regenerated from the god kit by <code style="font:600 12px ui-monospace,monospace">build-kit-v2.sh</code> on every ship; never edited directly, so it cannot drift. Strips the Holt narrative, NotebookLM sources, and sub-kits down to kernel + skills + tools + templates: what a fresh engagement needs to boot a fleet. Same commands, same scars, same doctrine.</p>"""
enta_extra="""<div class=stats>"""+stat(25,'EL-scars (own id-space)')+stat(4,'commands (subset)')+stat(6,'engagement profiles')+stat(672,'KB on Mac B')+"""</div>
<p style="max-width:900px;color:var(--ink);opacity:.85;line-height:1.65"><b>The first derived instance — proof the method generalizes.</b> Lives on Mac B at <code style="font:600 12px ui-monospace,monospace">~/Desktop/EntabeniRepos/think-like-entabeni</code>, serving the Entabeni engagement (POS-POC, lodging, ecommerce, CNS-PWA…). Independent scar id-space: <b>25 EL-ids, zero L-ids</b> — lessons live there unless deliberately promoted EL→L. Runs a <b>4-command subset</b> (no /learn — the god kit's harvester watches it instead; no /skin-it — design isn't its lane) plus <b>6 engagement profiles</b> (<code style="font:600 12px ui-monospace,monospace">profiles/</code>: entabeni-api, entabeni-apps, ecommerce, ecommerce-LODGE-POC, lodging, CNS-PWA, customer-success-tool) that ground each repo's workers. Small-window mode is <b>pinned as a requirement</b> — Mac B runs tight-context by design, where the god kit treats it as optional.</p>"""

# ---- cover ----
ticks=''.join(f"""<a class=tick style="background:{era(s['num'])}" title="L{s['num']} — {html.escape(s['name'][:70])}" href="scar-codex.html#L{s['num']}"></a>""" for s in scars)
legend=''.join(f"<span class=lg><i style='background:{c}'></i>{lbl}</span>" for _,c,lbl in ERAS)
last6=' '.join(f"<span class=tie>L{s['num']}</span>" for s in scars[-6:])
kits_section=f"""
<h2>One method, three kits<span class=sub>click a kit to see its commands fire</span></h2>
<div class=navcards>
<a class="ncard rise" href="god-kit.html" style="border-left:5px solid {K}">
<h3>⬢ The God Kit <span style="font:700 11px ui-monospace,monospace;color:{K}">CANONICAL</span></h3>
<p>The master at <code>~/Projects/holt-ecosystem-packets/think-like-fable</code> — {NF} files, <b>{NS} L-scars</b>, {PRINCIPLES} principles, {NGC} commands, the narrative canon, NotebookLM sources, two sub-kits. Single-writer: the harvester allocates L-ids, Wolf greenlights, <code>ship-kit.sh</code> mirrors it everywhere (L34-guarded). Truth accumulates here first.</p><span class=go>commands + firing order →</span></a>
<a class="ncard rise" href="orchestration-kit.html" style="border-left:5px solid {P}">
<h3>⬡ The Orchestration Kit <span style="font:700 11px ui-monospace,monospace;color:{P}">EDITION · v2 ZIP</span></h3>
<p>The deployable subset — regenerated from canonical by <code>build-kit-v2.sh</code> on every ship, never hand-edited, so it <b>cannot drift</b>. Kernel + skills + tools + templates (78 files): what a fresh engagement drops in to boot a fleet with <code>/begin</code>. Same scars, same doctrine — an edition, not a fork.</p><span class=go>commands + firing order →</span></a>
<a class="ncard rise" href="entabeni-kit.html" style="border-left:5px solid {T}">
<h3>⬠ The Entabeni Kit <span style="font:700 11px ui-monospace,monospace;color:{T}">FIRST DERIVED INSTANCE</span></h3>
<p>On Mac B at <code>~/Desktop/EntabeniRepos/think-like-entabeni</code> — derived for the Entabeni engagement. Own id-space (<b>25 EL-scars, zero L-ids</b>; promotion EL→L is deliberate), a 4-command subset, <b>6 engagement profiles</b> grounding each repo, and small-window mode <b>pinned</b>. Proof the method generalizes.</p><span class=go>commands + firing order →</span></a>
</div>"""
index=f"""{H('think-like-fable — Kit Report')}{NAV('i')}<div class=wrap>
<div class=eyebrow>production orchestration kit · frozen july 18, 2026 · review edition</div>
<h1>think-like-fable</h1><div class=rule></div>
<div class=tag>orchestrator = judgment · workers = tokens — the full kit, mapped and scarred, for review</div>
<div class=stats>{stat(NF,'files')}{stat(NZ,'zones')}{stat(NS,'scars')}{stat(NR,'rules')}{stat(NT,'tie links')}{stat(NSKILL,'skills')}{stat(PRINCIPLES,'principles')}</div>
{kits_section}
<h2>The scar record<span class=sub>one tick per lesson, L1 → L{LAST} — click any tick to open it</span></h2>
<div class=strip>{ticks}</div><div class=legend>{legend}</div>
<div class=navcards>
<a class="ncard rise" href="kit-atlas.html"><h3>⬛ Kit Atlas</h3><p>Every file — what it does, where it lives, how heavy it is. 8 zones, chip map + full scrutiny tables.</p><span class=go>open atlas →</span></a>
<a class="ncard rise" href="scar-codex.html"><h3>▦ Scar Codex</h3><p>All {NS} lessons verbatim from the kernel — incident, every rule, every tie. Filterable. Latest: {last6}</p><span class=go>open codex →</span></a>
</div>{FOOT}</div><script>{JS}</script>"""
open('index.html','w').write(index)

# ---- atlas ----
by=lambda z:[f for f in files if (f['dir'].split('/')[0]==z) or (z=='.' and f['dir']=='.')]
zones=[];tables=[]
for z,t,d,zc in ZONES:
    fs=sorted(by(z),key=lambda f:-f['size'])
    chips=''.join(f"""<span class=chip style="background:{kind(f['path'])[1]};font-size:{max(10,min(15,9+f['size']//9000))}px" onclick="location.hash='f{abs(hash(f['path']))%99999}'" title="{html.escape(purposes.get(f['path'],''))}">{html.escape(f['path'].split('/')[-1])}</span>""" for f in fs)
    zones.append(f"<div class='zone rise' style='--zc:{zc}'><h3>{t}</h3><div class=zn>{len(fs)} files · {sum(f['size'] for f in fs)//1024}KB — {d}</div><div class=chips>{chips}</div></div>")
    rows=''.join(f"""<tr id=f{abs(hash(f['path']))%99999}><td class=path><span class=badge style="background:{kind(f['path'])[1]}">{kind(f['path'])[0]}</span>{html.escape(f['path'])}</td><td>{html.escape(purposes.get(f['path'],f['head']))}</td><td class=n>{fsz(f['size'])}</td><td class=n>{ampm(f['mtime'])}</td></tr>""" for f in fs)
    tables.append(f"<h2 style='color:{zc}'>{t}<span class=sub>{d}</span></h2><div class=tbl><table><tr><th>File</th><th>What it does</th><th>Size</th><th>Modified</th></tr>{rows}</table></div>")
atlas=f"""{H('Kit Atlas — think-like-fable')}{NAV('a')}<div class=wrap>
<div class=eyebrow>every file · what it does · where it is</div><h1 style="font-size:clamp(34px,4.5vw,54px)">Kit Atlas</h1><div class=rule></div>
<div class=stats>{stat(NF,'files')}{stat(NZ,'zones')}{stat(sum(f['size'] for f in files)//1024,'KB total')}{stat(len([f for f in files if f['path'].startswith('kernel')]),'kernel files')}</div>
<div class=atlas>{''.join(zones)}</div>{''.join(tables)}{FOOT}</div><script>{JS}</script>"""
open('kit-atlas.html','w').write(atlas)

# ---- codex ----
deg=Counter()
for s in scars:
    for t in s['ties']: deg[str(t)]+=1
top=' '.join(f"<span class=tie>{t} ×{c}</span>" for t,c in deg.most_common(12))
cards=[]
for s in scars:
    rules=''.join(f"<li>{html.escape(r)}</li>" for r in s['rules'])
    tiez=''.join(f"<a class=tie href='#L{re.sub(r'\\D','',str(t))}'>{html.escape(str(t))}</a>" for t in s['ties'])
    inc=f"<details><summary>incident</summary><div class=inc>{html.escape(s['incident'])}</div></details>" if s['incident'] else ""
    cards.append(f"""<div class="card rise" id=L{s['num']} style="--ec:{era(s['num'])}" data-t="{html.escape((s['id']+' '+s['incident']+' '+' '.join(s['rules'])).lower())}">
<h3><span class=lid>L{s['num']}</span>{html.escape(s['name'].upper())}</h3><div class=note>{html.escape(s['note'])}</div>{inc}<ol>{rules}</ol><div class=ties>{tiez}</div></div>""")
codex=f"""{H('Scar Codex — '+str(NS)+' lessons')}{NAV('s')}<div class=wrap>
<div class=eyebrow>the scar file, verbatim · kernel/lessons.yaml</div><h1 style="font-size:clamp(34px,4.5vw,54px)">Scar Codex</h1><div class=rule></div>
<div class=stats>{stat(NS,'scars')}{stat(NR,'rules')}{stat(NT,'tie links')}{stat(NW,'added this window')}</div>
<div class=strip>{ticks.replace('scar-codex.html#','#')}</div><div class=legend>{legend}</div>
<div style="margin-top:10px;color:var(--mut);font-size:13px">most-cited: {top}</div>
<div class=searchbar><input id=q placeholder="Filter {NS} scars — try: mtime · luna · auth · verify · delegation" oninput=F()></div>
{''.join(cards)}{FOOT}</div><script>{JS}</script>"""
open('scar-codex.html','w').write(codex)

# ---- kit pages ----
orch_paths=[line.strip() for line in open('_orch-files.txt') if line.strip()]
enta_paths=[]
for line in open('_enta-tree.txt'):
    line=line.rstrip('\n')
    if not line:
        continue
    path,size,mtime,head=line.split('|',3)
    enta_paths.append(path)
    ENTA_META[path]={'path':path,'size':int(size),'mtime':mtime,'head':head}
a=kit_page('god-kit.html','g','The God Kit','canonical · think-like-fable · single-writer','The complete canonical kit — every command below, with the exact parts that fire and in what order.',GOD_CMDS,god_extra,[f['path'] for f in files])
b=kit_page('orchestration-kit.html','o','The Orchestration Kit','edition · v2 zip · regenerated every ship','The 8 run-a-fleet commands — governance and hardware lanes (/pi /machines /ship /harvest /verdict /guardrails /drift) are GOD-KIT ONLY and excluded from this edition.',ORCH_CMDS,orch_extra,orch_paths)
c=kit_page('entabeni-kit.html','e','The Entabeni Kit','first derived instance · mac b · own EL id-space','The 4-command subset the Entabeni engagement runs, grounded by six per-repo profiles.',ENTA_CMDS,enta_extra,enta_paths,True)

# ---- kit drift page (reads drift.json if present; regenerate via _drift-scan.py then _build.py) ----
try:
    dd=json.load(open('drift.json'))
    sm=dd['summary']; en=dd.get('entabeni',{})
    ST={'CLEAN':('#0e7a63','clean — exact mirror'),'BEHIND':('#c98f0a','behind canonical — resync via ship-kit.sh'),'LOCAL-SCARS':('#c0392b','LOCAL SCARS — harvest into canonical FIRST (L34), never clobber')}
    rows=''.join(f"""<tr><td class=path>{html.escape(m['name'])}</td>
<td><span class=badge style="background:{ST[m['status']][0]}">{m['status']}</span> {ST[m['status']][1]}</td>
<td class=n>{m['drift_count']}</td>
<td>{html.escape(', '.join(m['local_scars']) or '—')}</td>
<td style="font:11px ui-monospace,monospace;color:var(--mut)">{html.escape(', '.join(m['files_drifting'][:6]) or '—')}{'…' if m['drift_count']>6 else ''}</td></tr>""" for m in dd['mirrors'])
    if en.get('reachable'):
        ch=en.get('changed_24h',[]); enta_html=f"""<div class='ncard rise' style='border-left:5px solid #c98f0a'>
<h3>⬠ think-like-entabeni <span style="font:700 11px ui-monospace,monospace;color:#c98f0a">INDEPENDENT · WATCHED, NOT RESYNCED</span></h3>
<p>Mac B · <code>{html.escape(en['path'])}</code> · <b>{en['el_count']} EL-scars</b> (own id-space; promotion EL→L is deliberate) · skills: {html.escape(', '.join(en.get('skills',[])))}</p>
<p><b>Changed in the last 24h ({len(ch)} files):</b> {html.escape(', '.join(ch) or 'nothing — quiet')}</p></div>"""
    else:
        enta_html="<div class='ncard rise' style='border-left:5px solid #c0392b'><h3>⬠ think-like-entabeni</h3><p><b>UNREACHABLE at last scan</b> — macB ssh failed; drift state unknown (an unreachable kit is a blind spot, not a clean one).</p></div>"
    inst_cards=[]
    for inst in dd.get('instances',[]):
        reach=inst.get('reachable'); border='#0e7a63' if reach else '#c0392b'
        chips=''.join(f"<span class=tie style='background:color-mix(in srgb,var(--hot) 14%,transparent);color:var(--hot)'>{html.escape(x.split('-')[0])}</span>" for x in inst.get('unpromoted',[])[:30])
        body=''
        if reach:
            ch=', '.join(inst.get('changed_24h',[])[:10]) or 'nothing'
            body=(f"<p><b>{inst.get('scar_count',0)} scars</b> · <b style='color:var(--hot)'>{len(inst.get('unpromoted',[]))} candidates</b> whose lesson is not yet in the god kit "
                  f"(stem-match — some may already be covered under other L-ids; triage before promoting, L120) · "
                  f"<b>{len(inst.get('new_since_last',[]))} new since last scan</b></p>"
                  f"<div style='display:flex;flex-wrap:wrap;gap:6px;margin-top:8px'>{chips}</div>"
                  f"<p style='margin-top:8px;font-size:12.5px'><b>changed 24h:</b> {html.escape(ch)}</p>")
        inst_cards.append(f"<div class='ncard rise' style='border-left:5px solid {border}'>"
            f"<h3>⬠ {html.escape(inst['name'])} <span style='font:700 11px ui-monospace,monospace;color:#c98f0a'>{inst.get('scar_prefix','EL')}-space · {'reachable' if reach else 'UNREACHABLE'}</span></h3>"
            f"<p style='color:var(--mut);font-size:13px'>{html.escape(inst.get('desc',''))}</p>{body}</div>")
    inst_html=''.join(inst_cards) or enta_html
    drift=f"""{H('Kit Drift — mirrors & derived kits')}{NAV('d')}<div class=wrap>
<div class=eyebrow>canonical vs every copy · scanned {dd['scanned']} · rescan: _drift-scan.py → _build.py</div>
<h1 style="font-size:clamp(34px,4.5vw,54px)">Kit Drift</h1><div class=rule></div>
<div class=stats>{stat(len(dd['mirrors']),'mirrors')}{stat(sm['clean'],'clean')}{stat(sm['behind'],'behind')}{stat(sm['local_scars'],'local scars')}{stat(dd.get('promotion_queue',0),'lessons to review')}</div>
<h2>The doctrine<span class=sub>BEHIND → resync (ship-kit.sh). LOCAL SCARS → harvest first, then resync — the sync itself can be the clobber (L34). Instances → watch and promote deliberately (L120), never resync.</span></h2>
<div class=tbl><table><tr><th>Mirror</th><th>Status</th><th>Files off</th><th>Local scar ids</th><th>What differs</th></tr>{rows}</table></div>
<h2>Orchestration-kit instances<span class=sub>watched lesson sources (L120) — each evolves in its own id-space; new lessons flow UP into the god kit by deliberate promotion</span></h2>
{inst_html}
{FOOT}</div><script>{JS}</script>"""
    open('kit-drift.html','w').write(drift)
    print(f"kit-drift.html {len(drift)//1024}KB")
except FileNotFoundError:
    print("no drift.json — skipping drift page")

print(f"index {len(index)//1024}KB · atlas {len(atlas)//1024}KB · codex {len(codex)//1024}KB · god {a//1024}KB · orch {b//1024}KB · entabeni {c//1024}KB")
