@brignano/design 0.1.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 ADDED
@@ -0,0 +1,217 @@
1
+ # Design decisions
2
+
3
+ Why the values in `tokens.css` are what they are. Each entry supersedes whatever
4
+ came before it; nothing here is re-litigated without a new entry.
5
+
6
+ ---
7
+
8
+ ## ADR-0001 — The interactive hue is cobalt
9
+
10
+ **Status:** accepted · 2026-08-23
11
+ **Supersedes:** `brignano.io/docs/tsd-site-modernization.md` §10.3
12
+
13
+ ### Context
14
+
15
+ `brignano.io` shipped electric indigo/violet (`#6d28d9` / `#a78bfa`). `life`
16
+ shipped a warm functional ramp. Neither was chosen as an interactive colour —
17
+ §10.3 records violet as resolving a *negative* constraint:
18
+
19
+ > **Accent color:** ✅ **Shift the hue** away from emerald. Candidate: an
20
+ > **electric indigo/violet** … **amber as fallback**.
21
+
22
+ That decision answered "not green." A long argument followed about whether amber
23
+ or violet should replace it, which was the wrong argument — see ADR-0004. Once
24
+ identity, affordance and state were separated, the only question left was what
25
+ colour a *control* should be.
26
+
27
+ ### Decision
28
+
29
+ Cobalt (`#2563eb` / `#60a5fa`), with a full hover / pressed / focus / disabled ramp.
30
+
31
+ ### Rationale
32
+
33
+ Cobalt, indigo and violet were all measured against both grounds. **All three
34
+ cleared every gate**, so contrast did not decide it:
35
+
36
+ | Candidate | Text (light) | Fill (light) | Text (dark) | Pressed (dark) |
37
+ |---|---|---|---|---|
38
+ | cobalt | 6.70 | 5.17 | 7.64 | 10.77 |
39
+ | indigo | 7.90 | 6.29 | 6.51 | 9.74 |
40
+ | violet | 7.10 | 5.70 | 7.13 | 10.52 |
41
+
42
+ It was decided on the two things contrast can't measure:
43
+
44
+ 1. **Semantically inert.** Amber means caution, green means success, red means
45
+ error — all of them arrive with meaning already attached. Blue arrives with
46
+ none, which is exactly why product UIs converged on it. That is a functional
47
+ property, not a fashion.
48
+ 2. **Complementary to the mark hue.** Cobalt sits opposite larch on the wheel, so
49
+ identity and affordance never compete for the same attention.
50
+
51
+ Cobalt over indigo/violet specifically: it has the most dark-mode headroom of the
52
+ three, and blue is the most conventional link colour in existence — the lowest
53
+ cognitive cost and the highest affordance. **Distinctiveness belongs in the brand
54
+ layer; conventionality belongs in the control layer.** That is how you get both.
55
+
56
+ ### Consequences
57
+
58
+ - `brignano.io` migration surface: 4 literal hexes in `app/globals.css` plus ~15
59
+ `violet-*` utilities. Map **by job**, not find-and-replace: `.text-primary-color`
60
+ → `--i-ink`, filled buttons → `--i`, focus rings → `--i-ink`.
61
+ - The trail's summit zone (`tsd-signature-experience.md` §5.2, "summit
62
+ indigo/violet") is re-inked with `--mark`.
63
+ - `life` is unaffected — it has no interactive colour today.
64
+
65
+ ---
66
+
67
+ ## ADR-0002 — Charts keep a full categorical palette, re-derived and validated
68
+
69
+ **Status:** accepted · 2026-08-23
70
+
71
+ ### Context
72
+
73
+ An earlier draft suggested the `/coding` chart palette was "undercutting the
74
+ identity" and should derive from the brand accent. **That was wrong**, and it is
75
+ worth recording why.
76
+
77
+ Series colour answers *"which of these eight things am I looking at?"*, which needs
78
+ eight hues a human can separate at a glance. Deriving eight series colours from one
79
+ hue produces eight of that hue nobody can tell apart. **Charts genuinely need the
80
+ range.** The constraint is *separability*, not restraint.
81
+
82
+ ### The actual problem
83
+
84
+ `components/stats/chart-colors.ts` is not too colourful — it is insufficiently
85
+ separable. Validated (light, 10 slots):
86
+
87
+ ```
88
+ [FAIL] Lightness band outside band: #ef9aa3, #34d399
89
+ [WARN] CVD separation worst adjacent #a78bfa <-> #06b6d4 dE 6.6 (deutan)
90
+ [FAIL] Normal-vision floor worst adjacent #ef9aa3 <-> #f97316 dE 13.7
91
+ [WARN] Contrast vs surface 8 of 10 below 3:1
92
+ ```
93
+
94
+ ΔE 13.7 means those two are hard to separate **with full colour vision**, before
95
+ colourblindness is considered.
96
+
97
+ ### Decision
98
+
99
+ Keep a full 8-hue categorical palette, capped at 8 + "Other". Replace the values
100
+ with a set that passes every hard gate in both modes (adjacent CVD ΔE 9.1 light /
101
+ 8.4 dark; normal-vision ΔE 19.6 / 19.3). Values in `tokens.chart.css`.
102
+
103
+ ### Consequences
104
+
105
+ - **Slot order is load-bearing** — the gates measure *adjacent* pairs, so
106
+ re-ordering silently breaks colourblind safety. Fixed order, never cycled.
107
+ - Three light-mode slots sit below 3:1; that is legal only with relief (visible
108
+ labels or a table view). The donut's centre readout and legend already satisfy it.
109
+ - `stats-pie.tsx` already takes the top 8 and aggregates the rest — now the
110
+ documented rule rather than an implementation detail.
111
+ - Colour must follow the **entity**, not its rank. The current code indexes by
112
+ sorted position, so a data shift repaints every series. Map name → slot once.
113
+ - `chart-colors.ts` should read the CSS custom properties rather than literal hex,
114
+ or the two will drift the way everything else did.
115
+
116
+ ---
117
+
118
+ ## ADR-0003 — The system lives in one public repo, consumed as a package
119
+
120
+ **Status:** accepted · 2026-08-23
121
+
122
+ ### Decision
123
+
124
+ A standalone repo — `brignano/design` — installed as `@brignano/design`.
125
+
126
+ ### Rationale
127
+
128
+ **It cannot live in `life`.** `life` is private and holds confirmation codes and
129
+ booking details. `brignano.io` is public. A public site cannot install tokens from
130
+ a private repo without handing out credentials to the repo storing your boarding
131
+ passes. Hard blocker, not preference.
132
+
133
+ **It should not live in `brignano.io`.** That makes the site the owner and every
134
+ tool a downstream guest, pinning tools to the site's release cycle.
135
+
136
+ **Copy-paste is what already failed** — two repos, two stylesheets, two accents,
137
+ two chart palettes, and a documented decision only one of them followed.
138
+
139
+ ### Distribution
140
+
141
+ Three channels, because there are three kinds of consumer:
142
+
143
+ | Consumer | Channel |
144
+ |---|---|
145
+ | npm projects with a build (`brignano.io`, `life`) | git dependency pinned to a tag |
146
+ | no-build surfaces (`homelab`, a static page) | jsDelivr against the public repo |
147
+ | **projects that don't exist yet** | a Claude skill generated from this repo |
148
+
149
+ The third is the one that matters for "as I create new things" — a skill follows
150
+ the account, so a new project gets the system without anyone wiring it up.
151
+
152
+ npm publishing is deferred until `brignano.io` actually consumes it, so the first
153
+ published version isn't one that's immediately superseded. The scope `@brignano` is
154
+ already owned (it is the npm username), so no org setup is needed.
155
+
156
+ ### Consequences
157
+
158
+ - The repo must be **public**: private npm needs a paid plan, jsDelivr only serves
159
+ public repos, and a public `brignano.io` that can't `npm install` is broken.
160
+ - Nothing here is secret — hex values and spacing rules aren't a moat — and the
161
+ documented reasoning is itself a portfolio asset.
162
+ - Consumers pin the **same tag** and bump together. If one tracks `main` and another
163
+ a tag, the drift this repo exists to prevent comes straight back.
164
+ - A token rename is a breaking change.
165
+
166
+ ---
167
+
168
+ ## ADR-0004 — One hue, one meaning
169
+
170
+ **Status:** accepted · 2026-08-23
171
+ **Supersedes:** the "single accent" model in the first draft of this system
172
+
173
+ ### Context
174
+
175
+ The first draft had one accent carrying identity, affordance *and* warning. That is
176
+ what produced a long amber-versus-violet argument, and the argument was unwinnable
177
+ because the question was wrong: no hue can be all three without one meaning bleeding
178
+ into the others.
179
+
180
+ Worse, the founding rule of this system — *colour carries meaning, never
181
+ decoration* — was being violated by the system itself. A hue with two meanings is
182
+ exactly the thing the rule forbids, just relocated.
183
+
184
+ ### Decision
185
+
186
+ Every hue answers exactly one question, on every surface, in both tiers. Two
187
+ specific consequences:
188
+
189
+ **1. Attention is one meaning.** A warning toast, the `NEXT UP` strip, and a
190
+ deposit closing in six days are the same message at different volumes, so they are
191
+ the same colour. There is no separate "warning" hue and no separate "now/next" hue.
192
+ This is what lets a marketing site and an operational tool share one palette
193
+ honestly rather than by coincidence.
194
+
195
+ **2. Identity is not a fill.** `--mark` inks a graphic — the trail, the monogram, a
196
+ hero rule — and nothing else.
197
+
198
+ The intermediate proposal was to keep a `--brand-*` family and manage the overlap
199
+ with amber's warning meaning by convention: brand only at large decorative scale,
200
+ warning always with an icon. That works, and warm-brand companies do it. It was
201
+ rejected because it is enforced by remembering, every time, forever — and rules
202
+ enforced by remembering get forgotten by a version of you in a hurry. Naming is
203
+ cheaper and more reliable enforcement: `--brand` invites someone to tint a button
204
+ with it; `--mark` does not.
205
+
206
+ ### Consequences
207
+
208
+ - `brignano.io` gives up a brand fill for tinting eyebrows and section rules. Those
209
+ go to ink and neutrals — arguably better, since tinted eyebrow rules are the
210
+ portfolio-template tell the modernization TSD set out to escape.
211
+ - Identity now rests on the trail, the monogram, the type, and restraint. If that
212
+ feels thin in place, the fix is a stronger *mark*, not a tinted interface.
213
+ - Feedback state (success/attention/danger) and domain status (booked/active/
214
+ overdue) share hue families deliberately. Context plus the mandatory icon-and-word
215
+ separates them; a form error is not "rust means overdue", but they are the same
216
+ register of message.
217
+ - Nothing amber is clickable anywhere in the system.
package/DESIGN.md ADDED
@@ -0,0 +1,210 @@
1
+ # DESIGN.md — the design system
2
+
3
+ The visual contract for every brignano surface: `brignano.io`, `life`, `homelab`,
4
+ `driftwood`, and whatever comes next. Values live in [`tokens.css`](./tokens.css)
5
+ and [`tokens.chart.css`](./tokens.chart.css); the reasoning is in
6
+ [`DECISIONS.md`](./DECISIONS.md).
7
+
8
+ **Read this before styling anything.** An agent asked to build a page in any of
9
+ these repos should use these tokens, not invent a palette.
10
+
11
+ ---
12
+
13
+ ## 1. The identity in one line
14
+
15
+ Competence and calm. Warm, not cold; confident, not loud. Near-monochrome, with
16
+ colour used so sparingly that when it appears you look at it.
17
+
18
+ Nothing here should read as a developer-portfolio template, and nothing should
19
+ read as a toy.
20
+
21
+ ## 2. Two tiers, one foundation
22
+
23
+ The professional site should be more polished than a private trip planner. That
24
+ is a difference of **density and scale**, not of design system. Colour, type
25
+ family, motion and shape are identical; what changes is how much room things get.
26
+
27
+ | | **Marketing** — `brignano.io` | **Tool** — `life`, `homelab`, `driftwood` |
28
+ |---|---|---|
29
+ | Opt in | `class="tier-marketing"` on `<html>` | default, no class |
30
+ | Body size | 17px | 15px |
31
+ | Section rhythm | `--s9` (96px) | `--s6` (32px) |
32
+ | Measure | 68ch | 780px |
33
+ | Display face | Silkscreen — monogram, hero eyebrows | none (aliases to `--sans`) |
34
+ | Motion | the trail, scroll reveal | none |
35
+ | Density | whitespace carries the layout | scanning beats reading |
36
+
37
+ ## 3. Colour — one hue, one meaning
38
+
39
+ > **Colour carries meaning. Never decoration.**
40
+ > If a colour doesn't tell you something you'd otherwise have to read, it
41
+ > doesn't ship.
42
+
43
+ Every hue answers exactly one question, on every surface, in both tiers:
44
+
45
+ | Token | Answers | Hue |
46
+ |---|---|---|
47
+ | neutral ramp | the page itself | cool grey |
48
+ | `--i-*` | *you can act on this* | cobalt |
49
+ | `--success` | *settled, confirmed, done* | forest |
50
+ | `--attention` | *this wants you* | larch amber |
51
+ | `--danger` | *this has gone wrong* | rust |
52
+ | `--mark` | identity | larch amber, graphic only |
53
+
54
+ On a typical tool-tier screen you should see **one or two** coloured things. If
55
+ you see five, the rule has slipped.
56
+
57
+ ### Attention is one meaning
58
+
59
+ A warning toast, the `NEXT UP` strip, and a deposit closing in six days are the
60
+ same message at different volumes. They get the same colour. There is no separate
61
+ "warning" hue and no separate "now/next" hue — collapsing them is what makes the
62
+ system coherent across a marketing site and an operational tool.
63
+
64
+ ### Identity is not a fill
65
+
66
+ `--mark` inks a **graphic**: the trail line, the `A|B` monogram, a hero rule.
67
+ That is its entire permitted use. It is never a button, chip, badge, wash, border,
68
+ or link.
69
+
70
+ This is the resolution of a real problem. Amber used to be both the brand colour
71
+ and the warning colour, kept apart by a rule about scale — and rules enforced by
72
+ remembering get forgotten. Identity moved to the things that were always carrying
73
+ it (the trail, the monogram, the type, restraint), so amber in UI chrome now only
74
+ ever means *attention*. If you're reaching for `--mark` to tint an interface
75
+ element, the answer is a neutral or a state colour.
76
+
77
+ ### Colour is a surface, not text
78
+
79
+ A wash (12% light / 20% dark via `color-mix`) behind an element with the text kept
80
+ near-ink reads as *a state*. Coloured letters just read as coloured letters and
81
+ fight legibility at small sizes. Use the `-surface` and `-line` tokens for the
82
+ wash and its edge, and `-ink` when a state genuinely must be a label.
83
+
84
+ `--attention` (`#b4741f`) is **3.85:1** on white. It is a mark and fill colour,
85
+ not a text colour. `--attention-ink` (`#8a5714`, 6.08:1) is the text step.
86
+
87
+ ### Never colour alone
88
+
89
+ Every state ships with an **icon and a word**. Colour is the accelerant, not the
90
+ signal — this covers colour-vision deficiency, greyscale printing, and forced-colours
91
+ mode in one rule.
92
+
93
+ ### What does NOT get colour
94
+
95
+ Event type. Six types in six colours is exactly the complication this avoids —
96
+ type is already carried by a text label. Nor `idea`, `planning`, `someday`, or
97
+ `parked`: nothing is being asked of you, so nothing should catch your eye.
98
+
99
+ ## 4. Interaction
100
+
101
+ The part that makes an interface feel built rather than styled.
102
+
103
+ - `--i` fill, `--i-hover`, `--i-pressed`, `--i-ink` for links/icons/focus.
104
+ - **Disabled is neutral, never hued** (`--i-disabled-bg` / `--i-disabled-ink`). A
105
+ greyed-out control must stop signalling "click me".
106
+ - Focus is always visible: 2px `--i-ink` outline, 2px offset. Never remove it.
107
+ - Hover and pressed surfaces on neutral elements come from `--surface-hover` and
108
+ `--surface-active`, not from opacity tricks.
109
+
110
+ ## 5. Type
111
+
112
+ Three roles, no more:
113
+
114
+ - **`--sans` (Geist)** — body and headings, both tiers. Tabular figures (`tnum`) on
115
+ body copy so dates, prices and countdowns align in a column.
116
+ - **`--mono` (IBM Plex Mono)** — data that must read *precise*: confirmation codes,
117
+ timestamps, stat numbers, eyebrow labels.
118
+ - **`--display` (Silkscreen)** — marketing tier only. The `A|B` monogram and hero
119
+ eyebrows. Used **once, well** — it is memorable precisely because it is
120
+ concentrated. On the tool tier it aliases to `--sans`.
121
+
122
+ No serif. Abstract over literal — no mascots, clip-art, or stock scenery.
123
+
124
+ ## 6. Space, shape, elevation
125
+
126
+ 4px base (`--s1` … `--s9`). Radius: `--radius-sm` for chips and pills, `--radius`
127
+ for cards, `--radius-lg` for hero surfaces.
128
+
129
+ **Elevation differs by theme.** Light mode separates surfaces with a hairline plus
130
+ a shadow (`--shadow-1/2/3`). Dark mode does not use borders — surfaces get
131
+ *lighter* with elevation. This is why `--line` can stay nearly invisible in dark.
132
+
133
+ ## 7. Motion
134
+
135
+ **Content is visible by default. Motion is progressive enhancement layered on top
136
+ — never the thing that makes content appear.**
137
+
138
+ This is a hard contract. `brignano.io` shipped a homepage whose contact CTA
139
+ rendered at `opacity: 0` on **100% of loads**, because an animation library owned
140
+ the visible state and computed its scroll offsets before a client-side widget grew
141
+ the page. Any animation system that can leave content hidden when JS fails, races,
142
+ or is disabled is disqualified.
143
+
144
+ `prefers-reduced-motion` renders the final state immediately, with no transforms —
145
+ and that block ships inside `tokens.css` so no surface can forget it. One easing
146
+ curve (`--ease`), three durations, compositor-only properties.
147
+
148
+ ## 8. Charts
149
+
150
+ The one place the restraint rule does **not** apply: a categorical chart needs
151
+ enough distinct hues to tell series apart, and eight ambers help nobody. Full
152
+ rationale in [`tokens.chart.css`](./tokens.chart.css). The short version:
153
+
154
+ - Series colours are `--chart-1…8`, **assigned in fixed order, never cycled**.
155
+ - Colour follows the entity, never its rank — a filter must not repaint survivors.
156
+ - A 9th series folds into `--chart-other`, facets, or gets cut.
157
+ - State colours are reserved and never become "series 4".
158
+ - ≥2 series always carries a legend; ≤4 are also direct-labelled.
159
+ - Text wears text tokens, never the series colour.
160
+ - **Never a dual-axis chart.** Two measures of different scale → two charts.
161
+
162
+ ## 9. Accessibility floor
163
+
164
+ Non-negotiable in both themes:
165
+
166
+ - Body text ≥ 4.5:1; large text and UI marks ≥ 3:1.
167
+ - Touch targets ≥ 44px on coarse pointers, with a `.touch-hit` opt-out for controls
168
+ whose shape is meaningful (a 22px circular checkbox forced to 44px becomes an
169
+ oval — it expands its hit area instead).
170
+ - Every state carries an icon and a word.
171
+ - Measured ratios sit next to their values in `tokens.css`. Changing a value means
172
+ re-measuring it.
173
+
174
+ ## 10. Integrating with a Tailwind consumer
175
+
176
+ Tailwind's dark variant keys off a `.dark` class; these tokens key off
177
+ `[data-theme]`. **Both must be set together**, by the same code, in both places
178
+ that change the theme (the pre-paint script and the toggle):
179
+
180
+ ```js
181
+ root.classList.toggle("dark", d);
182
+ root.setAttribute("data-theme", d ? "dark" : "light");
183
+ ```
184
+
185
+ Setting only the class is a silent, half-broken state: Tailwind utilities flip
186
+ but the tokens do not, and the page renders one theme's tokens under the other
187
+ theme's utilities. Setting only the attribute leaves every `dark:` utility dead.
188
+
189
+ The reason the attribute is required rather than relying on
190
+ `prefers-color-scheme`: a site with a manual toggle has already *resolved* the
191
+ OS preference into an explicit choice. If the tokens also consulted the media
192
+ query, a user on a dark OS who chose light would get dark tokens under light
193
+ utilities. `[data-theme]` is what makes the choice authoritative.
194
+
195
+ Import order matters — Tailwind first, then tokens, then the bridge:
196
+
197
+ ```css
198
+ @import "tailwindcss";
199
+ @import "@brignano/design/tokens.css";
200
+ @import "@brignano/design/tailwind.css";
201
+ ```
202
+
203
+ ## 11. The one rule that keeps this true
204
+
205
+ **Never hardcode a hex outside `tokens.css`.**
206
+
207
+ Hardcoded values silently survive a theme change. `life`'s status pills carried
208
+ literal greys for months and would have stayed light grey in dark mode.
209
+ `brignano.io` carried four literal violet hexes plus ~15 `violet-*` utilities,
210
+ which is why changing the accent was a migration rather than a one-line edit.
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # @brignano/design
2
+
3
+ The shared design system for every brignano surface — `brignano.io`, `life`,
4
+ `homelab`, `driftwood`, and whatever comes next.
5
+
6
+ | File | What it is |
7
+ |---|---|
8
+ | [`DESIGN.md`](./DESIGN.md) | **Start here.** The rules — the three colour layers, the two tiers, type, motion. |
9
+ | [`DECISIONS.md`](./DECISIONS.md) | Why the values are what they are. ADRs. |
10
+ | [`tokens.css`](./tokens.css) | Single source of truth for colour, type, space, shape, motion. |
11
+ | [`tokens.chart.css`](./tokens.chart.css) | Validated categorical palette for data viz. |
12
+
13
+ ## The shape of it
14
+
15
+ Three layers, so no colour does two jobs:
16
+
17
+ - **`--brand-*`** — identity. Larch amber. The trail, the monogram. Never a control.
18
+ - **`--i-*`** — affordance. Cobalt. Links, buttons, focus, selection. Semantically inert.
19
+ - **`--ok / --warn / --err / --info`** — state. Conventional hues, always with an icon and a word.
20
+
21
+ Distinctiveness lives in the brand layer; conventionality lives in the control layer.
22
+
23
+ ## Consuming it
24
+
25
+ ```jsonc
26
+ "dependencies": { "@brignano/design": "github:brignano/design#v1.0.0" }
27
+ ```
28
+
29
+ ```css
30
+ @import '@brignano/design/tokens.css';
31
+ @import '@brignano/design/tokens.chart.css'; /* only if the surface has charts */
32
+ ```
33
+
34
+ The tool tier is the default. A marketing surface opts in with
35
+ `class="tier-marketing"` on `<html>`, which unlocks the display face and opens the
36
+ type scale. Colour is identical across tiers.
37
+
38
+ ## For agents
39
+
40
+ Read `DESIGN.md` before styling anything. Use the tokens. **Never hardcode a hex
41
+ outside `tokens.css`** — a literal survives a theme change silently, which is how
42
+ `life`'s status pills sat light grey in dark mode for months.
43
+
44
+ ## Status
45
+
46
+ Pre-1.0. Not yet published to npm — install from the git tag above. Publishing
47
+ `v1.0.0` waits until `brignano.io` is actually consuming it, so the first published
48
+ version isn't one that's immediately superseded.
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@brignano/design",
3
+ "version": "0.1.0",
4
+ "description": "Shared design system for brignano surfaces \u2014 colour, type, space, shape, motion.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/brignano/design.git"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public",
12
+ "provenance": true
13
+ },
14
+ "files": [
15
+ "tokens.css",
16
+ "tokens.chart.css",
17
+ "tailwind.css",
18
+ "DESIGN.md",
19
+ "DECISIONS.md"
20
+ ],
21
+ "exports": {
22
+ "./tokens.css": "./tokens.css",
23
+ "./tokens.chart.css": "./tokens.chart.css",
24
+ "./tailwind.css": "./tailwind.css"
25
+ },
26
+ "sideEffects": [
27
+ "*.css"
28
+ ]
29
+ }
package/tailwind.css ADDED
@@ -0,0 +1,55 @@
1
+ /* Tailwind v4 bridge for @brignano/design.
2
+ Import AFTER tokens.css and after `@import "tailwindcss"`:
3
+
4
+ @import "tailwindcss";
5
+ @import "@brignano/design/tokens.css";
6
+ @import "@brignano/design/tailwind.css";
7
+
8
+ `@theme inline` is load-bearing: it makes the generated utilities emit
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. */
11
+ @theme inline {
12
+ --color-ink: var(--ink);
13
+ --color-ink-soft: var(--ink-soft);
14
+ --color-slate: var(--slate);
15
+ --color-bg: var(--bg);
16
+ --color-card: var(--card);
17
+ --color-line: var(--line);
18
+ --color-line-strong: var(--line-strong);
19
+ --color-surface-hover: var(--surface-hover);
20
+ --color-surface-active: var(--surface-active);
21
+
22
+ --color-interactive: var(--i);
23
+ --color-interactive-ink: var(--i-ink);
24
+ --color-interactive-hover: var(--i-hover);
25
+ --color-interactive-pressed: var(--i-pressed);
26
+ --color-interactive-surface: var(--i-surface);
27
+ --color-on-interactive: var(--on-i);
28
+
29
+ --color-success: var(--success);
30
+ --color-success-ink: var(--success-ink);
31
+ --color-success-surface: var(--success-surface);
32
+ --color-attention: var(--attention);
33
+ --color-attention-ink: var(--attention-ink);
34
+ --color-attention-surface: var(--attention-surface);
35
+ --color-danger: var(--danger);
36
+ --color-danger-ink: var(--danger-ink);
37
+ --color-danger-surface: var(--danger-surface);
38
+
39
+ --color-mark: var(--mark);
40
+ --color-mark-ink: var(--mark-ink);
41
+
42
+ --font-sans: var(--sans);
43
+ --font-mono: var(--mono);
44
+ --font-display: var(--display);
45
+
46
+ --radius-sm: var(--radius-sm);
47
+ --radius-DEFAULT: var(--radius);
48
+ --radius-lg: var(--radius-lg);
49
+
50
+ --shadow-1: var(--shadow-1);
51
+ --shadow-2: var(--shadow-2);
52
+ --shadow-3: var(--shadow-3);
53
+
54
+ --ease-brand: var(--ease);
55
+ }
@@ -0,0 +1,134 @@
1
+ /* ─────────────────────────────────────────────────────────────────────────
2
+ tokens.chart.css — the categorical palette for data visualisation.
3
+
4
+ WHY THIS IS A SEPARATE FILE FROM THE BRAND ACCENT
5
+ -------------------------------------------------
6
+ A chart's colours do a different job from a brand's colours, so they are
7
+ not derived from the accent and they are not the status ramp.
8
+
9
+ brand accent (--accent) identity. One hue. "This is Anthony."
10
+ status (--forest/rust) state. RESERVED — never a series colour.
11
+ categorical (this file) IDENTITY OF A SERIES. Needs 8 hues that a
12
+ human can tell apart at a glance, including
13
+ the ~8% of men with a colour-vision
14
+ deficiency.
15
+
16
+ Trying to derive eight distinguishable series hues from one amber gives
17
+ you eight ambers nobody can separate. Charts genuinely need the range —
18
+ the constraint is separability, not restraint.
19
+
20
+ THESE VALUES ARE VALIDATED, NOT CHOSEN BY EYE
21
+ ----------------------------------------------
22
+ Both modes pass every hard gate of the six-check colour formula:
23
+
24
+ lightness band PASS (all 8 inside the mode's band)
25
+ chroma floor PASS (all 8 >= 0.1)
26
+ CVD separation PASS worst adjacent dE 9.1 light / 8.4 dark (>=8)
27
+ normal-vision PASS worst adjacent dE 19.6 light / 19.3 dark (>=15)
28
+ contrast light: slots 3, 4, 5 sit below 3:1 — see RELIEF below
29
+ dark: all 8 clear 3:1
30
+
31
+ The ORDER is the colourblind-safety mechanism, not decoration. Slots are
32
+ assigned in fixed order and NEVER cycled. Re-ordering silently breaks the
33
+ adjacent-pair separation the gates above measure. If you change anything
34
+ here, re-run the validator on both modes before shipping.
35
+
36
+ RULES OF USE
37
+ ------------
38
+ - Fixed order, never cycled. A 9th series is not a generated hue: fold it
39
+ into "Other", facet into small multiples, or cut it. (`stats-pie.tsx`
40
+ already does this correctly — top 8 + "Other".)
41
+ - Colour follows the ENTITY, never its rank. A filter that changes the
42
+ series count must not repaint the survivors. Map name -> slot once.
43
+ - RELIEF: on light mode, slots 3/4/5 fall below 3:1 against the surface.
44
+ That is legal only with visible direct labels or a table view — which
45
+ the donut's centre readout and the legend already provide. Do not use
46
+ those three as the ONLY signal for a mark with no label.
47
+ - Never colour-alone: >=2 series always carries a legend, and <=4 are also
48
+ direct-labelled.
49
+ - Text wears text tokens (--ink / --ink-soft / --slate), never the series
50
+ colour. A coloured mark beside a label carries the identity.
51
+ ───────────────────────────────────────────────────────────────────────── */
52
+
53
+ :root {
54
+ --chart-surface: var(--card);
55
+
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 */
64
+
65
+ /* Everything past slot 8 aggregates here. Deliberately neutral: "Other"
66
+ is not an entity, so it does not earn a hue. */
67
+ --chart-other: var(--slate);
68
+
69
+ /* Axes, gridlines and the ring behind overlapping marks. Recessive by
70
+ design — the data is the ink, the frame is not. */
71
+ --chart-grid: var(--line);
72
+ --chart-axis: var(--slate);
73
+
74
+ /* ─────────────────────────────────────────────────────────────────────────
75
+ SEQUENTIAL — one hue, light to dark, for CONTINUOUS MAGNITUDE.
76
+ Not categorical. Use when the thing being coloured is "how much", not
77
+ "which one": the GitHub contribution heatmap, a density map, a calendar.
78
+
79
+ Rules that differ from the categorical palette above:
80
+ - ONE hue. Never a rainbow — a rainbow implies categories where there
81
+ are only degrees.
82
+ - Lightness must be MONOTONIC. Verified: light L 0.838 -> 0.107
83
+ descending, dark L 0.015 -> 0.532 ascending, every adjacent step
84
+ separated by dL >= 0.037.
85
+ - `--seq-0` is NEUTRAL on purpose. Zero contributions is not a small
86
+ amount of the hue, it is the absence of a value — colouring it a pale
87
+ blue would claim data that isn't there.
88
+
89
+ The hue is cobalt, the same family as --i. That is intentional: a
90
+ magnitude ramp is informational, the same register as a link, and giving
91
+ it its own hue would add a meaning the system doesn't need.
92
+ ───────────────────────────────────────────────────────────────────────── */
93
+ --seq-0: var(--line);
94
+ --seq-1: #c7dcfb;
95
+ --seq-2: #8ab4f4;
96
+ --seq-3: #3b82f6;
97
+ --seq-4: #1d4ed8;
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;
134
+ }
package/tokens.css ADDED
@@ -0,0 +1,380 @@
1
+ /* ─────────────────────────────────────────────────────────────────────────
2
+ tokens.css — the shared foundation for every brignano surface.
3
+
4
+ Consumers:
5
+ brignano.io (Next / Tailwind v4) — marketing tier
6
+ life (Astro) — tool tier
7
+ homelab, driftwood, future tools — tool tier
8
+
9
+ ── ONE HUE, ONE MEANING ───────────────────────────────────────────────
10
+ Every colour here answers exactly one question, everywhere it appears,
11
+ on every surface. That is the whole design of this file.
12
+
13
+ NEUTRAL the page. Most of every screen.
14
+ --i-* "you can act on this" cobalt
15
+ --success "settled, confirmed, done" forest
16
+ --attention "this wants you" larch amber
17
+ --danger "this has gone wrong" rust
18
+ --mark identity — a drawn mark, not a UI colour
19
+
20
+ Two consequences worth stating out loud, because both were mistakes in
21
+ earlier drafts of this system:
22
+
23
+ 1. ATTENTION IS ONE MEANING. A warning toast, the NEXT UP strip, and a
24
+ deposit closing in six days are the same message at different
25
+ volumes, so they are the same colour. There is no separate "warning"
26
+ hue and no separate "now/next" hue.
27
+
28
+ 2. IDENTITY IS NOT A FILL. Amber used to be the brand colour AND the
29
+ warning colour, managed by a rule about scale. Rules enforced by
30
+ remembering get forgotten. So identity moved to the thing that was
31
+ always carrying it — the trail, the monogram, the type — and --mark
32
+ exists only to ink a graphic. It is never a button, chip, wash, or
33
+ border. The naming is the enforcement.
34
+
35
+ ── RULES THAT MAKE THIS FILE TRUSTWORTHY ──────────────────────────────
36
+ 1. NEVER hardcode a hex outside this file. A literal survives a theme
37
+ change silently — that is how `life`'s status pills sat light grey in
38
+ dark mode for months.
39
+ 2. Every colour is declared in BOTH theme scopes. CSS cannot share a
40
+ declaration block between a media query and a plain selector, so the
41
+ dark list is duplicated on purpose. Edit one, edit the other.
42
+ 3. Ratios in the comments are MEASURED. Change a value, re-measure it.
43
+
44
+ See DESIGN.md for the rules that govern use, DECISIONS.md for why.
45
+ ───────────────────────────────────────────────────────────────────────── */
46
+
47
+ :root {
48
+ color-scheme: light;
49
+
50
+ /* ── Neutral ramp ─────────────────────────────────────────────────────
51
+ Twelve steps, up from five. The gap that mattered most: there was no
52
+ hover or pressed background token, so every interactive surface in
53
+ both repos was improvising.
54
+
55
+ Deliberately COOL-leaning — a cool grey lets the warm mark and the
56
+ amber attention state read as warm, instead of muddying into them. */
57
+ --n-0: #ffffff;
58
+ --n-25: #fafafa;
59
+ --n-50: #f3f3f1;
60
+ --n-100: #ececea; /* hairline */
61
+ --n-200: #e2e2e0; /* hover surface */
62
+ --n-300: #cbcbce; /* strong border, pressed surface */
63
+ --n-400: #a9a9ae;
64
+ --n-500: #8a8a8f; /* muted label 3.44:1 — large text / UI marks only */
65
+ --n-600: #6b6b73; /* 5.28:1 */
66
+ --n-700: #52525b; /* secondary 7.73:1 */
67
+ --n-800: #33333a;
68
+ --n-900: #111111; /* primary 18.88:1 */
69
+
70
+ /* Semantic aliases. Components use THESE, not the raw steps. */
71
+ --bg: var(--n-25);
72
+ --card: var(--n-0);
73
+ --surface-hover: var(--n-100);
74
+ --surface-active: var(--n-200);
75
+ --ink: var(--n-900);
76
+ --ink-soft: var(--n-700);
77
+ --slate: var(--n-500);
78
+ --line: var(--n-100);
79
+ --line-soft: var(--n-50);
80
+ --line-strong: var(--n-300);
81
+ --on-dark: var(--n-25);
82
+
83
+ /* ── Interactive — cobalt ─────────────────────────────────────────────
84
+ Everything you can act on: links, buttons, focus, selection. Chosen to
85
+ be semantically INERT — it carries no inherited meaning the way amber,
86
+ green and red do — and complementary to the mark hue, so identity and
87
+ affordance never compete for the same attention.
88
+
89
+ --i fill 5.17:1 (white label 5.17:1)
90
+ --i-ink link/icon/focus 6.70:1 on #fff
91
+ --i-hover hover fill 10.36:1
92
+ --i-pressed pressed fill 12.65:1
93
+
94
+ DISABLED IS NEUTRAL, NEVER HUED — a greyed-out control must stop
95
+ signalling "click me". */
96
+ --i: #2563eb;
97
+ --i-ink: #1d4ed8;
98
+ --i-hover: #1e40af;
99
+ --i-pressed: #1e3a8a;
100
+ --i-surface: color-mix(in srgb, var(--i) 12%, transparent);
101
+ --i-line: color-mix(in srgb, var(--i) 32%, transparent);
102
+ --i-selection: color-mix(in srgb, var(--i) 26%, transparent);
103
+ --on-i: #ffffff;
104
+ --i-disabled-bg: var(--n-200);
105
+ --i-disabled-ink: var(--n-400);
106
+
107
+ /* ── State ────────────────────────────────────────────────────────────
108
+ Three hues, one meaning each, identical on every surface and both
109
+ tiers. Always shipped with an ICON AND A WORD — colour is never the
110
+ only signal.
111
+
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
115
+
116
+ `--info` is the interactive hue on purpose: an informational message
117
+ is the same "neutral, no action implied" register as a link.
118
+
119
+ `-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
+ --success: #2f6b4f;
122
+ --success-ink: #245740;
123
+ --attention: #b4741f; /* 3.85:1 — mark/fill only, NOT text */
124
+ --attention-ink: #8a5714; /* 6.08:1 */
125
+ --danger: #a4442c;
126
+ --danger-ink: #8a3722;
127
+ --info: var(--i);
128
+ --info-ink: var(--i-ink);
129
+
130
+ --success-surface: color-mix(in srgb, var(--success) 12%, transparent);
131
+ --attention-surface: color-mix(in srgb, var(--attention) 12%, transparent);
132
+ --danger-surface: color-mix(in srgb, var(--danger) 12%, transparent);
133
+ --info-surface: var(--i-surface);
134
+ --success-line: color-mix(in srgb, var(--success) 32%, transparent);
135
+ --attention-line: color-mix(in srgb, var(--attention) 32%, transparent);
136
+ --danger-line: color-mix(in srgb, var(--danger) 32%, transparent);
137
+ --info-line: var(--i-line);
138
+
139
+ /* ── Mark — identity ──────────────────────────────────────────────────
140
+ Inks a GRAPHIC: the trail line, the A|B monogram, a hero rule. That is
141
+ the entire permitted use.
142
+
143
+ It is NOT a button, chip, badge, wash, border, or link. If you are
144
+ reaching for it to tint a UI element, the answer is a neutral or a
145
+ state colour — see DESIGN.md §3.
146
+
147
+ It shares larch amber with --attention deliberately: the identity and
148
+ the attention state are the same warmth, and because --mark never
149
+ appears as a UI surface the two can never be mistaken for each other
150
+ in the same context. */
151
+ --mark: #b4741f;
152
+ --mark-ink: #8a5714; /* when the mark carries a label, 6.08:1 */
153
+
154
+ /* ── Legacy aliases ───────────────────────────────────────────────────
155
+ Kept so existing `life` components keep compiling. Do not use in new
156
+ work — reach for the semantic tokens above.
157
+
158
+ --larch-deep is declared explicitly in BOTH themes rather than
159
+ aliased, because "deep" inverts: darker in light, BRIGHTER in dark. An
160
+ alias to either role silently changes one theme — it moved
161
+ .pill-active from #f0bd74 to #e0a44f before this was caught. */
162
+ --forest: var(--success);
163
+ --forest-deep: var(--success-ink);
164
+ --larch: var(--attention);
165
+ --larch-deep: #8a5714;
166
+ --rust: var(--danger);
167
+ --rust-deep: var(--danger-ink);
168
+ --gold: var(--slate);
169
+
170
+ /* ── Type ─────────────────────────────────────────────────────────────
171
+ --sans Geist — body and headings, both tiers
172
+ --mono IBM Plex Mono — data that must read precise
173
+ --display Silkscreen — MARKETING TIER ONLY (monogram, hero eyebrows)
174
+
175
+ NOTE: Geist is not yet installed in `life` — only brignano.io
176
+ self-hosts it. Until added, the stack falls through to Inter and that
177
+ repo renders as before. Adding it is a deliberate separate change. */
178
+ --sans: 'Geist', 'Inter Variable', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
179
+ --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, monospace;
180
+ --display: var(--sans);
181
+
182
+ /* ── Space ── 4px base. Tiers differ by which steps they reach for. */
183
+ --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px;
184
+ --s6: 32px; --s7: 48px; --s8: 64px; --s9: 96px;
185
+
186
+ /* ── Shape & elevation ────────────────────────────────────────────────
187
+ In LIGHT these are shadows; in DARK the surface lightens instead. */
188
+ --radius-sm: 6px;
189
+ --radius: 12px;
190
+ --radius-lg: 18px;
191
+ --shadow-1: 0 1px 2px rgba(17, 17, 17, 0.04);
192
+ --shadow-2: 0 2px 8px rgba(17, 17, 17, 0.07);
193
+ --shadow-3: 0 8px 24px rgba(17, 17, 17, 0.10);
194
+ --scrim: rgba(17, 17, 17, 0.55);
195
+ --shadow: var(--shadow-1); /* legacy alias */
196
+
197
+ /* ── Motion ───────────────────────────────────────────────────────────
198
+ Content is ALWAYS visible by default; motion is enhancement layered on
199
+ top, never the thing that makes content appear. brignano.io shipped a
200
+ homepage whose contact CTA was invisible on 100% of loads because an
201
+ animation library owned the visible state. See DESIGN.md §6. */
202
+ --ease: cubic-bezier(0.22, 1, 0.36, 1);
203
+ --dur-fast: 150ms;
204
+ --dur: 250ms;
205
+ --dur-slow: 600ms;
206
+ }
207
+
208
+ /* ── Dark ───────────────────────────────────────────────────────────────
209
+ Selected, not inverted:
210
+ 1. Not pure black. #0d0d0f — pure black behind bright text causes
211
+ halation and is fatiguing at night, which is when a personal tool
212
+ actually gets opened.
213
+ 2. Surfaces LIGHTEN with elevation instead of gaining borders.
214
+ 3. Accents lift in luminance rather than shifting hue, and washes carry
215
+ a higher mix — 12% over #0d0d0f is invisible where it reads on white.
216
+
217
+ Declared twice: the media query is the default and no-JS fallback;
218
+ [data-theme] is the in-app toggle and must WIN over the system.
219
+ `:not([data-theme='light'])` lets the toggle force light under a dark OS. */
220
+ @media (prefers-color-scheme: dark) {
221
+ :root:not([data-theme='light']) {
222
+ color-scheme: dark;
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);
332
+ }
333
+
334
+ /* ── Marketing tier ─────────────────────────────────────────────────────
335
+ `class="tier-marketing"` on <html> (brignano.io). Unlocks the display
336
+ face and opens the type scale. COLOUR IS IDENTICAL ACROSS TIERS — that
337
+ is the point of this file. Tool tier is the default. */
338
+ .tier-marketing {
339
+ --display: 'Silkscreen', var(--mono);
340
+ --text-xs: 0.75rem;
341
+ --text-sm: 0.875rem;
342
+ --text-base: 1.0625rem;
343
+ --text-lg: 1.25rem;
344
+ --text-xl: 1.5rem;
345
+ --text-2xl: 2rem;
346
+ --text-3xl: 2.75rem;
347
+ --text-4xl: clamp(2.75rem, 6vw, 4rem);
348
+ --section-gap: var(--s9);
349
+ --measure: 68ch;
350
+ }
351
+
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
+ ::selection {
367
+ background: var(--i-selection);
368
+ }
369
+
370
+ /* Ships here so no surface can forget it. */
371
+ @media (prefers-reduced-motion: reduce) {
372
+ *,
373
+ *::before,
374
+ *::after {
375
+ animation-duration: 0.01ms !important;
376
+ animation-iteration-count: 1 !important;
377
+ transition-duration: 0.01ms !important;
378
+ scroll-behavior: auto !important;
379
+ }
380
+ }