@brignano/design 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DECISIONS.md +161 -0
- package/DESIGN.md +88 -13
- package/README.md +35 -3
- package/package.json +5 -2
- package/tailwind.css +26 -2
- package/tokens.chart.css +19 -54
- package/tokens.css +176 -204
package/DECISIONS.md
CHANGED
|
@@ -215,3 +215,164 @@ with it; `--mark` does not.
|
|
|
215
215
|
separates them; a form error is not "rust means overdue", but they are the same
|
|
216
216
|
register of message.
|
|
217
217
|
- Nothing amber is clickable anywhere in the system.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## ADR-0005 — Diagrams are not charts: neutral nodes, colour for state only
|
|
222
|
+
|
|
223
|
+
**Status:** accepted · 2026-10-13
|
|
224
|
+
|
|
225
|
+
### Context
|
|
226
|
+
|
|
227
|
+
`driftwood` draws architecture diagrams and was the first consumer to need them.
|
|
228
|
+
Its first pass on these tokens took the chart exception (§8, ADR-0002) and tinted
|
|
229
|
+
each node by family (edge, compute, data, messaging…) with `--chart-1…6`. On a
|
|
230
|
+
real 46-resource platform that put six hues on screen before anything was wrong,
|
|
231
|
+
and the database that was down was one more tinted box among them.
|
|
232
|
+
|
|
233
|
+
### Decision
|
|
234
|
+
|
|
235
|
+
A diagram follows the restraint rule. The chart exception exists because a chart
|
|
236
|
+
has nothing *but* colour to tell series apart; a diagram node has an icon, a name
|
|
237
|
+
and a kind label, so family is already carried three times. Nodes are neutral.
|
|
238
|
+
Colour is reserved for state: health (`--danger`, `--attention`) and what a change
|
|
239
|
+
did, via three new aliases — `--diff-added`, `--diff-changed`, `--diff-removed` —
|
|
240
|
+
over the existing state hues. No new hue enters the system.
|
|
241
|
+
|
|
242
|
+
### Consequences
|
|
243
|
+
|
|
244
|
+
- On a healthy, unchanged system the diagram is grey, and that is correct:
|
|
245
|
+
nothing is asking for you.
|
|
246
|
+
- `--diff-changed` shares cobalt with links. Context separates them (a node edge
|
|
247
|
+
with a `~ changed` tag is not underlined text), the same way feedback and domain
|
|
248
|
+
status share hues in ADR-0004.
|
|
249
|
+
- Standalone exports (SVG files, Mermaid in a pull request) take the light theme's
|
|
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.
|
|
303
|
+
- Not a token rename and not a visual change, so a patch release.
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## ADR-0007 — The neutral ramp is one temperature: OKLCH hue 286°
|
|
309
|
+
|
|
310
|
+
**Status:** accepted · 2026-10-25
|
|
311
|
+
|
|
312
|
+
### Context
|
|
313
|
+
|
|
314
|
+
`tokens.css` called the ramp "cool-leaning", and most of it was: light 300–800
|
|
315
|
+
and dark 0–800 sit at OKLCH hue 286°. But light 50, 100 and 200 — the hairline,
|
|
316
|
+
`--line-soft`, the hover and pressed surfaces — sat at 106°, a faint yellow, and
|
|
317
|
+
dark 900, the primary ink, did too. A border and the label beside it disagreed
|
|
318
|
+
on temperature. It was too slight to notice as a colour, but enough to make a
|
|
319
|
+
surface read slightly dirty next to a cool one.
|
|
320
|
+
|
|
321
|
+
### Decision
|
|
322
|
+
|
|
323
|
+
Every tinted step sits at hue 286°. The off-hue steps (light 25, 50, 100, 200,
|
|
324
|
+
900; dark 800, 900) are regenerated at **their existing OKLCH lightness**, so
|
|
325
|
+
contrast stays within 0.04 of its old value. Their chroma fits the curve
|
|
326
|
+
already set by their neighbours: near zero at the ends, peaking in the
|
|
327
|
+
mid-tones. Steps already at 286° are untouched. Pure white stays pure white.
|
|
328
|
+
`npm run check` fails any step that leaves 286° ±3°.
|
|
329
|
+
|
|
330
|
+
### Consequences
|
|
331
|
+
|
|
332
|
+
- A visible change, if a quiet one, so the release that carries it is a minor.
|
|
333
|
+
- The cool ground does the job the original comment gave it: the warm mark
|
|
334
|
+
and the amber attention state are the only warm things on a page.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## ADR-0008 — Cobalt is our own: OKLCH hue 256°, not Tailwind's blue
|
|
339
|
+
|
|
340
|
+
**Status:** accepted · 2026-10-25
|
|
341
|
+
**Amends:** ADR-0001 (the hue stays cobalt; the values change)
|
|
342
|
+
|
|
343
|
+
### Context
|
|
344
|
+
|
|
345
|
+
ADR-0001 chose cobalt for two reasons: it's semantically inert, and it's
|
|
346
|
+
complementary to larch, so identity and affordance don't compete. The values
|
|
347
|
+
it shipped, `#2563eb` / `#1d4ed8` / `#1e40af` / `#1e3a8a` and `#60a5fa` /
|
|
348
|
+
`#93c5fd` / `#bfdbfe`, are exactly Tailwind's `blue-600…900` and `blue-300…400`,
|
|
349
|
+
and the sequential chart ramp was Tailwind's blues too. That undercut both the
|
|
350
|
+
second reason and DESIGN.md §1's "nothing here should read as a template":
|
|
351
|
+
|
|
352
|
+
- Tailwind's blue is at OKLCH 263–266°, leaning violet. Larch is ≈68°, so its
|
|
353
|
+
opposite is ≈248°. The stock values were 15° further from that than they
|
|
354
|
+
needed to be.
|
|
355
|
+
- It isn't even one hue. Across the ramp the stock steps wander from 251.8°
|
|
356
|
+
to 265.6°, so hover and pressed shift colour as well as depth.
|
|
357
|
+
- It is the single most recognisable colour in the Tailwind default theme.
|
|
358
|
+
|
|
359
|
+
### Decision
|
|
360
|
+
|
|
361
|
+
Every cobalt step, interactive and sequential, sits at **OKLCH hue 256°**, and
|
|
362
|
+
`npm run check` holds it there ±3°. 256° is as near to larch's opposite as sRGB
|
|
363
|
+
allows while keeping cobalt's chroma (≈0.18 at the fill). At 248–252° the gamut
|
|
364
|
+
caps chroma near 0.14–0.16, which reads as steel or ocean blue, and links lose
|
|
365
|
+
some of the "you can act on this" that ADR-0001 chose blue for.
|
|
366
|
+
|
|
367
|
+
Each step's lightness was **solved for the contrast its predecessor met**, so
|
|
368
|
+
every ratio is within 0.07 of before: fill 5.18:1 under a white label, link
|
|
369
|
+
6.78:1 on white, dark fill 7.07:1 on card. The sequential ramp keeps each
|
|
370
|
+
step's OKLCH lightness and stays monotonic.
|
|
371
|
+
|
|
372
|
+
### Consequences
|
|
373
|
+
|
|
374
|
+
- The blue is deeper and calmer than Tailwind's electric violet-blue, and it is
|
|
375
|
+
one colour from fill to pressed. It still reads unmistakably as a link.
|
|
376
|
+
- A visible change, so the release that carries it is a minor.
|
|
377
|
+
- `--chart-1` is a separately validated categorical slot (ADR-0002) and is
|
|
378
|
+
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
|
|
@@ -159,19 +164,89 @@ rationale in [`tokens.chart.css`](./tokens.chart.css). The short version:
|
|
|
159
164
|
- Text wears text tokens, never the series colour.
|
|
160
165
|
- **Never a dual-axis chart.** Two measures of different scale → two charts.
|
|
161
166
|
|
|
162
|
-
## 9.
|
|
167
|
+
## 9. Diagrams and diffs
|
|
168
|
+
|
|
169
|
+
An architecture diagram (`driftwood`, `homelab`) is a picture of a system, read to
|
|
170
|
+
answer "what changed, and what is broken". It follows the restraint rule, **not**
|
|
171
|
+
the chart exception: node *type* is a category like event type, and is carried by
|
|
172
|
+
an icon and a label, never a hue. A diagram where every node is tinted by family
|
|
173
|
+
is thirty coloured things, and the one that is down disappears among them.
|
|
174
|
+
|
|
175
|
+
- **Nodes are neutral.** `--card` fill, `--line-strong` edge, `--radius` corners,
|
|
176
|
+
an icon in `--ink-soft`, the name in `--ink`, the kind in `--mono` at
|
|
177
|
+
`--text-xs` in `--ink-soft`. Grouping is a zone: `--line` hairline, no fill or
|
|
178
|
+
a faint one, a `--mono` eyebrow label.
|
|
179
|
+
- **Edges are `--slate`.** They are UI marks (3:1), never text.
|
|
180
|
+
- **Only state gets colour**, and always as a tag with a symbol and a word as well:
|
|
181
|
+
|
|
182
|
+
| Token | Means | Tag |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| `--diff-added` | this change created it | `+ added` |
|
|
185
|
+
| `--diff-changed` | this change modified it | `~ changed` |
|
|
186
|
+
| `--diff-removed` | this change removes it — dashed, struck through | `− removed` |
|
|
187
|
+
| `--danger` | it is down | `✕ down` |
|
|
188
|
+
| `--attention` | degraded, or differs from the design | `▲ degraded` |
|
|
189
|
+
|
|
190
|
+
The diff tokens alias the state hues on purpose (added is *settled*, a change is
|
|
191
|
+
*informational*, a removal is the change that can lose data), so a diff adds no
|
|
192
|
+
hue to the system. Health wins the fill (`-surface`); a diff only takes the edge.
|
|
193
|
+
- **Selection** is the focus treatment: a 2px `--i-ink` outline, offset. Asking
|
|
194
|
+
"what depends on this?" dims everything unrelated to ~30% opacity and inks the
|
|
195
|
+
connecting edges in `--ink`; it never introduces a colour.
|
|
196
|
+
- **Lead with the verdict.** A page about a deploy opens with one sentence saying
|
|
197
|
+
what happened and what needs you, then the pills that back it up, then the
|
|
198
|
+
picture.
|
|
199
|
+
- **Diagrams follow the theme.** On a page they use the tokens like everything
|
|
200
|
+
else. A diagram exported as a standalone file (an SVG, a Mermaid block in a pull
|
|
201
|
+
request) cannot use `var()`; it takes the **light** theme's values — the first
|
|
202
|
+
argument of each `light-dark()` — resolved from this file at build time, never
|
|
203
|
+
typed in.
|
|
204
|
+
|
|
205
|
+
## 10. Accessibility floor
|
|
163
206
|
|
|
164
207
|
Non-negotiable in both themes:
|
|
165
208
|
|
|
166
|
-
- Body text ≥ 4.5:1; large text and UI marks ≥ 3:1.
|
|
209
|
+
- Body text ≥ 4.5:1; large text and UI marks ≥ 3:1. `--slate` is the quietest
|
|
210
|
+
text step and clears 4.5:1 in both themes; `--n-500` in light does not, so it
|
|
211
|
+
is never text.
|
|
167
212
|
- Touch targets ≥ 44px on coarse pointers, with a `.touch-hit` opt-out for controls
|
|
168
213
|
whose shape is meaningful (a 22px circular checkbox forced to 44px becomes an
|
|
169
214
|
oval — it expands its hit area instead).
|
|
170
215
|
- Every state carries an icon and a word.
|
|
171
|
-
- Measured ratios sit next to their values in `tokens.css
|
|
172
|
-
|
|
216
|
+
- Measured ratios sit next to their values in `tokens.css`, and `npm run check`
|
|
217
|
+
(run in CI on every pull request) fails if any floor above is missed, if a colour
|
|
218
|
+
is declared anywhere but once, or if a hex appears outside the token files.
|
|
173
219
|
|
|
174
|
-
##
|
|
220
|
+
## 11. Themes, and integrating with a Tailwind consumer
|
|
221
|
+
|
|
222
|
+
### How the theme is chosen
|
|
223
|
+
|
|
224
|
+
Every colour is one declaration holding both values, `light-dark(<light>, <dark>)`,
|
|
225
|
+
and the browser picks a side from the element's `color-scheme`:
|
|
226
|
+
|
|
227
|
+
- **No attribute** — `color-scheme: light dark`, so the page follows the OS.
|
|
228
|
+
- **`data-theme="light"` / `"dark"`** — the explicit choice, which wins over the OS.
|
|
229
|
+
|
|
230
|
+
The pick happens where a colour is *used*, so **`data-theme` works on any element,
|
|
231
|
+
not just `<html>`.** A dark hero band on a light marketing page is
|
|
232
|
+
`<section data-theme="dark">`. Give that element its own
|
|
233
|
+
`background: var(--bg); color: var(--ink)` — backgrounds don't inherit.
|
|
234
|
+
|
|
235
|
+
Two consequences for code that touches tokens directly:
|
|
236
|
+
|
|
237
|
+
- **Don't read a token's value with `getPropertyValue`.** It returns the
|
|
238
|
+
declaration text, `light-dark(#076bd3, #6ba5f3)`, not a colour. To get the
|
|
239
|
+
colour a chart library needs, resolve it on an element in the right subtree:
|
|
240
|
+
set `el.style.color = 'var(--chart-1)'` and read `getComputedStyle(el).color`.
|
|
241
|
+
Re-read it when the theme changes.
|
|
242
|
+
- **Don't wrap a token in `color-mix()` or `light-dark()`.** Nesting is valid CSS,
|
|
243
|
+
but not every engine has been verified to resolve it. If you need a wash, it's
|
|
244
|
+
a token (`--*-surface`, `--*-line`); if it isn't, add it here.
|
|
245
|
+
|
|
246
|
+
Needs Chrome/Edge 123, Firefox 120, Safari 17.5 or later (all shipped by May 2024).
|
|
247
|
+
An older browser drops every colour token and renders unstyled rather than wrong.
|
|
248
|
+
|
|
249
|
+
### Tailwind
|
|
175
250
|
|
|
176
251
|
Tailwind's dark variant keys off a `.dark` class; these tokens key off
|
|
177
252
|
`[data-theme]`. **Both must be set together**, by the same code, in both places
|
|
@@ -186,11 +261,11 @@ Setting only the class is a silent, half-broken state: Tailwind utilities flip
|
|
|
186
261
|
but the tokens do not, and the page renders one theme's tokens under the other
|
|
187
262
|
theme's utilities. Setting only the attribute leaves every `dark:` utility dead.
|
|
188
263
|
|
|
189
|
-
The reason the attribute is required rather than
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
264
|
+
The reason the attribute is required rather than leaving the OS to decide: a
|
|
265
|
+
site with a manual toggle has already *resolved* the OS preference into an
|
|
266
|
+
explicit choice. Without the attribute, the tokens would still follow the OS, and
|
|
267
|
+
a user on a dark OS who chose light would get dark tokens under light utilities.
|
|
268
|
+
`[data-theme]` is what makes the choice authoritative.
|
|
194
269
|
|
|
195
270
|
Import order matters — Tailwind first, then tokens, then the bridge:
|
|
196
271
|
|
|
@@ -200,7 +275,7 @@ Import order matters — Tailwind first, then tokens, then the bridge:
|
|
|
200
275
|
@import "@brignano/design/tailwind.css";
|
|
201
276
|
```
|
|
202
277
|
|
|
203
|
-
##
|
|
278
|
+
## 12. The one rule that keeps this true
|
|
204
279
|
|
|
205
280
|
**Never hardcode a hex outside `tokens.css`.**
|
|
206
281
|
|
package/README.md
CHANGED
|
@@ -9,16 +9,17 @@ The shared design system for every brignano surface — `brignano.io`, `life`,
|
|
|
9
9
|
| [`DECISIONS.md`](./DECISIONS.md) | Why the values are what they are. ADRs. |
|
|
10
10
|
| [`tokens.css`](./tokens.css) | Single source of truth for colour, type, space, shape, motion. |
|
|
11
11
|
| [`tokens.chart.css`](./tokens.chart.css) | Validated categorical palette for data viz. |
|
|
12
|
+
| [`previews/`](./previews) | Live swatches. Open one in a browser to see the tokens render. |
|
|
12
13
|
|
|
13
14
|
## The shape of it
|
|
14
15
|
|
|
15
16
|
Three layers, so no colour does two jobs:
|
|
16
17
|
|
|
17
|
-
- **`--
|
|
18
|
+
- **`--mark`** — identity. Larch amber. Inks the trail and the monogram. Never a control.
|
|
18
19
|
- **`--i-*`** — affordance. Cobalt. Links, buttons, focus, selection. Semantically inert.
|
|
19
|
-
- **`--
|
|
20
|
+
- **`--success / --attention / --danger`** — state. Conventional hues, always with an icon and a word.
|
|
20
21
|
|
|
21
|
-
Distinctiveness lives in the
|
|
22
|
+
Distinctiveness lives in the mark; conventionality lives in the control layer.
|
|
22
23
|
|
|
23
24
|
## Consuming it
|
|
24
25
|
|
|
@@ -31,10 +32,40 @@ npm install @brignano/design
|
|
|
31
32
|
@import '@brignano/design/tokens.chart.css'; /* only if the surface has charts */
|
|
32
33
|
```
|
|
33
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
|
+
|
|
34
40
|
The tool tier is the default. A marketing surface opts in with
|
|
35
41
|
`class="tier-marketing"` on `<html>`, which unlocks the display face and opens the
|
|
36
42
|
type scale. Colour is identical across tiers.
|
|
37
43
|
|
|
44
|
+
## Previews
|
|
45
|
+
|
|
46
|
+
`previews/` holds a page per area — colour, type, elevation, controls, feedback, diagrams,
|
|
47
|
+
status. Each renders its subject twice, once in the viewer's theme and once on a
|
|
48
|
+
forced-dark ground, so drift between the two shows up without toggling anything.
|
|
49
|
+
No build step: open the file.
|
|
50
|
+
|
|
51
|
+
Every preview opens with a card marker:
|
|
52
|
+
|
|
53
|
+
```html
|
|
54
|
+
<!-- @dsCard group="Foundations" -->
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
That first line is what a [claude.ai/design][ds] design-system project reads to
|
|
58
|
+
build its card index — `group` becomes the section heading in the Design System
|
|
59
|
+
pane. The markers are comments, so they cost nothing when the page is opened
|
|
60
|
+
locally.
|
|
61
|
+
|
|
62
|
+
**A sync to such a project keeps this layout** — `tokens.css` at the project
|
|
63
|
+
root, `previews/` one level below it. `_preview.css` reaches it with
|
|
64
|
+
`@import '../tokens.css'` rather than carrying its own copy, because a second
|
|
65
|
+
copy of the tokens is the drift this repo exists to prevent. The trade is that
|
|
66
|
+
the parent reference is load-bearing: flatten the uploaded bundle and every
|
|
67
|
+
preview renders unstyled.
|
|
68
|
+
|
|
38
69
|
## For agents
|
|
39
70
|
|
|
40
71
|
Read `DESIGN.md` before styling anything. Use the tokens. **Never hardcode a hex
|
|
@@ -84,4 +115,5 @@ and repo, workflow filename `publish.yml`, and **environment name blank**, since
|
|
|
84
115
|
these workflows declare no `environment:`. Configure it before the first tag, or
|
|
85
116
|
the first publish fails the way this one did.
|
|
86
117
|
|
|
118
|
+
[ds]: https://claude.ai/design
|
|
87
119
|
[tp]: https://docs.npmjs.com/trusted-publishers/
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brignano/design",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Shared design system for brignano surfaces
|
|
3
|
+
"version": "0.3.0",
|
|
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,47 @@
|
|
|
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-on-dark: var(--on-dark);
|
|
21
27
|
|
|
22
28
|
--color-interactive: var(--i);
|
|
23
29
|
--color-interactive-ink: var(--i-ink);
|
|
24
30
|
--color-interactive-hover: var(--i-hover);
|
|
25
31
|
--color-interactive-pressed: var(--i-pressed);
|
|
26
32
|
--color-interactive-surface: var(--i-surface);
|
|
33
|
+
--color-interactive-line: var(--i-line);
|
|
27
34
|
--color-on-interactive: var(--on-i);
|
|
28
35
|
|
|
29
36
|
--color-success: var(--success);
|
|
30
37
|
--color-success-ink: var(--success-ink);
|
|
31
38
|
--color-success-surface: var(--success-surface);
|
|
39
|
+
--color-success-line: var(--success-line);
|
|
32
40
|
--color-attention: var(--attention);
|
|
33
41
|
--color-attention-ink: var(--attention-ink);
|
|
34
42
|
--color-attention-surface: var(--attention-surface);
|
|
43
|
+
--color-attention-line: var(--attention-line);
|
|
35
44
|
--color-danger: var(--danger);
|
|
36
45
|
--color-danger-ink: var(--danger-ink);
|
|
37
46
|
--color-danger-surface: var(--danger-surface);
|
|
47
|
+
--color-danger-line: var(--danger-line);
|
|
48
|
+
--color-info: var(--info);
|
|
49
|
+
--color-info-ink: var(--info-ink);
|
|
50
|
+
--color-info-surface: var(--info-surface);
|
|
51
|
+
--color-info-line: var(--info-line);
|
|
52
|
+
|
|
53
|
+
--color-diff-added: var(--diff-added);
|
|
54
|
+
--color-diff-added-ink: var(--diff-added-ink);
|
|
55
|
+
--color-diff-added-surface: var(--diff-added-surface);
|
|
56
|
+
--color-diff-changed: var(--diff-changed);
|
|
57
|
+
--color-diff-changed-ink: var(--diff-changed-ink);
|
|
58
|
+
--color-diff-changed-surface: var(--diff-changed-surface);
|
|
59
|
+
--color-diff-removed: var(--diff-removed);
|
|
60
|
+
--color-diff-removed-ink: var(--diff-removed-ink);
|
|
61
|
+
--color-diff-removed-surface: var(--diff-removed-surface);
|
|
38
62
|
|
|
39
63
|
--color-mark: var(--mark);
|
|
40
64
|
--color-mark-ink: var(--mark-ink);
|
|
@@ -44,7 +68,7 @@
|
|
|
44
68
|
--font-display: var(--display);
|
|
45
69
|
|
|
46
70
|
--radius-sm: var(--radius-sm);
|
|
47
|
-
--radius
|
|
71
|
+
--radius: var(--radius);
|
|
48
72
|
--radius-lg: var(--radius-lg);
|
|
49
73
|
|
|
50
74
|
--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,102 @@
|
|
|
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);
|
|
75
122
|
--ink: var(--n-900);
|
|
76
123
|
--ink-soft: var(--n-700);
|
|
77
|
-
--slate:
|
|
124
|
+
--slate: light-dark(#6b6b73, #8b8b93); /* n-600 / n-500 — text-safe: eyebrows, axis labels, captions */
|
|
78
125
|
--line: var(--n-100);
|
|
79
126
|
--line-soft: var(--n-50);
|
|
80
|
-
--line-strong: var(--n-300);
|
|
81
|
-
|
|
127
|
+
--line-strong: var(--n-300); /* 1.62:1 — decoration, not a boundary */
|
|
128
|
+
/* The edge that tells you a control is there: inputs, secondary buttons.
|
|
129
|
+
WCAG 1.4.11 asks 3:1 of a boundary you need to find the control, which
|
|
130
|
+
--line-strong is nowhere near. Light is n-500 (3.44:1 on card, 3.29:1
|
|
131
|
+
on bg); dark sits between n-400 and n-500, which the ramp has no step
|
|
132
|
+
for (3.39:1 on card, 3.68:1 on bg). */
|
|
133
|
+
--line-control: light-dark(#8a8a8f, #6b6b73);
|
|
134
|
+
--on-dark: light-dark(#fafafb, #0d0d0f); /* n-25 / n-0 */
|
|
82
135
|
|
|
83
136
|
/* ── Interactive — cobalt ─────────────────────────────────────────────
|
|
84
137
|
Everything you can act on: links, buttons, focus, selection. Chosen to
|
|
@@ -86,21 +139,28 @@
|
|
|
86
139
|
green and red do — and complementary to the mark hue, so identity and
|
|
87
140
|
affordance never compete for the same attention.
|
|
88
141
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
142
|
+
Every step sits at OKLCH hue 256° (`npm run check` holds it), as near
|
|
143
|
+
to larch's opposite (~248°) as sRGB allows while keeping cobalt's
|
|
144
|
+
chroma. It is deliberately NOT Tailwind's blue-600 (263°, a touch
|
|
145
|
+
violet and electric): same affordance, our own colour (ADR-0008).
|
|
146
|
+
|
|
147
|
+
--i fill 5.18:1 / 7.07:1 (label 5.18 / 7.46)
|
|
148
|
+
--i-ink link/icon/focus 6.78:1 / 7.07:1
|
|
149
|
+
--i-hover hover fill 8.65:1 / 10.50:1 (label)
|
|
150
|
+
--i-pressed pressed fill 10.44:1 / 13.32:1 (label)
|
|
151
|
+
|
|
152
|
+
In dark, fill and ink converge, and hover/pressed get LIGHTER.
|
|
93
153
|
|
|
94
154
|
DISABLED IS NEUTRAL, NEVER HUED — a greyed-out control must stop
|
|
95
155
|
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;
|
|
156
|
+
--i: light-dark(#076bd3, #6ba5f3);
|
|
157
|
+
--i-ink: light-dark(#0159b5, #6ba5f3);
|
|
158
|
+
--i-hover: light-dark(#014a97, #98c4ff);
|
|
159
|
+
--i-pressed: light-dark(#003e82, #c0dbff);
|
|
160
|
+
--i-surface: light-dark(color-mix(in srgb, #076bd3 12%, transparent), color-mix(in srgb, #6ba5f3 20%, transparent));
|
|
161
|
+
--i-line: light-dark(color-mix(in srgb, #076bd3 32%, transparent), color-mix(in srgb, #6ba5f3 44%, transparent));
|
|
162
|
+
--i-selection: light-dark(color-mix(in srgb, #076bd3 26%, transparent), color-mix(in srgb, #6ba5f3 30%, transparent));
|
|
163
|
+
--on-i: light-dark(#ffffff, #111111);
|
|
104
164
|
--i-disabled-bg: var(--n-200);
|
|
105
165
|
--i-disabled-ink: var(--n-400);
|
|
106
166
|
|
|
@@ -109,33 +169,53 @@
|
|
|
109
169
|
tiers. Always shipped with an ICON AND A WORD — colour is never the
|
|
110
170
|
only signal.
|
|
111
171
|
|
|
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
|
|
172
|
+
--success settled · confirmed · done · succeeded 6.29:1 / 8.15:1
|
|
173
|
+
--attention now · next · warning · needs you 6.08:1 / 8.17:1 (ink)
|
|
174
|
+
--danger overdue · failed · invalid 6.10:1 / 6.06:1
|
|
115
175
|
|
|
116
176
|
`--info` is the interactive hue on purpose: an informational message
|
|
117
177
|
is the same "neutral, no action implied" register as a link.
|
|
118
178
|
|
|
119
179
|
`-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
|
|
180
|
+
`-surface` and `-line` are the wash and its edge: 12% / 32% of the hue
|
|
181
|
+
in light, 20% / 44% in dark. */
|
|
182
|
+
--success: light-dark(#2f6b4f, #6fbf95);
|
|
183
|
+
--success-ink: light-dark(#245740, #8fd4ae);
|
|
184
|
+
--attention: light-dark(#b4741f, #e0a44f); /* 3.85:1 in light — mark/fill only, NOT text */
|
|
185
|
+
--attention-ink: light-dark(#8a5714, #e0a44f); /* 6.08:1 / 8.17:1 — mark and label converge in dark */
|
|
186
|
+
--danger: light-dark(#a4442c, #e07a5f);
|
|
187
|
+
--danger-ink: light-dark(#8a3722, #ec9880);
|
|
127
188
|
--info: var(--i);
|
|
128
189
|
--info-ink: var(--i-ink);
|
|
129
190
|
|
|
130
|
-
--success-surface: color-mix(in srgb,
|
|
131
|
-
--attention-surface: color-mix(in srgb,
|
|
132
|
-
--danger-surface: color-mix(in srgb,
|
|
191
|
+
--success-surface: light-dark(color-mix(in srgb, #2f6b4f 12%, transparent), color-mix(in srgb, #6fbf95 20%, transparent));
|
|
192
|
+
--attention-surface: light-dark(color-mix(in srgb, #b4741f 12%, transparent), color-mix(in srgb, #e0a44f 20%, transparent));
|
|
193
|
+
--danger-surface: light-dark(color-mix(in srgb, #a4442c 12%, transparent), color-mix(in srgb, #e07a5f 20%, transparent));
|
|
133
194
|
--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,
|
|
195
|
+
--success-line: light-dark(color-mix(in srgb, #2f6b4f 32%, transparent), color-mix(in srgb, #6fbf95 44%, transparent));
|
|
196
|
+
--attention-line: light-dark(color-mix(in srgb, #b4741f 32%, transparent), color-mix(in srgb, #e0a44f 44%, transparent));
|
|
197
|
+
--danger-line: light-dark(color-mix(in srgb, #a4442c 32%, transparent), color-mix(in srgb, #e07a5f 44%, transparent));
|
|
137
198
|
--info-line: var(--i-line);
|
|
138
199
|
|
|
200
|
+
/* ── Diff ─────────────────────────────────────────────────────────────
|
|
201
|
+
What a change did to a thing: a deploy, a plan, a pull request. Aliases
|
|
202
|
+
of the state hues, so a diff adds no hue of its own (DESIGN.md §9):
|
|
203
|
+
|
|
204
|
+
--diff-added created by this change success — settled
|
|
205
|
+
--diff-changed modified by this change interactive — informational
|
|
206
|
+
--diff-removed removed by this change danger — can lose data
|
|
207
|
+
|
|
208
|
+
Always with a symbol and a word (+ added, ~ changed, − removed). */
|
|
209
|
+
--diff-added: var(--success);
|
|
210
|
+
--diff-added-ink: var(--success-ink);
|
|
211
|
+
--diff-added-surface: var(--success-surface);
|
|
212
|
+
--diff-changed: var(--i);
|
|
213
|
+
--diff-changed-ink: var(--i-ink);
|
|
214
|
+
--diff-changed-surface: var(--i-surface);
|
|
215
|
+
--diff-removed: var(--danger);
|
|
216
|
+
--diff-removed-ink: var(--danger-ink);
|
|
217
|
+
--diff-removed-surface: var(--danger-surface);
|
|
218
|
+
|
|
139
219
|
/* ── Mark — identity ──────────────────────────────────────────────────
|
|
140
220
|
Inks a GRAPHIC: the trail line, the A|B monogram, a hero rule. That is
|
|
141
221
|
the entire permitted use.
|
|
@@ -148,21 +228,21 @@
|
|
|
148
228
|
the attention state are the same warmth, and because --mark never
|
|
149
229
|
appears as a UI surface the two can never be mistaken for each other
|
|
150
230
|
in the same context. */
|
|
151
|
-
--mark: #b4741f;
|
|
152
|
-
--mark-ink: #8a5714; /* when the mark carries a label, 6.08:1 */
|
|
231
|
+
--mark: light-dark(#b4741f, #e0a44f);
|
|
232
|
+
--mark-ink: light-dark(#8a5714, #e0a44f); /* when the mark carries a label, 6.08:1 / 8.17:1 */
|
|
153
233
|
|
|
154
234
|
/* ── Legacy aliases ───────────────────────────────────────────────────
|
|
155
235
|
Kept so existing `life` components keep compiling. Do not use in new
|
|
156
236
|
work — reach for the semantic tokens above.
|
|
157
237
|
|
|
158
|
-
--larch-deep is
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
238
|
+
--larch-deep is written out rather than aliased, because "deep"
|
|
239
|
+
inverts: darker in light, BRIGHTER in dark. An alias to either role
|
|
240
|
+
silently changes one theme — it moved .pill-active from #f0bd74 to
|
|
241
|
+
#e0a44f before this was caught. */
|
|
162
242
|
--forest: var(--success);
|
|
163
243
|
--forest-deep: var(--success-ink);
|
|
164
244
|
--larch: var(--attention);
|
|
165
|
-
--larch-deep: #8a5714;
|
|
245
|
+
--larch-deep: light-dark(#8a5714, #f0bd74);
|
|
166
246
|
--rust: var(--danger);
|
|
167
247
|
--rust-deep: var(--danger-ink);
|
|
168
248
|
--gold: var(--slate);
|
|
@@ -184,151 +264,51 @@
|
|
|
184
264
|
--s6: 32px; --s7: 48px; --s8: 64px; --s9: 96px;
|
|
185
265
|
|
|
186
266
|
/* ── Shape & elevation ────────────────────────────────────────────────
|
|
187
|
-
In LIGHT these are shadows; in DARK the surface lightens instead
|
|
267
|
+
In LIGHT these are shadows; in DARK the surface lightens instead, and
|
|
268
|
+
the shadow only has to separate it from a near-black ground.
|
|
269
|
+
|
|
270
|
+
light-dark() switches colours, not geometry, and the two themes' blur
|
|
271
|
+
differs. So shadows 2 and 3 carry one layer per theme, with the other
|
|
272
|
+
theme's layer transparent — it draws nothing. */
|
|
188
273
|
--radius-sm: 6px;
|
|
189
274
|
--radius: 12px;
|
|
190
275
|
--radius-lg: 18px;
|
|
191
|
-
--shadow-1: 0 1px 2px rgba(17, 17, 17, 0.04);
|
|
192
|
-
--shadow-2:
|
|
193
|
-
|
|
194
|
-
|
|
276
|
+
--shadow-1: 0 1px 2px light-dark(rgba(17, 17, 17, 0.04), rgba(0, 0, 0, 0.45));
|
|
277
|
+
--shadow-2:
|
|
278
|
+
0 2px 8px light-dark(rgba(17, 17, 17, 0.07), transparent),
|
|
279
|
+
0 2px 10px light-dark(transparent, rgba(0, 0, 0, 0.55));
|
|
280
|
+
--shadow-3:
|
|
281
|
+
0 8px 24px light-dark(rgba(17, 17, 17, 0.10), transparent),
|
|
282
|
+
0 10px 28px light-dark(transparent, rgba(0, 0, 0, 0.65));
|
|
283
|
+
--scrim: light-dark(rgba(17, 17, 17, 0.55), rgba(0, 0, 0, 0.68));
|
|
195
284
|
--shadow: var(--shadow-1); /* legacy alias */
|
|
196
285
|
|
|
197
286
|
/* ── Motion ───────────────────────────────────────────────────────────
|
|
198
287
|
Content is ALWAYS visible by default; motion is enhancement layered on
|
|
199
288
|
top, never the thing that makes content appear. brignano.io shipped a
|
|
200
289
|
homepage whose contact CTA was invisible on 100% of loads because an
|
|
201
|
-
animation library owned the visible state. See DESIGN.md §
|
|
290
|
+
animation library owned the visible state. See DESIGN.md §7. */
|
|
202
291
|
--ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
203
292
|
--dur-fast: 150ms;
|
|
204
293
|
--dur: 250ms;
|
|
205
294
|
--dur-slow: 600ms;
|
|
206
295
|
}
|
|
207
296
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
--n-0: #0d0d0f;
|
|
225
|
-
--n-25: #131316;
|
|
226
|
-
--n-50: #17171a;
|
|
227
|
-
--n-100: #202024;
|
|
228
|
-
--n-200: #2a2a2f;
|
|
229
|
-
--n-300: #3a3a41;
|
|
230
|
-
--n-400: #55555e;
|
|
231
|
-
--n-500: #8b8b93; /* 5.29:1 on card */
|
|
232
|
-
--n-600: #9c9ca4;
|
|
233
|
-
--n-700: #a8a8b0; /* 7.58:1 */
|
|
234
|
-
--n-800: #d0d0d2;
|
|
235
|
-
--n-900: #f2f2f0; /* 15.96:1 */
|
|
236
|
-
|
|
237
|
-
--bg: var(--n-0);
|
|
238
|
-
--card: var(--n-50);
|
|
239
|
-
--surface-hover: var(--n-100);
|
|
240
|
-
--surface-active: var(--n-200);
|
|
241
|
-
--on-dark: var(--n-0);
|
|
242
|
-
|
|
243
|
-
--i: #60a5fa; /* 7.04:1 */
|
|
244
|
-
--i-ink: #60a5fa;
|
|
245
|
-
--i-hover: #93c5fd;
|
|
246
|
-
--i-pressed: #bfdbfe;
|
|
247
|
-
--i-surface: color-mix(in srgb, var(--i) 20%, transparent);
|
|
248
|
-
--i-line: color-mix(in srgb, var(--i) 44%, transparent);
|
|
249
|
-
--i-selection: color-mix(in srgb, var(--i) 30%, transparent);
|
|
250
|
-
--on-i: #111111;
|
|
251
|
-
|
|
252
|
-
--success: #6fbf95;
|
|
253
|
-
--success-ink: #8fd4ae;
|
|
254
|
-
--attention: #e0a44f;
|
|
255
|
-
--attention-ink: #e0a44f; /* 8.17:1 — mark and label converge in dark */
|
|
256
|
-
--danger: #e07a5f;
|
|
257
|
-
--danger-ink: #ec9880;
|
|
258
|
-
|
|
259
|
-
--success-surface: color-mix(in srgb, var(--success) 20%, transparent);
|
|
260
|
-
--attention-surface: color-mix(in srgb, var(--attention) 20%, transparent);
|
|
261
|
-
--danger-surface: color-mix(in srgb, var(--danger) 20%, transparent);
|
|
262
|
-
--success-line: color-mix(in srgb, var(--success) 44%, transparent);
|
|
263
|
-
--attention-line: color-mix(in srgb, var(--attention) 44%, transparent);
|
|
264
|
-
--danger-line: color-mix(in srgb, var(--danger) 44%, transparent);
|
|
265
|
-
|
|
266
|
-
--mark: #e0a44f;
|
|
267
|
-
--mark-ink: #e0a44f;
|
|
268
|
-
|
|
269
|
-
--larch-deep: #f0bd74; /* brighter step in dark — see the light block */
|
|
270
|
-
|
|
271
|
-
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45);
|
|
272
|
-
--shadow-2: 0 2px 10px rgba(0, 0, 0, 0.55);
|
|
273
|
-
--shadow-3: 0 10px 28px rgba(0, 0, 0, 0.65);
|
|
274
|
-
--scrim: rgba(0, 0, 0, 0.68);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
:root[data-theme='dark'] {
|
|
279
|
-
color-scheme: dark;
|
|
280
|
-
|
|
281
|
-
--n-0: #0d0d0f;
|
|
282
|
-
--n-25: #131316;
|
|
283
|
-
--n-50: #17171a;
|
|
284
|
-
--n-100: #202024;
|
|
285
|
-
--n-200: #2a2a2f;
|
|
286
|
-
--n-300: #3a3a41;
|
|
287
|
-
--n-400: #55555e;
|
|
288
|
-
--n-500: #8b8b93;
|
|
289
|
-
--n-600: #9c9ca4;
|
|
290
|
-
--n-700: #a8a8b0;
|
|
291
|
-
--n-800: #d0d0d2;
|
|
292
|
-
--n-900: #f2f2f0;
|
|
293
|
-
|
|
294
|
-
--bg: var(--n-0);
|
|
295
|
-
--card: var(--n-50);
|
|
296
|
-
--surface-hover: var(--n-100);
|
|
297
|
-
--surface-active: var(--n-200);
|
|
298
|
-
--on-dark: var(--n-0);
|
|
299
|
-
|
|
300
|
-
--i: #60a5fa;
|
|
301
|
-
--i-ink: #60a5fa;
|
|
302
|
-
--i-hover: #93c5fd;
|
|
303
|
-
--i-pressed: #bfdbfe;
|
|
304
|
-
--i-surface: color-mix(in srgb, var(--i) 20%, transparent);
|
|
305
|
-
--i-line: color-mix(in srgb, var(--i) 44%, transparent);
|
|
306
|
-
--i-selection: color-mix(in srgb, var(--i) 30%, transparent);
|
|
307
|
-
--on-i: #111111;
|
|
308
|
-
|
|
309
|
-
--success: #6fbf95;
|
|
310
|
-
--success-ink: #8fd4ae;
|
|
311
|
-
--attention: #e0a44f;
|
|
312
|
-
--attention-ink: #e0a44f;
|
|
313
|
-
--danger: #e07a5f;
|
|
314
|
-
--danger-ink: #ec9880;
|
|
315
|
-
|
|
316
|
-
--success-surface: color-mix(in srgb, var(--success) 20%, transparent);
|
|
317
|
-
--attention-surface: color-mix(in srgb, var(--attention) 20%, transparent);
|
|
318
|
-
--danger-surface: color-mix(in srgb, var(--danger) 20%, transparent);
|
|
319
|
-
--success-line: color-mix(in srgb, var(--success) 44%, transparent);
|
|
320
|
-
--attention-line: color-mix(in srgb, var(--attention) 44%, transparent);
|
|
321
|
-
--danger-line: color-mix(in srgb, var(--danger) 44%, transparent);
|
|
322
|
-
|
|
323
|
-
--mark: #e0a44f;
|
|
324
|
-
--mark-ink: #e0a44f;
|
|
325
|
-
|
|
326
|
-
--larch-deep: #f0bd74;
|
|
327
|
-
|
|
328
|
-
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45);
|
|
329
|
-
--shadow-2: 0 2px 10px rgba(0, 0, 0, 0.55);
|
|
330
|
-
--shadow-3: 0 10px 28px rgba(0, 0, 0, 0.65);
|
|
331
|
-
--scrim: rgba(0, 0, 0, 0.68);
|
|
297
|
+
:root {
|
|
298
|
+
/* Tool-tier scale — compact, because scanning beats reading.
|
|
299
|
+
Declared BEFORE the marketing tier on purpose: `:root` and
|
|
300
|
+
`.tier-marketing` have the same specificity, so on <html> whichever
|
|
301
|
+
comes later wins. With this block last, the class did nothing. */
|
|
302
|
+
--text-xs: 0.6875rem;
|
|
303
|
+
--text-sm: 0.8125rem;
|
|
304
|
+
--text-base: 0.9375rem;
|
|
305
|
+
--text-lg: 1.0625rem;
|
|
306
|
+
--text-xl: 1.25rem;
|
|
307
|
+
--text-2xl: 1.5rem;
|
|
308
|
+
--text-3xl: 1.875rem;
|
|
309
|
+
--text-4xl: 2.25rem;
|
|
310
|
+
--section-gap: var(--s6);
|
|
311
|
+
--measure: 780px;
|
|
332
312
|
}
|
|
333
313
|
|
|
334
314
|
/* ── Marketing tier ─────────────────────────────────────────────────────
|
|
@@ -344,29 +324,21 @@
|
|
|
344
324
|
--text-xl: 1.5rem;
|
|
345
325
|
--text-2xl: 2rem;
|
|
346
326
|
--text-3xl: 2.75rem;
|
|
347
|
-
--text-4xl: clamp(2.75rem,
|
|
327
|
+
--text-4xl: clamp(2.75rem, 1rem + 4.5vw, 4rem); /* rem term keeps it zoomable (WCAG 1.4.4) */
|
|
348
328
|
--section-gap: var(--s9);
|
|
349
329
|
--measure: 68ch;
|
|
350
330
|
}
|
|
351
331
|
|
|
352
|
-
:root {
|
|
353
|
-
/* Tool-tier scale — compact, because scanning beats reading. */
|
|
354
|
-
--text-xs: 0.6875rem;
|
|
355
|
-
--text-sm: 0.8125rem;
|
|
356
|
-
--text-base: 0.9375rem;
|
|
357
|
-
--text-lg: 1.0625rem;
|
|
358
|
-
--text-xl: 1.25rem;
|
|
359
|
-
--text-2xl: 1.5rem;
|
|
360
|
-
--text-3xl: 1.875rem;
|
|
361
|
-
--text-4xl: 2.25rem;
|
|
362
|
-
--section-gap: var(--s6);
|
|
363
|
-
--measure: 780px;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
332
|
::selection {
|
|
367
333
|
background: var(--i-selection);
|
|
368
334
|
}
|
|
369
335
|
|
|
336
|
+
/* Native checkboxes, radios, range and progress take the control hue
|
|
337
|
+
instead of the browser's own blue. */
|
|
338
|
+
:root {
|
|
339
|
+
accent-color: var(--i);
|
|
340
|
+
}
|
|
341
|
+
|
|
370
342
|
/* Ships here so no surface can forget it. */
|
|
371
343
|
@media (prefers-reduced-motion: reduce) {
|
|
372
344
|
*,
|