efficient-shell 0.0.0 → 4.126.3

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 (121) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +758 -3
  3. package/dist/Browser-LM4RRZIL.js +478 -0
  4. package/dist/Browser-LM4RRZIL.js.map +1 -0
  5. package/dist/Calculator-HCKFTSJR.js +188 -0
  6. package/dist/Calculator-HCKFTSJR.js.map +1 -0
  7. package/dist/ConfirmDialog-LJXMB45A.js +3 -0
  8. package/dist/ConfirmDialog-LJXMB45A.js.map +1 -0
  9. package/dist/CurrencyConverter-BKKORN6K.js +232 -0
  10. package/dist/CurrencyConverter-BKKORN6K.js.map +1 -0
  11. package/dist/Documents-DD3QI5ER.js +437 -0
  12. package/dist/Documents-DD3QI5ER.js.map +1 -0
  13. package/dist/Files-BOPCPMJE.js +801 -0
  14. package/dist/Files-BOPCPMJE.js.map +1 -0
  15. package/dist/Notepad-KJOLF63O.js +406 -0
  16. package/dist/Notepad-KJOLF63O.js.map +1 -0
  17. package/dist/PdfViewer-LSP3YNFC.js +4 -0
  18. package/dist/PdfViewer-LSP3YNFC.js.map +1 -0
  19. package/dist/PomodoroTimer-SVJ62RXM.js +629 -0
  20. package/dist/PomodoroTimer-SVJ62RXM.js.map +1 -0
  21. package/dist/Preview-5UOC7ZRU.js +2955 -0
  22. package/dist/Preview-5UOC7ZRU.js.map +1 -0
  23. package/dist/Sidebar-J76PV4QF.js +290 -0
  24. package/dist/Sidebar-J76PV4QF.js.map +1 -0
  25. package/dist/Spreadsheet-J7BH7UHC.js +515 -0
  26. package/dist/Spreadsheet-J7BH7UHC.js.map +1 -0
  27. package/dist/Stock-CDUS4VCZ.js +154 -0
  28. package/dist/Stock-CDUS4VCZ.js.map +1 -0
  29. package/dist/Weather-EEDBWBPQ.js +427 -0
  30. package/dist/Weather-EEDBWBPQ.js.map +1 -0
  31. package/dist/WorldClock-FFFULVTJ.js +198 -0
  32. package/dist/WorldClock-FFFULVTJ.js.map +1 -0
  33. package/dist/apps/index.d.ts +267 -0
  34. package/dist/apps/index.js +147 -0
  35. package/dist/apps/index.js.map +1 -0
  36. package/dist/brand.css +2 -0
  37. package/dist/chunk-2VM2FRZJ.js +215 -0
  38. package/dist/chunk-2VM2FRZJ.js.map +1 -0
  39. package/dist/chunk-32W6VY2J.js +549 -0
  40. package/dist/chunk-32W6VY2J.js.map +1 -0
  41. package/dist/chunk-3CUSBZWG.js +59 -0
  42. package/dist/chunk-3CUSBZWG.js.map +1 -0
  43. package/dist/chunk-3L463T5H.js +26 -0
  44. package/dist/chunk-3L463T5H.js.map +1 -0
  45. package/dist/chunk-4TVV2BIU.js +13334 -0
  46. package/dist/chunk-4TVV2BIU.js.map +1 -0
  47. package/dist/chunk-6XQHH7D2.js +105 -0
  48. package/dist/chunk-6XQHH7D2.js.map +1 -0
  49. package/dist/chunk-7CCHEEYC.js +94 -0
  50. package/dist/chunk-7CCHEEYC.js.map +1 -0
  51. package/dist/chunk-7KIOEKHS.js +617 -0
  52. package/dist/chunk-7KIOEKHS.js.map +1 -0
  53. package/dist/chunk-AXWC3332.js +1103 -0
  54. package/dist/chunk-AXWC3332.js.map +1 -0
  55. package/dist/chunk-BFBWLCO6.js +16 -0
  56. package/dist/chunk-BFBWLCO6.js.map +1 -0
  57. package/dist/chunk-CT3HVZRZ.js +26 -0
  58. package/dist/chunk-CT3HVZRZ.js.map +1 -0
  59. package/dist/chunk-D7PYW2QS.js +265 -0
  60. package/dist/chunk-D7PYW2QS.js.map +1 -0
  61. package/dist/chunk-DMCFOWOJ.js +153 -0
  62. package/dist/chunk-DMCFOWOJ.js.map +1 -0
  63. package/dist/chunk-FQB24B42.js +416 -0
  64. package/dist/chunk-FQB24B42.js.map +1 -0
  65. package/dist/chunk-GA35NEND.js +56 -0
  66. package/dist/chunk-GA35NEND.js.map +1 -0
  67. package/dist/chunk-HNH7FOYB.js +69 -0
  68. package/dist/chunk-HNH7FOYB.js.map +1 -0
  69. package/dist/chunk-HYZ5QMVO.js +462 -0
  70. package/dist/chunk-HYZ5QMVO.js.map +1 -0
  71. package/dist/chunk-I464GYJJ.js +17 -0
  72. package/dist/chunk-I464GYJJ.js.map +1 -0
  73. package/dist/chunk-JFQGPY7W.js +819 -0
  74. package/dist/chunk-JFQGPY7W.js.map +1 -0
  75. package/dist/chunk-MGGY74RY.js +14 -0
  76. package/dist/chunk-MGGY74RY.js.map +1 -0
  77. package/dist/chunk-N3CG5HG5.js +144 -0
  78. package/dist/chunk-N3CG5HG5.js.map +1 -0
  79. package/dist/chunk-QRGFTK2X.js +79 -0
  80. package/dist/chunk-QRGFTK2X.js.map +1 -0
  81. package/dist/chunk-SZ4WMHUX.js +7 -0
  82. package/dist/chunk-SZ4WMHUX.js.map +1 -0
  83. package/dist/chunk-W6XTG23K.js +3708 -0
  84. package/dist/chunk-W6XTG23K.js.map +1 -0
  85. package/dist/chunk-YG45DAUT.js +96 -0
  86. package/dist/chunk-YG45DAUT.js.map +1 -0
  87. package/dist/chunk-YNGPTOL6.js +46 -0
  88. package/dist/chunk-YNGPTOL6.js.map +1 -0
  89. package/dist/chunk-YNHLQ24V.js +400 -0
  90. package/dist/chunk-YNHLQ24V.js.map +1 -0
  91. package/dist/chunk-YNKK7NE3.js +171 -0
  92. package/dist/chunk-YNKK7NE3.js.map +1 -0
  93. package/dist/chunk-YPO6RDMX.js +20 -0
  94. package/dist/chunk-YPO6RDMX.js.map +1 -0
  95. package/dist/file-intake/index.d.ts +87 -0
  96. package/dist/file-intake/index.js +3 -0
  97. package/dist/file-intake/index.js.map +1 -0
  98. package/dist/index-dQia6NpB.d.ts +5904 -0
  99. package/dist/index.d.ts +2495 -0
  100. package/dist/index.js +7460 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/markdown/index.d.ts +118 -0
  103. package/dist/markdown/index.js +184 -0
  104. package/dist/markdown/index.js.map +1 -0
  105. package/dist/markup/index.d.ts +235 -0
  106. package/dist/markup/index.js +135 -0
  107. package/dist/markup/index.js.map +1 -0
  108. package/dist/page-tabs/index.d.ts +154 -0
  109. package/dist/page-tabs/index.js +706 -0
  110. package/dist/page-tabs/index.js.map +1 -0
  111. package/dist/shell.css +105 -0
  112. package/dist/sounds-NT4DEZGD.js +3 -0
  113. package/dist/sounds-NT4DEZGD.js.map +1 -0
  114. package/dist/styles.css +22 -0
  115. package/dist/themes.css +249 -0
  116. package/dist/types-BOinJXUf.d.ts +118 -0
  117. package/dist/ui/index.d.ts +4 -0
  118. package/dist/ui/index.js +15 -0
  119. package/dist/ui/index.js.map +1 -0
  120. package/dist/ui.css +1518 -0
  121. package/package.json +172 -3
