@fiestaboard/ui 3.4.0 → 5.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/README.md +13 -1
- 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/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/editor.css +10 -3
- package/dist/fonts.css +16 -8
- package/dist/index.d.ts +3 -5
- package/dist/index.js +90 -92
- package/dist/theme.css +684 -519
- package/package.json +4 -6
- 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
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* app/globals.css. It must be imported into a Tailwind v4 pipeline:
|
|
6
6
|
*
|
|
7
7
|
* @import "tailwindcss";
|
|
8
|
-
* @import "@fiestaboard/ui/fonts.css"; <- opt-in
|
|
9
|
-
* @import "@fiestaboard/ui/theme.css"; skip fonts.css if you self-host
|
|
8
|
+
* @import "@fiestaboard/ui/fonts.css"; <- opt-in @font-face registrations;
|
|
9
|
+
* @import "@fiestaboard/ui/theme.css"; skip fonts.css if you self-host
|
|
10
10
|
* @source "../node_modules/@fiestaboard/ui/dist";
|
|
11
11
|
*
|
|
12
12
|
* Dark mode is class-based: toggle the `dark` class on <html>.
|
|
@@ -15,23 +15,14 @@
|
|
|
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
|
|
|
30
21
|
@theme inline {
|
|
31
22
|
--color-background: var(--background);
|
|
32
23
|
--color-foreground: var(--foreground);
|
|
33
|
-
--font-sans: var(--font-
|
|
34
|
-
--font-mono: var(--font-
|
|
24
|
+
--font-sans: var(--font-sans-stack);
|
|
25
|
+
--font-mono: var(--font-mono-stack);
|
|
35
26
|
--color-sidebar-ring: var(--sidebar-ring);
|
|
36
27
|
--color-sidebar-border: var(--sidebar-border);
|
|
37
28
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
@@ -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);
|
|
@@ -190,17 +196,55 @@
|
|
|
190
196
|
}
|
|
191
197
|
|
|
192
198
|
:root {
|
|
193
|
-
/*
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
199
|
+
/* ============================================================
|
|
200
|
+
* FONT FAMILY TOKENS.
|
|
201
|
+
*
|
|
202
|
+
* The family names here match the @font-face declarations registered by
|
|
203
|
+
* the opt-in @fiestaboard/ui/fonts.css subpath. Consumers that self-host
|
|
204
|
+
* the faces skip fonts.css; without it these tokens degrade gracefully to
|
|
205
|
+
* the system stacks below. These CSS vars were once set by Next.js's
|
|
206
|
+
* next/font wrapper in layout.tsx — moved here so they're owned by the
|
|
207
|
+
* stylesheet, not a build-time JS wrapper.
|
|
208
|
+
*
|
|
209
|
+
* The token NAMES are deliberately vendor-neutral. They used to be
|
|
210
|
+
* --font-geist-sans / --font-geist-mono, which meant the design system
|
|
211
|
+
* published a specific foundry's name as its public contract and every
|
|
212
|
+
* future typeface change was a rename. A design system names the ROLE,
|
|
213
|
+
* not the vendor. The old names are aliased below for one minor.
|
|
214
|
+
*
|
|
215
|
+
* WHY ARCHIVO. It replaced Geist, which was never chosen so much as
|
|
216
|
+
* inherited — Geist is Vercel's house font, so the product read as
|
|
217
|
+
* "built with Next.js" rather than as FiestaBoard. Archivo is drawn from
|
|
218
|
+
* American grotesque signage, which is the world the hardware already
|
|
219
|
+
* lives in, and it carries a wdth 62-125 axis that no other candidate
|
|
220
|
+
* offered: normal width is a disciplined UI workhorse, expanded is a
|
|
221
|
+
* departure board, from one family.
|
|
222
|
+
*
|
|
223
|
+
* WHY SPLINE SANS MONO. The mono token is not a code-editor setting: every
|
|
224
|
+
* split-flap glyph the product draws is `font-mono font-semibold` (see
|
|
225
|
+
* board-display.tsx and static-board-display.tsx), so this face renders the
|
|
226
|
+
* hardware preview, and the template editor that previews it. It was chosen
|
|
227
|
+
* on the two metrics that govern a fixed character cell read at a distance:
|
|
228
|
+
* cap height 72.7% of em (against Geist Mono's 71.0%) and glyph advance
|
|
229
|
+
* 60.0%. Weight range is 300-700; the only weights paired with `font-mono`
|
|
230
|
+
* in this package are 500 and 600, so nothing synthesizes.
|
|
231
|
+
*
|
|
232
|
+
* NOTE: fonts.css registers the WEIGHT-ONLY file (34 KB latin), so
|
|
233
|
+
* font-stretch is inert today. Nothing in this package sets font-stretch
|
|
234
|
+
* yet, and shipping an axis nothing uses is the same mistake as shipping
|
|
235
|
+
* an undesigned palette (see the #173 note above). When display-width
|
|
236
|
+
* type actually lands, switch fonts.css to
|
|
237
|
+
* @fontsource-variable/archivo/wdth.css (88 KB latin) and the same
|
|
238
|
+
* "Archivo Variable" family name gains the axis with no token change.
|
|
239
|
+
* ============================================================ */
|
|
240
|
+
--font-sans-stack: "Archivo Variable", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
241
|
+
--font-mono-stack: "Spline Sans Mono Variable", ui-monospace, SFMono-Regular, "SF Mono", Monaco, Menlo, monospace;
|
|
242
|
+
/* DEPRECATED, removed in the next major: the pre-Archivo token names.
|
|
243
|
+
Aliased so a consumer referencing var(--font-geist-sans) directly does
|
|
244
|
+
not break on upgrade — they now resolve to whatever the role tokens
|
|
245
|
+
above hold, which is the point. */
|
|
246
|
+
--font-geist-sans: var(--font-sans-stack);
|
|
247
|
+
--font-geist-mono: var(--font-mono-stack);
|
|
204
248
|
/* Tell the UA which form-control palette to paint (#160). Without this,
|
|
205
249
|
every browser-drawn widget — accent-color'd checkboxes, file/date/number
|
|
206
250
|
chrome, scrollbars, autofill — renders in LIGHT chrome, which on the dark
|
|
@@ -208,12 +252,31 @@
|
|
|
208
252
|
Declared next to the palette it belongs to, once per theme. */
|
|
209
253
|
color-scheme: light;
|
|
210
254
|
--radius: 0.625rem;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
255
|
+
/* ============================================================
|
|
256
|
+
* HUE LOCK (#231). Every chromatic token in this file resolves to one
|
|
257
|
+
* of the SIX hues the hardware can physically flip, converted from
|
|
258
|
+
* lib/board-colors.ts:
|
|
259
|
+
*
|
|
260
|
+
* red 29 · orange 73 · yellow 104 · green 133 · blue 251 · violet 315
|
|
261
|
+
*
|
|
262
|
+
* Hue is theme-invariant: a semantic token keeps its hue in light and
|
|
263
|
+
* dark, and only LIGHTNESS moves, set by the contrast target. Before
|
|
264
|
+
* this, the palette carried ~20 hue angles with no ramp behind them and
|
|
265
|
+
* --warning swung 25deg between themes, so a token could re-encode its
|
|
266
|
+
* own meaning on a theme toggle.
|
|
267
|
+
*
|
|
268
|
+
* Surfaces are no longer chroma-0: they carry a 0.003-0.016 warm cast at
|
|
269
|
+
* hue 73 (the board's orange) so the neutral 90% of the product reads as
|
|
270
|
+
* chosen rather than inherited from stock shadcn.
|
|
271
|
+
*
|
|
272
|
+
* Do not add a seventh hue. token-registry.ts asserts this in CI.
|
|
273
|
+
* ============================================================ */
|
|
274
|
+
--background: oklch(0.965 0.003 73);
|
|
275
|
+
--foreground: oklch(0.16 0.008 73);
|
|
276
|
+
--card: oklch(0.995 0.003 73);
|
|
277
|
+
--card-foreground: oklch(0.16 0.008 73);
|
|
278
|
+
--popover: oklch(0.995 0.003 73);
|
|
279
|
+
--popover-foreground: oklch(0.16 0.008 73);
|
|
217
280
|
/* ============================================================
|
|
218
281
|
* INVARIANT — "one token means 'this control is on'" (#156/#159).
|
|
219
282
|
* --primary is the single pigment that says a control is the primary
|
|
@@ -224,18 +287,48 @@
|
|
|
224
287
|
* into separate hues, and do not let --primary drift to a neutral —
|
|
225
288
|
* that is what made the dark theme brandless (see .dark below).
|
|
226
289
|
* --primary carries the FiestaBoard warm orange in BOTH themes.
|
|
290
|
+
*
|
|
291
|
+
* --primary IS THE TILE, and the label on it is board ink (#231).
|
|
292
|
+
*
|
|
293
|
+
* It was oklch(0.52 0.111 73) = #8f5d00 — the board's orange darkened until
|
|
294
|
+
* a WHITE label cleared AA on it. But maxC(0.52, 73) is exactly 0.111, so
|
|
295
|
+
* that value was not a badly-tuned orange, it was the most orange anything
|
|
296
|
+
* can be at that lightness: a mustard. The brand had been WCAG-darkened into
|
|
297
|
+
* a different colour, which is the failure the audit opened with.
|
|
298
|
+
*
|
|
299
|
+
* The white label was the only reason it had to be dark, and the white label
|
|
300
|
+
* was never designed — it is stock shadcn's --primary-foreground that nobody
|
|
301
|
+
* re-asked when the FiestaBoard palette landed. The dark theme had already
|
|
302
|
+
* worked this out and used board ink.
|
|
303
|
+
*
|
|
304
|
+
* So: --primary is the literal #f5a623, theme-invariant, with board ink on
|
|
305
|
+
* it at 8.84:1 — better than white on #8f5d00 ever managed (5.63:1). The
|
|
306
|
+
* accessible option was the MORE saturated one the whole time. It also makes
|
|
307
|
+
* the most-clicked pixel in the product the exact colour the hardware flips.
|
|
308
|
+
*
|
|
309
|
+
* White on this field is 2.03:1 and is banned; there is exactly one
|
|
310
|
+
* on-primary foreground token so that is enforceable.
|
|
227
311
|
* ============================================================ */
|
|
228
|
-
--primary:
|
|
229
|
-
--primary-foreground: oklch(0.
|
|
230
|
-
|
|
231
|
-
--
|
|
232
|
-
--
|
|
233
|
-
/*
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
--
|
|
237
|
-
--
|
|
238
|
-
--
|
|
312
|
+
--primary: #f5a623;
|
|
313
|
+
--primary-foreground: oklch(0.205 0.008 73);
|
|
314
|
+
/* Hover RAISES label contrast (8.84 -> 10.78); press lowers it to 7.04. */
|
|
315
|
+
--primary-hover: oklch(0.84 0.128 73);
|
|
316
|
+
--primary-active: oklch(0.72 0.153 73);
|
|
317
|
+
/* Board ink hairline that bounds the focus ring. The ring is --primary, and
|
|
318
|
+
an orange band is only 1.83:1 against a light page, so the INK carries the
|
|
319
|
+
boundary and the orange carries the identity. */
|
|
320
|
+
--ring-edge: oklch(0.205 0.008 73);
|
|
321
|
+
--secondary: oklch(0.925 0.003 73);
|
|
322
|
+
--secondary-foreground: oklch(0.16 0.008 73);
|
|
323
|
+
--muted: oklch(0.935 0.003 73);
|
|
324
|
+
/* Muted text needs >=4.5:1 contrast vs --background to meet WCAG AA.
|
|
325
|
+
8.32:1 on --background, 9.07:1 on --card, 7.61:1 on --muted. */
|
|
326
|
+
--muted-foreground: oklch(0.4 0.006 73);
|
|
327
|
+
--accent: oklch(0.945 0.003 73);
|
|
328
|
+
--accent-foreground: oklch(0.16 0.008 73);
|
|
329
|
+
/* Board red. 5.36:1 on --background; white label 5.93:1. */
|
|
330
|
+
--destructive: oklch(0.52 0.16 29);
|
|
331
|
+
--destructive-foreground: oklch(1 0 0);
|
|
239
332
|
/* ============================================================
|
|
240
333
|
* --border vs --input: TWO DIFFERENT INTENTS, NOT ONE VALUE (#161).
|
|
241
334
|
*
|
|
@@ -255,64 +348,231 @@
|
|
|
255
348
|
* Derived from --foreground as an alpha fraction so the boundary
|
|
256
349
|
* tracks the text color in every theme and season instead of being a
|
|
257
350
|
* 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.
|
|
351
|
+
* 3:1 on both surfaces with headroom (light 50% -> 3.55/3.62,
|
|
352
|
+
* dark 40% -> 3.51/3.56); they differ because contrast ramps faster
|
|
353
|
+
* for light-on-dark than dark-on-light. They moved up 2 points with the
|
|
354
|
+
* hue lock because --foreground itself moved.
|
|
355
|
+
*
|
|
356
|
+
* --border now joins the same derivation instead of being a hand-tuned
|
|
357
|
+
* grey, so decoration tracks the text through every theme and season too.
|
|
261
358
|
*
|
|
262
359
|
* DO NOT collapse these two tokens into one value in either direction.
|
|
263
360
|
* ============================================================ */
|
|
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
|
-
--
|
|
361
|
+
--border: color-mix(in oklch, var(--foreground) 11%, transparent);
|
|
362
|
+
--input: color-mix(in oklch, var(--foreground) 50%, transparent);
|
|
363
|
+
--ring: var(--primary); /* = --primary, see the invariant above */
|
|
364
|
+
/* ============================================================
|
|
365
|
+
* THE RAIL FOLLOWS THE THEME (#231).
|
|
366
|
+
*
|
|
367
|
+
* It was theme-invariant for a while — one warm graphite rail in both
|
|
368
|
+
* modes, argued as "a fixture, like the board bezel". That argument was
|
|
369
|
+
* measured and it does not hold: the same hex is 14.87:1 against the
|
|
370
|
+
* light page and 1.20:1 against the dark one. It was never invariant in
|
|
371
|
+
* the way that matters. In light mode it was the single highest-contrast
|
|
372
|
+
* object on the screen, which is the exact "the chrome out-shouts the
|
|
373
|
+
* product" fault that got the gradient deleted.
|
|
374
|
+
*
|
|
375
|
+
* So light mode gets a light rail at L 0.93 — a step DOWN from the page,
|
|
376
|
+
* not up. Chrome separation in light mode is always a low ratio (1.11:1
|
|
377
|
+
* here) and that is correct: the edge is carried by the derived border,
|
|
378
|
+
* not by a luminance cliff. Going lighter than the page instead lands at
|
|
379
|
+
* 1.03-1.05:1, which is genuinely invisible.
|
|
380
|
+
*
|
|
381
|
+
* The fork is deliberately ONE PAIR — this token and its foreground.
|
|
382
|
+
* Everything else on the rail derives from --sidebar-foreground (see
|
|
383
|
+
* .sidebar-gradient), which is what stops this from re-becoming the
|
|
384
|
+
* many-valued fork that shipped black labels onto a dark rail.
|
|
385
|
+
* ============================================================ */
|
|
386
|
+
--sidebar: oklch(0.93 0.003 73);
|
|
387
|
+
--sidebar-foreground: oklch(0.16 0.008 73);
|
|
388
|
+
--sidebar-accent: oklch(1 0 0 / 14%);
|
|
389
|
+
--sidebar-accent-foreground: oklch(0.985 0.002 73);
|
|
390
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
391
|
+
--sidebar-ring: var(--primary); /* = --primary, see the invariant above */
|
|
392
|
+
/* Board blue. 5.01:1 on --background; white label 5.54:1. */
|
|
393
|
+
--info: oklch(0.52 0.151 251);
|
|
394
|
+
--info-foreground: oklch(1 0 0);
|
|
395
|
+
/* Board green. 4.76:1 on --background; white label 5.27:1. */
|
|
396
|
+
--success: oklch(0.52 0.147 133);
|
|
397
|
+
--success-foreground: oklch(1 0 0);
|
|
398
|
+
/* Board yellow. --warning and --brand used to sit 5deg apart on the hue
|
|
399
|
+
wheel and were held apart by lightness alone (1.61:1). Under the hue
|
|
400
|
+
lock they are 31deg apart — genuinely different pigments — so the
|
|
401
|
+
separation no longer depends on a lightness budget that light mode
|
|
402
|
+
could not afford. 1.56:1 of luminance remains between them.
|
|
403
|
+
NOTE: yellow cannot be both dark enough to read as text on white and
|
|
404
|
+
light enough to read as yellow. --warning is therefore a FILL, ICON
|
|
405
|
+
and BORDER color, never a text color: its label is board ink at 4.93:1.
|
|
406
|
+
Alert enforces this — status color never touches a sentence. */
|
|
407
|
+
--warning: oklch(0.62 0.131 104);
|
|
408
|
+
--warning-foreground: oklch(0.205 0.008 73);
|
|
409
|
+
--tag-variable: oklch(0.52 0.16 315);
|
|
410
|
+
--tag-variable-foreground: oklch(0.4 0.1 315);
|
|
411
|
+
--tag-success: oklch(0.52 0.147 133);
|
|
412
|
+
--tag-success-foreground: oklch(0.4 0.1 133);
|
|
413
|
+
--tag-formula: oklch(0.52 0.151 251);
|
|
414
|
+
--tag-formula-foreground: oklch(0.4 0.1 251);
|
|
415
|
+
/* ============================================================
|
|
416
|
+
* PLUGIN CATEGORIES ARE LITERAL TILES (#231), theme-invariant.
|
|
417
|
+
*
|
|
418
|
+
* These are the exact hardware hexes from --color-board-* above, not an
|
|
419
|
+
* approximation of them: a physical tile does not have a theme, and it
|
|
420
|
+
* does not have a rounding error either. They are consumed ONLY by
|
|
421
|
+
* PluginCategoryBadge, which renders them as a tile square on the board's
|
|
422
|
+
* own dark surface — never as a text color, never as a page fill.
|
|
423
|
+
*
|
|
424
|
+
* The old set shared ONE lightness across all seven, so the badges
|
|
425
|
+
* measured 1.001:1 against each other and collapsed completely under
|
|
426
|
+
* deuteranopia. The tiles span 4.21:1 to 19.65:1 against board black, so
|
|
427
|
+
* six of the seven separate on lightness alone with hue removed.
|
|
428
|
+
* The exception is home/orange vs transit/green (dL 0.001) — that pair is
|
|
429
|
+
* told apart by the word beside the tile, not by the tile.
|
|
430
|
+
*
|
|
431
|
+
* The mapping is NOT claimed to come from the hardware docs, which
|
|
432
|
+
* publish a temperature-and-status vocabulary (red = alerts and hot),
|
|
433
|
+
* not a content taxonomy. It is argued on its own terms: transit is go,
|
|
434
|
+
* weather is sky, art is the brightest pigment the board owns, and
|
|
435
|
+
* utility is the white tile because a utility should be the quietest
|
|
436
|
+
* loud thing in the set.
|
|
437
|
+
* ============================================================ */
|
|
438
|
+
--category-entertainment: #eb4034; /* board red */
|
|
439
|
+
--category-entertainment-foreground: oklch(0.52 0.16 29);
|
|
440
|
+
--category-home: #f5a623; /* board orange */
|
|
441
|
+
--category-home-foreground: oklch(0.52 0.111 73);
|
|
442
|
+
--category-art: #f8e71c; /* board yellow */
|
|
443
|
+
--category-art-foreground: oklch(0.52 0.11 104);
|
|
444
|
+
--category-transit: #7ed321; /* board green */
|
|
445
|
+
--category-transit-foreground: oklch(0.52 0.147 133);
|
|
446
|
+
--category-weather: #4a90d9; /* board blue */
|
|
447
|
+
--category-weather-foreground: oklch(0.52 0.151 251);
|
|
448
|
+
--category-data: #9b59b6; /* board violet */
|
|
449
|
+
--category-data-foreground: oklch(0.52 0.16 315);
|
|
450
|
+
--category-utility: #ffffff; /* board white */
|
|
451
|
+
--category-utility-foreground: oklch(0.4 0.014 73);
|
|
452
|
+
/* ============================================================
|
|
453
|
+
* THE SIX HUES AT AN INK LIGHTNESS (#231).
|
|
454
|
+
*
|
|
455
|
+
* The --category-* tokens above are the literal tiles: correct on the
|
|
456
|
+
* board's black surface, useless anywhere else. Board yellow on light
|
|
457
|
+
* paper is 1.15:1 and board orange is 1.83:1 — neither can draw a 2px
|
|
458
|
+
* icon stroke, let alone text.
|
|
459
|
+
*
|
|
460
|
+
* These are the same six hues moved to the ink plateau, where they can.
|
|
461
|
+
* Every one clears 4.7:1 on --background and 5.1:1 on --card in light,
|
|
462
|
+
* and 9.2:1 / 8.5:1 in dark — so they are legal for icons, rules, and
|
|
463
|
+
* text alike. Hue is identical to the tile; only lightness differs.
|
|
464
|
+
*
|
|
465
|
+
* This is what a surface reaches for when it wants "one of the brand's
|
|
466
|
+
* colours" and is not a board.
|
|
467
|
+
* ============================================================ */
|
|
468
|
+
--hue-red: oklch(0.52 0.16 29);
|
|
469
|
+
--hue-orange: oklch(0.52 0.111 73);
|
|
470
|
+
--hue-yellow: oklch(0.52 0.11 104);
|
|
471
|
+
--hue-green: oklch(0.52 0.147 133);
|
|
472
|
+
--hue-blue: oklch(0.52 0.151 251);
|
|
473
|
+
--hue-violet: oklch(0.52 0.16 315);
|
|
474
|
+
/* Scrim. In LIGHT a dialog reads 5.39:1 against the scrimmed page.
|
|
475
|
+
In DARK that number is meaningless — WCAG ratio compresses to ~1.3:1
|
|
476
|
+
between any two near-black surfaces no matter how they are tuned — so
|
|
477
|
+
modality in dark is carried by --elevation-modal's ring and shadow,
|
|
478
|
+
not by a surface contrast ratio. Do not "fix" the dark figure; it is
|
|
479
|
+
not measuring what it looks like it is measuring. */
|
|
480
|
+
--overlay: oklch(0.16 0.012 73 / 60%);
|
|
481
|
+
/* ============================================================
|
|
482
|
+
* BRAND: ONE PIGMENT, THREE JOBS (#231).
|
|
483
|
+
*
|
|
484
|
+
* --brand is an INVARIANT ALIAS of --primary, declared the same way
|
|
485
|
+
* --ring is. They used to be two tokens that were byte-identical in
|
|
486
|
+
* light and 0.17 lightness apart in dark, so a consumer picked a variant
|
|
487
|
+
* in light mode and the theme toggle silently changed their mind. One
|
|
488
|
+
* pigment, stated as a rule, is the honest version of that.
|
|
489
|
+
*
|
|
490
|
+
* --brand-tile is the LITERAL #f5a623 tile, identical in both themes,
|
|
491
|
+
* and is a FIELD ONLY. It is 1.83:1 against a light page, so it can be a
|
|
492
|
+
* button face but can never be a link or a text color; its label must be
|
|
493
|
+
* --brand-tile-foreground (board ink, 8.83:1), never white (2.03:1).
|
|
494
|
+
*
|
|
495
|
+
* --brand-hover always moves TOWARD --foreground, so hover increases
|
|
496
|
+
* contrast in both themes (light 5.08 -> 6.51, dark 8.34 -> 9.51)
|
|
497
|
+
* instead of decreasing it, which is what the old --brand-emphasis
|
|
498
|
+
* hover did on the docs site (9.86 -> 3.99, below AA).
|
|
499
|
+
* ============================================================ */
|
|
500
|
+
/* --brand is the TEXT step and is no longer an alias of --primary. Now that
|
|
501
|
+
--primary is the literal tile, it is 1.83:1 on a light page — legal as a
|
|
502
|
+
field, illegal as a link. --hue-orange is the same hue at the ink plateau
|
|
503
|
+
(5.08:1 light / 9.63:1 dark), which is what body copy and links need. */
|
|
504
|
+
--brand: var(--hue-orange);
|
|
505
|
+
--brand-foreground: oklch(0.205 0.008 73);
|
|
506
|
+
--brand-tile: var(--primary); /* DEPRECATED alias — same pigment now */
|
|
507
|
+
/* ============================================================
|
|
508
|
+
* THE WORDMARK IS A LOGOTYPE, and WCAG 1.4.3 exempts logotypes from
|
|
509
|
+
* contrast outright: "text that is part of a logo or brand name has no
|
|
510
|
+
* minimum contrast requirement."
|
|
511
|
+
*
|
|
512
|
+
* It was reading --brand, which is --hue-orange — the token that exists
|
|
513
|
+
* SPECIFICALLY to clear AA as body text. So the one element the spec
|
|
514
|
+
* excuses was the one held to the strictest version of the rule, and it
|
|
515
|
+
* paid in saturation: #8f5d00, a dark ochre, sitting directly beside a
|
|
516
|
+
* taco mark full of bright orange. The wordmark looked duller than the
|
|
517
|
+
* icon it is locked up with.
|
|
518
|
+
*
|
|
519
|
+
* Freed from a floor it never had, it goes where it belongs — in the
|
|
520
|
+
* middle of the mark's own shell tones. 2.60:1 on the light rail.
|
|
521
|
+
* ============================================================ */
|
|
522
|
+
--brand-wordmark: oklch(0.66 0.141 73);
|
|
523
|
+
/* The backdrop is a DARK board in both themes, so light mode needs the
|
|
524
|
+
lower number: a full-strength wall of black tiles on paper stops being a
|
|
525
|
+
background and becomes the subject, with the card marooned in a hole
|
|
526
|
+
punched through it.
|
|
527
|
+
At this strength the field reads as TEXTURE rather than as text, and that
|
|
528
|
+
is the right call once it covers the whole viewport — the grid carries the
|
|
529
|
+
brand on its own, and legible words wallpapered edge to edge would compete
|
|
530
|
+
with the form the screen exists to get filled in. */
|
|
531
|
+
--board-backdrop-opacity: 0.16;
|
|
532
|
+
--brand-tile-foreground: oklch(0.205 0.008 73);
|
|
533
|
+
--brand-hover: color-mix(in oklch, var(--brand) 82%, var(--foreground));
|
|
534
|
+
/* DEPRECATED alias, removed in the next minor. Kept so consumers pinned
|
|
535
|
+
to an older minor — and the docs site's build canary, which greps the
|
|
536
|
+
compiled CSS for `bg-brand-emphasis` and exits(1) if it is missing —
|
|
537
|
+
do not break on the same commit that changes every value. */
|
|
538
|
+
--brand-emphasis: var(--brand);
|
|
539
|
+
/* Navigation active state — board violet, theme-invariant.
|
|
540
|
+
Was indigo-600, the only cool accent in a gold-and-terracotta brand
|
|
541
|
+
and the one token in the file carrying no rationale. White label
|
|
542
|
+
9.11:1. The fill is only 1.81:1 against the rail, so the 3:1 state
|
|
543
|
+
indicator SC 1.4.11 asks for is carried by the gold left marker on
|
|
544
|
+
.nav-active, not by the fill.
|
|
545
|
+
|
|
546
|
+
NO LONGER TRUE, and the marker is gone with it. The pill was dark violet
|
|
547
|
+
because it carried a WHITE label, which forced it down to 1.81:1 against
|
|
548
|
+
the rail — under SC 1.4.11's 3:1 for a state — and the gold bar was a
|
|
549
|
+
patch for that number, not a design. Flipping the label to board ink frees
|
|
550
|
+
the fill to go bright — and the fill it goes to is --primary itself, so
|
|
551
|
+
"the current page" becomes the same pigment as "the focused control" and
|
|
552
|
+
"the primary action". That is the #156/#159 invariant finally covering the
|
|
553
|
+
thing it always claimed to. 8.12:1 against the rail, ink label 8.84:1, so
|
|
554
|
+
the fill carries the state on its own: 1.4.11 asks for 3:1, not for a
|
|
555
|
+
marker, and does not care which pixels supply it.
|
|
556
|
+
|
|
557
|
+
A bright violet also cleared the floor (5.66:1) and was rejected on
|
|
558
|
+
hierarchy, not on arithmetic: it became the loudest object on the screen,
|
|
559
|
+
louder than the primary action, which is backwards for a nav state. */
|
|
560
|
+
/* THE ONE PLACE THE PILL CANNOT BE --primary. The tile is L 0.78, so it
|
|
561
|
+
needs a dark surround: against a light rail it tops out at 2.00:1 even
|
|
562
|
+
if the rail were pure white, under 1.4.11's 3:1 for a state. On a light
|
|
563
|
+
rail the pill inverts instead — ink fill, page-coloured label, 16.78:1
|
|
564
|
+
against the rail. The dark rail keeps the tile (8.12:1), so "one pigment
|
|
565
|
+
means on" still holds everywhere it physically can. */
|
|
566
|
+
--nav-active: var(--foreground);
|
|
567
|
+
--nav-active-foreground: var(--background);
|
|
568
|
+
--nav-active-hover: oklch(1 0 0 / 14%);
|
|
569
|
+
/* Fiesta gradient palette — three board hues at ONE lightness (0.46),
|
|
570
|
+
identical in both themes. Because that surface never changes luminance
|
|
571
|
+
across its 117s loop, the contrast of anything sitting on it is a
|
|
572
|
+
constant rather than a function of wall-clock time. */
|
|
573
|
+
--fiesta-red: oklch(0.46 0.15 29);
|
|
574
|
+
--fiesta-orange: oklch(0.46 0.098 73);
|
|
575
|
+
--fiesta-purple: oklch(0.46 0.15 315);
|
|
316
576
|
/* Page icon gradient stops — 6 vars so pride month can expand to a full rainbow */
|
|
317
577
|
--icon-g1: var(--fiesta-red);
|
|
318
578
|
--icon-g2: var(--fiesta-orange);
|
|
@@ -320,10 +580,12 @@
|
|
|
320
580
|
--icon-g4: var(--fiesta-orange);
|
|
321
581
|
--icon-g5: var(--fiesta-purple);
|
|
322
582
|
--icon-g6: var(--fiesta-purple);
|
|
323
|
-
/* Elevation - subtle card and modal shadows
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
--elevation-
|
|
583
|
+
/* Elevation - subtle card and modal shadows.
|
|
584
|
+
Warm-tinted rather than neutral black: a grey shadow under a warm card
|
|
585
|
+
on a warm page reads as a rendering artifact, not as depth. */
|
|
586
|
+
--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);
|
|
587
|
+
--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);
|
|
588
|
+
--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
589
|
|
|
328
590
|
/* ============================================================
|
|
329
591
|
* Motion tokens (B4) — one duration scale + named easings.
|
|
@@ -379,15 +641,15 @@
|
|
|
379
641
|
* surface | --radius-surface | rounded-lg | 10px | modal surfaces (dialog, alert-dialog), alert, sidebar nav pills
|
|
380
642
|
* card | --radius-card | rounded-xl | 12px | card, page-header icon tile
|
|
381
643
|
* 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,
|
|
644
|
+
* chrome | --radius-chrome | (css/inline) | 14px | desktop sidebar shell
|
|
645
|
+
* chrome-mobile | --radius-chrome-mobile | (css/inline) | 16px | mobile header/menu shell, menu clip-path reveal
|
|
384
646
|
*
|
|
385
647
|
* Components KEEP their Tailwind rounded-* classes — those already
|
|
386
648
|
* resolve through the --radius calc chain, so each role token below
|
|
387
649
|
* is defined with the same math and stays in lockstep with them.
|
|
388
650
|
* New components pick a ROLE from this table instead of a raw size.
|
|
389
651
|
* The chrome radii are deliberately fixed px (not on the rem-based
|
|
390
|
-
* --radius chain)
|
|
652
|
+
* --radius chain).
|
|
391
653
|
*
|
|
392
654
|
* Deliberately off-scale (left as-is): pride-burst particle 2px,
|
|
393
655
|
* scroll-area viewport rounded-[inherit]. Both are arbitrary-value
|
|
@@ -411,7 +673,7 @@
|
|
|
411
673
|
* --z-sidebar 50 desktop sidebar rail
|
|
412
674
|
* --z-sidebar-toggle 51 edge collapse button — LOCAL value: the
|
|
413
675
|
* fixed z-50 aside is its own stacking
|
|
414
|
-
* context, so 51 only beats the
|
|
676
|
+
* context, so 51 only beats the rail's own
|
|
415
677
|
* canvas / z-[1] content column inside it
|
|
416
678
|
* --z-mobile-backdrop 90 dimming layer behind the mobile menu
|
|
417
679
|
* --z-mobile-menu 95 mobile slide-down menu panel
|
|
@@ -443,7 +705,7 @@
|
|
|
443
705
|
*
|
|
444
706
|
* Not on the scale (local stacking / one-offs): select scroll
|
|
445
707
|
* buttons z-10 (sticky inside the popup), sidebar/header content
|
|
446
|
-
* z-[1] (above the
|
|
708
|
+
* z-[1] (above the rail surface), board-switch view-transition
|
|
447
709
|
* groups 0/1 (::view-transition pseudo-tree only).
|
|
448
710
|
* ============================================================ */
|
|
449
711
|
--z-popover: 50;
|
|
@@ -462,12 +724,16 @@
|
|
|
462
724
|
|
|
463
725
|
.dark {
|
|
464
726
|
color-scheme: dark; /* see the note in :root (#160) */
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
--
|
|
470
|
-
--
|
|
727
|
+
/* Hue lock applies here too: same six hues, same hue angles as :root.
|
|
728
|
+
Only lightness moves. Surfaces carry the same warm cast at hue 73, and
|
|
729
|
+
the card lift went 0.030 -> 0.050 so a dark card reads as an object
|
|
730
|
+
rather than an outline drawn on a void. */
|
|
731
|
+
--background: oklch(0.145 0.004 73);
|
|
732
|
+
--foreground: oklch(0.965 0.003 73);
|
|
733
|
+
--card: oklch(0.195 0.004 73);
|
|
734
|
+
--card-foreground: oklch(0.965 0.003 73);
|
|
735
|
+
--popover: oklch(0.235 0.004 73);
|
|
736
|
+
--popover-foreground: oklch(0.965 0.003 73);
|
|
471
737
|
/* Brand-bearing primary (#156). This used to be oklch(0.922 0 0) — stock
|
|
472
738
|
shadcn's achromatic dark primary, never re-designed when the FiestaBoard
|
|
473
739
|
palette landed. Dark is the DEFAULT theme, so every default Button,
|
|
@@ -476,67 +742,94 @@
|
|
|
476
742
|
surface (light 0.52 -> dark 0.72) and held on the same hue family.
|
|
477
743
|
Upholds the "one token means 'this control is on'" invariant declared in
|
|
478
744
|
: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
|
-
--
|
|
745
|
+
/* Theme-invariant: a physical tile does not have a theme. Identical to
|
|
746
|
+
:root — see the note there. 9.77:1 on --background here. */
|
|
747
|
+
--primary: #f5a623;
|
|
748
|
+
--primary-foreground: oklch(0.205 0.008 73);
|
|
749
|
+
--primary-hover: oklch(0.84 0.128 73);
|
|
750
|
+
--primary-active: oklch(0.72 0.153 73);
|
|
751
|
+
--ring-edge: oklch(0.205 0.008 73);
|
|
752
|
+
--secondary: oklch(0.255 0.004 73);
|
|
753
|
+
--secondary-foreground: oklch(0.965 0.003 73);
|
|
754
|
+
--muted: oklch(0.235 0.004 73);
|
|
755
|
+
/* 8.10:1 on --background, 7.51:1 on --card, 6.86:1 on --muted. */
|
|
756
|
+
--muted-foreground: oklch(0.725 0.004 73);
|
|
757
|
+
--accent: oklch(0.225 0.004 73);
|
|
758
|
+
--accent-foreground: oklch(0.965 0.003 73);
|
|
759
|
+
/* Board red. 6.88:1 on --background; board-ink label 6.23:1. */
|
|
760
|
+
--destructive: oklch(0.7 0.17 29);
|
|
761
|
+
--destructive-foreground: oklch(0.205 0.008 73);
|
|
491
762
|
/* --border stays decorative-subtle; --input is the 3:1 control boundary.
|
|
492
763
|
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
|
-
--
|
|
764
|
+
--border: color-mix(in oklch, var(--foreground) 12%, transparent);
|
|
765
|
+
--input: color-mix(in oklch, var(--foreground) 40%, transparent);
|
|
766
|
+
--ring: var(--primary); /* = --primary, see the invariant in :root */
|
|
767
|
+
/* Warm graphite, one step up from --card so the rail reads as housing
|
|
768
|
+
rather than as more page. See the note in :root. */
|
|
769
|
+
--sidebar: oklch(0.24 0.005 73);
|
|
770
|
+
--sidebar-foreground: oklch(0.985 0.002 73);
|
|
771
|
+
--sidebar-accent: oklch(1 0 0 / 14%);
|
|
772
|
+
--sidebar-accent-foreground: oklch(0.985 0.002 73);
|
|
773
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
774
|
+
--sidebar-ring: var(--primary); /* = --primary, see the invariant in :root */
|
|
775
|
+
/* Board blue. 8.57:1 on --background; board-ink label 7.76:1. */
|
|
776
|
+
--info: oklch(0.74 0.139 251);
|
|
777
|
+
--info-foreground: oklch(0.205 0.008 73);
|
|
778
|
+
/* Board green. 10.40:1 on --background; board-ink label 9.42:1. */
|
|
779
|
+
--success: oklch(0.78 0.17 133);
|
|
780
|
+
--success-foreground: oklch(0.205 0.008 73);
|
|
781
|
+
/* Board yellow, the SAME hue as the light --warning (104). The old pair
|
|
782
|
+
drifted 25deg between themes — the only hue-unstable token in the file
|
|
783
|
+
— because light had to separate warning from brand on lightness while
|
|
784
|
+
dark separated them on hue. Under the hue lock the separation is
|
|
785
|
+
structural in both themes: warning is yellow, brand is orange, 31deg
|
|
786
|
+
apart, and neither has to move to stay legible.
|
|
787
|
+
13.05:1 on --background; board-ink label 11.81:1. */
|
|
788
|
+
--warning: oklch(0.86 0.17 104);
|
|
789
|
+
--warning-foreground: oklch(0.205 0.008 73);
|
|
790
|
+
--tag-variable: oklch(0.78 0.164 315);
|
|
791
|
+
--tag-variable-foreground: oklch(0.86 0.1 315);
|
|
792
|
+
--tag-success: oklch(0.78 0.17 133);
|
|
793
|
+
--tag-success-foreground: oklch(0.86 0.1 133);
|
|
794
|
+
--tag-formula: oklch(0.78 0.116 251);
|
|
795
|
+
--tag-formula-foreground: oklch(0.86 0.09 251);
|
|
796
|
+
/* Categories are literal tiles and therefore theme-invariant — these are
|
|
797
|
+
byte-identical to :root on purpose. See the note there. */
|
|
798
|
+
--category-entertainment: #eb4034; /* board red */
|
|
799
|
+
--category-entertainment-foreground: oklch(0.78 0.129 29);
|
|
800
|
+
--category-home: #f5a623; /* board orange */
|
|
801
|
+
--category-home-foreground: oklch(0.78 0.166 73);
|
|
802
|
+
--category-art: #f8e71c; /* board yellow */
|
|
803
|
+
--category-art-foreground: oklch(0.78 0.164 104);
|
|
804
|
+
--category-transit: #7ed321; /* board green */
|
|
805
|
+
--category-transit-foreground: oklch(0.78 0.17 133);
|
|
806
|
+
--category-weather: #4a90d9; /* board blue */
|
|
807
|
+
--category-weather-foreground: oklch(0.78 0.116 251);
|
|
808
|
+
--category-data: #9b59b6; /* board violet */
|
|
809
|
+
--category-data-foreground: oklch(0.78 0.164 315);
|
|
810
|
+
--category-utility: #ffffff; /* board white */
|
|
811
|
+
--category-utility-foreground: oklch(0.725 0.012 73);
|
|
812
|
+
/* Same six hues, lifted to the dark ink plateau. See :root. */
|
|
813
|
+
--hue-red: oklch(0.78 0.129 29);
|
|
814
|
+
--hue-orange: oklch(0.78 0.166 73);
|
|
815
|
+
--hue-yellow: oklch(0.78 0.164 104);
|
|
816
|
+
--hue-green: oklch(0.78 0.17 133);
|
|
817
|
+
--hue-blue: oklch(0.78 0.116 251);
|
|
818
|
+
--hue-violet: oklch(0.78 0.164 315);
|
|
819
|
+
/* See the :root note: the dark figure here is not a contrast ratio worth
|
|
820
|
+
quoting. Modality in dark is carried by --elevation-modal. */
|
|
821
|
+
--overlay: oklch(0.08 0.006 73 / 72%);
|
|
822
|
+
--brand: var(--hue-orange); /* text step — see :root */
|
|
823
|
+
--brand-foreground: oklch(0.205 0.008 73);
|
|
824
|
+
--brand-tile: var(--primary); /* DEPRECATED alias — same pigment now */
|
|
825
|
+
/* On a dark rail the wordmark can be the literal tile — the exact pigment
|
|
826
|
+
the hardware flips, and the one the icon beside it already uses. 8.12:1. */
|
|
827
|
+
--brand-wordmark: var(--primary);
|
|
828
|
+
/* Dark tiles on a dark page need far less to read. See :root. */
|
|
829
|
+
--board-backdrop-opacity: 0.28;
|
|
830
|
+
--brand-tile-foreground: oklch(0.205 0.008 73);
|
|
831
|
+
--brand-hover: color-mix(in oklch, var(--brand) 82%, var(--foreground));
|
|
832
|
+
--brand-emphasis: var(--brand); /* DEPRECATED — see :root */
|
|
540
833
|
/* Navigation active state — indigo palette.
|
|
541
834
|
NOT "lighter for dark mode" (#162): --nav-active is the solid FALLBACK
|
|
542
835
|
fill for the active nav pill, used whenever the frosted-glass treatment
|
|
@@ -547,17 +840,29 @@
|
|
|
547
840
|
one. Darkened to clear AAA (7.47:1) instead. Kept as light as AAA allows
|
|
548
841
|
rather than the 0.42 the audit suggested (8.91:1), because darkening the
|
|
549
842
|
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
|
-
--
|
|
843
|
+
surface behind it — see the fallback rules near .nav-active.
|
|
844
|
+
|
|
845
|
+
Now board violet and theme-invariant — identical to :root. Because the
|
|
846
|
+
rail is a flat, known surface, the pill no
|
|
847
|
+
longer needs a per-theme value, and its contrast no longer depends on
|
|
848
|
+
where a 117-second animation happens to be. */
|
|
849
|
+
--nav-active: var(--primary); /* see :root */
|
|
850
|
+
--nav-active-foreground: var(--primary-foreground);
|
|
851
|
+
--nav-active-hover: oklch(1 0 0 / 14%);
|
|
852
|
+
/* Identical to :root. See the note there. */
|
|
853
|
+
--fiesta-red: oklch(0.46 0.15 29);
|
|
854
|
+
--fiesta-orange: oklch(0.46 0.098 73);
|
|
855
|
+
--fiesta-purple: oklch(0.46 0.15 315);
|
|
856
|
+
/* A dark card is an object, not an outline. This is the same lit-top /
|
|
857
|
+
shadowed-base recipe that makes a character tile read as a raised flap
|
|
858
|
+
(see components/board/board-surfaces.ts charLeafBoxShadow.black), so
|
|
859
|
+
depth in the chrome and depth on the board are one idea. */
|
|
860
|
+
--elevation-card:
|
|
861
|
+
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);
|
|
862
|
+
--elevation-card-hover:
|
|
863
|
+
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);
|
|
864
|
+
/* Carries modality in dark, where a surface contrast ratio cannot. */
|
|
865
|
+
--elevation-modal: 0 24px 48px -12px rgb(0 0 0 / 0.75), inset 0 0 0 1px oklch(1 0 0 / 0.14);
|
|
561
866
|
}
|
|
562
867
|
|
|
563
868
|
@layer base {
|
|
@@ -570,7 +875,16 @@
|
|
|
570
875
|
}
|
|
571
876
|
body {
|
|
572
877
|
@apply bg-background text-foreground;
|
|
573
|
-
font-feature-settings: "ss01"
|
|
878
|
+
/* `font-feature-settings: "ss01"` used to sit here, inherited from
|
|
879
|
+
FiestaBoard's globals.css. It was already a no-op before the typeface
|
|
880
|
+
change: neither the Geist nor the Geist Mono builds this package used
|
|
881
|
+
to ship exposed an ss01 feature (their GSUB tables carry
|
|
882
|
+
ccmp/kern/liga/locl/pnum/tnum and no stylistic sets), and neither
|
|
883
|
+
Archivo nor Spline Sans Mono does either. Left in place it would be
|
|
884
|
+
a trap — the first future typeface that DOES define ss01 would
|
|
885
|
+
silently switch on an arbitrary alternate glyph set nobody chose.
|
|
886
|
+
If a stylistic set is ever wanted, name the feature and the face it
|
|
887
|
+
belongs to at the call site. */
|
|
574
888
|
/* Clip horizontal overflow at the document root so the body itself stays the
|
|
575
889
|
* vertical scroll container. Applying overflow-x to <main> would force its
|
|
576
890
|
* computed overflow-y to `auto`, recreating a nested scrollbar. */
|
|
@@ -668,54 +982,6 @@
|
|
|
668
982
|
* Consumed by the chrome components (Sidebar, PageHeader, …).
|
|
669
983
|
* ============================================================ */
|
|
670
984
|
|
|
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
985
|
/* Board switch transition — the app-wide board selector wraps its state
|
|
720
986
|
update in document.startViewTransition() (see current-board-context.tsx),
|
|
721
987
|
stamping html[data-board-switch="forward"|"backward"]. Only the page
|
|
@@ -880,118 +1146,84 @@ html[data-board-switch="backward"]::view-transition-new(board-page) {
|
|
|
880
1146
|
/* Sidebar & mobile header: Fiesta-brand gradient (red, orange, purple).
|
|
881
1147
|
Repeating gradient for seamless loop (no flash).
|
|
882
1148
|
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
|
-
|
|
896
|
-
/* Logo in light mode: deep warm brown Fiesta + black Board for contrast across gradient */
|
|
897
|
-
.sidebar-gradient:not(.dark) .logo-on-gradient,
|
|
898
|
-
.sidebar-gradient-horizontal:not(.dark) .logo-on-gradient {
|
|
899
|
-
--brand: oklch(0.28 0.08 50);
|
|
900
|
-
--sidebar-foreground: oklch(0 0 0);
|
|
901
|
-
}
|
|
902
1149
|
|
|
903
|
-
/*
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1150
|
+
/* One logo treatment, both themes. These used to fork — near-black "Board" on
|
|
1151
|
+
a light coral rail in light mode, white on a dark rail in dark mode. The
|
|
1152
|
+
rail is now a fixture that does not change with the theme, so the fork is
|
|
1153
|
+
not just unnecessary, it was actively wrong: it painted black text onto a
|
|
1154
|
+
dark rail the moment the light stops were darkened. */
|
|
1155
|
+
/* The `.logo-on-gradient` override is gone: FiestaLogo now reads
|
|
1156
|
+
--brand-wordmark and --foreground directly, so it is correct on the rail, on
|
|
1157
|
+
a wizard card and anywhere else without a scoped rule pinning it. That rule
|
|
1158
|
+
had already caused two bugs by pinning what should have been a token — white
|
|
1159
|
+
"Board" that vanished on a light rail, then a tile-orange "Fiesta" that would
|
|
1160
|
+
have. */
|
|
909
1161
|
|
|
910
|
-
/*
|
|
911
|
-
|
|
912
|
-
|
|
1162
|
+
/* ============================================================
|
|
1163
|
+
* THE RAIL IS QUIET; THE LOCKUP CARRIES THE BRAND (#231).
|
|
1164
|
+
*
|
|
1165
|
+
* This used to be a full-height animated gradient in two hand-forked
|
|
1166
|
+
* palettes — a light coral/peach ramp in light mode, deep oxblood-to-olive
|
|
1167
|
+
* in dark. Three things were wrong with it, and they compounded:
|
|
1168
|
+
*
|
|
1169
|
+
* 1. On an empty install the rail was the brightest, most saturated
|
|
1170
|
+
* object on the screen. The chrome out-shouted the product: the board
|
|
1171
|
+
* is the thing to look at, and it was losing to the navigation.
|
|
1172
|
+
* 2. A red → orange → violet ramp spends its middle third in the browns
|
|
1173
|
+
* and olives between those hues. Over 800px of rail that reads as mud,
|
|
1174
|
+
* and no amount of retuning the stops fixes an interpolation problem.
|
|
1175
|
+
* 3. Nothing sitting on it could be designed, because its background was
|
|
1176
|
+
* a function of wall-clock time.
|
|
1177
|
+
*
|
|
1178
|
+
* The gradient is now confined to the LOCKUP BAND at the top — behind the
|
|
1179
|
+
* logo and the board switcher, where identity belongs and where 88px of
|
|
1180
|
+
* colour reads as a brand mark rather than as a wall. Below that it fades
|
|
1181
|
+
* into the flat rail. The nav body sits on one predictable surface, which
|
|
1182
|
+
* is what lets the active pill and the labels be tuned once instead of
|
|
1183
|
+
* sampled per frame.
|
|
1184
|
+
*
|
|
1185
|
+
* The rail is the same warm graphite in both themes (see --sidebar), so the
|
|
1186
|
+
* whole light/dark fork below it is gone — along with the class of bug where
|
|
1187
|
+
* darkening the light stops left black labels on a dark rail.
|
|
1188
|
+
* ============================================================ */
|
|
1189
|
+
/* Every local override here is DERIVED from --sidebar-foreground rather than
|
|
1190
|
+
hardcoded to white. That is the whole reason the rail can follow the theme
|
|
1191
|
+
again: the previous set assumed a dark rail, so the moment the rail went
|
|
1192
|
+
light it painted white labels on a light surface. Deriving means the rail
|
|
1193
|
+
has exactly one value to fork (--sidebar and its foreground) and everything
|
|
1194
|
+
sitting on it follows for free. */
|
|
913
1195
|
.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);
|
|
1196
|
+
background: var(--sidebar);
|
|
1197
|
+
color: var(--sidebar-foreground);
|
|
928
1198
|
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%);
|
|
1199
|
+
--sidebar-accent: color-mix(in oklch, var(--sidebar-foreground) 10%, transparent);
|
|
1200
|
+
--sidebar-accent-foreground: var(--sidebar-foreground);
|
|
1201
|
+
--sidebar-border: color-mix(in oklch, var(--sidebar-foreground) 14%, transparent);
|
|
1202
|
+
--foreground: var(--sidebar-foreground);
|
|
1203
|
+
--muted-foreground: color-mix(in oklch, var(--sidebar-foreground) 72%, transparent);
|
|
1204
|
+
--accent: color-mix(in oklch, var(--sidebar-foreground) 10%, transparent);
|
|
1205
|
+
--accent-foreground: var(--sidebar-foreground);
|
|
1206
|
+
--border: color-mix(in oklch, var(--sidebar-foreground) 14%, transparent);
|
|
958
1207
|
}
|
|
959
1208
|
|
|
1209
|
+
/* No CSS gradient paints here any more (#231). The rail is a flat surface;
|
|
1210
|
+
colour arrives through the wordmark, the active pill, and the per-page hue
|
|
1211
|
+
on the page icon. */
|
|
1212
|
+
|
|
960
1213
|
.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);
|
|
1214
|
+
/* Flat, like the desktop rail (#231). The mobile bar and the rail are the
|
|
1215
|
+
same object at two widths, so they get the same treatment — a gradient on
|
|
1216
|
+
one and not the other is how two chromes start disagreeing. */
|
|
1217
|
+
background: var(--sidebar);
|
|
1218
|
+
color: var(--sidebar-foreground);
|
|
976
1219
|
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
|
-
}
|
|
1220
|
+
--foreground: var(--sidebar-foreground);
|
|
1221
|
+
--sidebar-accent: color-mix(in oklch, var(--sidebar-foreground) 12%, transparent);
|
|
1222
|
+
--sidebar-accent-foreground: var(--sidebar-foreground);
|
|
1223
|
+
--muted-foreground: color-mix(in oklch, var(--sidebar-foreground) 78%, transparent);
|
|
1224
|
+
--accent: color-mix(in oklch, var(--sidebar-foreground) 12%, transparent);
|
|
1225
|
+
--accent-foreground: var(--sidebar-foreground);
|
|
1226
|
+
--border: color-mix(in oklch, var(--sidebar-foreground) 16%, transparent);
|
|
995
1227
|
}
|
|
996
1228
|
.dark .sidebar-gradient-horizontal {
|
|
997
1229
|
color: oklch(1 0 0);
|
|
@@ -1004,67 +1236,12 @@ html[data-board-switch="backward"]::view-transition-new(board-page) {
|
|
|
1004
1236
|
--border: oklch(1 0 0 / 18%);
|
|
1005
1237
|
}
|
|
1006
1238
|
|
|
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
|
-
|
|
1239
|
+
/* .sidebar-gradient-layer is gone (#57 is closed). It existed to host a
|
|
1240
|
+
compositor-only copy of the scrolling gradient, and later to give the
|
|
1241
|
+
seasonal WebGL aurora a stacking context under the rail's content. Both of
|
|
1242
|
+
those are gone now, and a flat background clips to its own
|
|
1243
|
+
border-radius, so the element, the `isolation: isolate` it required, and the
|
|
1244
|
+
will-change hint it carried all had nothing left to do. */
|
|
1068
1245
|
/* Navigation entrance animations */
|
|
1069
1246
|
@keyframes nav-slide-in-left {
|
|
1070
1247
|
from {
|
|
@@ -1086,17 +1263,10 @@ html[data-board-switch="backward"]::view-transition-new(board-page) {
|
|
|
1086
1263
|
transform: translateY(0);
|
|
1087
1264
|
}
|
|
1088
1265
|
}
|
|
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
1266
|
@media (min-width: 1024px) {
|
|
1094
1267
|
.sidebar-gradient {
|
|
1095
1268
|
animation: nav-slide-in-left var(--motion-duration-slowest) var(--motion-ease-out) both;
|
|
1096
1269
|
}
|
|
1097
|
-
.sidebar-gradient .sidebar-gradient-layer::before {
|
|
1098
|
-
animation-delay: 0.4s;
|
|
1099
|
-
}
|
|
1100
1270
|
}
|
|
1101
1271
|
.sidebar-gradient-horizontal {
|
|
1102
1272
|
animation: nav-slide-in-top var(--motion-duration-slower) var(--motion-ease-out) both;
|
|
@@ -1132,157 +1302,149 @@ html[data-board-switch="backward"]::view-transition-new(board-page) {
|
|
|
1132
1302
|
.content-shift {
|
|
1133
1303
|
transition: none;
|
|
1134
1304
|
}
|
|
1135
|
-
.sidebar-gradient
|
|
1136
|
-
animation: none;
|
|
1137
|
-
background-position: 50% 25%;
|
|
1138
|
-
}
|
|
1305
|
+
.sidebar-gradient,
|
|
1139
1306
|
.sidebar-gradient-horizontal {
|
|
1140
1307
|
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
1308
|
}
|
|
1153
1309
|
}
|
|
1154
1310
|
.reduce-motion .sidebar-transition,
|
|
1155
1311
|
.reduce-motion .content-shift {
|
|
1156
1312
|
transition: none;
|
|
1157
1313
|
}
|
|
1158
|
-
.reduce-motion .sidebar-gradient
|
|
1159
|
-
animation: none;
|
|
1160
|
-
background-position: 50% 25%;
|
|
1161
|
-
}
|
|
1314
|
+
.reduce-motion .sidebar-gradient,
|
|
1162
1315
|
.reduce-motion .sidebar-gradient-horizontal {
|
|
1163
1316
|
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
1317
|
}
|
|
1175
1318
|
|
|
1176
|
-
/*
|
|
1319
|
+
/* ============================================================
|
|
1320
|
+
* Navigation active state — ONE treatment, every surface (#231).
|
|
1321
|
+
*
|
|
1322
|
+
* This used to be five: a frosted backdrop-filter pill over the gradient,
|
|
1323
|
+
* a darker frosted variant for dark mode, and three fallback tiers
|
|
1324
|
+
* (@supports, prefers-reduced-transparency, prefers-contrast) that each
|
|
1325
|
+
* re-declared the solid fill. All five existed because the pill sat on an
|
|
1326
|
+
* animated gradient whose luminance changed over a 117-second loop, so its
|
|
1327
|
+
* contrast could not be computed — only sampled at runtime.
|
|
1328
|
+
*
|
|
1329
|
+
* The rail is now one flat lightness, so the surface
|
|
1330
|
+
* behind the pill is a known quantity and the pill can simply be opaque.
|
|
1331
|
+
* A solid board-violet fill carries a white label at 9.11:1 in both themes.
|
|
1332
|
+
*
|
|
1333
|
+
* NO MARKER. There was a 3px gold bar on the leading edge, and it existed for
|
|
1334
|
+
* one reason: the pill was dark violet with a white label, which put it at
|
|
1335
|
+
* 1.81:1 against the rail, under SC 1.4.11's 3:1 for a state. The bar was a
|
|
1336
|
+
* patch for a number, and a left accent rail is a stock device besides.
|
|
1337
|
+
*
|
|
1338
|
+
* Flipping the label to board ink lets the fill go bright — 5.66:1 against the
|
|
1339
|
+
* rail — so the state carries itself. 1.4.11 asks for 3:1; it does not ask for
|
|
1340
|
+
* a marker, and it does not care which pixels supply it. `font-semibold` on
|
|
1341
|
+
* the label remains as the non-colour second signal.
|
|
1342
|
+
* ============================================================ */
|
|
1177
1343
|
.nav-active {
|
|
1178
1344
|
background: var(--nav-active);
|
|
1179
1345
|
color: var(--nav-active-foreground);
|
|
1180
1346
|
}
|
|
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
|
-
}
|
|
1347
|
+
|
|
1190
1348
|
.nav-active-hover:hover {
|
|
1191
1349
|
background: var(--nav-active-hover);
|
|
1192
1350
|
}
|
|
1193
1351
|
|
|
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);
|
|
1352
|
+
/* The hover tint is the only chrome token left carrying alpha, so this is
|
|
1353
|
+
the one transparency branch that still has work to do. */
|
|
1354
|
+
/* A literal, not a color-mix. Mixing hue 315 into the hue-73 rail takes the
|
|
1355
|
+
shorter arc and lands around hue 47 — a seventh hue, and a brown one, inside
|
|
1356
|
+
a palette whose whole rule is six. */
|
|
1357
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
1358
|
+
.nav-active-hover:hover {
|
|
1359
|
+
background: oklch(0.34 0.02 73);
|
|
1360
|
+
}
|
|
1206
1361
|
}
|
|
1207
1362
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
-
|
|
1363
|
+
/* Users who ask for more contrast get the most legible path, not the least
|
|
1364
|
+
(#162) — a heavier marker and a white hairline around the pill. */
|
|
1365
|
+
/* More contrast means MORE CONTRAST, not more chrome: the same pigment, lifted.
|
|
1366
|
+
Fill vs rail 8.12 -> 10.60:1, ink label 8.84 -> 11.54:1. No rim, no marker. */
|
|
1367
|
+
@media (prefers-contrast: more) {
|
|
1368
|
+
.nav-active {
|
|
1369
|
+
--nav-active: oklch(0.86 0.115 73);
|
|
1370
|
+
}
|
|
1371
|
+
.nav-active-hover:hover {
|
|
1372
|
+
background: oklch(1 0 0 / 24%);
|
|
1373
|
+
}
|
|
1214
1374
|
}
|
|
1215
1375
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1376
|
+
/* ============================================================
|
|
1377
|
+
* FOCUS RING — two-tone, because --ring is now the tile.
|
|
1378
|
+
*
|
|
1379
|
+
* The old recipe was `focus-visible:ring-ring/50 focus-visible:ring-[3px]`.
|
|
1380
|
+
* Two problems. The alpha: composited in gamma space a 50% band measures
|
|
1381
|
+
* 2.04-2.19:1 in light and 2.63-2.76:1 in dark against the surfaces it lands
|
|
1382
|
+
* on — under SC 2.4.11's 3:1, on every control in the system, and that defect
|
|
1383
|
+
* predates this palette. And the pigment: --ring follows --primary, which is
|
|
1384
|
+
* now #f5a623 at 1.83:1 on a light page, so a solid orange band would not fix
|
|
1385
|
+
* it either.
|
|
1386
|
+
*
|
|
1387
|
+
* So the ring is bounded by board ink. The ink hairlines carry the boundary
|
|
1388
|
+
* (16:1 on light surfaces) and the orange band carries the identity, which is
|
|
1389
|
+
* the same division of labour the tile chip uses. In dark the outer hairline
|
|
1390
|
+
* merges into the page (1.11:1) and the orange band — 9.77:1 there — carries
|
|
1391
|
+
* the indicator directly, which is why the recipe is safe in both themes for
|
|
1392
|
+
* different reasons.
|
|
1393
|
+
* ============================================================ */
|
|
1394
|
+
.focus-ring {
|
|
1395
|
+
outline: none;
|
|
1221
1396
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1397
|
+
.focus-ring:focus-visible {
|
|
1398
|
+
outline: none;
|
|
1399
|
+
box-shadow:
|
|
1400
|
+
0 0 0 1px var(--ring-edge),
|
|
1401
|
+
0 0 0 3px var(--ring),
|
|
1402
|
+
0 0 0 4px var(--ring-edge);
|
|
1228
1403
|
}
|
|
1229
1404
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1405
|
+
/* ============================================================
|
|
1406
|
+
* BOARD BACKDROP — the one-shot settle (#231).
|
|
1407
|
+
*
|
|
1408
|
+
* Each row drops and settles once on mount, like a row of flaps landing, and
|
|
1409
|
+
* then STOPS. That is the whole performance contract: a first-run screen is
|
|
1410
|
+
* disproportionately likely to be on a phone, and an ambient loop there costs
|
|
1411
|
+
* battery for as long as the screen is open. The WebGL aurora this replaces
|
|
1412
|
+
* held a GL context and ran a shader every frame; this runs for ~700ms and
|
|
1413
|
+
* then the field is inert DOM.
|
|
1414
|
+
*
|
|
1415
|
+
* transform and opacity only, so it stays on the compositor: no layout, no
|
|
1416
|
+
* paint, no main-thread work per frame.
|
|
1417
|
+
* ============================================================ */
|
|
1418
|
+
@keyframes board-backdrop-settle {
|
|
1419
|
+
from {
|
|
1420
|
+
opacity: 0;
|
|
1421
|
+
transform: translateY(-8px) scaleY(0.72);
|
|
1240
1422
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
backdrop-filter: none;
|
|
1245
|
-
-webkit-backdrop-filter: none;
|
|
1423
|
+
to {
|
|
1424
|
+
opacity: var(--board-backdrop-opacity, 0.28);
|
|
1425
|
+
transform: translateY(0) scaleY(1);
|
|
1246
1426
|
}
|
|
1247
1427
|
}
|
|
1248
1428
|
|
|
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
|
-
}
|
|
1429
|
+
.board-backdrop-row {
|
|
1430
|
+
opacity: var(--board-backdrop-opacity, 0.28);
|
|
1431
|
+
animation: board-backdrop-settle 0.42s var(--motion-ease-standard) both;
|
|
1264
1432
|
}
|
|
1265
1433
|
|
|
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;
|
|
1434
|
+
/* Already settled, nothing declared. Not `animation: none` on top of a rule
|
|
1435
|
+
that also sets the from-state — the row must not be left at opacity 0. */
|
|
1436
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1437
|
+
.board-backdrop-row {
|
|
1438
|
+
animation: none;
|
|
1439
|
+
opacity: var(--board-backdrop-opacity, 0.28);
|
|
1440
|
+
transform: none;
|
|
1284
1441
|
}
|
|
1285
1442
|
}
|
|
1443
|
+
.reduce-motion .board-backdrop-row {
|
|
1444
|
+
animation: none;
|
|
1445
|
+
opacity: var(--board-backdrop-opacity, 0.28);
|
|
1446
|
+
transform: none;
|
|
1447
|
+
}
|
|
1286
1448
|
|
|
1287
1449
|
/* Card fade-in animation (inspired by reactbits CardNav) */
|
|
1288
1450
|
@keyframes card-fade-in {
|
|
@@ -1303,8 +1465,11 @@ html[data-board-switch="backward"]::view-transition-new(board-page) {
|
|
|
1303
1465
|
|
|
1304
1466
|
/* Type scale: ratio 1.25; body 1rem (16px). Tailwind text-xs/sm/base/lg/xl/2xl/3xl/4xl align. */
|
|
1305
1467
|
/* Page typography - consistent titles and descriptions */
|
|
1468
|
+
/* The page's H1 has to outrank CardTitle, which is text-base/600. At
|
|
1469
|
+
text-lg/500 it did not — a section heading inside a sibling card read as
|
|
1470
|
+
more important than the name of the page (#231). Now two full steps up. */
|
|
1306
1471
|
.page-title {
|
|
1307
|
-
@apply text-
|
|
1472
|
+
@apply text-2xl sm:text-3xl font-semibold tracking-tight;
|
|
1308
1473
|
text-wrap: balance;
|
|
1309
1474
|
}
|
|
1310
1475
|
.page-description {
|