@mystravil/hardpoint 0.2.0 → 0.3.0

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 (54) hide show
  1. package/CHANGELOG.md +51 -38
  2. package/LICENSE +25 -25
  3. package/README.md +67 -64
  4. package/dist/base.css +625 -625
  5. package/dist/components/AppShell.svelte +698 -698
  6. package/dist/components/Button.svelte +221 -174
  7. package/dist/components/Button.svelte.d.ts +18 -2
  8. package/dist/components/Checkbox.svelte +235 -235
  9. package/dist/components/CheckboxGroup.svelte +207 -207
  10. package/dist/components/Combobox.svelte +901 -901
  11. package/dist/components/Dialog.svelte +359 -359
  12. package/dist/components/Disclosure.svelte +201 -201
  13. package/dist/components/EmptyState.svelte +88 -88
  14. package/dist/components/ErrorState.svelte +135 -135
  15. package/dist/components/ImageViewer.svelte +764 -764
  16. package/dist/components/KeyValueList.svelte +114 -114
  17. package/dist/components/ListDetail.svelte +218 -218
  18. package/dist/components/LoadRegion.svelte +219 -219
  19. package/dist/components/MediaFrame.svelte +110 -110
  20. package/dist/components/MediaImage.svelte +256 -256
  21. package/dist/components/Menu.svelte +275 -258
  22. package/dist/components/MenuCheckboxItem.svelte +119 -119
  23. package/dist/components/MenuGroup.svelte +50 -50
  24. package/dist/components/MenuItem.svelte +94 -94
  25. package/dist/components/MenuRadioGroup.svelte +62 -62
  26. package/dist/components/MenuRadioItem.svelte +110 -110
  27. package/dist/components/MenuSeparator.svelte +28 -28
  28. package/dist/components/Meter.svelte +283 -283
  29. package/dist/components/NativeSelect.svelte +135 -135
  30. package/dist/components/Notice.svelte +262 -262
  31. package/dist/components/Popover.svelte +222 -222
  32. package/dist/components/Progress.svelte +157 -157
  33. package/dist/components/RadioGroup.svelte +440 -440
  34. package/dist/components/Select.svelte +471 -471
  35. package/dist/components/Skeleton.svelte +149 -149
  36. package/dist/components/Spinner.svelte +118 -118
  37. package/dist/components/Stat.svelte +253 -253
  38. package/dist/components/Steps.svelte +189 -189
  39. package/dist/components/Switch.svelte +222 -222
  40. package/dist/components/Table.svelte +504 -504
  41. package/dist/components/Tabs.svelte +246 -246
  42. package/dist/components/Tag.svelte +40 -40
  43. package/dist/components/TextArea.svelte +272 -272
  44. package/dist/components/TextField.svelte +396 -256
  45. package/dist/components/TextField.svelte.d.ts +16 -1
  46. package/dist/components/Timeline.svelte +156 -156
  47. package/dist/components/Toaster.svelte +411 -411
  48. package/dist/components/Tooltip.svelte +200 -200
  49. package/dist/components/TooltipGroup.svelte +46 -46
  50. package/dist/components/internal/Scrim.svelte +52 -52
  51. package/dist/components/internal/Sheet.svelte +263 -263
  52. package/dist/fonts.css +71 -71
  53. package/dist/tokens.css +283 -283
  54. package/package.json +81 -76
