Humuhumu33 commited on
Commit
ca50dda
Β·
verified Β·
1 Parent(s): 405d55f

wallet frame kappa-objects: theme/mobile css + teleport/theme js (fix half-shipped kappa-links)

Browse files
sha256/31365b0b2f83cc3ae69f1ab91477a7cbed4fd2ab00366a84d5aea9212db0d4dc ADDED
@@ -0,0 +1,886 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Holo Theme (holo-theme.js) β€” the Hologram OS theme runtime. Zero dependencies,
2
+ // content-addressable. A self-contained module: call it once and it brings its own
3
+ // stylesheet β€” <script src="…/holo-theme.js"></script> is the whole adoption.
4
+ // The lean OS service the brief calls a "unified runtime theme engine": it resolves the
5
+ // initial theme, applies it, persists the user's choice, and propagates live changes
6
+ // across the whole nested-holospace tree β€” instantly, no rebuild, no restart.
7
+ //
8
+ // W3C-native end to end: state is expressed only as CSS custom properties + the
9
+ // color-scheme/light-dark() palette attribute (holo-theme.css); transport is the HTML
10
+ // Standard's postMessage; the theme's identity is a ΞΊ artifact (ADR-022).
11
+ //
12
+ // Precedence model (see THEMING-W3C-ADR.md):
13
+ // β€’ USER accessibility (font scale, density, motion) β€” always wins. Applied here as
14
+ // INLINE custom properties on :root, so it beats app stylesheets. enforce-os adds
15
+ // !important so it beats app !important too.
16
+ // β€’ App vs OS aesthetics β€” negotiated. An app declares <meta name="holo-theme-policy"
17
+ // content="own"> to keep its colors; default "adopt" lets it follow the OS palette
18
+ // (apps that use the --holo-* tokens follow automatically; apps with private vars
19
+ // resist until migrated β€” an honest limit, like canvas/VM surfaces).
20
+ //
21
+ // API (window.HoloTheme):
22
+ // .get() β†’ current state
23
+ // .setPalette("auto"|"light"|"dark")
24
+ // .setPresentation("standard"|"immersive")
25
+ // .setFontScale(n) β†’ 0.85 … 1.4 (Settings β€Ί Display β€Ί Text size)
26
+ // .setFontFamily(css-font-list) β†’ UI typeface (Settings β€Ί Display β€Ί Typeface)
27
+ // .setDensity(n) / .setAccent(c) / .reset()
28
+ // .openSettings() β†’ the built-in Display settings panel (any surface)
29
+
30
+ (function () {
31
+ "use strict";
32
+ if (window.HoloTheme) return;
33
+
34
+ // Bundled: bring our own stylesheet so adopting Holo Theme is a single <script> call.
35
+ // A page may still pre-link holo-theme.css in <head> to avoid any flash β€” we skip
36
+ // injection if it is already present.
37
+ (function injectCss() {
38
+ try {
39
+ if (document.querySelector('link[href*="holo-theme.css"]')) return;
40
+ var src = (document.currentScript && document.currentScript.src) || "";
41
+ var href = /\/\.holo\/sha256\//.test(src) ? "/_shared/holo-theme.css" : src ? src.replace(/holo-theme\.js(\?.*)?$/, "holo-theme.css") : "_shared/holo-theme.css";
42
+ var l = document.createElement("link"); l.rel = "stylesheet"; l.href = href;
43
+ (document.head || document.documentElement).appendChild(l);
44
+ } catch (e) {}
45
+ })();
46
+
47
+ // Resolve sibling shared files (to lazy-load the DTCG format module on demand).
48
+ var SELF = (document.currentScript && document.currentScript.src) || "";
49
+ // ΞΊ-routed (src is /.holo/sha256/<hex>.js): siblings can't be path-resolved β†’ use the canonical _shared mount.
50
+ var SHARED = /\/\.holo\/sha256\//.test(SELF) ? "/_shared/" : SELF ? SELF.replace(/holo-theme\.js(\?.*)?$/, "") : "_shared/";
51
+
52
+ // Bootstrap the Holo UX experience layer (ADR-028): device-tier resolution, golden-ratio
53
+ // proportion (holo-ux.js self-injects holo-phi.css), and propagation down the holospace tree.
54
+ // Loading it from the theme runtime β€” which every Holo Theme / Holo UI citizen already loads
55
+ // (the kernel injects it too) β€” makes every citizen a Holo UX citizen with NO per-app script
56
+ // tag: one canonical wire (Law L2). Idempotent + additive (holo-ux.js guards its own effects).
57
+ (function bootHoloUx() {
58
+ try {
59
+ if (window.HoloUX || document.querySelector('script[src*="holo-ux.js"]')) return;
60
+ var s = document.createElement("script"); s.type = "module"; s.src = SHARED + "holo-ux.js";
61
+ (document.head || document.documentElement).appendChild(s);
62
+ } catch (e) {}
63
+ })();
64
+
65
+ // Bootstrap the Holo FX motion engine (the OS's faithful adoption of unicode-animations):
66
+ // window.HoloFX β€” one sharp, content-addressed loading vocabulary (braille Β· scan Β· dna Β·
67
+ // cascade) for every surface. Same canonical wire as Holo UX (Law L2): every Holo Theme
68
+ // citizen gets it with NO per-app script tag. Classic script (sets window.HoloFX), idempotent.
69
+ (function bootHoloFx() {
70
+ try {
71
+ if (window.HoloFX || document.querySelector('script[src*="holo-fx.js"]')) return;
72
+ var s = document.createElement("script"); s.src = SHARED + "holo-fx.js";
73
+ (document.head || document.documentElement).appendChild(s);
74
+ } catch (e) {}
75
+ })();
76
+
77
+ // Bootstrap the canonical ΞΊβ†’render path (window.HoloRender): the ONE lean, low-latency way every app
78
+ // displays a content-addressed UOR object β€” resolve by ΞΊ (arena-cached), verify by re-derivation, mount
79
+ // its ORIGINAL bytes (no compiler on the hot path). Same canonical wire as Holo UX / FX / Telemetry (Law
80
+ // L2): every Holo Theme citizen gets it with NO per-app script tag. The renderer rides the substrate's
81
+ // SINGLE resolver (resolveByKappa) + ΞΊ-route when present, else its own re-derive fallback (same L5 law).
82
+ (function bootHoloRender() {
83
+ try {
84
+ if (window.HoloRender || window.__holoRenderBoot) return;
85
+ window.__holoRenderBoot = 1;
86
+ var code =
87
+ 'import HoloRender from "' + SHARED + 'holo-render.js";\n' +
88
+ 'const route = (h) => "/.holo/sha256/" + h;\n' +
89
+ 'let resolver;\n' +
90
+ 'try { const m = await import("/holo-resolver.mjs"); const store = new Map();\n' +
91
+ ' const src = async (k) => { try { const r = await fetch(route(String(k).split(":").pop())); return r.ok ? new Uint8Array(await r.arrayBuffer()) : null; } catch (e) { return null; } };\n' +
92
+ ' resolver = (k) => m.resolveByKappa(k, [src], store); } catch (e) {}\n' +
93
+ 'try { await HoloRender.configure({ base: "/", route, resolver }); window.HoloRender = HoloRender;\n' +
94
+ ' (document.documentElement || document).dispatchEvent(new Event("holo-render-ready")); } catch (e) { console.warn("holo-render boot:", e); }\n';
95
+ var s = document.createElement("script"); s.type = "module"; s.textContent = code;
96
+ (document.head || document.documentElement).appendChild(s);
97
+ } catch (e) {}
98
+ })();
99
+
100
+ // Clean chrome (Law L2, one universal wire): the OS injects a row of per-app floating "chrome" tiles
101
+ // (Open Holo UI Β· capture-a-thought Β· go-live Β· capture-screen Β· manage). They clutter every app's
102
+ // surface; remove them OS-wide for the cleanest UX. One CSS rule applies in every app that loads the
103
+ // engine β€” no per-app edit. These actions remain reachable from the shell/dock, not over each app.
104
+ (function bootCleanChrome() {
105
+ try {
106
+ if (document.getElementById("holo-clean-chrome")) return;
107
+ var s = document.createElement("style"); s.id = "holo-clean-chrome";
108
+ s.textContent = "#holo-themedash-btn,#holo-notepad-btn,#holo-stream-btn,#holo-capture-btn,#holo-manage-btn{display:none!important}";
109
+ (document.head || document.documentElement).appendChild(s);
110
+ } catch (e) {}
111
+ })();
112
+
113
+ // Bootstrap the Remix layer (window.HoloEdit): right-click any ΞΊ-object β†’ Inspect Β· Edit Β· Share Β· Move Β·
114
+ // Hide Β· Delete; an edit forks a NEW ΞΊ (auto-persisted, re-derivable, shareable). Same canonical wire as
115
+ // UX / FX / Render (Law L2): the shell β€” the container for every app β€” and every app inherit it with NO
116
+ // per-app tag, and agents get the identical powers via window.HoloEdit.api. Idempotent (it self-guards).
117
+ (function bootHoloEdit() {
118
+ try {
119
+ if (window.HoloEdit || document.querySelector('script[src*="holo-edit.js"]')) return;
120
+ var s = document.createElement("script"); s.defer = true; s.src = SHARED + "holo-edit.js";
121
+ (document.head || document.documentElement).appendChild(s);
122
+ } catch (e) {}
123
+ })();
124
+
125
+ // Bootstrap Holo Telemetry (ADR-0073): system-wide observability native to the substrate β€” the
126
+ // OpenTelemetry data model + W3C Trace Context as content-addressed UOR objects (window.HoloTelemetry).
127
+ // Same canonical wire as Holo UX / FX (Law L2): every Holo Theme citizen gets it with NO per-app script
128
+ // tag. The module self-registers on holo-app-ready; once ready, a tiny PerformanceObserver BRIDGE turns
129
+ // the existing perf marks/measures every app already emits into local spans β€” instrumentation with no
130
+ // per-app code. Telemetry stays LOCAL by default (Law L1); nothing is exported without an explicit,
131
+ // conscience-gated consent. Idempotent + additive (the module guards its own effects).
132
+ (function bootHoloTelemetry() {
133
+ try {
134
+ if (window.HoloTelemetry || document.querySelector('script[src*="holo-telemetry.mjs"]')) return;
135
+ var s = document.createElement("script"); s.type = "module"; s.src = SHARED + "holo-telemetry.mjs";
136
+ (document.head || document.documentElement).appendChild(s);
137
+ // the one canonical bridge: existing PerformanceObserver "measure" entries β†’ local spans. Best-effort,
138
+ // gated by reduced-data-cost intent and never exporting β€” pure local self-observation.
139
+ var bridge = function () {
140
+ try {
141
+ if (!window.HoloTelemetry || window.__holoTelemetryBridge) return;
142
+ window.__holoTelemetryBridge = true;
143
+ if (typeof PerformanceObserver === "undefined") return;
144
+ var tr = window.HoloTelemetry.tracer("perf", "1.0");
145
+ var po = new PerformanceObserver(function (list) {
146
+ for (var i = 0; i < list.getEntries().length; i++) {
147
+ var e = list.getEntries()[i];
148
+ try { tr.startSpan(e.name, { kind: "internal", attributes: { entryType: e.entryType }, start: e.startTime })
149
+ .end({ end: e.startTime + (e.duration || 0) }); } catch (x) {}
150
+ }
151
+ });
152
+ po.observe({ entryTypes: ["measure"] });
153
+ } catch (x) {}
154
+ };
155
+ if (window.HoloTelemetry) bridge();
156
+ else if (document.documentElement) document.documentElement.addEventListener("holo-telemetry-ready", bridge, { once: true });
157
+ } catch (e) {}
158
+ })();
159
+
160
+ // Bootstrap the AUTONOMY SPINE (S0–S4): the OS observes itself (tap), reflects (coherence), proposes
161
+ // (observer), speaks under discipline (courier), and may act bounded (trust). Same canonical wire as
162
+ // Telemetry (Law L2) β€” no per-app script tag. Each module guards its own effects (binds its window.Holo*
163
+ // only when its deps exist, chained by holo-*-ready events); loading them is idempotent + additive. The
164
+ // live cadence is driven by the heal-boot idle loop calling window.HoloSpine.runOnce(tick) β€” proposals
165
+ // only; acting stays behind HoloTrust. Local-first throughout (Law L1).
166
+ (function bootHoloSpine() {
167
+ try {
168
+ if (window.__holoSpineBoot) return; window.__holoSpineBoot = true;
169
+ var mods = ["holo-control-dsp.js", "holo-telemetry-tap.mjs", "holo-coherence.mjs",
170
+ "holo-observer.mjs", "holo-courier.mjs", "holo-trust.mjs", "holo-spine.mjs"];
171
+ mods.forEach(function (m) {
172
+ if (document.querySelector('script[src*="' + m + '"]')) return;
173
+ var s = document.createElement("script");
174
+ s.type = "module"; // every entry in this spine-boot list is an ES module. holo-control-dsp.js is .js-named but `export`s β€” loading it as a classic script threw "Unexpected token 'export'" and silently dropped the DSP core. Module scripts defer by default.
175
+ s.src = SHARED + m;
176
+ (document.head || document.documentElement).appendChild(s);
177
+ });
178
+ } catch (e) {}
179
+ })();
180
+
181
+ var KEY = "holo.theme.v1"; // one OS-wide choice, shared same-origin
182
+ var TKEY = "holo.theme.tokens.v1"; // imported DTCG theme (JSON)
183
+ var CKEY = "holo.theme.tokens.css.v1";// its compiled CSS (fast init, no format module)
184
+ var FKEY = "holo.theme.fonts.v1"; // the live font registry (catalog + imported), JSON
185
+ var FCKEY = "holo.theme.fonts.css.v1";// its compiled @font-face CSS (fast init)
186
+ var LKEY = "holo.theme.lookandfeel.v1";// the active Global Theme's look-and-feel block (JSON)
187
+ var themeJson = null; // the active custom theme (DTCG), if any
188
+ var lookAndFeel = null; // the active Global Theme's look-and-feel (icons/decoration/layout…)
189
+ var userFonts = []; // registered fonts: [{family, category, stack, faces, origin}]
190
+ var _catalog = null; // the ΞΊ-pinned font library (fonts/index.json), lazy
191
+
192
+ // ── Portable themes (DTCG ⇄ CSS via holo-theme-format.js) ────────────────────────
193
+ // A theme is injected as a <style id="holo-theme-tokens"> block (unlayered β†’ it beats
194
+ // holo-theme.css's layered defaults; the engine's inline user overrides still win).
195
+ function injectTokens(cssText) {
196
+ var s = document.getElementById("holo-theme-tokens");
197
+ if (cssText == null || cssText === "") { if (s) s.remove(); return; }
198
+ if (!s) { s = document.createElement("style"); s.id = "holo-theme-tokens"; (document.head || document.documentElement).appendChild(s); }
199
+ s.textContent = cssText;
200
+ }
201
+ function loadFormat() {
202
+ return new Promise(function (res, rej) {
203
+ if (globalThis.HoloThemeFormat) return res(globalThis.HoloThemeFormat);
204
+ var s = document.createElement("script");
205
+ s.src = SHARED + "holo-theme-format.js";
206
+ s.onload = function () { res(globalThis.HoloThemeFormat); };
207
+ s.onerror = function () { rej(new Error("could not load holo-theme-format.js")); };
208
+ (document.head || document.documentElement).appendChild(s);
209
+ });
210
+ }
211
+ function broadcastTokens(css, json) {
212
+ var msg = { type: "holo-theme-tokens", css: css, json: json };
213
+ childFrames().forEach(function (w) { try { w.postMessage(msg, "*"); } catch (e) {} });
214
+ }
215
+ // Import a DTCG theme (object or JSON string): validate β†’ compile β†’ apply β†’ persist β†’ broadcast.
216
+ function importTheme(json, opts) {
217
+ opts = opts || {};
218
+ return loadFormat().then(function (F) {
219
+ if (!F) return { ok: false, errors: ["format module unavailable"] };
220
+ if (typeof json === "string") { try { json = JSON.parse(json); } catch (e) { return { ok: false, errors: ["invalid JSON: " + e.message] }; } }
221
+ var v = F.validate(json); if (!v.ok) return v;
222
+ var css; try { css = F.toCss(json); } catch (e) { return { ok: false, errors: [String(e.message || e)] }; }
223
+ themeJson = json; injectTokens(css);
224
+ if (opts.persist !== false) { try { localStorage.setItem(TKEY, JSON.stringify(json)); localStorage.setItem(CKEY, css); } catch (e) {} }
225
+ apply();
226
+ if (opts.broadcast !== false) broadcastTokens(css, json);
227
+ root.dispatchEvent(new CustomEvent("holo-theme-change", { detail: state, bubbles: false }));
228
+ return { ok: true };
229
+ });
230
+ }
231
+ // Apply a Holo GLOBAL THEME (a KDE Look-and-Feel package adopted as one ΞΊ-object): it IS a
232
+ // DTCG theme (color + font + radius), so its visual layer flows through importTheme unchanged β€”
233
+ // Holo Theme stays the single source of truth (ADR-023). The package's $extensions
234
+ // "org.hologram.lookandfeel" block (icons Β· window decoration Β· layout Β· splash Β· cursor) is
235
+ // surfaced for the shell as data-attributes + a holo-lookandfeel-change event, and persisted.
236
+ function applyGlobalTheme(gt, opts) {
237
+ opts = opts || {};
238
+ if (typeof gt === "string") { try { gt = JSON.parse(gt); } catch (e) { return Promise.resolve({ ok: false, errors: ["invalid JSON: " + e.message] }); } }
239
+ var laf = (gt && gt.$extensions && gt.$extensions["org.hologram.lookandfeel"]) || null;
240
+ return importTheme(gt, opts).then(function (res) {
241
+ if (!res.ok) return res;
242
+ applyLookAndFeel(laf, opts);
243
+ return { ok: true, lookAndFeel: laf };
244
+ });
245
+ }
246
+ function applyLookAndFeel(laf, opts) {
247
+ opts = opts || {};
248
+ lookAndFeel = laf;
249
+ if (laf && laf.icons) root.setAttribute("data-holo-icons", laf.icons); else root.removeAttribute("data-holo-icons");
250
+ var deco = laf && laf.decoration && (laf.decoration.theme || laf.decoration);
251
+ if (deco) root.setAttribute("data-holo-decoration", deco); else root.removeAttribute("data-holo-decoration");
252
+ if (laf && laf.layout) root.setAttribute("data-holo-layout", laf.layout); else root.removeAttribute("data-holo-layout");
253
+ if (opts.persist !== false) { try { if (laf) localStorage.setItem(LKEY, JSON.stringify(laf)); else localStorage.removeItem(LKEY); } catch (e) {} }
254
+ root.dispatchEvent(new CustomEvent("holo-lookandfeel-change", { detail: laf, bubbles: false }));
255
+ }
256
+ function readVars() {
257
+ var cs = getComputedStyle(root), out = {};
258
+ ["--holo-bg", "--holo-surface", "--holo-surface-2", "--holo-border", "--holo-ink", "--holo-ink-dim",
259
+ "--holo-accent", "--holo-accent-ink", "--holo-ok", "--holo-warn", "--holo-danger",
260
+ "--holo-font-sans", "--holo-font-serif", "--holo-font-mono", "--holo-font-scale",
261
+ "--holo-radius-sm", "--holo-radius", "--holo-radius-lg", "--holo-density"
262
+ ].forEach(function (n) { out[n] = cs.getPropertyValue(n).trim(); });
263
+ return out;
264
+ }
265
+ // Export the current theme as a DTCG token object (the shareable artifact). WYSIWYG:
266
+ // reads the LIVE custom properties (base theme + any user tweaks), and carries over the
267
+ // imported theme's metadata + web fonts so nothing is lost.
268
+ function exportTheme(meta) {
269
+ return loadFormat().then(function (F) {
270
+ if (!F) return null;
271
+ var ext = themeJson && themeJson.$extensions && themeJson.$extensions["org.hologram.theme"];
272
+ var t = F.fromVars(readVars(), meta || (ext ? { name: ext.name, author: ext.author } : { name: "My Hologram theme" }));
273
+ if (themeJson) {
274
+ if (themeJson.$description) t.$description = themeJson.$description;
275
+ if (themeJson.font && themeJson.font.face) t.font.face = themeJson.font.face;
276
+ if (themeJson.font && themeJson.font.variation) t.font.variation = themeJson.font.variation;
277
+ }
278
+ return t;
279
+ });
280
+ }
281
+ function clearTheme() { themeJson = null; injectTokens(""); try { localStorage.removeItem(TKEY); localStorage.removeItem(CKEY); localStorage.removeItem(KKEY); } catch (e) {} applyLookAndFeel(null, {}); broadcastTokens("", null); apply(); }
282
+
283
+ // ── ΞΊ-addressed theme catalog β€” themes as content-addressed, self-verifying UOR objects ─────────
284
+ // A theme is a DTCG object served at /_shared/themes/<file>; its identity is ΞΊ = sha256 of its bytes.
285
+ // setThemeByKappa fetches it, RE-DERIVES sha256, refuses on mismatch (Law L5), then applies it live.
286
+ var _themes = null; // cached themes/index.json entries
287
+ var KKEY = "holo.theme.kappa.v1"; // the active ΞΊ-theme (so the studio can highlight it)
288
+ function listThemes() {
289
+ if (_themes) return Promise.resolve(_themes);
290
+ return fetch(SHARED + "themes/index.json").then(function (r) { return r.ok ? r.json() : { themes: [] }; })
291
+ .then(function (j) { _themes = (j && j.themes) || []; return _themes; })
292
+ .catch(function () { _themes = []; return _themes; });
293
+ }
294
+ function sha256Hex(buf) {
295
+ if (!(globalThis.crypto && crypto.subtle)) return Promise.resolve(null);
296
+ return crypto.subtle.digest("SHA-256", buf).then(function (d) {
297
+ var b = new Uint8Array(d), s = ""; for (var i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0"); return s;
298
+ });
299
+ }
300
+ function setThemeByKappa(kappa) {
301
+ var want = String(kappa).replace(/^sha256:/, "");
302
+ return listThemes().then(function (themes) {
303
+ var t = themes.filter(function (x) { return x.kappa === "sha256:" + want || x.kappa === kappa || x.name === kappa; })[0];
304
+ if (!t) return { ok: false, errors: ["unknown theme " + kappa] };
305
+ return fetch(SHARED + "themes/" + t.file).then(function (r) { return r.ok ? r.arrayBuffer() : Promise.reject(new Error("theme fetch failed")); })
306
+ .then(function (buf) {
307
+ return sha256Hex(buf).then(function (hex) {
308
+ // self-verify: the bytes must re-derive the claimed ΞΊ (Law L5). null = no SubtleCrypto (file://) β†’ skip.
309
+ var hexWant = t.kappa.replace(/^sha256:/, "");
310
+ if (hex && hex !== hexWant) return { ok: false, errors: ["ΞΊ mismatch β€” theme bytes do not re-derive (Law L5)"] };
311
+ var json; try { json = JSON.parse(new TextDecoder().decode(buf)); } catch (e) { return { ok: false, errors: ["invalid theme JSON"] }; }
312
+ // Applying a theme hands AESTHETIC governance to it: clear any ad-hoc accent/typeface
313
+ // override so the theme's own tokens win (accessibility β€” font scale/min/density β€” is kept).
314
+ if (state.accent || state.fontFamily) set({ accent: "", fontFamily: "" });
315
+ return importTheme(json).then(function (res) { if (res.ok) { try { localStorage.setItem(KKEY, t.kappa); } catch (e) {} } return res; });
316
+ });
317
+ });
318
+ }).catch(function (e) { return { ok: false, errors: [String(e.message || e)] }; });
319
+ }
320
+ function setTheme(nameOrKappa) {
321
+ return listThemes().then(function (themes) {
322
+ var t = themes.filter(function (x) { return x.kappa === nameOrKappa || x.name === nameOrKappa || x.file === nameOrKappa; })[0];
323
+ return t ? setThemeByKappa(t.kappa) : { ok: false, errors: ["unknown theme " + nameOrKappa] };
324
+ });
325
+ }
326
+ function activeThemeKappa() { try { return localStorage.getItem(KKEY) || null; } catch (e) { return null; } }
327
+
328
+ var root = document.documentElement;
329
+ var isTop = (window.parent === window);
330
+ var policy = (document.querySelector('meta[name="holo-theme-policy"]') || {}).content || "adopt";
331
+
332
+ // Built-in typeface presets the Settings panel offers (all standards-only font lists).
333
+ var FONTS = {
334
+ "Helvetica Neue": '"Helvetica Neue", Helvetica, Arial, "Segoe UI", Roboto, system-ui, sans-serif',
335
+ System: 'system-ui, -apple-system, "Segoe UI", Roboto, Inter, "Helvetica Neue", Arial, sans-serif',
336
+ Humanist: 'Inter, "Segoe UI", Roboto, system-ui, sans-serif',
337
+ Serif: 'ui-serif, Georgia, "Times New Roman", serif',
338
+ Mono: 'ui-monospace, "Cascadia Code", "JetBrains Mono", Menlo, Consolas, monospace',
339
+ Readable: 'Atkinson Hyperlegible, "Comic Neue", Verdana, system-ui, sans-serif' // dyslexia-friendly fallbacks
340
+ };
341
+
342
+ var DEFAULTS = {
343
+ palette: "auto", // auto = follow prefers-color-scheme (Media Queries L5)
344
+ presentation: "standard",
345
+ immersive: false, // Immersive backdrop ON β€” a ΞΊ-sealed wallpaper behind translucent surfaces
346
+ wallpaper: "", // the immersive backdrop: a ΞΊ (sha256:/blake3:) or a URL; "" = none
347
+ fontScale: 1,
348
+ fontMin: 16, // readability FLOOR in px (0 = off). The whole rem ramp clamps up to it.
349
+ fontFamily: "", // "" = OS default (--holo-font-sans from holo-theme.css)
350
+ density: 1,
351
+ accent: "", // "" = OS default
352
+ enforce: false // user "enforce OS theme on every app" switch
353
+ };
354
+
355
+ function read() {
356
+ try { return Object.assign({}, DEFAULTS, JSON.parse(localStorage.getItem(KEY) || "{}")); }
357
+ catch (e) { return Object.assign({}, DEFAULTS); }
358
+ }
359
+ function write(s) { try { localStorage.setItem(KEY, JSON.stringify(s)); } catch (e) {} }
360
+
361
+ var state = read();
362
+
363
+ function clamp(n, lo, hi) { n = parseFloat(n); return isNaN(n) ? lo : Math.min(hi, Math.max(lo, n)); }
364
+
365
+ // Apply the current state to THIS document using only W3C primitives.
366
+ function apply() {
367
+ var prio = state.enforce ? "important" : ""; // enforce-os = user beats app !important
368
+ var pushAesthetic = (policy !== "own") || state.enforce;
369
+ var pinned = state.palette && state.palette !== "auto";
370
+
371
+ // Palette: data attribute (for CSS hooks / app reaction) + INLINE color-scheme.
372
+ // Inline is required β€” a layered @layer rule would lose to an app's UNLAYERED
373
+ // `color-scheme` (e.g. the shell's `:root{color-scheme:dark}`). For "own" apps we
374
+ // leave their own scheme untouched (unless the user chose enforce-os).
375
+ if (pinned) root.setAttribute("data-holo-palette", state.palette);
376
+ else root.removeAttribute("data-holo-palette");
377
+ root.setAttribute("data-holo-presentation", state.presentation || "standard");
378
+ if (pushAesthetic) root.style.setProperty("color-scheme", pinned ? state.palette : "light dark", prio);
379
+ else root.style.removeProperty("color-scheme");
380
+
381
+ // Immersive backdrop β€” an orthogonal axis to the light/dark palette: a ΞΊ-sealed wallpaper
382
+ // published behind translucent surfaces (data-holo-immersive="on" + --holo-wallpaper). The
383
+ // pre-paint bootstrap (holo-appearance-boot.js) sets the SAME attribute/var from the SAME
384
+ // state, so the async engine and the first frame never disagree.
385
+ root.setAttribute("data-holo-immersive", state.immersive ? "on" : "off");
386
+ if (state.wallpaper) setVar("--holo-wallpaper", 'url("' + wallUrl(state.wallpaper) + '")', prio);
387
+ else root.style.removeProperty("--holo-wallpaper");
388
+
389
+ // Shoelace (Web Awesome) activates dark tokens via a class β€” follow the resolved palette.
390
+ if (pushAesthetic) {
391
+ var dark = pinned ? (state.palette === "dark")
392
+ : !!(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches);
393
+ root.classList.toggle("sl-theme-dark", dark);
394
+ }
395
+
396
+ // Accessibility (always applied, even to "own" apps) β€” inline custom props win.
397
+ setVar("--holo-font-scale", clamp(state.fontScale, 0.7, 2), prio);
398
+ setVar("--holo-density", clamp(state.density, 0.7, 1.4), prio);
399
+ // Readability floor: the type ramp + root font-size clamp UP to this (holo-theme.css /
400
+ // holo-mobile.css use max(--holo-font-min, …)). 0 = off β†’ no floor. px units required.
401
+ setVar("--holo-font-min", clamp(state.fontMin, 0, 28) + "px", prio);
402
+
403
+ // Aesthetics β€” applied unless this surface owns its theme (then only enforce-os pushes).
404
+ if (pushAesthetic && state.fontFamily) setVar("--holo-font-sans", state.fontFamily, prio);
405
+ else if (!state.fontFamily) root.style.removeProperty("--holo-font-sans");
406
+ if (pushAesthetic && state.accent) setVar("--holo-accent", state.accent, prio);
407
+ else if (!state.accent) root.style.removeProperty("--holo-accent");
408
+ }
409
+ function setVar(name, val, prio) { root.style.setProperty(name, val, prio || ""); }
410
+
411
+ // ── Propagation across the nested-holospace tree (postMessage) ───────────────────
412
+ function childFrames() {
413
+ var out = [];
414
+ var ifr = document.getElementsByTagName("iframe");
415
+ for (var i = 0; i < ifr.length; i++) if (ifr[i].contentWindow) out.push(ifr[i].contentWindow);
416
+ return out;
417
+ }
418
+ function broadcastDown() {
419
+ var msg = { type: "holo-theme", state: state };
420
+ childFrames().forEach(function (w) { try { w.postMessage(msg, "*"); } catch (e) {} });
421
+ }
422
+
423
+ // ── Font registry β€” fonts as first-class, content-addressed citizens ─────────────
424
+ // Imported and catalog fonts are injected as a <style id="holo-user-fonts"> block of
425
+ // @font-face rules (separate from a DTCG theme, so fonts and palettes are independent).
426
+ // An IMPORTED font's bytes ride inline as a data: URL, so the SAME CSS string propagates
427
+ // to every isolated holospace and each document loads it locally β€” no server route, no
428
+ // cross-origin fetch. A CATALOG font uses its ΞΊ path URL (smaller). Identical transport
429
+ // to themes: postMessage down the nested tree (HTML Standard).
430
+ function injectFonts(cssText) {
431
+ var s = document.getElementById("holo-user-fonts");
432
+ if (cssText == null || cssText === "") { if (s) s.remove(); return; }
433
+ if (!s) { s = document.createElement("style"); s.id = "holo-user-fonts"; (document.head || document.documentElement).appendChild(s); }
434
+ s.textContent = cssText;
435
+ }
436
+ function buildFontsCss(list) {
437
+ var F = globalThis.HoloThemeFormat; if (!F) return "";
438
+ return (list || []).map(function (d) { return F.faceCss(d.faces); }).filter(Boolean).join("\n");
439
+ }
440
+ function broadcastFonts(css, list) {
441
+ var msg = { type: "holo-fonts", css: css, list: list };
442
+ childFrames().forEach(function (w) { try { w.postMessage(msg, "*"); } catch (e) {} });
443
+ }
444
+ function quoteFamily(f) { return (/\s/.test(f) && !/^["']/.test(f)) ? '"' + f + '"' : f; }
445
+ function fontStack(desc) {
446
+ if (desc.stack && desc.stack.length) return desc.stack.map(quoteFamily).join(", ");
447
+ var fb = desc.category === "mono" ? 'ui-monospace, "Cascadia Code", Menlo, monospace'
448
+ : desc.category === "serif" ? 'ui-serif, Georgia, "Times New Roman", serif'
449
+ : 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
450
+ return quoteFamily(desc.family) + ", " + fb;
451
+ }
452
+ // Preload the ACTIVE registered font's primary path src (a data: URL is already inline,
453
+ // so it needs none). For local ΞΊ fonts this removes the font-display:swap flash β€” the
454
+ // "magical, instant" feel comes from preload, not swap.
455
+ function preloadActive() {
456
+ try {
457
+ var ex = document.getElementById("holo-font-preload"); if (ex) ex.remove();
458
+ var fam = (state.fontFamily || "").split(",")[0].replace(/^\s*["']?|["']?\s*$/g, "");
459
+ if (!fam) return;
460
+ var d = userFonts.filter(function (x) { return x.family === fam; })[0]; if (!d) return;
461
+ var face = (d.faces || [])[0]; if (!face || !face.src) return;
462
+ var m = /url\(\s*["']?([^"')]+)["']?\s*\)/.exec(face.src); var url = m && m[1];
463
+ if (!url || /^data:/.test(url)) return;
464
+ var l = document.createElement("link");
465
+ l.id = "holo-font-preload"; l.rel = "preload"; l.as = "font"; l.type = "font/woff2";
466
+ l.crossOrigin = "anonymous"; l.href = url;
467
+ (document.head || document.documentElement).appendChild(l);
468
+ } catch (e) {}
469
+ }
470
+ // Register a font {family, faces:[…], category?, stack?, origin?}. Dedupes by family.
471
+ function addFont(desc, opts) {
472
+ opts = opts || {};
473
+ return loadFormat().then(function (F) {
474
+ if (!F) return { ok: false, errors: ["format module unavailable"] };
475
+ if (!desc || !desc.family || !Array.isArray(desc.faces) || !desc.faces.length) return { ok: false, errors: ["a font needs a family and at least one face"] };
476
+ userFonts = userFonts.filter(function (x) { return x.family !== desc.family; }).concat([desc]);
477
+ var css = buildFontsCss(userFonts); injectFonts(css);
478
+ if (opts.persist !== false) { try { localStorage.setItem(FKEY, JSON.stringify(userFonts)); localStorage.setItem(FCKEY, css); } catch (e) {} }
479
+ if (opts.broadcast !== false) broadcastFonts(css, userFonts);
480
+ if (opts.activate !== false) setFontFamily(fontStack(desc)); // setFontFamily also broadcasts state
481
+ preloadActive();
482
+ root.dispatchEvent(new CustomEvent("holo-theme-change", { detail: state, bubbles: false }));
483
+ return { ok: true, family: desc.family };
484
+ });
485
+ }
486
+ function removeFont(family) {
487
+ userFonts = userFonts.filter(function (x) { return x.family !== family; });
488
+ var css = buildFontsCss(userFonts); injectFonts(css);
489
+ try { localStorage.setItem(FKEY, JSON.stringify(userFonts)); localStorage.setItem(FCKEY, css); } catch (e) {}
490
+ broadcastFonts(css, userFonts);
491
+ preloadActive();
492
+ }
493
+ // Import a raw font File β†’ registry descriptor (bytes inline as data: URL) β†’ register + activate.
494
+ function importFontFile(file) {
495
+ return new Promise(function (resolve) {
496
+ if (!file) return resolve({ ok: false, errors: ["no file"] });
497
+ file.arrayBuffer().then(function (buf) {
498
+ var bytes = new Uint8Array(buf), bin = "", CH = 0x8000;
499
+ for (var i = 0; i < bytes.length; i += CH) bin += String.fromCharCode.apply(null, bytes.subarray(i, i + CH));
500
+ var base64 = btoa(bin);
501
+ loadFormat().then(function (F) {
502
+ if (!F) return resolve({ ok: false, errors: ["format module unavailable"] });
503
+ var desc; try { desc = F.importDescriptor({ filename: file.name, base64: base64, mime: file.type }); }
504
+ catch (e) { return resolve({ ok: false, errors: [String(e.message || e)] }); }
505
+ addFont(desc, { activate: true }).then(resolve);
506
+ });
507
+ }, function (e) { resolve({ ok: false, errors: [String(e)] }); });
508
+ });
509
+ }
510
+ // The ΞΊ-pinned font library (fonts/index.json) β€” one-click add. Lazy + cached.
511
+ function loadCatalog() {
512
+ if (_catalog) return Promise.resolve(_catalog);
513
+ return fetch(SHARED + "fonts/index.json").then(function (r) { return r.ok ? r.json() : { fonts: [] }; })
514
+ .then(function (j) { _catalog = j || { fonts: [] }; return _catalog; })
515
+ .catch(function () { _catalog = { fonts: [] }; return _catalog; });
516
+ }
517
+ function catalogToDesc(e) {
518
+ return {
519
+ family: e.family, category: e.category || "sans", stack: e.stack, origin: "catalog",
520
+ faces: (e.faces || []).map(function (f) {
521
+ return { family: e.family, src: f.src, weight: f.weight || "100 900", style: f.style || "normal", display: f.display || "swap", unicodeRange: f.unicodeRange };
522
+ })
523
+ };
524
+ }
525
+
526
+ function set(patch, opts) {
527
+ opts = opts || {};
528
+ Object.assign(state, patch);
529
+ apply();
530
+ if (opts.persist !== false) write(state);
531
+ if (opts.broadcast !== false) broadcastDown();
532
+ root.dispatchEvent(new CustomEvent("holo-theme-change", { detail: state, bubbles: false }));
533
+ }
534
+
535
+ // Receive from parent; re-broadcast to our own children (transitive nesting).
536
+ window.addEventListener("message", function (e) {
537
+ var d = e.data;
538
+ if (!d || typeof d !== "object") return;
539
+ if (d.type === "holo-theme" && d.state) {
540
+ state = Object.assign({}, DEFAULTS, d.state);
541
+ apply(); write(state); broadcastDown();
542
+ } else if (d.type === "holo-theme-tokens") {
543
+ // A custom DTCG theme arrived from the parent β€” inject + persist + propagate down.
544
+ themeJson = d.json || null; injectTokens(d.css);
545
+ try { if (d.json) { localStorage.setItem(TKEY, JSON.stringify(d.json)); localStorage.setItem(CKEY, d.css || ""); } else { localStorage.removeItem(TKEY); localStorage.removeItem(CKEY); } } catch (err) {}
546
+ apply(); broadcastTokens(d.css, d.json);
547
+ } else if (d.type === "holo-fonts") {
548
+ // Registered fonts arrived from the parent β€” inject the @font-face block (bytes ride
549
+ // inline for imports) + persist + propagate down. No format module needed: css is compiled.
550
+ userFonts = Array.isArray(d.list) ? d.list : [];
551
+ injectFonts(d.css || "");
552
+ try { if (userFonts.length) { localStorage.setItem(FKEY, JSON.stringify(userFonts)); localStorage.setItem(FCKEY, d.css || ""); } else { localStorage.removeItem(FKEY); localStorage.removeItem(FCKEY); } } catch (err) {}
553
+ preloadActive(); broadcastFonts(d.css, userFonts);
554
+ } else if (d.type === "holo-theme-hello" && e.source) {
555
+ // A freshly-mounted child asks for the current theme; answer state + custom tokens + fonts.
556
+ try { e.source.postMessage({ type: "holo-theme", state: state }, "*"); } catch (err) {}
557
+ var sEl = document.getElementById("holo-theme-tokens");
558
+ if (sEl && themeJson) { try { e.source.postMessage({ type: "holo-theme-tokens", css: sEl.textContent, json: themeJson }, "*"); } catch (err) {} }
559
+ var fEl = document.getElementById("holo-user-fonts");
560
+ if (fEl && userFonts.length) { try { e.source.postMessage({ type: "holo-fonts", css: fEl.textContent, list: userFonts }, "*"); } catch (err) {} }
561
+ }
562
+ });
563
+
564
+ // ── Initial theme: saved choice β†’ (else) system preference β†’ (else) OS default ───
565
+ // Restore a previously imported custom theme first (fast path: compiled CSS, no format module).
566
+ try {
567
+ var savedCss = localStorage.getItem(CKEY);
568
+ if (savedCss) { injectTokens(savedCss); themeJson = JSON.parse(localStorage.getItem(TKEY) || "null"); }
569
+ var savedLaf = localStorage.getItem(LKEY);
570
+ if (savedLaf) applyLookAndFeel(JSON.parse(savedLaf), { persist: false });
571
+ } catch (e) {}
572
+ // Restore registered fonts (fast path: compiled @font-face CSS, no format module).
573
+ try {
574
+ var savedFonts = localStorage.getItem(FCKEY);
575
+ if (savedFonts) { injectFonts(savedFonts); userFonts = JSON.parse(localStorage.getItem(FKEY) || "[]"); }
576
+ } catch (e) {}
577
+ apply();
578
+ preloadActive();
579
+ // Re-resolve when the system appearance changes while we're in "auto".
580
+ if (window.matchMedia) {
581
+ try {
582
+ var mq = window.matchMedia("(prefers-color-scheme: dark)");
583
+ var onSys = function () { if (state.palette === "auto") apply(); };
584
+ if (mq.addEventListener) mq.addEventListener("change", onSys);
585
+ else if (mq.addListener) mq.addListener(onSys);
586
+ } catch (e) {}
587
+ }
588
+ // Ask the parent holospace for the OS-wide theme (covers mount-timing for nested apps).
589
+ if (!isTop) { try { window.parent.postMessage({ type: "holo-theme-hello" }, "*"); } catch (e) {} }
590
+ // When new child frames load, push the current theme + custom tokens + fonts to them.
591
+ document.addEventListener("load", function (e) {
592
+ if (e.target && e.target.tagName === "IFRAME" && e.target.contentWindow) {
593
+ var w = e.target.contentWindow;
594
+ try { w.postMessage({ type: "holo-theme", state: state }, "*"); } catch (err) {}
595
+ var sEl = document.getElementById("holo-theme-tokens");
596
+ if (sEl && themeJson) { try { w.postMessage({ type: "holo-theme-tokens", css: sEl.textContent, json: themeJson }, "*"); } catch (err) {} }
597
+ var fEl = document.getElementById("holo-user-fonts");
598
+ if (fEl && userFonts.length) { try { w.postMessage({ type: "holo-fonts", css: fEl.textContent, list: userFonts }, "*"); } catch (err) {} }
599
+ }
600
+ }, true);
601
+
602
+ // ── Built-in Display settings panel β€” usable from any surface (the OS shell wires a
603
+ // button to it). Pure DOM, themed by the very tokens it edits. ────────────────
604
+ function openSettings() {
605
+ if (document.getElementById("holo-theme-panel")) { closeSettings(); return; }
606
+ var css = document.createElement("style");
607
+ css.id = "holo-theme-panel-css";
608
+ css.textContent =
609
+ '#holo-theme-scrim{position:fixed;inset:0;z-index:2147482500;background:#0008;backdrop-filter:blur(2px)}' +
610
+ '#holo-theme-panel{position:fixed;z-index:2147482600;top:0;right:0;height:100%;width:min(360px,92vw);' +
611
+ 'background:var(--holo-surface,#14161b);color:var(--holo-ink,#e6edf3);border-left:1px solid var(--holo-border,#23272f);' +
612
+ 'box-shadow:-12px 0 40px #0006;display:flex;flex-direction:column;font-family:var(--holo-font-sans);overflow:auto;container-type:inline-size;container-name:holopanel}' +
613
+ '@container holopanel (max-width:300px){#holo-theme-panel .seg button{flex:1 1 100%}}' +
614
+ '#holo-theme-panel h2{font-size:var(--holo-text-lg,1.15rem);margin:0;padding:18px 20px;border-bottom:1px solid var(--holo-border,#23272f);display:flex;justify-content:space-between;align-items:center}' +
615
+ '#holo-theme-panel .grp{padding:16px 20px;border-bottom:1px solid var(--holo-border,#23272f);display:flex;flex-direction:column;gap:10px}' +
616
+ '#holo-theme-panel label{font-size:var(--holo-text-sm,.85rem);color:var(--holo-ink-dim,#8b949e)}' +
617
+ '#holo-theme-panel .seg{display:flex;gap:6px;flex-wrap:wrap}' +
618
+ '#holo-theme-panel .seg button{flex:1 1 auto;min-height:40px;border:1px solid var(--holo-border,#23272f);background:transparent;color:inherit;border-radius:10px;cursor:pointer;font:inherit;font-size:var(--holo-text-sm,.85rem)}' +
619
+ '#holo-theme-panel .seg button[aria-pressed="true"]{background:var(--holo-accent,#5b8cff);color:var(--holo-accent-ink,#fff);border-color:transparent}' +
620
+ '#holo-theme-panel input[type=range]{width:100%}' +
621
+ '#holo-theme-panel select,#holo-theme-panel input[type=color]{width:100%;min-height:40px;background:var(--holo-surface-2,#181b21);color:inherit;border:1px solid var(--holo-border,#23272f);border-radius:10px;padding:0 10px;font:inherit}' +
622
+ '#holo-theme-panel .x{min-width:40px;min-height:40px;border:0;background:transparent;color:inherit;font-size:1.3rem;cursor:pointer}' +
623
+ '#holo-theme-panel .sample{font-size:var(--holo-text,1rem);line-height:1.5}' +
624
+ '#holo-theme-panel .reset{margin:16px 20px;min-height:44px;border:1px solid var(--holo-border,#23272f);background:transparent;color:inherit;border-radius:10px;cursor:pointer;font:inherit}';
625
+ document.head.appendChild(css);
626
+
627
+ var scrim = el("div", { id: "holo-theme-scrim" });
628
+ scrim.onclick = closeSettings;
629
+ var lastFocus = document.activeElement; // restored on close (WCAG 2.4.3)
630
+ var p = el("aside", { id: "holo-theme-panel", role: "dialog", "aria-modal": "true", "aria-labelledby": "holo-theme-title", tabindex: "-1" });
631
+
632
+ var seg = function (label, opts, cur, on) {
633
+ var g = el("div", { class: "grp" }); g.appendChild(el("label", {}, label));
634
+ var s = el("div", { class: "seg" });
635
+ opts.forEach(function (o) {
636
+ var b = el("button", { "aria-pressed": String(o[1] === cur) }, o[0]);
637
+ b.onclick = function () { on(o[1]); refresh(); };
638
+ s.appendChild(b);
639
+ });
640
+ g.appendChild(s); return g;
641
+ };
642
+
643
+ function refresh() {
644
+ // re-render by closing + reopening keeps it tiny; state already applied live.
645
+ var sc = p.scrollTop; p.innerHTML = ""; build(); p.scrollTop = sc; p.focus();
646
+ }
647
+ function build() {
648
+ var head = el("h2", { id: "holo-theme-title" }, "Holo UI");
649
+ var x = el("button", { class: "x", "aria-label": "Close" }, "Γ—"); x.onclick = closeSettings;
650
+ head.appendChild(x); p.appendChild(head);
651
+
652
+ p.appendChild(seg("Appearance", [["Auto", "auto"], ["Light", "light"], ["Dark", "dark"]], state.palette, function (v) { setPalette(v); }));
653
+ p.appendChild(seg("Mode", [["Standard", "standard"], ["Immersive", "immersive"]], state.presentation, function (v) { setPresentation(v); }));
654
+ // Immersive backdrop β€” the ΞΊ-sealed wallpaper layer (the curated picker lands in the shell
655
+ // Settings Β· Appearance section; this is the engine-level on/off the boot bootstrap reads).
656
+ p.appendChild(seg("Immersive backdrop", [["Off", "off"], ["On", "on"]], state.immersive ? "on" : "off", function (v) { setImmersive(v === "on"); }));
657
+
658
+ // Text size
659
+ var gT = el("div", { class: "grp" });
660
+ gT.appendChild(el("label", {}, "Text size β€” " + Math.round(state.fontScale * 100) + "%"));
661
+ var rng = el("input", { type: "range", min: "0.85", max: "1.4", step: "0.05", value: String(state.fontScale) });
662
+ rng.oninput = function () { setFontScale(this.value); gT.firstChild.textContent = "Text size β€” " + Math.round(state.fontScale * 100) + "%"; };
663
+ gT.appendChild(rng);
664
+ gT.appendChild(el("div", { class: "sample" }, "The quick brown fox jumps over the lazy dog."));
665
+ p.appendChild(gT);
666
+
667
+ // Minimum text size β€” the readability FLOOR. No text rendered through the OS tokens
668
+ // (the type ramp, form controls, base text) can fall below this. "Off" restores the raw ramp.
669
+ p.appendChild(seg("Minimum text size", [["Off", 0], ["14", 14], ["16", 16], ["18", 18], ["20", 20]],
670
+ state.fontMin, function (v) { setFontMin(v); }));
671
+
672
+ // Typeface β€” built-in presets + every registered (imported / library) font.
673
+ var gF = el("div", { class: "grp" });
674
+ gF.appendChild(el("label", {}, "Typeface"));
675
+ var sel = el("select", {});
676
+ sel.appendChild(el("option", { value: "" }, "OS default"));
677
+ Object.keys(FONTS).forEach(function (name) {
678
+ var o = el("option", { value: FONTS[name] }, name);
679
+ if (FONTS[name] === state.fontFamily) o.selected = true;
680
+ sel.appendChild(o);
681
+ });
682
+ userFonts.forEach(function (d) {
683
+ var val = fontStack(d);
684
+ var o = el("option", { value: val }, d.family + (d.origin === "import" ? " Β· imported" : ""));
685
+ if (val === state.fontFamily) o.selected = true;
686
+ sel.appendChild(o);
687
+ });
688
+ sel.onchange = function () { setFontFamily(this.value); };
689
+ gF.appendChild(sel);
690
+ gF.appendChild(el("div", { class: "sample" }, "The quick brown fox jumps over the lazy dog. 0123456789"));
691
+
692
+ // Add a font β€” import ANY WOFF2/WOFF/TTF/OTF; it’s content-addressed and applied
693
+ // across every holospace instantly (bytes ride inline, so isolation is no barrier).
694
+ var addRow = el("div", { class: "seg" });
695
+ var addBtn = el("button", {}, "οΌ‹ Add font…");
696
+ var ffile = el("input", { type: "file", accept: ".woff2,.woff,.ttf,.otf,font/woff2,font/woff,font/ttf,font/otf" }); ffile.style.display = "none";
697
+ ffile.onchange = function () {
698
+ var f = ffile.files && ffile.files[0]; if (!f) { return; }
699
+ addBtn.disabled = true; addBtn.textContent = "Adding…";
700
+ importFontFile(f).then(function (r) {
701
+ if (!r.ok) { var b = el("div", {}, "Font import failed: " + ((r.errors || []).join("; ") || "invalid")); b.style.cssText = "color:var(--holo-danger);font-size:var(--holo-text-sm);margin-top:6px"; gF.appendChild(b); addBtn.disabled = false; addBtn.textContent = "οΌ‹ Add font…"; }
702
+ else refresh();
703
+ });
704
+ ffile.value = "";
705
+ };
706
+ addBtn.onclick = function () { ffile.click(); };
707
+ addRow.appendChild(addBtn); gF.appendChild(addRow); gF.appendChild(ffile);
708
+
709
+ // Remove imported fonts (catalog fonts are ΞΊ-pinned; leave them).
710
+ var imported = userFonts.filter(function (d) { return d.origin === "import"; });
711
+ if (imported.length) {
712
+ var rm = el("div", { class: "seg" });
713
+ imported.forEach(function (d) {
714
+ var b = el("button", { title: "Remove " + d.family }, "βœ• " + d.family);
715
+ b.onclick = function () { removeFont(d.family); if (state.fontFamily === fontStack(d)) setFontFamily(""); refresh(); };
716
+ rm.appendChild(b);
717
+ });
718
+ gF.appendChild(rm);
719
+ }
720
+ var fhint = el("div", {}, "Drop in any WOFF2/TTF β€” it’s applied uniformly across the whole OS, instantly.");
721
+ fhint.style.cssText = "font-size:var(--holo-text-sm,.8rem);color:var(--holo-ink-dim);margin-top:2px"; gF.appendChild(fhint);
722
+ p.appendChild(gF);
723
+
724
+ // Font library β€” the ΞΊ-pinned catalog (fonts/index.json). One click = add + activate.
725
+ var gL = el("div", { class: "grp" });
726
+ gL.appendChild(el("label", {}, "Font library Β· ΞΊ-pinned"));
727
+ var lib = el("div", { class: "seg" }); gL.appendChild(lib); p.appendChild(gL);
728
+ lib.appendChild(el("div", {}, "Loading…"));
729
+ loadCatalog().then(function (cat) {
730
+ lib.innerHTML = "";
731
+ var fonts = (cat && cat.fonts) || [];
732
+ if (!fonts.length) { lib.appendChild(el("div", {}, "β€”")); return; }
733
+ fonts.forEach(function (entry) {
734
+ var have = userFonts.some(function (x) { return x.family === entry.family; });
735
+ var b = el("button", { "aria-pressed": String(have) }, (have ? "βœ“ " : "οΌ‹ ") + entry.family);
736
+ b.onclick = function () { addFont(catalogToDesc(entry), { activate: true }).then(function () { refresh(); }); };
737
+ lib.appendChild(b);
738
+ });
739
+ });
740
+
741
+ // Accent
742
+ var gA = el("div", { class: "grp" });
743
+ gA.appendChild(el("label", {}, "Accent colour"));
744
+ var col = el("input", { type: "color", value: toHex(state.accent) || "#5b8cff" });
745
+ col.oninput = function () { setAccent(this.value); };
746
+ gA.appendChild(col);
747
+ p.appendChild(gA);
748
+
749
+ // Enforce on apps
750
+ p.appendChild(seg("Apply to apps", [["Respect apps", "respect"], ["Enforce OS theme", "enforce"]],
751
+ state.enforce ? "enforce" : "respect", function (v) { set({ enforce: v === "enforce" }); }));
752
+
753
+ // Theme β€” create / save / load as a W3C Design Tokens (DTCG) file (shareable).
754
+ var gM = el("div", { class: "grp" });
755
+ gM.appendChild(el("label", {}, "Theme Β· W3C Design Tokens (.json)"));
756
+ var row = el("div", { class: "seg" });
757
+ var expBtn = el("button", {}, "– Export");
758
+ expBtn.onclick = function () {
759
+ exportTheme().then(function (t) {
760
+ if (!t) return;
761
+ var ext = t.$extensions && t.$extensions["org.hologram.theme"];
762
+ var name = (ext && ext.name) || "hologram";
763
+ var a = el("a", {});
764
+ a.href = URL.createObjectURL(new Blob([JSON.stringify(t, null, 2)], { type: "application/json" }));
765
+ a.download = String(name).toLowerCase().replace(/\s+/g, "-") + ".tokens.json";
766
+ document.body.appendChild(a); a.click(); a.remove();
767
+ setTimeout(function () { URL.revokeObjectURL(a.href); }, 2000);
768
+ });
769
+ };
770
+ var impBtn = el("button", {}, "β€’ Import");
771
+ var file = el("input", { type: "file", accept: ".json,application/json" }); file.style.display = "none";
772
+ file.onchange = function () {
773
+ var f = file.files && file.files[0]; if (!f) return;
774
+ f.text().then(function (txt) {
775
+ importTheme(txt).then(function (r) {
776
+ if (!r.ok) { var b = el("div", { class: "themeerr" }, "Import failed: " + ((r.errors || []).join("; ") || "invalid")); b.style.cssText = "color:var(--holo-danger);font-size:var(--holo-text-sm);margin-top:6px"; gM.appendChild(b); }
777
+ else refresh();
778
+ });
779
+ });
780
+ file.value = "";
781
+ };
782
+ impBtn.onclick = function () { file.click(); };
783
+ var defBtn = el("button", {}, "OS default");
784
+ defBtn.onclick = function () { clearTheme(); refresh(); };
785
+ row.appendChild(expBtn); row.appendChild(impBtn); row.appendChild(defBtn);
786
+ gM.appendChild(row); gM.appendChild(file);
787
+ var hint = el("div", {}, "Adjust the controls above, then Export to save & share your theme β€” or Import one.");
788
+ hint.style.cssText = "font-size:var(--holo-text-sm,.8rem);color:var(--holo-ink-dim);margin-top:2px"; gM.appendChild(hint);
789
+ // The canonical, full appearance surface is Holo Control (the Holo UI app's Appearance panel):
790
+ // themes + accent + typography + accessibility + import/fork, all live. This quick panel stays for
791
+ // fast in-place tweaks; the button below opens the complete control panel (ADR-0079).
792
+ var browse = el("button", {}, "🎚 Open Holo Control β€” full appearance settings");
793
+ browse.style.cssText = "margin-top:8px;min-height:40px;border:1px solid var(--holo-border);background:transparent;color:inherit;border-radius:10px;cursor:pointer;font:inherit;font-size:var(--holo-text-sm)";
794
+ browse.onclick = function () { try { window.open(SHARED + "../apps/ui/index.html#appearance", "_blank"); } catch (e) {} };
795
+ gM.appendChild(browse);
796
+ p.appendChild(gM);
797
+
798
+ var reset = el("button", { class: "reset" }, "Reset to defaults"); reset.onclick = function () { resetAll(); clearTheme(); refresh(); };
799
+ p.appendChild(reset);
800
+ }
801
+ build();
802
+ document.body.appendChild(scrim);
803
+ document.body.appendChild(p);
804
+
805
+ // ARIA modal behaviour (WCAG 2.1.2 / 2.4.3 / 4.1.2): focus in, trap Tab, Escape closes.
806
+ var focusables = function () { return p.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); };
807
+ (focusables()[0] || p).focus();
808
+ p.__lastFocus = lastFocus;
809
+ p.__onkey = function (e) {
810
+ if (e.key === "Escape") { e.preventDefault(); closeSettings(); return; }
811
+ if (e.key !== "Tab") return;
812
+ var f = focusables(); if (!f.length) return;
813
+ var first = f[0], last = f[f.length - 1];
814
+ if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
815
+ else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
816
+ };
817
+ document.addEventListener("keydown", p.__onkey, true);
818
+ }
819
+ function closeSettings() {
820
+ var p = document.getElementById("holo-theme-panel");
821
+ if (p && p.__onkey) document.removeEventListener("keydown", p.__onkey, true);
822
+ ["holo-theme-panel", "holo-theme-scrim", "holo-theme-panel-css"].forEach(function (id) {
823
+ var n = document.getElementById(id); if (n) n.remove();
824
+ });
825
+ if (p && p.__lastFocus && p.__lastFocus.focus) { try { p.__lastFocus.focus(); } catch (e) {} }
826
+ }
827
+ function el(tag, attrs, text) {
828
+ var n = document.createElement(tag);
829
+ if (attrs) for (var k in attrs) n.setAttribute(k, attrs[k]);
830
+ if (text != null) n.textContent = text;
831
+ return n;
832
+ }
833
+ function toHex(c) { return /^#[0-9a-f]{6}$/i.test(c || "") ? c : ""; }
834
+ // Resolve an immersive wallpaper reference to a URL: a ΞΊ (sha256:/blake3:/sha512:<hex>)
835
+ // becomes the serverless ΞΊ-store path /.holo/<algo>/<hex> (re-derived on fetch, Law L5);
836
+ // anything else (a path, data: URL) is used verbatim.
837
+ function wallUrl(w) {
838
+ if (!w) return "";
839
+ var m = String(w).match(/^(sha256|blake3|sha512):([0-9a-f]+)$/i);
840
+ return m ? "/.holo/" + m[1].toLowerCase() + "/" + m[2] : String(w);
841
+ }
842
+
843
+ // ── Public API ───────────────────────────────────────────────────────────────
844
+ function setPalette(v) { set({ palette: v }); }
845
+ function setPresentation(v) { set({ presentation: v }); }
846
+ function setImmersive(on) { set({ immersive: !!on }); }
847
+ function setWallpaper(k) { set({ wallpaper: k || "" }); }
848
+ // The one user-facing appearance choice (Settings Β· login picker): Dark Β· Light Β· Immersive.
849
+ // Dark/Light pin the palette and drop the backdrop; Immersive turns the backdrop on and keeps
850
+ // the current palette (a light OR dark immersive backdrop both read well behind the scrim).
851
+ function setMode(m) {
852
+ if (m === "light") set({ palette: "light", immersive: false });
853
+ else if (m === "dark") set({ palette: "dark", immersive: false });
854
+ else if (m === "immersive") set({ immersive: true });
855
+ }
856
+ function setFontScale(n) { set({ fontScale: clamp(n, 0.7, 2) }); }
857
+ function setFontMin(n) { set({ fontMin: clamp(n, 0, 28) }); } // readability floor in px (0 = off)
858
+ function setFontFamily(f) { set({ fontFamily: f || "" }); }
859
+ function setDensity(n) { set({ density: clamp(n, 0.7, 1.4) }); }
860
+ function setAccent(c) { set({ accent: c || "" }); }
861
+ function resetAll() { state = Object.assign({}, DEFAULTS); apply(); write(state); broadcastDown(); }
862
+
863
+ window.HoloTheme = {
864
+ get: function () { return Object.assign({}, state); },
865
+ set: set, setPalette: setPalette, setPresentation: setPresentation,
866
+ setImmersive: setImmersive, setWallpaper: setWallpaper, setMode: setMode,
867
+ setFontScale: setFontScale, setFontMin: setFontMin, setFontFamily: setFontFamily, setDensity: setDensity,
868
+ setAccent: setAccent, reset: resetAll, openSettings: openSettings, closeSettings: closeSettings,
869
+ // Portable themes (DTCG): import a token file/object, export the current theme, clear back to OS default.
870
+ importTheme: importTheme, exportTheme: exportTheme, clearTheme: clearTheme,
871
+ getThemeJson: function () { return themeJson ? JSON.parse(JSON.stringify(themeJson)) : null; },
872
+ // ΞΊ-addressed theme catalog: list the content-addressed DTCG themes, and swap to one by ΞΊ (or name).
873
+ // setThemeByKappa re-derives sha256 and refuses on mismatch (Law L5) before applying β€” live, OS-wide.
874
+ listThemes: listThemes, setTheme: setTheme, setThemeByKappa: setThemeByKappa, activeThemeKappa: activeThemeKappa,
875
+ setVar: function (name, val) { setVar(name, val); root.dispatchEvent(new CustomEvent("holo-theme-change", { detail: state, bubbles: false })); },
876
+ // Global Theme (KDE Look-and-Feel package adopted as one ΞΊ-object): import flows through
877
+ // importTheme, then surfaces the look-and-feel block (icons/decoration/layout/splash) to the shell.
878
+ applyGlobalTheme: applyGlobalTheme,
879
+ getLookAndFeel: function () { return lookAndFeel ? JSON.parse(JSON.stringify(lookAndFeel)) : null; },
880
+ // Fonts as first-class citizens: import a file (data: URL), add a descriptor, or pull from
881
+ // the ΞΊ-pinned library β€” each propagates @font-face across every isolated holospace.
882
+ importFontFile: importFontFile, addFont: addFont, removeFont: removeFont,
883
+ loadFontCatalog: loadCatalog, getFonts: function () { return JSON.parse(JSON.stringify(userFonts)); },
884
+ FONTS: FONTS, policy: policy
885
+ };
886
+ })();
sha256/a0ebbc696bda5154727b76b42c11921231cbc652de3e705dd6b632456e6f34aa ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // teleport.js β€” ONE reusable Share affordance for every hologram page.
2
+ //
3
+ // Drop-in, zero config: <script src="teleport.js" defer></script>
4
+ //
5
+ // It gives the page a floating "Share" button that builds a TELEPORT LINK β€” a
6
+ // single URL that, opened on ANY device, boots the hologram environment (the
7
+ // browser everyone already has) and lands on this exact content. No install, no
8
+ // server. The link is handed to the OS share sheet via the W3C Web Share API
9
+ // (navigator.share), with a clipboard fallback on desktop browsers that lack it.
10
+ //
11
+ // Content-addressed (#k=): if the page knows its ΞΊ β€” via
12
+ // <meta name="holo-kappa" content="blake3:…"> or HoloTeleport.stamp("blake3:…")
13
+ // β€” the link carries #k=<ΞΊ> so the content is identified by its hash, not just
14
+ // its location. Opening such a link shows a provenance chip; a page that can hand
15
+ // us the exact bytes (HoloTeleport.verifyAgainst) gets a TRUE re-derivation check
16
+ // (Law L5): a βœ“ only when the re-derived ΞΊ equals the pinned one β€” never faked.
17
+ //
18
+ // ΞΊ is computed by the app's own wasm (blake3, identical to the native ΞΊ-store)
19
+ // when present, else SHA-256 via Web Crypto β€” the prefix (blake3:/sha256:) always
20
+ // names the algorithm, so nothing is ever mislabelled.
21
+
22
+ (function () {
23
+ "use strict";
24
+ const W = window;
25
+ if (W.HoloTeleport) return;
26
+
27
+ const metaOf = (n) => {
28
+ const e = document.querySelector(`meta[name="${n}"]`) || document.querySelector(`meta[property="${n}"]`);
29
+ return e ? (e.getAttribute("content") || "") : "";
30
+ };
31
+ let pageKappa = metaOf("holo-kappa") || ""; // optional content hash for this page
32
+
33
+ // ── teleport-link builder ────────────────────────────────────────────────
34
+ // Host-agnostic: uses location.origin, so it works on localhost today and on a
35
+ // real host (e.g. GitHub Pages) the moment you publish β€” same code.
36
+ function link(opts) {
37
+ opts = opts || {};
38
+ const base = location.origin + location.pathname + (opts.search != null ? opts.search : location.search);
39
+ const k = opts.kappa || pageKappa;
40
+ return base + (k ? "#k=" + encodeURIComponent(k) : (opts.keepHash === false ? "" : location.hash || ""));
41
+ }
42
+ const titleOf = () => (document.title || "hologram").replace(/\s+β€”\s+holospaces$/i, "") + " Β· on hologram";
43
+ const textOf = () =>
44
+ metaOf("description") || metaOf("og:description") ||
45
+ "Opens on any device β€” boots hologram and this content, verified by its content hash. No install.";
46
+
47
+ // ── share: OS sheet via Web Share, clipboard fallback ────────────────────
48
+ async function share(opts) {
49
+ const url = link(opts);
50
+ const data = { title: (opts && opts.title) || titleOf(), text: (opts && opts.text) || textOf(), url };
51
+ try {
52
+ if (navigator.share && (!navigator.canShare || navigator.canShare(data))) {
53
+ await navigator.share(data); toast("Shared."); return url;
54
+ }
55
+ } catch (e) { if (e && e.name === "AbortError") return url; }
56
+ try { await navigator.clipboard.writeText(url); toast("Teleport link copied β€” paste it anywhere."); }
57
+ catch { W.prompt && W.prompt("Teleport link:", url); }
58
+ return url;
59
+ }
60
+
61
+ // ── ΞΊ helpers β€” blake3 via the app wasm, else SHA-256 ────────────────────
62
+ let _wasm; // memoised import
63
+ async function kappaOf(bytes) {
64
+ const u8 = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
65
+ try {
66
+ if (_wasm === undefined) { _wasm = null; const m = await import("./pkg/holospaces_web.js").catch(() => null); if (m) { if (m.default) await m.default(); _wasm = m; } }
67
+ if (_wasm && _wasm.kappa) { const k = _wasm.kappa(u8); return k.includes(":") ? k : "blake3:" + k; }
68
+ } catch {}
69
+ try {
70
+ const d = await crypto.subtle.digest("SHA-256", u8);
71
+ return "sha256:" + Array.from(new Uint8Array(d), (b) => b.toString(16).padStart(2, "0")).join("");
72
+ } catch { return ""; }
73
+ }
74
+ // TRUE verification: re-derive the ΞΊ of the given bytes and compare to want.
75
+ async function verifyAgainst(want, bytes) {
76
+ if (!want) return false;
77
+ const got = await kappaOf(bytes);
78
+ const ok = !!got && got === want;
79
+ chip(ok ? "βœ“ verified genuine Β· " + shortK(want) : "ΞΊ mismatch β€” refused (content β‰  shared hash)", ok ? "ok" : "bad");
80
+ return ok;
81
+ }
82
+ const shortK = (k) => { const [a, h] = String(k).split(":"); return h ? a + ":" + h.slice(0, 12) + "…" : String(k).slice(0, 16) + "…"; };
83
+
84
+ // ── on open: surface #k= provenance, then TRY a real re-derivation check ──
85
+ // Convention: on teleport.js pages, #k= is THIS page's content ΞΊ. So we can
86
+ // refetch the page bytes and re-derive β€” a βœ“ only on a true match (Law L5),
87
+ // a refusal banner on mismatch. If the bytes aren't fetchable we leave the
88
+ // neutral provenance chip rather than claim anything.
89
+ async function onOpen() {
90
+ const h = new URLSearchParams((location.hash || "").replace(/^#/, ""));
91
+ const k = h.get("k");
92
+ if (!k) return;
93
+ const want = decodeURIComponent(k);
94
+ chip("content-addressed Β· ΞΊ " + shortK(want), "info");
95
+ try {
96
+ const buf = await (await fetch(location.pathname, { cache: "no-store" })).arrayBuffer();
97
+ const got = await kappaOf(new Uint8Array(buf));
98
+ if (got && got === want) chip("βœ“ verified genuine Β· ΞΊ " + shortK(want), "ok");
99
+ else if (got && want.split(":")[0] === got.split(":")[0]) chip("ΞΊ mismatch β€” content differs from the shared hash", "bad");
100
+ } catch { /* opaque/cross-origin β€” keep the neutral provenance chip */ }
101
+ }
102
+
103
+ // ── UI: floating Share button + toast + provenance chip (self-contained) ──
104
+ function injectStyle() {
105
+ if (document.getElementById("holo-teleport-css")) return;
106
+ const s = document.createElement("style"); s.id = "holo-teleport-css";
107
+ s.textContent = `
108
+ #holo-toast{position:fixed;left:50%;bottom:84px;transform:translateX(-50%);z-index:2147483001;max-width:88vw;
109
+ background:#11181c;color:#e6edf3;border:1px solid #243038;border-radius:10px;padding:10px 14px;font:var(--holo-text-sm, 1rem) system-ui;
110
+ box-shadow:0 12px 36px rgba(0,0,0,.5);opacity:0;transition:opacity .18s;pointer-events:none}
111
+ #holo-toast.in{opacity:1}
112
+ #holo-chip{position:fixed;left:50%;top:12px;transform:translateX(-50%);z-index:2147483001;
113
+ background:#0d1117ee;border:1px solid #243038;border-radius:999px;padding:6px 13px;font:var(--holo-text-sm, 1rem)/1.2 ui-monospace,monospace;
114
+ color:#9fb0bd;backdrop-filter:blur(6px)}
115
+ #holo-chip.ok{border-color:#2dd4bf;color:#7defc9} #holo-chip.bad{border-color:#f87171;color:#fca5a5}`;
116
+ document.head.appendChild(s);
117
+ }
118
+ // (No auto-injected floating "Share" button β€” removed by request. Sharing remains
119
+ // available programmatically via HoloTeleport.share(); the #k= provenance chip and
120
+ // Law-L5 re-derivation are unaffected.)
121
+ let toastT;
122
+ function toast(m) {
123
+ injectStyle(); let t = document.getElementById("holo-toast");
124
+ if (!t) { t = document.createElement("div"); t.id = "holo-toast"; document.body.appendChild(t); }
125
+ t.textContent = m; requestAnimationFrame(() => t.classList.add("in"));
126
+ clearTimeout(toastT); toastT = setTimeout(() => t.classList.remove("in"), 3600);
127
+ }
128
+ function chip(m, kind) {
129
+ injectStyle(); let c = document.getElementById("holo-chip");
130
+ if (!c) { c = document.createElement("div"); c.id = "holo-chip"; document.body.appendChild(c); }
131
+ c.className = kind || "info"; c.textContent = m;
132
+ }
133
+
134
+ function boot() { injectStyle(); onOpen(); }
135
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
136
+ else boot();
137
+
138
+ W.HoloTeleport = {
139
+ link, share, kappaOf, verifyAgainst,
140
+ stamp: (k) => { pageKappa = k || ""; }, // page declares its content hash
141
+ toast, chip,
142
+ };
143
+ })();
sha256/b2bfa104b9ee8e3bb631dcd0ec0e6f7017b8898c4304666f5b3dfeeebc926eb9 ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* holo-mobile.css β€” the Hologram mobile conformance layer.
2
+ *
3
+ * One stylesheet every holospace links. It does NOT theme apps β€” they keep their
4
+ * own colors and feel. It enforces the mobile *mechanics* that must be standard on
5
+ * every screen: no horizontal overflow, safe-area insets, β‰₯48px tap targets, and
6
+ * 16px form controls (no iOS auto-zoom). The spec is WCAG 2.2 (Β§1.4.10 Reflow,
7
+ * Β§2.5.8 Target Size) + Material Design 3 (48dp). Conformance is enforced by
8
+ * mobile-conformance-witness.mjs (the gate), not by this file alone.
9
+ *
10
+ * Specificity discipline: structural defaults use :where() (specificity 0) so any
11
+ * app rule overrides them freely. This layer is loaded AFTER each page's own styles.
12
+ * Deliberately NO `overflow-x:clip` backstop β€” clipping would hide overflow from the
13
+ * witness and let real violations pass. Overflow is fixed at the cause, not masked.
14
+ */
15
+
16
+ :root {
17
+ /* Safe-area insets (notch / home indicator). 0 on devices without them. */
18
+ --safe-t: env(safe-area-inset-top, 0px);
19
+ --safe-r: env(safe-area-inset-right, 0px);
20
+ --safe-b: env(safe-area-inset-bottom, 0px);
21
+ --safe-l: env(safe-area-inset-left, 0px);
22
+
23
+ /* Shared tokens β€” consistency without dictating color. Apps map these to brand. */
24
+ --holo-tap: 48px; /* Material Design 3 minimum touch target */
25
+ --holo-space-1: 4px; --holo-space-2: 8px; --holo-space-3: 12px;
26
+ --holo-space-4: 16px; --holo-space-5: 24px; --holo-space-6: 32px; /* 8px grid */
27
+ --holo-radius-sm: 8px; --holo-radius: 12px; --holo-radius-lg: 16px;
28
+ /* Fluid type scale (clamps to the viewport; readable floor, sane ceiling). Authored
29
+ * in rem β€” NOT px β€” so the whole ramp scales from the OS text-size lever
30
+ * (--holo-font-scale β†’ root font-size; see holo-theme.css). 1rem = 16px at base.
31
+ * The sub-1rem steps clamp UP to the readability floor (--holo-font-min, default 16px,
32
+ * set by the engine) with max(), so caption/label text can never render too small to read;
33
+ * -lg/-xl already exceed the floor so the hierarchy above it is preserved. */
34
+ --holo-text-sm: max(var(--holo-font-min, 16px), clamp(0.8125rem, 0.78rem + 0.2vw, 0.875rem));
35
+ --holo-text: max(var(--holo-font-min, 16px), clamp(0.9375rem, 0.9rem + 0.3vw, 1rem));
36
+ --holo-text-lg: clamp(1.125rem, 1.0rem + 0.6vw, 1.3125rem);
37
+ --holo-text-xl: clamp(1.5rem, 1.2rem + 1.6vw, 2.125rem);
38
+ }
39
+
40
+ /* ── Global guards (safe on every page) ─────────────────────────────────────── */
41
+ *, *::before, *::after { box-sizing: border-box; }
42
+ :where(html) { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
43
+ /* Replaced/wide elements must never force the page wider than the viewport. */
44
+ :where(img, svg, video, canvas, iframe, table) { max-width: 100%; }
45
+ :where(pre, code) { overflow-wrap: anywhere; }
46
+
47
+ /* ── Tap targets β€” WCAG 2.5.8 + MD3 48px floor (low specificity = app-overridable). */
48
+ :where(button, [role="button"], a.btn, .btn, input:not([type="checkbox"]):not([type="radio"]):not([type="range"]), select, summary) {
49
+ min-height: var(--holo-tap);
50
+ }
51
+ /* Buttons (incl. icon-only) also meet the 48px width floor by default. */
52
+ :where(button, [role="button"], a.btn, .btn, summary) { min-width: var(--holo-tap); }
53
+ /* Escape hatch for genuinely-dense, essential chrome where 48px is impractical
54
+ * (status bars, emulator window controls) β€” WCAG 2.5.8 allows such exceptions.
55
+ * The conformance witness skips tap-size checks inside `.holo-dense`. Use sparingly. */
56
+ :where(.holo-dense) :where(button, [role="button"], a.btn) { min-width: 0; min-height: 0; }
57
+
58
+ /* ── Form controls β€” readability floor (β‰₯16px) also stops iOS auto-zoom on focus. ── */
59
+ :where(input, select, textarea) { font-size: max(var(--holo-font-min, 16px), 1rem); }
60
+
61
+ /* ── Safe-area variables exposed for adoption. Applied generically to the body so
62
+ * content clears the notch/home-indicator; bespoke fixed bars opt in via the
63
+ * utility classes below (env() on a bar's own padding is overridden by the app's
64
+ * padding rule, so we keep this best-effort + opt-in rather than fighting apps). */
65
+ :where(body) { padding-left: var(--safe-l); padding-right: var(--safe-r); }
66
+ .holo-safe-top { padding-top: max(var(--holo-space-2), var(--safe-t)); }
67
+ .holo-safe-bottom { padding-bottom: max(var(--holo-space-2), var(--safe-b)); }
68
+
69
+ /* ── Adopt-in primitives β€” the standard way to present these elements ───────── */
70
+
71
+ /* A top app bar that never overflows: brand stays put, the rest can shrink/scroll. */
72
+ .holo-appbar {
73
+ display: flex; align-items: center; gap: var(--holo-space-3);
74
+ padding: max(var(--holo-space-2), var(--safe-t)) var(--holo-space-4) var(--holo-space-2);
75
+ min-height: var(--holo-tap);
76
+ }
77
+ .holo-appbar > .brand, .holo-appbar > :first-child { flex: 0 0 auto; min-width: 0; }
78
+ .holo-appbar > .grow { flex: 1 1 auto; min-width: 0; }
79
+
80
+ /* A horizontally scrollable chip/meta row β€” scrolls instead of clipping (the fix
81
+ * for the Linux/QVAC header chips). Momentum scroll, no visible scrollbar. */
82
+ .holo-scroll-x {
83
+ display: flex; flex-wrap: nowrap; gap: var(--holo-space-2);
84
+ overflow-x: auto; -webkit-overflow-scrolling: touch;
85
+ scrollbar-width: none; min-width: 0; max-width: 100%;
86
+ }
87
+ .holo-scroll-x::-webkit-scrollbar { display: none; }
88
+ .holo-scroll-x > * { flex: 0 0 auto; }
89
+
90
+ /* Long unbroken strings (hashes, ΞΊ ids, URLs) wrap instead of widening the page. */
91
+ .holo-break { overflow-wrap: anywhere; word-break: break-word; }
92
+
93
+ /* ════════════════════════════════════════════════════════════════════════════════
94
+ * CONTAINER-QUERY WINDOW-SIZE CLASSES β€” Material Design 3 adaptive layout.
95
+ *
96
+ * A Holo app renders INSIDE a window/iframe, not at the device viewport β€” so its
97
+ * responsiveness must key on the app's OWN width, not the screen. We establish the app
98
+ * surface as a named container (`holoapp`) and expose MD3 window-size classes
99
+ * (Compact <600 Β· Medium 600–839 Β· Expanded β‰₯840) as @container queries + a --holo-size
100
+ * token. The SAME app is therefore Compact whether maximized on a phone OR floated in a
101
+ * small desktop window β€” one rule, every context, no per-app viewport breakpoints. The
102
+ * host marks the surface with [data-holo-surface] (the shell run-injection Β· the scaffold
103
+ * template Β· holo-embed Β· the Service Worker for standalone-served apps); OS chrome never
104
+ * sets it, so this whole block is inert there.
105
+ *
106
+ * PWA: combined with viewport-fit=cover + the env() safe-areas above, this is exactly
107
+ * what an installed display:standalone PWA needs β€” the layer makes an app correct in a
108
+ * browser tab AND installed to the home screen, with no app-specific work.
109
+ * ──────────────────────────────────────────────────────────────────────────────── */
110
+ :where(html[data-holo-surface]) { container: holoapp / inline-size; --holo-size: expanded; }
111
+ /* body INHERITS the default expanded; the @container rules set --holo-size DIRECTLY on body β€” a declared
112
+ * value beats an inherited one even at :where() specificity 0, so the narrower class wins when the
113
+ * container matches, without out-specifying the app. Read the class as var(--holo-size). */
114
+ @container holoapp (max-width: 839.98px) { :where(body) { --holo-size: medium; } }
115
+ @container holoapp (max-width: 599.98px) { :where(body) { --holo-size: compact; } }
116
+
117
+ /* Opt-in adaptive utilities β€” declarative Compact behavior keyed to the CONTAINER, so an
118
+ * app reflows correctly in a small window or on a phone without authoring breakpoints. */
119
+ .holo-cq-cols { display: grid; gap: var(--holo-space-3);
120
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); } /* multi-col β†’ single col when narrow */
121
+ @container holoapp (max-width: 599.98px) {
122
+ .holo-cq-row { flex-direction: column; align-items: stretch; } /* a horizontal row stacks when Compact */
123
+ .holo-hide-compact { display: none !important; } /* drop secondary chrome on a phone-class surface */
124
+ }
125
+ @container holoapp (min-width: 600px) {
126
+ .holo-show-compact { display: none !important; } /* Compact-only affordances hide when roomy */
127
+ }
sha256/c4414e9c61a6ae6b8c19145dddbbe24c68bea83784666434a94cf033b06cc483 ADDED
@@ -0,0 +1,384 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Holo Theme (holo-theme.css) β€” the Hologram OS theming kernel. Companion to holo-mobile.css:
2
+ * that file enforces mobile *mechanics*; this one defines the *visual language*
3
+ * (palette, typography, density) as a runtime contract every holospace can adopt.
4
+ *
5
+ * 100% W3C standards-track β€” NO framework, NO build tool is the source of truth:
6
+ * β€’ CSS Custom Properties for Cascading Variables L1 β€” the tokens
7
+ * β€’ CSS Properties and Values API (@property) β€” the typed token schema
8
+ * β€’ CSS Cascading & Inheritance L5 (@layer) β€” OS / app / user precedence
9
+ * β€’ CSS Color 4 + Color Adjustment L1 (light-dark()) β€” light & dark in one value
10
+ * β€’ Media Queries L5 (prefers-*) β€” honor system + a11y defaults
11
+ *
12
+ * The theme PAYLOAD is this file; the theme IDENTITY is its ΞΊ (sha256 + JSON-LD + SRI
13
+ * + did:holo:, emitted by build-holo-site.mjs per ADR-022). Switched live by
14
+ * holo-theme.js over the postMessage iframe tree. Gated by theme-conformance-witness.mjs.
15
+ *
16
+ * Precedence (last @layer wins): an app's UNLAYERED rules beat any layer here, so an
17
+ * app keeps its own look by simply declaring its own values ("own"). An app that does
18
+ * NOT declare a token falls through to the OS value ("adopt"). The user always wins for
19
+ * accessibility because holo-theme.js applies font-scale/density as INLINE custom
20
+ * properties on :root (author inline beats stylesheet rules; enforce-os adds !important).
21
+ */
22
+
23
+ @layer hos-base, hos-os, app, hos-user;
24
+
25
+ /* ── Typed token schema (W3C @property) β€” gives tokens a type, an initial value, and
26
+ * inheritance without any JSON format or compiler. Invalid values are ignored. ─── */
27
+ @property --holo-font-scale { syntax: "<number>"; inherits: true; initial-value: 1; }
28
+ @property --holo-density { syntax: "<number>"; inherits: true; initial-value: 1; }
29
+ @property --holo-accent { syntax: "<color>"; inherits: true; initial-value: #5b8cff; }
30
+ /* Minimum text size β€” the readability FLOOR (default 16px). Set live by the engine on :root;
31
+ * the type ramp (holo-mobile.css) and the root font-size below clamp UP to it with max(), so no
32
+ * OS/app text rendered through the tokens can fall below it. 0 = off (restore the raw ramp). */
33
+ @property --holo-font-min { syntax: "<length>"; inherits: true; initial-value: 16px; }
34
+
35
+ @layer hos-os {
36
+ :root {
37
+ /* Enable light-dark(); palette attribute (set by the engine) pins one mode. */
38
+ color-scheme: light dark;
39
+
40
+ /* ── Typography β€” the OS-settable type system (Hologram OS β€Ί Settings β€Ί Display).
41
+ * --holo-font-scale drives the ROOT font-size, so every rem-based size and the
42
+ * --holo-text* ramp (holo-mobile.css, authored in rem) scale from ONE lever. */
43
+ --holo-font-scale: 1; /* 0.85 … 1.4, set live by the engine */
44
+ /* Default OS typeface: Helvetica Neue (the iPhone's face) β€” a system font on Apple
45
+ * devices; falls back to Helvetica/Arial elsewhere. (Not vendored; it's proprietary.) */
46
+ --holo-font-sans: "Helvetica Neue", Helvetica, Arial, "Segoe UI", Roboto, system-ui, sans-serif;
47
+ --holo-font-serif: ui-serif, Georgia, "Times New Roman", serif;
48
+ --holo-font-mono: ui-monospace, "Cascadia Code", "JetBrains Mono", Menlo, Consolas, monospace;
49
+
50
+ /* ── Density β€” multiplies UI spacing; immersive mode tightens it. */
51
+ --holo-density: 1;
52
+
53
+ /* Focus ring width (WCAG 2.4.7/2.4.13); bumped under prefers-contrast. */
54
+ --holo-focus-width: 3px;
55
+
56
+ /* ── Palette (Chakra-derived, ADR) β€” core neutrals rebased onto Chakra's zinc ramp so every
57
+ * vendored shadcn/Magic UI component renders on the palette it was designed for, while the
58
+ * Holo brand accent is preserved. light & dark in one value (CSS Color Adjustment L1). */
59
+ --holo-bg: light-dark(#fafafa, #09090B);
60
+ --holo-surface: light-dark(#FFFFFF, #111111);
61
+ --holo-surface-2: light-dark(#f4f4f5, #18181b);
62
+ --holo-surface-emphasized: light-dark(#e4e4e7, #27272a);
63
+ --holo-border: light-dark(#e4e4e7, #27272a);
64
+ --holo-border-subtle: light-dark(#f4f4f5, #18181b);
65
+ --holo-border-emphasized: light-dark(#d4d4d8, #3f3f46);
66
+ --holo-ink: light-dark(#18181b, #fafafa);
67
+ --holo-ink-dim: light-dark(#71717a, #a1a1aa);
68
+ --holo-ink-subtle: light-dark(#a1a1aa, #71717a);
69
+ --holo-accent: light-dark(#3b5bdb, #5b8cff); /* Holo brand β€” preserved */
70
+ --holo-accent-ink: #ffffff;
71
+ --holo-ok: light-dark(#16a34a, #22c55e);
72
+ --holo-warn: light-dark(#ea580c, #f97316);
73
+ --holo-danger: light-dark(#dc2626, #ef4444);
74
+ /* Derived surface tints β€” Color 4 color-mix(), no extra tokens to maintain. */
75
+ --holo-accent-soft: color-mix(in srgb, var(--holo-accent) 16%, transparent);
76
+ --holo-hover: color-mix(in srgb, var(--holo-ink) 8%, transparent);
77
+
78
+ /* ── Chakra-derived foundation (ramps + scales) β€” additive, content-addressed theme system. */
79
+ /* color ramps β€” Chakra's tuned hues, available to every app (charts, status, colorPalette). */
80
+ --holo-white: #FFFFFF; --holo-black: #09090B;
81
+
82
+ /* gray */
83
+ --holo-gray-50: #fafafa;
84
+ --holo-gray-100: #f4f4f5;
85
+ --holo-gray-200: #e4e4e7;
86
+ --holo-gray-300: #d4d4d8;
87
+ --holo-gray-400: #a1a1aa;
88
+ --holo-gray-500: #71717a;
89
+ --holo-gray-600: #52525b;
90
+ --holo-gray-700: #3f3f46;
91
+ --holo-gray-800: #27272a;
92
+ --holo-gray-900: #18181b;
93
+ --holo-gray-950: #111111;
94
+
95
+ /* red */
96
+ --holo-red-50: #fef2f2;
97
+ --holo-red-100: #fee2e2;
98
+ --holo-red-200: #fecaca;
99
+ --holo-red-300: #fca5a5;
100
+ --holo-red-400: #f87171;
101
+ --holo-red-500: #ef4444;
102
+ --holo-red-600: #dc2626;
103
+ --holo-red-700: #991919;
104
+ --holo-red-800: #511111;
105
+ --holo-red-900: #300c0c;
106
+ --holo-red-950: #1f0808;
107
+
108
+ /* orange */
109
+ --holo-orange-50: #fff7ed;
110
+ --holo-orange-100: #ffedd5;
111
+ --holo-orange-200: #fed7aa;
112
+ --holo-orange-300: #fdba74;
113
+ --holo-orange-400: #fb923c;
114
+ --holo-orange-500: #f97316;
115
+ --holo-orange-600: #ea580c;
116
+ --holo-orange-700: #92310a;
117
+ --holo-orange-800: #6c2710;
118
+ --holo-orange-900: #3b1106;
119
+ --holo-orange-950: #220a04;
120
+
121
+ /* yellow */
122
+ --holo-yellow-50: #fefce8;
123
+ --holo-yellow-100: #fef9c3;
124
+ --holo-yellow-200: #fef08a;
125
+ --holo-yellow-300: #fde047;
126
+ --holo-yellow-400: #facc15;
127
+ --holo-yellow-500: #eab308;
128
+ --holo-yellow-600: #ca8a04;
129
+ --holo-yellow-700: #845209;
130
+ --holo-yellow-800: #713f12;
131
+ --holo-yellow-900: #422006;
132
+ --holo-yellow-950: #281304;
133
+
134
+ /* green */
135
+ --holo-green-50: #f0fdf4;
136
+ --holo-green-100: #dcfce7;
137
+ --holo-green-200: #bbf7d0;
138
+ --holo-green-300: #86efac;
139
+ --holo-green-400: #4ade80;
140
+ --holo-green-500: #22c55e;
141
+ --holo-green-600: #16a34a;
142
+ --holo-green-700: #116932;
143
+ --holo-green-800: #124a28;
144
+ --holo-green-900: #042713;
145
+ --holo-green-950: #03190c;
146
+
147
+ /* teal */
148
+ --holo-teal-50: #f0fdfa;
149
+ --holo-teal-100: #ccfbf1;
150
+ --holo-teal-200: #99f6e4;
151
+ --holo-teal-300: #5eead4;
152
+ --holo-teal-400: #2dd4bf;
153
+ --holo-teal-500: #14b8a6;
154
+ --holo-teal-600: #0d9488;
155
+ --holo-teal-700: #0c5d56;
156
+ --holo-teal-800: #114240;
157
+ --holo-teal-900: #032726;
158
+ --holo-teal-950: #021716;
159
+
160
+ /* blue */
161
+ --holo-blue-50: #eff6ff;
162
+ --holo-blue-100: #dbeafe;
163
+ --holo-blue-200: #bfdbfe;
164
+ --holo-blue-300: #a3cfff;
165
+ --holo-blue-400: #60a5fa;
166
+ --holo-blue-500: #3b82f6;
167
+ --holo-blue-600: #2563eb;
168
+ --holo-blue-700: #173da6;
169
+ --holo-blue-800: #1a3478;
170
+ --holo-blue-900: #14204a;
171
+ --holo-blue-950: #0c142e;
172
+
173
+ /* cyan */
174
+ --holo-cyan-50: #ecfeff;
175
+ --holo-cyan-100: #cffafe;
176
+ --holo-cyan-200: #a5f3fc;
177
+ --holo-cyan-300: #67e8f9;
178
+ --holo-cyan-400: #22d3ee;
179
+ --holo-cyan-500: #06b6d4;
180
+ --holo-cyan-600: #0891b2;
181
+ --holo-cyan-700: #0c5c72;
182
+ --holo-cyan-800: #134152;
183
+ --holo-cyan-900: #072a38;
184
+ --holo-cyan-950: #051b24;
185
+
186
+ /* purple */
187
+ --holo-purple-50: #faf5ff;
188
+ --holo-purple-100: #f3e8ff;
189
+ --holo-purple-200: #e9d5ff;
190
+ --holo-purple-300: #d8b4fe;
191
+ --holo-purple-400: #c084fc;
192
+ --holo-purple-500: #a855f7;
193
+ --holo-purple-600: #9333ea;
194
+ --holo-purple-700: #641ba3;
195
+ --holo-purple-800: #4a1772;
196
+ --holo-purple-900: #2f0553;
197
+ --holo-purple-950: #1a032e;
198
+
199
+ /* pink */
200
+ --holo-pink-50: #fdf2f8;
201
+ --holo-pink-100: #fce7f3;
202
+ --holo-pink-200: #fbcfe8;
203
+ --holo-pink-300: #f9a8d4;
204
+ --holo-pink-400: #f472b6;
205
+ --holo-pink-500: #ec4899;
206
+ --holo-pink-600: #db2777;
207
+ --holo-pink-700: #a41752;
208
+ --holo-pink-800: #6d0e34;
209
+ --holo-pink-900: #45061f;
210
+ --holo-pink-950: #2c0514;
211
+
212
+ /* alpha ramps */
213
+ --holo-whiteAlpha-50: rgba(255, 255, 255, 0.04);
214
+ --holo-whiteAlpha-100: rgba(255, 255, 255, 0.06);
215
+ --holo-whiteAlpha-200: rgba(255, 255, 255, 0.08);
216
+ --holo-whiteAlpha-300: rgba(255, 255, 255, 0.16);
217
+ --holo-whiteAlpha-400: rgba(255, 255, 255, 0.24);
218
+ --holo-whiteAlpha-500: rgba(255, 255, 255, 0.36);
219
+ --holo-whiteAlpha-600: rgba(255, 255, 255, 0.48);
220
+ --holo-whiteAlpha-700: rgba(255, 255, 255, 0.64);
221
+ --holo-whiteAlpha-800: rgba(255, 255, 255, 0.80);
222
+ --holo-whiteAlpha-900: rgba(255, 255, 255, 0.92);
223
+ --holo-blackAlpha-50: rgba(0, 0, 0, 0.04);
224
+ --holo-blackAlpha-100: rgba(0, 0, 0, 0.06);
225
+ --holo-blackAlpha-200: rgba(0, 0, 0, 0.08);
226
+ --holo-blackAlpha-300: rgba(0, 0, 0, 0.16);
227
+ --holo-blackAlpha-400: rgba(0, 0, 0, 0.24);
228
+ --holo-blackAlpha-500: rgba(0, 0, 0, 0.36);
229
+ --holo-blackAlpha-600: rgba(0, 0, 0, 0.48);
230
+ --holo-blackAlpha-700: rgba(0, 0, 0, 0.64);
231
+ --holo-blackAlpha-800: rgba(0, 0, 0, 0.80);
232
+ --holo-blackAlpha-900: rgba(0, 0, 0, 0.92);
233
+
234
+ /* spacing scale (rem) β€” fills --holo-space-3/4 used by holo-theme.css and drives the primitives. */
235
+ --holo-space-1: 0.25rem;
236
+ --holo-space-2: 0.5rem;
237
+ --holo-space-3: 0.75rem;
238
+ --holo-space-4: 1rem;
239
+ --holo-space-5: 1.25rem;
240
+ --holo-space-6: 1.5rem;
241
+ --holo-space-7: 1.75rem;
242
+ --holo-space-8: 2rem;
243
+ --holo-space-9: 2.25rem;
244
+ --holo-space-10: 2.5rem;
245
+ --holo-space-11: 2.75rem;
246
+ --holo-space-12: 3rem;
247
+ --holo-space-14: 3.5rem;
248
+ --holo-space-16: 4rem;
249
+ --holo-space-20: 5rem;
250
+ --holo-space-24: 6rem;
251
+ --holo-space-28: 7rem;
252
+ --holo-space-32: 8rem;
253
+ --holo-space-36: 9rem;
254
+ --holo-space-40: 10rem;
255
+ --holo-space-44: 11rem;
256
+ --holo-space-48: 12rem;
257
+ --holo-space-52: 13rem;
258
+ --holo-space-56: 14rem;
259
+ --holo-space-60: 15rem;
260
+ --holo-space-64: 16rem;
261
+ --holo-space-72: 18rem;
262
+ --holo-space-80: 20rem;
263
+ --holo-space-96: 24rem;
264
+ --holo-space-0p5: 0.125rem;
265
+ --holo-space-1p5: 0.375rem;
266
+ --holo-space-2p5: 0.625rem;
267
+ --holo-space-3p5: 0.875rem;
268
+ --holo-space-4p5: 1.125rem;
269
+
270
+ /* radius scale β€” --holo-radius keeps the OS default (0.75rem = the 12px components assumed). */
271
+ --holo-radius-none: 0;
272
+ --holo-radius-2xs: 0.0625rem;
273
+ --holo-radius-xs: 0.125rem;
274
+ --holo-radius-sm: 0.25rem;
275
+ --holo-radius-md: 0.375rem;
276
+ --holo-radius-lg: 0.5rem;
277
+ --holo-radius-xl: 0.75rem;
278
+ --holo-radius-2xl: 1rem;
279
+ --holo-radius-3xl: 1.5rem;
280
+ --holo-radius-4xl: 2rem;
281
+ --holo-radius-full: 9999px;
282
+ --holo-radius: var(--holo-radius-xl);
283
+
284
+ /* weight Β· line-height Β· tracking Β· elevation scales */
285
+ --holo-weight-thin: 100;
286
+ --holo-weight-extralight: 200;
287
+ --holo-weight-light: 300;
288
+ --holo-weight-normal: 400;
289
+ --holo-weight-medium: 500;
290
+ --holo-weight-semibold: 600;
291
+ --holo-weight-bold: 700;
292
+ --holo-weight-extrabold: 800;
293
+ --holo-weight-black: 900;
294
+ --holo-leading-shorter: 1.25;
295
+ --holo-leading-short: 1.375;
296
+ --holo-leading-moderate: 1.5;
297
+ --holo-leading-tall: 1.625;
298
+ --holo-leading-taller: 2;
299
+ --holo-tracking-tighter: -0.05em;
300
+ --holo-tracking-tight: -0.025em;
301
+ --holo-tracking-wide: 0.025em;
302
+ --holo-tracking-wider: 0.05em;
303
+ --holo-tracking-widest: 0.1em;
304
+ --holo-shadow-xs: 0 1px 2px light-dark(rgba(0,0,0,0.06), rgba(0,0,0,0.40));
305
+ --holo-shadow-sm: 0 1px 3px light-dark(rgba(0,0,0,0.08), rgba(0,0,0,0.46)), 0 1px 2px light-dark(rgba(0,0,0,0.06), rgba(0,0,0,0.40));
306
+ --holo-shadow-md: 0 4px 12px -2px light-dark(rgba(0,0,0,0.10), rgba(0,0,0,0.52));
307
+ --holo-shadow-lg: 0 12px 28px -8px light-dark(rgba(0,0,0,0.14), rgba(0,0,0,0.60));
308
+ --holo-shadow-xl: 0 24px 56px -16px light-dark(rgba(0,0,0,0.18), rgba(0,0,0,0.66));
309
+
310
+ /* ── Glass / translucency (ADR-0059) β€” TranslucentTB's accent-state model
311
+ * (holo-translucenttb-accents.json) expressed as CSS. A surface picks ONE state
312
+ * via its own [data-glass] attribute; CSS blur px = TranslucentTB blur_radius/3.
313
+ * Acrylic = blur + saturate + a faint accent tint (Fluent material). Used by the
314
+ * dock (#holo-dock) and adoptable by any chrome that wants the same glass. */
315
+ --holo-glass-tint: light-dark(rgba(246,248,252,0.72), rgba(22,26,34,0.70));
316
+ --holo-glass-blur: 22px; /* β‰ˆ TranslucentTB blur_radius 66 / 3 β€” crisp Fluent frost */
317
+ --holo-glass-sat: 1.8;
318
+ --holo-glass-border: light-dark(rgba(255,255,255,0.90), rgba(255,255,255,0.22));
319
+ --holo-glass-edge: light-dark(rgba(255,255,255,0.85), rgba(255,255,255,0.28)); /* inset top bevel β€” reads on any bg */
320
+ --holo-glass-shadow: 0 0.5rem 2.5rem light-dark(rgba(20,30,50,0.28), rgba(0,0,0,0.60));
321
+ --holo-glass-opaque-bg: light-dark(#ffffff, #14161b); --holo-glass-opaque-fx: none;
322
+ --holo-glass-clear-bg: transparent; --holo-glass-clear-fx: none;
323
+ --holo-glass-blur-bg: var(--holo-glass-tint); --holo-glass-blur-fx: blur(var(--holo-glass-blur)) saturate(1.4) brightness(1.04);
324
+ --holo-glass-acrylic-bg: color-mix(in srgb, var(--holo-glass-tint) 86%, var(--holo-accent) 14%);
325
+ --holo-glass-acrylic-fx: blur(var(--holo-glass-blur)) saturate(var(--holo-glass-sat)) brightness(1.08);
326
+ }
327
+
328
+ /* Explicit palette beats the system default (Media Queries L5 still applies on auto). */
329
+ :root[data-holo-palette="light"] { color-scheme: light; }
330
+ :root[data-holo-palette="dark"] { color-scheme: dark; }
331
+
332
+ /* Presentation axis β€” orthogonal to palette. Immersive = denser chrome, calm surfaces. */
333
+ :root[data-holo-presentation="immersive"] { --holo-density: 0.82; }
334
+
335
+ /* ── Global application at specificity 0 (:where) so ANY app rule overrides freely.
336
+ * This is the only place the OS reaches every surface by default; it intentionally
337
+ * sets typography (the user's readability) but NOT colors (apps keep their identity
338
+ * unless they adopt the tokens), matching holo-mobile.css's restraint. */
339
+ :where(html) { font-size: max(var(--holo-font-min, 16px), calc(100% * var(--holo-font-scale, 1))); }
340
+ /* font-optical-sizing + synthesis are CSS Fonts Module controls (real type quality). */
341
+ :where(body) { font-family: var(--holo-font-sans); font-optical-sizing: auto; font-variation-settings: var(--holo-font-variation, normal); }
342
+ :where(code, pre, kbd, samp, .holo-mono) { font-family: var(--holo-font-mono); }
343
+
344
+ /* Visible keyboard focus (WCAG 2.4.7 Focus Visible / 2.4.13 Focus Appearance). Mouse
345
+ * focus stays quiet via :focus-visible; specificity 0 so apps can restyle, not remove. */
346
+ :where(:focus-visible) { outline: var(--holo-focus-width, 3px) solid var(--holo-accent); outline-offset: 2px; }
347
+
348
+ /* Honor the user's motion preference OS-wide (WCAG 2.3.3 / Media Queries L5). */
349
+ @media (prefers-reduced-motion: reduce) {
350
+ :where(*, *::before, *::after) {
351
+ animation-duration: 0.001ms !important; animation-iteration-count: 1 !important;
352
+ transition-duration: 0.001ms !important; scroll-behavior: auto !important;
353
+ }
354
+ }
355
+
356
+ /* Honor the user's contrast preference (Media Queries L5 / WCAG 1.4.6 + 1.4.11):
357
+ * push borders/ink to the extremes and widen the focus ring. */
358
+ @media (prefers-contrast: more) {
359
+ :root {
360
+ --holo-border: light-dark(#000000, #ffffff);
361
+ --holo-ink: light-dark(#000000, #ffffff);
362
+ --holo-ink-dim: light-dark(#26303a, #d7dee6);
363
+ --holo-focus-width: 4px;
364
+ }
365
+ }
366
+ }
367
+
368
+ /* ── Adopt-in primitives β€” the standard way to render OS-themed surface chrome. Apps
369
+ * opt in by using these tokens/classes; they then follow palette + font changes. */
370
+ .holo-surface { background: var(--holo-surface); color: var(--holo-ink); border: 1px solid var(--holo-border); border-radius: var(--holo-radius, 12px); }
371
+ .holo-accent { background: var(--holo-accent); color: var(--holo-accent-ink); }
372
+ .holo-ink-dim { color: var(--holo-ink-dim); }
373
+
374
+ /* ── Container-query responsiveness (W3C CSS Containment / Container Queries) ────────
375
+ * A holospace nested in a small iframe must lay out by its OWN width, not the device
376
+ * viewport β€” that's exactly what container queries give us (media queries can't see
377
+ * the iframe's box). A region opts in with .holo-cq, and its children adapt to the
378
+ * CONTAINER size. This is the standards-correct answer for an OS of nested frames. */
379
+ .holo-cq { container-type: inline-size; container-name: holo; }
380
+ .holo-cq-grid { display: grid; gap: var(--holo-space-4, 16px); grid-template-columns: 1fr; }
381
+ @container holo (min-width: 30rem) { .holo-cq-grid { grid-template-columns: repeat(2, 1fr); } }
382
+ @container holo (min-width: 52rem) { .holo-cq-grid { grid-template-columns: repeat(3, 1fr); } }
383
+ .holo-cq-row { display: flex; flex-direction: column; gap: var(--holo-space-3, 12px); }
384
+ @container holo (min-width: 34rem) { .holo-cq-row { flex-direction: row; align-items: center; } }