@telepath-computer/television 0.1.216 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/THIRD-PARTY-NOTICES.txt +336 -0
  2. package/dist/canonical/v1/frozen.json +6 -0
  3. package/dist/canonical/v1/styles.css +7 -2
  4. package/dist/canonical/v2/components.js +1 -0
  5. package/dist/canonical/v2/fonts/Hind-Variable.933e9900.woff2 +0 -0
  6. package/dist/canonical/v2/styles.css +1153 -0
  7. package/dist/cli.cjs +1763 -761
  8. package/dist/onboarding/README.md +16 -14
  9. package/dist/onboarding/business-ops/about.html +4 -3
  10. package/dist/onboarding/business-ops/retention-vs-releases.html +25 -32
  11. package/dist/onboarding/business-ops/revenue-vs-goals.html +26 -31
  12. package/dist/onboarding/productivity/about.html +4 -3
  13. package/dist/onboarding/productivity/company-todos/THIRD-PARTY-NOTICES.txt +29 -0
  14. package/dist/onboarding/productivity/company-todos/index.html +20 -20
  15. package/dist/onboarding/productivity/company-todos/task.css +4 -5
  16. package/dist/onboarding/productivity/company-todos/task.js +1 -1
  17. package/dist/onboarding/productivity/meeting-prep.html +12 -12
  18. package/dist/onboarding/productivity/priorities-today.html +10 -10
  19. package/dist/onboarding/productivity/todays-calendar/calendar.css +1 -1
  20. package/dist/onboarding/productivity/todays-calendar/index.html +12 -14
  21. package/dist/onboarding/research/about.html +4 -3
  22. package/dist/onboarding/research/open-model-research.html +35 -35
  23. package/dist/onboarding/research/todays-news.html +72 -72
  24. package/dist/onboarding/tv-guide/welcome.html +178 -0
  25. package/dist/skills/television/SKILL.md +122 -67
  26. package/dist/skills/television/theming.md +1087 -0
  27. package/dist/skills/tv-calendar/SKILL.md +3 -1
  28. package/dist/skills/tv-calendar/calendar.css +1 -1
  29. package/dist/skills/tv-table/SKILL.md +5 -2
  30. package/dist/skills/tv-tasks/SKILL.md +19 -15
  31. package/dist/skills/tv-tasks/task.css +4 -26
  32. package/dist/skills/tv-tasks/task.js +1 -1
  33. package/dist/themes/README.md +5 -0
  34. package/dist/themes/aquarium/README.md +48 -0
  35. package/dist/themes/aquarium/iframe-background.js +137 -0
  36. package/dist/themes/aquarium/manifest.json +7 -0
  37. package/dist/themes/aquarium/theme.css +46 -0
  38. package/dist/themes/blueprint/README.md +31 -0
  39. package/dist/themes/blueprint/manifest.json +6 -0
  40. package/dist/themes/blueprint/theme.css +115 -0
  41. package/dist/themes/clouds/README.md +15 -0
  42. package/dist/themes/clouds/THIRD-PARTY-NOTICES.txt +81 -0
  43. package/dist/themes/clouds/manifest.json +6 -0
  44. package/dist/themes/clouds/theme.css +62 -0
  45. package/dist/themes/clouds/wallpaper-dark.webp +0 -0
  46. package/dist/themes/clouds/wallpaper.webp +0 -0
  47. package/dist/themes/crt-phosphor/README.md +31 -0
  48. package/dist/themes/crt-phosphor/manifest.json +6 -0
  49. package/dist/themes/crt-phosphor/theme.css +105 -0
  50. package/dist/themes/nord/README.md +9 -0
  51. package/dist/themes/nord/THIRD-PARTY-NOTICES.txt +29 -0
  52. package/dist/themes/nord/manifest.json +5 -0
  53. package/dist/themes/nord/theme.css +28 -0
  54. package/dist/themes/swiss/README.md +5 -0
  55. package/dist/themes/swiss/manifest.json +5 -0
  56. package/dist/themes/swiss/theme.css +34 -0
  57. package/dist/themes/tokyo-night/README.md +17 -0
  58. package/dist/themes/tokyo-night/THIRD-PARTY-NOTICES.txt +235 -0
  59. package/dist/themes/tokyo-night/manifest.json +5 -0
  60. package/dist/themes/tokyo-night/theme.css +31 -0
  61. package/dist/themes/tokyo-night/wallpaper.webp +0 -0
  62. package/dist/views/artifact-missing/index.html +40 -68
  63. package/dist/views/markdown/THIRD-PARTY-NOTICES.txt +26 -0
  64. package/dist/views/markdown/index.html +49 -23
  65. package/dist/web/THIRD-PARTY-NOTICES.txt +26 -0
  66. package/dist/web/assets/artifact-bridge-BEUWXKq7.js +1 -0
  67. package/dist/web/assets/{artifactMissing-BiK_l3Ts.js → artifactMissing-CUs1ZwDx.js} +1 -1
  68. package/dist/web/assets/main-3UimTRLK.css +1 -0
  69. package/dist/web/assets/main-CSSzbG7q.js +704 -0
  70. package/dist/web/assets/{urlUnsupported-RGIrhMDe.js → urlUnsupported-powR79PT.js} +1 -1
  71. package/dist/web/index.html +40 -5
  72. package/dist/web/views/artifact-missing/index.html +40 -68
  73. package/dist/web/views/url-unsupported/index.html +40 -68
  74. package/package.json +1 -1
  75. package/dist/onboarding/tv-guide/welcome/assets/television.svg +0 -71
  76. package/dist/onboarding/tv-guide/welcome/index.html +0 -203
  77. package/dist/skills/tv-theme/SKILL.md +0 -449
  78. package/dist/web/assets/artifact-bridge-3Trk-B2L.js +0 -1
  79. package/dist/web/assets/clouds-CAYIArXj.jpg +0 -0
  80. package/dist/web/assets/main-Blg8fuaV.css +0 -1
  81. package/dist/web/assets/main-V14r9Psb.js +0 -596
