@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 +217 -0
- package/DESIGN.md +210 -0
- package/README.md +48 -0
- package/package.json +29 -0
- package/tailwind.css +55 -0
- package/tokens.chart.css +134 -0
- package/tokens.css +380 -0
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
|
+
}
|
package/tokens.chart.css
ADDED
|
@@ -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
|
+
}
|