from pathlib import Path
import html
import json
import re
from datetime import datetime, timezone

ROOT = Path(__file__).parent
SOURCE = ROOT / "source" / "anthrocybernetics-core-workbook"
OUTPUT = ROOT / "index.html"

CORE_META = {
    "01-opening-and-orientation.md": ("Orientation", "Open the archive", "Begin with the evidence rule, the narrative arc, and the first observation."),
    "02-core-grammar.md": ("Orientation", "The Five Foci", "Learn the cycle that routes observation from rhythm to pathway and back again."),
    "03-phase-find-the-rhythm.md": ("Five Foci", "Phase", "Find the rhythms, timing relationships, and recovery windows already active."),
    "04-field-name-the-conditions.md": ("Five Foci", "Field", "Name the boundary, the conditions crossing it, and the pathway of influence."),
    "05-arc-meet-the-threshold.md": ("Five Foci", "Arc", "Notice accumulation, threshold, transition, and the state that follows."),
    "06-form-notice-what-persists.md": ("Five Foci", "Form", "Study the patterns that persist while material, energy, or information moves."),
    "07-lattice-map-the-connections.md": ("Five Foci", "Lattice", "Map topology, bottlenecks, redundancy, propagation, and repair."),
    "08-daily-three-and-capacity.md": ("Working Instruments", "Daily Three", "Use a small operating loop for attention, load, capacity, and one structural choice."),
    "09-oscillator-lens.md": ("Working Instruments", "Oscillator Lens", "Use amplitude, frequency, phase, and damping as bounded analytical terms."),
    "10-cascade-and-relational-map.md": ("Working Instruments", "Cascade", "Enter the optional relational map without turning it into a score or social credit system."),
    "11-repair-throttling-and-renewal.md": ("Working Instruments", "Repair", "Protect capacity, throttle unsafe load, and distinguish repair from reconciliation."),
    "12-scale-up-and-nested-systems.md": ("Scale and Return", "Nested Systems", "Move across scales without flattening the differences between person, group, ecosystem, and planet."),
    "13-cosmic-weather-revisited.md": ("Scale and Return", "Cosmic Weather", "Widen the field while keeping mechanisms, timescales, and evidence boundaries visible."),
    "14-closing-the-sovereign-star.md": ("Scale and Return", "The Local Star", "Return to the individual as a situated source of observation and transformation."),
    "15-templates-and-reference.md": ("Reference", "Templates", "Collect operating cards, field checks, logs, glossary entries, and module schema."),
    "16-notes-for-facilitators.md": ("Reference", "Facilitators", "Teach and extend the kernel without losing its evidence discipline."),
}

