@brignano/design 0.2.0 → 0.3.1
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/DECISIONS.md +133 -0
- package/DESIGN.md +74 -13
- package/README.md +8 -3
- package/package.json +4 -1
- package/tailwind.css +27 -2
- package/tokens.chart.css +19 -54
- package/tokens.css +148 -194
package/DECISIONS.md
CHANGED
|
@@ -248,3 +248,136 @@ over the existing state hues. No new hue enters the system.
|
|
|
248
248
|
status share hues in ADR-0004.
|
|
249
249
|
- Standalone exports (SVG files, Mermaid in a pull request) take the light theme's
|
|
250
250
|
values, resolved from `tokens.css` by the consumer at build time.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## ADR-0006 — One declaration per colour, via `light-dark()`
|
|
255
|
+
|
|
256
|
+
**Status:** accepted · 2026-10-25
|
|
257
|
+
|
|
258
|
+
### Context
|
|
259
|
+
|
|
260
|
+
Dark mode lived in three lists: a `prefers-color-scheme` block (the OS default),
|
|
261
|
+
a `[data-theme='dark']` block (the toggle, which has to win), and a copy in
|
|
262
|
+
`previews/_preview.css` so a preview could show both themes at once. CSS can't
|
|
263
|
+
share one declaration block between a media query and a selector, so the
|
|
264
|
+
duplication was structural, and the file's own rule — "edit one, edit the other"
|
|
265
|
+
— was enforced by remembering. The preview copy had already fallen behind
|
|
266
|
+
(`--i-selection`, `--scrim` and `--on-dark` were missing).
|
|
267
|
+
|
|
268
|
+
### Decision
|
|
269
|
+
|
|
270
|
+
Every colour is declared once, on `:root`, as `light-dark(<light>, <dark>)`. The
|
|
271
|
+
theme is `color-scheme`: `light dark` by default, pinned by `[data-theme]`.
|
|
272
|
+
|
|
273
|
+
Arguments are always concrete colours, never a `var()` to another
|
|
274
|
+
theme-dependent token. Nested `light-dark()` — directly, or inside
|
|
275
|
+
`color-mix()` — is valid CSS, but support across engines couldn't be verified
|
|
276
|
+
when this was written, and getting it wrong would blank colours in one browser.
|
|
277
|
+
Where that forces a repeat (a wash repeats its hue, `--bg` repeats a ramp step),
|
|
278
|
+
`npm run check` holds the copy to its source.
|
|
279
|
+
|
|
280
|
+
Shadows differ in blur as well as colour, and `light-dark()` only switches
|
|
281
|
+
colour, so the two themes' shadows are separate layers, each transparent in the
|
|
282
|
+
other theme.
|
|
283
|
+
|
|
284
|
+
### Rationale
|
|
285
|
+
|
|
286
|
+
- **Drift becomes impossible rather than checked.** There's no second list.
|
|
287
|
+
- **Theming becomes scoped for free.** The value is picked where a colour is
|
|
288
|
+
used, so `data-theme="dark"` on a `<section>` themes that section. The preview's
|
|
289
|
+
forced-dark panel is now `color-scheme: dark` and nothing else.
|
|
290
|
+
- **No change in output.** Verified in Chromium against the previous file: all
|
|
291
|
+
119 tokens are identical in five setups (OS light, OS dark, a toggle forcing
|
|
292
|
+
each way, and both agreeing). The shadows and every preview page are
|
|
293
|
+
identical pixel for pixel.
|
|
294
|
+
|
|
295
|
+
### Consequences
|
|
296
|
+
|
|
297
|
+
- Browser floor: Chrome/Edge 123, Firefox 120, Safari 17.5.
|
|
298
|
+
- `getPropertyValue('--x')` returns the `light-dark(…)` text. JavaScript that
|
|
299
|
+
needs a colour resolves it through `getComputedStyle` on an element
|
|
300
|
+
(DESIGN.md §11). ADR-0002's advice that `chart-colors.ts` read the custom
|
|
301
|
+
properties still stands, but by that route.
|
|
302
|
+
- Consumers shouldn't wrap tokens in `color-mix()`; washes are tokens. The
|
|
303
|
+
first consumer that needed one (`life`'s sticky bars) got `--surface-sticky`
|
|
304
|
+
in 0.3.1 rather than a local mix.
|
|
305
|
+
- A build that lowers `light-dark()` for older browsers (Lightning CSS, inside
|
|
306
|
+
Next.js and Vite) fixes the theme at `:root`, losing scoped theming. Found in
|
|
307
|
+
`brignano.io`; consumers set `browserslist` to the floor (DESIGN.md §11).
|
|
308
|
+
- Not a token rename and not a visual change, so a patch release.
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## ADR-0007 — The neutral ramp is one temperature: OKLCH hue 286°
|
|
314
|
+
|
|
315
|
+
**Status:** accepted · 2026-10-25
|
|
316
|
+
|
|
317
|
+
### Context
|
|
318
|
+
|
|
319
|
+
`tokens.css` called the ramp "cool-leaning", and most of it was: light 300–800
|
|
320
|
+
and dark 0–800 sit at OKLCH hue 286°. But light 50, 100 and 200 — the hairline,
|
|
321
|
+
`--line-soft`, the hover and pressed surfaces — sat at 106°, a faint yellow, and
|
|
322
|
+
dark 900, the primary ink, did too. A border and the label beside it disagreed
|
|
323
|
+
on temperature. It was too slight to notice as a colour, but enough to make a
|
|
324
|
+
surface read slightly dirty next to a cool one.
|
|
325
|
+
|
|
326
|
+
### Decision
|
|
327
|
+
|
|
328
|
+
Every tinted step sits at hue 286°. The off-hue steps (light 25, 50, 100, 200,
|
|
329
|
+
900; dark 800, 900) are regenerated at **their existing OKLCH lightness**, so
|
|
330
|
+
contrast stays within 0.04 of its old value. Their chroma fits the curve
|
|
331
|
+
already set by their neighbours: near zero at the ends, peaking in the
|
|
332
|
+
mid-tones. Steps already at 286° are untouched. Pure white stays pure white.
|
|
333
|
+
`npm run check` fails any step that leaves 286° ±3°.
|
|
334
|
+
|
|
335
|
+
### Consequences
|
|
336
|
+
|
|
337
|
+
- A visible change, if a quiet one, so the release that carries it is a minor.
|
|
338
|
+
- The cool ground does the job the original comment gave it: the warm mark
|
|
339
|
+
and the amber attention state are the only warm things on a page.
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## ADR-0008 — Cobalt is our own: OKLCH hue 256°, not Tailwind's blue
|
|
344
|
+
|
|
345
|
+
**Status:** accepted · 2026-10-25
|
|
346
|
+
**Amends:** ADR-0001 (the hue stays cobalt; the values change)
|
|
347
|
+
|
|
348
|
+
### Context
|
|
349
|
+
|
|
350
|
+
ADR-0001 chose cobalt for two reasons: it's semantically inert, and it's
|
|
351
|
+
complementary to larch, so identity and affordance don't compete. The values
|
|
352
|
+
it shipped, `#2563eb` / `#1d4ed8` / `#1e40af` / `#1e3a8a` and `#60a5fa` /
|
|
353
|
+
`#93c5fd` / `#bfdbfe`, are exactly Tailwind's `blue-600…900` and `blue-300…400`,
|
|
354
|
+
and the sequential chart ramp was Tailwind's blues too. That undercut both the
|
|
355
|
+
second reason and DESIGN.md §1's "nothing here should read as a template":
|
|
356
|
+
|
|
357
|
+
- Tailwind's blue is at OKLCH 263–266°, leaning violet. Larch is ≈68°, so its
|
|
358
|
+
opposite is ≈248°. The stock values were 15° further from that than they
|
|
359
|
+
needed to be.
|
|
360
|
+
- It isn't even one hue. Across the ramp the stock steps wander from 251.8°
|
|
361
|
+
to 265.6°, so hover and pressed shift colour as well as depth.
|
|
362
|
+
- It is the single most recognisable colour in the Tailwind default theme.
|
|
363
|
+
|
|
364
|
+
### Decision
|
|
365
|
+
|
|
366
|
+
Every cobalt step, interactive and sequential, sits at **OKLCH hue 256°**, and
|
|
367
|
+
`npm run check` holds it there ±3°. 256° is as near to larch's opposite as sRGB
|
|
368
|
+
allows while keeping cobalt's chroma (≈0.18 at the fill). At 248–252° the gamut
|
|
369
|
+
caps chroma near 0.14–0.16, which reads as steel or ocean blue, and links lose
|
|
370
|
+
some of the "you can act on this" that ADR-0001 chose blue for.
|
|
371
|
+
|
|
372
|
+
Each step's lightness was **solved for the contrast its predecessor met**, so
|
|
373
|
+
every ratio is within 0.07 of before: fill 5.18:1 under a white label, link
|
|
374
|
+
6.78:1 on white, dark fill 7.07:1 on card. The sequential ramp keeps each
|
|
375
|
+
step's OKLCH lightness and stays monotonic.
|
|
376
|
+
|
|
377
|
+
### Consequences
|
|
378
|
+
|
|
379
|
+
- The blue is deeper and calmer than Tailwind's electric violet-blue, and it is
|
|
380
|
+
one colour from fill to pressed. It still reads unmistakably as a link.
|
|
381
|
+
- A visible change, so the release that carries it is a minor.
|
|
382
|
+
- `--chart-1` is a separately validated categorical slot (ADR-0002) and is
|
|
383
|
+
unchanged.
|
package/DESIGN.md
CHANGED
|
@@ -106,13 +106,18 @@ The part that makes an interface feel built rather than styled.
|
|
|
106
106
|
- Focus is always visible: 2px `--i-ink` outline, 2px offset. Never remove it.
|
|
107
107
|
- Hover and pressed surfaces on neutral elements come from `--surface-hover` and
|
|
108
108
|
`--surface-active`, not from opacity tricks.
|
|
109
|
+
- A control's own edge — an input, a secondary button — is `--line-control`
|
|
110
|
+
(≥ 3:1). `--line-strong` is a divider, too faint to tell you a control is there.
|
|
111
|
+
- Native checkboxes, radios and ranges inherit `accent-color: var(--i)` from
|
|
112
|
+
`tokens.css`; don't restyle them just to recolour them.
|
|
109
113
|
|
|
110
114
|
## 5. Type
|
|
111
115
|
|
|
112
116
|
Three roles, no more:
|
|
113
117
|
|
|
114
|
-
- **`--sans` (Geist)** — body and headings, both tiers. Tabular figures
|
|
115
|
-
body copy so dates, prices and
|
|
118
|
+
- **`--sans` (Geist)** — body and headings, both tiers. Tabular figures
|
|
119
|
+
(`font-variant-numeric: tabular-nums`) on body copy so dates, prices and
|
|
120
|
+
countdowns align in a column.
|
|
116
121
|
- **`--mono` (IBM Plex Mono)** — data that must read *precise*: confirmation codes,
|
|
117
122
|
timestamps, stat numbers, eyebrow labels.
|
|
118
123
|
- **`--display` (Silkscreen)** — marketing tier only. The `A|B` monogram and hero
|
|
@@ -130,6 +135,10 @@ for cards, `--radius-lg` for hero surfaces.
|
|
|
130
135
|
a shadow (`--shadow-1/2/3`). Dark mode does not use borders — surfaces get
|
|
131
136
|
*lighter* with elevation. This is why `--line` can stay nearly invisible in dark.
|
|
132
137
|
|
|
138
|
+
**Sticky chrome** — a header or toolbar that content scrolls under — is
|
|
139
|
+
`--surface-sticky` with `backdrop-filter: blur(8px)`: 92% of the card in light,
|
|
140
|
+
of the page ground in dark, so it reads as the surface it sits on, not a new layer.
|
|
141
|
+
|
|
133
142
|
## 7. Motion
|
|
134
143
|
|
|
135
144
|
**Content is visible by default. Motion is progressive enhancement layered on top
|
|
@@ -193,22 +202,74 @@ is thirty coloured things, and the one that is down disappears among them.
|
|
|
193
202
|
picture.
|
|
194
203
|
- **Diagrams follow the theme.** On a page they use the tokens like everything
|
|
195
204
|
else. A diagram exported as a standalone file (an SVG, a Mermaid block in a pull
|
|
196
|
-
request) cannot use `var()`; it takes the **light** theme's values
|
|
197
|
-
this file at build time, never
|
|
205
|
+
request) cannot use `var()`; it takes the **light** theme's values — the first
|
|
206
|
+
argument of each `light-dark()` — resolved from this file at build time, never
|
|
207
|
+
typed in.
|
|
198
208
|
|
|
199
209
|
## 10. Accessibility floor
|
|
200
210
|
|
|
201
211
|
Non-negotiable in both themes:
|
|
202
212
|
|
|
203
|
-
- Body text ≥ 4.5:1; large text and UI marks ≥ 3:1.
|
|
213
|
+
- Body text ≥ 4.5:1; large text and UI marks ≥ 3:1. `--slate` is the quietest
|
|
214
|
+
text step and clears 4.5:1 in both themes; `--n-500` in light does not, so it
|
|
215
|
+
is never text.
|
|
204
216
|
- Touch targets ≥ 44px on coarse pointers, with a `.touch-hit` opt-out for controls
|
|
205
217
|
whose shape is meaningful (a 22px circular checkbox forced to 44px becomes an
|
|
206
218
|
oval — it expands its hit area instead).
|
|
207
219
|
- Every state carries an icon and a word.
|
|
208
|
-
- Measured ratios sit next to their values in `tokens.css
|
|
209
|
-
|
|
220
|
+
- Measured ratios sit next to their values in `tokens.css`, and `npm run check`
|
|
221
|
+
(run in CI on every pull request) fails if any floor above is missed, if a colour
|
|
222
|
+
is declared anywhere but once, or if a hex appears outside the token files.
|
|
223
|
+
|
|
224
|
+
## 11. Themes, and integrating with a Tailwind consumer
|
|
225
|
+
|
|
226
|
+
### How the theme is chosen
|
|
227
|
+
|
|
228
|
+
Every colour is one declaration holding both values, `light-dark(<light>, <dark>)`,
|
|
229
|
+
and the browser picks a side from the element's `color-scheme`:
|
|
230
|
+
|
|
231
|
+
- **No attribute** — `color-scheme: light dark`, so the page follows the OS.
|
|
232
|
+
- **`data-theme="light"` / `"dark"`** — the explicit choice, which wins over the OS.
|
|
233
|
+
|
|
234
|
+
The pick happens where a colour is *used*, so **`data-theme` works on any element,
|
|
235
|
+
not just `<html>`.** A dark hero band on a light marketing page is
|
|
236
|
+
`<section data-theme="dark">`. Give that element its own
|
|
237
|
+
`background: var(--bg); color: var(--ink)` — backgrounds don't inherit.
|
|
238
|
+
|
|
239
|
+
Two consequences for code that touches tokens directly:
|
|
240
|
+
|
|
241
|
+
- **Don't read a token's value with `getPropertyValue`.** It returns the
|
|
242
|
+
declaration text, `light-dark(#076bd3, #6ba5f3)`, not a colour. To get the
|
|
243
|
+
colour a chart library needs, resolve it on an element in the right subtree:
|
|
244
|
+
set `el.style.color = 'var(--chart-1)'` and read `getComputedStyle(el).color`.
|
|
245
|
+
Re-read it when the theme changes.
|
|
246
|
+
- **Don't wrap a token in `color-mix()` or `light-dark()`.** Nesting is valid CSS,
|
|
247
|
+
but not every engine has been verified to resolve it. If you need a wash, it's
|
|
248
|
+
a token (`--*-surface`, `--*-line`, `--surface-sticky`); if it isn't, add it here.
|
|
249
|
+
|
|
250
|
+
Needs Chrome/Edge 123, Firefox 120, Safari 17.5 or later (all shipped by May 2024).
|
|
251
|
+
An older browser drops every colour token and renders unstyled rather than wrong.
|
|
252
|
+
|
|
253
|
+
### Build tools that lower `light-dark()`
|
|
254
|
+
|
|
255
|
+
A CSS pipeline that targets browsers older than that floor rewrites
|
|
256
|
+
`light-dark()` into a fallback. Lightning CSS (inside Next.js, Vite, Parcel)
|
|
257
|
+
turns each token into `var(--lightningcss-light, …) var(--lightningcss-dark, …)`
|
|
258
|
+
and flips those two variables wherever `color-scheme` changes. The tokens
|
|
259
|
+
resolve on `:root`, so the side is fixed there. Page-wide theming still works,
|
|
260
|
+
but **`data-theme` on an inner element stops re-theming that section.**
|
|
261
|
+
|
|
262
|
+
The fix is to target the same floor the tokens need, which leaves `light-dark()`
|
|
263
|
+
alone. In the consumer's `package.json`:
|
|
264
|
+
|
|
265
|
+
```json
|
|
266
|
+
"browserslist": ["chrome >= 123", "edge >= 123", "firefox >= 120", "safari >= 17.5"]
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Verified with Lightning CSS: older targets lower the tokens; these targets
|
|
270
|
+
pass `light-dark(#076bd3, #6ba5f3)` through untouched.
|
|
210
271
|
|
|
211
|
-
|
|
272
|
+
### Tailwind
|
|
212
273
|
|
|
213
274
|
Tailwind's dark variant keys off a `.dark` class; these tokens key off
|
|
214
275
|
`[data-theme]`. **Both must be set together**, by the same code, in both places
|
|
@@ -223,11 +284,11 @@ Setting only the class is a silent, half-broken state: Tailwind utilities flip
|
|
|
223
284
|
but the tokens do not, and the page renders one theme's tokens under the other
|
|
224
285
|
theme's utilities. Setting only the attribute leaves every `dark:` utility dead.
|
|
225
286
|
|
|
226
|
-
The reason the attribute is required rather than
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
287
|
+
The reason the attribute is required rather than leaving the OS to decide: a
|
|
288
|
+
site with a manual toggle has already *resolved* the OS preference into an
|
|
289
|
+
explicit choice. Without the attribute, the tokens would still follow the OS, and
|
|
290
|
+
a user on a dark OS who chose light would get dark tokens under light utilities.
|
|
291
|
+
`[data-theme]` is what makes the choice authoritative.
|
|
231
292
|
|
|
232
293
|
Import order matters — Tailwind first, then tokens, then the bridge:
|
|
233
294
|
|
package/README.md
CHANGED
|
@@ -15,11 +15,11 @@ The shared design system for every brignano surface — `brignano.io`, `life`,
|
|
|
15
15
|
|
|
16
16
|
Three layers, so no colour does two jobs:
|
|
17
17
|
|
|
18
|
-
- **`--
|
|
18
|
+
- **`--mark`** — identity. Larch amber. Inks the trail and the monogram. Never a control.
|
|
19
19
|
- **`--i-*`** — affordance. Cobalt. Links, buttons, focus, selection. Semantically inert.
|
|
20
|
-
- **`--
|
|
20
|
+
- **`--success / --attention / --danger`** — state. Conventional hues, always with an icon and a word.
|
|
21
21
|
|
|
22
|
-
Distinctiveness lives in the
|
|
22
|
+
Distinctiveness lives in the mark; conventionality lives in the control layer.
|
|
23
23
|
|
|
24
24
|
## Consuming it
|
|
25
25
|
|
|
@@ -32,6 +32,11 @@ npm install @brignano/design
|
|
|
32
32
|
@import '@brignano/design/tokens.chart.css'; /* only if the surface has charts */
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
Light and dark follow the OS until something sets `data-theme="light"` or
|
|
36
|
+
`"dark"` — on `<html>` for the whole page, or on any element to theme just that
|
|
37
|
+
part (DESIGN.md §11). Each colour is a single `light-dark()` declaration, so this
|
|
38
|
+
needs a browser from mid-2024 or later.
|
|
39
|
+
|
|
35
40
|
The tool tier is the default. A marketing surface opts in with
|
|
36
41
|
`class="tier-marketing"` on `<html>`, which unlocks the display face and opens the
|
|
37
42
|
type scale. Colour is identical across tiers.
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brignano/design",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Shared design system for brignano surfaces — colour, type, space, shape, motion.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/brignano/design.git"
|
|
9
9
|
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"check": "node scripts/check.mjs"
|
|
12
|
+
},
|
|
10
13
|
"publishConfig": {
|
|
11
14
|
"access": "public",
|
|
12
15
|
"provenance": true
|
package/tailwind.css
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
`@theme inline` is load-bearing: it makes the generated utilities emit
|
|
9
9
|
`var(--i)` rather than baking a literal at build time, so `bg-interactive`
|
|
10
|
-
follows the light/dark switch instead of freezing one theme's value.
|
|
10
|
+
follows the light/dark switch instead of freezing one theme's value.
|
|
11
|
+
|
|
12
|
+
The bare `--radius` key is what drives `rounded`. Tailwind v4 has no
|
|
13
|
+
`-DEFAULT` suffix; with one, `rounded` silently stayed at 0.25rem. */
|
|
11
14
|
@theme inline {
|
|
12
15
|
--color-ink: var(--ink);
|
|
13
16
|
--color-ink-soft: var(--ink-soft);
|
|
@@ -15,26 +18,48 @@
|
|
|
15
18
|
--color-bg: var(--bg);
|
|
16
19
|
--color-card: var(--card);
|
|
17
20
|
--color-line: var(--line);
|
|
21
|
+
--color-line-soft: var(--line-soft);
|
|
18
22
|
--color-line-strong: var(--line-strong);
|
|
23
|
+
--color-line-control: var(--line-control);
|
|
19
24
|
--color-surface-hover: var(--surface-hover);
|
|
20
25
|
--color-surface-active: var(--surface-active);
|
|
26
|
+
--color-surface-sticky: var(--surface-sticky);
|
|
27
|
+
--color-on-dark: var(--on-dark);
|
|
21
28
|
|
|
22
29
|
--color-interactive: var(--i);
|
|
23
30
|
--color-interactive-ink: var(--i-ink);
|
|
24
31
|
--color-interactive-hover: var(--i-hover);
|
|
25
32
|
--color-interactive-pressed: var(--i-pressed);
|
|
26
33
|
--color-interactive-surface: var(--i-surface);
|
|
34
|
+
--color-interactive-line: var(--i-line);
|
|
27
35
|
--color-on-interactive: var(--on-i);
|
|
28
36
|
|
|
29
37
|
--color-success: var(--success);
|
|
30
38
|
--color-success-ink: var(--success-ink);
|
|
31
39
|
--color-success-surface: var(--success-surface);
|
|
40
|
+
--color-success-line: var(--success-line);
|
|
32
41
|
--color-attention: var(--attention);
|
|
33
42
|
--color-attention-ink: var(--attention-ink);
|
|
34
43
|
--color-attention-surface: var(--attention-surface);
|
|
44
|
+
--color-attention-line: var(--attention-line);
|
|
35
45
|
--color-danger: var(--danger);
|
|
36
46
|
--color-danger-ink: var(--danger-ink);
|
|
37
47
|
--color-danger-surface: var(--danger-surface);
|
|
48
|
+
--color-danger-line: var(--danger-line);
|
|
49
|
+
--color-info: var(--info);
|
|
50
|
+
--color-info-ink: var(--info-ink);
|
|
51
|
+
--color-info-surface: var(--info-surface);
|
|
52
|
+
--color-info-line: var(--info-line);
|
|
53
|
+
|
|
54
|
+
--color-diff-added: var(--diff-added);
|
|
55
|
+
--color-diff-added-ink: var(--diff-added-ink);
|
|
56
|
+
--color-diff-added-surface: var(--diff-added-surface);
|
|
57
|
+
--color-diff-changed: var(--diff-changed);
|
|
58
|
+
--color-diff-changed-ink: var(--diff-changed-ink);
|
|
59
|
+
--color-diff-changed-surface: var(--diff-changed-surface);
|
|
60
|
+
--color-diff-removed: var(--diff-removed);
|
|
61
|
+
--color-diff-removed-ink: var(--diff-removed-ink);
|
|
62
|
+
--color-diff-removed-surface: var(--diff-removed-surface);
|
|
38
63
|
|
|
39
64
|
--color-mark: var(--mark);
|
|
40
65
|
--color-mark-ink: var(--mark-ink);
|
|
@@ -44,7 +69,7 @@
|
|
|
44
69
|
--font-display: var(--display);
|
|
45
70
|
|
|
46
71
|
--radius-sm: var(--radius-sm);
|
|
47
|
-
--radius
|
|
72
|
+
--radius: var(--radius);
|
|
48
73
|
--radius-lg: var(--radius-lg);
|
|
49
74
|
|
|
50
75
|
--shadow-1: var(--shadow-1);
|
package/tokens.chart.css
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
WHY THIS IS A SEPARATE FILE FROM THE BRAND ACCENT
|
|
5
5
|
-------------------------------------------------
|
|
6
6
|
A chart's colours do a different job from a brand's colours, so they are
|
|
7
|
-
not derived from the
|
|
7
|
+
not derived from the mark and they are not the state hues.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
mark (--mark) identity. One hue. "This is Anthony."
|
|
10
|
+
state (--success/…) state. RESERVED — never a series colour.
|
|
11
11
|
categorical (this file) IDENTITY OF A SERIES. Needs 8 hues that a
|
|
12
12
|
human can tell apart at a glance, including
|
|
13
13
|
the ~8% of men with a colour-vision
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
THESE VALUES ARE VALIDATED, NOT CHOSEN BY EYE
|
|
21
21
|
----------------------------------------------
|
|
22
|
+
Values are light-dark(<light>, <dark>), one declaration each.
|
|
22
23
|
Both modes pass every hard gate of the six-check colour formula:
|
|
23
24
|
|
|
24
25
|
lightness band PASS (all 8 inside the mode's band)
|
|
@@ -53,14 +54,14 @@
|
|
|
53
54
|
:root {
|
|
54
55
|
--chart-surface: var(--card);
|
|
55
56
|
|
|
56
|
-
--chart-1: #2a78d6; /* blue */
|
|
57
|
-
--chart-2: #eb6834; /* orange */
|
|
58
|
-
--chart-3: #1baf7a; /* aqua — relief required (2.74:1) */
|
|
59
|
-
--chart-4: #eda100; /* yellow — relief required (2.11:1) */
|
|
60
|
-
--chart-5: #e87ba4; /* magenta — relief required (2.62:1) */
|
|
61
|
-
--chart-6: #008300; /* green
|
|
62
|
-
--chart-7: #4a3aa7; /* violet */
|
|
63
|
-
--chart-8: #e34948; /* red */
|
|
57
|
+
--chart-1: light-dark(#2a78d6, #3987e5); /* blue */
|
|
58
|
+
--chart-2: light-dark(#eb6834, #d95926); /* orange */
|
|
59
|
+
--chart-3: light-dark(#1baf7a, #199e70); /* aqua — relief required (2.74:1) */
|
|
60
|
+
--chart-4: light-dark(#eda100, #c98500); /* yellow — relief required (2.11:1) */
|
|
61
|
+
--chart-5: light-dark(#e87ba4, #d55181); /* magenta — relief required (2.62:1) */
|
|
62
|
+
--chart-6: #008300; /* green — same in both */
|
|
63
|
+
--chart-7: light-dark(#4a3aa7, #9085e9); /* violet */
|
|
64
|
+
--chart-8: light-dark(#e34948, #e66767); /* red */
|
|
64
65
|
|
|
65
66
|
/* Everything past slot 8 aggregates here. Deliberately neutral: "Other"
|
|
66
67
|
is not an entity, so it does not earn a hue. */
|
|
@@ -79,9 +80,9 @@
|
|
|
79
80
|
Rules that differ from the categorical palette above:
|
|
80
81
|
- ONE hue. Never a rainbow — a rainbow implies categories where there
|
|
81
82
|
are only degrees.
|
|
82
|
-
- Lightness must be MONOTONIC. Verified: light L 0.
|
|
83
|
-
descending, dark L 0.
|
|
84
|
-
separated by dL >= 0.
|
|
83
|
+
- Lightness must be MONOTONIC. Verified in OKLCH: light L 0.888 ->
|
|
84
|
+
0.487 descending, dark L 0.380 -> 0.808 ascending, every adjacent
|
|
85
|
+
step separated by dL >= 0.107.
|
|
85
86
|
- `--seq-0` is NEUTRAL on purpose. Zero contributions is not a small
|
|
86
87
|
amount of the hue, it is the absence of a value — colouring it a pale
|
|
87
88
|
blue would claim data that isn't there.
|
|
@@ -91,44 +92,8 @@
|
|
|
91
92
|
it its own hue would add a meaning the system doesn't need.
|
|
92
93
|
───────────────────────────────────────────────────────────────────────── */
|
|
93
94
|
--seq-0: var(--line);
|
|
94
|
-
--seq-1: #
|
|
95
|
-
--seq-2: #
|
|
96
|
-
--seq-3: #
|
|
97
|
-
--seq-4: #
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
@media (prefers-color-scheme: dark) {
|
|
101
|
-
:root:not([data-theme='light']) {
|
|
102
|
-
--chart-1: #3987e5;
|
|
103
|
-
--chart-2: #d95926;
|
|
104
|
-
--chart-3: #199e70;
|
|
105
|
-
--chart-4: #c98500;
|
|
106
|
-
--chart-5: #d55181;
|
|
107
|
-
--chart-6: #008300;
|
|
108
|
-
--chart-7: #9085e9;
|
|
109
|
-
--chart-8: #e66767;
|
|
110
|
-
|
|
111
|
-
--seq-0: var(--line);
|
|
112
|
-
--seq-1: #1e3a8a;
|
|
113
|
-
--seq-2: #1d4ed8;
|
|
114
|
-
--seq-3: #3b82f6;
|
|
115
|
-
--seq-4: #93c5fd;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
:root[data-theme='dark'] {
|
|
120
|
-
--chart-1: #3987e5;
|
|
121
|
-
--chart-2: #d95926;
|
|
122
|
-
--chart-3: #199e70;
|
|
123
|
-
--chart-4: #c98500;
|
|
124
|
-
--chart-5: #d55181;
|
|
125
|
-
--chart-6: #008300;
|
|
126
|
-
--chart-7: #9085e9;
|
|
127
|
-
--chart-8: #e66767;
|
|
128
|
-
|
|
129
|
-
--seq-0: var(--line);
|
|
130
|
-
--seq-1: #1e3a8a;
|
|
131
|
-
--seq-2: #1d4ed8;
|
|
132
|
-
--seq-3: #3b82f6;
|
|
133
|
-
--seq-4: #93c5fd;
|
|
95
|
+
--seq-1: light-dark(#c6dcfb, #014084);
|
|
96
|
+
--seq-2: light-dark(#87b5f3, #015cbb);
|
|
97
|
+
--seq-3: #2885f5;
|
|
98
|
+
--seq-4: light-dark(#015cbb, #98c3fe);
|
|
134
99
|
}
|
package/tokens.css
CHANGED
|
@@ -36,49 +36,107 @@
|
|
|
36
36
|
1. NEVER hardcode a hex outside this file. A literal survives a theme
|
|
37
37
|
change silently — that is how `life`'s status pills sat light grey in
|
|
38
38
|
dark mode for months.
|
|
39
|
-
2. Every colour is
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
2. Every colour is ONE declaration holding both themes:
|
|
40
|
+
`light-dark(<light>, <dark>)`. There is no dark list to keep in step.
|
|
41
|
+
The arguments are always concrete colours, never a var() to another
|
|
42
|
+
theme-dependent token — nested light-dark() is legal CSS, but not
|
|
43
|
+
every engine has been verified to resolve it. Where that forces a
|
|
44
|
+
value to repeat (a wash repeats its hue), `npm run check` holds the
|
|
45
|
+
copies to their source.
|
|
42
46
|
3. Ratios in the comments are MEASURED. Change a value, re-measure it.
|
|
43
47
|
|
|
44
48
|
See DESIGN.md for the rules that govern use, DECISIONS.md for why.
|
|
45
49
|
───────────────────────────────────────────────────────────────────────── */
|
|
46
50
|
|
|
51
|
+
/* ── Themes ─────────────────────────────────────────────────────────────
|
|
52
|
+
One declaration per colour, light value first: light-dark(<light>, <dark>).
|
|
53
|
+
The browser picks the side from the element's color-scheme:
|
|
54
|
+
|
|
55
|
+
default follows the OS (light dark below)
|
|
56
|
+
[data-theme] the in-app toggle, which must WIN over the OS
|
|
57
|
+
— `light` forces light under a dark OS
|
|
58
|
+
|
|
59
|
+
Because the pick happens where a colour is USED, not where it's declared,
|
|
60
|
+
[data-theme] (or color-scheme) on ANY element themes that subtree — a dark
|
|
61
|
+
hero band on a light page is one attribute. Give that element its own
|
|
62
|
+
`background: var(--bg)`; it doesn't inherit the page's.
|
|
63
|
+
|
|
64
|
+
Dark is selected, not inverted:
|
|
65
|
+
1. Not pure black. #0d0d0f — pure black behind bright text causes
|
|
66
|
+
halation and is fatiguing at night, which is when a personal tool
|
|
67
|
+
actually gets opened.
|
|
68
|
+
2. Surfaces LIGHTEN with elevation instead of gaining borders.
|
|
69
|
+
3. Accents lift in luminance rather than shifting hue, and washes carry
|
|
70
|
+
a higher mix — 12% over #0d0d0f is invisible where it reads on white.
|
|
71
|
+
|
|
72
|
+
Ratios are given light / dark. */
|
|
47
73
|
:root {
|
|
74
|
+
color-scheme: light dark;
|
|
75
|
+
}
|
|
76
|
+
[data-theme='light'] {
|
|
48
77
|
color-scheme: light;
|
|
78
|
+
}
|
|
79
|
+
[data-theme='dark'] {
|
|
80
|
+
color-scheme: dark;
|
|
81
|
+
}
|
|
49
82
|
|
|
83
|
+
:root {
|
|
50
84
|
/* ── Neutral ramp ─────────────────────────────────────────────────────
|
|
51
85
|
Twelve steps, up from five. The gap that mattered most: there was no
|
|
52
86
|
hover or pressed background token, so every interactive surface in
|
|
53
87
|
both repos was improvising.
|
|
54
88
|
|
|
55
|
-
Deliberately COOL
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
--n-
|
|
68
|
-
--n-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
--
|
|
72
|
-
--
|
|
89
|
+
Deliberately COOL — a cool grey lets the warm mark and the amber
|
|
90
|
+
attention state read as warm, instead of muddying into them.
|
|
91
|
+
|
|
92
|
+
ONE TEMPERATURE, top to bottom, both themes: every tinted step sits at
|
|
93
|
+
OKLCH hue 286° (±3°, `npm run check` holds it). Chroma is a whisper at
|
|
94
|
+
the ends (~0.0015 at n-25) and peaks in the mid-tones (~0.015 at
|
|
95
|
+
n-700), where a grey reads most neutral-flat without it. Only pure
|
|
96
|
+
white (light n-0) is untinted. An earlier ramp mixed warm hairlines
|
|
97
|
+
(106°) with cool text, so a border and the label beside it disagreed.
|
|
98
|
+
|
|
99
|
+
Changing a step: keep its OKLCH lightness, keep the hue, re-run the
|
|
100
|
+
check. */
|
|
101
|
+
--n-0: light-dark(#ffffff, #0d0d0f);
|
|
102
|
+
--n-25: light-dark(#fafafb, #131316);
|
|
103
|
+
--n-50: light-dark(#f3f3f4, #17171a);
|
|
104
|
+
--n-100: light-dark(#ececee, #202024); /* hairline */
|
|
105
|
+
--n-200: light-dark(#e1e1e4, #2a2a2f); /* hover surface */
|
|
106
|
+
--n-300: light-dark(#cbcbce, #3a3a41); /* strong border, pressed surface */
|
|
107
|
+
--n-400: light-dark(#a9a9ae, #55555e);
|
|
108
|
+
--n-500: light-dark(#8a8a8f, #8b8b93); /* 3.44:1 — UI marks only, never text / 5.29:1 */
|
|
109
|
+
--n-600: light-dark(#6b6b73, #9c9ca4); /* muted label 5.28:1 */
|
|
110
|
+
--n-700: light-dark(#52525b, #a8a8b0); /* secondary 7.73:1 / 7.58:1 */
|
|
111
|
+
--n-800: light-dark(#33333a, #d0d0d5);
|
|
112
|
+
--n-900: light-dark(#111114, #f2f2f4); /* primary 18.85:1 / 16.00:1 */
|
|
113
|
+
|
|
114
|
+
/* Semantic aliases. Components use THESE, not the raw steps.
|
|
115
|
+
Where a role sits on a different step in each theme, the step's two
|
|
116
|
+
hexes are written out (rule 2) and named in the comment; the check
|
|
117
|
+
holds them to the ramp. */
|
|
118
|
+
--bg: light-dark(#fafafb, #0d0d0f); /* n-25 / n-0 */
|
|
119
|
+
--card: light-dark(#ffffff, #17171a); /* n-0 / n-50 */
|
|
73
120
|
--surface-hover: var(--n-100);
|
|
74
121
|
--surface-active: var(--n-200);
|
|
122
|
+
/* Sticky chrome — a header or toolbar that content scrolls under, usually
|
|
123
|
+
with `backdrop-filter: blur()`. 92% of the surface it sits on: the card
|
|
124
|
+
in light, the page ground in dark, where cards are lighter than the
|
|
125
|
+
page. A token so consumers never color-mix() one (DESIGN.md §11). */
|
|
126
|
+
--surface-sticky: light-dark(color-mix(in srgb, #ffffff 92%, transparent), color-mix(in srgb, #0d0d0f 92%, transparent)); /* card / bg */
|
|
75
127
|
--ink: var(--n-900);
|
|
76
128
|
--ink-soft: var(--n-700);
|
|
77
|
-
--slate:
|
|
129
|
+
--slate: light-dark(#6b6b73, #8b8b93); /* n-600 / n-500 — text-safe: eyebrows, axis labels, captions */
|
|
78
130
|
--line: var(--n-100);
|
|
79
131
|
--line-soft: var(--n-50);
|
|
80
|
-
--line-strong: var(--n-300);
|
|
81
|
-
|
|
132
|
+
--line-strong: var(--n-300); /* 1.62:1 — decoration, not a boundary */
|
|
133
|
+
/* The edge that tells you a control is there: inputs, secondary buttons.
|
|
134
|
+
WCAG 1.4.11 asks 3:1 of a boundary you need to find the control, which
|
|
135
|
+
--line-strong is nowhere near. Light is n-500 (3.44:1 on card, 3.29:1
|
|
136
|
+
on bg); dark sits between n-400 and n-500, which the ramp has no step
|
|
137
|
+
for (3.39:1 on card, 3.68:1 on bg). */
|
|
138
|
+
--line-control: light-dark(#8a8a8f, #6b6b73);
|
|
139
|
+
--on-dark: light-dark(#fafafb, #0d0d0f); /* n-25 / n-0 */
|
|
82
140
|
|
|
83
141
|
/* ── Interactive — cobalt ─────────────────────────────────────────────
|
|
84
142
|
Everything you can act on: links, buttons, focus, selection. Chosen to
|
|
@@ -86,21 +144,28 @@
|
|
|
86
144
|
green and red do — and complementary to the mark hue, so identity and
|
|
87
145
|
affordance never compete for the same attention.
|
|
88
146
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
147
|
+
Every step sits at OKLCH hue 256° (`npm run check` holds it), as near
|
|
148
|
+
to larch's opposite (~248°) as sRGB allows while keeping cobalt's
|
|
149
|
+
chroma. It is deliberately NOT Tailwind's blue-600 (263°, a touch
|
|
150
|
+
violet and electric): same affordance, our own colour (ADR-0008).
|
|
151
|
+
|
|
152
|
+
--i fill 5.18:1 / 7.07:1 (label 5.18 / 7.46)
|
|
153
|
+
--i-ink link/icon/focus 6.78:1 / 7.07:1
|
|
154
|
+
--i-hover hover fill 8.65:1 / 10.50:1 (label)
|
|
155
|
+
--i-pressed pressed fill 10.44:1 / 13.32:1 (label)
|
|
156
|
+
|
|
157
|
+
In dark, fill and ink converge, and hover/pressed get LIGHTER.
|
|
93
158
|
|
|
94
159
|
DISABLED IS NEUTRAL, NEVER HUED — a greyed-out control must stop
|
|
95
160
|
signalling "click me". */
|
|
96
|
-
--i: #
|
|
97
|
-
--i-ink: #
|
|
98
|
-
--i-hover: #
|
|
99
|
-
--i-pressed: #
|
|
100
|
-
--i-surface: color-mix(in srgb,
|
|
101
|
-
--i-line: color-mix(in srgb,
|
|
102
|
-
--i-selection: color-mix(in srgb,
|
|
103
|
-
--on-i: #ffffff;
|
|
161
|
+
--i: light-dark(#076bd3, #6ba5f3);
|
|
162
|
+
--i-ink: light-dark(#0159b5, #6ba5f3);
|
|
163
|
+
--i-hover: light-dark(#014a97, #98c4ff);
|
|
164
|
+
--i-pressed: light-dark(#003e82, #c0dbff);
|
|
165
|
+
--i-surface: light-dark(color-mix(in srgb, #076bd3 12%, transparent), color-mix(in srgb, #6ba5f3 20%, transparent));
|
|
166
|
+
--i-line: light-dark(color-mix(in srgb, #076bd3 32%, transparent), color-mix(in srgb, #6ba5f3 44%, transparent));
|
|
167
|
+
--i-selection: light-dark(color-mix(in srgb, #076bd3 26%, transparent), color-mix(in srgb, #6ba5f3 30%, transparent));
|
|
168
|
+
--on-i: light-dark(#ffffff, #111111);
|
|
104
169
|
--i-disabled-bg: var(--n-200);
|
|
105
170
|
--i-disabled-ink: var(--n-400);
|
|
106
171
|
|
|
@@ -109,31 +174,32 @@
|
|
|
109
174
|
tiers. Always shipped with an ICON AND A WORD — colour is never the
|
|
110
175
|
only signal.
|
|
111
176
|
|
|
112
|
-
--success settled · confirmed · done · succeeded 6.29:1
|
|
113
|
-
--attention now · next · warning · needs you 6.08:1
|
|
114
|
-
--danger overdue · failed · invalid 6.10:1
|
|
177
|
+
--success settled · confirmed · done · succeeded 6.29:1 / 8.15:1
|
|
178
|
+
--attention now · next · warning · needs you 6.08:1 / 8.17:1 (ink)
|
|
179
|
+
--danger overdue · failed · invalid 6.10:1 / 6.06:1
|
|
115
180
|
|
|
116
181
|
`--info` is the interactive hue on purpose: an informational message
|
|
117
182
|
is the same "neutral, no action implied" register as a link.
|
|
118
183
|
|
|
119
184
|
`-ink` is the text-safe step; the bare token is for marks and fills.
|
|
120
|
-
`-surface` and `-line` are the wash and its edge
|
|
121
|
-
|
|
122
|
-
--success
|
|
123
|
-
--
|
|
124
|
-
--attention
|
|
125
|
-
--
|
|
126
|
-
--danger
|
|
185
|
+
`-surface` and `-line` are the wash and its edge: 12% / 32% of the hue
|
|
186
|
+
in light, 20% / 44% in dark. */
|
|
187
|
+
--success: light-dark(#2f6b4f, #6fbf95);
|
|
188
|
+
--success-ink: light-dark(#245740, #8fd4ae);
|
|
189
|
+
--attention: light-dark(#b4741f, #e0a44f); /* 3.85:1 in light — mark/fill only, NOT text */
|
|
190
|
+
--attention-ink: light-dark(#8a5714, #e0a44f); /* 6.08:1 / 8.17:1 — mark and label converge in dark */
|
|
191
|
+
--danger: light-dark(#a4442c, #e07a5f);
|
|
192
|
+
--danger-ink: light-dark(#8a3722, #ec9880);
|
|
127
193
|
--info: var(--i);
|
|
128
194
|
--info-ink: var(--i-ink);
|
|
129
195
|
|
|
130
|
-
--success-surface: color-mix(in srgb,
|
|
131
|
-
--attention-surface: color-mix(in srgb,
|
|
132
|
-
--danger-surface: color-mix(in srgb,
|
|
196
|
+
--success-surface: light-dark(color-mix(in srgb, #2f6b4f 12%, transparent), color-mix(in srgb, #6fbf95 20%, transparent));
|
|
197
|
+
--attention-surface: light-dark(color-mix(in srgb, #b4741f 12%, transparent), color-mix(in srgb, #e0a44f 20%, transparent));
|
|
198
|
+
--danger-surface: light-dark(color-mix(in srgb, #a4442c 12%, transparent), color-mix(in srgb, #e07a5f 20%, transparent));
|
|
133
199
|
--info-surface: var(--i-surface);
|
|
134
|
-
--success-line: color-mix(in srgb,
|
|
135
|
-
--attention-line: color-mix(in srgb,
|
|
136
|
-
--danger-line: color-mix(in srgb,
|
|
200
|
+
--success-line: light-dark(color-mix(in srgb, #2f6b4f 32%, transparent), color-mix(in srgb, #6fbf95 44%, transparent));
|
|
201
|
+
--attention-line: light-dark(color-mix(in srgb, #b4741f 32%, transparent), color-mix(in srgb, #e0a44f 44%, transparent));
|
|
202
|
+
--danger-line: light-dark(color-mix(in srgb, #a4442c 32%, transparent), color-mix(in srgb, #e07a5f 44%, transparent));
|
|
137
203
|
--info-line: var(--i-line);
|
|
138
204
|
|
|
139
205
|
/* ── Diff ─────────────────────────────────────────────────────────────
|
|
@@ -144,8 +210,7 @@
|
|
|
144
210
|
--diff-changed modified by this change interactive — informational
|
|
145
211
|
--diff-removed removed by this change danger — can lose data
|
|
146
212
|
|
|
147
|
-
Always with a symbol and a word (+ added, ~ changed, − removed).
|
|
148
|
-
in this block only: these resolve against whichever theme block wins. */
|
|
213
|
+
Always with a symbol and a word (+ added, ~ changed, − removed). */
|
|
149
214
|
--diff-added: var(--success);
|
|
150
215
|
--diff-added-ink: var(--success-ink);
|
|
151
216
|
--diff-added-surface: var(--success-surface);
|
|
@@ -168,21 +233,21 @@
|
|
|
168
233
|
the attention state are the same warmth, and because --mark never
|
|
169
234
|
appears as a UI surface the two can never be mistaken for each other
|
|
170
235
|
in the same context. */
|
|
171
|
-
--mark: #b4741f;
|
|
172
|
-
--mark-ink: #8a5714; /* when the mark carries a label, 6.08:1 */
|
|
236
|
+
--mark: light-dark(#b4741f, #e0a44f);
|
|
237
|
+
--mark-ink: light-dark(#8a5714, #e0a44f); /* when the mark carries a label, 6.08:1 / 8.17:1 */
|
|
173
238
|
|
|
174
239
|
/* ── Legacy aliases ───────────────────────────────────────────────────
|
|
175
240
|
Kept so existing `life` components keep compiling. Do not use in new
|
|
176
241
|
work — reach for the semantic tokens above.
|
|
177
242
|
|
|
178
|
-
--larch-deep is
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
243
|
+
--larch-deep is written out rather than aliased, because "deep"
|
|
244
|
+
inverts: darker in light, BRIGHTER in dark. An alias to either role
|
|
245
|
+
silently changes one theme — it moved .pill-active from #f0bd74 to
|
|
246
|
+
#e0a44f before this was caught. */
|
|
182
247
|
--forest: var(--success);
|
|
183
248
|
--forest-deep: var(--success-ink);
|
|
184
249
|
--larch: var(--attention);
|
|
185
|
-
--larch-deep: #8a5714;
|
|
250
|
+
--larch-deep: light-dark(#8a5714, #f0bd74);
|
|
186
251
|
--rust: var(--danger);
|
|
187
252
|
--rust-deep: var(--danger-ink);
|
|
188
253
|
--gold: var(--slate);
|
|
@@ -204,153 +269,36 @@
|
|
|
204
269
|
--s6: 32px; --s7: 48px; --s8: 64px; --s9: 96px;
|
|
205
270
|
|
|
206
271
|
/* ── Shape & elevation ────────────────────────────────────────────────
|
|
207
|
-
In LIGHT these are shadows; in DARK the surface lightens instead
|
|
272
|
+
In LIGHT these are shadows; in DARK the surface lightens instead, and
|
|
273
|
+
the shadow only has to separate it from a near-black ground.
|
|
274
|
+
|
|
275
|
+
light-dark() switches colours, not geometry, and the two themes' blur
|
|
276
|
+
differs. So shadows 2 and 3 carry one layer per theme, with the other
|
|
277
|
+
theme's layer transparent — it draws nothing. */
|
|
208
278
|
--radius-sm: 6px;
|
|
209
279
|
--radius: 12px;
|
|
210
280
|
--radius-lg: 18px;
|
|
211
|
-
--shadow-1: 0 1px 2px rgba(17, 17, 17, 0.04);
|
|
212
|
-
--shadow-2:
|
|
213
|
-
|
|
214
|
-
|
|
281
|
+
--shadow-1: 0 1px 2px light-dark(rgba(17, 17, 17, 0.04), rgba(0, 0, 0, 0.45));
|
|
282
|
+
--shadow-2:
|
|
283
|
+
0 2px 8px light-dark(rgba(17, 17, 17, 0.07), transparent),
|
|
284
|
+
0 2px 10px light-dark(transparent, rgba(0, 0, 0, 0.55));
|
|
285
|
+
--shadow-3:
|
|
286
|
+
0 8px 24px light-dark(rgba(17, 17, 17, 0.10), transparent),
|
|
287
|
+
0 10px 28px light-dark(transparent, rgba(0, 0, 0, 0.65));
|
|
288
|
+
--scrim: light-dark(rgba(17, 17, 17, 0.55), rgba(0, 0, 0, 0.68));
|
|
215
289
|
--shadow: var(--shadow-1); /* legacy alias */
|
|
216
290
|
|
|
217
291
|
/* ── Motion ───────────────────────────────────────────────────────────
|
|
218
292
|
Content is ALWAYS visible by default; motion is enhancement layered on
|
|
219
293
|
top, never the thing that makes content appear. brignano.io shipped a
|
|
220
294
|
homepage whose contact CTA was invisible on 100% of loads because an
|
|
221
|
-
animation library owned the visible state. See DESIGN.md §
|
|
295
|
+
animation library owned the visible state. See DESIGN.md §7. */
|
|
222
296
|
--ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
223
297
|
--dur-fast: 150ms;
|
|
224
298
|
--dur: 250ms;
|
|
225
299
|
--dur-slow: 600ms;
|
|
226
300
|
}
|
|
227
301
|
|
|
228
|
-
/* ── Dark ───────────────────────────────────────────────────────────────
|
|
229
|
-
Selected, not inverted:
|
|
230
|
-
1. Not pure black. #0d0d0f — pure black behind bright text causes
|
|
231
|
-
halation and is fatiguing at night, which is when a personal tool
|
|
232
|
-
actually gets opened.
|
|
233
|
-
2. Surfaces LIGHTEN with elevation instead of gaining borders.
|
|
234
|
-
3. Accents lift in luminance rather than shifting hue, and washes carry
|
|
235
|
-
a higher mix — 12% over #0d0d0f is invisible where it reads on white.
|
|
236
|
-
|
|
237
|
-
Declared twice: the media query is the default and no-JS fallback;
|
|
238
|
-
[data-theme] is the in-app toggle and must WIN over the system.
|
|
239
|
-
`:not([data-theme='light'])` lets the toggle force light under a dark OS. */
|
|
240
|
-
@media (prefers-color-scheme: dark) {
|
|
241
|
-
:root:not([data-theme='light']) {
|
|
242
|
-
color-scheme: dark;
|
|
243
|
-
|
|
244
|
-
--n-0: #0d0d0f;
|
|
245
|
-
--n-25: #131316;
|
|
246
|
-
--n-50: #17171a;
|
|
247
|
-
--n-100: #202024;
|
|
248
|
-
--n-200: #2a2a2f;
|
|
249
|
-
--n-300: #3a3a41;
|
|
250
|
-
--n-400: #55555e;
|
|
251
|
-
--n-500: #8b8b93; /* 5.29:1 on card */
|
|
252
|
-
--n-600: #9c9ca4;
|
|
253
|
-
--n-700: #a8a8b0; /* 7.58:1 */
|
|
254
|
-
--n-800: #d0d0d2;
|
|
255
|
-
--n-900: #f2f2f0; /* 15.96:1 */
|
|
256
|
-
|
|
257
|
-
--bg: var(--n-0);
|
|
258
|
-
--card: var(--n-50);
|
|
259
|
-
--surface-hover: var(--n-100);
|
|
260
|
-
--surface-active: var(--n-200);
|
|
261
|
-
--on-dark: var(--n-0);
|
|
262
|
-
|
|
263
|
-
--i: #60a5fa; /* 7.04:1 */
|
|
264
|
-
--i-ink: #60a5fa;
|
|
265
|
-
--i-hover: #93c5fd;
|
|
266
|
-
--i-pressed: #bfdbfe;
|
|
267
|
-
--i-surface: color-mix(in srgb, var(--i) 20%, transparent);
|
|
268
|
-
--i-line: color-mix(in srgb, var(--i) 44%, transparent);
|
|
269
|
-
--i-selection: color-mix(in srgb, var(--i) 30%, transparent);
|
|
270
|
-
--on-i: #111111;
|
|
271
|
-
|
|
272
|
-
--success: #6fbf95;
|
|
273
|
-
--success-ink: #8fd4ae;
|
|
274
|
-
--attention: #e0a44f;
|
|
275
|
-
--attention-ink: #e0a44f; /* 8.17:1 — mark and label converge in dark */
|
|
276
|
-
--danger: #e07a5f;
|
|
277
|
-
--danger-ink: #ec9880;
|
|
278
|
-
|
|
279
|
-
--success-surface: color-mix(in srgb, var(--success) 20%, transparent);
|
|
280
|
-
--attention-surface: color-mix(in srgb, var(--attention) 20%, transparent);
|
|
281
|
-
--danger-surface: color-mix(in srgb, var(--danger) 20%, transparent);
|
|
282
|
-
--success-line: color-mix(in srgb, var(--success) 44%, transparent);
|
|
283
|
-
--attention-line: color-mix(in srgb, var(--attention) 44%, transparent);
|
|
284
|
-
--danger-line: color-mix(in srgb, var(--danger) 44%, transparent);
|
|
285
|
-
|
|
286
|
-
--mark: #e0a44f;
|
|
287
|
-
--mark-ink: #e0a44f;
|
|
288
|
-
|
|
289
|
-
--larch-deep: #f0bd74; /* brighter step in dark — see the light block */
|
|
290
|
-
|
|
291
|
-
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45);
|
|
292
|
-
--shadow-2: 0 2px 10px rgba(0, 0, 0, 0.55);
|
|
293
|
-
--shadow-3: 0 10px 28px rgba(0, 0, 0, 0.65);
|
|
294
|
-
--scrim: rgba(0, 0, 0, 0.68);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
:root[data-theme='dark'] {
|
|
299
|
-
color-scheme: dark;
|
|
300
|
-
|
|
301
|
-
--n-0: #0d0d0f;
|
|
302
|
-
--n-25: #131316;
|
|
303
|
-
--n-50: #17171a;
|
|
304
|
-
--n-100: #202024;
|
|
305
|
-
--n-200: #2a2a2f;
|
|
306
|
-
--n-300: #3a3a41;
|
|
307
|
-
--n-400: #55555e;
|
|
308
|
-
--n-500: #8b8b93;
|
|
309
|
-
--n-600: #9c9ca4;
|
|
310
|
-
--n-700: #a8a8b0;
|
|
311
|
-
--n-800: #d0d0d2;
|
|
312
|
-
--n-900: #f2f2f0;
|
|
313
|
-
|
|
314
|
-
--bg: var(--n-0);
|
|
315
|
-
--card: var(--n-50);
|
|
316
|
-
--surface-hover: var(--n-100);
|
|
317
|
-
--surface-active: var(--n-200);
|
|
318
|
-
--on-dark: var(--n-0);
|
|
319
|
-
|
|
320
|
-
--i: #60a5fa;
|
|
321
|
-
--i-ink: #60a5fa;
|
|
322
|
-
--i-hover: #93c5fd;
|
|
323
|
-
--i-pressed: #bfdbfe;
|
|
324
|
-
--i-surface: color-mix(in srgb, var(--i) 20%, transparent);
|
|
325
|
-
--i-line: color-mix(in srgb, var(--i) 44%, transparent);
|
|
326
|
-
--i-selection: color-mix(in srgb, var(--i) 30%, transparent);
|
|
327
|
-
--on-i: #111111;
|
|
328
|
-
|
|
329
|
-
--success: #6fbf95;
|
|
330
|
-
--success-ink: #8fd4ae;
|
|
331
|
-
--attention: #e0a44f;
|
|
332
|
-
--attention-ink: #e0a44f;
|
|
333
|
-
--danger: #e07a5f;
|
|
334
|
-
--danger-ink: #ec9880;
|
|
335
|
-
|
|
336
|
-
--success-surface: color-mix(in srgb, var(--success) 20%, transparent);
|
|
337
|
-
--attention-surface: color-mix(in srgb, var(--attention) 20%, transparent);
|
|
338
|
-
--danger-surface: color-mix(in srgb, var(--danger) 20%, transparent);
|
|
339
|
-
--success-line: color-mix(in srgb, var(--success) 44%, transparent);
|
|
340
|
-
--attention-line: color-mix(in srgb, var(--attention) 44%, transparent);
|
|
341
|
-
--danger-line: color-mix(in srgb, var(--danger) 44%, transparent);
|
|
342
|
-
|
|
343
|
-
--mark: #e0a44f;
|
|
344
|
-
--mark-ink: #e0a44f;
|
|
345
|
-
|
|
346
|
-
--larch-deep: #f0bd74;
|
|
347
|
-
|
|
348
|
-
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45);
|
|
349
|
-
--shadow-2: 0 2px 10px rgba(0, 0, 0, 0.55);
|
|
350
|
-
--shadow-3: 0 10px 28px rgba(0, 0, 0, 0.65);
|
|
351
|
-
--scrim: rgba(0, 0, 0, 0.68);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
302
|
:root {
|
|
355
303
|
/* Tool-tier scale — compact, because scanning beats reading.
|
|
356
304
|
Declared BEFORE the marketing tier on purpose: `:root` and
|
|
@@ -381,7 +329,7 @@
|
|
|
381
329
|
--text-xl: 1.5rem;
|
|
382
330
|
--text-2xl: 2rem;
|
|
383
331
|
--text-3xl: 2.75rem;
|
|
384
|
-
--text-4xl: clamp(2.75rem,
|
|
332
|
+
--text-4xl: clamp(2.75rem, 1rem + 4.5vw, 4rem); /* rem term keeps it zoomable (WCAG 1.4.4) */
|
|
385
333
|
--section-gap: var(--s9);
|
|
386
334
|
--measure: 68ch;
|
|
387
335
|
}
|
|
@@ -390,6 +338,12 @@
|
|
|
390
338
|
background: var(--i-selection);
|
|
391
339
|
}
|
|
392
340
|
|
|
341
|
+
/* Native checkboxes, radios, range and progress take the control hue
|
|
342
|
+
instead of the browser's own blue. */
|
|
343
|
+
:root {
|
|
344
|
+
accent-color: var(--i);
|
|
345
|
+
}
|
|
346
|
+
|
|
393
347
|
/* Ships here so no surface can forget it. */
|
|
394
348
|
@media (prefers-reduced-motion: reduce) {
|
|
395
349
|
*,
|