package/dist/ui.css ADDED
@@ -0,0 +1,1518 @@
1
+ /**
2
+ * react-os-shell/ui.css — styles for the UI kit, without the window chrome.
3
+ *
4
+ * Import this when you take `react-os-shell/ui`:
5
+ *
6
+ * import "react-os-shell/ui.css";
7
+ *
8
+ * Import EXACTLY ONE of this and `react-os-shell/styles.css` — the latter is
9
+ * the umbrella over this file plus shell.css, so taking both doubles every rule.
10
+ *
11
+ * You supply Tailwind v4 yourself (`@import "tailwindcss"` in your own entry,
12
+ * which every consumer already does). This file deliberately does NOT import
13
+ * it: a consumer mid-migration off another component library may need the
14
+ * utility and theme layers WITHOUT preflight, whose bare element selectors
15
+ * would otherwise out-specify that library's `:where()`-wrapped reset.
16
+ *
17
+ * Scope — what dark mode actually is here:
18
+ * - the accent theme variants, via themes.css below
19
+ * - [data-theme="dark"] overrides of Tailwind UTILITY CLASS NAMES. There are
20
+ * no `dark:` variants in this package; you write the light class and the
21
+ * remap happens to you. Escaped-important classes (.\!bg-blue-50) and alpha
22
+ * variants (.bg-white\/70) compile to different class names, so each needs
23
+ * its own rule — that is why the list is long rather than clever.
24
+ * - the reduce-transparency escape hatch, form-control dark styling, the
25
+ * coarse-pointer 16px rule that defeats iOS focus-zoom, StatusBadge tones,
26
+ * and scrollbars.
27
+ */
28
+
29
+ @import "./themes.css";
30
+
31
+ /*
32
+ * Shape and type, as tokens the consuming app inherits.
33
+ *
34
+ * Every value here equals Tailwind's own default, so adding this changes
35
+ * nothing that renders. What it changes is WHERE the value comes from: a
36
+ * portal that deletes its own `@theme` block now takes its radius and its
37
+ * typeface from this package, and one edit here moves all of them.
38
+ *
39
+ * A consumer's `@theme` still wins — Tailwind merges the blocks and the later
40
+ * declaration replaces this one — so a portal that wants its own shape keeps
41
+ * it by saying so, rather than by this file not having an opinion.
42
+ *
43
+ * "Later" is a REQUIREMENT, not an observation: import this file ABOVE your own
44
+ * `@theme`. A portal that declares `--font-sans` and then imports the kit below
45
+ * it has its typeface silently reset to the system stack, with nothing in the
46
+ * diff to explain it. All five consuming portals import the kit near the top of
47
+ * `index.css` and declare after it, which is why this reads as a no-op today.
48
+ *
49
+ * On type: the token is here, the TYPEFACE is not. Shipping a face means
50
+ * hosting and licensing it, which is a product decision rather than a
51
+ * packaging one. Until that is made, a portal that sets nothing gets the
52
+ * system stack — the same thing it got before this block existed.
53
+ */
54
+ @theme {
55
+ --radius-sm: 0.25rem;
56
+ --radius-md: 0.375rem;
57
+ --radius-lg: 0.5rem;
58
+ --radius-xl: 0.75rem;
59
+ --radius-2xl: 1rem;
60
+
61
+ --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
62
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
63
+ }
64
+
65
+ /* Tailwind v4 doesn't scan node_modules by default — tell it to scan this
66
+ package's compiled JS so utility classes used in kit components get
67
+ generated when consumers import react-os-shell/ui.css. */
68
+ @source "./**/*.js";
69
+
70
+ /* Tailwind scans every text file it can reach, prose included, so a class
71
+ NAMED in the README, the CHANGELOG or a contributor guide is compiled into
72
+ the bundle as if a component used it. That is not academic: `h-[440px]` and
73
+ `bg-[#abc]`, which .design-sync/conventions.md cites as examples of values
74
+ that render as NOTHING, were both real classes in the shipped stylesheet.
75
+ Worse downstream — harness 52-brand-surface.md treats the compiled bundle as
76
+ the published vocabulary, so a passing mention in prose silently widened the
77
+ organisation's brand contract. Excluding the documentation dropped 21 such
78
+ classes and none the code uses. Excluding a path that does not exist is not
79
+ an error, so these hold for a consumer's node_modules copy too. */
80
+ @source not "../*.md";
81
+ @source not "../docs/**";
82
+ @source not "../.design-sync/**";
83
+
84
+ /* The default `utils/glass.ts` already assumes at `getMenuOpacity()`'s
85
+ fallback. Declared here so a ui-only consumer gets identical glass to a shell
86
+ consumer at rest, and has one knob to dial it. <Layout> overrides it inline
87
+ for shell consumers; reduce-transparency still beats both. */
88
+ :root {
89
+ --menu-opacity: 0.95;
90
+ }
91
+
92
+ /* ── Reduce transparency (Preferences → Customization) ──────────────────────
93
+ * When the user turns on "Reduce transparency", Layout adds this class to
94
+ * <html>. Two effects, applied globally so every present-and-future glass
95
+ * surface is covered without per-component edits:
96
+ * 1. Force every window/taskbar/menu opacity variable fully opaque, so the
97
+ * surfaces that tint via `rgb(... / var(--*-opacity))` become solid. The
98
+ * `!important` beats Layout's inline (non-important) custom-property writes.
99
+ * 2. Strip backdrop-filter everywhere — the blur is the GPU-expensive part on
100
+ * older machines. `!important` overrides both Tailwind `backdrop-blur-*`
101
+ * utilities and inline `backdropFilter` from glassStyle(). glassStyle()
102
+ * also returns a solid background under this class so menus/popups don't
103
+ * read as washed-out translucent panels once the blur is gone. */
104
+ html.rosh-reduce-transparency {
105
+ --active-header-opacity: 1 !important;
106
+ --active-content-opacity: 1 !important;
107
+ --inactive-header-opacity: 1 !important;
108
+ --inactive-content-opacity: 1 !important;
109
+ --menu-opacity: 1 !important;
110
+ --taskbar-opacity: 1 !important;
111
+ }
112
+ html.rosh-reduce-transparency *,
113
+ html.rosh-reduce-transparency *::before,
114
+ html.rosh-reduce-transparency *::after {
115
+ backdrop-filter: none !important;
116
+ -webkit-backdrop-filter: none !important;
117
+ }
118
+
119
+ /* ═══════════════════════════════════════════════════
120
+ The neutral ramp, light
121
+ ═══════════════════════════════════════════════════ */
122
+ /*
123
+ * The dark block below has named these twelve roles since 4.16; light named
124
+ * none of them, so `var(--ink)` resolved to nothing outside a dark theme.
125
+ * That is the same shape the --viz-* palette further down already gets right:
126
+ * declared for both themes, read by role.
127
+ *
128
+ * Adding light values is render-neutral for existing consumers — no rule
129
+ * outside a `[data-theme="dark"]` scope reads any of these twelve, in CSS or
130
+ * in TSX. It exists so a surface built WITHOUT the kit's utility classes —
131
+ * a report, an emailed document, a standalone page — can read the same roles
132
+ * the portals do. That surface is governed by harness 52-brand-surface.md and
133
+ * loads `react-os-shell/brand.css`.
134
+ *
135
+ * The values are the Tailwind steps the dark block opposite already remaps:
136
+ * white/gray-50/gray-100 surfaces, gray-100/200/300 lines, gray-900 down to
137
+ * gray-400 ink. Keep the two blocks in step.
138
+ */
139
+ :root {
140
+ --surface: #ffffff;
141
+ --surface-sunken: #f9fafb;
142
+ --surface-raised: #f3f4f6;
143
+ --line-subtle: #f3f4f6;
144
+ --line: #e5e7eb;
145
+ --line-strong: #d1d5db;
146
+ --ink-faintest: #9ca3af;
147
+ --ink-faint: #6b7280;
148
+ --ink-muted: #4b5563;
149
+ --ink: #374151;
150
+ --ink-strong: #1f2937;
151
+ --ink-strongest: #111827;
152
+ }
153
+
154
+ /* ═══════════════════════════════════════════════════
155
+ Theme: Dark
156
+ ═══════════════════════════════════════════════════ */
157
+ [data-theme="dark"] {
158
+ color-scheme: dark;
159
+
160
+ /*
161
+ * The neutral ramp, as variables.
162
+ *
163
+ * Every rule below reads these rather than a literal, so a consumer that
164
+ * wants a different dark — warmer, higher contrast, its own brand — sets
165
+ * twelve values instead of forking 100 rules. The status hues (red for
166
+ * danger, amber for warning) stay literal on purpose: they mean the same
167
+ * thing in every product, and a consumer redefining "danger" is a different
168
+ * conversation from one restyling its greys.
169
+ *
170
+ * The names say what a step DOES rather than which Tailwind number it
171
+ * replaces, because in a light theme the numbers run the other way round
172
+ * while the roles do not.
173
+ */
174
+ --surface: #1e1e2e;
175
+ --surface-sunken: #181825;
176
+ --surface-raised: #313244;
177
+ --line-subtle: #2a2a3c;
178
+ --line: #313244;
179
+ --line-strong: #45475a;
180
+ --ink-faintest: #6c7086;
181
+ /* 4.79:1 on --surface. Was #7f849c at 4.44:1 — under AA, and this step
182
+ is what `.text-gray-500` renders as in dark, so it carries real text. */
183
+ --ink-faint: #848aa2;
184
+ /*
185
+ * Currently unreachable, and worth knowing before you set it.
186
+ *
187
+ * Its only consumer is `.text-gray-600` below, which a later rule overrides
188
+ * to `--ink` so short taskbar-button labels stay readable on dark glass.
189
+ * That override is global by design — shell.css's docblock spells out why a
190
+ * bare utility selector cannot live there — so `text-gray-600` renders
191
+ * `--ink` everywhere and this step renders nowhere.
192
+ *
193
+ * Left declared rather than deleted: it names a real rung, and a consumer
194
+ * scoping its own `.text-gray-600` rule can reach it. Deleting it would be a
195
+ * rendering change, which this release deliberately is not.
196
+ */
197
+ --ink-muted: #9399b2;
198
+ --ink: #a6adc8;
199
+ --ink-strong: #bac2de;
200
+ --ink-strongest: #cdd6f4;
201
+ }
202
+
203
+ /* ── Chart tokens ───────────────────────────────────────────────────────────
204
+ * Eight categorical slots, a reserved status set, and the chart inks. Declared
205
+ * BELOW the neutral ramp on purpose: the dark chart inks read the ramp, so a
206
+ * consumer restyling its greys moves the grid and axis with them.
207
+ *
208
+ * Deliberately NOT remapped by themes.css. Every other accent token in this
209
+ * package follows the user's chosen theme; these must not. A series colour is
210
+ * an identity encoding — "4xx is amber" — and an identity that changes when
211
+ * someone picks the pink theme has stopped being one. See charts/palette.ts
212
+ * for the slot-order rule and the colour-vision validation these steps passed.
213
+ *
214
+ * The light steps are validated against white, the dark ones against
215
+ * `--surface`. They are the same eight hues re-stepped for the darker band,
216
+ * not a second palette. */
217
+ :root {
218
+ --viz-series-1: #2a78d6;
219
+ --viz-series-2: #eb6834;
220
+ --viz-series-3: #1baf7a;
221
+ --viz-series-4: #eda100;
222
+ --viz-series-5: #e87ba4;
223
+ --viz-series-6: #008300;
224
+ --viz-series-7: #4a3aa7;
225
+ --viz-series-8: #e34948;
226
+
227
+ --viz-good: #1baf7a;
228
+ --viz-neutral: #6b7280;
229
+ --viz-warning: #eda100;
230
+ --viz-serious: #eb6834;
231
+ --viz-critical: #e34948;
232
+
233
+ --viz-grid: #e5e7eb;
234
+ --viz-axis: #d1d5db;
235
+ --viz-label: #6b7280;
236
+ --viz-muted: #9ca3af;
237
+ --viz-surface: #ffffff;
238
+ }
239
+
240
+ [data-theme="dark"] {
241
+ --viz-series-1: #3987e5;
242
+ --viz-series-2: #d95926;
243
+ --viz-series-3: #199e70;
244
+ --viz-series-4: #c98500;
245
+ --viz-series-5: #d55181;
246
+ --viz-series-6: #008300;
247
+ --viz-series-7: #9085e9;
248
+ --viz-series-8: #e66767;
249
+
250
+ --viz-good: #199e70;
251
+ --viz-neutral: #7f849c;
252
+ --viz-warning: #c98500;
253
+ --viz-serious: #d95926;
254
+ --viz-critical: #e66767;
255
+
256
+ --viz-grid: var(--line-subtle);
257
+ --viz-axis: var(--line-strong);
258
+ --viz-label: var(--ink-faint);
259
+ --viz-muted: var(--ink-faintest);
260
+ --viz-surface: var(--surface);
261
+ }
262
+
263
+ /* ── Status tokens ──────────────────────────────────────────────────────────
264
+ * The nine semantic groups (`SemanticGroup` in shell/StatusBadge.tsx), as two
265
+ * registers a caller picks between with one prop rather than by hand-writing
266
+ * colours: SOFT for a badge inside a dense row, SOLID for the same fact in a
267
+ * detail header.
268
+ *
269
+ * WHY THE SOFT FILL IS TRANSPARENT. A status used to be an opaque tint —
270
+ * `bg-green-100` — calibrated against one backdrop: the flat white surface. On
271
+ * a raised panel or a hovered row that tint is still the same flat patch, and
272
+ * it reads as a hole rather than as a badge, because it does not know what is
273
+ * behind it. A wash of the group's own hue at a fixed alpha composites with
274
+ * whatever it sits on, so ONE value is right on `--surface`, `--surface-raised`
275
+ * and a hovered row alike. That is also why each `-soft` is declared once and
276
+ * never re-declared for dark: a transparent wash has no theme, the surface
277
+ * under it does.
278
+ *
279
+ * WHY THE INK IS NOT THE `-text` ROLE. The obvious ink for a status is
280
+ * `--success-text` / `--warning-text` / `--danger-text` from brand.css, which
281
+ * are calibrated to clear 4.5:1 on the FLAT surface. Over the wash they do not,
282
+ * and the worst backdrop (`--surface-raised`) is the one nobody checks:
283
+ *
284
+ * light --success-text #15803d 4.02:1 --warning-text #b45309 3.91:1
285
+ * --danger-text #dc2626 3.59:1 --accent-text #1d4ed8 5.12:1
286
+ * dark --success-text #16a34a 2.82:1 --warning-text #f59e0b 4.58:1
287
+ * --danger-text #f87171 3.86:1 --accent-text #60a5fa 4.00:1
288
+ *
289
+ * Six of those eight are under AA. So the soft tier carries its own ink, moved
290
+ * one step along the same hue until the WORST of the three backdrops clears
291
+ * 4.5:1. The floor it actually achieves is 5.67:1 light and 5.39:1 dark;
292
+ * `tests/statusSoftTier.test.ts` measures every pair on every backdrop and
293
+ * fails the build under 4.5, so the tier cannot drift back under.
294
+ *
295
+ * `draft` and `neutral` share the grey hue and differ by alpha, which is the
296
+ * distinction they have always had (`bg-gray-300` against `bg-gray-100`): a
297
+ * draft is a real state and reads slightly louder than a cancelled one.
298
+ */
299
+ :root {
300
+ --status-success-soft: rgba(34, 197, 94, 0.16);
301
+ --status-active-soft: rgba(59, 130, 246, 0.16);
302
+ --status-queued-soft: rgba(99, 102, 241, 0.16);
303
+ --status-info-soft: rgba(14, 165, 233, 0.16);
304
+ --status-pending-soft: rgba(234, 179, 8, 0.16);
305
+ --status-warning-soft: rgba(249, 115, 22, 0.16);
306
+ --status-danger-soft: rgba(239, 68, 68, 0.16);
307
+ --status-draft-soft: rgba(107, 114, 128, 0.22);
308
+ --status-neutral-soft: rgba(107, 114, 128, 0.10);
309
+
310
+ /* Light ink: the -800 step of each family. The -700 steps are what the
311
+ `-text` roles use, and they are the ones measured above at 3.59–4.02. */
312
+ --status-success-soft-ink: #166534;
313
+ --status-active-soft-ink: #1e40af;
314
+ --status-queued-soft-ink: #3730a3;
315
+ --status-info-soft-ink: #075985;
316
+ --status-pending-soft-ink: #854d0e;
317
+ --status-warning-soft-ink: #9a3412;
318
+ --status-danger-soft-ink: #991b1b;
319
+ --status-draft-soft-ink: var(--ink-strong);
320
+ --status-neutral-soft-ink: var(--ink);
321
+
322
+ /* The loud register. A saturated -700 fill with white on it, literal in both
323
+ themes for the same reason `--danger` is: a status means the same thing in
324
+ every product, and a solid badge is read at a glance rather than against a
325
+ surface. Banner's `emphasis="solid"` is the same decision one size up. */
326
+ --status-success-solid: #15803d;
327
+ --status-active-solid: #1d4ed8;
328
+ --status-queued-solid: #4338ca;
329
+ --status-info-solid: #0369a1;
330
+ --status-pending-solid: #a16207;
331
+ --status-warning-solid: #c2410c;
332
+ --status-danger-solid: #b91c1c;
333
+ --status-draft-solid: #374151;
334
+ --status-neutral-solid: #4b5563;
335
+ /* 4.92:1 (pending) to 10.31:1 (draft) — the whole solid tier clears AA. */
336
+ --status-on-solid: #ffffff;
337
+ }
338
+
339
+ /* Only the ink moves. The washes above hold, because they are transparent. */
340
+ [data-theme="dark"] {
341
+ --status-success-soft-ink: #86efac;
342
+ --status-active-soft-ink: #93c5fd;
343
+ --status-queued-soft-ink: #a5b4fc;
344
+ --status-info-soft-ink: #7dd3fc;
345
+ --status-pending-soft-ink: #fde68a;
346
+ --status-warning-soft-ink: #fdba74;
347
+ --status-danger-soft-ink: #fca5a5;
348
+ --status-draft-soft-ink: var(--ink-strongest);
349
+ --status-neutral-soft-ink: var(--ink-strong);
350
+ }
351
+
352
+ /* ── Timeline accent tokens ─────────────────────────────────────────────────
353
+ * Every colour a `TimelineTrack` paints with, and the ink that reads on top of
354
+ * one. There is ONE of them.
355
+ *
356
+ * "The timeline's blue is not our theme blue; it must be the theme colour, and
357
+ * pure flat blue — no skeuomorphic shading — consistent with everything else"
358
+ * (Henry, 2026-09-14, translated from his Chinese ruling). The bar used to mix
359
+ * three blues that were all nearly right and none of them the theme's: the
360
+ * `blue-500` utilities on the fill and the accent marks, `--status-active-solid`
361
+ * (#1d4ed8, which no accent remap reaches) behind the kind tokens, and
362
+ * `--status-success-solid` behind a completion. The theme's blue is the 600
363
+ * step — what the primary `Button` wears, what a link wears, what an active tab
364
+ * underlines with — so that is what all of them are now.
365
+ *
366
+ * A completion no longer keeps a hue of its own either: the CHECK GLYPH is what
367
+ * says it finished, and shape survives a greyscale print and a colour-blind
368
+ * reader in a way a second hue never did. The same argument already retired the
369
+ * five bespoke kind hues (a violet shipment, an amber DFM, an orange
370
+ * inspection) — a diamond is goods moving, a flask is a test, a document is a
371
+ * drawing revision.
372
+ *
373
+ * THE VALUE, and why it is a chain rather than a token:
374
+ *
375
+ * var(--accent-600, var(--color-blue-600, #2563eb))
376
+ *
377
+ * --accent-600 what `useTheme` stamps inline on <html> when the user
378
+ * picks a custom accent, and what `themes.css` remaps the
379
+ * `blue-600` utilities to under `[data-custom-accent]`. A
380
+ * mark and a primary Button therefore move together.
381
+ * --color-blue-600 Tailwind v4's own theme variable, which is LITERALLY what
382
+ * `.bg-blue-600` resolves to — so with no custom accent the
383
+ * two are the same computed value, not two values that look
384
+ * alike. It is not #2563eb: v4 states the palette in OKLCH
385
+ * and `blue-600` renders rgb(21 93 252).
386
+ * #2563eb the last resort — Tailwind v3's blue-600, and the value
387
+ * `brand.css` gives `--accent-600` — for a consumer that
388
+ * ships neither of the above.
389
+ *
390
+ * `ui.css` cannot declare `--accent-600` itself: a portal takes this file (or
391
+ * `styles.css`) WITHOUT `brand.css`, and declaring a default here would shadow
392
+ * nothing but would also stop `brand.css` from ever being the answer. The
393
+ * fallback chain is the portable form.
394
+ *
395
+ * NO DARK STEP. `.bg-blue-600` and `.border-blue-600` are not remapped for dark
396
+ * — a primary Button is the same fill on a dark page — so neither is the bar.
397
+ * The 600 step measures 3.40:1 on the dark `--surface-sunken` and 4.95:1 on the
398
+ * light one, clearing WCAG 1.4.11 (these are graphics, not text) in both, and
399
+ * `--tl-on-kind` is white ink on it at 5.17:1 either way.
400
+ *
401
+ * The one thing that DOES move in dark is ink the kit paints through
402
+ * `.text-blue-600` — the thumb, a cluster pill, the links — because blue-600 as
403
+ * TEXT on a dark surface is 3.17:1 and fails AA. `ui.css` already remaps that
404
+ * class to #93c5fd for dark, and the timeline inherits it rather than inventing
405
+ * a step of its own. */
406
+ :root {
407
+ /* The one colour. Everything below is an alias, kept as separate names
408
+ because a consumer retuning ONE kind is a conversation worth having in a
409
+ stylesheet rather than a fork of the component. */
410
+ --tl-accent: var(--accent-600, var(--color-blue-600, #2563eb));
411
+ --tl-dfm: var(--tl-accent);
412
+ --tl-shipment: var(--tl-accent);
413
+ --tl-testing: var(--tl-accent);
414
+ --tl-completion: var(--tl-accent);
415
+ --tl-inspection: var(--tl-accent);
416
+ --tl-invoice: var(--tl-accent);
417
+ /* The ink on a filled mark. `--on-accent` is white in both themes by
418
+ `brand.css`'s own contract — the accent fill does not move, so neither does
419
+ what reads on it. */
420
+ --tl-on-kind: var(--on-accent, #ffffff);
421
+ /* The focus ring, and the colour a hot label's name and date take. */
422
+ --tl-focus: var(--tl-accent);
423
+ /* The soft wash behind the current mark, a hovered dot and the mark a thumb
424
+ has just crossed. A box-shadow cannot be a utility class, so the accent is
425
+ mixed down here instead — 16% is the alpha the kit's own `-soft` status
426
+ washes carry, and `color-mix` is the only form of it that can follow a
427
+ custom accent, since `rgba()` cannot take a variable for its channels. */
428
+ --tl-soft: color-mix(in srgb, var(--tl-accent) 16%, transparent);
429
+ }
430
+
431
+ /* ── Budget overrun hatch ───────────────────────────────────────────────────
432
+ * `BudgetBar`'s third drawing. A run past its wall clock must not be pictured
433
+ * as a full bar: a full solid bar is the calm "finished" shape, and this is the
434
+ * opposite of that. The overrun segment is therefore HATCHED rather than
435
+ * filled — a texture no in-budget state ever wears, so the difference survives
436
+ * a glance, a greyscale screenshot and a colour-blind reader alike.
437
+ *
438
+ * A gradient rather than an image: no asset, no request, and it reads the
439
+ * danger tokens so it follows the theme. `background-color` under it keeps the
440
+ * band visible when a print stylesheet drops background images.
441
+ */
442
+ .rosh-budget-overrun {
443
+ background-color: var(--status-danger-soft);
444
+ background-image: repeating-linear-gradient(
445
+ 135deg,
446
+ var(--status-danger-solid) 0,
447
+ var(--status-danger-solid) 2px,
448
+ transparent 2px,
449
+ transparent 5px
450
+ );
451
+ }
452
+
453
+ /* ── Chart motion ───────────────────────────────────────────────────────────
454
+ * Every animation the chart family uses, as classes, so no chart implements
455
+ * motion itself and none of them can forget the reduced-motion rule below.
456
+ *
457
+ * The intro reveal is a WIPE: a white rect inside an SVG <mask>, scaled from
458
+ * nothing to full. Everything under the mask — fill, stroke and point markers
459
+ * alike — appears as it passes, which is what stops a chart assembling in
460
+ * pieces the way a stroke-dashoffset reveal does (that one can only animate
461
+ * strokes, so the fill and the dots arrive on their own schedule).
462
+ *
463
+ * The mask uses objectBoundingBox units, so the rect is 0–1 and one keyframe
464
+ * set works at every chart size.
465
+ *
466
+ * `fill-mode: both` matters on every rule: with a stagger delay the mark would
467
+ * otherwise render at its FINAL state, jump back to the start when its delay
468
+ * elapsed, and animate from there. */
469
+ @keyframes rosh-viz-wipe { from { transform: scaleX(0); } to { transform: scaleX(1); } }
470
+ @keyframes rosh-viz-rise { from { transform: scaleY(0); } to { transform: scaleY(1); } }
471
+ @keyframes rosh-viz-grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
472
+ @keyframes rosh-viz-sweep { from { opacity: 0; transform: rotate(-14deg) scale(0.94); } to { opacity: 1; transform: none; } }
473
+ @keyframes rosh-viz-fade { from { opacity: 0; } to { opacity: 1; } }
474
+ @keyframes rosh-viz-march { to { stroke-dashoffset: -24; } }
475
+ /* The loading sweep. Translated by twice its own width so the band clears the
476
+ plot at both ends rather than popping at the edges. */
477
+ @keyframes rosh-viz-shimmer { from { transform: translateX(0); } to { transform: translateX(200%); } }
478
+ /* The live-reading ping. Fades as it grows, so the halo is gone before it is
479
+ large enough to reach the neighbouring point. */
480
+ @keyframes rosh-viz-ping { from { transform: scale(1); opacity: 0.45; } to { transform: scale(3.2); opacity: 0; } }
481
+
482
+ .rosh-viz-wipe,
483
+ .rosh-viz-wipe-rtl,
484
+ .rosh-viz-wipe-center {
485
+ transform-box: fill-box;
486
+ animation: rosh-viz-wipe 900ms cubic-bezier(0.22, 1, 0.36, 1) both;
487
+ }
488
+ .rosh-viz-wipe { transform-origin: left; }
489
+ .rosh-viz-wipe-rtl { transform-origin: right; }
490
+ .rosh-viz-wipe-center { transform-origin: center; }
491
+
492
+ /* Grow from the baseline, not the centre — a bar that expands both ways reads
493
+ as a range rather than as a quantity. `fill-box` puts the origin on the rect
494
+ itself rather than on the viewport. */
495
+ .rosh-viz-rise { transform-box: fill-box; transform-origin: bottom; animation: rosh-viz-rise 640ms cubic-bezier(0.22, 1, 0.36, 1) both; }
496
+ /* A horizontal bar grows from its label edge, which is where the eye starts. */
497
+ .rosh-viz-grow { transform-origin: left; animation: rosh-viz-grow 620ms cubic-bezier(0.22, 1, 0.36, 1) both; }
498
+ .rosh-viz-sweep { transform-box: fill-box; transform-origin: center; animation: rosh-viz-sweep 620ms cubic-bezier(0.22, 1, 0.36, 1) both; }
499
+ .rosh-viz-fade { animation: rosh-viz-fade 700ms ease-out both; }
500
+ .rosh-viz-march { animation: rosh-viz-march 1s linear infinite; }
501
+ .rosh-viz-shimmer { animation: rosh-viz-shimmer 1.8s ease-in-out infinite; }
502
+ .rosh-viz-ping { transform-box: fill-box; transform-origin: center; animation: rosh-viz-ping 1.6s cubic-bezier(0, 0, 0.2, 1) infinite; }
503
+
504
+ /* Hover emphasis is a TRANSITION, not a jump. A mark that snaps to grey the
505
+ instant the pointer crosses a band reads as a glitch; 160ms is long enough to
506
+ register as a response and short enough not to lag the pointer. Transitioning
507
+ `filter` too is what makes the glow arrive with the colour rather than after
508
+ it. */
509
+ .rosh-viz-mark { transition: fill 160ms ease-out, fill-opacity 160ms ease-out, opacity 160ms ease-out, filter 200ms ease-out; }
510
+
511
+ /* One rule covers the whole family, present and future. A chart cannot opt out
512
+ of this, because no chart wrote the animation in the first place. */
513
+ @media (prefers-reduced-motion: reduce) {
514
+ .rosh-viz-mark { transition: none !important; }
515
+ .rosh-viz-wipe, .rosh-viz-wipe-rtl, .rosh-viz-wipe-center,
516
+ .rosh-viz-rise, .rosh-viz-grow, .rosh-viz-sweep, .rosh-viz-fade,
517
+ .rosh-viz-march {
518
+ animation: none !important;
519
+ opacity: 1 !important;
520
+ transform: none !important;
521
+ }
522
+ /* These two REST invisible — the shimmer exits at translateX(200%) and the
523
+ ping fades to nothing — so the entrance treatment above would park the
524
+ shimmer band fully lit over the skeleton and pin the ping halo at an
525
+ opacity it never reaches while animating. They stop by disappearing. */
526
+ .rosh-viz-shimmer, .rosh-viz-ping {
527
+ animation: none !important;
528
+ opacity: 0 !important;
529
+ }
530
+ }
531
+
532
+ /* ── Timeline track ─────────────────────────────────────────────────────────
533
+ * `TimelineTrack`, and through it `MilestoneTimeline` and `ProductionTimeline`.
534
+ *
535
+ * Written as classes here rather than as Tailwind utilities in the component
536
+ * for the same reason the chart family is: the whole card is drawn from ONE
537
+ * geometry object, and a dozen absolutely-positioned bands that have to agree
538
+ * with each other are clearer as named parts than as class strings. It is also
539
+ * what makes the motion impossible to get wrong — no component writes an
540
+ * animation, so none of them can forget the reduced-motion rule at the bottom.
541
+ *
542
+ * Colour comes from the neutral ramp and the `--tl-*` accent tokens, so both
543
+ * themes are covered without a single `[data-theme="dark"]` rule. The two
544
+ * exceptions are deliberate and live in the component: `bg-gray-200` for the
545
+ * rail and `bg-blue-600` for the fill, because those two must follow the dark
546
+ * remap and the user's accent respectively.
547
+ *
548
+ * FLAT, and kept that way by `tests/timelineTokens.test.ts`: no gradient and no
549
+ * drop shadow anywhere in this block. The fill carried a white gloss along its
550
+ * top edge and three elements carried a soft drop shadow (the reveal chip, the
551
+ * bubble and the popover, the scrubber thumb); all of them are gone, and the
552
+ * border or the flat ring that was underneath each one does the separating on
553
+ * its own. The `0 0 0 Npx` box-shadows that remain are RINGS — a halo in the
554
+ * card's own ground, a focus ring, the soft accent wash — which is a flat
555
+ * colour at a radius, not a shadow. Nothing else in the kit wears a gradient
556
+ * fill, and `StatusBadge` wears no shadow at all. */
557
+
558
+ /* The card chrome both timelines wear. Border WIDTH and STYLE only — the colour
559
+ comes from the utility class on the element, which carries the dark remap. */
560
+ .rosh-tl-card {
561
+ border-width: 1px; border-style: solid; border-radius: 12px; padding: 14px 16px 16px;
562
+ }
563
+ .rosh-tl-head { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 14px; }
564
+ /* The sentence: icon, title, facts. It wraps inside itself and may shrink, so
565
+ the controls beside it never get pushed onto a row of their own. */
566
+ .rosh-tl-head-main {
567
+ display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
568
+ flex: 1 1 auto; min-width: 0;
569
+ }
570
+ .rosh-tl-ico { flex: none; width: 15px; height: 15px; align-self: center; margin-right: -3px; }
571
+ .rosh-tl-ico svg { width: 15px; height: 15px; }
572
+ .rosh-tl-title { margin: 0; font-size: 13px; font-weight: 600; letter-spacing: -0.005em; }
573
+ /* `min-width: 0` is what lets a long meta line WRAP rather than push the
574
+ actions onto a row of their own: a flex item's default minimum is its
575
+ content, and five facts about a production order are wider than a card. */
576
+ .rosh-tl-meta { margin: 0; font-size: 12px; font-variant-numeric: tabular-nums; min-width: 0; }
577
+ .rosh-tl-meta b { font-weight: 500; }
578
+ .rosh-tl-head-actions {
579
+ display: inline-flex; align-items: center; gap: 10px; margin-left: auto; flex: none;
580
+ }
581
+
582
+ /* The play control: a pill, not an icon button — a 20px circle beside an
583
+ uppercase heading is what the two cards used to disagree about. */
584
+ .rosh-tl-play {
585
+ display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 12px 0 10px;
586
+ border-radius: 14px; cursor: pointer; white-space: nowrap;
587
+ font: inherit; font-size: 11.5px; font-weight: 500;
588
+ background: var(--surface-raised); border-width: 1px; border-style: solid;
589
+ transition: color 160ms ease-out, border-color 160ms ease-out;
590
+ }
591
+ .rosh-tl-play svg { width: 9px; height: 9px; fill: currentColor; }
592
+
593
+ .rosh-tl-foot {
594
+ display: flex; align-items: center; gap: 14px 20px; flex-wrap: wrap;
595
+ margin-top: 14px; padding-top: 12px;
596
+ border-top-width: 1px; border-top-style: solid;
597
+ }
598
+ .rosh-tl-status { margin: 0; font-size: 12px; font-variant-numeric: tabular-nums; }
599
+ .rosh-tl-legend { display: flex; gap: 6px; flex-wrap: wrap; margin-left: auto; }
600
+ .rosh-tl-legend > span {
601
+ display: inline-flex; align-items: center; gap: 6px; height: 22px; padding: 0 9px 0 7px;
602
+ border-width: 1px; border-style: solid; border-radius: 11px;
603
+ font-size: 10.5px; background: var(--surface-raised);
604
+ }
605
+ /* The legend's glyphs are the track's glyphs at legend size, so a chip can
606
+ never describe a shape the rail no longer draws. */
607
+ .rosh-tl-glyph {
608
+ width: 10px; height: 10px; flex: none; box-sizing: border-box;
609
+ display: flex; align-items: center; justify-content: center;
610
+ }
611
+ .rosh-tl-glyph.is-ring { border-radius: 50%; border-width: 2px; border-style: solid; }
612
+ .rosh-tl-glyph.is-diamond { border-radius: 2px; transform: rotate(45deg) scale(0.82); }
613
+ .rosh-tl-glyph.is-disc { border-radius: 50%; }
614
+ .rosh-tl-glyph svg { width: 7px; height: 7px; }
615
+ /* Upright and smaller inside a chip's diamond, for the reason the rail's
616
+ diamond gives — at legend size there is even less room in the corners. */
617
+ .rosh-tl-glyph.is-diamond svg { width: 6px; height: 6px; transform: rotate(-45deg); }
618
+
619
+ .rosh-tl-body { display: grid; grid-template-columns: minmax(0, 1fr) 156px; gap: 20px; }
620
+ .rosh-tl-body.is-solo { grid-template-columns: minmax(0, 1fr); }
621
+ /* The caption on an end of the axis — "Start · 20/01/2026". Inside the stage
622
+ rather than flanking it, so it sits over the coordinate it names instead of
623
+ beside a bar whose left edge it could only point at. Mono, like every other
624
+ date the card draws. */
625
+ .rosh-tl-edge {
626
+ position: absolute; white-space: nowrap;
627
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums;
628
+ font-size: 10px; line-height: 1.3; color: var(--ink-muted);
629
+ }
630
+ .rosh-tl-edge.is-end { transform: translateX(-100%); }
631
+
632
+ /* A 10px side inset, so a dot on the first or last day is never clipped. */
633
+ .rosh-tl-stage { position: relative; padding: 0 10px; flex: 1 1 auto; min-width: 0; }
634
+ .rosh-tl-layer { position: absolute; left: 10px; right: 10px; top: 0; bottom: 0; }
635
+
636
+ /* The off-layout copy of every label, measured so the packer has real widths. */
637
+ .rosh-tl-measure {
638
+ position: absolute; left: 0; top: 0; display: flex; gap: 8px;
639
+ visibility: hidden; pointer-events: none;
640
+ }
641
+ .rosh-tl-measure-box { flex: none; display: flex; flex-direction: column; white-space: nowrap; }
642
+
643
+ .rosh-tl-rail { position: absolute; left: 0; right: 0; border-radius: 999px; }
644
+ .rosh-tl-fill {
645
+ position: absolute; left: 0; border-radius: 999px;
646
+ transform-origin: left center;
647
+ }
648
+ /* The notch in the rail where the axis stops keeping time. Painted in the
649
+ card's own ground, so it reads as a cut rather than as a mark. */
650
+ .rosh-tl-cut {
651
+ position: absolute; width: 7px; transform: translateX(-50%);
652
+ background: var(--surface-sunken);
653
+ }
654
+ .rosh-tl-cut i, .rosh-tl-cutglyph i {
655
+ position: absolute; top: 0; width: 1px; height: 100%;
656
+ background: var(--line-strong); transform: skewX(-28deg);
657
+ }
658
+ .rosh-tl-cut i:first-child { left: 2px; }
659
+ .rosh-tl-cut i:last-child { left: 5px; }
660
+
661
+ .rosh-tl-tick { position: absolute; width: 1px; background: var(--line-strong); opacity: 0.85; }
662
+ .rosh-tl-tick.is-week { opacity: 0.5; }
663
+ .rosh-tl-month, .rosh-tl-rulercut span {
664
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums;
665
+ font-size: 10px; line-height: 1.1; color: var(--ink-faint);
666
+ }
667
+ .rosh-tl-month { position: absolute; white-space: nowrap; }
668
+ .rosh-tl-rulercut {
669
+ position: absolute; display: flex; flex-direction: column; align-items: center; gap: 3px;
670
+ transform: translateX(-50%); white-space: nowrap;
671
+ }
672
+ .rosh-tl-cutglyph { position: relative; width: 7px; height: 7px; }
673
+ .rosh-tl-cutglyph i:first-child { left: 1px; }
674
+ .rosh-tl-cutglyph i:last-child { left: 4px; }
675
+
676
+ .rosh-tl-todayline {
677
+ position: absolute; width: 0; border-left: 1px dashed var(--line-strong);
678
+ transform: translateX(-0.5px);
679
+ }
680
+ .rosh-tl-todaytag {
681
+ position: absolute; white-space: nowrap; border-radius: 5px; padding: 1px 6px;
682
+ font-size: 10px; font-weight: 500; letter-spacing: 0.02em;
683
+ color: var(--ink-muted); background: var(--surface-raised);
684
+ }
685
+
686
+ /* The list is the accessible reading of the card; the dots are absolutely
687
+ positioned against the layer, so the <li> must not establish a containing
688
+ block of its own. */
689
+ .rosh-tl-nodes { list-style: none; margin: 0; padding: 0; }
690
+ .rosh-tl-nodes > li { position: static; }
691
+ .rosh-tl-node {
692
+ position: absolute; padding: 0; border: 0; background: none; cursor: pointer;
693
+ display: flex; align-items: center; justify-content: center; border-radius: 50%;
694
+ transform: var(--rosh-tl-shift, translate(-50%, -50%))
695
+ rotate(var(--rosh-tl-rot, 0deg)) scale(var(--rosh-tl-sc, 1));
696
+ }
697
+ .rosh-tl-node svg { width: 10px; height: 10px; }
698
+ /* The halo is the CARD's ground, not white: a pure-white ring is a defect on a
699
+ dark card, and this one is right in both themes without a remap. */
700
+ .rosh-tl-node.is-dot {
701
+ width: 10px; height: 10px; background: var(--surface); border-width: 2px; border-style: solid;
702
+ box-shadow: 0 0 0 2px var(--surface-sunken);
703
+ }
704
+ .rosh-tl-node.is-key { width: 16px; height: 16px; box-shadow: 0 0 0 2px var(--surface-sunken); }
705
+ /* "You are here" wears a soft halo as well as the ground ring — it is the one
706
+ mark a reader looks for before reading anything. */
707
+ .rosh-tl-node.is-current {
708
+ width: 14px; height: 14px;
709
+ box-shadow: 0 0 0 2px var(--surface-sunken), 0 0 0 6px var(--tl-soft);
710
+ }
711
+ .rosh-tl-node.is-current svg { width: 9px; height: 9px; }
712
+ .rosh-tl-node.is-diamond { border-radius: 3px; width: 14px; height: 14px; --rosh-tl-rot: 45deg; }
713
+ /* A glyph inside a diamond is turned back upright, and drawn a pixel smaller
714
+ than one in a disc: the square a 14px diamond inscribes is under 10px across,
715
+ so the 10px a disc gives its glyph would touch the rotated corners. */
716
+ .rosh-tl-node.is-diamond svg { width: 9px; height: 9px; transform: rotate(-45deg); }
717
+ .rosh-tl-node.is-ghost {
718
+ width: 10px; height: 10px; background: var(--surface);
719
+ border: 1.5px dashed var(--line-strong); box-shadow: none; cursor: default;
720
+ }
721
+ /* A provisional mark: the kind's shape and glyph drawn hollow, on the best date
722
+ the record holds for something that has not happened — a sample ORDERED,
723
+ where a shipment will be. The outline says what it will be; the empty middle
724
+ says it is not that yet. The inline style sets the border colour. */
725
+ .rosh-tl-node.is-provisional {
726
+ background: var(--surface); border-width: 2px; border-style: solid;
727
+ }
728
+ /* Lit by the consumer — a stepper step hovered above the bar names the marks
729
+ it covers. The same soft wash the current mark wears, at the same radius, so
730
+ "these" and "here" are one vocabulary. */
731
+ .rosh-tl-node.is-lit { box-shadow: 0 0 0 2px var(--surface-sunken), 0 0 0 5px var(--tl-soft); }
732
+ /* The band under the marks a consumer lit: one soft stretch of the accent, the
733
+ rail's own height with room around it, rounded at both ends. */
734
+ .rosh-tl-band { position: absolute; border-radius: 999px; background: var(--tl-soft); pointer-events: none; }
735
+
736
+ /* ── The bare rail (`labels="none"`) ──────────────────────────────────────
737
+ No label lanes. The edge captions flank the rail on its own row, the only
738
+ text over the rail is a mark's caption, and the undated things are a dashed
739
+ cap at the end of a dashed tail. */
740
+ .rosh-tl-flank {
741
+ flex: none; white-space: nowrap; font-size: 11px; font-weight: 500; line-height: 1;
742
+ color: var(--ink-muted);
743
+ }
744
+ /* The caption over a mark — `v3` over a revision. Mono, like every other
745
+ small text the rail draws, and never a pointer target: the mark under it is. */
746
+ .rosh-tl-caption {
747
+ position: absolute; transform: translateX(-50%); white-space: nowrap; pointer-events: none;
748
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums;
749
+ font-size: 10px; line-height: 1.1; color: var(--ink-muted);
750
+ }
751
+ /* The tail: from the end of the axis to the cap. Dashed, because it is not
752
+ time — it is the distance to a date nobody has. */
753
+ .rosh-tl-tail { position: absolute; height: 0; border-top: 2px dashed var(--line-strong); }
754
+ /* The row under a bare production rail: Play, the return button when the thumb
755
+ has been moved, and the status line — one row, because the rail has no
756
+ header corner to put a control in. */
757
+ .rosh-tl-railrow { display: flex; align-items: center; gap: 12px; margin-top: 4px; flex-wrap: wrap; }
758
+ .rosh-tl-railrow .rosh-tl-status { margin: 0; }
759
+ /* The cap: the start anchor's own drawing, at the other end and for the same
760
+ reason, but a real button — its popover is where the undated things are
761
+ named. `.is-lit` still reaches it, so a consumer can light "the end". */
762
+ .rosh-tl-node.is-cap {
763
+ width: 14px; height: 14px; box-sizing: border-box; cursor: default;
764
+ background: var(--surface); border: 2px dashed var(--line-strong);
765
+ box-shadow: 0 0 0 2px var(--surface-sunken);
766
+ }
767
+
768
+ /* The start anchor. Hollow and dashed, because it is not an event — nothing was
769
+ filed on the day the axis opens, and a solid disc there would claim there
770
+ was. Decoration only: it takes no pointer, so the scrubber's hit strip runs
771
+ straight over it. */
772
+ .rosh-tl-start {
773
+ position: absolute; box-sizing: border-box; pointer-events: none;
774
+ width: 12px; height: 12px; border-radius: 50%;
775
+ transform: translate(-50%, -50%);
776
+ background: var(--surface); border: 1.5px dashed var(--line-strong);
777
+ box-shadow: 0 0 0 2px var(--surface-sunken);
778
+ }
779
+ /* A dated mark landed on the start day. The ring opens out to encircle it
780
+ rather than sitting under it — wider than the 16px key node, and transparent,
781
+ so both are legible and neither has moved off its date. */
782
+ .rosh-tl-start.is-around {
783
+ width: 26px; height: 26px; background: none; box-shadow: none;
784
+ }
785
+ /* A fold: one node standing for a run of marks too close to be drawn apart.
786
+ Same kind throughout, and it is that kind's own shape and glyph with the count
787
+ beside it — the reader should not have to learn a second vocabulary to be told
788
+ there are two shipments rather than one. */
789
+ .rosh-tl-count {
790
+ position: absolute; transform: translate(9px, -14px);
791
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums;
792
+ font-size: 9px; font-weight: 600; line-height: 1; letter-spacing: -0.02em;
793
+ color: var(--tl-accent); pointer-events: none;
794
+ padding: 1px 2px; border-radius: 4px; background: var(--surface-sunken);
795
+ }
796
+ /* Mixed kinds have no glyph that is true of all of them, so the fold is a plain
797
+ `×N` pill instead — wider than a dot, and the only mark on the rail that is
798
+ not a shape the legend explains. */
799
+ .rosh-tl-node.is-fold.is-mixed {
800
+ width: auto; min-width: 22px; height: 16px; padding: 0 5px; border-radius: 8px;
801
+ }
802
+ .rosh-tl-foldnum {
803
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums;
804
+ font-size: 9px; font-weight: 600; line-height: 1; letter-spacing: -0.02em;
805
+ }
806
+ .rosh-tl-node.is-active { box-shadow: 0 0 0 2px var(--surface-sunken), 0 0 0 5px var(--line); }
807
+ /* The flash as the scrubber's thumb crosses a mark. It rides the same 160ms
808
+ transition as the hover, so a fast drag pulses rather than strobes. */
809
+ .rosh-tl-node.is-hit {
810
+ --rosh-tl-sc: 1.5;
811
+ box-shadow: 0 0 0 2px var(--surface-sunken), 0 0 0 5px var(--tl-soft);
812
+ }
813
+ .rosh-tl-node:focus-visible, .rosh-tl-pill:focus-visible, .rosh-tl-thumb:focus-visible, .rosh-tl-more:focus-visible {
814
+ outline: 2px solid var(--tl-focus); outline-offset: 3px;
815
+ }
816
+ .rosh-tl-pulse {
817
+ position: absolute; inset: -1px; border-radius: inherit; pointer-events: none; opacity: 0;
818
+ }
819
+
820
+ .rosh-tl-label { position: absolute; display: flex; flex-direction: column; white-space: nowrap; }
821
+ .rosh-tl-label.is-up { justify-content: flex-end; }
822
+ /* The reveal for a label that fits no lane: a chip of its own, so it reads over
823
+ whichever lane label it lands on. */
824
+ .rosh-tl-label.is-reveal {
825
+ z-index: 4; padding: 2px 5px; border-radius: 7px;
826
+ background: var(--surface-raised); border: 1px solid var(--line);
827
+ }
828
+ .rosh-tl-name {
829
+ font-size: 11px; font-weight: 500; line-height: 1.25; color: var(--ink-strong);
830
+ transition: color 160ms cubic-bezier(0, 0, 0.2, 1);
831
+ }
832
+ .rosh-tl-date, .rosh-tl-detail {
833
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums;
834
+ font-size: 10px; line-height: 1.3; color: var(--ink-faint);
835
+ }
836
+ .rosh-tl-detail { font-family: inherit; color: var(--ink-muted); margin-top: 2px; }
837
+ .rosh-tl-label.is-current .rosh-tl-name { font-weight: 600; }
838
+ /* The label of whatever the pointer or the keyboard is on. Colour AND weight,
839
+ because on a card of eleven 11px labels colour alone is not a pointer. */
840
+ .rosh-tl-label.is-hot .rosh-tl-name { color: var(--tl-focus); font-weight: 600; }
841
+ .rosh-tl-label.is-hot .rosh-tl-date { color: var(--tl-focus); }
842
+ /* A phase bracket: the three-sided box under the rail that says "these ran in
843
+ parallel". Border WIDTH and STYLE only — the colour comes from the utility
844
+ class on the element, which carries the dark remap, and a `border:` shorthand
845
+ here would reset it to currentColor and win on being later in the cascade. */
846
+ .rosh-tl-phase {
847
+ position: absolute; height: 8px; pointer-events: none;
848
+ border-style: solid; border-width: 0 1px 1px 1px;
849
+ border-bottom-left-radius: 6px; border-bottom-right-radius: 6px;
850
+ }
851
+ .rosh-tl-phase-label {
852
+ position: absolute; transform: translateX(-50%); white-space: nowrap;
853
+ font-size: 9px; line-height: 1.2; pointer-events: none;
854
+ }
855
+
856
+ .rosh-tl-lead, .rosh-tl-cspan {
857
+ position: absolute; height: 1px; background: var(--line-strong); opacity: 0.8;
858
+ }
859
+ .rosh-tl-cspan { opacity: 0.7; }
860
+
861
+ .rosh-tl-pill {
862
+ position: absolute; display: inline-flex; align-items: center; gap: 4px;
863
+ height: 19px; padding: 0 7px; border-radius: 10px; cursor: pointer; white-space: nowrap;
864
+ font: inherit; font-size: 10px; font-weight: 500;
865
+ color: var(--ink); background: var(--surface); border: 1px solid currentColor;
866
+ transition: box-shadow 160ms cubic-bezier(0, 0, 0.2, 1);
867
+ }
868
+ .rosh-tl-pill i { width: 5px; height: 5px; border-radius: 50%; background: currentColor; flex: none; }
869
+ .rosh-tl-pill:hover, .rosh-tl-pill[aria-expanded="true"] { box-shadow: 0 0 0 3px var(--line); }
870
+
871
+ .rosh-tl-bubble, .rosh-tl-popover {
872
+ position: absolute; z-index: 6; min-width: 132px; max-width: 220px;
873
+ display: flex; flex-direction: column;
874
+ background: var(--surface-raised); border: 1px solid var(--line);
875
+ border-radius: 9px; padding: 7px 9px; transform: translateX(-50%);
876
+ }
877
+ /* The arrow is aimed at the dot the bubble describes, which is not the bubble's
878
+ own centre once it has been clamped inside the track. */
879
+ .rosh-tl-bubble::before, .rosh-tl-popover::before {
880
+ content: ""; position: absolute; top: -4px; left: var(--rosh-tl-arrow, 50%);
881
+ width: 7px; height: 7px; margin-left: -3.5px; background: var(--surface-raised);
882
+ border-left: 1px solid var(--line); border-top: 1px solid var(--line);
883
+ transform: rotate(45deg);
884
+ }
885
+ /* A preview is the document in miniature rather than a second line of text, so
886
+ it gets room — 280px, the width at which a chip row and a snippet still read
887
+ on a phone. */
888
+ .rosh-tl-bubble.is-preview { max-width: 280px; min-width: 180px; }
889
+ .rosh-tl-preview {
890
+ display: flex; flex-direction: column; gap: 3px;
891
+ margin-top: 5px; font-size: 11px; color: var(--ink-muted);
892
+ }
893
+ /* The way out of the popover and into the thing it is previewing. */
894
+ .rosh-tl-bubble-open {
895
+ margin-top: 6px; padding: 5px 0 0; width: 100%; text-align: left; cursor: pointer;
896
+ background: none; border: 0; border-top-width: 1px; border-top-style: solid;
897
+ border-top-color: var(--line);
898
+ font: inherit; font-size: 11px; font-weight: 500;
899
+ }
900
+ .rosh-tl-bubble-open:hover { text-decoration: underline; }
901
+ .rosh-tl-bubble-open:focus-visible { outline: 2px solid var(--tl-focus); outline-offset: 2px; }
902
+
903
+ .rosh-tl-popover-head {
904
+ margin: 0 0 5px; font-size: 10px; font-weight: 500; letter-spacing: 0.06em;
905
+ text-transform: uppercase; color: var(--ink-faint);
906
+ }
907
+ .rosh-tl-popover ol { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }
908
+ .rosh-tl-popover li { display: flex; gap: 8px; justify-content: space-between; font-size: 11px; }
909
+ /* What a FOLD says, which is the same list plus each member's own card: a run
910
+ that is all one date can never be opened, so this is the only place those
911
+ members exist. A column, because a preview under a row is a paragraph and not
912
+ a third column. */
913
+ .rosh-tl-fold-list ol { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
914
+ .rosh-tl-fold-list li { display: flex; flex-direction: column; font-size: 11px; }
915
+ .rosh-tl-foldrow { display: flex; gap: 8px; justify-content: space-between; margin: 0; }
916
+ .rosh-tl-fold-list .rosh-tl-preview { margin-top: 2px; }
917
+
918
+ .rosh-tl-hit { position: absolute; left: -10px; right: -10px; cursor: pointer; touch-action: none; }
919
+ /* The marks stand ABOVE that strip, and this one declaration is the whole of it.
920
+ `ThumbLayer` draws the strip after the list of dots and neither had a z-index,
921
+ so the strip won the paint order and took every pointer event aimed at a mark:
922
+ no hover, no focus, no click, on every track with a thumb — which is every
923
+ production scrubber. 4 is above the strip (auto) and below the thumb (5) and
924
+ the bubble (6), so the disc the reader has hold of still stays on top. The
925
+ strip keeps every pixel no dot is standing on, which is what leaves the drag
926
+ where it was: see the note over the strip in `TimelineTrack.tsx`. */
927
+ .rosh-tl-nodes .rosh-tl-node { z-index: 4; }
928
+ /* The magnet. A drag reports a stop change the moment the pointer crosses the
929
+ halfway line, and these 120ms carry the disc, its chip and the fill there
930
+ together — which is what makes six reports read as six settlings rather than
931
+ as one slide. The whole rule is off under reduced motion, at the bottom. */
932
+ .rosh-tl-thumb {
933
+ position: absolute; width: 18px; height: 18px; border-radius: 50%; z-index: 5;
934
+ background: var(--surface); border: 2px solid currentColor;
935
+ box-shadow: 0 0 0 4px var(--surface-sunken);
936
+ transform: translate(-50%, -50%); touch-action: none; cursor: grab;
937
+ transition: left 120ms cubic-bezier(0.22, 1, 0.36, 1);
938
+ }
939
+ .rosh-tl-fill.is-tweened { transition: width 120ms cubic-bezier(0.22, 1, 0.36, 1); }
940
+ /* While playback carries the thumb, an animation frame sets its position every
941
+ 16ms and the magnet's 120ms transitions have to come off: a transition chasing
942
+ a tween trails it by its own duration, which reads as the thumb lagging the
943
+ date on its chip — and a pause would keep sliding for 120ms after the frame
944
+ that froze it, landing somewhere nobody asked for. */
945
+ .rosh-tl-layer.is-gliding .rosh-tl-thumb,
946
+ .rosh-tl-layer.is-gliding .rosh-tl-chip,
947
+ .rosh-tl-layer.is-gliding .rosh-tl-fill.is-tweened { transition: none; }
948
+ /* The label of the mark the thumb is on, as a control: it opens the document
949
+ behind the dot. Styled as the label it replaces, underlined on hover so it
950
+ reads as the link it is. */
951
+ .rosh-tl-open {
952
+ padding: 0; border: 0; background: none; cursor: pointer; text-align: left;
953
+ font: inherit; font-size: 11px; font-weight: 500; line-height: 1.25;
954
+ }
955
+ .rosh-tl-open:hover { text-decoration: underline; }
956
+ .rosh-tl-open:focus-visible { outline: 2px solid var(--tl-focus); outline-offset: 2px; border-radius: 4px; }
957
+ .rosh-tl-thumb:active { cursor: grabbing; }
958
+ .rosh-tl-thumb::after { content: ""; position: absolute; inset: 3px; border-radius: 50%; background: currentColor; }
959
+ .rosh-tl-chip {
960
+ position: absolute; transform: translateX(-50%); z-index: 5; white-space: nowrap;
961
+ transition: left 120ms cubic-bezier(0.22, 1, 0.36, 1);
962
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums;
963
+ font-size: 10.5px; font-weight: 500; padding: 3px 8px; border-radius: 7px;
964
+ background: var(--ink-strongest); color: var(--surface);
965
+ }
966
+ .rosh-tl-chip::after {
967
+ content: ""; position: absolute; bottom: -3px; left: 50%; width: 7px; height: 7px;
968
+ margin-left: -3.5px; border-radius: 1px; background: var(--ink-strongest); transform: rotate(45deg);
969
+ }
970
+
971
+ .rosh-tl-pending { padding-top: 16px; }
972
+ .rosh-tl-pending-head { margin: 0 0 8px; font-size: 10px; font-weight: 500; color: var(--ink-faint); }
973
+ .rosh-tl-ghosts { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
974
+ .rosh-tl-ghosts li { display: flex; align-items: center; gap: 9px; font-size: 11px; color: var(--ink-muted); }
975
+ .rosh-tl-ghosts .rosh-tl-node { position: relative; --rosh-tl-shift: translate(0, 0); flex: none; }
976
+ .rosh-tl-ghosts b { font-weight: 500; color: var(--ink-faint); }
977
+ .rosh-tl-more { margin-top: 9px; font-size: 11px; text-decoration: underline; cursor: pointer; }
978
+
979
+ /* The vertical variant: under a 320px track the axis is abandoned rather than
980
+ squeezed, and the connector carries the fill. */
981
+ .rosh-tl-v { list-style: none; margin: 0; padding: 0; }
982
+ .rosh-tl-v > li {
983
+ position: relative; display: grid; grid-template-columns: 16px minmax(0, 1fr);
984
+ gap: 10px; padding-bottom: 14px;
985
+ }
986
+ .rosh-tl-v > li:last-child { padding-bottom: 0; }
987
+ .rosh-tl-v .rosh-tl-node {
988
+ position: relative; --rosh-tl-shift: translate(0, 0);
989
+ justify-self: center; align-self: start; margin-top: 3px;
990
+ }
991
+ .rosh-tl-vline {
992
+ position: absolute; left: 8px; top: 17px; bottom: -1px; width: 1px;
993
+ background: var(--line-strong); transform-origin: top center;
994
+ }
995
+ .rosh-tl-vline.is-on { width: 2px; left: 7.5px; background: currentColor; }
996
+ .rosh-tl-vline.is-dash { background: none; border-left: 1px dashed var(--line-strong); }
997
+ .rosh-tl-vrow { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
998
+ .rosh-tl-v > li[aria-current] .rosh-tl-name { font-weight: 600; }
999
+ .rosh-tl-vgap {
1000
+ position: relative; margin: 0; padding: 0 0 14px 26px; min-height: 20px;
1001
+ font-family: var(--font-mono); font-size: 10px; color: var(--ink-faint);
1002
+ }
1003
+
1004
+ /* ── Timeline motion ────────────────────────────────────────────────────────
1005
+ * Mount budget <= 600ms: the fill draws in 420ms, the dots pop on a stagger
1006
+ * that lands the last one by 520ms, and the labels settle by 580ms. `both` on
1007
+ * every rule — with a delay the mark would otherwise paint its FINAL state and
1008
+ * jump back when its delay elapsed. The draw is `scaleX`, not `width`: the
1009
+ * track is under a ResizeObserver, and animating width relays out every dot on
1010
+ * every frame. */
1011
+ @keyframes rosh-tl-draw { from { transform: scaleX(0); } to { transform: scaleX(1); } }
1012
+ @keyframes rosh-tl-drawy { from { transform: scaleY(0); } to { transform: scaleY(1); } }
1013
+ @keyframes rosh-tl-fade { from { opacity: 0; } to { opacity: 1; } }
1014
+ @keyframes rosh-tl-pop {
1015
+ from {
1016
+ opacity: 0;
1017
+ transform: var(--rosh-tl-shift, translate(-50%, -50%)) rotate(var(--rosh-tl-rot, 0deg)) scale(0.6);
1018
+ }
1019
+ to {
1020
+ opacity: 1;
1021
+ transform: var(--rosh-tl-shift, translate(-50%, -50%)) rotate(var(--rosh-tl-rot, 0deg)) scale(1);
1022
+ }
1023
+ }
1024
+ @keyframes rosh-tl-pulse { from { transform: scale(1); opacity: 0.35; } to { transform: scale(2.6); opacity: 0; } }
1025
+
1026
+ .rosh-tl-draw { animation: rosh-tl-draw 420ms cubic-bezier(0.22, 1, 0.36, 1) both; }
1027
+ .rosh-tl-drawy { animation: rosh-tl-drawy 420ms cubic-bezier(0.22, 1, 0.36, 1) both; }
1028
+ .rosh-tl-fade { animation: rosh-tl-fade 160ms cubic-bezier(0, 0, 0.2, 1) both; }
1029
+ .rosh-tl-pop { animation: rosh-tl-pop 180ms cubic-bezier(0.22, 1, 0.36, 1) both; }
1030
+ /* Three cycles, then stop. An endless "you are here" ring is the card's most
1031
+ frequent motion, and frequency is what makes motion annoying. */
1032
+ .rosh-tl-pulse.is-on {
1033
+ transform-origin: center;
1034
+ animation: rosh-tl-pulse 2000ms cubic-bezier(0, 0, 0.2, 1) 600ms 3 both;
1035
+ }
1036
+ /* Hover is a TRANSITION, never a keyframe — a keyframe restarts on re-render,
1037
+ and this element re-renders on every pointer move. */
1038
+ .rosh-tl-mark { transition: transform 160ms ease-out, box-shadow 160ms ease-out, background-color 160ms ease-out, border-color 160ms ease-out; }
1039
+ /* Half again as big, with a wash behind it: at 10px a 1.35 dot is a dot that
1040
+ moved, and the point of the hover is that the reader can tell it answered. */
1041
+ .rosh-tl-mark:hover, .rosh-tl-mark:focus-visible {
1042
+ --rosh-tl-sc: 1.5;
1043
+ box-shadow: 0 0 0 2px var(--surface-sunken), 0 0 0 3px var(--tl-soft);
1044
+ }
1045
+
1046
+ /* Zoom: the axis opening around a magnified stretch, and closing again. The
1047
+ transition is armed by the component for the length of ONE change and then
1048
+ taken away, because a positional transition left on permanently eases every
1049
+ dot sideways while a window edge is being dragged — which reads as lag, not
1050
+ as motion. */
1051
+ .rosh-tl-layer.is-zooming .rosh-tl-mark {
1052
+ transition: left 240ms cubic-bezier(0.22, 1, 0.36, 1),
1053
+ transform 160ms ease-out, box-shadow 160ms ease-out,
1054
+ background-color 160ms ease-out, border-color 160ms ease-out;
1055
+ }
1056
+ .rosh-tl-layer.is-zooming .rosh-tl-label,
1057
+ .rosh-tl-layer.is-zooming .rosh-tl-lead,
1058
+ .rosh-tl-layer.is-zooming .rosh-tl-cspan,
1059
+ .rosh-tl-layer.is-zooming .rosh-tl-tick,
1060
+ .rosh-tl-layer.is-zooming .rosh-tl-month,
1061
+ .rosh-tl-layer.is-zooming .rosh-tl-rulercut,
1062
+ .rosh-tl-layer.is-zooming .rosh-tl-cut,
1063
+ .rosh-tl-layer.is-zooming .rosh-tl-todayline,
1064
+ .rosh-tl-layer.is-zooming .rosh-tl-fill {
1065
+ transition: left 240ms cubic-bezier(0.22, 1, 0.36, 1),
1066
+ width 240ms cubic-bezier(0.22, 1, 0.36, 1);
1067
+ }
1068
+
1069
+ @media (prefers-reduced-motion: reduce) {
1070
+ .rosh-tl-layer.is-zooming .rosh-tl-mark,
1071
+ .rosh-tl-layer.is-zooming .rosh-tl-label,
1072
+ .rosh-tl-layer.is-zooming .rosh-tl-lead,
1073
+ .rosh-tl-layer.is-zooming .rosh-tl-cspan,
1074
+ .rosh-tl-layer.is-zooming .rosh-tl-tick,
1075
+ .rosh-tl-layer.is-zooming .rosh-tl-month,
1076
+ .rosh-tl-layer.is-zooming .rosh-tl-rulercut,
1077
+ .rosh-tl-layer.is-zooming .rosh-tl-cut,
1078
+ .rosh-tl-layer.is-zooming .rosh-tl-todayline,
1079
+ .rosh-tl-layer.is-zooming .rosh-tl-fill { transition: none !important; }
1080
+ .rosh-tl-draw, .rosh-tl-drawy, .rosh-tl-fade, .rosh-tl-pop {
1081
+ animation: none !important;
1082
+ opacity: 1 !important;
1083
+ }
1084
+ /* The two draws rest at full scale; the pop rests at the node's own transform,
1085
+ which carries its translate and rotate, so it cannot be reset to `none`. */
1086
+ .rosh-tl-draw, .rosh-tl-drawy { transform: none !important; }
1087
+ /* The ring RESTS invisible — it ends at opacity 0 — so the entrance treatment
1088
+ above would pin a halo at an opacity it never reaches while animating. */
1089
+ .rosh-tl-pulse.is-on { animation: none !important; opacity: 0 !important; }
1090
+ /* Colour still moves; scale does not. */
1091
+ .rosh-tl-mark { transition: background-color 160ms ease-out, border-color 160ms ease-out !important; }
1092
+ .rosh-tl-mark:hover, .rosh-tl-mark:focus-visible { --rosh-tl-sc: 1; }
1093
+ /* The thumb still MOVES under reduced motion — it just arrives. */
1094
+ .rosh-tl-thumb, .rosh-tl-chip, .rosh-tl-fill.is-tweened { transition: none !important; }
1095
+ }
1096
+
1097
+ /* Backgrounds */
1098
+ [data-theme="dark"] .bg-white { background-color: var(--surface) !important; }
1099
+ [data-theme="dark"] .bg-gray-50 { background-color: var(--surface-sunken) !important; }
1100
+ [data-theme="dark"] .bg-gray-100 { background-color: var(--surface) !important; }
1101
+ [data-theme="dark"] .bg-gray-200 { background-color: var(--surface-raised) !important; }
1102
+
1103
+ /* Text */
1104
+ [data-theme="dark"] .text-gray-900 { color: var(--ink-strongest) !important; }
1105
+ [data-theme="dark"] .text-gray-800 { color: var(--ink-strong) !important; }
1106
+ [data-theme="dark"] .text-gray-700 { color: var(--ink) !important; }
1107
+ [data-theme="dark"] .text-gray-600 { color: var(--ink-muted) !important; }
1108
+ [data-theme="dark"] .text-gray-500 { color: var(--ink-faint) !important; }
1109
+ [data-theme="dark"] .text-gray-400 { color: var(--ink-faintest) !important; }
1110
+
1111
+ /* Borders */
1112
+ [data-theme="dark"] .border-gray-200 { border-color: var(--line) !important; }
1113
+ [data-theme="dark"] .border-gray-300 { border-color: var(--line-strong) !important; }
1114
+ [data-theme="dark"] .border-gray-100 { border-color: var(--line-subtle) !important; }
1115
+
1116
+ /* Rings. `ring-gray-100` is a cut-out rather than a line: it rings a mark in
1117
+ the colour of the `bg-gray-100` surface under it (PageTabBar's unsaved dot,
1118
+ on the tab strip), so it takes that surface's token. Without this it kept
1119
+ its light gray and drew a pale halo on the dark strip. */
1120
+ [data-theme="dark"] .ring-gray-100 { --tw-ring-color: var(--surface) !important; }
1121
+
1122
+ /* Cards, shadows, tables */
1123
+ [data-theme="dark"] .shadow { --tw-shadow-color: rgba(0,0,0,0.4) !important; }
1124
+ [data-theme="dark"] .shadow-lg { --tw-shadow-color: rgba(0,0,0,0.5) !important; }
1125
+ [data-theme="dark"] .shadow-xl { --tw-shadow-color: rgba(0,0,0,0.5) !important; }
1126
+ [data-theme="dark"] .shadow-2xl { --tw-shadow-color: rgba(0,0,0,0.6) !important; }
1127
+ [data-theme="dark"] .divide-gray-200 > :not(:last-child) { border-color: var(--line) !important; }
1128
+ [data-theme="dark"] .divide-gray-100 > :not(:last-child) { border-color: var(--line-subtle) !important; }
1129
+
1130
+ /* Hover states */
1131
+ [data-theme="dark"] .hover\:bg-gray-50:hover { background-color: var(--line-subtle) !important; }
1132
+ [data-theme="dark"] .hover\:bg-gray-100:hover { background-color: var(--surface-raised) !important; }
1133
+ [data-theme="dark"] .hover\:bg-gray-200:hover { background-color: var(--line-strong) !important; }
1134
+
1135
+ /* Blue hover states — used by menu items, profile row, etc.
1136
+ In dark mode swap the near-white bg for a dark blue tint and the dark
1137
+ blue text for a light shade so highlighted items stay readable. */
1138
+ [data-theme="dark"] .bg-blue-50,
1139
+ [data-theme="dark"] .hover\:bg-blue-50:hover { background-color: rgba(59, 130, 246, 0.26) !important; }
1140
+ [data-theme="dark"] .text-blue-700,
1141
+ [data-theme="dark"] .hover\:text-blue-700:hover { color: #bfdbfe !important; }
1142
+ [data-theme="dark"] .text-blue-600 { color: #93c5fd !important; }
1143
+ [data-theme="dark"] .text-blue-800 { color: #bfdbfe !important; }
1144
+
1145
+ /* Bare *-100 highlight backgrounds + *-700 text (status pills, badges, avatars,
1146
+ dialog icons) have no dark override otherwise, so they keep their bright
1147
+ light-mode value and wash out the text on top. Mute the background to a low
1148
+ tint and lighten the text so highlighted content stays readable in dark mode. */
1149
+ [data-theme="dark"] .bg-blue-100 { background-color: rgba(59, 130, 246, 0.22) !important; }
1150
+
1151
+ /* Tailwind's `!` important modifier compiles to distinct class names
1152
+ (`.\!bg-blue-100` etc.), so the EditableGrid selection utilities escape the
1153
+ remaps above: the selection keeps its light-mode background while the cell
1154
+ text goes light, making selected cells unreadable in dark mode. The
1155
+ attribute selector outranks the utility's own !important. */
1156
+ [data-theme="dark"] .\!bg-blue-50 { background-color: rgba(59, 130, 246, 0.26) !important; }
1157
+ [data-theme="dark"] .\!bg-blue-100 { background-color: rgba(59, 130, 246, 0.22) !important; }
1158
+ [data-theme="dark"] .\!bg-blue-200 { background-color: rgba(59, 130, 246, 0.35) !important; }
1159
+ [data-theme="dark"] .\!text-gray-700 { color: var(--ink) !important; }
1160
+ [data-theme="dark"] .bg-green-100 { background-color: rgba(34, 197, 94, 0.18) !important; }
1161
+ [data-theme="dark"] .bg-emerald-100 { background-color: rgba(16, 185, 129, 0.18) !important; }
1162
+ [data-theme="dark"] .bg-sky-100 { background-color: rgba(14, 165, 233, 0.18) !important; }
1163
+ [data-theme="dark"] .bg-amber-100 { background-color: rgba(245, 158, 11, 0.18) !important; }
1164
+ [data-theme="dark"] .bg-yellow-100 { background-color: rgba(234, 179, 8, 0.18) !important; }
1165
+ [data-theme="dark"] .bg-orange-100 { background-color: rgba(249, 115, 22, 0.18) !important; }
1166
+ [data-theme="dark"] .bg-red-100 { background-color: rgba(239, 68, 68, 0.18) !important; }
1167
+ [data-theme="dark"] .bg-indigo-100 { background-color: rgba(99, 102, 241, 0.18) !important; }
1168
+ [data-theme="dark"] .bg-purple-100 { background-color: rgba(168, 85, 247, 0.18) !important; }
1169
+ [data-theme="dark"] .text-green-700 { color: #86efac !important; }
1170
+ /* The /70 form is a separate class, so the remap above misses it. The amber
1171
+ block already covers its own /30../50 tints; green did not, and dark green
1172
+ on the green tint was the result. */
1173
+ [data-theme="dark"] .text-green-700\/70 { color: rgba(134, 239, 172, 0.7) !important; }
1174
+ [data-theme="dark"] .text-emerald-700 { color: #6ee7b7 !important; }
1175
+ [data-theme="dark"] .text-sky-700 { color: #7dd3fc !important; }
1176
+ [data-theme="dark"] .text-amber-700 { color: #fcd34d !important; }
1177
+ [data-theme="dark"] .text-yellow-700 { color: #fde047 !important; }
1178
+ [data-theme="dark"] .text-orange-700 { color: #fdba74 !important; }
1179
+ [data-theme="dark"] .text-red-700 { color: #fca5a5 !important; }
1180
+ [data-theme="dark"] .text-indigo-700 { color: #a5b4fc !important; }
1181
+ [data-theme="dark"] .text-purple-700 { color: #d8b4fe !important; }
1182
+
1183
+ /* Brighten taskbar button text so it stays readable on dark glass.
1184
+ The default override (var(--ink-muted)) is too dim for short button labels. */
1185
+ [data-theme="dark"] .text-gray-600 { color: var(--ink) !important; }
1186
+
1187
+ /* Translucent button backgrounds — Tailwind generates separate classes for the
1188
+ /40 /60 alpha variants (bg-gray-50\/40 etc.) so the bare .bg-gray-50 override
1189
+ doesn't match. Override them explicitly with dark equivalents so taskbar
1190
+ tabs and the start-menu button don't look too bright in Dark mode. */
1191
+ [data-theme="dark"] .bg-gray-50\/40 { background-color: rgba(24, 24, 37, 0.4) !important; }
1192
+ /* /50 and /60 are used as in-window PANEL surfaces (sidebars, form sections,
1193
+ popup cards) — keep them solid in dark so a bright wallpaper can't bleed
1194
+ through the window glass and wash them out. */
1195
+ [data-theme="dark"] .bg-gray-50\/50 { background-color: var(--surface-sunken) !important; }
1196
+ [data-theme="dark"] .bg-gray-50\/60 { background-color: var(--surface-sunken) !important; }
1197
+ [data-theme="dark"] .hover\:bg-gray-50\/50:hover { background-color: var(--surface-sunken) !important; }
1198
+ [data-theme="dark"] .bg-gray-100\/40 { background-color: rgba(30, 30, 46, 0.4) !important; }
1199
+ [data-theme="dark"] .bg-gray-100\/60 { background-color: rgba(30, 30, 46, 0.6) !important; }
1200
+ [data-theme="dark"] .bg-gray-200\/40 { background-color: rgba(49, 50, 68, 0.4) !important; }
1201
+ [data-theme="dark"] .bg-gray-200\/50 { background-color: rgba(49, 50, 68, 0.5) !important; }
1202
+ [data-theme="dark"] .bg-blue-100\/60 { background-color: rgba(59, 130, 246, 0.22) !important; }
1203
+ /* Pale-blue tinted panels (selected rows, inline-reply box) — scale the
1204
+ .bg-blue-50 dark tint (0.26) down through the alpha steps. */
1205
+ [data-theme="dark"] .bg-blue-50\/30 { background-color: rgba(59, 130, 246, 0.10) !important; }
1206
+ [data-theme="dark"] .bg-blue-50\/40 { background-color: rgba(59, 130, 246, 0.12) !important; }
1207
+ [data-theme="dark"] .bg-blue-50\/50 { background-color: rgba(59, 130, 246, 0.15) !important; }
1208
+ [data-theme="dark"] .bg-blue-50\/60 { background-color: rgba(59, 130, 246, 0.18) !important; }
1209
+ [data-theme="dark"] .border-gray-200\/40 { border-color: rgba(69, 71, 90, 0.4) !important; }
1210
+ [data-theme="dark"] .border-gray-300\/40 { border-color: rgba(88, 91, 112, 0.4) !important; }
1211
+ [data-theme="dark"] .border-blue-400\/60 { border-color: rgba(96, 165, 250, 0.45) !important; }
1212
+ [data-theme="dark"] .hover\:bg-gray-200\/40:hover { background-color: rgba(49, 50, 68, 0.4) !important; }
1213
+
1214
+ /* White alpha variants — the bare .bg-white override doesn't match the /50../95
1215
+ alpha classes, so cards (the Widget gallery), the Exposé snapshot title pill
1216
+ and desktop overlays stayed bright white in dark mode while their (remapped,
1217
+ light) text washed out. Map them to the dark window tint. */
1218
+ [data-theme="dark"] .bg-white\/50 { background-color: rgba(30, 30, 46, 0.5) !important; }
1219
+ [data-theme="dark"] .bg-white\/60 { background-color: rgba(30, 30, 46, 0.6) !important; }
1220
+ [data-theme="dark"] .bg-white\/70 { background-color: rgba(30, 30, 46, 0.7) !important; }
1221
+ [data-theme="dark"] .bg-white\/80 { background-color: rgba(30, 30, 46, 0.85) !important; }
1222
+ [data-theme="dark"] .bg-white\/90 { background-color: rgba(30, 30, 46, 0.92) !important; }
1223
+ [data-theme="dark"] .bg-white\/95 { background-color: rgba(30, 30, 46, 0.95) !important; }
1224
+ [data-theme="dark"] .bg-blue-50\/70 { background-color: rgba(59, 130, 246, 0.22) !important; }
1225
+
1226
+ /* Inputs — apply the form-input dark styling, but skip elements that
1227
+ explicitly opt into a transparent background (e.g. the start-menu
1228
+ search) so they keep their parent's frosted-glass tile. */
1229
+ [data-theme="dark"] input:not(.bg-transparent):not([type="checkbox"]):not([type="radio"]):not([type="color"]):not([type="file"]),
1230
+ [data-theme="dark"] select:not(.bg-transparent),
1231
+ [data-theme="dark"] textarea:not(.bg-transparent) {
1232
+ background-color: var(--surface-sunken) !important;
1233
+ border-color: var(--line-strong) !important;
1234
+ color: var(--ink-strongest) !important;
1235
+ }
1236
+
1237
+ /* iOS auto-zooms the viewport when a focused form control's computed
1238
+ font-size is below 16px. On phones/tablets (same breakpoint as
1239
+ useIsMobile) pin text-bearing controls to 16px so focusing them never
1240
+ triggers the zoom. This keeps pinch-zoom working, unlike a
1241
+ `maximum-scale=1` viewport lock. !important beats Tailwind text-*
1242
+ utilities; the :not() list mirrors the dark-input rule above and skips
1243
+ controls that don't take a text caret. */
1244
+ @media (max-width: 767px), (pointer: coarse) {
1245
+ input:not([type="checkbox"]):not([type="radio"]):not([type="color"]):not([type="file"]),
1246
+ select,
1247
+ textarea {
1248
+ font-size: 16px !important;
1249
+ }
1250
+ }
1251
+
1252
+ /* Status badges — tone down brightness in dark mode (matches StatusBadge.tsx semantic groups) */
1253
+ [data-theme="dark"] .bg-gray-100.text-gray-800 { background-color: var(--surface-raised) !important; color: var(--ink) !important; }
1254
+ [data-theme="dark"] .bg-gray-300.text-gray-800 { background-color: var(--line-strong) !important; color: var(--ink-strongest) !important; }
1255
+ [data-theme="dark"] .bg-green-100.text-green-800 { background-color: rgba(34, 197, 94, 0.18) !important; color: #86efac !important; }
1256
+ [data-theme="dark"] .bg-blue-100.text-blue-800 { background-color: rgba(59, 130, 246, 0.18) !important; color: #93c5fd !important; }
1257
+ [data-theme="dark"] .bg-indigo-100.text-indigo-800 { background-color: rgba(99, 102, 241, 0.18) !important; color: #a5b4fc !important; }
1258
+ [data-theme="dark"] .bg-sky-100.text-sky-800 { background-color: rgba(14, 165, 233, 0.18) !important; color: #7dd3fc !important; }
1259
+ [data-theme="dark"] .bg-yellow-100.text-yellow-800 { background-color: rgba(234, 179, 8, 0.18) !important; color: #fde68a !important; }
1260
+ [data-theme="dark"] .bg-orange-100.text-orange-800 { background-color: rgba(249, 115, 22, 0.18) !important; color: #fdba74 !important; }
1261
+ [data-theme="dark"] .bg-red-100.text-red-800 { background-color: rgba(239, 68, 68, 0.18) !important; color: #fca5a5 !important; }
1262
+
1263
+ /* ── Extended dark-mode tint families ────────────────────────────────
1264
+ Upstreamed 2026-06-13 from the admin portal's 2026-06-10 audit so all
1265
+ consumers share one source of truth. Every hue used by chips/badges
1266
+ (tag colors, group colors, status accents, deal stages), notice
1267
+ banners, verdict panels, row tints and danger buttons gets the same
1268
+ scale as the families above: -50 → 10% tint, -100 → 18%, -200 → ~30%,
1269
+ borders -200 → 32% / -300 → 45%, and dark inks flip to the
1270
+ -400/-300/-200 light end of their scale.
1271
+ Deliberately NOT remapped: game surfaces that read as intentionally
1272
+ light (Calculator LCD bg-slate-200/text-slate-600/800, 2048/Chess
1273
+ tiles, weather gradients), the Customization theme-preview swatches
1274
+ (bg-pink-50/90, bg-green-50/90, bg-blue-50/90, bg-gray-200/90,
1275
+ bg-pink-200, bg-green-200 — they must show each theme's real colors),
1276
+ and low white/black alphas used as glass highlights on wallpaper. */
1277
+
1278
+ /* Red */
1279
+ [data-theme="dark"] .bg-red-50 { background-color: rgba(239, 68, 68, 0.10) !important; }
1280
+ [data-theme="dark"] .hover\:bg-red-50:hover { background-color: rgba(239, 68, 68, 0.16) !important; }
1281
+ [data-theme="dark"] .active\:bg-red-50:active { background-color: rgba(239, 68, 68, 0.16) !important; }
1282
+ [data-theme="dark"] .bg-red-50\/30 { background-color: rgba(239, 68, 68, 0.06) !important; }
1283
+ [data-theme="dark"] .bg-red-50\/50 { background-color: rgba(239, 68, 68, 0.08) !important; }
1284
+ [data-theme="dark"] .hover\:bg-red-100:hover { background-color: rgba(239, 68, 68, 0.22) !important; }
1285
+ [data-theme="dark"] .border-red-200 { border-color: rgba(239, 68, 68, 0.35) !important; }
1286
+ [data-theme="dark"] .border-red-300,
1287
+ [data-theme="dark"] .hover\:border-red-300:hover { border-color: rgba(239, 68, 68, 0.45) !important; }
1288
+ [data-theme="dark"] .text-red-600,
1289
+ [data-theme="dark"] .hover\:text-red-600:hover { color: #f87171 !important; }
1290
+ [data-theme="dark"] .hover\:text-red-700:hover { color: #fca5a5 !important; }
1291
+ [data-theme="dark"] .text-red-800,
1292
+ [data-theme="dark"] .hover\:text-red-800:hover { color: #fecaca !important; }
1293
+ [data-theme="dark"] .text-red-900 { color: #fecaca !important; }
1294
+
1295
+ /* Amber */
1296
+ [data-theme="dark"] .bg-amber-50 { background-color: rgba(245, 158, 11, 0.10) !important; }
1297
+ [data-theme="dark"] .hover\:bg-amber-50:hover { background-color: rgba(245, 158, 11, 0.14) !important; }
1298
+ [data-theme="dark"] .bg-amber-50\/30 { background-color: rgba(245, 158, 11, 0.06) !important; }
1299
+ [data-theme="dark"] .bg-amber-50\/40 { background-color: rgba(245, 158, 11, 0.07) !important; }
1300
+ [data-theme="dark"] .bg-amber-50\/50 { background-color: rgba(245, 158, 11, 0.08) !important; }
1301
+ [data-theme="dark"] .hover\:bg-amber-100:hover { background-color: rgba(245, 158, 11, 0.24) !important; }
1302
+ [data-theme="dark"] .bg-amber-200 { background-color: rgba(245, 158, 11, 0.28) !important; }
1303
+ [data-theme="dark"] .border-amber-200 { border-color: rgba(245, 158, 11, 0.35) !important; }
1304
+ [data-theme="dark"] .border-amber-300,
1305
+ [data-theme="dark"] .hover\:border-amber-300:hover { border-color: rgba(245, 158, 11, 0.45) !important; }
1306
+ [data-theme="dark"] .text-amber-600,
1307
+ [data-theme="dark"] .hover\:text-amber-600:hover { color: #fbbf24 !important; }
1308
+ [data-theme="dark"] .text-amber-800 { color: #fde68a !important; }
1309
+ [data-theme="dark"] .text-amber-900 { color: #fde68a !important; }
1310
+
1311
+ /* Yellow */
1312
+ [data-theme="dark"] .bg-yellow-50 { background-color: rgba(234, 179, 8, 0.10) !important; }
1313
+ [data-theme="dark"] .bg-yellow-50\/50 { background-color: rgba(234, 179, 8, 0.08) !important; }
1314
+ [data-theme="dark"] .border-yellow-200 { border-color: rgba(234, 179, 8, 0.35) !important; }
1315
+ [data-theme="dark"] .border-yellow-300 { border-color: rgba(234, 179, 8, 0.45) !important; }
1316
+ [data-theme="dark"] .text-yellow-600,
1317
+ [data-theme="dark"] .hover\:text-yellow-600:hover { color: #facc15 !important; }
1318
+ [data-theme="dark"] .text-yellow-900 { color: #fef08a !important; }
1319
+
1320
+ /* Green / emerald */
1321
+ [data-theme="dark"] .bg-green-50 { background-color: rgba(34, 197, 94, 0.10) !important; }
1322
+ [data-theme="dark"] .hover\:bg-green-50:hover { background-color: rgba(34, 197, 94, 0.14) !important; }
1323
+ [data-theme="dark"] .bg-green-50\/40 { background-color: rgba(34, 197, 94, 0.06) !important; }
1324
+ [data-theme="dark"] .border-green-200 { border-color: rgba(34, 197, 94, 0.32) !important; }
1325
+ [data-theme="dark"] .border-green-300,
1326
+ [data-theme="dark"] .hover\:border-green-300:hover { border-color: rgba(34, 197, 94, 0.45) !important; }
1327
+ [data-theme="dark"] .text-green-600,
1328
+ [data-theme="dark"] .hover\:text-green-600:hover { color: #4ade80 !important; }
1329
+ [data-theme="dark"] .text-green-900,
1330
+ [data-theme="dark"] .hover\:text-green-800:hover { color: #bbf7d0 !important; }
1331
+ [data-theme="dark"] .bg-emerald-50 { background-color: rgba(16, 185, 129, 0.10) !important; }
1332
+ [data-theme="dark"] .hover\:bg-emerald-100:hover { background-color: rgba(16, 185, 129, 0.24) !important; }
1333
+ [data-theme="dark"] .border-emerald-200 { border-color: rgba(16, 185, 129, 0.32) !important; }
1334
+ [data-theme="dark"] .border-emerald-300 { border-color: rgba(16, 185, 129, 0.45) !important; }
1335
+ [data-theme="dark"] .text-emerald-600 { color: #34d399 !important; }
1336
+ [data-theme="dark"] .text-emerald-800 { color: #a7f3d0 !important; }
1337
+
1338
+ /* Orange */
1339
+ [data-theme="dark"] .bg-orange-50 { background-color: rgba(249, 115, 22, 0.10) !important; }
1340
+ [data-theme="dark"] .hover\:bg-orange-50:hover { background-color: rgba(249, 115, 22, 0.14) !important; }
1341
+ [data-theme="dark"] .hover\:bg-orange-200:hover { background-color: rgba(249, 115, 22, 0.30) !important; }
1342
+ [data-theme="dark"] .border-orange-200 { border-color: rgba(249, 115, 22, 0.32) !important; }
1343
+ [data-theme="dark"] .border-orange-300,
1344
+ [data-theme="dark"] .hover\:border-orange-300:hover { border-color: rgba(249, 115, 22, 0.45) !important; }
1345
+ [data-theme="dark"] .text-orange-600 { color: #fb923c !important; }
1346
+ [data-theme="dark"] .text-orange-900 { color: #fed7aa !important; }
1347
+
1348
+ /* Sky / teal / cyan */
1349
+ [data-theme="dark"] .bg-sky-50 { background-color: rgba(14, 165, 233, 0.10) !important; }
1350
+ [data-theme="dark"] .border-sky-300 { border-color: rgba(14, 165, 233, 0.45) !important; }
1351
+ [data-theme="dark"] .bg-teal-50 { background-color: rgba(20, 184, 166, 0.10) !important; }
1352
+ [data-theme="dark"] .bg-teal-100 { background-color: rgba(20, 184, 166, 0.18) !important; }
1353
+ [data-theme="dark"] .text-teal-600 { color: #2dd4bf !important; }
1354
+ [data-theme="dark"] .text-teal-700 { color: #5eead4 !important; }
1355
+ [data-theme="dark"] .text-teal-800 { color: #99f6e4 !important; }
1356
+ [data-theme="dark"] .text-cyan-600 { color: #22d3ee !important; }
1357
+
1358
+ /* Purple / violet */
1359
+ [data-theme="dark"] .bg-purple-50 { background-color: rgba(168, 85, 247, 0.10) !important; }
1360
+ [data-theme="dark"] .hover\:bg-purple-50:hover { background-color: rgba(168, 85, 247, 0.14) !important; }
1361
+ [data-theme="dark"] .border-purple-200 { border-color: rgba(168, 85, 247, 0.32) !important; }
1362
+ [data-theme="dark"] .border-purple-300,
1363
+ [data-theme="dark"] .hover\:border-purple-300:hover { border-color: rgba(168, 85, 247, 0.45) !important; }
1364
+ [data-theme="dark"] .text-purple-600 { color: #c084fc !important; }
1365
+ [data-theme="dark"] .text-purple-800,
1366
+ [data-theme="dark"] .text-purple-900 { color: #e9d5ff !important; }
1367
+ [data-theme="dark"] .bg-violet-100 { background-color: rgba(139, 92, 246, 0.18) !important; }
1368
+ [data-theme="dark"] .border-violet-300 { border-color: rgba(139, 92, 246, 0.45) !important; }
1369
+ [data-theme="dark"] .text-violet-600 { color: #a78bfa !important; }
1370
+ [data-theme="dark"] .text-violet-700 { color: #c4b5fd !important; }
1371
+ [data-theme="dark"] .text-violet-800 { color: #ddd6fe !important; }
1372
+
1373
+ /* Pink / rose */
1374
+ [data-theme="dark"] .bg-pink-100 { background-color: rgba(236, 72, 153, 0.18) !important; }
1375
+ [data-theme="dark"] .border-pink-200 { border-color: rgba(236, 72, 153, 0.32) !important; }
1376
+ [data-theme="dark"] .border-pink-300 { border-color: rgba(236, 72, 153, 0.45) !important; }
1377
+ [data-theme="dark"] .text-pink-600 { color: #f472b6 !important; }
1378
+ [data-theme="dark"] .text-pink-800,
1379
+ [data-theme="dark"] .text-pink-900 { color: #fbcfe8 !important; }
1380
+ [data-theme="dark"] .bg-rose-100 { background-color: rgba(244, 63, 94, 0.18) !important; }
1381
+ [data-theme="dark"] .text-rose-700 { color: #fda4af !important; }
1382
+ [data-theme="dark"] .text-rose-800 { color: #fecdd3 !important; }
1383
+
1384
+ /* Indigo / slate */
1385
+ [data-theme="dark"] .bg-indigo-50 { background-color: rgba(99, 102, 241, 0.15) !important; }
1386
+ /* An OPACITY MODIFIER makes a different class name, so the remap above does
1387
+ not reach `bg-indigo-50/40`. Same escape the `!` important-modifier makes,
1388
+ handled for `.\!bg-blue-*` earlier in this sheet, and already handled for
1389
+ amber's own `/30../50` tints below. `/40` and `/50`
1390
+ of a -50 tint both land inside the base tint's own alpha, so they take it
1391
+ rather than a proportional slice: a near-white lavender panel under the dark
1392
+ sheet's light body text is unreadable, which is what shipped. */
1393
+ [data-theme="dark"] .bg-indigo-50\/40 { background-color: rgba(99, 102, 241, 0.12) !important; }
1394
+ [data-theme="dark"] .bg-indigo-50\/50 { background-color: rgba(99, 102, 241, 0.15) !important; }
1395
+ [data-theme="dark"] .hover\:bg-indigo-100:hover { background-color: rgba(99, 102, 241, 0.25) !important; }
1396
+ [data-theme="dark"] .text-indigo-600 { color: #a5b4fc !important; }
1397
+ [data-theme="dark"] .border-indigo-100 { border-color: rgba(99, 102, 241, 0.30) !important; }
1398
+ [data-theme="dark"] .border-indigo-200 { border-color: rgba(99, 102, 241, 0.35) !important; }
1399
+ [data-theme="dark"] .border-indigo-300 { border-color: rgba(99, 102, 241, 0.45) !important; }
1400
+ [data-theme="dark"] .text-slate-700 { color: #cbd5e1 !important; } /* slate-600/800 left for the Calculator LCD */
1401
+ [data-theme="dark"] .border-slate-300 { border-color: rgba(148, 163, 184, 0.35) !important; }
1402
+
1403
+ /* Blue — gaps beyond the accent remaps above. Hover/active steps sit one
1404
+ ladder step above their resting tint (base .26 → active .30; the /30../50
1405
+ alpha ladder steps 0.10/0.12/0.15 → hovers 0.12/0.15/0.18). */
1406
+ [data-theme="dark"] .bg-blue-200 { background-color: rgba(59, 130, 246, 0.35) !important; }
1407
+ [data-theme="dark"] .active\:bg-blue-200:active { background-color: rgba(59, 130, 246, 0.40) !important; }
1408
+ [data-theme="dark"] .bg-blue-200\/60 { background-color: rgba(59, 130, 246, 0.20) !important; }
1409
+ [data-theme="dark"] .hover\:bg-blue-100:hover { background-color: rgba(59, 130, 246, 0.26) !important; }
1410
+ [data-theme="dark"] .active\:bg-blue-50:active { background-color: rgba(59, 130, 246, 0.30) !important; }
1411
+ [data-theme="dark"] .hover\:bg-blue-50\/30:hover { background-color: rgba(59, 130, 246, 0.12) !important; }
1412
+ [data-theme="dark"] .hover\:bg-blue-50\/40:hover { background-color: rgba(59, 130, 246, 0.15) !important; }
1413
+ [data-theme="dark"] .hover\:bg-blue-50\/50:hover { background-color: rgba(59, 130, 246, 0.18) !important; }
1414
+ [data-theme="dark"] .border-blue-100 { border-color: rgba(59, 130, 246, 0.22) !important; }
1415
+ [data-theme="dark"] .border-blue-200,
1416
+ [data-theme="dark"] .hover\:border-blue-200:hover { border-color: rgba(59, 130, 246, 0.32) !important; }
1417
+ [data-theme="dark"] .border-blue-300,
1418
+ [data-theme="dark"] .hover\:border-blue-300:hover { border-color: rgba(59, 130, 246, 0.45) !important; }
1419
+ [data-theme="dark"] .border-blue-300\/50 { border-color: rgba(59, 130, 246, 0.25) !important; }
1420
+ [data-theme="dark"] .text-blue-900 { color: #bfdbfe !important; }
1421
+ [data-theme="dark"] .hover\:text-blue-600:hover { color: #93c5fd !important; }
1422
+ [data-theme="dark"] .hover\:text-blue-800:hover { color: #bfdbfe !important; }
1423
+ /* File-input buttons (file: compiles to ::file-selector-button) */
1424
+ [data-theme="dark"] .file\:bg-blue-50::file-selector-button { background-color: rgba(59, 130, 246, 0.26) !important; }
1425
+ [data-theme="dark"] .file\:text-blue-700::file-selector-button { color: #bfdbfe !important; }
1426
+ [data-theme="dark"] .hover\:file\:bg-blue-100:hover::file-selector-button { background-color: rgba(59, 130, 246, 0.26) !important; }
1427
+
1428
+ /* Gray interaction-state variants (separate class names from the bare remaps) */
1429
+ [data-theme="dark"] .hover\:text-gray-600:hover,
1430
+ [data-theme="dark"] .hover\:text-gray-700:hover { color: var(--ink) !important; }
1431
+ [data-theme="dark"] .hover\:text-gray-800:hover { color: var(--ink-strong) !important; }
1432
+ [data-theme="dark"] .hover\:text-gray-900:hover,
1433
+ [data-theme="dark"] .active\:text-gray-900:active { color: var(--ink-strongest) !important; }
1434
+ [data-theme="dark"] .active\:bg-gray-100:active { background-color: var(--surface-raised) !important; }
1435
+ [data-theme="dark"] .active\:bg-gray-200:active { background-color: var(--line-strong) !important; }
1436
+ [data-theme="dark"] .active\:bg-gray-300:active,
1437
+ [data-theme="dark"] .hover\:bg-gray-300:hover { background-color: #585b70 !important; }
1438
+ [data-theme="dark"] .disabled\:bg-gray-100:disabled { background-color: var(--surface) !important; }
1439
+ [data-theme="dark"] .disabled\:bg-gray-200:disabled { background-color: var(--surface-raised) !important; }
1440
+ [data-theme="dark"] .hover\:bg-gray-200\/50:hover { background-color: rgba(69, 71, 90, 0.5) !important; }
1441
+ [data-theme="dark"] .even\:bg-gray-50\/50:nth-child(even) { background-color: rgba(24, 24, 37, 0.5) !important; }
1442
+ [data-theme="dark"] .border-gray-50,
1443
+ [data-theme="dark"] .divide-gray-50 > :not(:last-child) { border-color: var(--line-subtle) !important; }
1444
+ [data-theme="dark"] .border-gray-200\/50 { border-color: rgba(49, 50, 68, 0.6) !important; }
1445
+ [data-theme="dark"] .hover\:border-gray-200:hover { border-color: var(--surface-raised) !important; }
1446
+ [data-theme="dark"] .hover\:border-gray-300:hover { border-color: var(--line-strong) !important; }
1447
+
1448
+ /* White panel surfaces / solid black ink. hover-to-white means "emphasize",
1449
+ which on dark glass means a solid panel tint; hover-to-black ink flips to
1450
+ brighter, not darker (e.g. the taskbar bell). */
1451
+ [data-theme="dark"] .bg-white\/85 { background-color: rgba(30, 30, 46, 0.85) !important; }
1452
+ [data-theme="dark"] .hover\:bg-white:hover { background-color: var(--surface) !important; }
1453
+ [data-theme="dark"] .text-black { color: var(--ink-strongest) !important; }
1454
+ [data-theme="dark"] .hover\:text-black:hover { color: #ffffff !important; }
1455
+
1456
+ /* Markdown — the clamped note (react-os-shell/markdown, `clamp`).
1457
+
1458
+ The fade MASKS the text rather than laying a coloured gradient over it. A
1459
+ gradient has to know what colour to fade to, and there is no answer that
1460
+ survives a tinted accent theme or dark mode — it shows as a pale band across
1461
+ the bottom of the note. A mask fades the ink itself and needs to know
1462
+ nothing about the surface.
1463
+
1464
+ `.rosmd-fade` is applied only when the clamped content actually OVERFLOWS,
1465
+ measured after layout in the component: a note that is long in characters
1466
+ but short on screen would otherwise get a fade under nothing. */
1467
+ .rosmd-clamp {
1468
+ max-height: 8rem;
1469
+ overflow: hidden;
1470
+ }
1471
+ .rosmd-fade {
1472
+ -webkit-mask-image: linear-gradient(to bottom, black calc(100% - 1.75rem), transparent);
1473
+ mask-image: linear-gradient(to bottom, black calc(100% - 1.75rem), transparent);
1474
+ }
1475
+
1476
+ /* Scrollbar */
1477
+ /* Force visible scrollbars on editable grids (macOS hides them by default) */
1478
+ .grid-scroll::-webkit-scrollbar { width: 10px; height: 10px; }
1479
+ .grid-scroll::-webkit-scrollbar-track { background: #f1f5f9; }
1480
+ .grid-scroll::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 5px; border: 2px solid #f1f5f9; }
1481
+ .grid-scroll::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
1482
+ .grid-scroll::-webkit-scrollbar-corner { background: #f1f5f9; }
1483
+
1484
+ [data-theme="dark"] ::-webkit-scrollbar { width: 8px; height: 8px; }
1485
+ [data-theme="dark"] ::-webkit-scrollbar-track { background: var(--surface-sunken); }
1486
+ [data-theme="dark"] ::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 4px; }
1487
+ [data-theme="dark"] ::-webkit-scrollbar-thumb:hover { background: #585b70; }
1488
+
1489
+ /* ── Opacity-modified utilities ───────────────────────────────────────────
1490
+ A Tailwind opacity modifier makes a DIFFERENT class name, so every remap in
1491
+ this sheet — all written against bare class names — stops at the base. The
1492
+ same escape the `!` important-modifier makes for `.\!bg-blue-*` above.
1493
+
1494
+ Everything below is a class whose BASE this sheet already remaps: the colour
1495
+ was already judged to need a dark treatment, and only the modifier escaped.
1496
+ The values are the ones a consumer portal had to hand-write locally and has
1497
+ been shipping, so they are tuned rather than derived: a `/70` of a tint does
1498
+ NOT take 70% of the tint's alpha, because a proportional slice of an already
1499
+ subtle tint disappears. Gray is the exception and does take a real alpha,
1500
+ because there the utility paints a surface rather than a tint over one. */
1501
+ [data-theme="dark"] .bg-gray-50\/70 { background-color: rgba(24, 24, 37, 0.7) !important; }
1502
+ [data-theme="dark"] .bg-gray-50\/80 { background-color: rgba(24, 24, 37, 0.8) !important; }
1503
+ [data-theme="dark"] .bg-gray-50\/95 { background-color: rgba(24, 24, 37, 0.95) !important; }
1504
+ [data-theme="dark"] .bg-gray-100\/90 { background-color: rgba(30, 30, 46, 0.9) !important; }
1505
+ [data-theme="dark"] .bg-gray-200\/80 { background-color: rgba(49, 50, 68, 0.8) !important; }
1506
+ [data-theme="dark"] .hover\:bg-gray-200\/70:hover { background-color: rgba(49, 50, 68, 0.7) !important; }
1507
+ [data-theme="dark"] .hover\:bg-gray-200\/90:hover { background-color: rgba(49, 50, 68, 0.9) !important; }
1508
+ [data-theme="dark"] .border-gray-200\/60 { border-color: rgba(69, 71, 90, 0.6) !important; }
1509
+ [data-theme="dark"] .bg-blue-50\/80 { background-color: rgba(59, 130, 246, 0.24) !important; }
1510
+ [data-theme="dark"] .bg-blue-50\/90 { background-color: rgba(59, 130, 246, 0.26) !important; }
1511
+ [data-theme="dark"] .text-blue-600\/70 { color: rgba(147, 197, 253, 0.7) !important; }
1512
+ [data-theme="dark"] .text-blue-700\/70 { color: rgba(191, 219, 254, 0.7) !important; }
1513
+ [data-theme="dark"] .text-blue-800\/80 { color: rgba(191, 219, 254, 0.8) !important; }
1514
+ [data-theme="dark"] .text-blue-900\/60 { color: rgba(191, 219, 254, 0.6) !important; }
1515
+ [data-theme="dark"] .bg-amber-200\/70 { background-color: rgba(245, 158, 11, 0.20) !important; }
1516
+ [data-theme="dark"] .hover\:bg-amber-50\/70:hover { background-color: rgba(245, 158, 11, 0.12) !important; }
1517
+ [data-theme="dark"] .text-amber-800\/80 { color: rgba(253, 230, 138, 0.8) !important; }
1518
+ [data-theme="dark"] .text-green-700\/80 { color: rgba(134, 239, 172, 0.8) !important; }