@fiestaboard/ui 3.3.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/board/board-backdrop.d.ts +60 -0
- package/dist/components/board/board-backdrop.js +71 -0
- package/dist/components/board/board-backdrop.js.map +1 -0
- package/dist/components/board/board-display.js +123 -123
- package/dist/components/board/board-display.js.map +1 -1
- package/dist/components/board/static-board-display.js +32 -32
- package/dist/components/chrome/fiesta-logo.js +2 -2
- package/dist/components/chrome/fiesta-logo.js.map +1 -1
- package/dist/components/chrome/page-header.d.ts +66 -4
- package/dist/components/chrome/page-header.js +31 -11
- package/dist/components/chrome/page-header.js.map +1 -1
- package/dist/components/chrome/page-layout.js +1 -1
- package/dist/components/chrome/page-layout.js.map +1 -1
- package/dist/components/chrome/sidebar.d.ts +2 -5
- package/dist/components/chrome/sidebar.js +152 -173
- package/dist/components/chrome/sidebar.js.map +1 -1
- package/dist/components/chrome/theme-toggle.d.ts +37 -10
- package/dist/components/chrome/theme-toggle.js +25 -12
- package/dist/components/chrome/theme-toggle.js.map +1 -1
- package/dist/components/containment/json-tree.js +2 -2
- package/dist/components/containment/json-tree.js.map +1 -1
- package/dist/components/editor/node-views/wrapped-text-view.js +1 -1
- package/dist/components/editor/node-views/wrapped-text-view.js.map +1 -1
- package/dist/components/editor/variable-picker-content.js +4 -4
- package/dist/components/feedback/alert.js +4 -4
- package/dist/components/feedback/alert.js.map +1 -1
- package/dist/components/feedback/badge.js +4 -4
- package/dist/components/feedback/badge.js.map +1 -1
- package/dist/components/forms/button.js +5 -5
- package/dist/components/forms/button.js.map +1 -1
- package/dist/components/layout/flex.d.ts +1 -1
- package/dist/components/layout/grid.d.ts +1 -1
- package/dist/components/layout/stack.d.ts +1 -1
- package/dist/components/plugin/board-showcase.js +2 -2
- package/dist/components/plugin/board-showcase.js.map +1 -1
- package/dist/components/plugin/plugin-category-badge.d.ts +27 -1
- package/dist/components/plugin/plugin-category-badge.js +18 -14
- package/dist/components/plugin/plugin-category-badge.js.map +1 -1
- package/dist/components/typography/text.js +1 -1
- package/dist/components/typography/text.js.map +1 -1
- package/dist/components/wizard/wizard-progress.d.ts +29 -0
- package/dist/components/wizard/wizard-progress.js +27 -0
- package/dist/components/wizard/wizard-progress.js.map +1 -0
- package/dist/components/wizard/wizard-shell.d.ts +63 -0
- package/dist/components/wizard/wizard-shell.js +69 -0
- package/dist/components/wizard/wizard-shell.js.map +1 -0
- package/dist/index.d.ts +3 -5
- package/dist/index.js +90 -92
- package/dist/theme.css +621 -503
- package/package.json +2 -4
- package/dist/components/effects/aurora.d.ts +0 -9
- package/dist/components/effects/aurora.js +0 -121
- package/dist/components/effects/aurora.js.map +0 -1
- package/dist/components/effects/react-bits/decrypted-text.d.ts +0 -14
- package/dist/components/effects/react-bits/decrypted-text.js +0 -88
- package/dist/components/effects/react-bits/decrypted-text.js.map +0 -1
- package/dist/components/seasons/aurora-canvas.d.ts +0 -22
- package/dist/components/seasons/aurora-canvas.js +0 -91
- package/dist/components/seasons/aurora-canvas.js.map +0 -1
- package/dist/components/seasons/sidebar-aurora-horizontal.d.ts +0 -3
- package/dist/components/seasons/sidebar-aurora-horizontal.js +0 -47
- package/dist/components/seasons/sidebar-aurora-horizontal.js.map +0 -1
- package/dist/components/seasons/sidebar-aurora.d.ts +0 -3
- package/dist/components/seasons/sidebar-aurora.js +0 -57
- package/dist/components/seasons/sidebar-aurora.js.map +0 -1
- package/dist/lib/seasons-drafts.d.ts +0 -26
- package/dist/lib/seasons-drafts.js +0 -106
- package/dist/lib/seasons-drafts.js.map +0 -1
- package/dist/lib/seasons.d.ts +0 -52
- package/dist/lib/seasons.js +0 -59
- package/dist/lib/seasons.js.map +0 -1
- package/dist/seasons/christmas.css +0 -64
- package/dist/seasons/easter.css +0 -62
- package/dist/seasons/fathers-day.css +0 -62
- package/dist/seasons/halloween.css +0 -69
- package/dist/seasons/mothers-day.css +0 -63
- package/dist/seasons/new-year.css +0 -66
- package/dist/seasons/pride.css +0 -68
- package/dist/seasons/thanksgiving.css +0 -64
package/dist/theme.css
CHANGED
|
@@ -15,15 +15,6 @@
|
|
|
15
15
|
* token values here without a parity check on the consumer side.
|
|
16
16
|
*/
|
|
17
17
|
@import "tw-animate-css";
|
|
18
|
-
/* Seasonal themes — one file per season, inert unless the season class
|
|
19
|
-
is stamped on <html>. Only the LIVE season (pride) is inlined here so
|
|
20
|
-
consumers get it automatically; the design drafts are NOT imported —
|
|
21
|
-
they'd ship ~18 KB of rule families keyed on classes the app never
|
|
22
|
-
stamps. A consumer that promotes a draft opts in with:
|
|
23
|
-
@import "@fiestaboard/ui/seasons/<id>.css";
|
|
24
|
-
(see README "Seasonal theming"). Storybook imports every draft
|
|
25
|
-
directly in storybook.css so the Season toolbar can preview them. */
|
|
26
|
-
@import "./seasons/pride.css";
|
|
27
18
|
|
|
28
19
|
@custom-variant dark (&:is(.dark *));
|
|
29
20
|
|
|
@@ -104,6 +95,14 @@
|
|
|
104
95
|
--color-category-home: var(--category-home);
|
|
105
96
|
--color-category-home-foreground: var(--category-home-foreground);
|
|
106
97
|
|
|
98
|
+
/* The six brand hues at an ink lightness — icons, rules, text. */
|
|
99
|
+
--color-hue-red: var(--hue-red);
|
|
100
|
+
--color-hue-orange: var(--hue-orange);
|
|
101
|
+
--color-hue-yellow: var(--hue-yellow);
|
|
102
|
+
--color-hue-green: var(--hue-green);
|
|
103
|
+
--color-hue-blue: var(--hue-blue);
|
|
104
|
+
--color-hue-violet: var(--hue-violet);
|
|
105
|
+
|
|
107
106
|
/* Overlay */
|
|
108
107
|
--color-overlay: var(--overlay);
|
|
109
108
|
|
|
@@ -111,6 +110,13 @@
|
|
|
111
110
|
--color-brand: var(--brand);
|
|
112
111
|
--color-brand-foreground: var(--brand-foreground);
|
|
113
112
|
--color-brand-emphasis: var(--brand-emphasis);
|
|
113
|
+
/* The literal tile. Field only — see the brand note in :root. */
|
|
114
|
+
--color-brand-tile: var(--brand-tile);
|
|
115
|
+
--color-primary-hover: var(--primary-hover);
|
|
116
|
+
--color-primary-active: var(--primary-active);
|
|
117
|
+
--color-ring-edge: var(--ring-edge);
|
|
118
|
+
--color-brand-tile-foreground: var(--brand-tile-foreground);
|
|
119
|
+
--color-brand-hover: var(--brand-hover);
|
|
114
120
|
|
|
115
121
|
/* Navigation active state (indigo) */
|
|
116
122
|
--color-nav-active: var(--nav-active);
|
|
@@ -208,12 +214,31 @@
|
|
|
208
214
|
Declared next to the palette it belongs to, once per theme. */
|
|
209
215
|
color-scheme: light;
|
|
210
216
|
--radius: 0.625rem;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
+
/* ============================================================
|
|
218
|
+
* HUE LOCK (#231). Every chromatic token in this file resolves to one
|
|
219
|
+
* of the SIX hues the hardware can physically flip, converted from
|
|
220
|
+
* lib/board-colors.ts:
|
|
221
|
+
*
|
|
222
|
+
* red 29 · orange 73 · yellow 104 · green 133 · blue 251 · violet 315
|
|
223
|
+
*
|
|
224
|
+
* Hue is theme-invariant: a semantic token keeps its hue in light and
|
|
225
|
+
* dark, and only LIGHTNESS moves, set by the contrast target. Before
|
|
226
|
+
* this, the palette carried ~20 hue angles with no ramp behind them and
|
|
227
|
+
* --warning swung 25deg between themes, so a token could re-encode its
|
|
228
|
+
* own meaning on a theme toggle.
|
|
229
|
+
*
|
|
230
|
+
* Surfaces are no longer chroma-0: they carry a 0.003-0.016 warm cast at
|
|
231
|
+
* hue 73 (the board's orange) so the neutral 90% of the product reads as
|
|
232
|
+
* chosen rather than inherited from stock shadcn.
|
|
233
|
+
*
|
|
234
|
+
* Do not add a seventh hue. token-registry.ts asserts this in CI.
|
|
235
|
+
* ============================================================ */
|
|
236
|
+
--background: oklch(0.965 0.003 73);
|
|
237
|
+
--foreground: oklch(0.16 0.008 73);
|
|
238
|
+
--card: oklch(0.995 0.003 73);
|
|
239
|
+
--card-foreground: oklch(0.16 0.008 73);
|
|
240
|
+
--popover: oklch(0.995 0.003 73);
|
|
241
|
+
--popover-foreground: oklch(0.16 0.008 73);
|
|
217
242
|
/* ============================================================
|
|
218
243
|
* INVARIANT — "one token means 'this control is on'" (#156/#159).
|
|
219
244
|
* --primary is the single pigment that says a control is the primary
|
|
@@ -224,18 +249,48 @@
|
|
|
224
249
|
* into separate hues, and do not let --primary drift to a neutral —
|
|
225
250
|
* that is what made the dark theme brandless (see .dark below).
|
|
226
251
|
* --primary carries the FiestaBoard warm orange in BOTH themes.
|
|
252
|
+
*
|
|
253
|
+
* --primary IS THE TILE, and the label on it is board ink (#231).
|
|
254
|
+
*
|
|
255
|
+
* It was oklch(0.52 0.111 73) = #8f5d00 — the board's orange darkened until
|
|
256
|
+
* a WHITE label cleared AA on it. But maxC(0.52, 73) is exactly 0.111, so
|
|
257
|
+
* that value was not a badly-tuned orange, it was the most orange anything
|
|
258
|
+
* can be at that lightness: a mustard. The brand had been WCAG-darkened into
|
|
259
|
+
* a different colour, which is the failure the audit opened with.
|
|
260
|
+
*
|
|
261
|
+
* The white label was the only reason it had to be dark, and the white label
|
|
262
|
+
* was never designed — it is stock shadcn's --primary-foreground that nobody
|
|
263
|
+
* re-asked when the FiestaBoard palette landed. The dark theme had already
|
|
264
|
+
* worked this out and used board ink.
|
|
265
|
+
*
|
|
266
|
+
* So: --primary is the literal #f5a623, theme-invariant, with board ink on
|
|
267
|
+
* it at 8.84:1 — better than white on #8f5d00 ever managed (5.63:1). The
|
|
268
|
+
* accessible option was the MORE saturated one the whole time. It also makes
|
|
269
|
+
* the most-clicked pixel in the product the exact colour the hardware flips.
|
|
270
|
+
*
|
|
271
|
+
* White on this field is 2.03:1 and is banned; there is exactly one
|
|
272
|
+
* on-primary foreground token so that is enforceable.
|
|
227
273
|
* ============================================================ */
|
|
228
|
-
--primary:
|
|
229
|
-
--primary-foreground: oklch(0.
|
|
230
|
-
|
|
231
|
-
--
|
|
232
|
-
--
|
|
233
|
-
/*
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
--
|
|
237
|
-
--
|
|
238
|
-
--
|
|
274
|
+
--primary: #f5a623;
|
|
275
|
+
--primary-foreground: oklch(0.205 0.008 73);
|
|
276
|
+
/* Hover RAISES label contrast (8.84 -> 10.78); press lowers it to 7.04. */
|
|
277
|
+
--primary-hover: oklch(0.84 0.128 73);
|
|
278
|
+
--primary-active: oklch(0.72 0.153 73);
|
|
279
|
+
/* Board ink hairline that bounds the focus ring. The ring is --primary, and
|
|
280
|
+
an orange band is only 1.83:1 against a light page, so the INK carries the
|
|
281
|
+
boundary and the orange carries the identity. */
|
|
282
|
+
--ring-edge: oklch(0.205 0.008 73);
|
|
283
|
+
--secondary: oklch(0.925 0.003 73);
|
|
284
|
+
--secondary-foreground: oklch(0.16 0.008 73);
|
|
285
|
+
--muted: oklch(0.935 0.003 73);
|
|
286
|
+
/* Muted text needs >=4.5:1 contrast vs --background to meet WCAG AA.
|
|
287
|
+
8.32:1 on --background, 9.07:1 on --card, 7.61:1 on --muted. */
|
|
288
|
+
--muted-foreground: oklch(0.4 0.006 73);
|
|
289
|
+
--accent: oklch(0.945 0.003 73);
|
|
290
|
+
--accent-foreground: oklch(0.16 0.008 73);
|
|
291
|
+
/* Board red. 5.36:1 on --background; white label 5.93:1. */
|
|
292
|
+
--destructive: oklch(0.52 0.16 29);
|
|
293
|
+
--destructive-foreground: oklch(1 0 0);
|
|
239
294
|
/* ============================================================
|
|
240
295
|
* --border vs --input: TWO DIFFERENT INTENTS, NOT ONE VALUE (#161).
|
|
241
296
|
*
|
|
@@ -255,64 +310,231 @@
|
|
|
255
310
|
* Derived from --foreground as an alpha fraction so the boundary
|
|
256
311
|
* tracks the text color in every theme and season instead of being a
|
|
257
312
|
* hand-tuned grey per palette. Percentages are the smallest that clear
|
|
258
|
-
* 3:1 on both surfaces with headroom (light
|
|
259
|
-
* dark
|
|
260
|
-
* for light-on-dark than dark-on-light.
|
|
313
|
+
* 3:1 on both surfaces with headroom (light 50% -> 3.55/3.62,
|
|
314
|
+
* dark 40% -> 3.51/3.56); they differ because contrast ramps faster
|
|
315
|
+
* for light-on-dark than dark-on-light. They moved up 2 points with the
|
|
316
|
+
* hue lock because --foreground itself moved.
|
|
317
|
+
*
|
|
318
|
+
* --border now joins the same derivation instead of being a hand-tuned
|
|
319
|
+
* grey, so decoration tracks the text through every theme and season too.
|
|
261
320
|
*
|
|
262
321
|
* DO NOT collapse these two tokens into one value in either direction.
|
|
263
322
|
* ============================================================ */
|
|
264
|
-
--border: oklch(
|
|
265
|
-
--input: color-mix(in oklch, var(--foreground)
|
|
266
|
-
--ring:
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
--
|
|
290
|
-
--
|
|
291
|
-
--
|
|
292
|
-
--
|
|
293
|
-
--
|
|
294
|
-
--
|
|
295
|
-
|
|
296
|
-
--
|
|
297
|
-
--
|
|
298
|
-
|
|
299
|
-
--
|
|
300
|
-
--
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
--
|
|
311
|
-
--
|
|
312
|
-
|
|
313
|
-
--
|
|
314
|
-
--
|
|
315
|
-
--
|
|
323
|
+
--border: color-mix(in oklch, var(--foreground) 11%, transparent);
|
|
324
|
+
--input: color-mix(in oklch, var(--foreground) 50%, transparent);
|
|
325
|
+
--ring: var(--primary); /* = --primary, see the invariant above */
|
|
326
|
+
/* ============================================================
|
|
327
|
+
* THE RAIL FOLLOWS THE THEME (#231).
|
|
328
|
+
*
|
|
329
|
+
* It was theme-invariant for a while — one warm graphite rail in both
|
|
330
|
+
* modes, argued as "a fixture, like the board bezel". That argument was
|
|
331
|
+
* measured and it does not hold: the same hex is 14.87:1 against the
|
|
332
|
+
* light page and 1.20:1 against the dark one. It was never invariant in
|
|
333
|
+
* the way that matters. In light mode it was the single highest-contrast
|
|
334
|
+
* object on the screen, which is the exact "the chrome out-shouts the
|
|
335
|
+
* product" fault that got the gradient deleted.
|
|
336
|
+
*
|
|
337
|
+
* So light mode gets a light rail at L 0.93 — a step DOWN from the page,
|
|
338
|
+
* not up. Chrome separation in light mode is always a low ratio (1.11:1
|
|
339
|
+
* here) and that is correct: the edge is carried by the derived border,
|
|
340
|
+
* not by a luminance cliff. Going lighter than the page instead lands at
|
|
341
|
+
* 1.03-1.05:1, which is genuinely invisible.
|
|
342
|
+
*
|
|
343
|
+
* The fork is deliberately ONE PAIR — this token and its foreground.
|
|
344
|
+
* Everything else on the rail derives from --sidebar-foreground (see
|
|
345
|
+
* .sidebar-gradient), which is what stops this from re-becoming the
|
|
346
|
+
* many-valued fork that shipped black labels onto a dark rail.
|
|
347
|
+
* ============================================================ */
|
|
348
|
+
--sidebar: oklch(0.93 0.003 73);
|
|
349
|
+
--sidebar-foreground: oklch(0.16 0.008 73);
|
|
350
|
+
--sidebar-accent: oklch(1 0 0 / 14%);
|
|
351
|
+
--sidebar-accent-foreground: oklch(0.985 0.002 73);
|
|
352
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
353
|
+
--sidebar-ring: var(--primary); /* = --primary, see the invariant above */
|
|
354
|
+
/* Board blue. 5.01:1 on --background; white label 5.54:1. */
|
|
355
|
+
--info: oklch(0.52 0.151 251);
|
|
356
|
+
--info-foreground: oklch(1 0 0);
|
|
357
|
+
/* Board green. 4.76:1 on --background; white label 5.27:1. */
|
|
358
|
+
--success: oklch(0.52 0.147 133);
|
|
359
|
+
--success-foreground: oklch(1 0 0);
|
|
360
|
+
/* Board yellow. --warning and --brand used to sit 5deg apart on the hue
|
|
361
|
+
wheel and were held apart by lightness alone (1.61:1). Under the hue
|
|
362
|
+
lock they are 31deg apart — genuinely different pigments — so the
|
|
363
|
+
separation no longer depends on a lightness budget that light mode
|
|
364
|
+
could not afford. 1.56:1 of luminance remains between them.
|
|
365
|
+
NOTE: yellow cannot be both dark enough to read as text on white and
|
|
366
|
+
light enough to read as yellow. --warning is therefore a FILL, ICON
|
|
367
|
+
and BORDER color, never a text color: its label is board ink at 4.93:1.
|
|
368
|
+
Alert enforces this — status color never touches a sentence. */
|
|
369
|
+
--warning: oklch(0.62 0.131 104);
|
|
370
|
+
--warning-foreground: oklch(0.205 0.008 73);
|
|
371
|
+
--tag-variable: oklch(0.52 0.16 315);
|
|
372
|
+
--tag-variable-foreground: oklch(0.4 0.1 315);
|
|
373
|
+
--tag-success: oklch(0.52 0.147 133);
|
|
374
|
+
--tag-success-foreground: oklch(0.4 0.1 133);
|
|
375
|
+
--tag-formula: oklch(0.52 0.151 251);
|
|
376
|
+
--tag-formula-foreground: oklch(0.4 0.1 251);
|
|
377
|
+
/* ============================================================
|
|
378
|
+
* PLUGIN CATEGORIES ARE LITERAL TILES (#231), theme-invariant.
|
|
379
|
+
*
|
|
380
|
+
* These are the exact hardware hexes from --color-board-* above, not an
|
|
381
|
+
* approximation of them: a physical tile does not have a theme, and it
|
|
382
|
+
* does not have a rounding error either. They are consumed ONLY by
|
|
383
|
+
* PluginCategoryBadge, which renders them as a tile square on the board's
|
|
384
|
+
* own dark surface — never as a text color, never as a page fill.
|
|
385
|
+
*
|
|
386
|
+
* The old set shared ONE lightness across all seven, so the badges
|
|
387
|
+
* measured 1.001:1 against each other and collapsed completely under
|
|
388
|
+
* deuteranopia. The tiles span 4.21:1 to 19.65:1 against board black, so
|
|
389
|
+
* six of the seven separate on lightness alone with hue removed.
|
|
390
|
+
* The exception is home/orange vs transit/green (dL 0.001) — that pair is
|
|
391
|
+
* told apart by the word beside the tile, not by the tile.
|
|
392
|
+
*
|
|
393
|
+
* The mapping is NOT claimed to come from the hardware docs, which
|
|
394
|
+
* publish a temperature-and-status vocabulary (red = alerts and hot),
|
|
395
|
+
* not a content taxonomy. It is argued on its own terms: transit is go,
|
|
396
|
+
* weather is sky, art is the brightest pigment the board owns, and
|
|
397
|
+
* utility is the white tile because a utility should be the quietest
|
|
398
|
+
* loud thing in the set.
|
|
399
|
+
* ============================================================ */
|
|
400
|
+
--category-entertainment: #eb4034; /* board red */
|
|
401
|
+
--category-entertainment-foreground: oklch(0.52 0.16 29);
|
|
402
|
+
--category-home: #f5a623; /* board orange */
|
|
403
|
+
--category-home-foreground: oklch(0.52 0.111 73);
|
|
404
|
+
--category-art: #f8e71c; /* board yellow */
|
|
405
|
+
--category-art-foreground: oklch(0.52 0.11 104);
|
|
406
|
+
--category-transit: #7ed321; /* board green */
|
|
407
|
+
--category-transit-foreground: oklch(0.52 0.147 133);
|
|
408
|
+
--category-weather: #4a90d9; /* board blue */
|
|
409
|
+
--category-weather-foreground: oklch(0.52 0.151 251);
|
|
410
|
+
--category-data: #9b59b6; /* board violet */
|
|
411
|
+
--category-data-foreground: oklch(0.52 0.16 315);
|
|
412
|
+
--category-utility: #ffffff; /* board white */
|
|
413
|
+
--category-utility-foreground: oklch(0.4 0.014 73);
|
|
414
|
+
/* ============================================================
|
|
415
|
+
* THE SIX HUES AT AN INK LIGHTNESS (#231).
|
|
416
|
+
*
|
|
417
|
+
* The --category-* tokens above are the literal tiles: correct on the
|
|
418
|
+
* board's black surface, useless anywhere else. Board yellow on light
|
|
419
|
+
* paper is 1.15:1 and board orange is 1.83:1 — neither can draw a 2px
|
|
420
|
+
* icon stroke, let alone text.
|
|
421
|
+
*
|
|
422
|
+
* These are the same six hues moved to the ink plateau, where they can.
|
|
423
|
+
* Every one clears 4.7:1 on --background and 5.1:1 on --card in light,
|
|
424
|
+
* and 9.2:1 / 8.5:1 in dark — so they are legal for icons, rules, and
|
|
425
|
+
* text alike. Hue is identical to the tile; only lightness differs.
|
|
426
|
+
*
|
|
427
|
+
* This is what a surface reaches for when it wants "one of the brand's
|
|
428
|
+
* colours" and is not a board.
|
|
429
|
+
* ============================================================ */
|
|
430
|
+
--hue-red: oklch(0.52 0.16 29);
|
|
431
|
+
--hue-orange: oklch(0.52 0.111 73);
|
|
432
|
+
--hue-yellow: oklch(0.52 0.11 104);
|
|
433
|
+
--hue-green: oklch(0.52 0.147 133);
|
|
434
|
+
--hue-blue: oklch(0.52 0.151 251);
|
|
435
|
+
--hue-violet: oklch(0.52 0.16 315);
|
|
436
|
+
/* Scrim. In LIGHT a dialog reads 5.39:1 against the scrimmed page.
|
|
437
|
+
In DARK that number is meaningless — WCAG ratio compresses to ~1.3:1
|
|
438
|
+
between any two near-black surfaces no matter how they are tuned — so
|
|
439
|
+
modality in dark is carried by --elevation-modal's ring and shadow,
|
|
440
|
+
not by a surface contrast ratio. Do not "fix" the dark figure; it is
|
|
441
|
+
not measuring what it looks like it is measuring. */
|
|
442
|
+
--overlay: oklch(0.16 0.012 73 / 60%);
|
|
443
|
+
/* ============================================================
|
|
444
|
+
* BRAND: ONE PIGMENT, THREE JOBS (#231).
|
|
445
|
+
*
|
|
446
|
+
* --brand is an INVARIANT ALIAS of --primary, declared the same way
|
|
447
|
+
* --ring is. They used to be two tokens that were byte-identical in
|
|
448
|
+
* light and 0.17 lightness apart in dark, so a consumer picked a variant
|
|
449
|
+
* in light mode and the theme toggle silently changed their mind. One
|
|
450
|
+
* pigment, stated as a rule, is the honest version of that.
|
|
451
|
+
*
|
|
452
|
+
* --brand-tile is the LITERAL #f5a623 tile, identical in both themes,
|
|
453
|
+
* and is a FIELD ONLY. It is 1.83:1 against a light page, so it can be a
|
|
454
|
+
* button face but can never be a link or a text color; its label must be
|
|
455
|
+
* --brand-tile-foreground (board ink, 8.83:1), never white (2.03:1).
|
|
456
|
+
*
|
|
457
|
+
* --brand-hover always moves TOWARD --foreground, so hover increases
|
|
458
|
+
* contrast in both themes (light 5.08 -> 6.51, dark 8.34 -> 9.51)
|
|
459
|
+
* instead of decreasing it, which is what the old --brand-emphasis
|
|
460
|
+
* hover did on the docs site (9.86 -> 3.99, below AA).
|
|
461
|
+
* ============================================================ */
|
|
462
|
+
/* --brand is the TEXT step and is no longer an alias of --primary. Now that
|
|
463
|
+
--primary is the literal tile, it is 1.83:1 on a light page — legal as a
|
|
464
|
+
field, illegal as a link. --hue-orange is the same hue at the ink plateau
|
|
465
|
+
(5.08:1 light / 9.63:1 dark), which is what body copy and links need. */
|
|
466
|
+
--brand: var(--hue-orange);
|
|
467
|
+
--brand-foreground: oklch(0.205 0.008 73);
|
|
468
|
+
--brand-tile: var(--primary); /* DEPRECATED alias — same pigment now */
|
|
469
|
+
/* ============================================================
|
|
470
|
+
* THE WORDMARK IS A LOGOTYPE, and WCAG 1.4.3 exempts logotypes from
|
|
471
|
+
* contrast outright: "text that is part of a logo or brand name has no
|
|
472
|
+
* minimum contrast requirement."
|
|
473
|
+
*
|
|
474
|
+
* It was reading --brand, which is --hue-orange — the token that exists
|
|
475
|
+
* SPECIFICALLY to clear AA as body text. So the one element the spec
|
|
476
|
+
* excuses was the one held to the strictest version of the rule, and it
|
|
477
|
+
* paid in saturation: #8f5d00, a dark ochre, sitting directly beside a
|
|
478
|
+
* taco mark full of bright orange. The wordmark looked duller than the
|
|
479
|
+
* icon it is locked up with.
|
|
480
|
+
*
|
|
481
|
+
* Freed from a floor it never had, it goes where it belongs — in the
|
|
482
|
+
* middle of the mark's own shell tones. 2.60:1 on the light rail.
|
|
483
|
+
* ============================================================ */
|
|
484
|
+
--brand-wordmark: oklch(0.66 0.141 73);
|
|
485
|
+
/* The backdrop is a DARK board in both themes, so light mode needs the
|
|
486
|
+
lower number: a full-strength wall of black tiles on paper stops being a
|
|
487
|
+
background and becomes the subject, with the card marooned in a hole
|
|
488
|
+
punched through it.
|
|
489
|
+
At this strength the field reads as TEXTURE rather than as text, and that
|
|
490
|
+
is the right call once it covers the whole viewport — the grid carries the
|
|
491
|
+
brand on its own, and legible words wallpapered edge to edge would compete
|
|
492
|
+
with the form the screen exists to get filled in. */
|
|
493
|
+
--board-backdrop-opacity: 0.16;
|
|
494
|
+
--brand-tile-foreground: oklch(0.205 0.008 73);
|
|
495
|
+
--brand-hover: color-mix(in oklch, var(--brand) 82%, var(--foreground));
|
|
496
|
+
/* DEPRECATED alias, removed in the next minor. Kept so consumers pinned
|
|
497
|
+
to an older minor — and the docs site's build canary, which greps the
|
|
498
|
+
compiled CSS for `bg-brand-emphasis` and exits(1) if it is missing —
|
|
499
|
+
do not break on the same commit that changes every value. */
|
|
500
|
+
--brand-emphasis: var(--brand);
|
|
501
|
+
/* Navigation active state — board violet, theme-invariant.
|
|
502
|
+
Was indigo-600, the only cool accent in a gold-and-terracotta brand
|
|
503
|
+
and the one token in the file carrying no rationale. White label
|
|
504
|
+
9.11:1. The fill is only 1.81:1 against the rail, so the 3:1 state
|
|
505
|
+
indicator SC 1.4.11 asks for is carried by the gold left marker on
|
|
506
|
+
.nav-active, not by the fill.
|
|
507
|
+
|
|
508
|
+
NO LONGER TRUE, and the marker is gone with it. The pill was dark violet
|
|
509
|
+
because it carried a WHITE label, which forced it down to 1.81:1 against
|
|
510
|
+
the rail — under SC 1.4.11's 3:1 for a state — and the gold bar was a
|
|
511
|
+
patch for that number, not a design. Flipping the label to board ink frees
|
|
512
|
+
the fill to go bright — and the fill it goes to is --primary itself, so
|
|
513
|
+
"the current page" becomes the same pigment as "the focused control" and
|
|
514
|
+
"the primary action". That is the #156/#159 invariant finally covering the
|
|
515
|
+
thing it always claimed to. 8.12:1 against the rail, ink label 8.84:1, so
|
|
516
|
+
the fill carries the state on its own: 1.4.11 asks for 3:1, not for a
|
|
517
|
+
marker, and does not care which pixels supply it.
|
|
518
|
+
|
|
519
|
+
A bright violet also cleared the floor (5.66:1) and was rejected on
|
|
520
|
+
hierarchy, not on arithmetic: it became the loudest object on the screen,
|
|
521
|
+
louder than the primary action, which is backwards for a nav state. */
|
|
522
|
+
/* THE ONE PLACE THE PILL CANNOT BE --primary. The tile is L 0.78, so it
|
|
523
|
+
needs a dark surround: against a light rail it tops out at 2.00:1 even
|
|
524
|
+
if the rail were pure white, under 1.4.11's 3:1 for a state. On a light
|
|
525
|
+
rail the pill inverts instead — ink fill, page-coloured label, 16.78:1
|
|
526
|
+
against the rail. The dark rail keeps the tile (8.12:1), so "one pigment
|
|
527
|
+
means on" still holds everywhere it physically can. */
|
|
528
|
+
--nav-active: var(--foreground);
|
|
529
|
+
--nav-active-foreground: var(--background);
|
|
530
|
+
--nav-active-hover: oklch(1 0 0 / 14%);
|
|
531
|
+
/* Fiesta gradient palette — three board hues at ONE lightness (0.46),
|
|
532
|
+
identical in both themes. Because that surface never changes luminance
|
|
533
|
+
across its 117s loop, the contrast of anything sitting on it is a
|
|
534
|
+
constant rather than a function of wall-clock time. */
|
|
535
|
+
--fiesta-red: oklch(0.46 0.15 29);
|
|
536
|
+
--fiesta-orange: oklch(0.46 0.098 73);
|
|
537
|
+
--fiesta-purple: oklch(0.46 0.15 315);
|
|
316
538
|
/* Page icon gradient stops — 6 vars so pride month can expand to a full rainbow */
|
|
317
539
|
--icon-g1: var(--fiesta-red);
|
|
318
540
|
--icon-g2: var(--fiesta-orange);
|
|
@@ -320,10 +542,12 @@
|
|
|
320
542
|
--icon-g4: var(--fiesta-orange);
|
|
321
543
|
--icon-g5: var(--fiesta-purple);
|
|
322
544
|
--icon-g6: var(--fiesta-purple);
|
|
323
|
-
/* Elevation - subtle card and modal shadows
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
--elevation-
|
|
545
|
+
/* Elevation - subtle card and modal shadows.
|
|
546
|
+
Warm-tinted rather than neutral black: a grey shadow under a warm card
|
|
547
|
+
on a warm page reads as a rendering artifact, not as depth. */
|
|
548
|
+
--elevation-card: 0 1px 2px 0 oklch(0.3 0.03 73 / 0.05), 0 2px 8px -2px oklch(0.3 0.03 73 / 0.07);
|
|
549
|
+
--elevation-card-hover: 0 2px 8px -2px oklch(0.3 0.03 73 / 0.09), 0 4px 16px -4px oklch(0.3 0.03 73 / 0.07);
|
|
550
|
+
--elevation-modal: 0 16px 40px -12px oklch(0.3 0.03 73 / 0.2), 0 4px 10px -4px oklch(0.3 0.03 73 / 0.1);
|
|
327
551
|
|
|
328
552
|
/* ============================================================
|
|
329
553
|
* Motion tokens (B4) — one duration scale + named easings.
|
|
@@ -379,15 +603,15 @@
|
|
|
379
603
|
* surface | --radius-surface | rounded-lg | 10px | modal surfaces (dialog, alert-dialog), alert, sidebar nav pills
|
|
380
604
|
* card | --radius-card | rounded-xl | 12px | card, page-header icon tile
|
|
381
605
|
* pill | --radius-pill | rounded-full | ∞ | badge, switch, slider, overlay close (dialog + sheet), scroll-area thumb, sidebar edge toggle, empty-state icon
|
|
382
|
-
* chrome | --radius-chrome | (css/inline) | 14px | desktop sidebar shell
|
|
383
|
-
* chrome-mobile | --radius-chrome-mobile | (css/inline) | 16px | mobile header/menu shell,
|
|
606
|
+
* chrome | --radius-chrome | (css/inline) | 14px | desktop sidebar shell
|
|
607
|
+
* chrome-mobile | --radius-chrome-mobile | (css/inline) | 16px | mobile header/menu shell, menu clip-path reveal
|
|
384
608
|
*
|
|
385
609
|
* Components KEEP their Tailwind rounded-* classes — those already
|
|
386
610
|
* resolve through the --radius calc chain, so each role token below
|
|
387
611
|
* is defined with the same math and stays in lockstep with them.
|
|
388
612
|
* New components pick a ROLE from this table instead of a raw size.
|
|
389
613
|
* The chrome radii are deliberately fixed px (not on the rem-based
|
|
390
|
-
* --radius chain)
|
|
614
|
+
* --radius chain).
|
|
391
615
|
*
|
|
392
616
|
* Deliberately off-scale (left as-is): pride-burst particle 2px,
|
|
393
617
|
* scroll-area viewport rounded-[inherit]. Both are arbitrary-value
|
|
@@ -411,7 +635,7 @@
|
|
|
411
635
|
* --z-sidebar 50 desktop sidebar rail
|
|
412
636
|
* --z-sidebar-toggle 51 edge collapse button — LOCAL value: the
|
|
413
637
|
* fixed z-50 aside is its own stacking
|
|
414
|
-
* context, so 51 only beats the
|
|
638
|
+
* context, so 51 only beats the rail's own
|
|
415
639
|
* canvas / z-[1] content column inside it
|
|
416
640
|
* --z-mobile-backdrop 90 dimming layer behind the mobile menu
|
|
417
641
|
* --z-mobile-menu 95 mobile slide-down menu panel
|
|
@@ -443,7 +667,7 @@
|
|
|
443
667
|
*
|
|
444
668
|
* Not on the scale (local stacking / one-offs): select scroll
|
|
445
669
|
* buttons z-10 (sticky inside the popup), sidebar/header content
|
|
446
|
-
* z-[1] (above the
|
|
670
|
+
* z-[1] (above the rail surface), board-switch view-transition
|
|
447
671
|
* groups 0/1 (::view-transition pseudo-tree only).
|
|
448
672
|
* ============================================================ */
|
|
449
673
|
--z-popover: 50;
|
|
@@ -462,12 +686,16 @@
|
|
|
462
686
|
|
|
463
687
|
.dark {
|
|
464
688
|
color-scheme: dark; /* see the note in :root (#160) */
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
--
|
|
470
|
-
--
|
|
689
|
+
/* Hue lock applies here too: same six hues, same hue angles as :root.
|
|
690
|
+
Only lightness moves. Surfaces carry the same warm cast at hue 73, and
|
|
691
|
+
the card lift went 0.030 -> 0.050 so a dark card reads as an object
|
|
692
|
+
rather than an outline drawn on a void. */
|
|
693
|
+
--background: oklch(0.145 0.004 73);
|
|
694
|
+
--foreground: oklch(0.965 0.003 73);
|
|
695
|
+
--card: oklch(0.195 0.004 73);
|
|
696
|
+
--card-foreground: oklch(0.965 0.003 73);
|
|
697
|
+
--popover: oklch(0.235 0.004 73);
|
|
698
|
+
--popover-foreground: oklch(0.965 0.003 73);
|
|
471
699
|
/* Brand-bearing primary (#156). This used to be oklch(0.922 0 0) — stock
|
|
472
700
|
shadcn's achromatic dark primary, never re-designed when the FiestaBoard
|
|
473
701
|
palette landed. Dark is the DEFAULT theme, so every default Button,
|
|
@@ -476,67 +704,94 @@
|
|
|
476
704
|
surface (light 0.52 -> dark 0.72) and held on the same hue family.
|
|
477
705
|
Upholds the "one token means 'this control is on'" invariant declared in
|
|
478
706
|
:root: --ring and --sidebar-ring below repeat this exact value. */
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
--primary-foreground: oklch(0.
|
|
483
|
-
--
|
|
484
|
-
--
|
|
485
|
-
--
|
|
486
|
-
--
|
|
487
|
-
--
|
|
488
|
-
--
|
|
489
|
-
--
|
|
490
|
-
--
|
|
707
|
+
/* Theme-invariant: a physical tile does not have a theme. Identical to
|
|
708
|
+
:root — see the note there. 9.77:1 on --background here. */
|
|
709
|
+
--primary: #f5a623;
|
|
710
|
+
--primary-foreground: oklch(0.205 0.008 73);
|
|
711
|
+
--primary-hover: oklch(0.84 0.128 73);
|
|
712
|
+
--primary-active: oklch(0.72 0.153 73);
|
|
713
|
+
--ring-edge: oklch(0.205 0.008 73);
|
|
714
|
+
--secondary: oklch(0.255 0.004 73);
|
|
715
|
+
--secondary-foreground: oklch(0.965 0.003 73);
|
|
716
|
+
--muted: oklch(0.235 0.004 73);
|
|
717
|
+
/* 8.10:1 on --background, 7.51:1 on --card, 6.86:1 on --muted. */
|
|
718
|
+
--muted-foreground: oklch(0.725 0.004 73);
|
|
719
|
+
--accent: oklch(0.225 0.004 73);
|
|
720
|
+
--accent-foreground: oklch(0.965 0.003 73);
|
|
721
|
+
/* Board red. 6.88:1 on --background; board-ink label 6.23:1. */
|
|
722
|
+
--destructive: oklch(0.7 0.17 29);
|
|
723
|
+
--destructive-foreground: oklch(0.205 0.008 73);
|
|
491
724
|
/* --border stays decorative-subtle; --input is the 3:1 control boundary.
|
|
492
725
|
Full rationale in the :root block — do not merge the two. */
|
|
493
|
-
--border:
|
|
494
|
-
--input: color-mix(in oklch, var(--foreground)
|
|
495
|
-
--ring:
|
|
496
|
-
--
|
|
497
|
-
|
|
498
|
-
--sidebar
|
|
499
|
-
--sidebar-
|
|
500
|
-
--sidebar-
|
|
501
|
-
--sidebar-
|
|
502
|
-
--
|
|
503
|
-
--
|
|
504
|
-
|
|
505
|
-
--
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
light
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
--
|
|
518
|
-
--
|
|
519
|
-
--tag-
|
|
520
|
-
--tag-
|
|
521
|
-
--tag-
|
|
522
|
-
--
|
|
523
|
-
--
|
|
524
|
-
--
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
--category-
|
|
528
|
-
--category-entertainment: oklch(0.
|
|
529
|
-
--category-
|
|
530
|
-
--category-
|
|
531
|
-
--category-art
|
|
532
|
-
--category-
|
|
533
|
-
--category-
|
|
534
|
-
--category-
|
|
535
|
-
--category-
|
|
536
|
-
--
|
|
537
|
-
--
|
|
538
|
-
--
|
|
539
|
-
--
|
|
726
|
+
--border: color-mix(in oklch, var(--foreground) 12%, transparent);
|
|
727
|
+
--input: color-mix(in oklch, var(--foreground) 40%, transparent);
|
|
728
|
+
--ring: var(--primary); /* = --primary, see the invariant in :root */
|
|
729
|
+
/* Warm graphite, one step up from --card so the rail reads as housing
|
|
730
|
+
rather than as more page. See the note in :root. */
|
|
731
|
+
--sidebar: oklch(0.24 0.005 73);
|
|
732
|
+
--sidebar-foreground: oklch(0.985 0.002 73);
|
|
733
|
+
--sidebar-accent: oklch(1 0 0 / 14%);
|
|
734
|
+
--sidebar-accent-foreground: oklch(0.985 0.002 73);
|
|
735
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
736
|
+
--sidebar-ring: var(--primary); /* = --primary, see the invariant in :root */
|
|
737
|
+
/* Board blue. 8.57:1 on --background; board-ink label 7.76:1. */
|
|
738
|
+
--info: oklch(0.74 0.139 251);
|
|
739
|
+
--info-foreground: oklch(0.205 0.008 73);
|
|
740
|
+
/* Board green. 10.40:1 on --background; board-ink label 9.42:1. */
|
|
741
|
+
--success: oklch(0.78 0.17 133);
|
|
742
|
+
--success-foreground: oklch(0.205 0.008 73);
|
|
743
|
+
/* Board yellow, the SAME hue as the light --warning (104). The old pair
|
|
744
|
+
drifted 25deg between themes — the only hue-unstable token in the file
|
|
745
|
+
— because light had to separate warning from brand on lightness while
|
|
746
|
+
dark separated them on hue. Under the hue lock the separation is
|
|
747
|
+
structural in both themes: warning is yellow, brand is orange, 31deg
|
|
748
|
+
apart, and neither has to move to stay legible.
|
|
749
|
+
13.05:1 on --background; board-ink label 11.81:1. */
|
|
750
|
+
--warning: oklch(0.86 0.17 104);
|
|
751
|
+
--warning-foreground: oklch(0.205 0.008 73);
|
|
752
|
+
--tag-variable: oklch(0.78 0.164 315);
|
|
753
|
+
--tag-variable-foreground: oklch(0.86 0.1 315);
|
|
754
|
+
--tag-success: oklch(0.78 0.17 133);
|
|
755
|
+
--tag-success-foreground: oklch(0.86 0.1 133);
|
|
756
|
+
--tag-formula: oklch(0.78 0.116 251);
|
|
757
|
+
--tag-formula-foreground: oklch(0.86 0.09 251);
|
|
758
|
+
/* Categories are literal tiles and therefore theme-invariant — these are
|
|
759
|
+
byte-identical to :root on purpose. See the note there. */
|
|
760
|
+
--category-entertainment: #eb4034; /* board red */
|
|
761
|
+
--category-entertainment-foreground: oklch(0.78 0.129 29);
|
|
762
|
+
--category-home: #f5a623; /* board orange */
|
|
763
|
+
--category-home-foreground: oklch(0.78 0.166 73);
|
|
764
|
+
--category-art: #f8e71c; /* board yellow */
|
|
765
|
+
--category-art-foreground: oklch(0.78 0.164 104);
|
|
766
|
+
--category-transit: #7ed321; /* board green */
|
|
767
|
+
--category-transit-foreground: oklch(0.78 0.17 133);
|
|
768
|
+
--category-weather: #4a90d9; /* board blue */
|
|
769
|
+
--category-weather-foreground: oklch(0.78 0.116 251);
|
|
770
|
+
--category-data: #9b59b6; /* board violet */
|
|
771
|
+
--category-data-foreground: oklch(0.78 0.164 315);
|
|
772
|
+
--category-utility: #ffffff; /* board white */
|
|
773
|
+
--category-utility-foreground: oklch(0.725 0.012 73);
|
|
774
|
+
/* Same six hues, lifted to the dark ink plateau. See :root. */
|
|
775
|
+
--hue-red: oklch(0.78 0.129 29);
|
|
776
|
+
--hue-orange: oklch(0.78 0.166 73);
|
|
777
|
+
--hue-yellow: oklch(0.78 0.164 104);
|
|
778
|
+
--hue-green: oklch(0.78 0.17 133);
|
|
779
|
+
--hue-blue: oklch(0.78 0.116 251);
|
|
780
|
+
--hue-violet: oklch(0.78 0.164 315);
|
|
781
|
+
/* See the :root note: the dark figure here is not a contrast ratio worth
|
|
782
|
+
quoting. Modality in dark is carried by --elevation-modal. */
|
|
783
|
+
--overlay: oklch(0.08 0.006 73 / 72%);
|
|
784
|
+
--brand: var(--hue-orange); /* text step — see :root */
|
|
785
|
+
--brand-foreground: oklch(0.205 0.008 73);
|
|
786
|
+
--brand-tile: var(--primary); /* DEPRECATED alias — same pigment now */
|
|
787
|
+
/* On a dark rail the wordmark can be the literal tile — the exact pigment
|
|
788
|
+
the hardware flips, and the one the icon beside it already uses. 8.12:1. */
|
|
789
|
+
--brand-wordmark: var(--primary);
|
|
790
|
+
/* Dark tiles on a dark page need far less to read. See :root. */
|
|
791
|
+
--board-backdrop-opacity: 0.28;
|
|
792
|
+
--brand-tile-foreground: oklch(0.205 0.008 73);
|
|
793
|
+
--brand-hover: color-mix(in oklch, var(--brand) 82%, var(--foreground));
|
|
794
|
+
--brand-emphasis: var(--brand); /* DEPRECATED — see :root */
|
|
540
795
|
/* Navigation active state — indigo palette.
|
|
541
796
|
NOT "lighter for dark mode" (#162): --nav-active is the solid FALLBACK
|
|
542
797
|
fill for the active nav pill, used whenever the frosted-glass treatment
|
|
@@ -547,17 +802,29 @@
|
|
|
547
802
|
one. Darkened to clear AAA (7.47:1) instead. Kept as light as AAA allows
|
|
548
803
|
rather than the 0.42 the audit suggested (8.91:1), because darkening the
|
|
549
804
|
pill trades text contrast against the pill's own separation from the
|
|
550
|
-
surface behind it — see the fallback rules near .nav-active.
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
--
|
|
557
|
-
--
|
|
558
|
-
--
|
|
559
|
-
|
|
560
|
-
--
|
|
805
|
+
surface behind it — see the fallback rules near .nav-active.
|
|
806
|
+
|
|
807
|
+
Now board violet and theme-invariant — identical to :root. Because the
|
|
808
|
+
rail is a flat, known surface, the pill no
|
|
809
|
+
longer needs a per-theme value, and its contrast no longer depends on
|
|
810
|
+
where a 117-second animation happens to be. */
|
|
811
|
+
--nav-active: var(--primary); /* see :root */
|
|
812
|
+
--nav-active-foreground: var(--primary-foreground);
|
|
813
|
+
--nav-active-hover: oklch(1 0 0 / 14%);
|
|
814
|
+
/* Identical to :root. See the note there. */
|
|
815
|
+
--fiesta-red: oklch(0.46 0.15 29);
|
|
816
|
+
--fiesta-orange: oklch(0.46 0.098 73);
|
|
817
|
+
--fiesta-purple: oklch(0.46 0.15 315);
|
|
818
|
+
/* A dark card is an object, not an outline. This is the same lit-top /
|
|
819
|
+
shadowed-base recipe that makes a character tile read as a raised flap
|
|
820
|
+
(see components/board/board-surfaces.ts charLeafBoxShadow.black), so
|
|
821
|
+
depth in the chrome and depth on the board are one idea. */
|
|
822
|
+
--elevation-card:
|
|
823
|
+
inset 0 1px 0 rgb(255 255 255 / 0.1), inset 0 -1px 0 rgb(0 0 0 / 0.55), 0 2px 8px -3px rgb(0 0 0 / 0.55);
|
|
824
|
+
--elevation-card-hover:
|
|
825
|
+
inset 0 1px 0 rgb(255 255 255 / 0.14), inset 0 -1px 0 rgb(0 0 0 / 0.55), 0 4px 14px -4px rgb(0 0 0 / 0.6);
|
|
826
|
+
/* Carries modality in dark, where a surface contrast ratio cannot. */
|
|
827
|
+
--elevation-modal: 0 24px 48px -12px rgb(0 0 0 / 0.75), inset 0 0 0 1px oklch(1 0 0 / 0.14);
|
|
561
828
|
}
|
|
562
829
|
|
|
563
830
|
@layer base {
|
|
@@ -668,54 +935,6 @@
|
|
|
668
935
|
* Consumed by the chrome components (Sidebar, PageHeader, …).
|
|
669
936
|
* ============================================================ */
|
|
670
937
|
|
|
671
|
-
/* Pride styling lives in seasons/pride.css alongside the draft seasons. */
|
|
672
|
-
|
|
673
|
-
/* Season easter egg — confetti burst particles spawned by JS on logo click
|
|
674
|
-
(class name kept as pride-burst-* for app compatibility; every season's
|
|
675
|
-
fireSeasonBurst() uses it) */
|
|
676
|
-
@keyframes pride-burst {
|
|
677
|
-
0% {
|
|
678
|
-
transform: translate(0, 0) rotate(0deg) scale(1);
|
|
679
|
-
opacity: 1;
|
|
680
|
-
}
|
|
681
|
-
100% {
|
|
682
|
-
transform: translate(var(--tx), var(--ty)) rotate(var(--rot)) scale(0.2);
|
|
683
|
-
opacity: 0;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
/* Wrapper so the 48 particles append to the live tree in a single mutation
|
|
687
|
-
and share one animationend listener. Fixed (out of flow) so the empty
|
|
688
|
-
container never affects body layout; no transform/filter/contain here, so
|
|
689
|
-
the fixed particles still resolve against the viewport. */
|
|
690
|
-
.pride-burst {
|
|
691
|
-
position: fixed;
|
|
692
|
-
top: 0;
|
|
693
|
-
left: 0;
|
|
694
|
-
/* The fixed container forms a stacking context, trapping the particles'
|
|
695
|
-
z-index inside it — so the container itself must sit at confetti level
|
|
696
|
-
or the burst paints under sheets/modals/tooltips. */
|
|
697
|
-
z-index: var(--z-confetti);
|
|
698
|
-
pointer-events: none;
|
|
699
|
-
}
|
|
700
|
-
.pride-burst-particle {
|
|
701
|
-
position: fixed;
|
|
702
|
-
width: 7px;
|
|
703
|
-
height: 7px;
|
|
704
|
-
border-radius: 2px;
|
|
705
|
-
pointer-events: none;
|
|
706
|
-
z-index: var(--z-confetti);
|
|
707
|
-
/* 0.8s default is a one-off (JS overrides via --dur), so it stays inline */
|
|
708
|
-
animation: pride-burst var(--dur, 0.8s) var(--motion-ease-out) forwards;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
/* fireSeasonBurst() already skips the burst under reduced motion, but mirror
|
|
712
|
-
every other motion surface in this file with a CSS gate as a backstop. */
|
|
713
|
-
@media (prefers-reduced-motion: reduce) {
|
|
714
|
-
.pride-burst-particle {
|
|
715
|
-
animation: none !important;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
938
|
/* Board switch transition — the app-wide board selector wraps its state
|
|
720
939
|
update in document.startViewTransition() (see current-board-context.tsx),
|
|
721
940
|
stamping html[data-board-switch="forward"|"backward"]. Only the page
|
|
@@ -880,118 +1099,84 @@ html[data-board-switch="backward"]::view-transition-new(board-page) {
|
|
|
880
1099
|
/* Sidebar & mobile header: Fiesta-brand gradient (red, orange, purple).
|
|
881
1100
|
Repeating gradient for seamless loop (no flash).
|
|
882
1101
|
Static when prefers-reduced-motion. */
|
|
883
|
-
@keyframes sidebar-gradient-flow {
|
|
884
|
-
/* Runs on .sidebar-gradient-layer::before (see below): translate exactly one
|
|
885
|
-
repeat period (300% of the container = 75% of the 400% layer) for a
|
|
886
|
-
seamless loop — no flash at end. transform-only so the loop stays on the
|
|
887
|
-
compositor with zero per-frame repaint (issue #57). */
|
|
888
|
-
0% {
|
|
889
|
-
transform: translateY(0);
|
|
890
|
-
}
|
|
891
|
-
100% {
|
|
892
|
-
transform: translateY(-75%);
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
1102
|
|
|
896
|
-
/*
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1103
|
+
/* One logo treatment, both themes. These used to fork — near-black "Board" on
|
|
1104
|
+
a light coral rail in light mode, white on a dark rail in dark mode. The
|
|
1105
|
+
rail is now a fixture that does not change with the theme, so the fork is
|
|
1106
|
+
not just unnecessary, it was actively wrong: it painted black text onto a
|
|
1107
|
+
dark rail the moment the light stops were darkened. */
|
|
1108
|
+
/* The `.logo-on-gradient` override is gone: FiestaLogo now reads
|
|
1109
|
+
--brand-wordmark and --foreground directly, so it is correct on the rail, on
|
|
1110
|
+
a wizard card and anywhere else without a scoped rule pinning it. That rule
|
|
1111
|
+
had already caused two bugs by pinning what should have been a token — white
|
|
1112
|
+
"Board" that vanished on a light rail, then a tile-orange "Fiesta" that would
|
|
1113
|
+
have. */
|
|
902
1114
|
|
|
903
|
-
/*
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1115
|
+
/* ============================================================
|
|
1116
|
+
* THE RAIL IS QUIET; THE LOCKUP CARRIES THE BRAND (#231).
|
|
1117
|
+
*
|
|
1118
|
+
* This used to be a full-height animated gradient in two hand-forked
|
|
1119
|
+
* palettes — a light coral/peach ramp in light mode, deep oxblood-to-olive
|
|
1120
|
+
* in dark. Three things were wrong with it, and they compounded:
|
|
1121
|
+
*
|
|
1122
|
+
* 1. On an empty install the rail was the brightest, most saturated
|
|
1123
|
+
* object on the screen. The chrome out-shouted the product: the board
|
|
1124
|
+
* is the thing to look at, and it was losing to the navigation.
|
|
1125
|
+
* 2. A red → orange → violet ramp spends its middle third in the browns
|
|
1126
|
+
* and olives between those hues. Over 800px of rail that reads as mud,
|
|
1127
|
+
* and no amount of retuning the stops fixes an interpolation problem.
|
|
1128
|
+
* 3. Nothing sitting on it could be designed, because its background was
|
|
1129
|
+
* a function of wall-clock time.
|
|
1130
|
+
*
|
|
1131
|
+
* The gradient is now confined to the LOCKUP BAND at the top — behind the
|
|
1132
|
+
* logo and the board switcher, where identity belongs and where 88px of
|
|
1133
|
+
* colour reads as a brand mark rather than as a wall. Below that it fades
|
|
1134
|
+
* into the flat rail. The nav body sits on one predictable surface, which
|
|
1135
|
+
* is what lets the active pill and the labels be tuned once instead of
|
|
1136
|
+
* sampled per frame.
|
|
1137
|
+
*
|
|
1138
|
+
* The rail is the same warm graphite in both themes (see --sidebar), so the
|
|
1139
|
+
* whole light/dark fork below it is gone — along with the class of bug where
|
|
1140
|
+
* darkening the light stops left black labels on a dark rail.
|
|
1141
|
+
* ============================================================ */
|
|
1142
|
+
/* Every local override here is DERIVED from --sidebar-foreground rather than
|
|
1143
|
+
hardcoded to white. That is the whole reason the rail can follow the theme
|
|
1144
|
+
again: the previous set assumed a dark rail, so the moment the rail went
|
|
1145
|
+
light it painted white labels on a light surface. Deriving means the rail
|
|
1146
|
+
has exactly one value to fork (--sidebar and its foreground) and everything
|
|
1147
|
+
sitting on it follows for free. */
|
|
913
1148
|
.sidebar-gradient {
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
--sidebar-fiesta-purple: var(--fiesta-purple);
|
|
917
|
-
background: repeating-linear-gradient(
|
|
918
|
-
170deg,
|
|
919
|
-
var(--sidebar-fiesta-red) 0%,
|
|
920
|
-
var(--sidebar-fiesta-orange) 33.33%,
|
|
921
|
-
var(--sidebar-fiesta-purple) 66.66%,
|
|
922
|
-
var(--sidebar-fiesta-red) 100%
|
|
923
|
-
);
|
|
924
|
-
background-size: 100% 300%;
|
|
925
|
-
background-position: 50% 0%;
|
|
926
|
-
background-repeat: repeat-y;
|
|
927
|
-
color: oklch(0 0 0);
|
|
1149
|
+
background: var(--sidebar);
|
|
1150
|
+
color: var(--sidebar-foreground);
|
|
928
1151
|
border-radius: var(--radius-chrome);
|
|
929
|
-
--sidebar-
|
|
930
|
-
--sidebar-accent:
|
|
931
|
-
--sidebar-
|
|
932
|
-
--
|
|
933
|
-
--foreground: oklch(
|
|
934
|
-
--
|
|
935
|
-
--accent:
|
|
936
|
-
--
|
|
937
|
-
--border: oklch(0 0 0 / 12%);
|
|
938
|
-
}
|
|
939
|
-
/* Dark mode: rich Fiesta gradient — deep-toned stops defined ONCE for both
|
|
940
|
-
the vertical sidebar and the mobile horizontal header. */
|
|
941
|
-
.dark .sidebar-gradient,
|
|
942
|
-
.dark .sidebar-gradient-horizontal {
|
|
943
|
-
--sidebar-fiesta-red: #7a2a24;
|
|
944
|
-
--sidebar-fiesta-orange: #6d5014;
|
|
945
|
-
--sidebar-fiesta-purple: #4e3070; /* board violet, darker for dark theme */
|
|
946
|
-
}
|
|
947
|
-
.dark .sidebar-gradient {
|
|
948
|
-
color: oklch(1 0 0);
|
|
949
|
-
--sidebar-foreground: oklch(1 0 0);
|
|
950
|
-
--sidebar-accent: oklch(1 0 0 / 24%);
|
|
951
|
-
--sidebar-accent-foreground: oklch(1 0 0);
|
|
952
|
-
--sidebar-border: oklch(1 0 0 / 18%);
|
|
953
|
-
--foreground: oklch(1 0 0);
|
|
954
|
-
--muted-foreground: oklch(1 0 0 / 85%);
|
|
955
|
-
--accent: oklch(1 0 0 / 20%);
|
|
956
|
-
--accent-foreground: oklch(1 0 0);
|
|
957
|
-
--border: oklch(1 0 0 / 18%);
|
|
1152
|
+
--sidebar-accent: color-mix(in oklch, var(--sidebar-foreground) 10%, transparent);
|
|
1153
|
+
--sidebar-accent-foreground: var(--sidebar-foreground);
|
|
1154
|
+
--sidebar-border: color-mix(in oklch, var(--sidebar-foreground) 14%, transparent);
|
|
1155
|
+
--foreground: var(--sidebar-foreground);
|
|
1156
|
+
--muted-foreground: color-mix(in oklch, var(--sidebar-foreground) 72%, transparent);
|
|
1157
|
+
--accent: color-mix(in oklch, var(--sidebar-foreground) 10%, transparent);
|
|
1158
|
+
--accent-foreground: var(--sidebar-foreground);
|
|
1159
|
+
--border: color-mix(in oklch, var(--sidebar-foreground) 14%, transparent);
|
|
958
1160
|
}
|
|
959
1161
|
|
|
1162
|
+
/* No CSS gradient paints here any more (#231). The rail is a flat surface;
|
|
1163
|
+
colour arrives through the wordmark, the active pill, and the per-page hue
|
|
1164
|
+
on the page icon. */
|
|
1165
|
+
|
|
960
1166
|
.sidebar-gradient-horizontal {
|
|
961
|
-
/*
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
background: repeating-linear-gradient(
|
|
967
|
-
135deg,
|
|
968
|
-
var(--sidebar-fiesta-red) 0,
|
|
969
|
-
var(--sidebar-fiesta-orange) 33.33%,
|
|
970
|
-
var(--sidebar-fiesta-purple) 66.66%,
|
|
971
|
-
var(--sidebar-fiesta-red) 100%
|
|
972
|
-
);
|
|
973
|
-
background-size: 400% 100%;
|
|
974
|
-
background-position: 0% 50%;
|
|
975
|
-
color: oklch(0 0 0);
|
|
1167
|
+
/* Flat, like the desktop rail (#231). The mobile bar and the rail are the
|
|
1168
|
+
same object at two widths, so they get the same treatment — a gradient on
|
|
1169
|
+
one and not the other is how two chromes start disagreeing. */
|
|
1170
|
+
background: var(--sidebar);
|
|
1171
|
+
color: var(--sidebar-foreground);
|
|
976
1172
|
border-radius: var(--radius-chrome-mobile);
|
|
977
|
-
--foreground:
|
|
978
|
-
--sidebar-accent:
|
|
979
|
-
--sidebar-accent-foreground:
|
|
980
|
-
--muted-foreground: oklch(
|
|
981
|
-
--accent:
|
|
982
|
-
--accent-foreground:
|
|
983
|
-
--border:
|
|
984
|
-
}
|
|
985
|
-
@keyframes sidebar-gradient-flow-h {
|
|
986
|
-
/* Runs on .sidebar-gradient-layer::before: one period (33.33% of the 400%
|
|
987
|
-
background box = 100% of the container = 50% of the 200% layer) per loop —
|
|
988
|
-
same wrap as the old background-position scroll, compositor-only. */
|
|
989
|
-
0% {
|
|
990
|
-
transform: translateX(0);
|
|
991
|
-
}
|
|
992
|
-
100% {
|
|
993
|
-
transform: translateX(-50%);
|
|
994
|
-
}
|
|
1173
|
+
--foreground: var(--sidebar-foreground);
|
|
1174
|
+
--sidebar-accent: color-mix(in oklch, var(--sidebar-foreground) 12%, transparent);
|
|
1175
|
+
--sidebar-accent-foreground: var(--sidebar-foreground);
|
|
1176
|
+
--muted-foreground: color-mix(in oklch, var(--sidebar-foreground) 78%, transparent);
|
|
1177
|
+
--accent: color-mix(in oklch, var(--sidebar-foreground) 12%, transparent);
|
|
1178
|
+
--accent-foreground: var(--sidebar-foreground);
|
|
1179
|
+
--border: color-mix(in oklch, var(--sidebar-foreground) 16%, transparent);
|
|
995
1180
|
}
|
|
996
1181
|
.dark .sidebar-gradient-horizontal {
|
|
997
1182
|
color: oklch(1 0 0);
|
|
@@ -1004,67 +1189,12 @@ html[data-board-switch="backward"]::view-transition-new(board-page) {
|
|
|
1004
1189
|
--border: oklch(1 0 0 / 18%);
|
|
1005
1190
|
}
|
|
1006
1191
|
|
|
1007
|
-
/*
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
.sidebar-gradient-layer::before: it re-inherits that same background
|
|
1014
|
-
(container → layer → ::before), overscans it by exactly one repeat
|
|
1015
|
-
period, and translates with a transform loop — so the flow runs on the
|
|
1016
|
-
compositor with no per-frame rasterization, and the .nav-active
|
|
1017
|
-
backdrop-filter pills get a cacheable backdrop (#93). Dark seasons stay
|
|
1018
|
-
correct for free: the inherited background is their flat hex, so the
|
|
1019
|
-
layer paints the same flat color. The layer clips itself to the rounded
|
|
1020
|
-
corners because the desktop <aside> cannot take overflow:hidden (the
|
|
1021
|
-
edge toggle button overhangs it). */
|
|
1022
|
-
.sidebar-gradient,
|
|
1023
|
-
.sidebar-gradient-horizontal {
|
|
1024
|
-
/* Keep the z-index:-1 layer above this element's own background even when
|
|
1025
|
-
the container isn't otherwise a stacking context (e.g. story wrappers). */
|
|
1026
|
-
isolation: isolate;
|
|
1027
|
-
}
|
|
1028
|
-
.sidebar-gradient-layer {
|
|
1029
|
-
position: absolute;
|
|
1030
|
-
inset: 0;
|
|
1031
|
-
z-index: -1; /* under the container's content, above its background */
|
|
1032
|
-
overflow: hidden;
|
|
1033
|
-
border-radius: inherit;
|
|
1034
|
-
background: inherit; /* inheritance link: ::before re-inherits from here */
|
|
1035
|
-
pointer-events: none;
|
|
1036
|
-
}
|
|
1037
|
-
.sidebar-gradient-layer::before {
|
|
1038
|
-
content: "";
|
|
1039
|
-
position: absolute;
|
|
1040
|
-
top: 0;
|
|
1041
|
-
left: 0;
|
|
1042
|
-
background: inherit;
|
|
1043
|
-
will-change: transform;
|
|
1044
|
-
}
|
|
1045
|
-
/* Vertical: the container's gradient box is 100% × 300%. The ::before
|
|
1046
|
-
overscans to 400% height (100% visible + one 300% travel period), so
|
|
1047
|
-
translateY(-75%) [= -300% of the container] wraps onto the repeat. */
|
|
1048
|
-
.sidebar-gradient .sidebar-gradient-layer::before {
|
|
1049
|
-
width: 100%;
|
|
1050
|
-
height: 400%;
|
|
1051
|
-
background-size: 100% 75%; /* same box as the container's 100% 300% */
|
|
1052
|
-
background-position: 50% 0%; /* pinned to the container's t=0 anchor */
|
|
1053
|
-
background-repeat: repeat-y;
|
|
1054
|
-
animation: sidebar-gradient-flow 117s linear infinite;
|
|
1055
|
-
}
|
|
1056
|
-
/* Horizontal: the container's gradient box is 400% × 100%; one loop travels
|
|
1057
|
-
33.33% background-position = 100% of the container, so the ::before
|
|
1058
|
-
overscans to 200% width and translateX(-50%) reproduces one period. */
|
|
1059
|
-
.sidebar-gradient-horizontal .sidebar-gradient-layer::before {
|
|
1060
|
-
width: 200%;
|
|
1061
|
-
height: 100%;
|
|
1062
|
-
background-size: 200% 100%; /* same box as the container's 400% 100% */
|
|
1063
|
-
background-position: 0% 50%;
|
|
1064
|
-
background-repeat: repeat;
|
|
1065
|
-
animation: sidebar-gradient-flow-h 117s linear 0.35s infinite;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1192
|
+
/* .sidebar-gradient-layer is gone (#57 is closed). It existed to host a
|
|
1193
|
+
compositor-only copy of the scrolling gradient, and later to give the
|
|
1194
|
+
seasonal WebGL aurora a stacking context under the rail's content. Both of
|
|
1195
|
+
those are gone now, and a flat background clips to its own
|
|
1196
|
+
border-radius, so the element, the `isolation: isolate` it required, and the
|
|
1197
|
+
will-change hint it carried all had nothing left to do. */
|
|
1068
1198
|
/* Navigation entrance animations */
|
|
1069
1199
|
@keyframes nav-slide-in-left {
|
|
1070
1200
|
from {
|
|
@@ -1086,17 +1216,10 @@ html[data-board-switch="backward"]::view-transition-new(board-page) {
|
|
|
1086
1216
|
transform: translateY(0);
|
|
1087
1217
|
}
|
|
1088
1218
|
}
|
|
1089
|
-
/* Desktop: entrance on the container; the gradient flow runs on
|
|
1090
|
-
.sidebar-gradient-layer::before and starts after 400ms. */
|
|
1091
|
-
/* 117s gradient loop is an ambient one-off; its 0.4s/0.35s start delays
|
|
1092
|
-
mirror the entrance durations (slowest/slower) and stay literal with them */
|
|
1093
1219
|
@media (min-width: 1024px) {
|
|
1094
1220
|
.sidebar-gradient {
|
|
1095
1221
|
animation: nav-slide-in-left var(--motion-duration-slowest) var(--motion-ease-out) both;
|
|
1096
1222
|
}
|
|
1097
|
-
.sidebar-gradient .sidebar-gradient-layer::before {
|
|
1098
|
-
animation-delay: 0.4s;
|
|
1099
|
-
}
|
|
1100
1223
|
}
|
|
1101
1224
|
.sidebar-gradient-horizontal {
|
|
1102
1225
|
animation: nav-slide-in-top var(--motion-duration-slower) var(--motion-ease-out) both;
|
|
@@ -1132,157 +1255,149 @@ html[data-board-switch="backward"]::view-transition-new(board-page) {
|
|
|
1132
1255
|
.content-shift {
|
|
1133
1256
|
transition: none;
|
|
1134
1257
|
}
|
|
1135
|
-
.sidebar-gradient
|
|
1136
|
-
animation: none;
|
|
1137
|
-
background-position: 50% 25%;
|
|
1138
|
-
}
|
|
1258
|
+
.sidebar-gradient,
|
|
1139
1259
|
.sidebar-gradient-horizontal {
|
|
1140
1260
|
animation: none;
|
|
1141
|
-
background-position: 25% 50%;
|
|
1142
|
-
}
|
|
1143
|
-
/* Freeze the compositor layer at the same intended mid-gradient pose
|
|
1144
|
-
(equivalent to background-position 50% 25% / 25% 50% on the container). */
|
|
1145
|
-
.sidebar-gradient .sidebar-gradient-layer::before {
|
|
1146
|
-
animation: none;
|
|
1147
|
-
transform: translateY(-12.5%); /* -50% of the container = bg-position 50% 25% */
|
|
1148
|
-
}
|
|
1149
|
-
.sidebar-gradient-horizontal .sidebar-gradient-layer::before {
|
|
1150
|
-
animation: none;
|
|
1151
|
-
transform: translateX(-37.5%); /* -75% of the container = bg-position 25% 50% */
|
|
1152
1261
|
}
|
|
1153
1262
|
}
|
|
1154
1263
|
.reduce-motion .sidebar-transition,
|
|
1155
1264
|
.reduce-motion .content-shift {
|
|
1156
1265
|
transition: none;
|
|
1157
1266
|
}
|
|
1158
|
-
.reduce-motion .sidebar-gradient
|
|
1159
|
-
animation: none;
|
|
1160
|
-
background-position: 50% 25%;
|
|
1161
|
-
}
|
|
1267
|
+
.reduce-motion .sidebar-gradient,
|
|
1162
1268
|
.reduce-motion .sidebar-gradient-horizontal {
|
|
1163
1269
|
animation: none;
|
|
1164
|
-
background-position: 25% 50%;
|
|
1165
|
-
}
|
|
1166
|
-
/* Same freeze pose as the prefers-reduced-motion block above. */
|
|
1167
|
-
.reduce-motion .sidebar-gradient .sidebar-gradient-layer::before {
|
|
1168
|
-
animation: none;
|
|
1169
|
-
transform: translateY(-12.5%);
|
|
1170
|
-
}
|
|
1171
|
-
.reduce-motion .sidebar-gradient-horizontal .sidebar-gradient-layer::before {
|
|
1172
|
-
animation: none;
|
|
1173
|
-
transform: translateX(-37.5%);
|
|
1174
1270
|
}
|
|
1175
1271
|
|
|
1176
|
-
/*
|
|
1272
|
+
/* ============================================================
|
|
1273
|
+
* Navigation active state — ONE treatment, every surface (#231).
|
|
1274
|
+
*
|
|
1275
|
+
* This used to be five: a frosted backdrop-filter pill over the gradient,
|
|
1276
|
+
* a darker frosted variant for dark mode, and three fallback tiers
|
|
1277
|
+
* (@supports, prefers-reduced-transparency, prefers-contrast) that each
|
|
1278
|
+
* re-declared the solid fill. All five existed because the pill sat on an
|
|
1279
|
+
* animated gradient whose luminance changed over a 117-second loop, so its
|
|
1280
|
+
* contrast could not be computed — only sampled at runtime.
|
|
1281
|
+
*
|
|
1282
|
+
* The rail is now one flat lightness, so the surface
|
|
1283
|
+
* behind the pill is a known quantity and the pill can simply be opaque.
|
|
1284
|
+
* A solid board-violet fill carries a white label at 9.11:1 in both themes.
|
|
1285
|
+
*
|
|
1286
|
+
* NO MARKER. There was a 3px gold bar on the leading edge, and it existed for
|
|
1287
|
+
* one reason: the pill was dark violet with a white label, which put it at
|
|
1288
|
+
* 1.81:1 against the rail, under SC 1.4.11's 3:1 for a state. The bar was a
|
|
1289
|
+
* patch for a number, and a left accent rail is a stock device besides.
|
|
1290
|
+
*
|
|
1291
|
+
* Flipping the label to board ink lets the fill go bright — 5.66:1 against the
|
|
1292
|
+
* rail — so the state carries itself. 1.4.11 asks for 3:1; it does not ask for
|
|
1293
|
+
* a marker, and it does not care which pixels supply it. `font-semibold` on
|
|
1294
|
+
* the label remains as the non-colour second signal.
|
|
1295
|
+
* ============================================================ */
|
|
1177
1296
|
.nav-active {
|
|
1178
1297
|
background: var(--nav-active);
|
|
1179
1298
|
color: var(--nav-active-foreground);
|
|
1180
1299
|
}
|
|
1181
|
-
|
|
1182
|
-
(#162), which necessarily brings it closer in luminance to the near-black
|
|
1183
|
-
surfaces it sits on. A 1px light inset keeps the pill's OWN boundary
|
|
1184
|
-
identifiable (SC 1.4.11 — "state" of a component) without reintroducing the
|
|
1185
|
-
transparency these fallbacks exist to avoid. Light mode needs no help: its
|
|
1186
|
-
pill already sits 7.6:1 off --sidebar. */
|
|
1187
|
-
.dark .nav-active {
|
|
1188
|
-
box-shadow: inset 0 0 0 1px oklch(1 0 0 / 0.22);
|
|
1189
|
-
}
|
|
1300
|
+
|
|
1190
1301
|
.nav-active-hover:hover {
|
|
1191
1302
|
background: var(--nav-active-hover);
|
|
1192
1303
|
}
|
|
1193
1304
|
|
|
1194
|
-
/*
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
*/
|
|
1199
|
-
|
|
1200
|
-
.
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
box-shadow: inset 0 0 0 1px oklch(1 0 0 / 0.18);
|
|
1204
|
-
backdrop-filter: blur(14px) saturate(1.35);
|
|
1205
|
-
-webkit-backdrop-filter: blur(14px) saturate(1.35);
|
|
1305
|
+
/* The hover tint is the only chrome token left carrying alpha, so this is
|
|
1306
|
+
the one transparency branch that still has work to do. */
|
|
1307
|
+
/* A literal, not a color-mix. Mixing hue 315 into the hue-73 rail takes the
|
|
1308
|
+
shorter arc and lands around hue 47 — a seventh hue, and a brown one, inside
|
|
1309
|
+
a palette whose whole rule is six. */
|
|
1310
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
1311
|
+
.nav-active-hover:hover {
|
|
1312
|
+
background: oklch(0.34 0.02 73);
|
|
1313
|
+
}
|
|
1206
1314
|
}
|
|
1207
1315
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
-
|
|
1316
|
+
/* Users who ask for more contrast get the most legible path, not the least
|
|
1317
|
+
(#162) — a heavier marker and a white hairline around the pill. */
|
|
1318
|
+
/* More contrast means MORE CONTRAST, not more chrome: the same pigment, lifted.
|
|
1319
|
+
Fill vs rail 8.12 -> 10.60:1, ink label 8.84 -> 11.54:1. No rim, no marker. */
|
|
1320
|
+
@media (prefers-contrast: more) {
|
|
1321
|
+
.nav-active {
|
|
1322
|
+
--nav-active: oklch(0.86 0.115 73);
|
|
1323
|
+
}
|
|
1324
|
+
.nav-active-hover:hover {
|
|
1325
|
+
background: oklch(1 0 0 / 24%);
|
|
1326
|
+
}
|
|
1214
1327
|
}
|
|
1215
1328
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1329
|
+
/* ============================================================
|
|
1330
|
+
* FOCUS RING — two-tone, because --ring is now the tile.
|
|
1331
|
+
*
|
|
1332
|
+
* The old recipe was `focus-visible:ring-ring/50 focus-visible:ring-[3px]`.
|
|
1333
|
+
* Two problems. The alpha: composited in gamma space a 50% band measures
|
|
1334
|
+
* 2.04-2.19:1 in light and 2.63-2.76:1 in dark against the surfaces it lands
|
|
1335
|
+
* on — under SC 2.4.11's 3:1, on every control in the system, and that defect
|
|
1336
|
+
* predates this palette. And the pigment: --ring follows --primary, which is
|
|
1337
|
+
* now #f5a623 at 1.83:1 on a light page, so a solid orange band would not fix
|
|
1338
|
+
* it either.
|
|
1339
|
+
*
|
|
1340
|
+
* So the ring is bounded by board ink. The ink hairlines carry the boundary
|
|
1341
|
+
* (16:1 on light surfaces) and the orange band carries the identity, which is
|
|
1342
|
+
* the same division of labour the tile chip uses. In dark the outer hairline
|
|
1343
|
+
* merges into the page (1.11:1) and the orange band — 9.77:1 there — carries
|
|
1344
|
+
* the indicator directly, which is why the recipe is safe in both themes for
|
|
1345
|
+
* different reasons.
|
|
1346
|
+
* ============================================================ */
|
|
1347
|
+
.focus-ring {
|
|
1348
|
+
outline: none;
|
|
1221
1349
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1350
|
+
.focus-ring:focus-visible {
|
|
1351
|
+
outline: none;
|
|
1352
|
+
box-shadow:
|
|
1353
|
+
0 0 0 1px var(--ring-edge),
|
|
1354
|
+
0 0 0 3px var(--ring),
|
|
1355
|
+
0 0 0 4px var(--ring-edge);
|
|
1228
1356
|
}
|
|
1229
1357
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1358
|
+
/* ============================================================
|
|
1359
|
+
* BOARD BACKDROP — the one-shot settle (#231).
|
|
1360
|
+
*
|
|
1361
|
+
* Each row drops and settles once on mount, like a row of flaps landing, and
|
|
1362
|
+
* then STOPS. That is the whole performance contract: a first-run screen is
|
|
1363
|
+
* disproportionately likely to be on a phone, and an ambient loop there costs
|
|
1364
|
+
* battery for as long as the screen is open. The WebGL aurora this replaces
|
|
1365
|
+
* held a GL context and ran a shader every frame; this runs for ~700ms and
|
|
1366
|
+
* then the field is inert DOM.
|
|
1367
|
+
*
|
|
1368
|
+
* transform and opacity only, so it stays on the compositor: no layout, no
|
|
1369
|
+
* paint, no main-thread work per frame.
|
|
1370
|
+
* ============================================================ */
|
|
1371
|
+
@keyframes board-backdrop-settle {
|
|
1372
|
+
from {
|
|
1373
|
+
opacity: 0;
|
|
1374
|
+
transform: translateY(-8px) scaleY(0.72);
|
|
1240
1375
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
backdrop-filter: none;
|
|
1245
|
-
-webkit-backdrop-filter: none;
|
|
1376
|
+
to {
|
|
1377
|
+
opacity: var(--board-backdrop-opacity, 0.28);
|
|
1378
|
+
transform: translateY(0) scaleY(1);
|
|
1246
1379
|
}
|
|
1247
1380
|
}
|
|
1248
1381
|
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
background: var(--nav-active);
|
|
1253
|
-
/* Same reasoning as the @supports fallback above (#162). */
|
|
1254
|
-
box-shadow: inset 0 0 0 1px oklch(1 0 0 / 0.22);
|
|
1255
|
-
backdrop-filter: none;
|
|
1256
|
-
-webkit-backdrop-filter: none;
|
|
1257
|
-
}
|
|
1258
|
-
.sidebar-gradient .nav-active-hover:hover,
|
|
1259
|
-
.sidebar-gradient-horizontal .nav-active-hover:hover {
|
|
1260
|
-
background: var(--nav-active-hover);
|
|
1261
|
-
backdrop-filter: none;
|
|
1262
|
-
-webkit-backdrop-filter: none;
|
|
1263
|
-
}
|
|
1382
|
+
.board-backdrop-row {
|
|
1383
|
+
opacity: var(--board-backdrop-opacity, 0.28);
|
|
1384
|
+
animation: board-backdrop-settle 0.42s var(--motion-ease-standard) both;
|
|
1264
1385
|
}
|
|
1265
1386
|
|
|
1266
|
-
/*
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
background: var(--nav-active);
|
|
1274
|
-
color: var(--nav-active-foreground);
|
|
1275
|
-
box-shadow: inset 0 0 0 2px oklch(1 0 0 / 0.35);
|
|
1276
|
-
backdrop-filter: none;
|
|
1277
|
-
-webkit-backdrop-filter: none;
|
|
1278
|
-
}
|
|
1279
|
-
.sidebar-gradient .nav-active-hover:hover,
|
|
1280
|
-
.sidebar-gradient-horizontal .nav-active-hover:hover {
|
|
1281
|
-
background: var(--nav-active-hover);
|
|
1282
|
-
backdrop-filter: none;
|
|
1283
|
-
-webkit-backdrop-filter: none;
|
|
1387
|
+
/* Already settled, nothing declared. Not `animation: none` on top of a rule
|
|
1388
|
+
that also sets the from-state — the row must not be left at opacity 0. */
|
|
1389
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1390
|
+
.board-backdrop-row {
|
|
1391
|
+
animation: none;
|
|
1392
|
+
opacity: var(--board-backdrop-opacity, 0.28);
|
|
1393
|
+
transform: none;
|
|
1284
1394
|
}
|
|
1285
1395
|
}
|
|
1396
|
+
.reduce-motion .board-backdrop-row {
|
|
1397
|
+
animation: none;
|
|
1398
|
+
opacity: var(--board-backdrop-opacity, 0.28);
|
|
1399
|
+
transform: none;
|
|
1400
|
+
}
|
|
1286
1401
|
|
|
1287
1402
|
/* Card fade-in animation (inspired by reactbits CardNav) */
|
|
1288
1403
|
@keyframes card-fade-in {
|
|
@@ -1303,8 +1418,11 @@ html[data-board-switch="backward"]::view-transition-new(board-page) {
|
|
|
1303
1418
|
|
|
1304
1419
|
/* Type scale: ratio 1.25; body 1rem (16px). Tailwind text-xs/sm/base/lg/xl/2xl/3xl/4xl align. */
|
|
1305
1420
|
/* Page typography - consistent titles and descriptions */
|
|
1421
|
+
/* The page's H1 has to outrank CardTitle, which is text-base/600. At
|
|
1422
|
+
text-lg/500 it did not — a section heading inside a sibling card read as
|
|
1423
|
+
more important than the name of the page (#231). Now two full steps up. */
|
|
1306
1424
|
.page-title {
|
|
1307
|
-
@apply text-
|
|
1425
|
+
@apply text-2xl sm:text-3xl font-semibold tracking-tight;
|
|
1308
1426
|
text-wrap: balance;
|
|
1309
1427
|
}
|
|
1310
1428
|
.page-description {
|