MODULE_META = {
    "17-cognitive-manufacturing.md": ("Applied Modules", "Cognitive Manufacturing", "Translate signal and intent into bounded fabrication with provenance and hard limits."),
    "18-biogeometry.md": ("Applied Modules", "BioGeometry", "Study form as environmental signal while keeping claims, safeguards, and evidence distinct."),
    "19-environmental-psychology-and-place.md": ("Applied Modules", "Environmental Psychology and Place", "Trace how settings shape attention, behavior, stress, belonging, and agency."),
    "20-cybernetics-and-feedback.md": ("Applied Modules", "Cybernetics and Feedback", "Examine feedback, regulation, recursion, control, autonomy, and unintended effects."),
    "21-biosemiotics-and-living-signals.md": ("Applied Modules", "Biosemiotics and Living Signals", "Ask how living systems detect differences, interpret cues, and coordinate responses."),
    "22-embodied-cognition-and-enaction.md": ("Applied Modules", "Embodied Cognition and Enaction", "Treat cognition as enacted through bodies, environments, tools, and action."),
    "23-rhythm-and-entrainment.md": ("Applied Modules", "Rhythm and Entrainment", "Study timing, coordination, synchrony, and the limits of resonance language."),
    "24-experimental-design-for-resonance-claims.md": ("Applied Modules", "Experimental Design", "Turn an attractive resonance hypothesis into a fair, falsifiable study."),
    "25-sacred-architecture-across-cultures.md": ("Applied Modules", "Sacred Architecture", "Compare built form, ritual, orientation, and collective meaning across cultures."),
    "26-information-meaning-and-signal-translation.md": ("Applied Modules", "Information and Meaning", "Follow signals as they become signs, interpretations, and actions across media."),
    "27-ecological-reciprocity-and-regenerative-systems.md": ("Applied Modules", "Ecological Reciprocity", "Design human systems that return capacity to the living systems they depend on."),
    "28-emf-measurement-literacy.md": ("Applied Modules", "EMF Measurement Literacy", "Keep exposure, experience, measurement, and explanation separate."),
    "29-ethics-of-intervention.md": ("Applied Modules", "Ethics of Intervention", "Change systems without erasing autonomy, consent, context, or withdrawal."),
    "30-maintenance-failure-and-repair.md": ("Applied Modules", "Maintenance, Failure, and Repair", "Keep systems alive through observation, graceful failure, maintenance, and repair."),
    "31-anthrocybernetic-water-systems.md": ("Applied Modules", "Anthrocybernetic Water Systems", "Make flow, storage, energy, return, observation, and safety boundaries visible."),
    "32-macro-culture-and-climate-corridors.md": ("Applied Modules", "Macro-Culture and Climate Corridors", "Study civilization as a coupled environmental, energetic, hydrological, and historical system."),
}

REFERENCE_META = {
    "CONTENTS.md": ("Archive Index", "Contents", "The full Workbook map, Five Foci card, evidence card, and central discipline."),
    "CHANGELOG.md": ("Archive Index", "Changelog", "The revision trail from the first core workbook through the applied module series."),
    "SOP.md": ("Archive Index", "Source SOP", "The canonical workbook's own build and evidence-boundary procedure."),
    "index.html": ("Archive Index", "Original Visual Summary", "The published visual summary that inspired this CookieBlob preview."),
}


def inline(value):
    value = html.escape(value, quote=False)
    value = re.sub(r"`([^`]+)`", r"<code>\1</code>", value)
    value = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", value)
    value = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<em>\1</em>", value)
    value = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'<a href="\2" target="_blank" rel="noreferrer">\1</a>', value)
    return value


def markdown_html(source):
    lines = source.replace("\r\n", "\n").split("\n")
    out = []
    i = 0
    paragraph = []
    list_items = []
    ordered_items = []

    def flush_paragraph():
        nonlocal paragraph
        if paragraph:
            out.append("<p>" + " ".join(inline(x.strip()) for x in paragraph) + "</p>")
            paragraph = []

    def flush_lists():
        nonlocal list_items, ordered_items
        if list_items:
            out.append("<ul>" + "".join(f"<li>{x}</li>" for x in list_items) + "</ul>")
            list_items = []
        if ordered_items:
            out.append("<ol>" + "".join(f"<li>{x}</li>" for x in ordered_items) + "</ol>")
            ordered_items = []

    while i < len(lines):
        line = lines[i]
        stripped = line.strip()
        if stripped.startswith("```"):
            flush_paragraph(); flush_lists()
            language = stripped[3:].strip()
            code = []
            i += 1
            while i < len(lines) and not lines[i].strip().startswith("```"):
                code.append(lines[i])
                i += 1
            cls = f' class="language-{html.escape(language)}"' if language else ""
            out.append(f"<pre><code{cls}>{html.escape(chr(10).join(code))}</code></pre>")
            i += 1
            continue
        if not stripped:
            flush_paragraph(); flush_lists(); i += 1; continue
        heading = re.match(r"^(#{1,4})\s+(.+)$", stripped)
        if heading:
            flush_paragraph(); flush_lists()
            level = len(heading.group(1))
            out.append(f"<h{level}>{inline(heading.group(2))}</h{level}>")
            i += 1
            continue
        if stripped.startswith(">"):
            flush_paragraph(); flush_lists()
            quote = stripped[1:].strip()
            out.append(f"<blockquote>{inline(quote)}</blockquote>")
            i += 1
            continue
        bullet = re.match(r"^[-*+]\s+(.+)$", stripped)
        if bullet:
            flush_paragraph(); ordered_items = []
            list_items.append(inline(bullet.group(1))); i += 1; continue
        ordered = re.match(r"^\d+[.)]\s+(.+)$", stripped)
        if ordered:
            flush_paragraph(); list_items = []
            ordered_items.append(inline(ordered.group(1))); i += 1; continue
        if stripped.startswith("|") and "|" in stripped[1:]:
            flush_paragraph(); flush_lists()
            table = []
            while i < len(lines) and lines[i].strip().startswith("|"):
                cells = [cell.strip() for cell in lines[i].strip().strip("|").split("|")]
                table.append(cells); i += 1
            if len(table) >= 2 and all(re.fullmatch(r"[-: ]+", cell or "-") for cell in table[1]):
                table.pop(1)
            if table:
                head = "".join(f"<th>{inline(cell)}</th>" for cell in table[0])
                rows = "".join("<tr>" + "".join(f"<td>{inline(cell)}</td>" for cell in row) + "</tr>" for row in table[1:])
                out.append(f"<div class=\"table-wrap\"><table><thead><tr>{head}</tr></thead><tbody>{rows}</tbody></table></div>")
            continue
        if stripped.startswith("---"):
            flush_paragraph(); flush_lists(); out.append("<hr>"); i += 1; continue
        paragraph.append(stripped)
        i += 1
    flush_paragraph(); flush_lists()
    return "\n".join(out)