package/dist/tokens.css CHANGED
@@ -1,283 +1,283 @@
1
- /* Hardpoint tokens. Framework-free: plain custom properties and media queries,
2
- no Svelte selectors, so vanilla apps (omp-remote) can import this file.
3
- Dark by default, light through data-theme (see the light palette below).
4
- Colours come from omp-remote's apps/web/public/styles.css; --border-control,
5
- --surface-band and --surface-selected are new (see docs/BRIEF.md, Tokens).
6
-
7
- Breakpoints (custom properties cannot be used in @media, so they live here
8
- as a comment; components repeat the numbers):
9
- compact < 600px
10
- medium 600–899px
11
- expanded 900–1199px
12
- large >= 1200px */
13
-
14
- :root {
15
- color-scheme: dark;
16
-
17
- /* Surfaces. Major surfaces stay literal black. */
18
- --canvas: #000000;
19
- --surface: #000000;
20
- /* Small hover, active and expanded regions. */
21
- --surface-hover: #0a0a0a;
22
- /* The selected tint, under a 3px titanium edge. 1.17:1 on black, so it
23
- shows; --text 14:1 and --muted 7.4:1 on it. */
24
- --surface-selected: #191712;
25
- /* Bubbles, option lists and tracks that must read as a layer. */
26
- --surface-raised: #191712;
27
- /* The table header band: one filled strip with caps column labels.
28
- --text 9.3:1 and --muted 4.9:1 on it. */
29
- --surface-band: #3a3632;
30
- /* Dims the page behind a dialog or sheet. */
31
- --scrim: #000000cc;
32
-
33
- /* Inks. Two only: no third grey. */
34
- --text: #e6e3dc; /* 16.4:1 on black */
35
- --muted: #aaa69f; /* 8.7:1 on black */
36
-
37
- /* Titanium accent and focus. Not a hue. */
38
- --accent: #e6e3dc;
39
- --focus: #e6e3dc;
40
-
41
- /* Lines. --border is for dividers and decoration only (2.5:1); anything
42
- that identifies a control uses --border-control (3:1 or more). */
43
- --border: #514d48;
44
- --border-control: #625d57; /* 3.2:1 on black, 3.0:1 on --surface-hover */
45
-
46
- /* Status. Meaning only, never brand. */
47
- --danger: #ff3b35; /* errors and failures */
48
- --warn: #f2c14e; /* waiting, blocked, needs you */
49
- --ok: #35c46b; /* done, success, online */
50
-
51
- /* Glyphs drawn as backgrounds and masks. The chevron is a mask, painted
52
- in --muted. */
53
- --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m3 6 5 5 5-5' fill='none' stroke='%23AAA69F' stroke-width='1.5' stroke-linecap='square'/%3E%3C/svg%3E");
54
- --status-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M3 3h14v14H3zM10 9v5M10 6v1' fill='none' stroke='%23E6E3DC' stroke-width='1.5' stroke-linecap='square'/%3E%3C/svg%3E");
55
-
56
- /* Faces. Each has a fixed role; see base.css type roles. The fallback
57
- faces come from fonts.css: local fonts sized to Geist, so text keeps
58
- its width when Geist arrives. Without fonts.css they are skipped. */
59
- --sans: 'Geist', 'Geist Fallback Arial', 'Geist Fallback Roboto', sans-serif;
60
- --mono: 'Geist Mono', 'Geist Mono Fallback', monospace;
61
- --pixel: 'Geist Pixel Square', var(--mono);
62
-
63
- /* Type scale. Inputs are 16px at every width. 14px is for dense UI on
64
- fine pointers only. */
65
- --type-label: 12px; /* tracked caps labels */
66
- --type-caption: 13px;
67
- --type-dense: 14px;
68
- --type-body: 16px;
69
- --type-section: 20px;
70
- --type-page: 24px; /* 20px on compact */
71
- /* The view title in the shell header, in tracked caps. */
72
- --type-view: 20px; /* 16px on compact */
73
- /* The subject only: 32px up to 400px wide, 56px from 1200px. */
74
- --type-display: clamp(32px, 20px + 3vw, 56px);
75
- /* Geist Pixel draws on a 0.038em module; only multiples of 26.32px land
76
- the module on whole pixels. Line heights keep the baseline on a whole
77
- pixel (Blink rounds ascent 1005 and descent 295 per size). */
78
- --type-pixel: 26.32px;
79
- --type-pixel-leading: 32px;
80
- --type-pixel-lg: 52.63px;
81
- --type-pixel-lg-leading: 63px;
82
-
83
- /* The longest line of reading text: about 70 characters of English in
84
- Geist, at any font size. In em, not ch: Geist's zero is 0.663em wide,
85
- so 60ch held about 86 characters, and ch changes width when Geist
86
- replaces the fallback. */
87
- --measure: 32em;
88
-
89
- /* Caps labels. Set --label-transform: none to turn caps off. */
90
- --label-transform: uppercase;
91
- --label-tracking: 0.06em;
92
-
93
- /* Space: a 4px grid. Gaps between groups are at least twice the gap
94
- within a group. Vertical rhythm multiplies by --density. */
95
- --space-1: 4px;
96
- --space-2: 8px;
97
- --space-3: 12px;
98
- --space-4: 16px;
99
- --space-5: 20px;
100
- --space-6: 24px;
101
- --space-8: 32px;
102
- --space-10: 40px;
103
- --space-12: 48px;
104
- --space-16: 64px;
105
- /* Page edges, shared by every page. */
106
- --gutter: 16px;
107
- /* How far fills (table bands, row tints) reach past the text edge into
108
- the gutter, so text keeps the page edge while fills extend further. */
109
- --bleed: 16px;
110
- /* The shell's one header row, before the top safe area. Sticky elements
111
- below it offset by this; so does scroll-padding. */
112
- --header-height: 56px;
113
-
114
- /* Density scales vertical rhythm: 1 as designed, 0.625 compact. */
115
- --density: 1;
116
- /* Control height by pointer: 36px fine, 32px fine + compact, 44px coarse. */
117
- --control-height: 36px;
118
- /* Minimum hit area: 24px anywhere (WCAG 2.5.8), 44px on coarse pointers. */
119
- --hit-target: 24px;
120
-
121
- /* Motion. Durations by role (BRIEF, motion rule 4); exits are about 70
122
- percent of entrances. Responses ease out with --settle; never ease in.
123
- The JS transitions in ./motion read these from the element. */
124
- --settle: cubic-bezier(0.2, 0.7, 0.2, 1);
125
- --dur-press: 80ms; /* :active feedback */
126
- --dur-hover: 120ms; /* hover, fine pointers only */
127
- /* Short motion on frequent actions (decided 2026-11-11): short enough to
128
- be noticed, never in the way of daily use. Each one fades opacity or
129
- colour on --settle, only when a value changes, never on the first
130
- render or a mount. Quiet motion, so it runs in both tiers, and reduced
131
- motion keeps it because nothing moves. The user chose these values on
132
- /motion on 2026-11-11 ("smooth while being quick"); 0ms is a snap. */
133
- --dur-mark: 80ms; /* a selection mark fading in at its new place: checkbox check and dash, menu marks, switch thumb */
134
- --dur-panel: 100ms; /* a tab panel chosen by pointer or touch fading in; keys always cut */
135
- --dur-segment: 120ms; /* progress segments lighting or going out, a steps edge and tint */
136
- --dur-message: 120ms; /* a field error fading in with the well's edge; it clears at once */
137
- --dur-pop: 160ms; /* menus, popovers, select lists, tooltips */
138
- --dur-pop-exit: 110ms;
139
- --dur-sheet: 240ms; /* dialogs and sheets */
140
- --dur-sheet-exit: 170ms;
141
- /* Disclosure: the height snaps open and the content fades in from 0 over
142
- this time on --settle; closing cuts. The user chose height snaps and
143
- content fades on 2026-11-11. Only a fade, so it also runs under
144
- reduced motion. */
145
- --dur-expand: 120ms;
146
- /* Expressive stagger (motion rule 7): the first six items, 30ms apart,
147
- each entering in 150ms, so the sequence ends by 300ms. */
148
- --dur-enter: 150ms;
149
- --stagger-step: 30ms;
150
- /* Content replacing skeleton blocks (motion rule 2): LoadRegion
151
- crossfades from the blocks to the content over this time when the
152
- blocks were on screen. The user chose the fade over a cut on /motion
153
- (2026-11-10); 0ms makes it a cut. Only a fade, so it also runs under
154
- reduced motion. */
155
- --dur-reveal: 150ms;
156
- /* Number ticks (motion rule 5): old value to new on a change, never on
157
- load. The user chose --settle on 2026-11-10 over the reel's counter
158
- curve, cubic-bezier(0.37, 0, 0.63, 1). */
159
- --tick: var(--settle);
160
- --dur-tick: 400ms;
161
- /* Image resolve (motion rule 10), expressive tier only: an image that
162
- arrives after its skeleton showed sharpens from a mosaic in five steps
163
- of this length (MediaImage). One reel frame at 24 fps. */
164
- --resolve-step: 42ms;
165
- /* Layer scale (z-index): sticky content < shell < dialog and sheet < popover, menu, select < toast < tooltip. */
166
- --layer-sticky: 3;
167
- --layer-shell: 5;
168
- --layer-dialog: 40;
169
- --layer-popover: 50;
170
- /* The toast, over dialogs, sheets and popovers, so a toast fired from one shows; under tooltips. */
171
- --layer-toast: 55;
172
- --layer-tooltip: 60;
173
- /* Spinner: the glyph steps every 167ms (4 frames of the reel) and appears only after 300ms of waiting. */
174
- --spinner-step: 167ms;
175
- --spinner-delay: 300ms;
176
- /* Skeleton: blocks show only once a wait passes 300ms (motion rule 2). */
177
- --skeleton-delay: 300ms;
178
- /* AppShell navigation: the rail from 600px, the sidebar from 900px, the drawer's widest panel on compact. */
179
- --rail-width: 80px;
180
- --sidebar-width: 240px;
181
- --drawer-width: 320px;
182
- /* ListDetail: the list pane's width side by side, gutters included. The
183
- narrowest phone's width, so a list laid out for the phone screen fits
184
- the column as it is. */
185
- --list-detail-width: 320px;
186
- }
187
-
188
- /* Light palette (2026-11-11). An app picks the theme with data-theme on
189
- <html>: 'light', 'dark' or 'system' (follows the device). Without the
190
- attribute the theme is dark. Root only: overlays render in a portal on
191
- <body>, so a theme set on a subtree would not reach them.
192
- Values from omp-remote's light palette, except three tokens omp-remote
193
- lacks (--surface-selected, --surface-band, --border-control) and --ok,
194
- darkened from #1b7f45 so status text passes 4.5:1 on --surface-raised.
195
- On --canvas: --text 15.9:1, --muted 6.0:1, --border 2.5:1 (decoration),
196
- --border-control 3.4:1 (3.0:1 on --surface-hover). The chevron is a
197
- mask, so its stroke only matters to apps that paint it as an image.
198
- The two light blocks must stay identical. */
199
- :root[data-theme='light'] {
200
- color-scheme: light;
201
- --canvas: #f7f5f0;
202
- --surface: #f7f5f0;
203
- --surface-hover: #edeae3;
204
- /* 1.15:1 on --canvas, as the dark tint is 1.17:1 on black; --text 13.8:1
205
- and --muted 5.2:1 on it. */
206
- --surface-selected: #e9e5dc;
207
- --surface-raised: #e9e5dc;
208
- /* 1.30:1 on --canvas, the darkest band that keeps --muted at 4.5:1 or
209
- more (disabled buttons and pages): --text 12.2:1, --muted 4.6:1. */
210
- --surface-band: #ddd8cd;
211
- --scrim: #1c1a1780;
212
- --text: #1c1a17;
213
- --muted: #625d55;
214
- --accent: #1c1a17;
215
- --focus: #1c1a17;
216
- --border: #a39c92;
217
- --border-control: #8c857b;
218
- --danger: #c4211b; /* 5.4:1 on --canvas, 4.7:1 on --surface-raised */
219
- --warn: #8a5c00; /* 5.3:1, 4.6:1 */
220
- --ok: #18763f; /* 5.2:1, 4.5:1 */
221
- --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m3 6 5 5 5-5' fill='none' stroke='%23625D55' stroke-width='1.5' stroke-linecap='square'/%3E%3C/svg%3E");
222
- --status-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M3 3h14v14H3zM10 9v5M10 6v1' fill='none' stroke='%231C1A17' stroke-width='1.5' stroke-linecap='square'/%3E%3C/svg%3E");
223
- }
224
-
225
- @media (prefers-color-scheme: light) {
226
- :root[data-theme='system'] {
227
- color-scheme: light;
228
- --canvas: #f7f5f0;
229
- --surface: #f7f5f0;
230
- --surface-hover: #edeae3;
231
- --surface-selected: #e9e5dc;
232
- --surface-raised: #e9e5dc;
233
- --surface-band: #ddd8cd;
234
- --scrim: #1c1a1780;
235
- --text: #1c1a17;
236
- --muted: #625d55;
237
- --accent: #1c1a17;
238
- --focus: #1c1a17;
239
- --border: #a39c92;
240
- --border-control: #8c857b;
241
- --danger: #c4211b;
242
- --warn: #8a5c00;
243
- --ok: #18763f;
244
- --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m3 6 5 5 5-5' fill='none' stroke='%23625D55' stroke-width='1.5' stroke-linecap='square'/%3E%3C/svg%3E");
245
- --status-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M3 3h14v14H3zM10 9v5M10 6v1' fill='none' stroke='%231C1A17' stroke-width='1.5' stroke-linecap='square'/%3E%3C/svg%3E");
246
- }
247
- }
248
-
249
- /* Density applies to any subtree, not just the root, so a dense table can sit
250
- inside a comfortable page. */
251
- [data-density='compact'] {
252
- --density: 0.625;
253
- --control-height: 32px;
254
- }
255
-
256
- /* Compact density never shrinks touch targets. */
257
- @media (pointer: coarse) {
258
- :root,
259
- [data-density='compact'] {
260
- --control-height: 44px;
261
- --hit-target: 44px;
262
- }
263
- }
264
-
265
- @media (min-width: 600px) {
266
- :root {
267
- --gutter: 24px;
268
- }
269
- }
270
-
271
- @media (min-width: 900px) {
272
- :root {
273
- --gutter: 32px;
274
- }
275
- }
276
-
277
- @media (max-width: 599px) {
278
- :root {
279
- --type-page: 20px;
280
- --type-view: 16px;
281
- --bleed: 8px;
282
- }
283
- }
1
+ /* Hardpoint tokens. Framework-free: plain custom properties and media queries,
2
+ no Svelte selectors, so vanilla apps (omp-remote) can import this file.
3
+ Dark by default, light through data-theme (see the light palette below).
4
+ Colours come from omp-remote's apps/web/public/styles.css; --border-control,
5
+ --surface-band and --surface-selected are new (see docs/BRIEF.md, Tokens).
6
+
7
+ Breakpoints (custom properties cannot be used in @media, so they live here
8
+ as a comment; components repeat the numbers):
9
+ compact < 600px
10
+ medium 600–899px
11
+ expanded 900–1199px
12
+ large >= 1200px */
13
+
14
+ :root {
15
+ color-scheme: dark;
16
+
17
+ /* Surfaces. Major surfaces stay literal black. */
18
+ --canvas: #000000;
19
+ --surface: #000000;
20
+ /* Small hover, active and expanded regions. */
21
+ --surface-hover: #0a0a0a;
22
+ /* The selected tint, under a 3px titanium edge. 1.17:1 on black, so it
23
+ shows; --text 14:1 and --muted 7.4:1 on it. */
24
+ --surface-selected: #191712;
25
+ /* Bubbles, option lists and tracks that must read as a layer. */
26
+ --surface-raised: #191712;
27
+ /* The table header band: one filled strip with caps column labels.
28
+ --text 9.3:1 and --muted 4.9:1 on it. */
29
+ --surface-band: #3a3632;
30
+ /* Dims the page behind a dialog or sheet. */
31
+ --scrim: #000000cc;
32
+
33
+ /* Inks. Two only: no third grey. */
34
+ --text: #e6e3dc; /* 16.4:1 on black */
35
+ --muted: #aaa69f; /* 8.7:1 on black */
36
+
37
+ /* Titanium accent and focus. Not a hue. */
38
+ --accent: #e6e3dc;
39
+ --focus: #e6e3dc;
40
+
41
+ /* Lines. --border is for dividers and decoration only (2.5:1); anything
42
+ that identifies a control uses --border-control (3:1 or more). */
43
+ --border: #514d48;
44
+ --border-control: #625d57; /* 3.2:1 on black, 3.0:1 on --surface-hover */
45
+
46
+ /* Status. Meaning only, never brand. */
47
+ --danger: #ff3b35; /* errors and failures */
48
+ --warn: #f2c14e; /* waiting, blocked, needs you */
49
+ --ok: #35c46b; /* done, success, online */
50
+
51
+ /* Glyphs drawn as backgrounds and masks. The chevron is a mask, painted
52
+ in --muted. */
53
+ --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m3 6 5 5 5-5' fill='none' stroke='%23AAA69F' stroke-width='1.5' stroke-linecap='square'/%3E%3C/svg%3E");
54
+ --status-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M3 3h14v14H3zM10 9v5M10 6v1' fill='none' stroke='%23E6E3DC' stroke-width='1.5' stroke-linecap='square'/%3E%3C/svg%3E");
55
+
56
+ /* Faces. Each has a fixed role; see base.css type roles. The fallback
57
+ faces come from fonts.css: local fonts sized to Geist, so text keeps
58
+ its width when Geist arrives. Without fonts.css they are skipped. */
59
+ --sans: 'Geist', 'Geist Fallback Arial', 'Geist Fallback Roboto', sans-serif;
60
+ --mono: 'Geist Mono', 'Geist Mono Fallback', monospace;
61
+ --pixel: 'Geist Pixel Square', var(--mono);
62
+
63
+ /* Type scale. Inputs are 16px at every width. 14px is for dense UI on
64
+ fine pointers only. */
65
+ --type-label: 12px; /* tracked caps labels */
66
+ --type-caption: 13px;
67
+ --type-dense: 14px;
68
+ --type-body: 16px;
69
+ --type-section: 20px;
70
+ --type-page: 24px; /* 20px on compact */
71
+ /* The view title in the shell header, in tracked caps. */
72
+ --type-view: 20px; /* 16px on compact */
73
+ /* The subject only: 32px up to 400px wide, 56px from 1200px. */
74
+ --type-display: clamp(32px, 20px + 3vw, 56px);
75
+ /* Geist Pixel draws on a 0.038em module; only multiples of 26.32px land
76
+ the module on whole pixels. Line heights keep the baseline on a whole
77
+ pixel (Blink rounds ascent 1005 and descent 295 per size). */
78
+ --type-pixel: 26.32px;
79
+ --type-pixel-leading: 32px;
80
+ --type-pixel-lg: 52.63px;
81
+ --type-pixel-lg-leading: 63px;
82
+
83
+ /* The longest line of reading text: about 70 characters of English in
84
+ Geist, at any font size. In em, not ch: Geist's zero is 0.663em wide,
85
+ so 60ch held about 86 characters, and ch changes width when Geist
86
+ replaces the fallback. */
87
+ --measure: 32em;
88
+
89
+ /* Caps labels. Set --label-transform: none to turn caps off. */
90
+ --label-transform: uppercase;
91
+ --label-tracking: 0.06em;
92
+
93
+ /* Space: a 4px grid. Gaps between groups are at least twice the gap
94
+ within a group. Vertical rhythm multiplies by --density. */
95
+ --space-1: 4px;
96
+ --space-2: 8px;
97
+ --space-3: 12px;
98
+ --space-4: 16px;
99
+ --space-5: 20px;
100
+ --space-6: 24px;
101
+ --space-8: 32px;
102
+ --space-10: 40px;
103
+ --space-12: 48px;
104
+ --space-16: 64px;
105
+ /* Page edges, shared by every page. */
106
+ --gutter: 16px;
107
+ /* How far fills (table bands, row tints) reach past the text edge into
108
+ the gutter, so text keeps the page edge while fills extend further. */
109
+ --bleed: 16px;
110
+ /* The shell's one header row, before the top safe area. Sticky elements
111
+ below it offset by this; so does scroll-padding. */
112
+ --header-height: 56px;
113
+
114
+ /* Density scales vertical rhythm: 1 as designed, 0.625 compact. */
115
+ --density: 1;
116
+ /* Control height by pointer: 36px fine, 32px fine + compact, 44px coarse. */
117
+ --control-height: 36px;
118
+ /* Minimum hit area: 24px anywhere (WCAG 2.5.8), 44px on coarse pointers. */
119
+ --hit-target: 24px;
120
+
121
+ /* Motion. Durations by role (BRIEF, motion rule 4); exits are about 70
122
+ percent of entrances. Responses ease out with --settle; never ease in.
123
+ The JS transitions in ./motion read these from the element. */
124
+ --settle: cubic-bezier(0.2, 0.7, 0.2, 1);
125
+ --dur-press: 80ms; /* :active feedback */
126
+ --dur-hover: 120ms; /* hover, fine pointers only */
127
+ /* Short motion on frequent actions (decided 2026-11-11): short enough to
128
+ be noticed, never in the way of daily use. Each one fades opacity or
129
+ colour on --settle, only when a value changes, never on the first
130
+ render or a mount. Quiet motion, so it runs in both tiers, and reduced
131
+ motion keeps it because nothing moves. The user chose these values on
132
+ /motion on 2026-11-11 ("smooth while being quick"); 0ms is a snap. */
133
+ --dur-mark: 80ms; /* a selection mark fading in at its new place: checkbox check and dash, menu marks, switch thumb */
134
+ --dur-panel: 100ms; /* a tab panel chosen by pointer or touch fading in; keys always cut */
135
+ --dur-segment: 120ms; /* progress segments lighting or going out, a steps edge and tint */
136
+ --dur-message: 120ms; /* a field error fading in with the well's edge; it clears at once */
137
+ --dur-pop: 160ms; /* menus, popovers, select lists, tooltips */
138
+ --dur-pop-exit: 110ms;
139
+ --dur-sheet: 240ms; /* dialogs and sheets */
140
+ --dur-sheet-exit: 170ms;
141
+ /* Disclosure: the height snaps open and the content fades in from 0 over
142
+ this time on --settle; closing cuts. The user chose height snaps and
143
+ content fades on 2026-11-11. Only a fade, so it also runs under
144
+ reduced motion. */
145
+ --dur-expand: 120ms;
146
+ /* Expressive stagger (motion rule 7): the first six items, 30ms apart,
147
+ each entering in 150ms, so the sequence ends by 300ms. */
148
+ --dur-enter: 150ms;
149
+ --stagger-step: 30ms;
150
+ /* Content replacing skeleton blocks (motion rule 2): LoadRegion
151
+ crossfades from the blocks to the content over this time when the
152
+ blocks were on screen. The user chose the fade over a cut on /motion
153
+ (2026-11-10); 0ms makes it a cut. Only a fade, so it also runs under
154
+ reduced motion. */
155
+ --dur-reveal: 150ms;
156
+ /* Number ticks (motion rule 5): old value to new on a change, never on
157
+ load. The user chose --settle on 2026-11-10 over the reel's counter
158
+ curve, cubic-bezier(0.37, 0, 0.63, 1). */
159
+ --tick: var(--settle);
160
+ --dur-tick: 400ms;
161
+ /* Image resolve (motion rule 10), expressive tier only: an image that
162
+ arrives after its skeleton showed sharpens from a mosaic in five steps
163
+ of this length (MediaImage). One reel frame at 24 fps. */
164
+ --resolve-step: 42ms;
165
+ /* Layer scale (z-index): sticky content < shell < dialog and sheet < popover, menu, select < toast < tooltip. */
166
+ --layer-sticky: 3;
167
+ --layer-shell: 5;
168
+ --layer-dialog: 40;
169
+ --layer-popover: 50;
170
+ /* The toast, over dialogs, sheets and popovers, so a toast fired from one shows; under tooltips. */
171
+ --layer-toast: 55;
172
+ --layer-tooltip: 60;
173
+ /* Spinner: the glyph steps every 167ms (4 frames of the reel) and appears only after 300ms of waiting. */
174
+ --spinner-step: 167ms;
175
+ --spinner-delay: 300ms;
176
+ /* Skeleton: blocks show only once a wait passes 300ms (motion rule 2). */
177
+ --skeleton-delay: 300ms;
178
+ /* AppShell navigation: the rail from 600px, the sidebar from 900px, the drawer's widest panel on compact. */
179
+ --rail-width: 80px;
180
+ --sidebar-width: 240px;
181
+ --drawer-width: 320px;
182
+ /* ListDetail: the list pane's width side by side, gutters included. The
183
+ narrowest phone's width, so a list laid out for the phone screen fits
184
+ the column as it is. */
185
+ --list-detail-width: 320px;
186
+ }
187
+
188
+ /* Light palette (2026-11-11). An app picks the theme with data-theme on
189
+ <html>: 'light', 'dark' or 'system' (follows the device). Without the
190
+ attribute the theme is dark. Root only: overlays render in a portal on
191
+ <body>, so a theme set on a subtree would not reach them.
192
+ Values from omp-remote's light palette, except three tokens omp-remote
193
+ lacks (--surface-selected, --surface-band, --border-control) and --ok,
194
+ darkened from #1b7f45 so status text passes 4.5:1 on --surface-raised.
195
+ On --canvas: --text 15.9:1, --muted 6.0:1, --border 2.5:1 (decoration),
196
+ --border-control 3.4:1 (3.0:1 on --surface-hover). The chevron is a
197
+ mask, so its stroke only matters to apps that paint it as an image.
198
+ The two light blocks must stay identical. */
199
+ :root[data-theme='light'] {
200
+ color-scheme: light;
201
+ --canvas: #f7f5f0;
202
+ --surface: #f7f5f0;
203
+ --surface-hover: #edeae3;
204
+ /* 1.15:1 on --canvas, as the dark tint is 1.17:1 on black; --text 13.8:1
205
+ and --muted 5.2:1 on it. */
206
+ --surface-selected: #e9e5dc;
207
+ --surface-raised: #e9e5dc;
208
+ /* 1.30:1 on --canvas, the darkest band that keeps --muted at 4.5:1 or
209
+ more (disabled buttons and pages): --text 12.2:1, --muted 4.6:1. */
210
+ --surface-band: #ddd8cd;
211
+ --scrim: #1c1a1780;
212
+ --text: #1c1a17;
213
+ --muted: #625d55;
214
+ --accent: #1c1a17;
215
+ --focus: #1c1a17;
216
+ --border: #a39c92;
217
+ --border-control: #8c857b;
218
+ --danger: #c4211b; /* 5.4:1 on --canvas, 4.7:1 on --surface-raised */
219
+ --warn: #8a5c00; /* 5.3:1, 4.6:1 */
220
+ --ok: #18763f; /* 5.2:1, 4.5:1 */
221
+ --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m3 6 5 5 5-5' fill='none' stroke='%23625D55' stroke-width='1.5' stroke-linecap='square'/%3E%3C/svg%3E");
222
+ --status-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M3 3h14v14H3zM10 9v5M10 6v1' fill='none' stroke='%231C1A17' stroke-width='1.5' stroke-linecap='square'/%3E%3C/svg%3E");
223
+ }
224
+
225
+ @media (prefers-color-scheme: light) {
226
+ :root[data-theme='system'] {
227
+ color-scheme: light;
228
+ --canvas: #f7f5f0;
229
+ --surface: #f7f5f0;
230
+ --surface-hover: #edeae3;
231
+ --surface-selected: #e9e5dc;
232
+ --surface-raised: #e9e5dc;
233
+ --surface-band: #ddd8cd;
234
+ --scrim: #1c1a1780;
235
+ --text: #1c1a17;
236
+ --muted: #625d55;
237
+ --accent: #1c1a17;
238
+ --focus: #1c1a17;
239
+ --border: #a39c92;
240
+ --border-control: #8c857b;
241
+ --danger: #c4211b;
242
+ --warn: #8a5c00;
243
+ --ok: #18763f;
244
+ --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m3 6 5 5 5-5' fill='none' stroke='%23625D55' stroke-width='1.5' stroke-linecap='square'/%3E%3C/svg%3E");
245
+ --status-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M3 3h14v14H3zM10 9v5M10 6v1' fill='none' stroke='%231C1A17' stroke-width='1.5' stroke-linecap='square'/%3E%3C/svg%3E");
246
+ }
247
+ }
248
+
249
+ /* Density applies to any subtree, not just the root, so a dense table can sit
250
+ inside a comfortable page. */
251
+ [data-density='compact'] {
252
+ --density: 0.625;
253
+ --control-height: 32px;
254
+ }
255
+
256
+ /* Compact density never shrinks touch targets. */
257
+ @media (pointer: coarse) {
258
+ :root,
259
+ [data-density='compact'] {
260
+ --control-height: 44px;
261
+ --hit-target: 44px;
262
+ }
263
+ }
264
+
265
+ @media (min-width: 600px) {
266
+ :root {
267
+ --gutter: 24px;
268
+ }
269
+ }
270
+
271
+ @media (min-width: 900px) {
272
+ :root {
273
+ --gutter: 32px;
274
+ }
275
+ }
276
+
277
+ @media (max-width: 599px) {
278
+ :root {
279
+ --type-page: 20px;
280
+ --type-view: 16px;
281
+ --bleed: 8px;
282
+ }
283
+ }