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