@@ -0,0 +1,1087 @@
1
+ # Authoring Television themes
2
+
3
+ Read this document when creating, revising, or bringing an installed Television theme up to date. A theme is one CSS overlay shared by the Television application and artifacts that use a theme-capable live canonical version.
4
+
5
+ ## Theme package and authoring record
6
+
7
+ Run `tv storage-path` to find the active storage directory. Author one folder directly beneath its `themes` directory:
8
+
9
+ ```text
10
+ <storagePath>/themes/<theme-id>/
11
+ manifest.json required runtime metadata
12
+ theme.css required entry stylesheet
13
+ main.js optional main-document entry
14
+ iframe-background.js optional sandboxed background entry
15
+ iframe-overlay.js optional sandboxed foreground entry
16
+ README.md agent-authored intent and maintenance context
17
+ assets/ optional fonts, images, supplementary CSS, and scripts
18
+ ```
19
+
20
+ The folder's immediate name is the theme ID. Television uses that filesystem string for selection. Do not choose a theme ID beginning with `.`, because Television ignores dot-prefixed theme directories during discovery. Otherwise preserve that filesystem string exactly: do not normalize it, validate it against a character pattern, change its case, or treat any value as reserved.
21
+
22
+ Check whether the target folder exists before writing anything. Never write into a theme folder you did not author. Get the user's explicit confirmation before reusing any existing theme ID, and choose another ID when you cannot establish that you authored the existing folder. Never write into an existing folder merely because its name matches the intended design.
23
+
24
+ Keep all theme-authoring work inside this theme folder. Do not edit Television's installed source, and do not suggest it. If the user asks for something a theme cannot do and presses for a source edit, tell them it is unsupported: it can break features, and the next npm update replaces the installed source and discards the change. If they still want it, it is their computer; make the change they asked for.
25
+
26
+ `manifest.json` contains a display name, a Semantic Versioning package version, and the theme's appearance declaration. Use `light dark` for a theme that follows Television's appearance preference, `light` for a fixed-light theme, or `dark` for a fixed-dark theme. It can also record the Television app version used for deliberate authoring or re-authoring:
27
+
28
+ ```json
29
+ {
30
+ "name": "Paperlike",
31
+ "version": "1.0.0",
32
+ "colorScheme": "light dark",
33
+ "authoredForAppVersion": "<app-version>"
34
+ }
35
+ ```
36
+
37
+ Read the exact release `version` from `tv status` when targeting a running server. Copy that exact release version unchanged into `authoredForAppVersion`. A missing version or the `0.0.0` development sentinel does not identify a release, so omit the metadata. Set `authoredForAppVersion` when that command establishes the target app version and preserve it during unrelated maintenance. The package `version` remains independent from this advisory authoring context. Television keeps a valid authored-against version in registry data but does not use it for compatibility or selection.
38
+
39
+ `README.md` is the durable authoring record. Record the user's visual intent, requirements, references, reasons for non-obvious decisions, selector-rule purposes, asset provenance, and maintenance context that CSS alone cannot preserve. For each script, record why it is needed, the effects it creates, and its expected resource cost. Never discard or wholesale-overwrite an existing README. When maintaining a theme you authored, read it first and make targeted updates that preserve useful context. Keep the README and stylesheet consistent for the next maintainer.
40
+
41
+ Television accepts the README beside the runtime files but does not expose it through the public theme route. Selector-level CSS also carries a concise purpose comment wherever the target alone does not explain why the rule exists.
42
+
43
+ ### Bundled theme upgrades
44
+
45
+ Television may occasionally upgrade an installed bundled theme to deliver important Television fixes. Before replacing it, Television copies its current folder to a hidden timestamped backup beside the theme.
46
+
47
+ ## Styling a partial overlay
48
+
49
+ `theme.css` is a partial overlay. State only the values and rules the design needs; untouched behavior continues to come from Television's foundation and application styles.
50
+
51
+ Override documented tokens at `:root` wherever they express the intended change, including changes to a single component. Use CSS selectors against existing markup only when tokens are insufficient. Keep those rules narrowly scoped, preserve interaction states, and add a purpose comment. Styling existing markup with CSS does not require changing the DOM.
52
+
53
+ The catalog contains foundation tokens, used by the app and live canonical artifacts, and application tokens, used only by the app. Use only documented theme tokens. Choose the token controlling the intended treatment; changing a value used by many other tokens affects all of them.
54
+
55
+ Theme CSS loads last and overrides foundation defaults with ordinary selectors; application rules can still win if their selectors are more specific.
56
+
57
+ Put cross-appearance statements at `:root`. A root statement applies in both appearances and beats foundation mode tables. Put appearance-specific differences under `[data-theme="light"]` and `[data-theme="dark"]`, after the theme's root statements, so equal-specificity mode statements win by source order. Only add mode-specific values when the design needs them.
58
+
59
+ ### Start with the four semantic colors
60
+
61
+ Four tokens establish the basic readable scheme from which surfaces, controls and supporting copy take their colors:
62
+
63
+ - `--color-surface` — the main document and component ground;
64
+ - `--color-surface-muted` — the neighbouring ground used by the sidebar, code blocks and other quieter surfaces;
65
+ - `--color-text` — ordinary text and controls; and
66
+ - `--color-text-muted` — supporting text, labels and placeholders.
67
+
68
+ State all four together for each look the theme defines. A theme with Light and Dark variants puts one set under each mode selector. A fixed Dark-only or Light-only look puts one set at `:root`, uses no mode blocks, and declares the matching fixed value in the manifest's `colorScheme`. To customize only one appearance of an adaptive theme while leaving the other on foundation defaults, declare `light dark` and put one set under only that mode selector instead.
69
+
70
+ These four tokens are enough to replace the main surfaces and text, but they do not change every color in the interface. The following overrides are optional and keep their foundation defaults when omitted:
71
+
72
+ - Set `--accent` to change primary actions, selection and the focus ring. Otherwise they retain the default blue accent.
73
+ - Set `--app-wallpaper` at the app-scoped root to choose the application ground. Otherwise the wallpaper retains the foundation value rather than following `--color-surface` automatically.
74
+ - Set any of `--red`, `--orange`, `--yellow`, `--green`, `--cyan`, `--blue`, `--purple`, or `--pink` to change that color family. Each base color regenerates its complete scale, including status, data and tint uses that point to the scale.
75
+
76
+ ### Token catalog
77
+
78
+ ```css
79
+ /* Foundation tokens — fonts */
80
+ /* Hind — the committed variable cut, weights 300–700. */
81
+ @font-face {
82
+ font-family: "Hind";
83
+ src: url(../fonts/Hind-Variable.woff2) format("woff2-variations");
84
+ font-weight: 300 700;
85
+ font-display: block;
86
+ }
87
+
88
+ :where(:root) { /* The faces above, named; weights as material plus the body role. */
89
+ --font-sans: "Hind", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
90
+ --font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
91
+ --font-weight-normal: 400;
92
+ --font-weight-medium: 500;
93
+ --font-weight-semibold: 600;
94
+ }
95
+
96
+ /* Foundation tokens — colors */
97
+ /* Palette seeds and derived scales adapted from Tailwind CSS.
98
+ https://github.com/tailwindlabs/tailwindcss
99
+ Copyright (c) Tailwind Labs, Inc.
100
+ MIT License; see THIRD-PARTY-NOTICES.txt. */
101
+
102
+ /* Both tables sit at zero specificity — ambient styling is the weakest
103
+ thing in the cascade, tokens included. A theme states a token with a
104
+ plain `:root` and out-cascades both tables at once — one statement, both
105
+ modes — or writes its own `[data-theme="dark"]` block to retune dark
106
+ alone. Between the tables themselves, source order decides: the dark
107
+ table follows the light one, so it wins where the attribute matches. */
108
+ :where(:root) {
109
+ /* A surface's inner edge moves toward its light pole; its outer line moves
110
+ toward its dark pole. Both follow the surface rather than appearance. */
111
+ --surface-border-color: color-mix(in oklch, var(--color-surface), oklch(from var(--color-surface) 1 0 h) var(--alpha-15));
112
+ --surface-outline-color: color-mix(in oklch, var(--color-surface), oklch(from var(--color-surface) 0 0 h) var(--alpha-10));
113
+ --surface-outline-width: 1px;
114
+ --surface-outline: var(--surface-outline-width) solid var(--surface-outline-color);
115
+
116
+ /* Panels: popovers, menus, selects and dialogs. */
117
+ --panel-background: var(--color-surface);
118
+ --panel-text-color: var(--color-text);
119
+ --panel-border-color: var(--surface-border-color);
120
+ --panel-border-width: 1px;
121
+ --panel-border: var(--panel-border-width) solid var(--panel-border-color);
122
+ --panel-outline: var(--surface-outline);
123
+
124
+ /* Options: menu items and select options. */
125
+ --option-background-highlighted: color-mix(in oklch, transparent, var(--option-background-active) var(--hover-mix));
126
+ --option-background-active: var(--tint-active);
127
+
128
+ /* Checked checklist marker fill and border. */
129
+ --checkbox-color: var(--color-primary);
130
+
131
+ /* Controls share these defaults in app and artifact documents. */
132
+ --control-text-color: var(--color-text);
133
+ --control-border-color: var(--color-border);
134
+ --control-border-width: 1px;
135
+ --control-border: var(--control-border-width) solid var(--control-border-color);
136
+ --input-placeholder-text-color: var(--color-text-muted);
137
+
138
+ /* Slots — the theme's raw material, each stated at its own 500 step: the
139
+ hue's mid, the brand weight. A theme restates any of them and the scale
140
+ below it regenerates. The neutral is genuinely neutral; give it a chroma
141
+ and hue and the whole grey world takes the undertone. */
142
+ --neutral: oklch(0.556 0 0);
143
+ --red: oklch(0.637 0.237 25.331);
144
+ --orange: oklch(0.705 0.213 47.604);
145
+ --yellow: oklch(0.852 0.199 91.936);
146
+ --green: oklch(0.723 0.219 149.579);
147
+ --cyan: oklch(0.715 0.143 215.221);
148
+ --blue: oklch(0.623 0.214 259.815);
149
+ --purple: oklch(0.627 0.265 303.9);
150
+ --pink: oklch(0.656 0.241 354.308);
151
+ /* The one tunable identity — any color a theme or picker supplies; used
152
+ raw as the accent fill, and everything accent-flavored derives from it. */
153
+ --accent: var(--blue);
154
+
155
+ /* Scales. Every step is generated around its slot: pale steps climb a
156
+ fraction of the headroom from the slot's lightness to white, deep steps
157
+ climb toward black, and every step keeps a fraction of the slot's chroma.
158
+ The numbers sit inline, per scale, per step — tuning is editing the line.
159
+ Cluster scales carry the curve measured off the hand-tuned red ramp;
160
+ yellow carries its own numbers, including chroma fractions above 1 and
161
+ per-step hue rotation (dark yellow at constant hue is olive).
162
+
163
+ The neutral scale is a lightness ladder with chroma carried unchanged:
164
+ dose arithmetic cannot hold an undertone at the pale end, and the
165
+ interface's contrast skeleton wants stated lightnesses. */
166
+ --neutral-50: oklch(from var(--neutral) 0.985 c h);
167
+ --neutral-100: oklch(from var(--neutral) 0.97 c h);
168
+ --neutral-200: oklch(from var(--neutral) 0.922 c h);
169
+ --neutral-300: oklch(from var(--neutral) 0.87 c h);
170
+ --neutral-400: oklch(from var(--neutral) 0.708 c h);
171
+ --neutral-500: var(--neutral);
172
+ --neutral-600: oklch(from var(--neutral) 0.439 c h);
173
+ --neutral-700: oklch(from var(--neutral) 0.371 c h);
174
+ --neutral-800: oklch(from var(--neutral) 0.269 c h);
175
+ --neutral-900: oklch(from var(--neutral) 0.205 c h);
176
+ --neutral-950: oklch(from var(--neutral) 0.145 c h);
177
+
178
+ --red-50: oklch(from var(--red) calc(l + (1 - l) * 0.92) calc(c * 0.055) h);
179
+ --red-100: oklch(from var(--red) calc(l + (1 - l) * 0.824) calc(c * 0.135) h);
180
+ --red-200: oklch(from var(--red) calc(l + (1 - l) * 0.683) calc(c * 0.262) h);
181
+ --red-300: oklch(from var(--red) calc(l + (1 - l) * 0.471) calc(c * 0.481) h);
182
+ --red-400: oklch(from var(--red) calc(l + (1 - l) * 0.185) calc(c * 0.806) h);
183
+ --red-500: var(--red);
184
+ --red-600: oklch(from var(--red) calc(l * (1 - 0.094)) calc(c * 1.034) h);
185
+ --red-700: oklch(from var(--red) calc(l * (1 - 0.207)) calc(c * 0.899) h);
186
+ --red-800: oklch(from var(--red) calc(l * (1 - 0.303)) calc(c * 0.747) h);
187
+ --red-900: oklch(from var(--red) calc(l * (1 - 0.378)) calc(c * 0.595) h);
188
+ --red-950: oklch(from var(--red) calc(l * (1 - 0.595)) calc(c * 0.435) h);
189
+
190
+ --orange-50: oklch(from var(--orange) calc(l + (1 - l) * 0.92) calc(c * 0.055) h);
191
+ --orange-100: oklch(from var(--orange) calc(l + (1 - l) * 0.824) calc(c * 0.135) h);
192
+ --orange-200: oklch(from var(--orange) calc(l + (1 - l) * 0.683) calc(c * 0.262) h);
193
+ --orange-300: oklch(from var(--orange) calc(l + (1 - l) * 0.471) calc(c * 0.481) h);
194
+ --orange-400: oklch(from var(--orange) calc(l + (1 - l) * 0.185) calc(c * 0.806) h);
195
+ --orange-500: var(--orange);
196
+ --orange-600: oklch(from var(--orange) calc(l * (1 - 0.094)) calc(c * 1.034) h);
197
+ --orange-700: oklch(from var(--orange) calc(l * (1 - 0.207)) calc(c * 0.899) h);
198
+ --orange-800: oklch(from var(--orange) calc(l * (1 - 0.303)) calc(c * 0.747) h);
199
+ --orange-900: oklch(from var(--orange) calc(l * (1 - 0.378)) calc(c * 0.595) h);
200
+ --orange-950: oklch(from var(--orange) calc(l * (1 - 0.595)) calc(c * 0.435) h);
201
+
202
+ --yellow-50: oklch(from var(--yellow) calc(l + (1 - l) * 0.937) calc(c * 0.141) calc(h + 10));
203
+ --yellow-100: oklch(from var(--yellow) calc(l + (1 - l) * 0.868) calc(c * 0.386) calc(h + 11));
204
+ --yellow-200: oklch(from var(--yellow) calc(l + (1 - l) * 0.732) calc(c * 0.701) calc(h + 10));
205
+ --yellow-300: oklch(from var(--yellow) calc(l + (1 - l) * 0.537) calc(c * 0.989) calc(h + 6));
206
+ --yellow-400: oklch(from var(--yellow) calc(l + (1 - l) * 0.278) calc(c * 1.082) calc(h + 3));
207
+ --yellow-500: var(--yellow);
208
+ --yellow-600: oklch(from var(--yellow) calc(l * (1 - 0.08)) calc(c * 0.95) calc(h - 6));
209
+ --yellow-700: oklch(from var(--yellow) calc(l * (1 - 0.19)) calc(c * 0.8) calc(h - 14));
210
+ --yellow-800: oklch(from var(--yellow) calc(l * (1 - 0.33)) calc(c * 0.65) calc(h - 26));
211
+ --yellow-900: oklch(from var(--yellow) calc(l * (1 - 0.46)) calc(c * 0.52) calc(h - 32));
212
+ --yellow-950: oklch(from var(--yellow) calc(l * (1 - 0.64)) calc(c * 0.359) calc(h - 38));
213
+
214
+ --green-50: oklch(from var(--green) calc(l + (1 - l) * 0.92) calc(c * 0.055) h);
215
+ --green-100: oklch(from var(--green) calc(l + (1 - l) * 0.824) calc(c * 0.135) h);
216
+ --green-200: oklch(from var(--green) calc(l + (1 - l) * 0.683) calc(c * 0.262) h);
217
+ --green-300: oklch(from var(--green) calc(l + (1 - l) * 0.471) calc(c * 0.481) h);
218
+ --green-400: oklch(from var(--green) calc(l + (1 - l) * 0.185) calc(c * 0.806) h);
219
+ --green-500: var(--green);
220
+ --green-600: oklch(from var(--green) calc(l * (1 - 0.094)) calc(c * 1.034) h);
221
+ --green-700: oklch(from var(--green) calc(l * (1 - 0.207)) calc(c * 0.899) h);
222
+ --green-800: oklch(from var(--green) calc(l * (1 - 0.303)) calc(c * 0.747) h);
223
+ --green-900: oklch(from var(--green) calc(l * (1 - 0.378)) calc(c * 0.595) h);
224
+ --green-950: oklch(from var(--green) calc(l * (1 - 0.595)) calc(c * 0.435) h);
225
+
226
+ --cyan-50: oklch(from var(--cyan) calc(l + (1 - l) * 0.92) calc(c * 0.055) h);
227
+ --cyan-100: oklch(from var(--cyan) calc(l + (1 - l) * 0.824) calc(c * 0.135) h);
228
+ --cyan-200: oklch(from var(--cyan) calc(l + (1 - l) * 0.683) calc(c * 0.262) h);
229
+ --cyan-300: oklch(from var(--cyan) calc(l + (1 - l) * 0.471) calc(c * 0.481) h);
230
+ --cyan-400: oklch(from var(--cyan) calc(l + (1 - l) * 0.185) calc(c * 0.806) h);
231
+ --cyan-500: var(--cyan);
232
+ --cyan-600: oklch(from var(--cyan) calc(l * (1 - 0.094)) calc(c * 1.034) h);
233
+ --cyan-700: oklch(from var(--cyan) calc(l * (1 - 0.207)) calc(c * 0.899) h);
234
+ --cyan-800: oklch(from var(--cyan) calc(l * (1 - 0.303)) calc(c * 0.747) h);
235
+ --cyan-900: oklch(from var(--cyan) calc(l * (1 - 0.378)) calc(c * 0.595) h);
236
+ --cyan-950: oklch(from var(--cyan) calc(l * (1 - 0.595)) calc(c * 0.435) h);
237
+
238
+ --blue-50: oklch(from var(--blue) calc(l + (1 - l) * 0.92) calc(c * 0.055) h);
239
+ --blue-100: oklch(from var(--blue) calc(l + (1 - l) * 0.824) calc(c * 0.135) h);
240
+ --blue-200: oklch(from var(--blue) calc(l + (1 - l) * 0.683) calc(c * 0.262) h);
241
+ --blue-300: oklch(from var(--blue) calc(l + (1 - l) * 0.471) calc(c * 0.481) h);
242
+ --blue-400: oklch(from var(--blue) calc(l + (1 - l) * 0.185) calc(c * 0.806) h);
243
+ --blue-500: var(--blue);
244
+ --blue-600: oklch(from var(--blue) calc(l * (1 - 0.094)) calc(c * 1.034) h);
245
+ --blue-700: oklch(from var(--blue) calc(l * (1 - 0.207)) calc(c * 0.899) h);
246
+ --blue-800: oklch(from var(--blue) calc(l * (1 - 0.303)) calc(c * 0.747) h);
247
+ --blue-900: oklch(from var(--blue) calc(l * (1 - 0.378)) calc(c * 0.595) h);
248
+ --blue-950: oklch(from var(--blue) calc(l * (1 - 0.595)) calc(c * 0.435) h);
249
+
250
+ --purple-50: oklch(from var(--purple) calc(l + (1 - l) * 0.92) calc(c * 0.055) h);
251
+ --purple-100: oklch(from var(--purple) calc(l + (1 - l) * 0.824) calc(c * 0.135) h);
252
+ --purple-200: oklch(from var(--purple) calc(l + (1 - l) * 0.683) calc(c * 0.262) h);
253
+ --purple-300: oklch(from var(--purple) calc(l + (1 - l) * 0.471) calc(c * 0.481) h);
254
+ --purple-400: oklch(from var(--purple) calc(l + (1 - l) * 0.185) calc(c * 0.806) h);
255
+ --purple-500: var(--purple);
256
+ --purple-600: oklch(from var(--purple) calc(l * (1 - 0.094)) calc(c * 1.034) h);
257
+ --purple-700: oklch(from var(--purple) calc(l * (1 - 0.207)) calc(c * 0.899) h);
258
+ --purple-800: oklch(from var(--purple) calc(l * (1 - 0.303)) calc(c * 0.747) h);
259
+ --purple-900: oklch(from var(--purple) calc(l * (1 - 0.378)) calc(c * 0.595) h);
260
+ --purple-950: oklch(from var(--purple) calc(l * (1 - 0.595)) calc(c * 0.435) h);
261
+
262
+ --pink-50: oklch(from var(--pink) calc(l + (1 - l) * 0.92) calc(c * 0.055) h);
263
+ --pink-100: oklch(from var(--pink) calc(l + (1 - l) * 0.824) calc(c * 0.135) h);
264
+ --pink-200: oklch(from var(--pink) calc(l + (1 - l) * 0.683) calc(c * 0.262) h);
265
+ --pink-300: oklch(from var(--pink) calc(l + (1 - l) * 0.471) calc(c * 0.481) h);
266
+ --pink-400: oklch(from var(--pink) calc(l + (1 - l) * 0.185) calc(c * 0.806) h);
267
+ --pink-500: var(--pink);
268
+ --pink-600: oklch(from var(--pink) calc(l * (1 - 0.094)) calc(c * 1.034) h);
269
+ --pink-700: oklch(from var(--pink) calc(l * (1 - 0.207)) calc(c * 0.899) h);
270
+ --pink-800: oklch(from var(--pink) calc(l * (1 - 0.303)) calc(c * 0.747) h);
271
+ --pink-900: oklch(from var(--pink) calc(l * (1 - 0.378)) calc(c * 0.595) h);
272
+ --pink-950: oklch(from var(--pink) calc(l * (1 - 0.595)) calc(c * 0.435) h);
273
+
274
+ --accent-50: oklch(from var(--accent) calc(l + (1 - l) * 0.92) calc(c * 0.055) h);
275
+ --accent-100: oklch(from var(--accent) calc(l + (1 - l) * 0.824) calc(c * 0.135) h);
276
+ --accent-200: oklch(from var(--accent) calc(l + (1 - l) * 0.683) calc(c * 0.262) h);
277
+ --accent-300: oklch(from var(--accent) calc(l + (1 - l) * 0.471) calc(c * 0.481) h);
278
+ --accent-400: oklch(from var(--accent) calc(l + (1 - l) * 0.185) calc(c * 0.806) h);
279
+ --accent-500: var(--accent);
280
+ --accent-600: oklch(from var(--accent) calc(l * (1 - 0.094)) calc(c * 1.034) h);
281
+ --accent-700: oklch(from var(--accent) calc(l * (1 - 0.207)) calc(c * 0.899) h);
282
+ --accent-800: oklch(from var(--accent) calc(l * (1 - 0.303)) calc(c * 0.747) h);
283
+ --accent-900: oklch(from var(--accent) calc(l * (1 - 0.378)) calc(c * 0.595) h);
284
+ --accent-950: oklch(from var(--accent) calc(l * (1 - 0.595)) calc(c * 0.435) h);
285
+
286
+ /* Alpha scales — each color slot at a ladder of opacities, compositing on
287
+ whatever is beneath: restate the slot and its alphas follow. Used for
288
+ tinted backgrounds and translucent content. */
289
+ --neutral-alpha-5: oklch(from var(--neutral) l c h / var(--alpha-5)); --neutral-alpha-10: oklch(from var(--neutral) l c h / var(--alpha-10)); --neutral-alpha-15: oklch(from var(--neutral) l c h / var(--alpha-15));
290
+ --neutral-alpha-25: oklch(from var(--neutral) l c h / var(--alpha-25)); --neutral-alpha-50: oklch(from var(--neutral) l c h / var(--alpha-50)); --neutral-alpha-75: oklch(from var(--neutral) l c h / var(--alpha-75));
291
+ --red-alpha-5: oklch(from var(--red) l c h / var(--alpha-5)); --red-alpha-10: oklch(from var(--red) l c h / var(--alpha-10)); --red-alpha-15: oklch(from var(--red) l c h / var(--alpha-15));
292
+ --red-alpha-25: oklch(from var(--red) l c h / var(--alpha-25)); --red-alpha-50: oklch(from var(--red) l c h / var(--alpha-50)); --red-alpha-75: oklch(from var(--red) l c h / var(--alpha-75));
293
+ --orange-alpha-5: oklch(from var(--orange) l c h / var(--alpha-5)); --orange-alpha-10: oklch(from var(--orange) l c h / var(--alpha-10)); --orange-alpha-15: oklch(from var(--orange) l c h / var(--alpha-15));
294
+ --orange-alpha-25: oklch(from var(--orange) l c h / var(--alpha-25)); --orange-alpha-50: oklch(from var(--orange) l c h / var(--alpha-50)); --orange-alpha-75: oklch(from var(--orange) l c h / var(--alpha-75));
295
+ --yellow-alpha-5: oklch(from var(--yellow) l c h / var(--alpha-5)); --yellow-alpha-10: oklch(from var(--yellow) l c h / var(--alpha-10)); --yellow-alpha-15: oklch(from var(--yellow) l c h / var(--alpha-15));
296
+ --yellow-alpha-25: oklch(from var(--yellow) l c h / var(--alpha-25)); --yellow-alpha-50: oklch(from var(--yellow) l c h / var(--alpha-50)); --yellow-alpha-75: oklch(from var(--yellow) l c h / var(--alpha-75));
297
+ --green-alpha-5: oklch(from var(--green) l c h / var(--alpha-5)); --green-alpha-10: oklch(from var(--green) l c h / var(--alpha-10)); --green-alpha-15: oklch(from var(--green) l c h / var(--alpha-15));
298
+ --green-alpha-25: oklch(from var(--green) l c h / var(--alpha-25)); --green-alpha-50: oklch(from var(--green) l c h / var(--alpha-50)); --green-alpha-75: oklch(from var(--green) l c h / var(--alpha-75));
299
+ --cyan-alpha-5: oklch(from var(--cyan) l c h / var(--alpha-5)); --cyan-alpha-10: oklch(from var(--cyan) l c h / var(--alpha-10)); --cyan-alpha-15: oklch(from var(--cyan) l c h / var(--alpha-15));
300
+ --cyan-alpha-25: oklch(from var(--cyan) l c h / var(--alpha-25)); --cyan-alpha-50: oklch(from var(--cyan) l c h / var(--alpha-50)); --cyan-alpha-75: oklch(from var(--cyan) l c h / var(--alpha-75));
301
+ --blue-alpha-5: oklch(from var(--blue) l c h / var(--alpha-5)); --blue-alpha-10: oklch(from var(--blue) l c h / var(--alpha-10)); --blue-alpha-15: oklch(from var(--blue) l c h / var(--alpha-15));
302
+ --blue-alpha-25: oklch(from var(--blue) l c h / var(--alpha-25)); --blue-alpha-50: oklch(from var(--blue) l c h / var(--alpha-50)); --blue-alpha-75: oklch(from var(--blue) l c h / var(--alpha-75));
303
+ --purple-alpha-5: oklch(from var(--purple) l c h / var(--alpha-5)); --purple-alpha-10: oklch(from var(--purple) l c h / var(--alpha-10)); --purple-alpha-15: oklch(from var(--purple) l c h / var(--alpha-15));
304
+ --purple-alpha-25: oklch(from var(--purple) l c h / var(--alpha-25)); --purple-alpha-50: oklch(from var(--purple) l c h / var(--alpha-50)); --purple-alpha-75: oklch(from var(--purple) l c h / var(--alpha-75));
305
+ --pink-alpha-5: oklch(from var(--pink) l c h / var(--alpha-5)); --pink-alpha-10: oklch(from var(--pink) l c h / var(--alpha-10)); --pink-alpha-15: oklch(from var(--pink) l c h / var(--alpha-15));
306
+ --pink-alpha-25: oklch(from var(--pink) l c h / var(--alpha-25)); --pink-alpha-50: oklch(from var(--pink) l c h / var(--alpha-50)); --pink-alpha-75: oklch(from var(--pink) l c h / var(--alpha-75));
307
+ --accent-alpha-5: oklch(from var(--accent) l c h / var(--alpha-5)); --accent-alpha-10: oklch(from var(--accent) l c h / var(--alpha-10)); --accent-alpha-15: oklch(from var(--accent) l c h / var(--alpha-15));
308
+ --accent-alpha-25: oklch(from var(--accent) l c h / var(--alpha-25)); --accent-alpha-50: oklch(from var(--accent) l c h / var(--alpha-50)); --accent-alpha-75: oklch(from var(--accent) l c h / var(--alpha-75));
309
+
310
+ /* The alpha ladder the alpha scales read — nominal percent, values tunable
311
+ within a few points of their names. */
312
+ --alpha-3: 3%; --alpha-5: 5%; --alpha-10: 10%; --alpha-15: 15%; --alpha-20: 20%;
313
+ --alpha-25: 25%; --alpha-40: 40%; --alpha-50: 50%; --alpha-75: 75%;
314
+
315
+ /* Active strength and hover position are independent. Hover moves this
316
+ fraction of the way from resting to the resolved active color. The
317
+ default is halfway in both appearances. */
318
+ --hover-mix: 50%;
319
+ --alpha-active: 8%;
320
+
321
+ /* Ghost controls use their text pigment at active strength. Hover blends
322
+ transparent with that resolved endpoint, including theme overrides. */
323
+ --tint-hover: color-mix(in oklch, transparent, var(--tint-active) var(--hover-mix));
324
+ --tint-active: oklch(from currentColor l c h / var(--alpha-active));
325
+
326
+ /* Stencil icons: a drawing from the glyph vocabulary, packaged as a mask
327
+ for the places a rule must draw it — a pseudo-element can hold no
328
+ element, so the checklist's done mark and the select's selected mark
329
+ paint through this stencil instead of composing tv-icon. */
330
+ --icon-check: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12"><path d="M10.2 3.1 5.1 8.2 2.4 5.5" fill="none" stroke="black" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>');
331
+
332
+ /* Neutral roles — the interface's jobs, pointed at the material. Sheets
333
+ read roles and scales, never a value of their own. */
334
+ /* Pure white, stated: the surface is a frame for user content, and
335
+ white-backgrounded artifacts and images must composite seamlessly. */
336
+ --color-surface: white;
337
+ --color-surface-muted: var(--neutral-100);
338
+ --color-text: var(--neutral-900);
339
+ --color-text-muted: var(--neutral-500);
340
+ /* Reversed type — the text knocked out of a solid fill. One token: fills
341
+ share the readable band by construction (meanings sit at fill weight),
342
+ and a variant whose fill is too light for reversed type wears
343
+ --color-text instead. */
344
+ --color-text-reversed: white;
345
+ /* Borders are tints of the text role, not stated steps: a translucent
346
+ edge always separates from whatever ground it sits on — an opaque
347
+ step vanishes the day a region paints that same step. Dark appearance
348
+ uses stronger tints to keep the edges visible. */
349
+ --color-border: oklch(from var(--color-text) l c h / 12%);
350
+ /* Translucent surface fills; backdrop blur is applied by the component. */
351
+ --tint-surface: oklch(from var(--color-surface) l c h / var(--alpha-50));
352
+ --tint-surface-muted: oklch(from var(--color-surface-muted) l c h / var(--alpha-50));
353
+ /* A control is a small surface, lifted by its border. */
354
+ --control-background: var(--color-surface);
355
+ /* Situations — danger, alert, success, primary. Each is one pointer at
356
+ the hue's 500; its tints and state fills derive from it, so repointing
357
+ the situation moves everything that wears it. */
358
+ --color-danger: var(--red-500);
359
+ --tint-danger: oklch(from var(--color-danger) l c h / var(--alpha-10));
360
+ --tint-danger-hover: color-mix(in oklch, transparent, var(--tint-danger-active) var(--hover-mix));
361
+ --tint-danger-active: oklch(from var(--color-danger) l c h / var(--alpha-active));
362
+
363
+ --color-alert: var(--yellow-500);
364
+ --tint-alert: oklch(from var(--color-alert) l c h / var(--alpha-10));
365
+ --tint-alert-hover: color-mix(in oklch, transparent, var(--tint-alert-active) var(--hover-mix));
366
+ --tint-alert-active: oklch(from var(--color-alert) l c h / var(--alpha-active));
367
+
368
+ --color-success: var(--green-500);
369
+ --tint-success: oklch(from var(--color-success) l c h / var(--alpha-10));
370
+
371
+ /* The primary action — the accent worn as a situation: the default
372
+ button, the current sidebar row. Selection wears it too; a selected
373
+ role returns the day selection and primary want different colors. */
374
+ --color-primary: var(--accent-500);
375
+ --tint-primary: oklch(from var(--color-primary) l c h / var(--alpha-10));
376
+ --tint-primary-hover: color-mix(in oklch, transparent, var(--tint-primary-active) var(--hover-mix));
377
+ --tint-primary-active: oklch(from var(--color-primary) l c h / var(--alpha-active));
378
+ /* The text on the primary fill, computed against it: dark ink above the
379
+ contrast flip, near-white below — so any accent, light gold included,
380
+ keeps a legible label wherever the fill appears. */
381
+ --color-primary-text: oklch(from var(--color-primary) clamp(0.2, (var(--contrast-flip) - l) * 1e6, 0.98) 0 h);
382
+ /* The keyboard's ring: one stroke wherever focus lands — width and
383
+ pigment here, the offset each control's own. */
384
+ --outline-focus: 2px solid var(--color-primary);
385
+
386
+ /* Filled active colors move toward black or white according to state-flip.
387
+ Hover follows the resolved active endpoint. Ordinary buttons consume
388
+ only resting and active fills; their hover appearance stays unchanged.
389
+ Derived states accept colors. Supply explicit states for gradients. */
390
+ --control-background-hover: color-mix(in oklch, var(--control-background), var(--control-background-active) var(--hover-mix));
391
+ --control-background-active: color-mix(in oklch, var(--control-background), oklch(from var(--control-background) clamp(0, (var(--state-flip) - l) * 1e6, 1) 0 h) var(--alpha-active));
392
+ --color-primary-hover: color-mix(in oklch, var(--color-primary), var(--color-primary-active) var(--hover-mix));
393
+ --color-primary-active: color-mix(in oklch, var(--color-primary), oklch(from var(--color-primary) clamp(0, (var(--state-flip) - l) * 1e6, 1) 0 h) var(--alpha-active));
394
+ --color-danger-hover: color-mix(in oklch, var(--color-danger), var(--color-danger-active) var(--hover-mix));
395
+ --color-danger-active: color-mix(in oklch, var(--color-danger), oklch(from var(--color-danger) clamp(0, (var(--state-flip) - l) * 1e6, 1) 0 h) var(--alpha-active));
396
+ --color-alert-hover: color-mix(in oklch, var(--color-alert), var(--color-alert-active) var(--hover-mix));
397
+ --color-alert-active: color-mix(in oklch, var(--color-alert), oklch(from var(--color-alert) clamp(0, (var(--state-flip) - l) * 1e6, 1) 0 h) var(--alpha-active));
398
+
399
+ /* A link follows its surrounding text unless a theme gives it an accent.
400
+ currentColor, not inherit: it means the ambient ink in any property it
401
+ lands in, and CSS-wide keywords substitute brittly through var(). */
402
+ --color-link: currentColor;
403
+ /* The dialog backdrop: near-black at the overlay opacity, dimming the
404
+ page; deriving from the neutral keeps the theme's undertone. */
405
+ --color-overlay: oklch(from var(--neutral-950) l c h / var(--alpha-40));
406
+
407
+ /* Two thresholds read a fill's own lightness and pick between two
408
+ outcomes. --state-flip steers the fill's motion in its states: a fill
409
+ lighter than it darkens on hover and press, a darker one lightens —
410
+ a genuinely dark fill has nowhere darker to go. --contrast-flip
411
+ steers the ink on the fill: a fill lighter than it wears dark text,
412
+ a darker one wears reversed type. Different questions flip at
413
+ different points — a red of 0.6 lightness wants reversed text yet
414
+ still darkens on press. */
415
+ --state-flip: 0.35;
416
+ --contrast-flip: 0.66;
417
+
418
+ color-scheme: light;
419
+ }
420
+
421
+ /* The dark table changes the role pointers. Scales are not restated as
422
+ scales; the hue slots take slightly lighter values (saturated mids vibrate
423
+ on near-black), and every derived step follows. The neutral slot does not
424
+ move — the roles flip which steps they take. */
425
+ :where([data-theme="dark"]) {
426
+ --alpha-active: 12%;
427
+ --red: oklch(0.704 0.191 22.216);
428
+ --orange: oklch(0.75 0.183 55.934);
429
+ --yellow: oklch(0.879 0.169 91.5);
430
+ --green: oklch(0.792 0.209 151.711);
431
+ --cyan: oklch(0.789 0.154 211.53);
432
+ --blue: oklch(0.707 0.165 254.624);
433
+ --purple: oklch(0.714 0.203 305.504);
434
+ --pink: oklch(0.718 0.202 349.761);
435
+
436
+ --color-surface: var(--neutral-900);
437
+ --color-surface-muted: var(--neutral-800);
438
+ --color-text: var(--neutral-100);
439
+ --color-text-muted: var(--neutral-400);
440
+ --color-border: oklch(from var(--color-text) l c h / 18%);
441
+
442
+ color-scheme: dark;
443
+ }
444
+
445
+ /* Foundation tokens — text sizes and line heights */
446
+ :where(:root) {
447
+ /* Controls follow the standard UI text size by default. */
448
+ --control-font-size: var(--text-md);
449
+
450
+ /* Type — one seed; each size a stated multiple, written as its tuned pixel
451
+ over the default base: retune --text-base and every size follows (at the
452
+ root — the var chain resolves there, not per subtree); read the fraction
453
+ and you see the pixels it lands on at 14. */
454
+ --text-base: 14px;
455
+ --text-sm: round(calc(var(--text-base) * 12 / 14), 1px);
456
+ --text-md: var(--text-base);
457
+ --text-lg: round(calc(var(--text-base) * 16 / 14), 1px);
458
+ --text-xl: round(calc(var(--text-base) * 18 / 14), 1px);
459
+ --text-2xl: round(calc(var(--text-base) * 20 / 14), 1px);
460
+ --text-3xl: round(calc(var(--text-base) * 24 / 14), 1px);
461
+ --text-4xl: round(calc(var(--text-base) * 30 / 14), 1px);
462
+
463
+ /* The control lines: the line-height a control's label uses, and the one
464
+ input its height derives from — the line, plus the space-2 padding step
465
+ top and bottom, plus a 1px border each side, gives every control the
466
+ same height: 18 + 4 + 2 = 24px (14 + 4 + 2 = 20px at the small step).
467
+ Stated in pixels, not a ratio: a control's cell is tighter than prose,
468
+ immune to the leading around it, and lands on even pixels in any font. */
469
+ --line-control: 18px;
470
+ --line-control-sm: 14px;
471
+ }
472
+
473
+ /* Foundation tokens — spacing and corners */
474
+ :where(:root) {
475
+ /* Ordinary buttons and text fields share control padding. */
476
+ --control-padding: var(--space-2) var(--space-16);
477
+
478
+ /* Spacing — the thirteen legal distances. The ladder is a constraint, not a
479
+ theming surface: names state their fixed pixel values, and an off-ladder
480
+ length in a sheet is a visible smell. A density mode, if ever real,
481
+ swaps the ladder wholesale rather than lying under these names. */
482
+ --space-2: 2px;
483
+ --space-3: 3px;
484
+ --space-4: 4px;
485
+ --space-6: 6px;
486
+ --space-8: 8px;
487
+ --space-10: 10px;
488
+ --space-12: 12px;
489
+ --space-16: 16px;
490
+ --space-20: 20px;
491
+ --space-24: 24px;
492
+ --space-32: 32px;
493
+ --space-48: 48px;
494
+ --space-64: 64px;
495
+
496
+ /* Radius roles — jobs, themable. A numbered ladder would be value-names a
497
+ theme cannot restate without lying. */
498
+ --control-radius: 6px; /* buttons, inputs, menu items, small insets */
499
+ --radius-pill: 9999px; /* fully rounded ends at any element size */
500
+
501
+ /* Shared panel corners. */
502
+ --panel-radius: 8px;
503
+
504
+ /* The clearance a placed panel keeps on every side — off the trigger and
505
+ off every window edge ([[ui/foundation/popover/index.md]], Placement).
506
+ The element reads it from computed style, so a theme override reaches
507
+ the placement algorithm. Foundation, not app chrome: the popover family
508
+ ships to artifact documents, so its clearance must travel with it. */
509
+ --popover-distance: var(--space-4);
510
+ }
511
+
512
+ /* Foundation tokens — shadows */
513
+ :where(:root) {
514
+ /* Reusable elevations, from a close contact to a broad floating shadow. */
515
+ --shadow-sm: 0 1px 4px oklch(from var(--neutral-950) l c h / var(--alpha-15));
516
+ --shadow-md: 0 4px 12px oklch(from var(--neutral-950) l c h / var(--alpha-15));
517
+ --shadow-lg:
518
+ 0 8px 24px oklch(from var(--neutral-950) l c h / var(--alpha-10)),
519
+ 0 2px 8px oklch(from var(--neutral-950) l c h / var(--alpha-5));
520
+
521
+ /* Broad window elevation for artifact frames. */
522
+ --shadow-xl:
523
+ 0 24px 48px oklch(from var(--neutral-950) l c h / var(--alpha-15)),
524
+ 0 4px 12px oklch(from var(--neutral-950) l c h / 7.5%);
525
+
526
+ /* Popovers, including menus and selects, sit closer than dialogs. */
527
+ --popover-shadow: var(--shadow-md);
528
+ --dialog-shadow: var(--shadow-lg);
529
+ }
530
+
531
+ /* Dark surfaces need stronger shadows with the same neutral pigment. */
532
+ :where([data-theme="dark"]) {
533
+ --shadow-sm: 0 1px 4px oklch(from var(--neutral-950) l c h / var(--alpha-40));
534
+ --shadow-md: 0 4px 12px oklch(from var(--neutral-950) l c h / var(--alpha-40));
535
+ --shadow-lg:
536
+ 0 8px 24px oklch(from var(--neutral-950) l c h / var(--alpha-40)),
537
+ 0 2px 8px oklch(from var(--neutral-950) l c h / var(--alpha-25));
538
+ --shadow-xl:
539
+ 0 24px 48px oklch(from var(--neutral-950) l c h / 30%),
540
+ 0 4px 12px oklch(from var(--neutral-950) l c h / 18.75%);
541
+ }
542
+
543
+ /* Foundation tokens — layers */
544
+ /* The app-level paint order ([[ui/foundation/index.md]], Layers): every
545
+ z-index either places an element on one of these or lifts above siblings
546
+ with a calculation on one. Zero specificity like every token table — a
547
+ plain `:root` rule from a theme beats it. */
548
+ :where(:root) {
549
+ /* The base plane the document lays out. Nothing is placed on it; lifts on
550
+ the base plane calculate from it. */
551
+ --layer-ground: 0;
552
+ /* Floating panels opened from a surface: menus, the top-bar popovers. */
553
+ --layer-panel: 100;
554
+ /* The interruption: the dialog overlay and its backdrop. */
555
+ --layer-overlay: 200;
556
+ }
557
+
558
+ /* Application tokens — grouped by component */
559
+ /* Application vocabulary — the component tokens of the application chrome,
560
+ as distinct from the materials the other token sheets state. The
561
+ application document loads this sheet on top of the foundation; an
562
+ artifact document never receives it ([[ui/foundation/index.md]],
563
+ Delivery). */
564
+ /* Resolve relative image URLs where the theme declares them, before the
565
+ application stylesheet consumes the value through --app-wallpaper. */
566
+ @property --app-wallpaper-image {
567
+ syntax: "<url> | none";
568
+ inherits: true;
569
+ initial-value: none;
570
+ }
571
+
572
+ :where(:root) {
573
+ /* Sidebar shell. */
574
+ --sidebar-background: var(--color-surface-muted);
575
+ --sidebar-text-color: var(--color-text);
576
+ --sidebar-shadow: none;
577
+ --sidebar-border-color: var(--surface-border-color);
578
+ --sidebar-border-width: 1px;
579
+ --sidebar-border: var(--sidebar-border-width) solid var(--sidebar-border-color);
580
+ --sidebar-outline: var(--surface-outline);
581
+ --sidebar-heading-text-color: var(--color-text-muted);
582
+ --sidebar-heading-font-size: var(--text-md);
583
+ --sidebar-width: 260px;
584
+
585
+ /* Channel rows and selection. */
586
+ --channel-background: transparent;
587
+ --channel-text-color: var(--sidebar-text-color);
588
+ --channel-background-hover: color-mix(in oklch, var(--channel-background), var(--channel-background-active) var(--hover-mix));
589
+ --channel-background-active: color-mix(in oklch, var(--channel-background), oklch(from currentColor l c h / 1) var(--alpha-active));
590
+ --channel-background-selected: var(--color-primary);
591
+ --channel-text-color-selected: var(--color-primary-text);
592
+ --channel-radius: var(--control-radius);
593
+
594
+ /* Tab labels. */
595
+ --tab-text-color: oklch(from var(--navbar-text-color) l c h / var(--alpha-75));
596
+ --tab-text-color-selected: var(--color-text);
597
+ --tab-font-size: var(--text-md);
598
+
599
+ /* Artifact frame and title bar. */
600
+ --artifact-frame-shadow: var(--shadow-xl);
601
+ --frame-background: var(--color-surface);
602
+ --frame-border-color: var(--surface-border-color);
603
+ --frame-border-width: 1px;
604
+ --frame-border: var(--frame-border-width) solid var(--frame-border-color);
605
+ --frame-outline: var(--surface-outline);
606
+ --frame-titlebar-background: var(--color-surface-muted);
607
+ --frame-titlebar-text-color: var(--color-text);
608
+ --frame-titlebar-border-color: transparent;
609
+ --frame-titlebar-border-width: 1px;
610
+ --frame-titlebar-border: var(--frame-titlebar-border-width) solid var(--frame-titlebar-border-color);
611
+ --frame-titlebar-padding: var(--space-4) var(--space-10) var(--space-6);
612
+ --frame-titlebar-divider-color: var(--color-border);
613
+
614
+ /* Controls displayed over wallpaper. Blur is a backdrop radius: content
615
+ stays sharp. Background and border default to transparent. */
616
+ --wallpaper-overlay-text-color: var(--color-text);
617
+ --wallpaper-overlay-background: transparent;
618
+ /* Wallpaper controls retain the ghost text-color reaction by default.
619
+ Themes can set the complete active background; hover follows it. */
620
+ --wallpaper-overlay-background-hover: color-mix(in oklch, var(--wallpaper-overlay-background), var(--wallpaper-overlay-background-active) var(--hover-mix));
621
+ --wallpaper-overlay-background-active: color-mix(in oklch, var(--wallpaper-overlay-background), oklch(from currentColor l c h / 1) var(--alpha-active));
622
+ --wallpaper-overlay-border: 1px solid transparent;
623
+ --wallpaper-overlay-outline: none;
624
+ --wallpaper-overlay-blur: 0px;
625
+
626
+ /* Navbar and empty stage. */
627
+ --navbar-background: transparent;
628
+ --navbar-text-color: var(--wallpaper-overlay-text-color);
629
+ --navbar-border-color: transparent;
630
+ --navbar-border-width: 1px;
631
+ --navbar-border: var(--navbar-border-width) solid var(--navbar-border-color);
632
+
633
+ /* The ground behind the stage: one background value, composed from an
634
+ optional image URL over the light or dark neutral ground. The image token
635
+ accepts url(...) or none; use the full wallpaper token for gradients or
636
+ other complete background treatments. */
637
+ --app-wallpaper-image: none;
638
+ --app-wallpaper: var(--app-wallpaper-image) center / cover no-repeat, var(--neutral-200);
639
+
640
+ /* The stage's dials ([[ui/app/stage/stage.frame]] consumes them): the gap
641
+ between pages, the inward inset from the stage edges, a background page's
642
+ presence, and the armed snap's outline. Declared here, at the root, so
643
+ a theme's root override wins — a declaration on the component would
644
+ shadow it. */
645
+ --page-gap: var(--space-24);
646
+ --page-inset: var(--space-12);
647
+ --page-inactive-blur: 0px;
648
+ --page-inactive-backdrop-blur: var(--wallpaper-overlay-blur);
649
+ --page-inactive-opacity: 0.55;
650
+ --page-inactive-scale: 0.94;
651
+ --page-snap-preview-border-color: oklch(from currentColor l c h / var(--alpha-25));
652
+ --page-snap-preview-background: transparent;
653
+ --page-snap-preview-border-width: 3px;
654
+ --page-snap-preview-border: var(--page-snap-preview-border-width) solid var(--page-snap-preview-border-color);
655
+ --page-snap-preview-radius: 12px;
656
+
657
+ /* The tab pill's own dials ([[ui/app/tab-strip/tab.frame]] and the strip
658
+ consume them). */
659
+ --tab-radius: var(--control-radius);
660
+ --tab-gap: var(--space-4); /* between tabs in the strip */
661
+ --tab-border: var(--wallpaper-overlay-border);
662
+ /* Transparent at rest; pointer states use the shared control tints. */
663
+ --tab-background: var(--wallpaper-overlay-background);
664
+ --tab-background-hover: color-mix(in oklch, var(--tab-background), var(--tab-background-active) var(--hover-mix));
665
+ --tab-background-active: var(--wallpaper-overlay-background-active);
666
+ --tab-background-selected: var(--color-surface-muted);
667
+ /* Reserve the edge in every state so theme borders do not shift tabs. */
668
+ --tab-border-selected: 1px solid var(--surface-border-color);
669
+ --tab-outline: var(--wallpaper-overlay-outline);
670
+ --tab-outline-selected: var(--surface-outline);
671
+
672
+ /* The drag placeholder's ghost fill — the "a carried thing could land
673
+ here" mark the tab strip and the sidebar share
674
+ ([[ui/app/tab-strip/tab-placeholder.frame]],
675
+ [[ui/app/sidebar/channel-placeholder.frame]]). */
676
+ --drag-placeholder-background: oklch(from var(--color-text) l c h / var(--alpha-10));
677
+
678
+ /* Dragged items lift above the smaller action pill. */
679
+ --drag-item-shadow: var(--shadow-md);
680
+ --drag-action-shadow: var(--shadow-sm);
681
+
682
+
683
+ /* The air between the navbar and the stage ([[ui/app/styles.css]] consumes
684
+ it for the bar's seat and the stage's top padding). */
685
+ --navbar-gap: var(--space-8);
686
+
687
+ /* The artifact frame's radius ([[ui/app/artifact-frame/artifact-frame.frame]]);
688
+ artifact documents also apply it by reference
689
+ ([[ui/app/artifact-frame/index.md#^af-document-corners]]). */
690
+ --frame-radius: 12px;
691
+ }
692
+
693
+ :where([data-theme="dark"]) {
694
+ --app-wallpaper: var(--app-wallpaper-image) center / cover no-repeat, var(--neutral-950);
695
+ }
696
+ ```
697
+
698
+ ## Application structure for this release
699
+
700
+ This reference describes the application structure shipped with the matching Television release. Selectors may change between releases. The same theme stylesheet loads in the app and supported artifacts: prefix app-only rules with `:root[data-television-document="app"]` to avoid matching similarly named artifact elements. Leave rules intended for both documents unprefixed. The outlines omit text, repeated entries, and runtime pairing IDs where those do not affect styling. The application fills trigger references and manages interactive state; a theme styles those states without changing them.
701
+
702
+ An artifact frame belongs to the application document. The document inside its iframe is separate: an app selector cannot reach across that boundary. The error-page section below describes standalone artifact documents and therefore does not use the app root prefix.
703
+
704
+ ### Shell and theme surfaces
705
+
706
+ ```html
707
+ <div id="app" tabindex="-1">
708
+ <aside class="app-sidebar"><nav class="sidebar">…</nav></aside>
709
+ <main class="app-main">
710
+ <header class="top-bar">…</header>
711
+ <section class="stage">…</section>
712
+ </main>
713
+ </div>
714
+ <div id="foreground-overlay" inert aria-hidden="true"></div>
715
+ ```
716
+
717
+ `#app` owns the application layout and suppresses ordinary text selection in its contents; readable regions can explicitly restore selection. `.app-sidebar` holds the fixed-width channel list; `.app-main` holds the wallpaper, navbar and stage. `.app-main > .top-bar` supplies the inset shared with the stage. `#app:focus` suppresses a ring on the container itself. The wallpaper region establishes a backdrop boundary so overlay blur samples the wallpaper rather than adjacent content, while fixed popovers retain viewport positioning.
718
+
719
+ For the optional iframe surfaces and protected overlay stacking, see [Theme effects and scripts](#theme-effects-and-scripts).
720
+
721
+ ### Channel sidebar
722
+
723
+ ```html
724
+ <nav class="sidebar">
725
+ <header class="sidebar-titlebar">
726
+ <button class="channel-create" variant="ghost" icon aria-label="New channel" title="New channel">…</button>
727
+ </header>
728
+ <div class="sidebar-body" role="listbox" aria-label="Channels">
729
+ <div class="channel-group" role="group" aria-labelledby="channel-group-pinned">
730
+ <div class="channel-group-label" id="channel-group-pinned">Pinned</div>
731
+ <div class="channel-row">
732
+ <div class="channel" role="option" aria-selected="true" tabindex="-1">…</div>
733
+ <button class="channel-menu-trigger" icon variant="ghost" size="sm" tabindex="-1" aria-hidden="true">…</button>
734
+ </div>
735
+ </div>
736
+ <!-- The Recent group uses channel-group-unpinned. -->
737
+ </div>
738
+ </nav>
739
+ ```
740
+
741
+ `.sidebar` paints its light inner edge over its own background, alongside its optional soft shadow. The app shell paints the adjoining dark line as the left border of `.app-main`, so it blends over the wallpaper without a raised layer. `.sidebar-titlebar` reserves the window controls and drag area. `.sidebar-body` scrolls both groups together; `.sidebar:has(.sidebar-body[continues-start]) .sidebar-titlebar` draws the top boundary while content extends above the viewport. `.channel-group + .channel-group` separates the groups, and `.channel-group-label` styles their labels. `.channel-create` sits at the right of the titlebar and is excluded from window dragging.
742
+
743
+ The channel name and its menu trigger are siblings within the row. `.channel` owns the name box; `.channel[aria-selected="true"]` paints the selection. The selected row changes `.channel-menu-trigger` text color through `.channel-row:has(> .channel[aria-selected="true"])`. Shared foundation tints follow that color for hover, press and expanded states. A hover or an expanded trigger preserves the unselected row tint through `:where(.channel-row:hover, .channel-row:has(.channel-menu-trigger[aria-expanded="true"])) .channel:not([aria-selected="true"])`. Trigger visibility follows row hover, `:focus-visible`, selected state, and expanded state. Use the expanded trigger state when styling an open menu subject. Panels remain in their authored DOM position when opened.
744
+
745
+ Renaming replaces the name and menu trigger with `input.channel-rename` and `button.channel-rename-commit`. The field uses shared native-input styling; these selectors retain its placement and the commit treatment in the same row seat. `.channel-row:has(.channel-rename)` keeps its focus ring visible. A dragged row carries `.channel-row.dragged`; an unpinning row also carries `.unpinning` and contains `.channel-drag-action` with a `tv-icon`. `.channel-row.dragged .channel` removes the spare trigger inset. `.channel-drag-action tv-icon` sizes the action glyph; `.channel-placeholder` marks the drop slot.
746
+
747
+ Row menus use `tv-menu`, `tv-menu-item`, `hr`, and `tv-menu-item[intent="danger"]`. These shared elements supply menu styling and item states. Deletion uses the shared dialog outline below.
748
+
749
+ ### Navbar and tabs
750
+
751
+ ```html
752
+ <header class="top-bar">
753
+ <div class="tab-strip" role="tablist">
754
+ <div class="tab" role="tab" aria-selected="true" tabindex="0">
755
+ <span class="tab-label">…</span>
756
+ </div>
757
+ </div>
758
+ <div class="top-bar-controls">…</div>
759
+ </header>
760
+ ```
761
+
762
+ `.top-bar` centers `.top-bar > .tab-strip` while tabs fit and keeps `.top-bar-controls` at the trailing edge. With `.tab-strip[data-overflow]`, the strip extends to the left wallpaper boundary and takes the space up to the fixed controls. Both child bands exclude native window dragging; empty bar ground remains available for it.
763
+
764
+ `.top-bar-controls > button:not([intent])` follows the navbar text token. Ordinary ghost buttons share the wallpaper-overlay background, border and blur; hover and pressed/expanded states replace the complete background. Buttons with an intent retain their semantic colours.
765
+
766
+ `.tab-strip` scrolls horizontally. Only the right edge fades while more tabs remain to the right. `.tab[data-item-edge-fade]` carries a mask on the individual tab, preserving its backdrop blur; the strip has no mask. `.tab` owns the pill; `.tab-label` truncates its text. `.tab[data-compression="hugging"]` retains intrinsic width, while `.tab[data-compression="capped"]` permits compression to the floor. `.tab:hover:not([aria-selected="true"])` and `.tab:active:not([aria-selected="true"])` style unselected interaction; `.tab[aria-selected="true"]` paints the current page cue. `.tab.dragged` raises the carried tab, and `span.tab-placeholder[aria-hidden="true"]` occupies its drop slot. Nonselected tabs carry `aria-selected="false"` and `tabindex="-1"`.
767
+
768
+ ### Stage and pages
769
+
770
+ ```html
771
+ <section class="stage">
772
+ <div class="filmstrip">
773
+ <div class="filmstrip-inner">
774
+ <div class="page" selected><div class="artifact-frame">…</div></div>
775
+ <div class="page"><div class="artifact-frame">…</div></div>
776
+ </div>
777
+ </div>
778
+ </section>
779
+ ```
780
+
781
+ `.stage` provides the clipping and size-container boundary. `body > .stage` fills a standalone stage; the app composition uses the flex region instead. `.filmstrip` is the viewport and `.filmstrip-inner` arranges pages. `.filmstrip-inner::before` and `::after` provide the end room needed for centering. `.page` receives its stored dimensions, and `.page[full-screen]` takes the available page box. `.page > .artifact-frame` fills that width.
782
+
783
+ `.page:not([selected])` scales the background page and blurs the wallpaper behind it; its direct `.artifact-frame` child controls content opacity and content blur separately; `.page:has(~ .page[selected])` and `.page[selected] ~ .page` set the corresponding transform origins. `.page[selected]` keeps the selected page above neighbors during reordering. `.page:not([selected]) .artifact-frame` takes no pointer input. Reduced-motion styling removes page and artifact-frame transitions. Preserve selection, clipping and size behavior when changing the appearance.
784
+
785
+ An empty channel adds `.stage-empty`, containing an artifact `tv-icon` and a paragraph in a compact, rounded box centered over the stage. It shares the wallpaper-overlay background, border, outline and blur, and has no shadow. Its text uses `--wallpaper-overlay-text-color`. `.stage-empty p` styles its supporting line. During an armed fullscreen snap, `.snap-outline[aria-hidden="true"]` overlays the page box without taking input. The outline and the tab/channel placeholders express transient drop or resize state, not persisted selection.
786
+
787
+ ### Artifact frame and its menu
788
+
789
+ ```html
790
+ <div class="artifact-frame">
791
+ <iframe title="…"></iframe>
792
+ <footer class="artifact-title-bar">
793
+ <tv-icon name="artifact"></tv-icon>
794
+ <span class="artifact-title">…</span>
795
+ <!-- Navigation controls appear together when either direction exists. -->
796
+ <button class="artifact-back" icon variant="ghost" size="sm">…</button>
797
+ <button class="artifact-forward" icon variant="ghost" size="sm">…</button>
798
+ <span class="artifact-bar-divider" aria-hidden="true"></span>
799
+ <button class="artifact-menu-trigger" icon variant="ghost" size="sm">…</button>
800
+ <tv-menu>…</tv-menu>
801
+ </footer>
802
+ </div>
803
+ ```
804
+
805
+ `.artifact-frame` owns the border, curve, surface and shadow; background pages suppress the shadow. `.artifact-frame > iframe` fills the document area. `.artifact-title-bar` paints the lower band, `.artifact-title-bar .artifact-title` takes the remaining width and truncates, and `.artifact-bar-divider` separates navigation from the menu trigger. The two direction buttons use `disabled` to show unavailable history. The document repeats the top clipping radius where required for composited iframe/webview rendering.
806
+
807
+ The artifact menu uses the same shared menu elements and placement as channel menus. Deletion uses the shared dialog outline below.
808
+
809
+ ### Settings
810
+
811
+ ```html
812
+ <button class="settings-trigger" id="settings-trigger" icon variant="ghost">…</button>
813
+ <tv-popover class="settings-popover" trigger="settings-trigger">
814
+ <div class="settings-heading">Settings</div>
815
+ <div class="settings-field">
816
+ <label id="settings-appearance-label">…</label>
817
+ <button id="settings-appearance" aria-labelledby="settings-appearance-label">…</button>
818
+ <tv-select trigger="settings-appearance"><tv-option value="system" selected>…</tv-option>…</tv-select>
819
+ </div>
820
+ <div class="settings-field">
821
+ <label id="settings-theme-label">…</label>
822
+ <div class="settings-theme-control">
823
+ <button id="settings-theme" aria-labelledby="settings-theme-label">…</button>
824
+ <tv-select trigger="settings-theme">…</tv-select>
825
+ <button icon variant="ghost" aria-label="Refresh themes">…</button>
826
+ </div>
827
+ </div>
828
+ </tv-popover>
829
+ ```
830
+
831
+ `.settings-popover` owns the panel interior, `.settings-heading` the heading, and `.settings-field` the field groups. `.settings-theme-control` arranges the theme choice and refresh button. `.settings-field button[aria-haspopup="listbox"]` makes each choice trigger fill its field. The shared select supplies combobox semantics, caret, `tv-option[selected]`, and its open highlight; opening it leaves Settings open.
832
+
833
+ An executable active theme adds `.settings-javascript-consent`, with `.settings-javascript-disclosure` and `label.settings-javascript-toggle`. The label contains a native checkbox carrying `role="switch"` and `name="theme-javascript-consent"`. `.settings-javascript-toggle input`, `input::before`, `input:checked`, `input:checked::before`, and `input:focus-visible` define its track, knob, enabled state and focus ring. `.settings-status[role="status"]`, `.settings-failure[role="alert"]`, and `.settings-errors` show loading, failure and registry diagnostics; `.settings-errors p` and `.settings-errors strong` separate the message and folder emphasis.
834
+
835
+ ### Skills, update notice and copy confirmation
836
+
837
+ ```html
838
+ <button class="skill-trigger" id="skills-trigger" icon variant="ghost">…</button>
839
+ <tv-popover class="skill-popover" trigger="skills-trigger">
840
+ <div class="skill-heading">…</div><p class="skill-intro">…</p>
841
+ <div class="skill-grid">
842
+ <article class="skill-card">
843
+ <div class="skill-thumb"><img alt=""></div>
844
+ <div class="skill-name">…</div><p class="skill-desc">…</p>
845
+ <button class="copy-button" size="sm">…</button>
846
+ <span class="copy-button-status" role="status" aria-live="polite"></span>
847
+ </article>
848
+ </div>
849
+ </tv-popover>
850
+ ```
851
+
852
+ `.skill-popover`, `.skill-heading`, and `.skill-intro` define the panel framing. `.skill-grid` arranges the cards. `.skill-card`, `.skill-thumb`, `.skill-thumb img`, `.skill-name`, and `.skill-desc` own each card; `.skill-card button.copy-button` places its copy action at the bottom.
853
+
854
+ The update control is `button.update-bell#update-bell[icon][intent="alert"]` paired with `tv-popover.update-popover[manual][trigger="update-bell"][role="status"]`. `.update-popover` contains readable notice text and `.update-actions` with `button.update-later` and an optional primary copy action. The manual panel stays open until the composing surface closes it.
855
+
856
+ The reusable copy control is `button.copy-button[size="sm"][prompt]`, optionally carrying `intent`, containing `.copy-button-idle` and `.copy-button-done`. Both contain an icon and label. `.copy-button[copied] .copy-button-idle` hides the idle content without changing its occupied space; `.copy-button[copied] .copy-button-done` overlays the confirmation. `.copy-button-status` is a separate visually hidden live announcement. Preserve that status region and stable button size when restyling confirmation.
857
+
858
+ ### Dialogs, connection states and upgrade gate
859
+
860
+ ```html
861
+ <div class="dialog-overlay">
862
+ <dialog open>
863
+ <div class="dialog-alert" role="alertdialog">
864
+ <h2>…</h2><p>…</p>
865
+ <div class="dialog-actions"><button>Cancel</button><button intent="danger">…</button></div>
866
+ </div>
867
+ </dialog>
868
+ </div>
869
+ ```
870
+
871
+ The dialog may contain ordinary content instead of an alert. Ambient selectors `:where(.dialog-overlay)`, `:where(dialog)`, and `:where(dialog:focus-visible)` define dimming, centering, panel chrome and container focus treatment. `:where(.dialog-alert)`, `:where(.dialog-alert h2)`, `:where(.dialog-alert p)`, and `:where(.dialog-actions)` define the shared confirmation interior. Keep the modal input boundary and focus behavior intact.
872
+
873
+ Connection and authorization states reuse the dialog with `.system-modal`: an icon, heading and optional supporting paragraph. `.system-modal h2` and `.system-modal p:not(.tv-error)` set their hierarchy. Connecting and disconnected states use a spinning `tv-icon`; an error adds `.system-modal .server-url`. Authorization uses `form.system-modal.auth-form`. The field uses shared native-input styling; `.auth-form .auth-token` and `.auth-form .auth-submit` stretch the field and submit control. Rejection adds `aria-invalid="true"` and associates the shared error paragraph through `aria-describedby`; the alert communicates the failure. The rejected state is:
874
+
875
+ ```html
876
+ <form class="system-modal auth-form">
877
+ <tv-icon name="locked" size="xl"></tv-icon>
878
+ <h2>…</h2><p>…</p>
879
+ <input class="auth-token" type="password" name="token" placeholder="…" aria-label="…" autofocus required aria-invalid="true" aria-describedby="auth-token-error">
880
+ <p id="auth-token-error" class="tv-error" role="alert">…</p>
881
+ <button class="auth-submit" intent="primary">…</button>
882
+ </form>
883
+ ```
884
+
885
+ The ordinary authorization state omits the invalid attribute, error association and error paragraph.
886
+
887
+ Upgrade instructions use `.desktop-upgrade-gate > .dialog-overlay > dialog > .upgrade-gate-body`. The gate fills the halted page; the body scrolls within the panel. `.upgrade-gate-body h1`, `.upgrade-gate-body p`, `.upgrade-gate-body pre`, and `.upgrade-gate-body :last-child` restore local reading rhythm and command formatting.
888
+
889
+ ### Standalone artifact error documents
890
+
891
+ ```html
892
+ <main class="artifact-error">
893
+ <div class="artifact-error-body">
894
+ <h1>…</h1><p>…</p>
895
+ <section class="artifact-error-block"><p>…</p><p><code>…</code></p></section>
896
+ </div>
897
+ </main>
898
+ ```
899
+
900
+ `.artifact-error` paints the document ground; `.artifact-error-body` limits the reading column. `.artifact-error h1`, `.artifact-error p`, and `.artifact-error-block` arrange its text. A missing artifact can include a path chip. An unsupported URL can include a link or `.plain-address`, followed by install and launch command chips. `.artifact-error a`, `.artifact-error .plain-address`, and `.artifact-error code` wrap long content; code is selected whole for copying. Optional host-supplied content may carry `[hidden]`.
901
+
902
+ ## Panels and surface edges
903
+
904
+ Popovers, including menus and select option lists, and dialogs share a light inner border and dark outer outline. Artifact frames, the sidebar and selected tabs reuse this treatment. Ordinary buttons and text fields use a simple border. The catalog provides the corresponding component overrides. Decorative outlines must leave keyboard focus visible.
905
+
906
+ ## Interaction feedback
907
+
908
+ Where a control provides hover or pressed feedback, the change must be visible and its text or icon remain readable. Prefer increasing label contrast on filled controls when that gives visible feedback; otherwise use a readable direction that does. Transparent controls can reveal their shape with a wash. Appearance mode and text color alone do not determine the right direction.
909
+
910
+ Most control families derive active background from resting background. Ordinary and semantic filled controls move the resting color toward a light or dark pole chosen from its lightness. Wallpaper-overlay controls add a wash of their text color. Hover then mixes the resting and active colors using `--hover-mix`. A root-level theme override of a documented resting token keeps this automatic derivation unless the theme also supplies an active token. An explicit active background is used as supplied, including opacity; it is not tinted or flipped again. Active also styles expanded triggers; selection is separate.
911
+
912
+ Unselected tabs use the wallpaper-overlay treatment by default. For one coordinated treatment across tabs, ordinary navbar controls and the empty-stage message, set `--wallpaper-overlay-background` at the app-scoped root. Its active and hover states derive automatically, and tabs inherit the complete family. Set `--wallpaper-overlay-background-active` only when the derived active treatment needs an explicit replacement.
913
+
914
+ Use the tab tokens only when tabs must differ from the shared overlay treatment. In that case set both `--tab-background` and `--tab-background-active`; tab active defaults to the shared overlay active background and does not derive from a tab-specific resting background. Leave `--tab-background-hover` alone so Television computes hover between the two tab values. The same local-pair rule applies when a selector gives individual controls different colors: set resting and active tokens on that element because an active value inherited from an ancestor was derived there, before the local resting override. Do not replace the state selectors themselves. Set `--tab-background-selected` separately because selection is not an interaction state.
915
+
916
+ Complete-background tokens also accept gradients and images, which cannot be color-interpolated. Supply explicit hover and active backgrounds for those treatments.
917
+
918
+ ## Image backgrounds
919
+
920
+ Themes may optionally include an image background. If included, follow the setup and readability guidance below.
921
+
922
+ ### Adding an image
923
+
924
+ Put wallpaper images inside the theme package and reference them from the app-scoped root in `theme.css`:
925
+
926
+ ```css
927
+ :root[data-television-document="app"] {
928
+ --app-wallpaper-image: url(assets/day.jpg);
929
+ }
930
+
931
+ /* Optional: use a different image in dark appearance. */
932
+ :root[data-television-document="app"][data-theme="dark"] {
933
+ --app-wallpaper-image: url(assets/night.jpg);
934
+ }
935
+ ```
936
+
937
+ `--app-wallpaper-image` is a registered URL value: relative paths resolve against the declaring stylesheet. Use `none` to remove the image. The default wallpaper treatment centers and covers the available ground. Use `--app-wallpaper` for a complete background declaration when changing positioning, adding gradients or composing other layers.
938
+
939
+ ### Readability over the image
940
+
941
+ Choose `--wallpaper-overlay-background` and `--wallpaper-overlay-text-color` together. Unselected tabs, ordinary navbar overlay controls and the empty-stage message share this treatment. Over a busy image, start with `--tint-surface-muted` or `--tint-surface` and `--color-text`. These fills use `--alpha-50`; add `--wallpaper-overlay-blur` separately if a frosted treatment helps. A dark translucent fill with light text can suit either appearance. Judge the visible result over the image: blur softens detail but does not ensure contrast. Increase the fill opacity or use a solid surface when necessary.
942
+
943
+ Check resting, hover, pressed, open-menu and selected states in both appearances, over bright and dark image regions and at different window sizes. Apply the [interaction feedback](#interaction-feedback) guidance over the actual backdrop. If the automatic feedback does not suit the image, set the complete active background, including opacity:
944
+
945
+ ```css
946
+ :root[data-television-document="app"] {
947
+ --wallpaper-overlay-background-active: oklch(27.9% 0.041 260.031 / 75%);
948
+ }
949
+ ```
950
+
951
+ Hover follows this override automatically. Tabs inherit the overlay active background but derive hover from their own resting background. If setting an explicit `--wallpaper-overlay-background-hover`, also set `--tab-background-hover: var(--wallpaper-overlay-background-hover)` when tabs should share it. This is needed for gradient or image fills, where interpolation is unavailable. Keep keyboard focus visible. The empty-stage message uses only the resting treatment.
952
+
953
+ ## Runtime and loading
954
+
955
+ Television publishes the active package at one stable `/theme/` path. Eligible package files are served byte for byte. Keep `@import` and `url(...)` references relative to `theme.css`; the browser resolves them beneath the stable active-package path. Do not construct a URL from the theme ID, and do not expect Television to rewrite CSS.
956
+
957
+ The application loads `/theme/theme.css` after its complete foundation and application surface. Artifacts that link a theme-capable live canonical version load the same entry after their complete canonical foundation. No Television-owned stylesheet content follows the active theme. Frozen canonical v1 remains its built, unthemed, light-only surface.
958
+
959
+ Saving any file in the active package tree uses the live-update path. The application and affected artifacts refresh: local HTML artifacts reload, the markdown editor preserves its editor and contents while refreshing canonical styling, built-in artifact error documents reload, and third-party URL artifacts remain loaded. This update fanout covers nested stylesheets, images, fonts, the manifest, the authoring README, and other package files whether or not the active stylesheet currently requests them. Television combines the registered manifest's `colorScheme` with the stored appearance preference. `light dark` follows the preference; `light` or `dark` fixes presentation to that value while preserving preference changes for a later adaptive theme or `None`. An effective appearance change updates app and artifact document state without reloading those documents, the entry stylesheet, or the main script. It recreates each enabled theme frame and reruns its entry script. A stored preference change under a fixed theme changes no presentation.
960
+
961
+ Registry refresh publishes package discovery and manifest metadata. This includes a changed `colorScheme`. Use it after adding or repairing a package or changing its manifest; `tv set-theme <theme-id>` performs that refresh before selecting an installed ID. Saving a manifest in the active package also follows the live-update path, but its registry record keeps the last scanned metadata until refresh or the next serving boot.
962
+
963
+ If the active package folder temporarily disappears, Television keeps its exact theme ID selected and shows foundation and canonical styling while watching for the same path. Restoring the folder reapplies the package without a registry refresh. Refreshing the registry while the folder is absent selects `None`, and restoring the folder after that does not select it again.
964
+
965
+ ## Theme effects and scripts
966
+
967
+ Theme effects are optional visual additions, such as textures, tint overlays, or animated backgrounds. Use them when the requested design calls for them, following the token-first approach above:
968
+
969
+ 1. Prefer CSS. The permanent `#foreground-overlay` spans the application viewport above the interface, accepts no pointer input, and is the preferred surface for tint, `backdrop-filter`, translucent imagery, texture, and scanline effects. Ordinary menus and popovers sit beneath it; native modal dialogs remain above it.
970
+ 2. Use JavaScript in a sandboxed frame when CSS cannot produce the effect and application DOM access is unnecessary, for example for an animation driven by host-supplied pointer information. Put effects behind the interface in `iframe-background.js` or above the interface and CSS foreground in `iframe-overlay.js`.
971
+ 3. Use main-page JavaScript only when the effect requires access to the main application document.
972
+
973
+ The layers, from back to front, are `#theme-iframe-background` (z-index `0`), `#app` (`1`), `#foreground-overlay` (`2147483646`), and `#theme-iframe-overlay` (`2147483647`). Theme CSS may set opacity, filters and other presentation on the three effect surfaces. Their fixed viewport positioning, protected stacking and `pointer-events: none` keep effects separate from application interaction. Television keeps each frame element's `color-scheme` and its document's declared scheme matched to the effective `data-theme`; that match is what keeps the frame transparent. Read `data-theme` from the frame document's root once at startup. Appearance changes recreate the frames and rerun their scripts, so scripts need no appearance listener. Never change `color-scheme` on the frame document's root: a mismatch with the frame element forces the frame opaque. Theme CSS cannot cross into frame documents.
974
+
975
+ ### Entry declarations
976
+
977
+ Each JavaScript surface has an independent manifest declaration and matching root entry:
978
+
979
+ | Manifest declaration | Root entry | Runtime |
980
+ | --- | --- | --- |
981
+ | `"enableMainJS": true` | `main.js` | Main application document after exact-ID user consent |
982
+ | `"enableIframeBackgroundJS": true` | `iframe-background.js` | Sandboxed frame behind the application |
983
+ | `"enableIframeOverlayJS": true` | `iframe-overlay.js` | Sandboxed frame above the application and CSS foreground |
984
+
985
+ For example, a package using all three entries declares:
986
+
987
+ ```json
988
+ {
989
+ "name": "Paperlike",
990
+ "version": "1.0.0",
991
+ "colorScheme": "light dark",
992
+ "authoredForAppVersion": "<app-version>",
993
+ "enableMainJS": true,
994
+ "enableIframeBackgroundJS": true,
995
+ "enableIframeOverlayJS": true
996
+ }
997
+ ```
998
+
999
+ Each declaration is optional and must be a boolean when present. A present declaration, including `false`, requires its matching readable root file; only `true` enables execution. An entry without its declaration does not execute. The reserved `/theme/main.js`, `/theme/iframe-background.js`, and `/theme/iframe-overlay.js` URLs return a successful empty JavaScript response when their manifest or consent gates are closed, or the active package or entry is unavailable. Other script files are ordinary package assets. Television does not parse or validate JavaScript.
1000
+
1001
+ The declarations follow the server's registered manifest snapshot. Editing a declaration does not change its delivery gate until the theme registry refreshes. Saving any package file still triggers the active-package live-update path and uses the registered manifest snapshot.
1002
+
1003
+ All three entries run as classic scripts in browser and desktop application documents and never run in artifacts. During top-level execution, capture `document.currentScript.src` and resolve package assets relative to that URL. Ordinary document-relative URLs resolve against the entry's document.
1004
+
1005
+ ### Sandboxed frames and pointer information
1006
+
1007
+ Iframe entries execute automatically without main-page consent. Each frame uses exactly `sandbox="allow-scripts"`, which gives its document an opaque origin. The script can draw and animate inside that document but cannot access the application DOM. The frames are inert, absent from sequential focus, and receive no direct pointer or keyboard input; pointer input continues to the application beneath them.
1008
+
1009
+ The host sends application pointer transitions and supported artifact pointer notifications to both current frames through `window` message events. Messages are not queued or replayed before a frame installs its listener. Accept messages only when `event.source === parent` and `data.type` is one of these closed names:
1010
+
1011
+ - `television-theme-pointer-move`
1012
+ - `television-theme-pointer-down`
1013
+ - `television-theme-pointer-up`
1014
+ - `television-theme-pointer-cancel`
1015
+ - `television-theme-pointer-click`
1016
+
1017
+ Every message contains only `type`, `clientX`, `clientY`, `button`, and `buttons`:
1018
+
1019
+ ```js
1020
+ {
1021
+ type,
1022
+ clientX,
1023
+ clientY,
1024
+ button,
1025
+ buttons
1026
+ }
1027
+ ```
1028
+
1029
+ `clientX` and `clientY` are application-viewport CSS pixels. `buttons` is the standard post-transition bitmask. Move carries `button: -1` and the current buttons; down and up carry the changed button and the post-transition bitmask; click carries the clicked button and `buttons: 0`; cancel carries `button: -1, buttons: 0`. The host's opaque recipient requires `"*"` as the destination origin. This wildcard does not create a reverse command channel: Television ignores messages sent from a theme frame.
1030
+
1031
+ Keep each frame transparent wherever the application should remain visible. Destroying a frame ends its isolated runtime: the frame's removal ends its document, listeners, timers, and effects without reloading the application.
1032
+
1033
+ ### Main-page trust boundary and lifecycle
1034
+
1035
+ A registered `enableMainJS: true` makes the root `main.js` eligible, and execution additionally requires the active theme's exact ID in the server's persisted consent set. Selecting or activating a theme does not grant consent. Before requesting consent, inspect and explain `main.js` and the effects it creates, then ask the user to grant consent in Settings. An authoring agent does not grant consent on the user's behalf. Settings shows the main-page JavaScript switch when the registered active theme declares `enableMainJS: true`. The user grants or withdraws consent there.
1036
+
1037
+ Consent is an explicit trust decision. Television loads eligible `main.js` as a classic script in each connected browser and desktop application document. It has ordinary access to that page's DOM, globals, browser storage, and network APIs. Electron grants no Node.js integration beyond capabilities the application renderer already exposes. Television's elements, globals, internal state, and other implementation details are not a stable JavaScript theme API and can change between releases. Consent persists for the exact theme ID until the user opts out, including while another theme is selected.
1038
+
1039
+ Keep main-page customization self-contained. Own top-level DOM rather than mutating Television-owned elements, and keep visual additions noninteractive. Excessive CPU or GPU use in any executable surface is a usability defect.
1040
+
1041
+ Every active-package refresh reruns an enabled and consented main script and destroys and recreates each enabled iframe, including refreshes caused by an unrelated package file. Main scripts therefore use repeat-safe ownership that recognizes and reuses or replaces their nodes, listeners, and timers. Frame replacement supplies cleanup for iframe effects. Consent-only changes preserve the frames unless the application reloads. Appearance changes recreate each enabled iframe and rerun its script; disconnect removes them. An active-package refresh, appearance change, or disconnect does not reload the application document, so successful main-page effects can remain until that document reloads.
1042
+
1043
+ When the main-script include is installed, opting out or selecting another theme or `None` automatically reloads the application document. The fresh document clears the script's DOM additions, listeners, timers, globals, and other document-lifetime effects before applying confirmed destination state. Persistent storage writes and completed network requests remain outside this reset boundary.
1044
+
1045
+ A load failure, syntax error, or uncaught exception in an iframe stays inside that frame. The same failure in `main.js` does not throw through Television's bundled application module, although successful main-page changes made before or around an error can still break the product.
1046
+
1047
+ ## Appearance
1048
+
1049
+ Television combines the manifest's required `colorScheme` with the server-wide appearance preference. `light dark` follows that preference. `light` and `dark` fix the effective appearance to the declared value without rewriting the preference. Television dynamically maintains the resulting `data-theme="light"` or `data-theme="dark"` on the application root. Television-managed artifact documents install the same resolver with fixed `system`: a browser artifact resolves from the iframe's inherited scheme, while an Electron artifact resolves from Electron's native application preference. Each generated theme-frame document starts with the application's effective value and is recreated when that value changes.
1050
+
1051
+ Hinge all appearance-dependent theme styling on the root attribute. Do not use `light-dark()` or `prefers-color-scheme`; those mechanisms can follow browser or device state instead of Television's root marker. The foundation supplies Television's zero-specificity `color-scheme` value in every theme-capable app and artifact document, matching native controls and embedded contexts to `data-theme`. Theme CSS never declares `color-scheme`; the manifest is the theme's one appearance declaration.
1052
+
1053
+ A fixed theme puts its semantic colors and other token statements at `:root`, uses no mode blocks, and declares the matching `light` or `dark` value in its manifest. An adaptive theme declares `light dark`, states shared choices at `:root`, then puts only intended differences under `[data-theme="light"]` and `[data-theme="dark"]` after those shared statements. It can customize one mode and leave the other on foundation defaults.
1054
+
1055
+ Inspect light and dark effective appearance for an adaptive theme. For a fixed theme, inspect its one effective appearance under both a matching and an opposing stored preference, confirming that the presentation stays fixed. Mode-dependent foundation values that the theme does not override—including border opacity, active-state tint strength, and the four shadow tokens—follow the effective root marker.
1056
+
1057
+ ## Theme selection
1058
+
1059
+ The user can choose a theme in the Settings UI. Agents activate one with `tv set-theme <theme-id>`, substituting the exact installed ID. Any capitalization of `none` selects no theme, which user-facing output labels `None`.
1060
+
1061
+ A successful command prints one of these transitions:
1062
+
1063
+ ```text
1064
+ Active theme changed from '<previous>' to '<new>'.
1065
+ Active theme unchanged: '<selection>'.
1066
+ Active theme: '<new>'.
1067
+ ```
1068
+
1069
+ The first two forms report the opening selection when it was available. The third confirms the new selection when the opening read was unavailable. Activation does not need a separate preliminary selection read; preserve prior-selection context when the command provides it.
1070
+
1071
+ ## Authoring workflow
1072
+
1073
+ 1. Gather the user's visual intent, references, palette and typography direction, and the application or artifact surfaces that matter.
1074
+ 2. Run `tv storage-path` to locate the themes directory and `tv status` to establish the target app version.
1075
+ 3. Choose an exact theme ID, inspect the target path, and apply the existing-folder safeguards above before writing.
1076
+ 4. Choose the least invasive visual surface, then write the manifest with its required `colorScheme`, entry stylesheet, README, any justified JavaScript entries, and relative assets as one package. Add purpose comments to narrow selector rules.
1077
+ 5. Activate the package with `tv set-theme <theme-id>`. Correct any manifest error the command reports.
1078
+ 6. When the package declares `main.js`, inspect and explain it, then ask the user to grant consent in Settings. For iframe entries, account for the sandbox, pointer-message contract, and frame replacement lifecycle.
1079
+ 7. Iterate on the files in its watched package tree. Refresh the theme registry to publish manifest changes.
1080
+ 8. Verify the application shell and one artifact using a theme-capable live canonical version. For an adaptive theme, inspect light and dark effective appearance. For a fixed theme, inspect matching and opposing stored preferences and confirm that presentation stays fixed. Check readability, asset loading, native controls, intended cross-document reach, and document continuity.
1081
+ 9. Leave the README, comments, stylesheet, script, manifest, and assets consistent for the next maintainer.
1082
+
1083
+ Visual verification can include screenshots when the environment can render both documents headlessly and interpret the results. Offer the user an optional review of the shell and one live-canonical artifact (four captures): light and dark effective appearance for an adaptive theme, or matching and opposing stored preferences for a fixed theme. Explain that it takes additional time. Wait for the user's consent before capturing them. Write captures only to a temporary location, review them, and delete every temporary capture after review. If the environment lacks either capability or the user declines, ask the user to inspect the same states.
1084
+
1085
+ ## Clouds as a worked example
1086
+
1087
+ The installed Clouds package at `<storagePath>/themes/clouds/` is a locally available structural example of package shape, relative assets, application-only scoping, and purpose-specific shell rules. Its version may meet or exceed Television's minimum, and its files may contain user edits, so read it as a local example rather than a pristine template. Use the vocabulary and reference in this document as the authority when adapting the example.