@unifedev/thread-pages 0.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.
package/theme.ts ADDED
@@ -0,0 +1,676 @@
1
+ /**
2
+ * The Thread Pages design system.
3
+ *
4
+ * Five worlds, each declaring both palettes at once as --l-* and --d-*, with one
5
+ * resolver publishing the live half onto the tokens the rest of the sheet uses.
6
+ * A page picks one with data-theme on <html> and needs no class names: every
7
+ * rule keys off semantic structure (fieldset/legend, a wrapping label, small,
8
+ * button), so plain HTML is already styled.
9
+ *
10
+ * This is carried into each new page by the seed rather than injected at render
11
+ * time, so a page owns its own look and a plugin update never restyles work the
12
+ * user already read.
13
+ */
14
+ export const THEME_CSS = String.raw`
15
+ :root {
16
+ --bg: #fbfbfa;
17
+ --surface: #ffffff;
18
+ --ink: #16181d;
19
+ --ink-2: #4a5058;
20
+ --ink-3: #767d87;
21
+ --rule: #e3e4e6;
22
+ --rule-soft: #eeeff0;
23
+ --accent: #2f5cc7;
24
+ --flag: #a8410f;
25
+ --ok: #1f6b45;
26
+ --code-bg: #f2f3f4;
27
+ --measure: 34rem;
28
+ }
29
+ @media (prefers-color-scheme: dark) {
30
+ :root {
31
+ --bg: #121316;
32
+ --surface: #191b1f;
33
+ --ink: #e9eaec;
34
+ --ink-2: #b0b5bc;
35
+ --ink-3: #838a93;
36
+ --rule: #2c2f35;
37
+ --rule-soft: #232227;
38
+ --accent: #8aa9f0;
39
+ --flag: #e8a37a;
40
+ --ok: #79c69d;
41
+ --code-bg: #22242a;
42
+ }
43
+ }
44
+
45
+ *, *::before, *::after { box-sizing: border-box; }
46
+
47
+ html { -webkit-text-size-adjust: 100%; }
48
+
49
+ body {
50
+ margin: 0;
51
+ background: var(--bg);
52
+ color: var(--ink);
53
+ font: 400 16.5px/1.6 ui-sans-serif, -apple-system, "SF Pro Text", "Segoe UI", Inter, system-ui, sans-serif;
54
+ font-feature-settings: "kern", "liga";
55
+ text-rendering: optimizeLegibility;
56
+ -webkit-font-smoothing: antialiased;
57
+ }
58
+
59
+ .wrap {
60
+ max-width: calc(var(--measure) + 6rem);
61
+ margin: 0 auto;
62
+ padding: 4.5rem 3rem 8rem;
63
+ }
64
+ @media (max-width: 640px) { .wrap { padding: 2.5rem 1.25rem 5rem; } }
65
+
66
+ /* ---- header ---- */
67
+
68
+ header.brief-head {
69
+ padding-bottom: 1.75rem;
70
+ margin-bottom: 3rem;
71
+ border-bottom: 1px solid var(--rule);
72
+ }
73
+ header.brief-head h1 {
74
+ margin: 0;
75
+ font-size: 1.9rem;
76
+ line-height: 1.2;
77
+ font-weight: 640;
78
+ letter-spacing: -0.021em;
79
+ text-wrap: balance;
80
+ }
81
+ .brief-meta {
82
+ margin: 0.85rem 0 0;
83
+ display: flex;
84
+ flex-wrap: wrap;
85
+ gap: 0.45rem 1.15rem;
86
+ font-size: 0.78rem;
87
+ line-height: 1.4;
88
+ color: var(--ink-3);
89
+ font-variant-numeric: tabular-nums;
90
+ }
91
+
92
+ /* ---- rhythm ---- */
93
+
94
+ main > * + * { margin-top: 1.05rem; }
95
+
96
+ h2 {
97
+ margin: 3rem 0 0;
98
+ font-size: 1.16rem;
99
+ line-height: 1.3;
100
+ font-weight: 620;
101
+ letter-spacing: -0.012em;
102
+ }
103
+ h2 + * { margin-top: 0.85rem; }
104
+
105
+ h3 {
106
+ margin: 2rem 0 0;
107
+ font-size: 0.94rem;
108
+ line-height: 1.35;
109
+ font-weight: 640;
110
+ letter-spacing: 0.005em;
111
+ color: var(--ink-2);
112
+ }
113
+ h3 + * { margin-top: 0.6rem; }
114
+
115
+ p, li { max-width: var(--measure); color: var(--ink-2); }
116
+ p { margin: 0; }
117
+ main > p:first-child { font-size: 1.06rem; color: var(--ink); }
118
+
119
+ ul, ol { margin: 0; padding-left: 1.3rem; }
120
+ li + li { margin-top: 0.42rem; }
121
+ li::marker { color: var(--ink-3); }
122
+
123
+ strong { font-weight: 620; color: var(--ink); }
124
+ em { font-style: italic; }
125
+
126
+ a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
127
+ a:hover { text-decoration-thickness: 2px; }
128
+
129
+ code {
130
+ font: 0.85em/1.5 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
131
+ background: var(--code-bg);
132
+ padding: 0.13em 0.36em;
133
+ border-radius: 4px;
134
+ }
135
+ pre {
136
+ margin: 0;
137
+ background: var(--code-bg);
138
+ border: 1px solid var(--rule-soft);
139
+ border-radius: 8px;
140
+ padding: 0.9rem 1.05rem;
141
+ overflow-x: auto;
142
+ font: 0.83rem/1.6 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
143
+ }
144
+ pre code { background: none; padding: 0; }
145
+
146
+ hr {
147
+ margin: 3rem 0;
148
+ border: 0;
149
+ border-top: 1px solid var(--rule);
150
+ }
151
+
152
+ /* ---- callouts: use sparingly ---- */
153
+
154
+ .card {
155
+ background: var(--surface);
156
+ border: 1px solid var(--rule);
157
+ border-radius: 10px;
158
+ padding: 1.15rem 1.3rem;
159
+ }
160
+ .card > * + * { margin-top: 0.7rem; }
161
+ .card > h3:first-child { margin-top: 0; }
162
+
163
+ .needs-you {
164
+ border-left: 3px solid var(--flag);
165
+ padding: 0.15rem 0 0.15rem 1.05rem;
166
+ }
167
+ .needs-you > * + * { margin-top: 0.55rem; }
168
+
169
+ .label {
170
+ display: inline-block;
171
+ font-size: 0.68rem;
172
+ font-weight: 660;
173
+ letter-spacing: 0.07em;
174
+ text-transform: uppercase;
175
+ color: var(--flag);
176
+ }
177
+ .label.done { color: var(--ok); }
178
+
179
+ table {
180
+ border-collapse: collapse;
181
+ width: 100%;
182
+ font-size: 0.9rem;
183
+ }
184
+ th, td {
185
+ text-align: left;
186
+ padding: 0.55rem 0.9rem 0.55rem 0;
187
+ border-bottom: 1px solid var(--rule-soft);
188
+ vertical-align: top;
189
+ }
190
+ th {
191
+ font-weight: 620;
192
+ font-size: 0.75rem;
193
+ letter-spacing: 0.05em;
194
+ text-transform: uppercase;
195
+ color: var(--ink-3);
196
+ }
197
+ td { color: var(--ink-2); }
198
+
199
+ /* ---- forms ----------------------------------------------------------
200
+ Styled off semantic structure, not classes, so a page only ever needs
201
+ plain HTML: fieldset/legend for a group, a wrapping label for a single
202
+ control, small for a hint, button for an action. ------------------- */
203
+
204
+ form {
205
+ margin-top: 1.75rem;
206
+ background: var(--surface);
207
+ border: 1px solid var(--rule);
208
+ border-radius: 10px;
209
+ padding: 1.4rem 1.45rem 1.3rem;
210
+ }
211
+ form > * + * { margin-top: 1.25rem; }
212
+
213
+ /* 'margin: 0' here used to beat 'form > * + *' on specificity, so two groups
214
+ of choices in a row ran together with no gap between them. */
215
+ form fieldset { padding: 0; border: 0; min-width: 0; }
216
+ form > fieldset { margin: 0; }
217
+ form > fieldset + fieldset,
218
+ form > * + fieldset,
219
+ form > fieldset + * { margin-top: 1.25rem; }
220
+ form legend,
221
+ form > label,
222
+ .field > label {
223
+ display: block;
224
+ padding: 0;
225
+ font-size: 0.82rem;
226
+ font-weight: 620;
227
+ letter-spacing: 0.005em;
228
+ color: var(--ink);
229
+ }
230
+ form legend { margin-bottom: 0.5rem; }
231
+
232
+ form small, .field .hint {
233
+ display: block;
234
+ margin-top: 0.35rem;
235
+ font-size: 0.78rem;
236
+ line-height: 1.45;
237
+ color: var(--ink-3);
238
+ }
239
+
240
+ form input[type="file"] {
241
+ display: block; margin-top: 0.45rem; font: inherit; font-size: 0.85rem;
242
+ color: var(--ink-2); max-width: 100%;
243
+ }
244
+ form input[type="file"]::file-selector-button {
245
+ font: inherit; font-size: 0.82rem; font-weight: 600; cursor: pointer;
246
+ color: var(--accent); background: transparent;
247
+ border: var(--rule-w) solid var(--rule); border-radius: calc(var(--radius) * 0.6);
248
+ padding: 0.35rem 0.75rem; margin-right: 0.6rem;
249
+ }
250
+ form input[type="text"], form input[type="number"], form input[type="url"],
251
+ form input[type="email"], form input[type="date"], form textarea, form select {
252
+ display: block;
253
+ width: 100%;
254
+ margin-top: 0.45rem;
255
+ font: inherit;
256
+ font-size: 0.92rem;
257
+ color: var(--ink);
258
+ background: var(--bg);
259
+ border: 1px solid var(--rule);
260
+ border-radius: 7px;
261
+ padding: 0.5rem 0.65rem;
262
+ }
263
+ form textarea { resize: vertical; min-height: 4.5rem; line-height: 1.55; }
264
+ form :focus-visible {
265
+ outline: 2px solid var(--accent);
266
+ outline-offset: -1px;
267
+ }
268
+
269
+ /* One option row: a label wrapping a radio or checkbox. */
270
+ form label:has(> input[type="radio"]),
271
+ form label:has(> input[type="checkbox"]),
272
+ .choice {
273
+ display: flex;
274
+ align-items: flex-start;
275
+ gap: 0.55rem;
276
+ font-size: 0.9rem;
277
+ font-weight: 400;
278
+ color: var(--ink-2);
279
+ cursor: pointer;
280
+ }
281
+ form label:has(> input[type="radio"]) + label,
282
+ form label:has(> input[type="checkbox"]) + label,
283
+ .choice + .choice { margin-top: 0.4rem; }
284
+ form input[type="radio"], form input[type="checkbox"] {
285
+ margin: 0.3rem 0 0;
286
+ flex: none;
287
+ accent-color: var(--accent);
288
+ }
289
+
290
+ form label:has(> input[type="range"]) {
291
+ display: flex;
292
+ align-items: center;
293
+ gap: 0.8rem;
294
+ font-size: 0.9rem;
295
+ font-weight: 400;
296
+ color: var(--ink-2);
297
+ }
298
+ form input[type="range"] { flex: 1; min-width: 8rem; accent-color: var(--accent); }
299
+ [data-thread-page-range] {
300
+ flex: none;
301
+ min-width: 2.2rem;
302
+ text-align: right;
303
+ font-size: 0.85rem;
304
+ font-variant-numeric: tabular-nums;
305
+ color: var(--ink);
306
+ }
307
+
308
+ [data-thread-page-status] {
309
+ margin-top: 0.9rem;
310
+ font-size: 0.82rem;
311
+ line-height: 1.5;
312
+ color: var(--ink-3);
313
+ }
314
+ [data-thread-page-status][data-state="error"] { color: var(--flag); }
315
+ [data-thread-page-status][data-state="sent"] { color: var(--ok); }
316
+
317
+ @media print {
318
+ body { background: #fff; color: #000; }
319
+ .wrap { padding: 0; max-width: none; }
320
+ form { display: none; }
321
+ }
322
+ /* ====================================================================
323
+ FIVE WORLDS
324
+
325
+ A theme here is not a palette. It is a palette, a typeface, a shape
326
+ language, a way a screen arrives, an atmosphere, and — the part that
327
+ matters most — its own idea of what an interactive thing looks like.
328
+ Picking one changes how you choose and how you commit, not just what
329
+ it costs to look at.
330
+
331
+ Each theme declares both palettes at once as --l-* and --d-*; one
332
+ resolver below maps the live half onto the tokens the base stylesheet
333
+ already uses. The same declarations carry [data-world="x"], which is how
334
+ a card on screen 1 renders a fragment of a page in a world you have not
335
+ entered yet.
336
+ ==================================================================== */
337
+
338
+ /* ---- 1. paper — quiet document. Nothing to notice. ---- */
339
+ [data-theme="paper"], [data-world="paper"] {
340
+ --l-bg:#fbfbfa; --l-surface:#ffffff; --l-ink:#16181d; --l-ink-2:#4a5058;
341
+ --l-ink-3:#6c737c; --l-rule:#e3e4e6; --l-rule-soft:#eeeff0; --l-code:#f2f3f4;
342
+ --l-ah:222; --l-as:62; --l-al:48; --l-flag:#a8410f; --l-ok:#1f6b45;
343
+ --d-bg:#121316; --d-surface:#191b1f; --d-ink:#e9eaec; --d-ink-2:#b0b5bc;
344
+ --d-ink-3:#838a93; --d-rule:#2c2f35; --d-rule-soft:#232227; --d-code:#22242a;
345
+ --d-ah:222; --d-as:70; --d-al:74; --d-flag:#e8a37a; --d-ok:#79c69d;
346
+ --font-body: ui-sans-serif, -apple-system, "SF Pro Text", "Segoe UI", Inter, system-ui, sans-serif;
347
+ --font-head: var(--font-body);
348
+ --radius:10px; --rule-w:1px; --head-weight:640; --head-track:-0.021em;
349
+ --measure:34rem; --shadow:none; --label-case:uppercase; --caps-track:0.07em;
350
+ --h1-size:1.9rem; --h1-lh:1.2;
351
+ }
352
+
353
+ /* ---- 2. terminal — console. Everything on a grid, nothing rounded. ---- */
354
+ [data-theme="terminal"], [data-world="terminal"] {
355
+ --l-bg:#f6f6f2; --l-surface:#ffffff; --l-ink:#15201a; --l-ink-2:#3c4a42;
356
+ --l-ink-3:#646f69; --l-rule:#c9d2cb; --l-rule-soft:#e2e7e2; --l-code:#eaeee9;
357
+ --l-ah:150; --l-as:88; --l-al:26; --l-flag:#a33a10; --l-ok:#14663c;
358
+ --d-bg:#080b09; --d-surface:#0d120e; --d-ink:#cfe6d5; --d-ink-2:#94b39d;
359
+ --d-ink-3:#6b8573; --d-rule:#20301e; --d-rule-soft:#161f16; --d-code:#111811;
360
+ --d-ah:150; --d-as:64; --d-al:62; --d-flag:#e0a44f; --d-ok:#63d18e;
361
+ --font-body: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
362
+ --font-head: var(--font-body);
363
+ --radius:0px; --rule-w:1px; --head-weight:700; --head-track:0em;
364
+ --measure:33rem; --shadow:none; --label-case:uppercase; --caps-track:0.14em;
365
+ --h1-size:1.6rem; --h1-lh:1.25;
366
+ }
367
+
368
+ /* ---- 3. atrium — daylight on paper. Warm, serif, things have weight. ---- */
369
+ [data-theme="atrium"], [data-world="atrium"] {
370
+ --l-bg:#f6f2e9; --l-surface:#fffdf8; --l-ink:#1e1a14; --l-ink-2:#4b4337;
371
+ --l-ink-3:#726958; --l-rule:#ddd5c4; --l-rule-soft:#ebe5d8; --l-code:#efe9db;
372
+ --l-ah:142; --l-as:34; --l-al:30; --l-flag:#8a4b18; --l-ok:#2c5f3f;
373
+ --d-bg:#16150f; --d-surface:#1f1d15; --d-ink:#f1ebdc; --d-ink-2:#c2b9a3;
374
+ --d-ink-3:#8f8672; --d-rule:#33301f; --d-rule-soft:#262418; --d-code:#242216;
375
+ --d-ah:130; --d-as:32; --d-al:66; --d-flag:#d99a5e; --d-ok:#8fc2a0;
376
+ --font-body: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
377
+ --font-head: var(--font-body);
378
+ --radius:14px; --rule-w:1px; --head-weight:600; --head-track:-0.008em;
379
+ --measure:38rem;
380
+ --shadow: 0 1px 2px rgba(40,30,10,.06), 0 10px 30px -14px rgba(40,30,10,.28);
381
+ --label-case:none; --caps-track:0.02em;
382
+ --h1-size:2.15rem; --h1-lh:1.15;
383
+ }
384
+
385
+ /* ---- 4. volume — depth. A lit scene with the page standing in it. ---- */
386
+ [data-theme="volume"], [data-world="volume"] {
387
+ --l-bg:#eef1f6; --l-surface:#ffffff; --l-ink:#0d1424; --l-ink-2:#3a4658;
388
+ --l-ink-3:#5e687b; --l-rule:#ccd4e2; --l-rule-soft:#e0e6ef; --l-code:#e6ebf3;
389
+ --l-ah:196; --l-as:78; --l-al:32; --l-flag:#9c4415; --l-ok:#136b58;
390
+ --d-bg:#080b12; --d-surface:#111823; --d-ink:#dfe8f5; --d-ink-2:#a3b2c8;
391
+ --d-ink-3:#74849b; --d-rule:#1e2a3c; --d-rule-soft:#151d2a; --d-code:#131b27;
392
+ --d-ah:190; --d-as:82; --d-al:62; --d-flag:#e2a06b; --d-ok:#4fc7ad;
393
+ --font-body: ui-sans-serif, -apple-system, "SF Pro Text", "Segoe UI", Inter, system-ui, sans-serif;
394
+ --font-head: var(--font-body);
395
+ --radius:6px; --rule-w:1px; --head-weight:700; --head-track:-0.03em;
396
+ --measure:34rem;
397
+ --shadow: 0 2px 4px rgba(0,0,0,.18), 0 22px 40px -20px rgba(0,0,0,.45);
398
+ --label-case:uppercase; --caps-track:0.11em;
399
+ --h1-size:2.1rem; --h1-lh:1.1;
400
+ }
401
+
402
+ /* ---- 5. bloom — shapes and colour. Big type, soft mass, round everything. ---- */
403
+ [data-theme="bloom"], [data-world="bloom"] {
404
+ --l-bg:#fdf7f4; --l-surface:#ffffff; --l-ink:#1d1226; --l-ink-2:#4c3a59;
405
+ --l-ink-3:#75647f; --l-rule:#ecdfe6; --l-rule-soft:#f5eef1; --l-code:#f6eef4;
406
+ --l-ah:330; --l-as:62; --l-al:42; --l-flag:#b03d24; --l-ok:#2f6b58;
407
+ --d-bg:#150e1c; --d-surface:#211729; --d-ink:#f4ecf6; --d-ink-2:#c0aecb;
408
+ --d-ink-3:#95839f; --d-rule:#33243d; --d-rule-soft:#261a2e; --d-code:#281c32;
409
+ --d-ah:326; --d-as:76; --d-al:72; --d-flag:#f0a184; --d-ok:#7fd0b4;
410
+ --font-body: "Avenir Next", Avenir, "Futura", ui-rounded, ui-sans-serif, -apple-system, system-ui, sans-serif;
411
+ --font-head: var(--font-body);
412
+ --radius:22px; --rule-w:1.5px; --head-weight:700; --head-track:-0.035em;
413
+ --measure:32rem;
414
+ --shadow: 0 2px 6px rgba(60,20,60,.06), 0 18px 40px -18px rgba(60,20,60,.22);
415
+ --label-case:none; --caps-track:0.03em;
416
+ --h1-size:2.6rem; --h1-lh:1.02;
417
+ }
418
+
419
+ /* ---- the resolver: which half of a palette is live ---- */
420
+
421
+ :root, [data-world] {
422
+ --bg:var(--l-bg); --surface:var(--l-surface); --ink:var(--l-ink);
423
+ --ink-2:var(--l-ink-2); --ink-3:var(--l-ink-3); --rule:var(--l-rule);
424
+ --rule-soft:var(--l-rule-soft); --code-bg:var(--l-code);
425
+ --ah:var(--l-ah); --as:var(--l-as); --al:var(--l-al);
426
+ --flag:var(--l-flag); --ok:var(--l-ok);
427
+ }
428
+ @media (prefers-color-scheme: dark) {
429
+ :root[data-mode="system"], :root[data-mode="system"] [data-world] {
430
+ --bg:var(--d-bg); --surface:var(--d-surface); --ink:var(--d-ink);
431
+ --ink-2:var(--d-ink-2); --ink-3:var(--d-ink-3); --rule:var(--d-rule);
432
+ --rule-soft:var(--d-rule-soft); --code-bg:var(--d-code);
433
+ --ah:var(--d-ah); --as:var(--d-as); --al:var(--d-al);
434
+ --flag:var(--d-flag); --ok:var(--d-ok);
435
+ }
436
+ }
437
+ :root[data-mode="dark"], :root[data-mode="dark"] [data-world] {
438
+ --bg:var(--d-bg); --surface:var(--d-surface); --ink:var(--d-ink);
439
+ --ink-2:var(--d-ink-2); --ink-3:var(--d-ink-3); --rule:var(--d-rule);
440
+ --rule-soft:var(--d-rule-soft); --code-bg:var(--d-code);
441
+ --ah:var(--d-ah); --as:var(--d-as); --al:var(--d-al);
442
+ --flag:var(--d-flag); --ok:var(--d-ok);
443
+ }
444
+
445
+ /* Accent is composed, so the hue slider moves one number and saturation and
446
+ lightness stay where the theme put them — which is what stops a dragged
447
+ hue quietly failing contrast. */
448
+ :root, [data-world] {
449
+ --accent: hsl(var(--ah) calc(var(--as) * 1%) calc(var(--al) * 1%));
450
+ --accent-soft: hsl(var(--ah) calc(var(--as) * 1%) calc(var(--al) * 1%) / 0.12);
451
+ --accent-line: hsl(var(--ah) calc(var(--as) * 1%) calc(var(--al) * 1%) / 0.42);
452
+ }
453
+
454
+ /* ---- motion, declared at its reduced value ----
455
+ Stillness is the default and movement is the enhancement, so a reader who
456
+ asked for less and a reader who said nothing get the same page. */
457
+ :root { --dur: 0ms; --slow: 0ms; --ease: cubic-bezier(.2,.75,.25,1); }
458
+ @media (prefers-reduced-motion: no-preference) {
459
+ :root { --dur: 220ms; --slow: 620ms; }
460
+ }
461
+
462
+ /* ---- density: two numbers the reader drags ---- */
463
+ :root { --space: 1; --size: 16.5px; }
464
+
465
+ body { font-family: var(--font-body); font-size: var(--size); }
466
+ h1, h2, h3, legend, .h { font-family: var(--font-head); }
467
+ /* The base sheet sizes the h1 through 'header.brief-head h1', which outranks a
468
+ bare 'h1' — so the world's own display scale has to be stated there too. */
469
+ header.brief-head h1, h1 {
470
+ font-size: var(--h1-size); line-height: var(--h1-lh);
471
+ font-weight: var(--head-weight); letter-spacing: var(--head-track);
472
+ }
473
+ h2 { font-weight: var(--head-weight); letter-spacing: var(--head-track); }
474
+ p, li { max-width: var(--measure); }
475
+
476
+ /* Prose stays inside --measure through 'p, li'; the page itself is wider so a
477
+ picker is not forced into one column. */
478
+ .wrap { max-width: calc(var(--measure) + 12rem);
479
+ padding: calc(3rem * var(--space)) 3rem calc(5rem * var(--space)); }
480
+ /* This rule sits after the base sheet's own narrow-screen padding and so
481
+ replaced it. On a 320 px screen that was 48 px of gutter each side — a
482
+ third of the width — until it was measured. */
483
+ @media (max-width: 40rem) {
484
+ .wrap { padding: calc(2.2rem * var(--space)) 1.15rem calc(4rem * var(--space)); }
485
+ }
486
+ main > * + * { margin-top: calc(1.05rem * var(--space)); }
487
+ h2 { margin-top: calc(2.4rem * var(--space)); }
488
+ h2 + * { margin-top: calc(0.8rem * var(--space)); }
489
+ .card { border-radius: var(--radius); box-shadow: var(--shadow);
490
+ padding: calc(1.1rem * var(--space)) 1.25rem; }
491
+ form { border-radius: var(--radius); box-shadow: var(--shadow); }
492
+ pre, code { border-radius: calc(var(--radius) * 0.35); }
493
+ th, .label { text-transform: var(--label-case); letter-spacing: var(--caps-track); }
494
+ hr, header.brief-head { border-color: var(--rule); }
495
+ header.brief-head { border-bottom-width: var(--rule-w); }
496
+
497
+ body { transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
498
+
499
+ /* ---- atmosphere -----------------------------------------------------
500
+ Every peak colour below is opaque and sits a few percent from its own
501
+ --bg, so the composite between them is bounded by two colours that can
502
+ both be measured. That is what makes an atmosphere layer checkable
503
+ rather than hoped about, and the page is complete with it off. */
504
+
505
+ [data-theme="paper"], [data-world="paper"] { --l-atmos-1:#fbfbfa; --l-atmos-2:#fbfbfa; --d-atmos-1:#121316; --d-atmos-2:#121316; }
506
+ [data-theme="terminal"], [data-world="terminal"] { --l-atmos-1:#f0f2ec; --l-atmos-2:#f2f4ef; --d-atmos-1:#0b110c; --d-atmos-2:#091009; }
507
+ [data-theme="atrium"], [data-world="atrium"] { --l-atmos-1:#fdf8ec; --l-atmos-2:#f2ecdf; --d-atmos-1:#1e1c13; --d-atmos-2:#100f0a; }
508
+ [data-theme="volume"], [data-world="volume"] { --l-atmos-1:#ffffff; --l-atmos-2:#e4e9f1; --d-atmos-1:#101927; --d-atmos-2:#04060a; }
509
+ [data-theme="bloom"], [data-world="bloom"] { --l-atmos-1:#fbe9f1; --l-atmos-2:#ebf1fd; --d-atmos-1:#241430; --d-atmos-2:#10182c; }
510
+
511
+ :root, [data-world] { --atmos-1:var(--l-atmos-1); --atmos-2:var(--l-atmos-2); }
512
+ @media (prefers-color-scheme: dark) {
513
+ :root[data-mode="system"], :root[data-mode="system"] [data-world] {
514
+ --atmos-1:var(--d-atmos-1); --atmos-2:var(--d-atmos-2);
515
+ }
516
+ }
517
+ :root[data-mode="dark"], :root[data-mode="dark"] [data-world] {
518
+ --atmos-1:var(--d-atmos-1); --atmos-2:var(--d-atmos-2);
519
+ }
520
+
521
+ html { background: var(--bg); }
522
+ body { background: none; }
523
+
524
+ .atmosphere { position: fixed; inset: 0; z-index: -1; pointer-events: none; }
525
+ :root[data-atmos="off"] .atmosphere { display: none; }
526
+
527
+ :root[data-theme="paper"] .atmosphere { display: none; }
528
+
529
+ /* terminal: a faint character grid, because that is what it is made of */
530
+ :root[data-theme="terminal"] .atmosphere {
531
+ background-image:
532
+ linear-gradient(to right, var(--atmos-1) 1px, transparent 1px),
533
+ linear-gradient(to bottom, var(--atmos-1) 1px, transparent 1px);
534
+ background-size: 1.1rem 1.65rem;
535
+ }
536
+ /* atrium: light from the upper left, and the floor falling away */
537
+ :root[data-theme="atrium"] .atmosphere {
538
+ background:
539
+ radial-gradient(70rem 42rem at 12% -12%, var(--atmos-1), transparent 68%),
540
+ linear-gradient(to bottom, transparent 55%, var(--atmos-2));
541
+ }
542
+ /* volume: one light source and a hard vignette, so the page reads as an object */
543
+ :root[data-theme="volume"] .atmosphere {
544
+ background:
545
+ radial-gradient(46rem 34rem at 50% -8%, var(--atmos-1), transparent 62%),
546
+ radial-gradient(90rem 70rem at 50% 120%, var(--atmos-2), transparent 70%);
547
+ }
548
+ /* bloom: mass and colour, nothing representational */
549
+ :root[data-theme="bloom"] .atmosphere {
550
+ background:
551
+ radial-gradient(32rem 32rem at 8% 4%, var(--atmos-1), transparent 62%),
552
+ radial-gradient(28rem 28rem at 96% 22%, var(--atmos-2), transparent 60%),
553
+ radial-gradient(38rem 26rem at 40% 108%, var(--atmos-1), transparent 66%);
554
+ }
555
+
556
+
557
+ /* ====================================================================
558
+ THE WORLD, APPLIED TO A PAGE
559
+
560
+ Above this line is the design system: five worlds, one resolver. Below
561
+ it is how a page written in plain semantic HTML picks the current world
562
+ up — no class names, because the authoring rule is that a page is plain
563
+ HTML and the conventions live here.
564
+
565
+ To change the whole system's look, change one attribute on <html>:
566
+ data-theme="paper | terminal | atrium | volume | bloom"
567
+ ==================================================================== */
568
+
569
+ /* ---- choosing: how a picked option reads, per world ---------------- */
570
+
571
+ form label:has(> input[type="radio"]),
572
+ form label:has(> input[type="checkbox"]) {
573
+ flex-wrap: wrap;
574
+ padding: 0.28rem 0.5rem;
575
+ margin-left: -0.5rem;
576
+ border-radius: calc(var(--radius) * 0.6);
577
+ transition: transform var(--dur) var(--ease), background-color var(--dur) var(--ease),
578
+ box-shadow var(--dur) var(--ease), color var(--dur) var(--ease);
579
+ }
580
+ /* A hint belongs under the option it qualifies, not squeezed beside it. */
581
+ form label:has(> input[type="radio"]) > small,
582
+ form label:has(> input[type="checkbox"]) > small {
583
+ flex: 0 0 100%; margin-left: 1.35rem; margin-top: 0.2rem;
584
+ }
585
+ form label:has(input:checked) { color: var(--ink); }
586
+
587
+ :root[data-theme="paper"] form label:has(input:checked) {
588
+ box-shadow: inset 2px 0 0 var(--accent);
589
+ }
590
+
591
+ /* terminal draws its own control, because a native radio is not made of
592
+ characters and everything else in this world is */
593
+ :root[data-theme="terminal"] form input[type="radio"],
594
+ :root[data-theme="terminal"] form input[type="checkbox"] { position: absolute; opacity: 0; }
595
+ :root[data-theme="terminal"] form label:has(> input[type="radio"])::before,
596
+ :root[data-theme="terminal"] form label:has(> input[type="checkbox"])::before {
597
+ content: "[ ]"; flex: none; color: var(--ink-3); letter-spacing: -0.05em;
598
+ }
599
+ :root[data-theme="terminal"] form label:has(input:checked)::before {
600
+ content: "[\2588]"; color: var(--accent);
601
+ }
602
+ :root[data-theme="terminal"] form label:has(input:checked) { background: var(--rule-soft); }
603
+ :root[data-theme="terminal"] form label:has(> input[type="radio"]) > small,
604
+ :root[data-theme="terminal"] form label:has(> input[type="checkbox"]) > small { margin-left: 2.1rem; }
605
+
606
+ :root[data-theme="atrium"] form label:has(input:checked) {
607
+ background: var(--surface); transform: translateY(-2px); box-shadow: var(--shadow);
608
+ }
609
+
610
+ :root[data-theme="volume"] form fieldset { perspective: 900px; }
611
+ :root[data-theme="volume"] form label:has(input:checked) {
612
+ background: var(--surface); box-shadow: var(--shadow);
613
+ border-left: 2px solid var(--accent);
614
+ transform: rotateY(-2.2deg) translateZ(16px);
615
+ }
616
+
617
+ :root[data-theme="bloom"] form label:has(> input[type="radio"]),
618
+ :root[data-theme="bloom"] form label:has(> input[type="checkbox"]) {
619
+ border-radius: 99px; margin-left: 0; padding: 0.34rem 0.85rem;
620
+ }
621
+ :root[data-theme="bloom"] form label:has(input:checked) {
622
+ background: var(--accent); color: var(--bg);
623
+ }
624
+
625
+ /* ---- committing: the primary action in the world's own material -----
626
+ Scoped to a form's own children so the dictation button, which lives
627
+ inside a .voice-field, keeps its own shape. */
628
+
629
+ form > button, form > p > button {
630
+ font: inherit; font-size: 0.9rem; font-weight: 640; cursor: pointer;
631
+ color: var(--bg); background: var(--accent);
632
+ border: var(--rule-w) solid var(--accent);
633
+ border-radius: calc(var(--radius) * 0.7);
634
+ padding: 0.55rem 1.15rem;
635
+ transition: transform var(--dur) var(--ease), filter var(--dur) var(--ease),
636
+ box-shadow var(--dur) var(--ease);
637
+ }
638
+ form > button + button, form > p > button + button { margin-left: 0.45rem; }
639
+ form > button:first-of-type, form > p > button:first-of-type {
640
+ color: var(--bg); background: var(--accent); border-color: var(--accent);
641
+ }
642
+ /* A second button is the alternative, not a rival. */
643
+ form > button:not(:first-of-type), form > p > button:not(:first-of-type) {
644
+ color: var(--accent); background: transparent; border-color: var(--rule);
645
+ }
646
+ form > button:hover:not(:disabled), form > p > button:hover:not(:disabled) { filter: brightness(1.08); }
647
+ form > button:disabled, form > p > button:disabled { opacity: 0.5; cursor: default; filter: none; }
648
+
649
+ :root[data-theme="terminal"] form > button:first-of-type,
650
+ :root[data-theme="terminal"] form > p > button:first-of-type {
651
+ background: var(--surface); color: var(--accent); border-color: var(--rule);
652
+ }
653
+ :root[data-theme="terminal"] form > button:hover:not(:disabled),
654
+ :root[data-theme="terminal"] form > p > button:hover:not(:disabled) { border-color: var(--accent); }
655
+
656
+ :root[data-theme="atrium"] form > button:first-of-type,
657
+ :root[data-theme="atrium"] form > p > button:first-of-type { border-radius: 99px; box-shadow: var(--shadow); }
658
+
659
+ /* volume commits by pressing an object, so the button is one: a lit face
660
+ over a darker edge that the press pushes into the surface */
661
+ :root[data-theme="volume"] form > button:first-of-type,
662
+ :root[data-theme="volume"] form > p > button:first-of-type {
663
+ box-shadow: 0 2px 0 hsl(var(--ah) calc(var(--as) * 1%) calc(var(--al) * 0.6%)),
664
+ 0 8px 16px -8px rgba(0, 0, 0, 0.55);
665
+ }
666
+ :root[data-theme="volume"] form > button:active:not(:disabled),
667
+ :root[data-theme="volume"] form > p > button:active:not(:disabled) {
668
+ transform: translateY(2px);
669
+ box-shadow: 0 0 0 hsl(var(--ah) calc(var(--as) * 1%) calc(var(--al) * 0.6%));
670
+ }
671
+
672
+ :root[data-theme="bloom"] form > button:first-of-type,
673
+ :root[data-theme="bloom"] form > p > button:first-of-type {
674
+ border-radius: 99px; padding: 0.6rem 1.35rem; font-weight: 700;
675
+ }
676
+ `;
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext",
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "skipLibCheck": true,
12
+ "types": ["node"]
13
+ },
14
+ "include": ["*.ts"]
15
+ }