def room_for(path, meta):
    section, title, summary = meta
    source = path.read_text(encoding="utf-8")
    room_id = re.sub(r"[^a-z0-9]+", "-", path.stem.lower()).strip("-")
    if path.name == "index.html":
        room_id = "original-visual-summary"
    return {
        "id": room_id,
        "file": path.name,
        "section": section,
        "title": title,
        "summary": summary,
        "source": source,
        "body": markdown_html(source),
        "words": len(re.findall(r"\b[\w’'-]+\b", source)),
    }


def main():
    groups = [(CORE_META, "Core Workbook"), (MODULE_META, "Applied Modules"), (REFERENCE_META, "Reference")]
    rooms = []
    for mapping, _ in groups:
        for filename, meta in mapping.items():
            path = SOURCE / ("modules" / Path(filename) if mapping is MODULE_META else Path(filename))
            if path.exists():
                rooms.append(room_for(path, meta))
    payload = json.dumps(rooms, ensure_ascii=False).replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")
    generated = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    html_doc = build_html(payload, generated, len(rooms))
    OUTPUT.write_text(html_doc, encoding="utf-8")
    print(f"Generated {OUTPUT} with {len(rooms)} rooms")


def build_html(payload, generated, room_count):
    return f'''<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="A CookieBlob-style preview of the Anthrocybernetics Core Workbook and its applied modules.">
<title>Anthrocybernetics — Core Workbook Preview</title>
<style>
:root {{ --ink:#101612; --panel:#17221b; --panel2:#1d2c22; --line:#3d5a43; --text:#e7ead9; --muted:#aab7a1; --brass:#e6bc65; --moss:#91c47b; --cyan:#73d6cf; --pink:#d796c8; --red:#ea887d; --shadow:0 18px 60px rgba(0,0,0,.28); }}
* {{ box-sizing:border-box; }}
html {{ scroll-behavior:smooth; }}
body {{ margin:0; background:radial-gradient(circle at 15% 0%,#243629 0,#101612 42%,#0b100d 100%); color:var(--text); font-family:Georgia, 'Times New Roman', serif; line-height:1.6; }}
button,input,textarea {{ font:inherit; }}
button {{ cursor:pointer; }}
.shell {{ width:min(1500px,100%); margin:0 auto; padding:20px; }}
.masthead {{ display:flex; justify-content:space-between; gap:24px; align-items:flex-end; padding:24px 0 18px; border-bottom:1px solid var(--line); }}
.eyebrow,.mono,.meta,.room-id,.status {{ font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; letter-spacing:.08em; text-transform:uppercase; }}
.eyebrow {{ color:var(--brass); font-size:.73rem; }}
h1 {{ margin:.25rem 0 .4rem; font-size:clamp(2rem,5vw,4.8rem); line-height:1; letter-spacing:-.045em; }}
.subtitle {{ max-width:760px; color:var(--muted); font-size:1.05rem; margin:0; }}
.mast-actions {{ display:flex; flex-wrap:wrap; gap:8px; justify-content:flex-end; }}
.btn {{ color:var(--text); background:var(--panel2); border:1px solid var(--line); border-radius:5px; padding:9px 12px; transition:.18s ease; }}
.btn:hover,.btn:focus-visible {{ border-color:var(--brass); color:var(--brass); transform:translateY(-1px); }}
.btn.primary {{ color:var(--ink); background:var(--brass); border-color:var(--brass); font-weight:bold; }}
.btn.danger:hover,.btn.danger:focus-visible {{ color:var(--red); border-color:var(--red); }}
.layout {{ display:grid; grid-template-columns:300px minmax(0,1fr) 290px; gap:16px; padding-top:16px; align-items:start; }}
.panel {{ background:rgba(23,34,27,.9); border:1px solid var(--line); border-radius:7px; box-shadow:var(--shadow); }}
.sidebar,.rail {{ position:sticky; top:16px; max-height:calc(100vh - 32px); overflow:auto; }}
.panel-head {{ padding:14px 15px 10px; border-bottom:1px solid var(--line); }}
.panel-head h2 {{ margin:0; font-size:1rem; color:var(--brass); }}
.search {{ width:100%; margin-top:10px; padding:10px 11px; color:var(--text); background:#0c130e; border:1px solid var(--line); border-radius:4px; outline:none; }}
.search:focus {{ border-color:var(--cyan); }}
.room-list {{ padding:8px; }}
.room-group {{ margin:12px 6px 6px; color:var(--muted); font:11px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; letter-spacing:.1em; text-transform:uppercase; }}
.room-btn {{ display:block; width:100%; text-align:left; color:var(--text); background:transparent; border:1px solid transparent; border-radius:4px; padding:9px 9px 8px; margin:2px 0; }}
.room-btn:hover,.room-btn:focus-visible {{ background:#223328; border-color:var(--line); }}
.room-btn.active {{ background:#2b3d2c; border-color:var(--brass); }}
.room-btn.done::after {{ content:' ✓'; color:var(--moss); }}
.room-btn-title {{ display:block; font-size:.9rem; }}
.room-btn-meta {{ display:block; margin-top:2px; color:var(--muted); font:10px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }}
.main {{ min-width:0; }}
.archive-card {{ padding:clamp(20px,4vw,48px); min-height:700px; }}
.room-kicker {{ display:flex; gap:10px; flex-wrap:wrap; align-items:center; color:var(--cyan); font:11px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; text-transform:uppercase; letter-spacing:.1em; }}
.room-kicker .tag {{ padding:3px 7px; border:1px solid var(--line); border-radius:20px; color:var(--brass); }}
.room-title {{ margin:15px 0 8px; font-size:clamp(2rem,4vw,3.5rem); line-height:1.05; letter-spacing:-.04em; }}
.room-summary {{ max-width:760px; color:var(--muted); font-size:1.1rem; margin:0 0 25px; }}
.foci {{ display:grid; grid-template-columns:repeat(5,1fr); gap:7px; margin:0 0 28px; }}
.focus {{ min-height:77px; color:var(--text); background:#111b14; border:1px solid var(--line); border-radius:5px; padding:9px; text-align:left; }}
.focus:hover,.focus:focus-visible,.focus.selected {{ border-color:var(--brass); background:#26372a; }}
.focus strong {{ display:block; color:var(--brass); font:12px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; letter-spacing:.08em; }}
.focus span {{ display:block; margin-top:5px; color:var(--muted); font-size:.77rem; line-height:1.25; }}
.reading {{ max-width:880px; }}
.reading h1,.reading h2,.reading h3,.reading h4 {{ color:#f1d28a; line-height:1.2; margin-top:1.7em; }}
.reading h1 {{ font-size:2rem; }} .reading h2 {{ font-size:1.5rem; }} .reading h3 {{ font-size:1.18rem; }}
.reading p {{ margin:1em 0; }}
.reading a {{ color:var(--cyan); }}
.reading strong {{ color:#f3dfaa; }}
.reading code {{ padding:2px 5px; color:var(--moss); background:#0b120d; border:1px solid #29422e; border-radius:3px; }}
.reading pre {{ overflow:auto; padding:15px; color:#cfe3c7; background:#0a100c; border:1px solid #29422e; border-radius:5px; font:13px/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; white-space:pre; }}
.reading pre code {{ padding:0; border:0; background:transparent; color:inherit; }}
.reading blockquote {{ margin:18px 0; padding:8px 16px; color:#e5d3a4; border-left:3px solid var(--brass); background:#18261b; }}
.reading li {{ margin:.35em 0; }}
.reading hr {{ border:0; border-top:1px solid var(--line); margin:28px 0; }}
.table-wrap {{ overflow:auto; margin:18px 0; }}
table {{ width:100%; border-collapse:collapse; font-size:.92rem; }}
th,td {{ padding:8px 9px; border:1px solid var(--line); text-align:left; vertical-align:top; }}
th {{ color:var(--brass); background:#1b2a1e; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:.75rem; text-transform:uppercase; letter-spacing:.05em; }}
.room-footer {{ display:flex; justify-content:space-between; gap:10px; flex-wrap:wrap; margin-top:34px; padding-top:17px; border-top:1px solid var(--line); }}
.notes {{ margin-top:32px; padding-top:20px; border-top:1px solid var(--line); }}
.notes h3 {{ margin:0 0 8px; color:var(--brass); }}
textarea {{ width:100%; min-height:110px; resize:vertical; padding:11px; color:var(--text); background:#0b120d; border:1px solid var(--line); border-radius:4px; }}
.notice {{ min-height:22px; margin:10px 0 0; color:var(--cyan); font:12px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }}
.rail-section {{ padding:15px; border-bottom:1px solid var(--line); }}
.rail-section:last-child {{ border-bottom:0; }}
.rail h2,.rail h3 {{ margin:0 0 10px; color:var(--brass); }}
.rail h2 {{ font-size:1rem; }} .rail h3 {{ font-size:.9rem; }}
.progress {{ height:8px; background:#0b120d; border:1px solid var(--line); border-radius:9px; overflow:hidden; }}
.progress > span {{ display:block; height:100%; background:linear-gradient(90deg,var(--moss),var(--brass)); width:0; transition:.3s ease; }}
.progress-label {{ display:flex; justify-content:space-between; margin-top:6px; color:var(--muted); font:11px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }}
.transmission {{ padding:9px 0; border-bottom:1px solid #29422e; }}
.transmission:last-child {{ border-bottom:0; }}
.transmission time {{ display:block; color:var(--muted); font:10px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }}
.transmission p {{ margin:3px 0 0; font-size:.86rem; }}
.transmission .signal {{ color:var(--cyan); }}
.legend {{ display:grid; gap:6px; color:var(--muted); font-size:.84rem; }}
.legend span {{ display:flex; gap:7px; align-items:center; }}
.dot {{ width:9px; height:9px; display:inline-block; border-radius:50%; }}
.dot.grounded {{ background:var(--moss); }} .dot.hypothesized {{ background:var(--brass); }} .dot.interpretive {{ background:var(--pink); }}
.empty {{ color:var(--muted); font-size:.88rem; }}
.footer {{ padding:24px 0 8px; color:var(--muted); font:11px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }}
@media (max-width:1120px) {{ .layout {{ grid-template-columns:260px minmax(0,1fr); }} .rail {{ grid-column:1 / -1; position:static; max-height:none; display:grid; grid-template-columns:repeat(3,1fr); }} .rail-section {{ border-right:1px solid var(--line); border-bottom:0; }} }}
@media (max-width:760px) {{ .shell {{ padding:12px; }} .masthead {{ display:block; }} .mast-actions {{ justify-content:flex-start; margin-top:18px; }} .layout {{ display:block; }} .sidebar,.rail {{ position:static; max-height:none; margin-bottom:12px; }} .sidebar {{ max-height:48vh; }} .rail {{ display:block; }} .rail-section {{ border-right:0; border-bottom:1px solid var(--line); }} .archive-card {{ min-height:0; padding:22px 17px; }} .foci {{ grid-template-columns:repeat(2,1fr); }} .focus:last-child {{ grid-column:span 2; }} }}
</style>
</head>
<body>
<div class="shell">
<header class="masthead">
<div><div class="eyebrow">CookieBlob room / preview instrument</div><h1>Anthrocybernetics</h1><p class="subtitle">The Core Workbook as a living archive: find the rhythm, name the field, meet the threshold, notice what persists, and map what carries it.</p></div>
<div class="mast-actions"><button class="btn primary" id="startBtn">Begin at the entrance</button><button class="btn" id="exportBtn">Export seed</button><label class="btn" for="importInput">Import seed</label><input id="importInput" type="file" accept="application/json" hidden><button class="btn danger" id="resetBtn">Reset preview</button></div>
</header>
<div class="layout">
<aside class="panel sidebar"><div class="panel-head"><h2>Archive rooms</h2><input class="search" id="searchInput" type="search" placeholder="Search rooms and text…" aria-label="Search workbook rooms"></div><nav class="room-list" id="roomList" aria-label="Workbook rooms"></nav></aside>
<main class="panel main"><article class="archive-card"><div id="roomKicker" class="room-kicker"></div><h2 id="roomTitle" class="room-title"></h2><p id="roomSummary" class="room-summary"></p><div class="foci" aria-label="Five Foci navigation"><button class="focus" data-focus="Phase"><strong>PHASE</strong><span>What rhythm is active?</span></button><button class="focus" data-focus="Field"><strong>FIELD</strong><span>What conditions surround it?</span></button><button class="focus" data-focus="Arc"><strong>ARC</strong><span>What threshold is near?</span></button><button class="focus" data-focus="Form"><strong>FORM</strong><span>What persists?</span></button><button class="focus" data-focus="Lattice"><strong>LATTICE</strong><span>What carries the load?</span></button></div><div id="reading" class="reading"></div><div class="room-footer"><button class="btn" id="prevBtn">← Previous room</button><button class="btn primary" id="completeBtn">Mark room complete</button><button class="btn" id="nextBtn">Next room →</button></div><div class="notes"><h3>Leave a note in the room</h3><textarea id="noteInput" placeholder="What did you observe? Keep the evidence status visible."></textarea><div><button class="btn primary" id="saveNoteBtn">Transmit note</button></div><div class="notice" id="notice" aria-live="polite"></div></div></article></main>
<aside class="panel rail"><section class="rail-section"><h2>Reading state</h2><div class="progress"><span id="progressBar"></span></div><div class="progress-label"><span id="progressText">0 / {room_count}</span><span id="focusText">Phase</span></div></section><section class="rail-section"><h3>Evidence card</h3><div class="legend"><span><i class="dot grounded"></i> Grounded — observed or supported</span><span><i class="dot hypothesized"></i> Hypothesized — needs testing</span><span><i class="dot interpretive"></i> Interpretive — meaning or metaphor</span></div></section><section class="rail-section"><h3>Transmission rail</h3><div id="transmissions"><div class="empty">The archive is waiting for its first signal.</div></div></section><section class="rail-section"><h3>About this room</h3><p class="empty">A local preview of the canonical workbook. The source remains at <a href="https://zo.pub/anthrocybernetics/anthrocybernetics-core-workbook" target="_blank" rel="noreferrer">zo.pub/anthrocybernetics</a>.</p></section></aside>
</div>
<footer class="footer">Generated {generated} · {room_count} rooms embedded · local CookieBlob state only · canonical Markdown remains authoritative</footer>
</div>
<script>
const ROOMS = {payload};
const STORAGE_KEY = 'cookieblob_anthrocybernetics_preview_v01';
const VERSION = '0.1';
const freshState = () => ({{version:VERSION,room:ROOMS[0]?.id || '',visited:[],completed:[],notes:{{}},transmissions:[],focus:'Phase',updated_at:null}});
const clone = value => JSON.parse(JSON.stringify(value));
const validState = value => value && value.version === VERSION && Array.isArray(value.visited) && Array.isArray(value.completed) && value.notes && typeof value.notes === 'object' && Array.isArray(value.transmissions);
const normalize = value => {{ const base=freshState(); if(!validState(value)) return base; const next=clone(base); next.room=ROOMS.some(room=>room.id===value.room)?value.room:base.room; next.visited=value.visited.filter(id=>ROOMS.some(room=>room.id===id)); next.completed=value.completed.filter(id=>ROOMS.some(room=>room.id===id)); next.notes=value.notes; next.transmissions=value.transmissions.slice(-80); next.focus=value.focus || 'Phase'; next.updated_at=value.updated_at || null; return next; }};
let state = (() => {{ try {{ const raw=localStorage.getItem(STORAGE_KEY); return raw ? normalize(JSON.parse(raw)) : freshState(); }} catch {{ return freshState(); }} }})();
const $ = id => document.getElementById(id);
const esc = value => String(value ?? '').replace(/[&<>"']/g, c => ({{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}}[c]));
const stamp = () => new Date().toISOString();
const current = () => ROOMS.find(room=>room.id===state.room) || ROOMS[0];
const save = () => {{ state.updated_at=stamp(); try {{ localStorage.setItem(STORAGE_KEY,JSON.stringify(state)); }} catch {{}} }};
const signal = text => {{ state.transmissions.push({{at:stamp(),text}}); state.transmissions=state.transmissions.slice(-80); }};
const notify = text => {{ $('notice').textContent=text; clearTimeout(notify.timer); notify.timer=setTimeout(()=>$('notice').textContent='',2800); }};
const sectionOrder = ['Orientation','Five Foci','Working Instruments','Scale and Return','Reference','Applied Modules','Archive Index'];
function renderList() {{ const query=$('searchInput').value.trim().toLowerCase(); const groups={{}}; ROOMS.forEach(room=>{{ const hay=(room.title+' '+room.summary+' '+room.source).toLowerCase(); if(query && !hay.includes(query)) return; (groups[room.section] ||= []).push(room); }}); $('roomList').innerHTML=sectionOrder.filter(section=>groups[section]?.length).map(section=>`<div class="room-group">${{esc(section)}}</div>${{groups[section].map(room=>`<button class="room-btn ${{room.id===state.room?'active ':''}}${{state.completed.includes(room.id)?'done':''}}" data-room="${{esc(room.id)}}"><span class="room-btn-title">${{esc(room.title)}}</span><span class="room-btn-meta">${{room.words}} words · ${{esc(room.file)}}</span></button>`).join('')}}`).join('') || '<div class="empty" style="padding:14px">No rooms match that search.</div>'; document.querySelectorAll('[data-room]').forEach(button=>button.addEventListener('click',()=>openRoom(button.dataset.room))); }}
function renderTransmissions() {{ const items=state.transmissions.slice().reverse().slice(0,12); $('transmissions').innerHTML=items.length ? items.map(item=>`<div class="transmission"><time>${{new Date(item.at).toLocaleString()}}</time><p class="signal">${{esc(item.text)}}</p></div>`).join('') : '<div class="empty">The archive is waiting for its first signal.</div>'; }}
function render() {{ const room=current(); $('roomKicker').innerHTML=`<span>${{esc(room.section)}}</span><span class="tag">${{esc(room.file)}}</span><span>${{room.words}} words</span>`; $('roomTitle').textContent=room.title; $('roomSummary').textContent=room.summary; $('reading').innerHTML=room.body; $('noteInput').value=state.notes[room.id] || ''; $('completeBtn').textContent=state.completed.includes(room.id)?'Room complete ✓':'Mark room complete'; const index=ROOMS.findIndex(item=>item.id===room.id); $('prevBtn').disabled=index<=0; $('nextBtn').disabled=index>=ROOMS.length-1; const pct=Math.round((state.completed.length/ROOMS.length)*100); $('progressBar').style.width=pct+'%'; $('progressText').textContent=`${{state.completed.length}} / ${{ROOMS.length}}`; $('focusText').textContent=state.focus; document.querySelectorAll('.focus').forEach(button=>button.classList.toggle('selected',button.dataset.focus===state.focus)); renderList(); renderTransmissions(); }}
function openRoom(id) {{ if(!ROOMS.some(room=>room.id===id)) return; state.room=id; if(!state.visited.includes(id)) {{ state.visited.push(id); signal(`Entered room: ${{ROOMS.find(room=>room.id===id).title}}`); }} save(); render(); window.scrollTo({{top:0,behavior:'smooth'}}); }}
function step(direction) {{ const index=ROOMS.findIndex(room=>room.id===state.room); const next=ROOMS[index+direction]; if(next) openRoom(next.id); }}
$('startBtn').addEventListener('click',()=>openRoom(ROOMS[0].id));
$('prevBtn').addEventListener('click',()=>step(-1)); $('nextBtn').addEventListener('click',()=>step(1));
$('completeBtn').addEventListener('click',()=>{{ if(!state.completed.includes(state.room)) {{ state.completed.push(state.room); signal(`Completed room: ${{current().title}}`); save(); render(); notify('Room marked complete.'); }} else notify('This room is already complete.'); }});
$('saveNoteBtn').addEventListener('click',()=>{{ const value=$('noteInput').value.trim(); if(!value) {{ notify('Write a note before transmitting.'); return; }} state.notes[state.room]=value; signal(`Note transmitted from ${{current().title}}`); save(); render(); notify('Note stored in the local CookieBlob state.'); }});
$('searchInput').addEventListener('input',renderList);
document.querySelectorAll('.focus').forEach(button=>button.addEventListener('click',()=>{{ state.focus=button.dataset.focus; signal(`Focus selected: ${{state.focus}}`); save(); render(); }}));
$('exportBtn').addEventListener('click',()=>{{ signal('Reading seed exported'); save(); const output=JSON.stringify(state,null,2); const blob=new Blob([output],{{type:'application/json'}}); const link=document.createElement('a'); link.href=URL.createObjectURL(blob); link.download='anthrocybernetics-cookieblob-seed.json'; document.body.appendChild(link); link.click(); setTimeout(()=>{{ URL.revokeObjectURL(link.href); link.remove(); }},1000); render(); notify('Portable seed exported.'); }});
$('importInput').addEventListener('change',event=>{{ const file=event.target.files[0]; if(!file) return; const reader=new FileReader(); reader.onload=()=>{{ try {{ const imported=JSON.parse(reader.result); if(!validState(imported)) throw new Error('invalid'); state=normalize(imported); signal('Reading seed imported'); save(); render(); notify('Seed imported.'); }} catch {{ notify('Seed rejected. Current reading state is unchanged.'); }} }}; reader.readAsText(file); event.target.value=''; }});
$('resetBtn').addEventListener('click',()=>{{ if(confirm('Reset only the Anthrocybernetics CookieBlob preview?')) {{ state=freshState(); signal('Preview state reset'); save(); render(); notify('Preview reset.'); }} }});
document.addEventListener('keydown',event=>{{ if(event.target.matches('input,textarea,select')) return; if(event.key==='ArrowLeft') step(-1); if(event.key==='ArrowRight') step(1); if(event.key.toLowerCase()==='c') $('completeBtn').click(); }});
render();
</script>
</body>
</html>'''


if __name__ == '__main__':
    main()
