cgcardona / muse public
render_domain_registry.py python
2129 lines 89.3 KB
9f8e681d feat: add Diff Algebra visualization section — five algorithm panels + … Gabriel Cardona <cgcardona@gmail.com> 2d ago
1 #!/usr/bin/env python3
2 """Muse Domain Registry — standalone HTML generator.
3
4 Produces a self-contained, shareable page that explains the MuseDomainPlugin
5 protocol, shows the registered plugin ecosystem, and guides developers through
6 scaffolding and publishing their own domain plugin.
7
8 Stand-alone usage
9 -----------------
10 python tools/render_domain_registry.py
11 python tools/render_domain_registry.py --out artifacts/domain_registry.html
12 """
13 from __future__ import annotations
14
15 import json
16 import pathlib
17 import subprocess
18 import sys
19
20 _ROOT = pathlib.Path(__file__).resolve().parent.parent
21
22
23 # ---------------------------------------------------------------------------
24 # Live domain data from the CLI
25 # ---------------------------------------------------------------------------
26
27
28 def _compute_crdt_demos() -> list[dict]:
29 """Run the four CRDT primitives live and return formatted demo output."""
30 sys.path.insert(0, str(_ROOT))
31 try:
32 from muse.core.crdts import GCounter, LWWRegister, ORSet, VectorClock
33
34 # ORSet
35 base, _ = ORSet().add("annotation-GO:0001234")
36 a, _ = base.add("annotation-GO:0001234")
37 b = base.remove("annotation-GO:0001234", base.tokens_for("annotation-GO:0001234"))
38 merged = a.join(b)
39 orset_out = "\n".join([
40 "ORSet — add-wins concurrent merge:",
41 f" base elements: {sorted(base.elements())}",
42 f" A re-adds → elements: {sorted(a.elements())}",
43 f" B removes → elements: {sorted(b.elements())}",
44 f" join(A, B) → elements: {sorted(merged.elements())}",
45 " [A's new token is not tombstoned — add always wins]",
46 ])
47
48 # LWWRegister
49 ra = LWWRegister.from_dict({"value": "80 BPM", "timestamp": 1.0, "author": "agent-A"})
50 rb = LWWRegister.from_dict({"value": "120 BPM", "timestamp": 2.0, "author": "agent-B"})
51 rm = ra.join(rb)
52 lww_out = "\n".join([
53 "LWWRegister — last-write-wins scalar:",
54 f" Agent A writes: '{ra.read()}' at t=1.0",
55 f" Agent B writes: '{rb.read()}' at t=2.0 (later)",
56 f" join(A, B) → '{rm.read()}' [higher timestamp wins]",
57 " join(B, A) → same result [commutativity]",
58 ])
59
60 # GCounter
61 ca = GCounter().increment("agent-A").increment("agent-A")
62 cb = GCounter().increment("agent-B").increment("agent-B").increment("agent-B")
63 cm = ca.join(cb)
64 gc_out = "\n".join([
65 "GCounter — grow-only distributed counter:",
66 f" Agent A x2 → A slot: {ca.value_for('agent-A')}",
67 f" Agent B x3 → B slot: {cb.value_for('agent-B')}",
68 f" join(A, B) global value: {cm.value()}",
69 " [monotonically non-decreasing — joins never lose counts]",
70 ])
71
72 # VectorClock
73 va = VectorClock().increment("agent-A")
74 vb = VectorClock().increment("agent-B")
75 vm = va.merge(vb)
76 vc_out = "\n".join([
77 "VectorClock — causal ordering:",
78 f" Agent A: {va.to_dict()}",
79 f" Agent B: {vb.to_dict()}",
80 f" concurrent_with(A, B): {va.concurrent_with(vb)}",
81 f" merge(A, B): {vm.to_dict()} [component-wise max]",
82 ])
83
84 return [
85 {"type": "ORSet", "sub": "Observed-Remove Set", "color": "#bc8cff", "icon": _ICONS["union"], "output": orset_out},
86 {"type": "LWWRegister", "sub": "Last-Write-Wins Register", "color": "#58a6ff", "icon": _ICONS["edit"], "output": lww_out},
87 {"type": "GCounter", "sub": "Grow-Only Distributed Counter", "color": "#3fb950", "icon": _ICONS["arrow-up"], "output": gc_out},
88 {"type": "VectorClock", "sub": "Causal Ordering", "color": "#f9a825", "icon": _ICONS["git-branch"], "output": vc_out},
89 ]
90 except Exception as exc:
91 print(f" ⚠ CRDT demo failed ({exc}); using static fallback")
92 return []
93
94
95 def _load_domains() -> list[dict]:
96 """Run `muse domains --json` and return parsed output."""
97 try:
98 result = subprocess.run(
99 [sys.executable, "-m", "muse", "domains", "--json"],
100 capture_output=True,
101 text=True,
102 cwd=str(_ROOT),
103 timeout=15,
104 )
105 if result.returncode == 0:
106 raw = result.stdout.strip()
107 data: list[dict] = json.loads(raw)
108 return data
109 except Exception:
110 pass
111
112 # Fallback: static reference data
113 return [
114 {
115 "domain": "music",
116 "active": "true",
117 "capabilities": ["Typed Deltas", "Domain Schema", "OT Merge"],
118 "schema": {
119 "schema_version": "1",
120 "merge_mode": "three_way",
121 "description": "MIDI and audio file versioning with note-level diff and semantic merge",
122 "dimensions": [
123 {"name": "melodic", "description": "Note pitches and durations over time"},
124 {"name": "harmonic", "description": "Chord progressions and key signatures"},
125 {"name": "dynamic", "description": "Velocity and expression curves"},
126 {"name": "structural", "description": "Track layout, time signatures, tempo map"},
127 ],
128 },
129 }
130 ]
131
132
133 # ---------------------------------------------------------------------------
134 # Scaffold template (shown in the "Build in 3 steps" section)
135 # ---------------------------------------------------------------------------
136
137 _TYPED_DELTA_EXAMPLE = """\
138 # muse show --json (any commit, any domain)
139 {
140 "commit_id": "b26f3c99",
141 "message": "Resolve: integrate shared-state (A+B reconciled)",
142 "operations": [
143 {
144 "op_type": "ReplaceOp",
145 "address": "shared-state.mid",
146 "before_hash": "a1b2c3d4",
147 "after_hash": "e5f6g7h8",
148 "dimensions": ["structural"]
149 },
150 {
151 "op_type": "InsertOp",
152 "address": "beta-a.mid",
153 "after_hash": "09ab1234",
154 "dimensions": ["rhythmic", "dynamic"]
155 }
156 ],
157 "summary": {
158 "inserted": 1,
159 "replaced": 1,
160 "deleted": 0
161 }
162 }"""
163
164
165 _SCAFFOLD_SNIPPET = """\
166 from __future__ import annotations
167 from muse.domain import (
168 MuseDomainPlugin, LiveState, StateSnapshot,
169 StateDelta, DriftReport, MergeResult, DomainSchema,
170 )
171
172 class GenomicsPlugin(MuseDomainPlugin):
173 \"\"\"Version control for genomic sequences.\"\"\"
174
175 def snapshot(self, live_state: LiveState) -> StateSnapshot:
176 # Serialize current genome state to a content-addressable blob
177 raise NotImplementedError
178
179 def diff(self, base: StateSnapshot,
180 target: StateSnapshot) -> StateDelta:
181 # Compute minimal delta between two snapshots
182 raise NotImplementedError
183
184 def merge(self, base: StateSnapshot,
185 left: StateSnapshot,
186 right: StateSnapshot) -> MergeResult:
187 # Three-way merge — surface conflicts per dimension
188 raise NotImplementedError
189
190 def drift(self, committed: StateSnapshot,
191 live: LiveState) -> DriftReport:
192 # Detect uncommitted changes in the working state
193 raise NotImplementedError
194
195 def apply(self, delta: StateDelta,
196 live_state: LiveState) -> LiveState:
197 # Reconstruct historical state from a delta
198 raise NotImplementedError
199
200 def schema(self) -> DomainSchema:
201 # Declare dimensions — drives diff algorithm selection
202 raise NotImplementedError
203 """
204
205 # ---------------------------------------------------------------------------
206 # SVG icon library — Lucide/Feather style, stroke="currentColor", no fixed size
207 # ---------------------------------------------------------------------------
208
209 def _icon(paths: str) -> str:
210 """Wrap SVG paths in a standard icon shell."""
211 return (
212 '<svg class="icon" viewBox="0 0 24 24" fill="none" '
213 'stroke="currentColor" stroke-width="1.75" '
214 'stroke-linecap="round" stroke-linejoin="round">'
215 + paths
216 + "</svg>"
217 )
218
219
220 _ICONS: dict[str, str] = {
221 # Domains
222 "music": _icon('<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>'),
223 "genomics": _icon('<path d="M2 15c6.667-6 13.333 0 20-6"/><path d="M2 9c6.667 6 13.333 0 20 6"/><line x1="5.5" y1="11" x2="5.5" y2="13"/><line x1="18.5" y1="11" x2="18.5" y2="13"/>'),
224 "cube": _icon('<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>'),
225 "trending": _icon('<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/>'),
226 "atom": _icon('<circle cx="12" cy="12" r="1"/><path d="M20.2 20.2c2.04-2.03.02-7.36-4.5-11.9-4.54-4.52-9.87-6.54-11.9-4.5-2.04 2.03-.02 7.36 4.5 11.9 4.54 4.52 9.87 6.54 11.9 4.5z"/><path d="M15.7 15.7c4.52-4.54 6.54-9.87 4.5-11.9-2.03-2.04-7.36-.02-11.9 4.5-4.52 4.54-6.54 9.87-4.5 11.9 2.03 2.04 7.36.02 11.9-4.5z"/>'),
227 "plus": _icon('<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/>'),
228 "activity": _icon('<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>'),
229 "pen-tool": _icon('<path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/>'),
230 # Distribution
231 "terminal": _icon('<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>'),
232 "package": _icon('<line x1="16.5" y1="9.4" x2="7.5" y2="4.21"/><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>'),
233 "globe": _icon('<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>'),
234 # Engine capabilities
235 "code": _icon('<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>'),
236 "layers": _icon('<polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/>'),
237 "git-merge": _icon('<circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M6 21V9a9 9 0 0 0 9 9"/>'),
238 "zap": _icon('<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>'),
239 # MuseHub features
240 "search": _icon('<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>'),
241 "lock": _icon('<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>'),
242 # CRDT primitives
243 "union": _icon('<path d="M5 5v8a7 7 0 0 0 14 0V5"/><line x1="3" y1="19" x2="21" y2="19"/>'),
244 "edit": _icon('<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>'),
245 "arrow-up": _icon('<line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/>'),
246 "git-branch": _icon('<line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>'),
247 # OT scenario outcome badges
248 "check-circle":_icon('<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>'),
249 "x-circle": _icon('<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>'),
250 }
251
252
253 # ---------------------------------------------------------------------------
254 # Planned / aspirational domains
255 # ---------------------------------------------------------------------------
256
257 _PLANNED_DOMAINS = [
258 {
259 "name": "Genomics",
260 "icon": _ICONS["genomics"],
261 "status": "planned",
262 "tagline": "Version sequences, variants, and annotations",
263 "dimensions": ["sequence", "variants", "annotations", "metadata"],
264 "color": "#3fb950",
265 },
266 {
267 "name": "3D / Spatial",
268 "icon": _ICONS["cube"],
269 "status": "planned",
270 "tagline": "Merge spatial fields, meshes, and simulation frames",
271 "dimensions": ["geometry", "materials", "physics", "temporal"],
272 "color": "#58a6ff",
273 },
274 {
275 "name": "Financial",
276 "icon": _ICONS["trending"],
277 "status": "planned",
278 "tagline": "Track model versions, alpha signals, and risk state",
279 "dimensions": ["signals", "positions", "risk", "parameters"],
280 "color": "#f9a825",
281 },
282 {
283 "name": "Scientific Simulation",
284 "icon": _ICONS["atom"],
285 "status": "planned",
286 "tagline": "Snapshot simulation state across timesteps and parameter spaces",
287 "dimensions": ["state", "parameters", "observables", "checkpoints"],
288 "color": "#ab47bc",
289 },
290 {
291 "name": "Your Domain",
292 "icon": _ICONS["plus"],
293 "status": "yours",
294 "tagline": "Six methods. Any multidimensional state. Full VCS for free.",
295 "dimensions": ["your_dim_1", "your_dim_2", "..."],
296 "color": "#4f8ef7",
297 },
298 ]
299
300 # ---------------------------------------------------------------------------
301 # Distribution model description
302 # ---------------------------------------------------------------------------
303
304 _DISTRIBUTION_LEVELS = [
305 {
306 "tier": "Local",
307 "icon": _ICONS["terminal"],
308 "title": "Local plugin (right now)",
309 "color": "#3fb950",
310 "steps": [
311 "muse domains --new &lt;name&gt;",
312 "Implement 6 methods in muse/plugins/&lt;name&gt;/plugin.py",
313 "Register in muse/plugins/registry.py",
314 "muse init --domain &lt;name&gt;",
315 ],
316 "desc": "Works today. Scaffold → implement → register. "
317 "Your plugin lives alongside the core.",
318 },
319 {
320 "tier": "Shareable",
321 "icon": _ICONS["package"],
322 "title": "pip-installable package (right now)",
323 "color": "#58a6ff",
324 "steps": [
325 "Package your plugin as a Python module",
326 "pip install git+https://github.com/you/muse-plugin-genomics",
327 "Register the entry-point in pyproject.toml",
328 "muse init --domain genomics",
329 ],
330 "desc": "Share your plugin as a standard Python package. "
331 "Anyone with pip can install and use it.",
332 },
333 {
334 "tier": "MuseHub",
335 "icon": _ICONS["globe"],
336 "title": "Centralized registry (coming — MuseHub)",
337 "color": "#bc8cff",
338 "steps": [
339 "musehub publish muse-plugin-genomics",
340 "musehub search genomics",
341 "muse init --domain @musehub/genomics",
342 "Browse plugins at musehub.io",
343 ],
344 "desc": "MuseHub is a planned centralized registry — npm for Muse plugins. "
345 "Versioned, searchable, one-command install.",
346 },
347 ]
348
349
350 # ---------------------------------------------------------------------------
351 # HTML template
352 # ---------------------------------------------------------------------------
353
354 def _render_capability_card(cap: dict) -> str:
355 color = cap["color"]
356 return f"""
357 <div class="cap-showcase-card" style="--cap-color:{color}">
358 <div class="cap-showcase-header">
359 <span class="cap-showcase-badge" style="color:{color};background:{color}15;border-color:{color}40">
360 {cap['icon']} {cap['type']}
361 </span>
362 <span class="cap-showcase-sub">{cap['sub']}</span>
363 </div>
364 <div class="cap-showcase-body">
365 <pre class="cap-showcase-output">{cap['output']}</pre>
366 </div>
367 </div>"""
368
369
370 def _render_domain_card(d: dict) -> str:
371 domain = d.get("domain", "unknown")
372 active = d.get("active") == "true"
373 schema = d.get("schema", {})
374 desc = schema.get("description", "")
375 dims = schema.get("dimensions", [])
376 caps = d.get("capabilities", [])
377
378 cap_html = " ".join(
379 f'<span class="cap-pill cap-{c.lower().replace(" ","-")}">{c}</span>'
380 for c in caps
381 )
382 dim_html = " · ".join(
383 f'<span class="dim-tag">{dim["name"]}</span>' for dim in dims
384 )
385
386 status_cls = "active-badge" if active else "reg-badge"
387 status_text = "● active" if active else "○ registered"
388 dot = '<span class="active-dot"></span>' if active else ""
389
390 short_desc = desc[:150] + ("…" if len(desc) > 150 else "")
391
392 return f"""
393 <div class="domain-card{' active-domain' if active else ''}">
394 <div class="domain-card-hdr">
395 <span class="{status_cls}">{status_text}</span>
396 <span class="domain-name-lg">{domain}</span>
397 {dot}
398 </div>
399 <div class="domain-card-body">
400 <p class="domain-desc">{short_desc}</p>
401 <div class="cap-row">{cap_html}</div>
402 <div class="dim-row"><strong>Dimensions:</strong> {dim_html}</div>
403 </div>
404 </div>"""
405
406
407 def _render_planned_card(p: dict) -> str:
408 dims = " · ".join(f'<span class="dim-tag">{d}</span>' for d in p["dimensions"])
409 cls = "planned-card yours" if p["status"] == "yours" else "planned-card"
410 return f"""
411 <div class="{cls}" style="--card-accent:{p['color']}">
412 <div class="planned-icon">{p['icon']}</div>
413 <div class="planned-name">{p['name']}</div>
414 <div class="planned-tag">{p['tagline']}</div>
415 <div class="planned-dims">{dims}</div>
416 {'<a class="cta-btn" href="#build">Build it →</a>' if p["status"] == "yours" else '<span class="coming-soon">coming soon</span>'}
417 </div>"""
418
419
420 def _render_dist_card(d: dict) -> str:
421 steps = "".join(
422 f'<li><code>{s}</code></li>' for s in d["steps"]
423 )
424 return f"""
425 <div class="dist-card" style="--dist-color:{d['color']}">
426 <div class="dist-header">
427 <span class="dist-icon">{d['icon']}</span>
428 <div>
429 <div class="dist-tier">{d['tier']}</div>
430 <div class="dist-title">{d['title']}</div>
431 </div>
432 </div>
433 <p class="dist-desc">{d['desc']}</p>
434 <ol class="dist-steps">{steps}</ol>
435 </div>"""
436
437
438 def render(output_path: pathlib.Path) -> None:
439 """Generate the domain registry HTML page."""
440 print(" Loading live domain data...")
441 domains = _load_domains()
442 print(f" Found {len(domains)} registered domain(s)")
443
444 print(" Computing live CRDT demos...")
445 crdt_demos = _compute_crdt_demos()
446
447 active_domains_html = "\n".join(_render_domain_card(d) for d in domains)
448 planned_html = "\n".join(_render_planned_card(p) for p in _PLANNED_DOMAINS)
449 dist_html = "\n".join(_render_dist_card(d) for d in _DISTRIBUTION_LEVELS)
450 crdt_cards_html = "\n".join(_render_capability_card(c) for c in crdt_demos)
451
452 html = _HTML_TEMPLATE.replace("{{ACTIVE_DOMAINS}}", active_domains_html)
453 html = html.replace("{{PLANNED_DOMAINS}}", planned_html)
454 html = html.replace("{{DIST_CARDS}}", dist_html)
455 html = html.replace("{{SCAFFOLD_SNIPPET}}", _SCAFFOLD_SNIPPET)
456 html = html.replace("{{TYPED_DELTA_EXAMPLE}}", _TYPED_DELTA_EXAMPLE)
457 html = html.replace("{{CRDT_CARDS}}", crdt_cards_html)
458 html = html.replace("{{DIFF_ALGEBRA}}", _DIFF_ALGEBRA_HTML)
459
460 # Inject SVG icons into template placeholders
461 _ICON_SLOTS: dict[str, str] = {
462 "MUSIC": _ICONS["music"],
463 "GENOMICS": _ICONS["genomics"],
464 "CUBE": _ICONS["cube"],
465 "TRENDING": _ICONS["trending"],
466 "ATOM": _ICONS["atom"],
467 "PLUS": _ICONS["plus"],
468 "ACTIVITY": _ICONS["activity"],
469 "PEN_TOOL": _ICONS["pen-tool"],
470 "CODE": _ICONS["code"],
471 "LAYERS": _ICONS["layers"],
472 "GIT_MERGE": _ICONS["git-merge"],
473 "ZAP": _ICONS["zap"],
474 "GLOBE": _ICONS["globe"],
475 "SEARCH": _ICONS["search"],
476 "PACKAGE": _ICONS["package"],
477 "LOCK": _ICONS["lock"],
478 "CHECK_CIRCLE": _ICONS["check-circle"],
479 "X_CIRCLE": _ICONS["x-circle"],
480 }
481 for slot, svg in _ICON_SLOTS.items():
482 html = html.replace(f"{{{{ICON_{slot}}}}}", svg)
483
484 output_path.write_text(html, encoding="utf-8")
485 size_kb = output_path.stat().st_size // 1024
486 print(f" HTML written ({size_kb}KB) → {output_path}")
487
488 # Also write as index.html so the domain registry IS the landing page.
489 index_path = output_path.parent / "index.html"
490 index_path.write_text(html, encoding="utf-8")
491 print(f" Landing page mirrored → {index_path}")
492
493
494 # ---------------------------------------------------------------------------
495 # Diff Algebra section — five algorithm visualizations + StructuredDelta flow
496 # ---------------------------------------------------------------------------
497
498 _DIFF_ALGEBRA_HTML = """
499 <section id="diff-algebra" style="background:var(--bg)">
500 <div class="inner">
501 <div class="section-eyebrow">Diff Algebra</div>
502 <h2>Five Algebras. One Typed Result.</h2>
503 <p class="section-lead">
504 The engine selects the algorithm per dimension from your plugin&rsquo;s
505 <code>schema()</code>. You declare the shape &mdash; the engine handles identity,
506 diffing, and merge selection automatically.
507 </p>
508
509 <div class="da-grid">
510
511 <!-- 1. SEQUENCE — spans full width -->
512 <div class="da-card da-seq-card">
513 <div class="da-card-hdr">
514 <span class="da-chip da-chip-seq">Sequence</span>
515 <span class="da-algo-name">Myers / LCS on SHA-256 IDs</span>
516 </div>
517 <div class="da-domains-row">notes &middot; nucleotides &middot; animation frames &middot; git objects</div>
518 <div class="da-visual" style="flex-direction:column;align-items:flex-start;gap:8px">
519 <div class="seq-vis">
520 <div class="seq-row-lbl">before</div>
521 <div class="seq-row">
522 <div class="seq-blk seq-match"><div class="seq-hash">a1b2</div><div class="seq-name">C4</div></div>
523 <div class="seq-blk seq-del"><div class="seq-hash">c3d4</div><div class="seq-name">E4</div></div>
524 <div class="seq-blk seq-match"><div class="seq-hash">e5f6</div><div class="seq-name">G4</div></div>
525 <div class="seq-blk seq-moved-from"><div class="seq-hash">g7h8</div><div class="seq-name">B&flat;4</div></div>
526 </div>
527 <div class="seq-ops-row">
528 <div class="seq-op-cell"><span class="da-op da-op-match">= match</span></div>
529 <div class="seq-op-cell"><span class="da-op da-op-delete">&times; DeleteOp</span></div>
530 <div class="seq-op-cell"><span class="da-op da-op-match">= match</span></div>
531 <div class="seq-op-cell"><span class="da-op da-op-move">&darr; MoveOp</span></div>
532 </div>
533 <div class="seq-row">
534 <div class="seq-blk seq-match"><div class="seq-hash">a1b2</div><div class="seq-name">C4</div></div>
535 <div class="seq-blk seq-ins"><div class="seq-hash">k1l2</div><div class="seq-name">F4</div></div>
536 <div class="seq-blk seq-match"><div class="seq-hash">e5f6</div><div class="seq-name">G4</div></div>
537 <div class="seq-blk seq-ins"><div class="seq-hash">n5o6</div><div class="seq-name">A4</div></div>
538 <div class="seq-blk seq-moved-to"><div class="seq-hash">g7h8</div><div class="seq-name">B&flat;4</div></div>
539 </div>
540 <div class="seq-ops-row">
541 <div class="seq-op-cell"></div>
542 <div class="seq-op-cell"><span class="da-op da-op-insert">+ InsertOp</span></div>
543 <div class="seq-op-cell"></div>
544 <div class="seq-op-cell"><span class="da-op da-op-insert">+ InsertOp</span></div>
545 <div class="seq-op-cell"><span class="da-op da-op-move">&uarr; arrived</span></div>
546 </div>
547 <div class="seq-row-lbl">after</div>
548 </div>
549 </div>
550 <div class="da-note">Identity is hash-based: two elements are equal iff their SHA-256 hashes match &mdash; content is never inspected by the core. Delete&thinsp;+&thinsp;insert pairs sharing the same hash are collapsed into MoveOps in a post-pass.</div>
551 </div>
552
553 <!-- 2. TREE -->
554 <div class="da-card">
555 <div class="da-card-hdr">
556 <span class="da-chip da-chip-tree">Tree</span>
557 <span class="da-algo-name">Zhang-Shasha / GumTree</span>
558 </div>
559 <div class="da-domains-row">scene graphs &middot; ASTs &middot; track hierarchies</div>
560 <div class="da-visual" style="padding:10px 8px">
561 <svg class="tree-vis" viewBox="0 0 290 128" xmlns="http://www.w3.org/2000/svg">
562 <!-- BEFORE -->
563 <text x="52" y="9" text-anchor="middle" font-size="7" fill="#484f58" font-family="monospace" font-weight="700">BEFORE</text>
564 <line x1="52" y1="27" x2="24" y2="57" stroke="#30363d" stroke-width="1.5"/>
565 <line x1="52" y1="27" x2="80" y2="57" stroke="#30363d" stroke-width="1.5"/>
566 <line x1="24" y1="73" x2="11" y2="100" stroke="#30363d" stroke-width="1.5"/>
567 <line x1="24" y1="73" x2="37" y2="100" stroke="#30363d" stroke-width="1.5"/>
568 <line x1="80" y1="73" x2="80" y2="100" stroke="#bc8cff" stroke-width="1.5" stroke-dasharray="3,2"/>
569 <rect x="31" y="14" width="42" height="16" rx="4" fill="#161b22" stroke="#58a6ff" stroke-width="1.5"/>
570 <text x="52" y="25" text-anchor="middle" font-size="8" fill="#58a6ff" font-family="monospace">session</text>
571 <rect x="7" y="59" width="34" height="16" rx="3" fill="#161b22" stroke="#484f58"/>
572 <text x="24" y="70" text-anchor="middle" font-size="8" fill="#8b949e" font-family="monospace">intro</text>
573 <rect x="63" y="59" width="34" height="16" rx="3" fill="#161b22" stroke="#484f58"/>
574 <text x="80" y="70" text-anchor="middle" font-size="8" fill="#8b949e" font-family="monospace">verse</text>
575 <rect x="4" y="98" width="15" height="13" rx="3" fill="#161b22" stroke="#484f58"/>
576 <text x="11" y="108" text-anchor="middle" font-size="7.5" fill="#8b949e" font-family="monospace">C4</text>
577 <rect x="28" y="98" width="15" height="13" rx="3" fill="#161b22" stroke="#484f58"/>
578 <text x="35" y="108" text-anchor="middle" font-size="7.5" fill="#8b949e" font-family="monospace">E4</text>
579 <rect x="72" y="98" width="15" height="13" rx="3" fill="rgba(188,140,255,0.1)" stroke="#bc8cff" stroke-width="1.5"/>
580 <text x="79" y="108" text-anchor="middle" font-size="7.5" fill="#bc8cff" font-family="monospace">G4</text>
581 <!-- divider -->
582 <line x1="143" y1="8" x2="143" y2="118" stroke="#30363d" stroke-width="1" stroke-dasharray="3,3"/>
583 <text x="143" y="126" text-anchor="middle" font-size="6.5" fill="#484f58" font-family="monospace">MoveOp + InsertOp</text>
584 <!-- AFTER -->
585 <text x="216" y="9" text-anchor="middle" font-size="7" fill="#484f58" font-family="monospace" font-weight="700">AFTER</text>
586 <line x1="216" y1="27" x2="183" y2="57" stroke="#30363d" stroke-width="1.5"/>
587 <line x1="216" y1="27" x2="249" y2="57" stroke="#30363d" stroke-width="1.5"/>
588 <line x1="183" y1="73" x2="162" y2="100" stroke="#30363d" stroke-width="1.5"/>
589 <line x1="183" y1="73" x2="178" y2="100" stroke="#30363d" stroke-width="1.5"/>
590 <line x1="183" y1="73" x2="195" y2="100" stroke="#bc8cff" stroke-width="1.5" stroke-dasharray="3,2"/>
591 <line x1="249" y1="73" x2="249" y2="100" stroke="#3fb950" stroke-width="1.5"/>
592 <rect x="195" y="14" width="42" height="16" rx="4" fill="#161b22" stroke="#58a6ff" stroke-width="1.5"/>
593 <text x="216" y="25" text-anchor="middle" font-size="8" fill="#58a6ff" font-family="monospace">session</text>
594 <rect x="166" y="59" width="34" height="16" rx="3" fill="#161b22" stroke="#484f58"/>
595 <text x="183" y="70" text-anchor="middle" font-size="8" fill="#8b949e" font-family="monospace">intro</text>
596 <rect x="232" y="59" width="34" height="16" rx="3" fill="#161b22" stroke="#484f58"/>
597 <text x="249" y="70" text-anchor="middle" font-size="8" fill="#8b949e" font-family="monospace">verse</text>
598 <rect x="155" y="98" width="15" height="13" rx="3" fill="#161b22" stroke="#484f58"/>
599 <text x="162" y="108" text-anchor="middle" font-size="7.5" fill="#8b949e" font-family="monospace">C4</text>
600 <rect x="171" y="98" width="15" height="13" rx="3" fill="#161b22" stroke="#484f58"/>
601 <text x="178" y="108" text-anchor="middle" font-size="7.5" fill="#8b949e" font-family="monospace">E4</text>
602 <rect x="188" y="98" width="15" height="13" rx="3" fill="rgba(188,140,255,0.12)" stroke="#bc8cff" stroke-width="1.5"/>
603 <text x="195" y="108" text-anchor="middle" font-size="7.5" fill="#bc8cff" font-family="monospace">G4</text>
604 <rect x="241" y="98" width="15" height="13" rx="3" fill="rgba(63,185,80,0.12)" stroke="#3fb950" stroke-width="1.5"/>
605 <text x="249" y="108" text-anchor="middle" font-size="7.5" fill="#3fb950" font-family="monospace">A4</text>
606 <rect x="156" y="116" width="8" height="8" rx="1" fill="rgba(188,140,255,0.1)" stroke="#bc8cff"/>
607 <text x="166" y="122" font-size="6" fill="#bc8cff" font-family="monospace">moved</text>
608 <rect x="204" y="116" width="8" height="8" rx="1" fill="rgba(63,185,80,0.1)" stroke="#3fb950"/>
609 <text x="214" y="122" font-size="6" fill="#3fb950" font-family="monospace">inserted</text>
610 </svg>
611 </div>
612 <div class="da-note">Edit distance over node hierarchy &mdash; moves preserve subtree identity across parent changes.</div>
613 </div>
614
615 <!-- 3. TENSOR -->
616 <div class="da-card">
617 <div class="da-card-hdr">
618 <span class="da-chip da-chip-tensor">Tensor</span>
619 <span class="da-algo-name">Sparse / block numerical diff</span>
620 </div>
621 <div class="da-domains-row">sim state &middot; voxel grids &middot; weight matrices</div>
622 <div class="da-visual" style="flex-direction:column;gap:12px">
623 <div class="tensor-wrap">
624 <div class="tensor-panel">
625 <div class="tensor-label">t&thinsp;=&thinsp;0</div>
626 <div class="tensor-grid">
627 <div class="tc tc-2"></div><div class="tc tc-0"></div><div class="tc tc-1"></div><div class="tc tc-0"></div>
628 <div class="tc tc-0"></div><div class="tc tc-3"></div><div class="tc tc-0"></div><div class="tc tc-1"></div>
629 <div class="tc tc-1"></div><div class="tc tc-0"></div><div class="tc tc-2"></div><div class="tc tc-0"></div>
630 <div class="tc tc-0"></div><div class="tc tc-1"></div><div class="tc tc-0"></div><div class="tc tc-3"></div>
631 </div>
632 </div>
633 <div class="tensor-arrow">&rarr;</div>
634 <div class="tensor-panel">
635 <div class="tensor-label">t&thinsp;=&thinsp;1&thinsp;(&Delta;)</div>
636 <div class="tensor-grid">
637 <div class="tc tc-2"></div><div class="tc tc-0"></div><div class="tc tc-1"></div><div class="tc tc-0"></div>
638 <div class="tc tc-0"></div><div class="tc tc-hot3"></div><div class="tc tc-0"></div><div class="tc tc-warm1"></div>
639 <div class="tc tc-1"></div><div class="tc tc-0"></div><div class="tc tc-hot2"></div><div class="tc tc-0"></div>
640 <div class="tc tc-0"></div><div class="tc tc-1"></div><div class="tc tc-0"></div><div class="tc tc-3"></div>
641 </div>
642 </div>
643 </div>
644 <div class="tensor-legend">
645 <span class="tl-item"><span class="tl-swatch tl-unchanged"></span>|&Delta;| = 0</span>
646 <span class="tl-item"><span class="tl-swatch tl-warm"></span>|&Delta;| &le; &epsilon; (within threshold)</span>
647 <span class="tl-item"><span class="tl-swatch tl-hot"></span>|&Delta;| &gt; &epsilon; &rarr; PatchOp</span>
648 </div>
649 </div>
650 <div class="da-note">Configurable &epsilon; threshold. Sparse mode records only changed blocks &mdash; efficient for large tensors.</div>
651 </div>
652
653 <!-- 4. SET -->
654 <div class="da-card">
655 <div class="da-card-hdr">
656 <span class="da-chip da-chip-set">Set</span>
657 <span class="da-algo-name">Set algebra &middot; add / remove</span>
658 </div>
659 <div class="da-domains-row">annotations &middot; tags &middot; gene ontology terms</div>
660 <div class="da-visual">
661 <div class="set-vis">
662 <div class="set-col">
663 <div class="set-header">before</div>
664 <div class="set-members">
665 <span class="set-member set-kept">GO:0001234</span>
666 <span class="set-member set-kept">GO:0005634</span>
667 <span class="set-member set-removed">GO:0006915</span>
668 <span class="set-member set-kept">GO:0016020</span>
669 </div>
670 </div>
671 <div class="set-ops-col">
672 <div class="set-op-line set-op-keep">&mdash;</div>
673 <div class="set-op-line set-op-keep">&mdash;</div>
674 <div class="set-op-line set-op-rm">&times; del</div>
675 <div class="set-op-line set-op-keep">&mdash;</div>
676 <div class="set-op-line set-op-add">+ ins</div>
677 </div>
678 <div class="set-col">
679 <div class="set-header">after</div>
680 <div class="set-members">
681 <span class="set-member set-kept">GO:0001234</span>
682 <span class="set-member set-kept">GO:0005634</span>
683 <span class="set-member set-kept">GO:0016020</span>
684 <span class="set-member set-added">GO:0042592</span>
685 </div>
686 </div>
687 </div>
688 </div>
689 <div class="da-note">Unordered &mdash; no position tracking. Pure membership delta: {removed} and {added}.</div>
690 </div>
691
692 <!-- 5. MAP -->
693 <div class="da-card">
694 <div class="da-card-hdr">
695 <span class="da-chip da-chip-map">Map</span>
696 <span class="da-algo-name">Recursive key-by-key delegation</span>
697 </div>
698 <div class="da-domains-row">metadata &middot; configs &middot; nested structures</div>
699 <div class="da-visual" style="align-items:flex-start">
700 <div class="map-vis">
701 <div class="map-entry map-entry-changed">
702 <span class="map-key">tempo</span>
703 <span class="map-val-before">120</span><span class="map-delta">&nbsp;&rarr;&nbsp;</span><span class="map-val-after">140</span>
704 <span class="map-sub-algo da-chip-tensor">scalar &rarr; PatchOp</span>
705 </div>
706 <div class="map-entry map-entry-changed">
707 <span class="map-key">notes</span>
708 <span class="map-val-before">[&hellip;]</span><span class="map-delta">&nbsp;&rarr;&nbsp;</span><span class="map-val-after">[&hellip;&prime;]</span>
709 <span class="map-sub-algo da-chip-seq">sequence &rarr; LCS</span>
710 </div>
711 <div class="map-entry map-entry-changed">
712 <span class="map-key">tags</span>
713 <span class="map-val-before">{&hellip;}</span><span class="map-delta">&nbsp;&rarr;&nbsp;</span><span class="map-val-after">{&hellip;&prime;}</span>
714 <span class="map-sub-algo da-chip-set">set &rarr; algebra</span>
715 </div>
716 <div class="map-entry map-entry-unchanged">
717 <span class="map-key">author</span>
718 <span class="map-val-before">"Bach"</span><span class="map-delta">&nbsp;=&nbsp;</span><span class="map-val-after">"Bach"</span>
719 <span style="margin-left:auto;font-size:9px;color:var(--dim);font-family:var(--mono)">unchanged</span>
720 </div>
721 </div>
722 </div>
723 <div class="da-note">Each key is diffed by whichever algorithm matches its declared type &mdash; recursively, to arbitrary depth.</div>
724 </div>
725
726 </div><!-- .da-grid -->
727
728 <!-- StructuredDelta taxonomy -->
729 <div class="da-delta-flow">
730 <div class="da-delta-top">
731 <span class="da-delta-label">diff() &rarr; StructuredDelta</span>
732 <span class="da-delta-sub">all five algorithms produce the same typed operation list</span>
733 </div>
734 <div class="da-delta-ops">
735 <span class="da-dop da-dop-ins">InsertOp</span>
736 <span class="da-dop da-dop-del">DeleteOp</span>
737 <span class="da-dop da-dop-mov">MoveOp</span>
738 <span class="da-dop da-dop-rep">ReplaceOp</span>
739 <span class="da-dop da-dop-pat">PatchOp</span>
740 </div>
741 <div class="da-delta-merge">
742 <div class="da-merge-branch da-merge-ot">
743 <div class="da-merge-mode-label">merge_mode: &ldquo;three_way&rdquo;</div>
744 <div class="da-merge-desc">Operational Transformation &mdash; independent ops commute automatically; conflicting ops surface for human resolution</div>
745 </div>
746 <div class="da-merge-divider">or</div>
747 <div class="da-merge-branch da-merge-crdt">
748 <div class="da-merge-mode-label">merge_mode: &ldquo;crdt&rdquo;</div>
749 <div class="da-merge-desc">CRDT join() &mdash; convergent, no coordination required; any two replicas always reach the same state</div>
750 </div>
751 </div>
752 </div>
753
754 </div>
755 </section>
756 """
757
758 # ---------------------------------------------------------------------------
759 # Large HTML template
760 # ---------------------------------------------------------------------------
761
762 _HTML_TEMPLATE = """\
763 <!DOCTYPE html>
764 <html lang="en">
765 <head>
766 <meta charset="utf-8">
767 <meta name="viewport" content="width=device-width, initial-scale=1">
768 <title>Muse — Version Anything</title>
769 <style>
770 *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
771 :root {
772 --bg: #0d1117;
773 --bg2: #161b22;
774 --bg3: #21262d;
775 --border: #30363d;
776 --text: #e6edf3;
777 --mute: #8b949e;
778 --dim: #484f58;
779 --accent: #4f8ef7;
780 --accent2: #58a6ff;
781 --green: #3fb950;
782 --red: #f85149;
783 --yellow: #d29922;
784 --purple: #bc8cff;
785 --mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
786 --ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
787 --r: 8px;
788 }
789 html { scroll-behavior: smooth; }
790 body {
791 background: var(--bg);
792 color: var(--text);
793 font-family: var(--ui);
794 font-size: 15px;
795 line-height: 1.7;
796 }
797 a { color: var(--accent2); text-decoration: none; }
798 a:hover { text-decoration: underline; }
799 code {
800 font-family: var(--mono);
801 font-size: 0.88em;
802 background: var(--bg3);
803 border: 1px solid var(--border);
804 border-radius: 4px;
805 padding: 1px 6px;
806 }
807
808 /* ---- Hero ---- */
809 .hero {
810 background: linear-gradient(160deg, #0d1117 0%, #161b22 50%, #0d1117 100%);
811 border-bottom: 1px solid var(--border);
812 padding: 80px 40px 100px;
813 text-align: center;
814 position: relative;
815 overflow: hidden;
816 }
817 .hero::before {
818 content: '';
819 position: absolute;
820 inset: 0;
821 background:
822 radial-gradient(ellipse 60% 40% at 20% 50%, rgba(79,142,247,0.07) 0%, transparent 70%),
823 radial-gradient(ellipse 50% 40% at 80% 50%, rgba(188,140,255,0.06) 0%, transparent 70%);
824 pointer-events: none;
825 }
826 .hero-wordmark {
827 font-family: var(--ui);
828 font-size: clamp(72px, 11vw, 130px);
829 font-weight: 800;
830 letter-spacing: -5px;
831 line-height: 1;
832 margin-bottom: 12px;
833 background: linear-gradient(90deg, #6ea8fe 0%, #a78bfa 50%, #c084fc 100%);
834 -webkit-background-clip: text;
835 -webkit-text-fill-color: transparent;
836 background-clip: text;
837 }
838 .hero-version-any {
839 font-size: clamp(18px, 2.8vw, 26px);
840 font-weight: 700;
841 color: #ffffff;
842 letter-spacing: 6px;
843 text-transform: uppercase;
844 margin-bottom: 32px;
845 }
846 .hero-sub {
847 font-size: 18px;
848 color: var(--mute);
849 max-width: 600px;
850 margin: 0 auto 40px;
851 line-height: 1.6;
852 }
853 .hero-sub strong { color: var(--text); }
854 .hero-cta-row {
855 display: flex;
856 gap: 12px;
857 justify-content: center;
858 flex-wrap: wrap;
859 }
860 .btn-primary {
861 background: var(--accent);
862 color: #fff;
863 font-weight: 600;
864 padding: 12px 28px;
865 border-radius: var(--r);
866 font-size: 15px;
867 border: none;
868 cursor: pointer;
869 text-decoration: none;
870 transition: opacity 0.15s, transform 0.1s;
871 display: inline-block;
872 }
873 .btn-primary:hover { opacity: 0.88; transform: translateY(-1px); text-decoration: none; }
874 .btn-outline {
875 background: transparent;
876 color: var(--text);
877 font-weight: 500;
878 padding: 12px 28px;
879 border-radius: var(--r);
880 font-size: 15px;
881 border: 1px solid var(--border);
882 cursor: pointer;
883 text-decoration: none;
884 display: inline-block;
885 transition: border-color 0.15s, color 0.15s;
886 }
887 .btn-outline:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
888
889 /* ---- Domain ticker ---- */
890 .domain-ticker {
891 margin: 32px auto 0;
892 max-width: 700px;
893 overflow: hidden;
894 position: relative;
895 height: 34px;
896 }
897 .domain-ticker::before,
898 .domain-ticker::after {
899 content: '';
900 position: absolute;
901 top: 0; bottom: 0;
902 width: 60px;
903 z-index: 2;
904 }
905 .domain-ticker::before { left: 0; background: linear-gradient(90deg, var(--bg), transparent); }
906 .domain-ticker::after { right: 0; background: linear-gradient(-90deg, var(--bg), transparent); }
907 .ticker-track {
908 display: flex;
909 gap: 10px;
910 animation: ticker-scroll 18s linear infinite;
911 width: max-content;
912 }
913 @keyframes ticker-scroll {
914 0% { transform: translateX(0); }
915 100% { transform: translateX(-50%); }
916 }
917 .ticker-item {
918 font-family: var(--mono);
919 font-size: 13px;
920 padding: 4px 14px;
921 border-radius: 20px;
922 border: 1px solid var(--border);
923 white-space: nowrap;
924 color: var(--mute);
925 }
926 .ticker-item.active { border-color: rgba(79,142,247,0.5); color: var(--accent2); background: rgba(79,142,247,0.08); }
927
928 /* ---- Sections ---- */
929 section { padding: 72px 40px; border-top: 1px solid var(--border); }
930 .inner { max-width: 1100px; margin: 0 auto; }
931 .section-eyebrow {
932 font-family: var(--mono);
933 font-size: 11px;
934 color: var(--accent2);
935 letter-spacing: 2px;
936 text-transform: uppercase;
937 margin-bottom: 10px;
938 }
939 section h2 {
940 font-size: 32px;
941 font-weight: 700;
942 letter-spacing: -0.5px;
943 margin-bottom: 12px;
944 }
945 .section-lead {
946 font-size: 16px;
947 color: var(--mute);
948 max-width: 620px;
949 margin-bottom: 48px;
950 line-height: 1.7;
951 }
952 .section-lead strong { color: var(--text); }
953
954 /* ---- Base icon ---- */
955 .icon {
956 display: inline-block;
957 vertical-align: -0.15em;
958 flex-shrink: 0;
959 }
960 .ticker-item .icon { width: 13px; height: 13px; vertical-align: -0.1em; }
961 .cap-showcase-badge .icon { width: 13px; height: 13px; vertical-align: -0.1em; }
962
963 /* ---- Protocol two-col layout ---- */
964 .proto-layout {
965 display: grid;
966 grid-template-columns: 148px 1fr;
967 gap: 0;
968 border: 1px solid var(--border);
969 border-radius: var(--r);
970 overflow: hidden;
971 margin-bottom: 40px;
972 align-items: stretch;
973 }
974 @media (max-width: 640px) {
975 .proto-layout { grid-template-columns: 1fr; }
976 .stat-strip { border-right: none; border-bottom: 1px solid var(--border); }
977 }
978
979 /* ---- Stat strip (left column) ---- */
980 .stat-strip {
981 display: flex;
982 flex-direction: column;
983 border-right: 1px solid var(--border);
984 }
985 .stat-cell {
986 flex: 1;
987 padding: 18px 20px;
988 border-bottom: 1px solid var(--border);
989 text-align: center;
990 display: flex;
991 flex-direction: column;
992 align-items: center;
993 justify-content: center;
994 }
995 .stat-cell:last-child { border-bottom: none; }
996 .stat-num {
997 font-family: var(--mono);
998 font-size: 26px;
999 font-weight: 700;
1000 color: var(--accent2);
1001 display: block;
1002 line-height: 1.1;
1003 }
1004 .stat-lbl { font-size: 11px; color: var(--mute); margin-top: 4px; line-height: 1.3; }
1005
1006 /* ---- Protocol table (right column) ---- */
1007 .proto-table {
1008 overflow: hidden;
1009 }
1010 .proto-row {
1011 display: grid;
1012 grid-template-columns: 90px 240px 1fr;
1013 border-bottom: 1px solid var(--border);
1014 }
1015 .proto-row:last-child { border-bottom: none; }
1016 .proto-row.hdr { background: var(--bg3); }
1017 .proto-row > div { padding: 11px 16px; }
1018 .proto-method { font-family: var(--mono); font-size: 13px; color: var(--accent2); font-weight: 600; }
1019 .proto-sig { font-family: var(--mono); font-size: 12px; color: var(--mute); }
1020 .proto-desc { font-size: 13px; color: var(--mute); }
1021 .proto-row.hdr .proto-method,
1022 .proto-row.hdr .proto-sig,
1023 .proto-row.hdr .proto-desc { font-family: var(--ui); font-size: 11px; font-weight: 600; color: var(--dim); text-transform: uppercase; letter-spacing: 0.6px; }
1024
1025 /* ---- Engine capability showcase ---- */
1026 .cap-showcase-grid {
1027 display: grid;
1028 grid-template-columns: repeat(auto-fill, minmax(480px, 1fr));
1029 gap: 24px;
1030 }
1031 @media (max-width: 600px) { .cap-showcase-grid { grid-template-columns: 1fr; } }
1032 .cap-showcase-card {
1033 border: 1px solid var(--border);
1034 border-top: 3px solid var(--cap-color, var(--accent));
1035 border-radius: var(--r);
1036 background: var(--bg);
1037 overflow: hidden;
1038 transition: transform 0.15s;
1039 }
1040 .cap-showcase-card:hover { transform: translateY(-2px); }
1041 .cap-showcase-header {
1042 padding: 14px 18px;
1043 border-bottom: 1px solid var(--border);
1044 background: var(--bg2);
1045 display: flex;
1046 align-items: center;
1047 gap: 12px;
1048 flex-wrap: wrap;
1049 }
1050 .cap-showcase-badge {
1051 font-size: 12px;
1052 font-family: var(--mono);
1053 padding: 3px 10px;
1054 border-radius: 4px;
1055 border: 1px solid;
1056 white-space: nowrap;
1057 }
1058 .cap-showcase-sub {
1059 font-size: 12px;
1060 color: var(--mute);
1061 font-style: italic;
1062 }
1063 .cap-showcase-body { padding: 16px 18px; }
1064 .cap-showcase-desc {
1065 font-size: 13px;
1066 color: var(--mute);
1067 margin-bottom: 14px;
1068 line-height: 1.6;
1069 }
1070 .cap-showcase-desc strong { color: var(--text); }
1071 .cap-showcase-output {
1072 background: #0a0e14;
1073 border: 1px solid var(--border);
1074 border-radius: 5px;
1075 padding: 12px 14px;
1076 font-family: var(--mono);
1077 font-size: 11.5px;
1078 color: #abb2bf;
1079 white-space: pre;
1080 overflow-x: auto;
1081 line-height: 1.65;
1082 }
1083 /* ---- Diff Algebra section ---- */
1084 .da-grid {
1085 display: grid;
1086 grid-template-columns: repeat(2, 1fr);
1087 gap: 20px;
1088 margin-bottom: 36px;
1089 }
1090 .da-seq-card { grid-column: span 2; }
1091 @media (max-width: 720px) {
1092 .da-grid { grid-template-columns: 1fr; }
1093 .da-seq-card { grid-column: span 1; }
1094 }
1095 .da-card {
1096 background: var(--bg2);
1097 border: 1px solid var(--border);
1098 border-radius: var(--r);
1099 padding: 18px;
1100 display: flex;
1101 flex-direction: column;
1102 gap: 10px;
1103 }
1104 .da-card-hdr { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
1105 .da-chip {
1106 font-family: var(--mono);
1107 font-size: 10.5px;
1108 font-weight: 700;
1109 padding: 3px 10px;
1110 border-radius: 4px;
1111 border: 1px solid;
1112 }
1113 .da-chip-seq { color:#58a6ff; background:rgba(88,166,255,0.1); border-color:rgba(88,166,255,0.3); }
1114 .da-chip-tree { color:#f9a825; background:rgba(249,168,37,0.1); border-color:rgba(249,168,37,0.3); }
1115 .da-chip-tensor { color:#ab47bc; background:rgba(171,71,188,0.1); border-color:rgba(171,71,188,0.3); }
1116 .da-chip-set { color:#3fb950; background:rgba(63,185,80,0.1); border-color:rgba(63,185,80,0.3); }
1117 .da-chip-map { color:#ef5350; background:rgba(239,83,80,0.1); border-color:rgba(239,83,80,0.3); }
1118 .da-algo-name { font-size:12px; color:var(--mute); font-family:var(--mono); }
1119 .da-domains-row { font-size:11px; color:var(--dim); }
1120 .da-visual {
1121 background: #0a0e14;
1122 border: 1px solid var(--border);
1123 border-radius: 5px;
1124 padding: 14px;
1125 flex: 1;
1126 min-height: 130px;
1127 display: flex;
1128 align-items: center;
1129 justify-content: center;
1130 }
1131 .da-note { font-size:11.5px; color:var(--mute); line-height:1.5; }
1132
1133 /* Sequence LCS */
1134 .seq-vis { display:flex; flex-direction:column; gap:6px; width:100%; }
1135 .seq-row-lbl { font-family:var(--mono); font-size:9px; color:var(--dim); text-transform:uppercase; letter-spacing:.6px; }
1136 .seq-row { display:flex; gap:6px; flex-wrap:wrap; }
1137 .seq-blk {
1138 display:flex; flex-direction:column; align-items:center;
1139 padding:5px 9px; border-radius:5px; border:1.5px solid;
1140 min-width:46px; gap:2px; transition:transform .15s;
1141 }
1142 .seq-blk:hover { transform:translateY(-2px); }
1143 .seq-hash { font-family:var(--mono); font-size:7.5px; opacity:.55; }
1144 .seq-name { font-family:var(--mono); font-size:14px; font-weight:700; }
1145 .seq-match { background:rgba(88,166,255,.07); border-color:rgba(88,166,255,.25); color:#58a6ff; }
1146 .seq-del { background:rgba(239,83,80,.1); border-color:rgba(239,83,80,.35); color:#ef5350; text-decoration:line-through; }
1147 .seq-ins { background:rgba(63,185,80,.1); border-color:rgba(63,185,80,.35); color:#3fb950; }
1148 .seq-moved-from{ background:rgba(188,140,255,.06); border-color:rgba(188,140,255,.25);color:#bc8cff; opacity:.55; }
1149 .seq-moved-to { background:rgba(188,140,255,.12); border-color:rgba(188,140,255,.5); color:#bc8cff; }
1150 .seq-ops-row { display:flex; gap:6px; padding:2px 0; flex-wrap:wrap; }
1151 .seq-op-cell { min-width:46px; display:flex; justify-content:center; }
1152 .da-op { font-size:9px; font-weight:700; letter-spacing:.3px; padding:1px 5px; border-radius:3px; white-space:nowrap; }
1153 .da-op-match { color:var(--dim); }
1154 .da-op-delete { color:#ef5350; background:rgba(239,83,80,.1); }
1155 .da-op-insert { color:#3fb950; background:rgba(63,185,80,.1); }
1156 .da-op-move { color:#bc8cff; background:rgba(188,140,255,.1); }
1157
1158 /* Tree SVG */
1159 .tree-vis { width:100%; height:130px; overflow:visible; }
1160
1161 /* Tensor */
1162 .tensor-wrap { display:flex; align-items:center; gap:14px; }
1163 .tensor-panel { display:flex; flex-direction:column; align-items:center; gap:5px; }
1164 .tensor-label { font-family:var(--mono); font-size:9px; color:var(--dim); }
1165 .tensor-grid { display:grid; grid-template-columns:repeat(4,26px); gap:3px; }
1166 .tc { width:26px; height:26px; border-radius:3px; }
1167 .tc-0 { background:#141c28; }
1168 .tc-1 { background:#1a2538; }
1169 .tc-2 { background:#1d2f50; }
1170 .tc-3 { background:#1a3060; }
1171 .tc-warm1 { background:rgba(249,168,37,.28); border:1px solid rgba(249,168,37,.4); }
1172 .tc-hot2 { background:rgba(239,83,80,.45); border:1px solid rgba(239,83,80,.65); }
1173 .tc-hot3 { background:rgba(239,83,80,.65); border:1px solid rgba(239,83,80,.9); }
1174 .tensor-arrow { font-size:22px; color:var(--dim); }
1175 .tensor-legend { display:flex; flex-direction:column; gap:5px; margin-top:4px; }
1176 .tl-item { display:flex; align-items:center; gap:5px; font-size:9px; color:var(--mute); }
1177 .tl-swatch { width:12px; height:12px; border-radius:2px; flex-shrink:0; }
1178 .tl-unchanged { background:#141c28; }
1179 .tl-warm { background:rgba(249,168,37,.28); border:1px solid rgba(249,168,37,.4); }
1180 .tl-hot { background:rgba(239,83,80,.65); border:1px solid rgba(239,83,80,.9); }
1181
1182 /* Set algebra */
1183 .set-vis { display:grid; grid-template-columns:1fr auto 1fr; gap:8px; align-items:start; width:100%; }
1184 .set-col .set-header { font-family:var(--mono); font-size:9px; color:var(--dim); text-transform:uppercase; letter-spacing:.6px; margin-bottom:6px; }
1185 .set-members { display:flex; flex-direction:column; gap:4px; }
1186 .set-member { font-family:var(--mono); font-size:10px; padding:3px 8px; border-radius:3px; border:1px solid; }
1187 .set-kept { color:var(--mute); border-color:var(--border); background:var(--bg3); }
1188 .set-removed { color:#ef5350; border-color:rgba(239,83,80,.35); background:rgba(239,83,80,.07); text-decoration:line-through; }
1189 .set-added { color:#3fb950; border-color:rgba(63,185,80,.35); background:rgba(63,185,80,.07); }
1190 .set-ops-col { display:flex; flex-direction:column; gap:4px; padding-top:21px; }
1191 .set-op-line { font-size:10px; font-weight:700; white-space:nowrap; height:26px; display:flex; align-items:center; }
1192 .set-op-keep { color:var(--dim); }
1193 .set-op-rm { color:#ef5350; }
1194 .set-op-add { color:#3fb950; }
1195
1196 /* Map recursive */
1197 .map-vis { display:flex; flex-direction:column; gap:5px; width:100%; font-family:var(--mono); }
1198 .map-entry { display:flex; align-items:center; gap:6px; padding:5px 8px; border-radius:4px; font-size:11px; flex-wrap:wrap; }
1199 .map-entry-changed { background:rgba(88,166,255,.04); border:1px solid rgba(88,166,255,.15); }
1200 .map-entry-unchanged { background:var(--bg3); border:1px solid var(--border); opacity:.65; }
1201 .map-key { color:#61afef; font-weight:700; min-width:52px; }
1202 .map-delta { color:var(--dim); }
1203 .map-val-before { color:var(--dim); font-size:10px; }
1204 .map-val-after { color:var(--text); font-size:10px; }
1205 .map-sub-algo { margin-left:auto; font-size:9px; font-weight:700; padding:1px 6px; border-radius:3px; border:1px solid; }
1206
1207 /* StructuredDelta flow */
1208 .da-delta-flow {
1209 background: var(--bg2);
1210 border: 1px solid var(--border);
1211 border-radius: var(--r);
1212 padding: 24px;
1213 }
1214 .da-delta-top { margin-bottom:14px; display:flex; align-items:baseline; gap:12px; flex-wrap:wrap; }
1215 .da-delta-label { font-family:var(--mono); font-size:15px; font-weight:700; color:var(--text); }
1216 .da-delta-sub { font-size:12px; color:var(--mute); }
1217 .da-delta-ops { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:18px; }
1218 .da-dop { font-family:var(--mono); font-size:12px; font-weight:700; padding:5px 14px; border-radius:5px; border:1px solid; }
1219 .da-dop-ins { color:#3fb950; background:rgba(63,185,80,.1); border-color:rgba(63,185,80,.3); }
1220 .da-dop-del { color:#ef5350; background:rgba(239,83,80,.1); border-color:rgba(239,83,80,.3); }
1221 .da-dop-mov { color:#bc8cff; background:rgba(188,140,255,.1);border-color:rgba(188,140,255,.3); }
1222 .da-dop-rep { color:#f9a825; background:rgba(249,168,37,.1); border-color:rgba(249,168,37,.3); }
1223 .da-dop-pat { color:#58a6ff; background:rgba(88,166,255,.1); border-color:rgba(88,166,255,.3); }
1224 .da-delta-merge { display:grid; grid-template-columns:1fr auto 1fr; gap:14px; align-items:center; }
1225 @media (max-width: 640px) { .da-delta-merge { grid-template-columns:1fr; } .da-merge-divider { text-align:center; } }
1226 .da-merge-branch { padding:14px 16px; border-radius:6px; border:1px solid; }
1227 .da-merge-ot { border-color:rgba(239,83,80,.3); background:rgba(239,83,80,.04); }
1228 .da-merge-crdt { border-color:rgba(188,140,255,.3); background:rgba(188,140,255,.04); }
1229 .da-merge-mode-label { font-family:var(--mono); font-size:11px; font-weight:700; margin-bottom:5px; }
1230 .da-merge-ot .da-merge-mode-label { color:#ef5350; }
1231 .da-merge-crdt .da-merge-mode-label { color:#bc8cff; }
1232 .da-merge-desc { font-size:11.5px; color:var(--mute); line-height:1.5; }
1233 .da-merge-divider { color:var(--dim); font-size:13px; font-weight:700; }
1234
1235 /* ---- OT Merge scenario cards ---- */
1236 .ot-scenarios { display: flex; flex-direction: column; gap: 10px; }
1237 .ot-scenario {
1238 background: var(--bg);
1239 border: 1px solid var(--border);
1240 border-left: 3px solid transparent;
1241 border-radius: 6px;
1242 padding: 12px 14px;
1243 display: flex;
1244 flex-direction: column;
1245 gap: 9px;
1246 }
1247 .ot-clean { border-left-color: #3fb950; }
1248 .ot-conflict { border-left-color: #ef5350; }
1249 .ot-scenario-hdr { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
1250 .ot-scenario-label {
1251 font-family: var(--mono);
1252 font-size: 9.5px;
1253 font-weight: 700;
1254 text-transform: uppercase;
1255 letter-spacing: 1px;
1256 color: var(--dim);
1257 }
1258 .ot-scenario-title { font-size: 11.5px; color: var(--mute); }
1259 .ot-ops { display: flex; flex-direction: column; gap: 5px; }
1260 .ot-op {
1261 display: flex;
1262 align-items: center;
1263 gap: 7px;
1264 font-family: var(--mono);
1265 font-size: 11.5px;
1266 flex-wrap: wrap;
1267 }
1268 .ot-op-side {
1269 font-size: 9px;
1270 font-weight: 700;
1271 color: var(--dim);
1272 background: var(--bg3);
1273 padding: 1px 6px;
1274 border-radius: 3px;
1275 min-width: 34px;
1276 text-align: center;
1277 }
1278 .ot-op-type { font-weight: 700; padding: 1px 7px; border-radius: 3px; font-size: 10.5px; }
1279 .ot-insert { background: rgba(63,185,80,0.13); color: #3fb950; }
1280 .ot-replace { background: rgba(249,168,37,0.13); color: #f9a825; }
1281 .ot-op-addr { color: #98c379; }
1282 .ot-op-meta { color: var(--dim); font-size: 10.5px; }
1283 .ot-result {
1284 display: flex;
1285 align-items: center;
1286 justify-content: space-between;
1287 flex-wrap: wrap;
1288 gap: 8px;
1289 padding-top: 9px;
1290 border-top: 1px solid var(--border);
1291 }
1292 .ot-reason { font-family: var(--mono); font-size: 11px; color: var(--mute); }
1293 .ot-badge {
1294 display: inline-flex;
1295 align-items: center;
1296 gap: 5px;
1297 font-size: 11px;
1298 font-weight: 700;
1299 padding: 3px 10px;
1300 border-radius: 12px;
1301 white-space: nowrap;
1302 }
1303 .ot-badge .icon { width: 11px; height: 11px; vertical-align: -0.05em; }
1304 .ot-badge-clean { background: rgba(63,185,80,0.1); color: #3fb950; border: 1px solid rgba(63,185,80,0.3); }
1305 .ot-badge-conflict { background: rgba(239,83,80,0.1); color: #ef5350; border: 1px solid rgba(239,83,80,0.3); }
1306
1307 .cap-showcase-domain-grid {
1308 display: flex;
1309 flex-direction: column;
1310 gap: 10px;
1311 }
1312 .crdt-mini-grid {
1313 display: grid;
1314 grid-template-columns: 1fr 1fr;
1315 gap: 10px;
1316 }
1317 @media (max-width: 700px) { .crdt-mini-grid { grid-template-columns: 1fr; } }
1318
1319 /* ---- Three steps ---- */
1320 .steps-grid {
1321 display: grid;
1322 grid-template-columns: repeat(3, 1fr);
1323 gap: 24px;
1324 }
1325 @media (max-width: 800px) { .steps-grid { grid-template-columns: 1fr; } }
1326 .step-card {
1327 border: 1px solid var(--border);
1328 border-radius: var(--r);
1329 background: var(--bg2);
1330 padding: 24px;
1331 position: relative;
1332 }
1333 .step-num {
1334 font-family: var(--mono);
1335 font-size: 11px;
1336 color: var(--accent);
1337 font-weight: 700;
1338 text-transform: uppercase;
1339 letter-spacing: 1px;
1340 margin-bottom: 10px;
1341 }
1342 .step-title { font-size: 17px; font-weight: 700; margin-bottom: 8px; }
1343 .step-desc { font-size: 13px; color: var(--mute); line-height: 1.6; margin-bottom: 16px; }
1344 .step-cmd {
1345 font-family: var(--mono);
1346 font-size: 12px;
1347 background: var(--bg3);
1348 border: 1px solid var(--border);
1349 border-radius: 5px;
1350 padding: 10px 14px;
1351 color: var(--accent2);
1352 }
1353
1354 /* ---- Code block ---- */
1355 .code-wrap {
1356 border: 1px solid var(--border);
1357 border-radius: var(--r);
1358 overflow: hidden;
1359 }
1360 .code-bar {
1361 background: var(--bg3);
1362 border-bottom: 1px solid var(--border);
1363 padding: 8px 16px;
1364 display: flex;
1365 align-items: center;
1366 gap: 8px;
1367 }
1368 .code-bar-dot {
1369 width: 10px; height: 10px; border-radius: 50%;
1370 }
1371 .code-bar-title {
1372 font-family: var(--mono);
1373 font-size: 12px;
1374 color: var(--mute);
1375 margin-left: 6px;
1376 }
1377 .code-body {
1378 background: #0a0e14;
1379 padding: 20px 24px;
1380 font-family: var(--mono);
1381 font-size: 12.5px;
1382 line-height: 1.7;
1383 color: #abb2bf;
1384 white-space: pre;
1385 overflow-x: auto;
1386 }
1387 /* Simple syntax highlights */
1388 .kw { color: #c678dd; }
1389 .kw2 { color: #e06c75; }
1390 .fn { color: #61afef; }
1391 .str { color: #98c379; }
1392 .cmt { color: #5c6370; font-style: italic; }
1393 .cls { color: #e5c07b; }
1394 .typ { color: #56b6c2; }
1395
1396 /* ---- Active domains grid ---- */
1397 .domain-grid {
1398 display: grid;
1399 grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
1400 gap: 20px;
1401 }
1402 .domain-card {
1403 border: 1px solid var(--border);
1404 border-radius: var(--r);
1405 background: var(--bg2);
1406 overflow: hidden;
1407 transition: border-color 0.2s, transform 0.15s;
1408 }
1409 .domain-card:hover { border-color: var(--accent); transform: translateY(-2px); }
1410 .domain-card.active-domain { border-color: rgba(63,185,80,0.4); }
1411 .domain-card-hdr {
1412 background: var(--bg3);
1413 padding: 12px 16px;
1414 border-bottom: 1px solid var(--border);
1415 display: flex;
1416 align-items: center;
1417 gap: 10px;
1418 }
1419 .active-badge { font-size: 11px; padding: 2px 8px; border-radius: 4px; background: rgba(63,185,80,0.12); border: 1px solid rgba(63,185,80,0.3); color: var(--green); font-family: var(--mono); }
1420 .reg-badge { font-size: 11px; padding: 2px 8px; border-radius: 4px; background: var(--bg); border: 1px solid var(--border); color: var(--mute); font-family: var(--mono); }
1421 .active-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); margin-left: auto; }
1422 .domain-name-lg { font-family: var(--mono); font-size: 16px; font-weight: 700; color: var(--text); }
1423 .domain-card-body { padding: 16px; }
1424 .domain-desc { font-size: 13px; color: var(--mute); margin-bottom: 12px; }
1425 .cap-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
1426 .cap-pill { font-size: 10px; padding: 2px 8px; border-radius: 12px; border: 1px solid var(--border); color: var(--mute); background: var(--bg3); }
1427 .cap-pill.cap-crdt { border-color: rgba(188,140,255,0.4); color: var(--purple); background: rgba(188,140,255,0.08); }
1428 .cap-pill.cap-ot-merge { border-color: rgba(88,166,255,0.4); color: var(--accent2); background: rgba(88,166,255,0.08); }
1429 .cap-pill.cap-domain-schema { border-color: rgba(63,185,80,0.4); color: var(--green); background: rgba(63,185,80,0.08); }
1430 .cap-pill.cap-typed-deltas { border-color: rgba(249,168,37,0.4); color: #f9a825; background: rgba(249,168,37,0.08); }
1431 .dim-row { font-size: 11px; color: var(--dim); }
1432 .dim-tag { color: var(--mute); }
1433
1434 /* ---- Planned domains ---- */
1435 .planned-grid {
1436 display: grid;
1437 grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
1438 gap: 16px;
1439 }
1440 .planned-card {
1441 border: 1px solid var(--border);
1442 border-radius: var(--r);
1443 background: var(--bg2);
1444 padding: 20px 16px;
1445 display: flex;
1446 flex-direction: column;
1447 gap: 8px;
1448 transition: border-color 0.2s, transform 0.15s;
1449 }
1450 .planned-card:hover { border-color: var(--card-accent,var(--accent)); transform: translateY(-2px); }
1451 .planned-card.yours { border: 2px dashed var(--accent); background: rgba(79,142,247,0.04); }
1452 .planned-icon { line-height: 0; }
1453 .planned-icon .icon { width: 28px; height: 28px; }
1454 .planned-name { font-size: 15px; font-weight: 700; color: var(--text); }
1455 .planned-tag { font-size: 12px; color: var(--mute); line-height: 1.5; }
1456 .planned-dims { font-size: 10px; color: var(--dim); }
1457 .coming-soon { font-size: 10px; color: var(--dim); border: 1px solid var(--border); border-radius: 12px; padding: 2px 8px; display: inline-block; margin-top: 4px; }
1458 .cta-btn {
1459 display: inline-block;
1460 margin-top: 6px;
1461 font-size: 12px;
1462 font-weight: 600;
1463 color: var(--accent2);
1464 border: 1px solid rgba(88,166,255,0.4);
1465 border-radius: 4px;
1466 padding: 4px 12px;
1467 text-decoration: none;
1468 transition: background 0.15s;
1469 }
1470 .cta-btn:hover { background: rgba(88,166,255,0.1); text-decoration: none; }
1471
1472 /* ---- Distribution tiers ---- */
1473 .dist-grid {
1474 display: grid;
1475 grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
1476 gap: 24px;
1477 }
1478 .dist-card {
1479 border: 1px solid var(--border);
1480 border-top: 3px solid var(--dist-color, var(--accent));
1481 border-radius: var(--r);
1482 background: var(--bg2);
1483 padding: 24px;
1484 transition: transform 0.15s;
1485 }
1486 .dist-card:hover { transform: translateY(-2px); }
1487 .dist-header { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 14px; }
1488 .dist-icon { line-height: 0; flex-shrink: 0; }
1489 .dist-icon .icon { width: 26px; height: 26px; }
1490 .dist-tier { font-family: var(--mono); font-size: 11px; color: var(--dist-color,var(--accent)); letter-spacing: 1px; text-transform: uppercase; font-weight: 700; }
1491 .dist-title { font-size: 14px; font-weight: 600; color: var(--text); margin-top: 2px; }
1492 .dist-desc { font-size: 13px; color: var(--mute); margin-bottom: 16px; line-height: 1.6; }
1493 .dist-steps { list-style: none; counter-reset: step; display: flex; flex-direction: column; gap: 6px; }
1494 .dist-steps li { counter-increment: step; display: flex; align-items: flex-start; gap: 8px; font-size: 12px; color: var(--mute); }
1495 .dist-steps li::before { content: counter(step); min-width: 18px; height: 18px; background: var(--dist-color,var(--accent)); color: #000; border-radius: 50%; font-size: 10px; font-weight: 700; display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-top: 1px; }
1496 .dist-steps code { background: var(--bg3); border: 1px solid var(--border); border-radius: 4px; padding: 1px 6px; font-size: 11px; }
1497
1498 /* ---- MuseHub teaser ---- */
1499 .musehub-section {
1500 background: linear-gradient(135deg, #0d1117 0%, #1a0d2e 50%, #0d1117 100%);
1501 padding: 80px 40px;
1502 text-align: center;
1503 border-top: 1px solid var(--border);
1504 }
1505 .musehub-logo {
1506 margin-bottom: 20px;
1507 line-height: 0;
1508 }
1509 .musehub-logo .icon { width: 48px; height: 48px; stroke: #bc8cff; }
1510 .musehub-section h2 {
1511 font-size: 36px;
1512 font-weight: 800;
1513 letter-spacing: -1px;
1514 margin-bottom: 12px;
1515 }
1516 .musehub-section h2 span {
1517 background: linear-gradient(135deg, #bc8cff, #4f8ef7);
1518 -webkit-background-clip: text;
1519 -webkit-text-fill-color: transparent;
1520 background-clip: text;
1521 }
1522 .musehub-desc {
1523 font-size: 16px;
1524 color: var(--mute);
1525 max-width: 560px;
1526 margin: 0 auto 36px;
1527 }
1528 .musehub-desc strong { color: var(--text); }
1529 .musehub-features {
1530 display: flex;
1531 gap: 24px;
1532 justify-content: center;
1533 flex-wrap: wrap;
1534 margin-bottom: 40px;
1535 }
1536 .mh-feature {
1537 background: var(--bg2);
1538 border: 1px solid rgba(188,140,255,0.2);
1539 border-radius: var(--r);
1540 padding: 16px 20px;
1541 text-align: left;
1542 min-width: 180px;
1543 }
1544 .mh-feature-icon { margin-bottom: 10px; line-height: 0; }
1545 .mh-feature-icon .icon { width: 22px; height: 22px; stroke: #bc8cff; }
1546 .mh-feature-title { font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 4px; }
1547 .mh-feature-desc { font-size: 12px; color: var(--mute); }
1548 .musehub-status {
1549 display: inline-flex;
1550 align-items: center;
1551 gap: 8px;
1552 background: rgba(188,140,255,0.1);
1553 border: 1px solid rgba(188,140,255,0.3);
1554 border-radius: 20px;
1555 padding: 8px 20px;
1556 font-size: 13px;
1557 color: var(--purple);
1558 }
1559 .mh-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--purple); animation: pulse 2s ease-in-out infinite; }
1560 @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
1561
1562 /* ---- Footer ---- */
1563 footer {
1564 background: var(--bg2);
1565 border-top: 1px solid var(--border);
1566 padding: 24px 40px;
1567 display: flex;
1568 justify-content: space-between;
1569 align-items: center;
1570 flex-wrap: wrap;
1571 gap: 12px;
1572 font-size: 13px;
1573 color: var(--mute);
1574 }
1575 footer a { color: var(--accent2); }
1576
1577 /* ---- Nav ---- */
1578 nav {
1579 background: var(--bg2);
1580 border-bottom: 1px solid var(--border);
1581 padding: 0 40px;
1582 display: flex;
1583 align-items: center;
1584 gap: 0;
1585 height: 52px;
1586 }
1587 .nav-logo {
1588 font-family: var(--mono);
1589 font-size: 16px;
1590 font-weight: 700;
1591 color: var(--accent2);
1592 margin-right: 32px;
1593 text-decoration: none;
1594 }
1595 .nav-logo:hover { text-decoration: none; }
1596 .nav-link {
1597 font-size: 13px;
1598 color: var(--mute);
1599 padding: 0 14px;
1600 height: 100%;
1601 display: flex;
1602 align-items: center;
1603 border-bottom: 2px solid transparent;
1604 text-decoration: none;
1605 transition: color 0.15s, border-color 0.15s;
1606 }
1607 .nav-link:hover { color: var(--text); text-decoration: none; }
1608 .nav-link.current { color: var(--text); border-bottom-color: var(--accent); }
1609 .nav-spacer { flex: 1; }
1610 .nav-badge {
1611 font-size: 11px;
1612 background: rgba(79,142,247,0.12);
1613 border: 1px solid rgba(79,142,247,0.3);
1614 color: var(--accent2);
1615 border-radius: 4px;
1616 padding: 2px 8px;
1617 font-family: var(--mono);
1618 }
1619 </style>
1620 </head>
1621 <body>
1622
1623 <nav>
1624 <a class="nav-logo" href="#">muse</a>
1625 <a class="nav-link" href="tour_de_force.html">Demo</a>
1626 <a class="nav-link current" href="index.html">Domain Registry</a>
1627 <a class="nav-link" href="../docs/guide/plugin-authoring-guide.md">Plugin Guide</a>
1628 <div class="nav-spacer"></div>
1629 <span class="nav-badge">v0.1.1</span>
1630 </nav>
1631
1632 <!-- =================== HERO =================== -->
1633 <div class="hero">
1634 <h1 class="hero-wordmark">muse</h1>
1635 <div class="hero-version-any">Version Anything</div>
1636 <p class="hero-sub">
1637 One protocol. Any domain. <strong>Six methods</strong> between you and a
1638 complete version control system — branching, merging, conflict resolution,
1639 time-travel, and typed diffs — for free.
1640 </p>
1641 <div class="hero-cta-row">
1642 <a class="btn-primary" href="#build">Build a Domain Plugin</a>
1643 <a class="btn-outline" href="tour_de_force.html">Watch the Demo →</a>
1644 </div>
1645 <div class="domain-ticker">
1646 <div class="ticker-track">
1647 <span class="ticker-item active">{{ICON_MUSIC}} music</span>
1648 <span class="ticker-item">{{ICON_GENOMICS}} genomics</span>
1649 <span class="ticker-item">{{ICON_CUBE}} 3d-spatial</span>
1650 <span class="ticker-item">{{ICON_TRENDING}} financial</span>
1651 <span class="ticker-item">{{ICON_ATOM}} simulation</span>
1652 <span class="ticker-item">{{ICON_ACTIVITY}} proteomics</span>
1653 <span class="ticker-item">{{ICON_PEN_TOOL}} cad</span>
1654 <span class="ticker-item">{{ICON_ZAP}} game-state</span>
1655 <span class="ticker-item">{{ICON_PLUS}} your-domain</span>
1656 <!-- duplicate for seamless loop -->
1657 <span class="ticker-item active">{{ICON_MUSIC}} music</span>
1658 <span class="ticker-item">{{ICON_GENOMICS}} genomics</span>
1659 <span class="ticker-item">{{ICON_CUBE}} 3d-spatial</span>
1660 <span class="ticker-item">{{ICON_TRENDING}} financial</span>
1661 <span class="ticker-item">{{ICON_ATOM}} simulation</span>
1662 <span class="ticker-item">{{ICON_ACTIVITY}} proteomics</span>
1663 <span class="ticker-item">{{ICON_PEN_TOOL}} cad</span>
1664 <span class="ticker-item">{{ICON_ZAP}} game-state</span>
1665 <span class="ticker-item">{{ICON_PLUS}} your-domain</span>
1666 </div>
1667 </div>
1668 </div>
1669
1670 <!-- =================== PROTOCOL =================== -->
1671 <section id="protocol">
1672 <div class="inner">
1673 <div class="section-eyebrow">The Contract</div>
1674 <h2>The MuseDomainPlugin Protocol</h2>
1675 <p class="section-lead">
1676 Every domain — music, genomics, 3D spatial, financial models — implements
1677 the same <strong>six-method protocol</strong>. The core engine handles
1678 everything else: content-addressed storage, DAG, branches, log, merge base,
1679 cherry-pick, revert, stash, tags.
1680 </p>
1681
1682 <div class="proto-layout">
1683 <div class="stat-strip">
1684 <div class="stat-cell"><span class="stat-num">6</span><span class="stat-lbl">methods to implement</span></div>
1685 <div class="stat-cell"><span class="stat-num">14</span><span class="stat-lbl">CLI commands, free</span></div>
1686 <div class="stat-cell"><span class="stat-num">∞</span><span class="stat-lbl">domains possible</span></div>
1687 <div class="stat-cell"><span class="stat-num">0</span><span class="stat-lbl">core changes needed</span></div>
1688 </div>
1689 <div class="proto-table">
1690 <div class="proto-row hdr">
1691 <div class="proto-method">Method</div>
1692 <div class="proto-sig">Signature</div>
1693 <div class="proto-desc">Purpose</div>
1694 </div>
1695 <div class="proto-row">
1696 <div class="proto-method">snapshot</div>
1697 <div class="proto-sig">snapshot(live) → StateSnapshot</div>
1698 <div class="proto-desc">Capture current state as a content-addressable blob</div>
1699 </div>
1700 <div class="proto-row">
1701 <div class="proto-method">diff</div>
1702 <div class="proto-sig">diff(base, target) → StateDelta</div>
1703 <div class="proto-desc">Compute minimal change between two snapshots (added · removed · modified)</div>
1704 </div>
1705 <div class="proto-row">
1706 <div class="proto-method">merge</div>
1707 <div class="proto-sig">merge(base, left, right) → MergeResult</div>
1708 <div class="proto-desc">Three-way reconcile divergent state lines; surface conflicts per dimension</div>
1709 </div>
1710 <div class="proto-row">
1711 <div class="proto-method">drift</div>
1712 <div class="proto-sig">drift(committed, live) → DriftReport</div>
1713 <div class="proto-desc">Detect uncommitted changes between HEAD and working state</div>
1714 </div>
1715 <div class="proto-row">
1716 <div class="proto-method">apply</div>
1717 <div class="proto-sig">apply(delta, live) → LiveState</div>
1718 <div class="proto-desc">Apply a delta during checkout to reconstruct historical state</div>
1719 </div>
1720 <div class="proto-row">
1721 <div class="proto-method">schema</div>
1722 <div class="proto-sig">schema() → DomainSchema</div>
1723 <div class="proto-desc">Declare data structure — drives diff algorithm selection per dimension</div>
1724 </div>
1725 </div>
1726 </div>
1727 </div>
1728 </section>
1729
1730 {{DIFF_ALGEBRA}}
1731
1732 <!-- =================== ENGINE CAPABILITIES =================== -->
1733 <section id="capabilities" style="background:var(--bg2)">
1734 <div class="inner">
1735 <div class="section-eyebrow">Engine Capabilities</div>
1736 <h2>What Every Plugin Gets for Free</h2>
1737 <p class="section-lead">
1738 The core engine provides four advanced capabilities that any domain plugin
1739 can opt into. Implement the protocol — the engine does the rest.
1740 </p>
1741
1742 <div class="cap-showcase-grid">
1743
1744 <div class="cap-showcase-card" style="--cap-color:#f9a825">
1745 <div class="cap-showcase-header">
1746 <span class="cap-showcase-badge" style="color:#f9a825;background:#f9a82515;border-color:#f9a82540">
1747 {{ICON_CODE}} Typed Delta Algebra
1748 </span>
1749 <span class="cap-showcase-sub">StructuredDelta — every change is a typed operation</span>
1750 </div>
1751 <div class="cap-showcase-body">
1752 <p class="cap-showcase-desc">
1753 Unlike Git's blob diffs, Muse deltas are <strong>typed objects</strong>:
1754 <code>InsertOp</code>, <code>ReplaceOp</code>, <code>DeleteOp</code> — each
1755 carrying the address, before/after hashes, and affected dimensions.
1756 Machine-readable with <code>muse show --json</code>.
1757 </p>
1758 <pre class="cap-showcase-output" data-lang="json">{{TYPED_DELTA_EXAMPLE}}</pre>
1759 </div>
1760 </div>
1761
1762 <div class="cap-showcase-card" style="--cap-color:#58a6ff">
1763 <div class="cap-showcase-header">
1764 <span class="cap-showcase-badge" style="color:#58a6ff;background:#58a6ff15;border-color:#58a6ff40">
1765 {{ICON_LAYERS}} Domain Schema
1766 </span>
1767 <span class="cap-showcase-sub">Per-domain dimensions drive diff algorithm selection</span>
1768 </div>
1769 <div class="cap-showcase-body">
1770 <p class="cap-showcase-desc">
1771 Each plugin's <code>schema()</code> method declares its dimensions and merge mode.
1772 The engine uses this to select the right diff algorithm per dimension and to
1773 surface only the dimensions that actually conflict.
1774 </p>
1775 <div class="cap-showcase-domain-grid" id="schema-domain-grid">
1776 {{ACTIVE_DOMAINS}}
1777 </div>
1778 </div>
1779 </div>
1780
1781 <div class="cap-showcase-card" style="--cap-color:#ef5350">
1782 <div class="cap-showcase-header">
1783 <span class="cap-showcase-badge" style="color:#ef5350;background:#ef535015;border-color:#ef535040">
1784 {{ICON_GIT_MERGE}} OT Merge
1785 </span>
1786 <span class="cap-showcase-sub">Operational transformation — independent ops commute automatically</span>
1787 </div>
1788 <div class="cap-showcase-body">
1789 <p class="cap-showcase-desc">
1790 Plugins implementing <strong>StructuredMergePlugin</strong> get operational
1791 transformation. Operations at different addresses commute automatically —
1792 only operations on the same address with incompatible intent surface a conflict.
1793 </p>
1794 <div class="ot-scenarios">
1795
1796 <div class="ot-scenario ot-clean">
1797 <div class="ot-scenario-hdr">
1798 <span class="ot-scenario-label">Scenario A</span>
1799 <span class="ot-scenario-title">Independent ops at different addresses</span>
1800 </div>
1801 <div class="ot-ops">
1802 <div class="ot-op">
1803 <span class="ot-op-side">left</span>
1804 <span class="ot-op-type ot-insert">InsertOp</span>
1805 <span class="ot-op-addr">"ot-notes-a.mid"</span>
1806 <span class="ot-op-meta">tick=0 · C4 E4 G4</span>
1807 </div>
1808 <div class="ot-op">
1809 <span class="ot-op-side">right</span>
1810 <span class="ot-op-type ot-insert">InsertOp</span>
1811 <span class="ot-op-addr">"ot-notes-b.mid"</span>
1812 <span class="ot-op-meta">tick=480 · D4 F4 A4</span>
1813 </div>
1814 </div>
1815 <div class="ot-result">
1816 <span class="ot-reason">transform → no overlap → ops commute</span>
1817 <span class="ot-badge ot-badge-clean">{{ICON_CHECK_CIRCLE}} Clean merge · both files applied</span>
1818 </div>
1819 </div>
1820
1821 <div class="ot-scenario ot-conflict">
1822 <div class="ot-scenario-hdr">
1823 <span class="ot-scenario-label">Scenario B</span>
1824 <span class="ot-scenario-title">Same address, conflicting musical intent</span>
1825 </div>
1826 <div class="ot-ops">
1827 <div class="ot-op">
1828 <span class="ot-op-side">left</span>
1829 <span class="ot-op-type ot-replace">ReplaceOp</span>
1830 <span class="ot-op-addr">"shared-melody.mid"</span>
1831 <span class="ot-op-meta">C4 E4 G4 · major triad</span>
1832 </div>
1833 <div class="ot-op">
1834 <span class="ot-op-side">right</span>
1835 <span class="ot-op-type ot-replace">ReplaceOp</span>
1836 <span class="ot-op-addr">"shared-melody.mid"</span>
1837 <span class="ot-op-meta">C4 Eb4 G4 · minor triad</span>
1838 </div>
1839 </div>
1840 <div class="ot-result">
1841 <span class="ot-reason">transform → same address · non-commuting content</span>
1842 <span class="ot-badge ot-badge-conflict">{{ICON_X_CIRCLE}} Conflict · human resolves</span>
1843 </div>
1844 </div>
1845
1846 </div>
1847 </div>
1848 </div>
1849
1850 <div class="cap-showcase-card" style="--cap-color:#bc8cff">
1851 <div class="cap-showcase-header">
1852 <span class="cap-showcase-badge" style="color:#bc8cff;background:#bc8cff15;border-color:#bc8cff40">
1853 {{ICON_ZAP}} CRDT Primitives
1854 </span>
1855 <span class="cap-showcase-sub">Convergent merge — any two replicas always reach the same state</span>
1856 </div>
1857 <div class="cap-showcase-body">
1858 <p class="cap-showcase-desc">
1859 Plugins implementing <strong>CRDTPlugin</strong> get four battle-tested
1860 convergent data structures. No coordination required between replicas.
1861 </p>
1862 <div class="crdt-mini-grid">
1863 {{CRDT_CARDS}}
1864 </div>
1865 </div>
1866 </div>
1867
1868 </div>
1869 </div>
1870 </section>
1871
1872 <!-- =================== BUILD =================== -->
1873 <section id="build" style="background:var(--bg)">
1874 <div class="inner">
1875 <div class="section-eyebrow">Build</div>
1876 <h2>Build in Three Steps</h2>
1877 <p class="section-lead">
1878 One command scaffolds the entire plugin skeleton. You fill in six methods.
1879 The full VCS follows.
1880 </p>
1881
1882 <div class="steps-grid">
1883 <div class="step-card">
1884 <div class="step-num">Step 1 · Scaffold</div>
1885 <div class="step-title">Generate the skeleton</div>
1886 <div class="step-desc">
1887 One command creates the plugin directory, class, and all six method stubs
1888 with full type annotations.
1889 </div>
1890 <div class="step-cmd">muse domains --new genomics</div>
1891 </div>
1892 <div class="step-card">
1893 <div class="step-num">Step 2 · Implement</div>
1894 <div class="step-title">Fill in the six methods</div>
1895 <div class="step-desc">
1896 Replace each <code>raise NotImplementedError</code> with your domain's
1897 snapshot, diff, merge, drift, apply, and schema logic.
1898 </div>
1899 <div class="step-cmd">vim muse/plugins/genomics/plugin.py</div>
1900 </div>
1901 <div class="step-card">
1902 <div class="step-num">Step 3 · Use</div>
1903 <div class="step-title">Full VCS, instantly</div>
1904 <div class="step-desc">
1905 Register in <code>registry.py</code>, then every Muse command works
1906 for your domain out of the box.
1907 </div>
1908 <div class="step-cmd">muse init --domain genomics</div>
1909 </div>
1910 </div>
1911 </div>
1912 </section>
1913
1914 <!-- =================== CODE =================== -->
1915 <section id="code">
1916 <div class="inner">
1917 <div class="section-eyebrow">The Scaffold</div>
1918 <h2>What <code>muse domains --new genomics</code> produces</h2>
1919 <p class="section-lead">
1920 A fully typed, immediately runnable plugin skeleton. Every method has the
1921 correct signature. You replace the stubs — the protocol does the rest.
1922 </p>
1923 <div class="code-wrap">
1924 <div class="code-bar">
1925 <div class="code-bar-dot" style="background:#ff5f57"></div>
1926 <div class="code-bar-dot" style="background:#febc2e"></div>
1927 <div class="code-bar-dot" style="background:#28c840"></div>
1928 <span class="code-bar-title">muse/plugins/genomics/plugin.py</span>
1929 </div>
1930 <div class="code-body">{{SCAFFOLD_SNIPPET}}</div>
1931 </div>
1932 <p style="margin-top:16px;font-size:13px;color:var(--mute)">
1933 Full walkthrough →
1934 <a href="../docs/guide/plugin-authoring-guide.md">docs/guide/plugin-authoring-guide.md</a>
1935 · CRDT extension →
1936 <a href="../docs/guide/crdt-reference.md">docs/guide/crdt-reference.md</a>
1937 </p>
1938 </div>
1939 </section>
1940
1941 <!-- =================== ACTIVE DOMAINS =================== -->
1942 <section id="registry" style="background:var(--bg2)">
1943 <div class="inner">
1944 <div class="section-eyebrow">Registry</div>
1945 <h2>Registered Domains</h2>
1946 <p class="section-lead">
1947 Domains currently registered in this Muse instance. The active domain
1948 is the one used when you run <code>muse commit</code>, <code>muse diff</code>,
1949 and all other commands.
1950 </p>
1951 <div class="domain-grid">
1952 {{ACTIVE_DOMAINS}}
1953 </div>
1954 </div>
1955 </section>
1956
1957 <!-- =================== PLANNED ECOSYSTEM =================== -->
1958 <section id="ecosystem">
1959 <div class="inner">
1960 <div class="section-eyebrow">Ecosystem</div>
1961 <h2>The Plugin Ecosystem</h2>
1962 <p class="section-lead">
1963 Music is the reference implementation. These are the domains planned
1964 next — and the slot waiting for yours.
1965 </p>
1966 <div class="planned-grid">
1967 {{PLANNED_DOMAINS}}
1968 </div>
1969 </div>
1970 </section>
1971
1972 <!-- =================== DISTRIBUTION =================== -->
1973 <section id="distribute" style="background:var(--bg2)">
1974 <div class="inner">
1975 <div class="section-eyebrow">Distribution</div>
1976 <h2>How to Share Your Plugin</h2>
1977 <p class="section-lead">
1978 Three tiers of distribution — from local prototype to globally searchable
1979 registry. Start local, publish when ready.
1980 </p>
1981 <div class="dist-grid">
1982 {{DIST_CARDS}}
1983 </div>
1984 </div>
1985 </section>
1986
1987 <!-- =================== MUSEHUB TEASER =================== -->
1988 <div class="musehub-section">
1989 <div class="musehub-logo">{{ICON_GLOBE}}</div>
1990 <h2><span>MuseHub</span> is coming</h2>
1991 <p class="musehub-desc">
1992 A <strong>centralized, searchable registry</strong> for Muse domain plugins —
1993 think npm or crates.io, but for any multidimensional versioned state.
1994 One command to publish. One command to install.
1995 </p>
1996 <div class="musehub-features">
1997 <div class="mh-feature">
1998 <div class="mh-feature-icon">{{ICON_SEARCH}}</div>
1999 <div class="mh-feature-title">Searchable</div>
2000 <div class="mh-feature-desc">Find plugins by domain, capability, or keyword</div>
2001 </div>
2002 <div class="mh-feature">
2003 <div class="mh-feature-icon">{{ICON_PACKAGE}}</div>
2004 <div class="mh-feature-title">Versioned</div>
2005 <div class="mh-feature-desc">Semantic versioning, pinned installs, changelogs</div>
2006 </div>
2007 <div class="mh-feature">
2008 <div class="mh-feature-icon">{{ICON_LOCK}}</div>
2009 <div class="mh-feature-title">Private registries</div>
2010 <div class="mh-feature-desc">Self-host for enterprise or research teams</div>
2011 </div>
2012 <div class="mh-feature">
2013 <div class="mh-feature-icon">{{ICON_ZAP}}</div>
2014 <div class="mh-feature-title">One command</div>
2015 <div class="mh-feature-desc"><code>muse init --domain @musehub/genomics</code></div>
2016 </div>
2017 </div>
2018 <div class="musehub-status">
2019 <div class="mh-dot"></div>
2020 MuseHub — planned · building in public at <a href="https://github.com/cgcardona/musehub" target="_blank" rel="noopener noreferrer" style="color:inherit;text-decoration:underline;text-underline-offset:3px;">github.com/cgcardona/musehub</a>
2021 </div>
2022 </div>
2023
2024 <footer>
2025 <span>Muse v0.1.1 · domain-agnostic version control for multidimensional state</span>
2026 <span>
2027 <a href="tour_de_force.html">Demo</a> ·
2028 <a href="https://github.com/cgcardona/muse">GitHub</a> ·
2029 <a href="../docs/guide/plugin-authoring-guide.md">Plugin Guide</a>
2030 </span>
2031 </footer>
2032
2033 <script>
2034 (function () {
2035 function esc(s) {
2036 return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
2037 }
2038
2039 function tokenizeJSON(raw) {
2040 let html = '';
2041 let i = 0;
2042 while (i < raw.length) {
2043 // Comment line starting with #
2044 if (raw[i] === '#') {
2045 const end = raw.indexOf('\n', i);
2046 const line = end === -1 ? raw.slice(i) : raw.slice(i, end);
2047 html += '<span style="color:#5c6370;font-style:italic">' + esc(line) + '</span>';
2048 i += line.length;
2049 continue;
2050 }
2051 // String literal
2052 if (raw[i] === '"') {
2053 let j = i + 1;
2054 while (j < raw.length && raw[j] !== '"') {
2055 if (raw[j] === '\\') j++;
2056 j++;
2057 }
2058 j++;
2059 const str = raw.slice(i, j);
2060 // Peek past whitespace — key if followed by ':'
2061 let k = j;
2062 while (k < raw.length && (raw[k] === ' ' || raw[k] === '\t')) k++;
2063 const color = raw[k] === ':' ? '#61afef' : '#98c379';
2064 html += '<span style="color:' + color + '">' + esc(str) + '</span>';
2065 i = j;
2066 continue;
2067 }
2068 // Number (including negative)
2069 if (/[0-9]/.test(raw[i]) || (raw[i] === '-' && /[0-9]/.test(raw[i + 1] || ''))) {
2070 let j = i;
2071 if (raw[j] === '-') j++;
2072 while (j < raw.length && /[0-9.eE+-]/.test(raw[j])) j++;
2073 html += '<span style="color:#d19a66">' + esc(raw.slice(i, j)) + '</span>';
2074 i = j;
2075 continue;
2076 }
2077 // Keywords: true / false / null
2078 const kws = [['true', '#c678dd'], ['false', '#c678dd'], ['null', '#c678dd']];
2079 let matched = false;
2080 for (const [kw, col] of kws) {
2081 if (raw.slice(i, i + kw.length) === kw) {
2082 html += '<span style="color:' + col + '">' + kw + '</span>';
2083 i += kw.length;
2084 matched = true;
2085 break;
2086 }
2087 }
2088 if (matched) continue;
2089 // Default character (punctuation / whitespace)
2090 html += esc(raw[i]);
2091 i++;
2092 }
2093 return html;
2094 }
2095
2096 document.querySelectorAll('pre[data-lang="json"]').forEach(function (pre) {
2097 pre.innerHTML = tokenizeJSON(pre.textContent);
2098 });
2099 })();
2100 </script>
2101
2102 </body>
2103 </html>
2104 """
2105
2106
2107 # ---------------------------------------------------------------------------
2108 # Entry point
2109 # ---------------------------------------------------------------------------
2110
2111 if __name__ == "__main__":
2112 import argparse
2113
2114 parser = argparse.ArgumentParser(
2115 description="Generate the Muse domain registry HTML page"
2116 )
2117 parser.add_argument(
2118 "--out",
2119 default=str(_ROOT / "artifacts" / "domain_registry.html"),
2120 help="Output HTML path",
2121 )
2122 args = parser.parse_args()
2123
2124 out_path = pathlib.Path(args.out)
2125 out_path.parent.mkdir(parents=True, exist_ok=True)
2126
2127 print("Generating domain_registry.html...")
2128 render(out_path)
2129 print(f"Open: file://{out_path.resolve()}")