@brignano/design 0.3.0 → 0.4.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 CHANGED
@@ -299,7 +299,12 @@ other theme.
299
299
  needs a colour resolves it through `getComputedStyle` on an element
300
300
  (DESIGN.md §11). ADR-0002's advice that `chart-colors.ts` read the custom
301
301
  properties still stands, but by that route.
302
- - Consumers shouldn't wrap tokens in `color-mix()`; washes are tokens.
302
+ - Consumers shouldn't wrap tokens in `color-mix()`; washes are tokens. The
303
+ first consumer that needed one (`life`'s sticky bars) got `--surface-sticky`
304
+ in 0.3.1 rather than a local mix.
305
+ - A build that lowers `light-dark()` for older browsers (Lightning CSS, inside
306
+ Next.js and Vite) fixes the theme at `:root`, losing scoped theming. Found in
307
+ `brignano.io`; consumers set `browserslist` to the floor (DESIGN.md §11).
303
308
  - Not a token rename and not a visual change, so a patch release.
304
309
 
305
310
 
package/DESIGN.md CHANGED
@@ -135,6 +135,10 @@ for cards, `--radius-lg` for hero surfaces.
135
135
  a shadow (`--shadow-1/2/3`). Dark mode does not use borders — surfaces get
136
136
  *lighter* with elevation. This is why `--line` can stay nearly invisible in dark.
137
137
 
138
+ **Sticky chrome** — a header or toolbar that content scrolls under — is
139
+ `--surface-sticky` with `backdrop-filter: blur(8px)`: 92% of the card in light,
140
+ of the page ground in dark, so it reads as the surface it sits on, not a new layer.
141
+
138
142
  ## 7. Motion
139
143
 
140
144
  **Content is visible by default. Motion is progressive enhancement layered on top
@@ -241,11 +245,30 @@ Two consequences for code that touches tokens directly:
241
245
  Re-read it when the theme changes.
242
246
  - **Don't wrap a token in `color-mix()` or `light-dark()`.** Nesting is valid CSS,
243
247
  but not every engine has been verified to resolve it. If you need a wash, it's
244
- a token (`--*-surface`, `--*-line`); if it isn't, add it here.
248
+ a token (`--*-surface`, `--*-line`, `--surface-sticky`); if it isn't, add it here.
245
249
 
246
250
  Needs Chrome/Edge 123, Firefox 120, Safari 17.5 or later (all shipped by May 2024).
247
251
  An older browser drops every colour token and renders unstyled rather than wrong.
248
252
 
253
+ ### Build tools that lower `light-dark()`
254
+
255
+ A CSS pipeline that targets browsers older than that floor rewrites
256
+ `light-dark()` into a fallback. Lightning CSS (inside Next.js, Vite, Parcel)
257
+ turns each token into `var(--lightningcss-light, …) var(--lightningcss-dark, …)`
258
+ and flips those two variables wherever `color-scheme` changes. The tokens
259
+ resolve on `:root`, so the side is fixed there. Page-wide theming still works,
260
+ but **`data-theme` on an inner element stops re-theming that section.**
261
+
262
+ The fix is to target the same floor the tokens need, which leaves `light-dark()`
263
+ alone. In the consumer's `package.json`:
264
+
265
+ ```json
266
+ "browserslist": ["chrome >= 123", "edge >= 123", "firefox >= 120", "safari >= 17.5"]
267
+ ```
268
+
269
+ Verified with Lightning CSS: older targets lower the tokens; these targets
270
+ pass `light-dark(#076bd3, #6ba5f3)` through untouched.
271
+
249
272
  ### Tailwind
250
273
 
251
274
  Tailwind's dark variant keys off a `.dark` class; these tokens key off
@@ -283,3 +306,47 @@ Hardcoded values silently survive a theme change. `life`'s status pills carried
283
306
  literal greys for months and would have stayed light grey in dark mode.
284
307
  `brignano.io` carried four literal violet hexes plus ~15 `violet-*` utilities,
285
308
  which is why changing the accent was a migration rather than a one-line edit.
309
+
310
+ ## 13. Product pages
311
+
312
+ A product's marketing page is one argument and one ask. `product-page.css` is its
313
+ structure; `previews/product-page.html` is the skeleton to copy. Import it after
314
+ the tokens, put `tier-marketing` on `<html>` and `pp` on `<body>` (or a wrapper).
315
+
316
+ ```css
317
+ @import '@brignano/design/tokens.css';
318
+ @import '@brignano/design/product-page.css';
319
+ ```
320
+
321
+ The order is fixed, because each band answers the question the last one raised:
322
+
323
+ 1. **Header** (`.pp-header`) — brand, two or three in-page links, the status, the
324
+ primary action. Sticky, on `--surface-sticky`.
325
+ 2. **Hero** (`.pp-hero`) — the status, a one-line **thesis** (`.pp-thesis`), a
326
+ subhead of one or two sentences, the primary action and one secondary, and a
327
+ **proof slot** (`.pp-media`). The proof is the product itself where it can be
328
+ (a live demo), else a real screenshot. Never stock imagery.
329
+ 3. **Story** (`ol.pp-story` of `.pp-chapter`) — three or four numbered chapters,
330
+ usually *the problem*, *how it works*, *who it's for*, *why now*. Each is a mono
331
+ label (`.pp-label`, numbered `01 /` by the list, so reordering renumbers), a
332
+ headline that is a sentence, a short body, and optionally `.pp-points` (two to
333
+ four ruled columns) or a `.pp-figure`.
334
+ 4. **Close** (`.pp-close`) — the primary action again, with the room a form needs
335
+ (`.pp-panel`).
336
+ 5. **Footer** (`.pp-footer`).
337
+
338
+ Rules:
339
+
340
+ - **One primary action per page.** `.pp-button` is that action, everywhere it
341
+ appears. Anything else is `.pp-button.is-secondary` or a link.
342
+ - **The status is a word with a dot** (`.pp-status`): "Pilot open", "In
343
+ development". `data-state="open"` takes `--success` (you can join now),
344
+ `"soon"` takes `--attention`; no attribute stays neutral. The word carries the
345
+ meaning on its own (§3, never colour alone).
346
+ - **Headlines are sentences.** Calm and declarative, no exclamation marks, no
347
+ claims the page can't back up.
348
+ - **Restraint carries the style.** Hairlines between chapters, whitespace, the
349
+ type scale. No gradients, no tinted section backgrounds, no decorative colour.
350
+
351
+ Every class starts with `.pp-`, so the file sits beside a site's own stylesheet.
352
+ A page can change `--pp-width` (default 1160px) and `--pp-gutter` on `.pp`.
package/README.md CHANGED
@@ -9,6 +9,7 @@ The shared design system for every brignano surface — `brignano.io`, `life`,
9
9
  | [`DECISIONS.md`](./DECISIONS.md) | Why the values are what they are. ADRs. |
10
10
  | [`tokens.css`](./tokens.css) | Single source of truth for colour, type, space, shape, motion. |
11
11
  | [`tokens.chart.css`](./tokens.chart.css) | Validated categorical palette for data viz. |
12
+ | [`product-page.css`](./product-page.css) | The structure of a product's marketing page: header, thesis hero, numbered story, closing ask. DESIGN.md §13; skeleton in `previews/product-page.html`. |
12
13
  | [`previews/`](./previews) | Live swatches. Open one in a browser to see the tokens render. |
13
14
 
14
15
  ## The shape of it
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@brignano/design",
3
- "version": "0.3.0",
4
- "description": "Shared design system for brignano surfaces — colour, type, space, shape, motion.",
3
+ "version": "0.4.0",
4
+ "description": "Shared design system for brignano surfaces \u2014 colour, type, space, shape, motion.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -17,6 +17,7 @@
17
17
  "files": [
18
18
  "tokens.css",
19
19
  "tokens.chart.css",
20
+ "product-page.css",
20
21
  "tailwind.css",
21
22
  "DESIGN.md",
22
23
  "DECISIONS.md"
@@ -24,7 +25,8 @@
24
25
  "exports": {
25
26
  "./tokens.css": "./tokens.css",
26
27
  "./tokens.chart.css": "./tokens.chart.css",
27
- "./tailwind.css": "./tailwind.css"
28
+ "./tailwind.css": "./tailwind.css",
29
+ "./product-page.css": "./product-page.css"
28
30
  },
29
31
  "sideEffects": [
30
32
  "*.css"
@@ -0,0 +1,172 @@
1
+ /* product-page.css — the page structure for a product's marketing site.
2
+ *
3
+ * @import '@brignano/design/tokens.css';
4
+ * @import '@brignano/design/product-page.css';
5
+ * <html class="tier-marketing">
6
+ *
7
+ * One page, read top to bottom, that makes one argument and asks for one thing:
8
+ *
9
+ * .pp-header brand, a few in-page links, the status, the primary action
10
+ * .pp-hero status, a one-line thesis, a subhead, two actions, a proof slot
11
+ * .pp-story 3–4 numbered chapters: problem, how it works, who it's for, why now
12
+ * .pp-close the primary action again, with whatever it needs (a form)
13
+ * .pp-footer
14
+ *
15
+ * The skeleton is previews/product-page.html: copy it, keep the order, replace
16
+ * the words. Rules (DESIGN.md §13):
17
+ *
18
+ * - One primary action per page (.pp-button). Everything else is secondary.
19
+ * - The status is a word with a dot, never a coloured badge on its own.
20
+ * - The proof slot is the product itself where possible (a live demo), else a
21
+ * real screenshot. No stock imagery, no illustrations of people.
22
+ * - Chapters are numbered by the list, so reordering them renumbers them.
23
+ *
24
+ * Every class starts with .pp- so it can sit beside a site's own stylesheet.
25
+ * Colours are tokens only; widths are --pp-width / --pp-gutter, which a page
26
+ * may override on .pp. */
27
+
28
+ .pp {
29
+ --pp-width: 1160px;
30
+ --pp-gutter: var(--s5);
31
+ background: var(--bg);
32
+ color: var(--ink);
33
+ font: var(--text-base) / 1.6 var(--sans);
34
+ font-variant-numeric: tabular-nums;
35
+ -webkit-font-smoothing: antialiased;
36
+ }
37
+ @media (max-width: 560px) { .pp { --pp-gutter: var(--s4); } }
38
+
39
+ /* The content column every band lines up on. */
40
+ .pp-wrap { max-width: var(--pp-width); margin: 0 auto; padding: 0 var(--pp-gutter); }
41
+
42
+ /* ── Skip link ─────────────────────────────────────────────────────────── */
43
+ .pp-skip { position: absolute; left: var(--s4); top: -100px; z-index: 20; padding: var(--s2) var(--s3);
44
+ background: var(--card); color: var(--ink); border-radius: var(--radius-sm); box-shadow: var(--shadow-2); }
45
+ .pp-skip:focus { top: var(--s3); }
46
+
47
+ /* ── Header ────────────────────────────────────────────────────────────── */
48
+ .pp-header {
49
+ position: sticky; top: 0; z-index: 10;
50
+ background: var(--surface-sticky); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
51
+ border-bottom: 1px solid var(--line);
52
+ }
53
+ .pp-header > .pp-wrap { display: flex; align-items: center; gap: var(--s5); min-height: 64px; }
54
+ .pp-brand {
55
+ display: inline-flex; align-items: center; gap: 10px; color: var(--ink); text-decoration: none;
56
+ font-weight: 600; font-size: var(--text-lg); letter-spacing: -0.01em;
57
+ }
58
+ .pp-brand img, .pp-brand svg { width: 26px; height: 26px; }
59
+ .pp-nav { display: flex; align-items: center; gap: var(--s5); margin-left: auto; }
60
+ .pp-nav a { color: var(--ink-soft); text-decoration: none; font-size: var(--text-sm); font-weight: 500; }
61
+ .pp-nav a:hover { color: var(--ink); }
62
+ .pp-header-actions { display: flex; align-items: center; gap: var(--s3); }
63
+ .pp-nav + .pp-header-actions { margin-left: 0; }
64
+ .pp-brand + .pp-header-actions { margin-left: auto; }
65
+ @media (max-width: 860px) { .pp-nav { display: none; } .pp-header-actions { margin-left: auto; } }
66
+ @media (max-width: 480px) { .pp-header .pp-status { display: none; } }
67
+
68
+ /* ── Status ────────────────────────────────────────────────────────────
69
+ Where the product stands, as a word with a dot: "Pilot open", "In
70
+ development", "Closed beta". The dot takes a state hue only when the state
71
+ asks something of the reader (DESIGN.md §3): open = success (you can join),
72
+ soon = attention (it is coming, watch for it). Without data-state it stays
73
+ neutral. The word always carries the meaning on its own. */
74
+ .pp-status {
75
+ display: inline-flex; align-items: center; gap: var(--s2); white-space: nowrap;
76
+ font: 500 var(--text-xs) / 1 var(--mono); letter-spacing: 0.08em; text-transform: uppercase;
77
+ color: var(--ink-soft); padding: 6px 10px; border: 1px solid var(--line-strong); border-radius: 999px;
78
+ }
79
+ .pp-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--slate); flex: none; }
80
+ .pp-status[data-state="open"]::before { background: var(--success); box-shadow: 0 0 0 3px var(--success-surface); }
81
+ .pp-status[data-state="soon"]::before { background: var(--attention); box-shadow: 0 0 0 3px var(--attention-surface); }
82
+
83
+ /* ── Buttons ───────────────────────────────────────────────────────────
84
+ .pp-button is the page's one primary action. .is-secondary is everything
85
+ else. Works on <a> and <button>. */
86
+ .pp-button {
87
+ display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
88
+ font: 600 var(--text-base) / 1.2 var(--sans); text-decoration: none; white-space: nowrap; cursor: pointer;
89
+ padding: 13px 22px; border-radius: 8px; border: 1px solid var(--i);
90
+ background: var(--i); color: var(--on-i);
91
+ transition: background-color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
92
+ }
93
+ .pp-button:hover { background: var(--i-hover); border-color: var(--i-hover); color: var(--on-i); }
94
+ .pp-button:active { background: var(--i-pressed); border-color: var(--i-pressed); }
95
+ .pp-button.is-secondary { background: transparent; color: var(--ink); border-color: var(--line-control); font-weight: 500; }
96
+ .pp-button.is-secondary:hover { background: var(--surface-hover); color: var(--ink); }
97
+ .pp-button.is-secondary:active { background: var(--surface-active); }
98
+ .pp-button.is-small { font-size: var(--text-sm); padding: 8px 14px; }
99
+ .pp-button:focus-visible { outline: 2px solid var(--i-ink); outline-offset: 2px; }
100
+ .pp-button:disabled { background: var(--i-disabled-bg); border-color: transparent; color: var(--i-disabled-ink); cursor: not-allowed; }
101
+ @media (pointer: coarse) { .pp-button { min-height: 44px; } }
102
+
103
+ .pp-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3); margin: 0; }
104
+ .pp-note { margin: var(--s3) 0 0; font-size: var(--text-sm); color: var(--ink-soft); }
105
+
106
+ /* ── Hero ──────────────────────────────────────────────────────────────
107
+ Copy on the left, proof on the right; stacked below 960px with the copy
108
+ first. The thesis is one line of argument, not a tagline. */
109
+ .pp-hero { padding: var(--s8) 0 var(--s8); }
110
+ .pp-hero > .pp-wrap { display: grid; gap: var(--s7); align-items: center; grid-template-columns: minmax(0, 1fr); }
111
+ @media (min-width: 960px) { .pp-hero > .pp-wrap { grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); } }
112
+ .pp-hero .pp-status { margin-bottom: var(--s5); }
113
+ .pp-thesis {
114
+ font-size: var(--text-4xl); font-weight: 600; line-height: 1.04; letter-spacing: -0.035em;
115
+ margin: 0 0 var(--s4); max-width: 14ch; text-wrap: balance;
116
+ }
117
+ .pp-subhead { font-size: var(--text-lg); line-height: 1.5; color: var(--ink-soft); margin: 0 0 var(--s6); max-width: 40ch; }
118
+ .pp-media { margin: 0; min-width: 0; }
119
+
120
+ /* ── Story ─────────────────────────────────────────────────────────────
121
+ An <ol> of chapters. Each opens on a hairline with "01 / Label" in mono,
122
+ then a headline that is a sentence, then a short body and an optional
123
+ figure. Wide screens put the label in its own column. */
124
+ .pp-story { list-style: none; margin: 0; padding: 0; counter-reset: pp-chapter; }
125
+ .pp-chapter { counter-increment: pp-chapter; padding: var(--s8) 0; border-top: 1px solid var(--line); scroll-margin-top: 64px; }
126
+ .pp-chapter > .pp-wrap { display: grid; gap: var(--s4) var(--s7); grid-template-columns: minmax(0, 1fr); }
127
+ @media (min-width: 960px) {
128
+ .pp-chapter > .pp-wrap { grid-template-columns: minmax(0, 3fr) minmax(0, 9fr); }
129
+ .pp-chapter-body { grid-column: 2; }
130
+ }
131
+ .pp-label { margin: 0; padding-top: 0.4em; font: 500 var(--text-xs) / 1.4 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-soft); }
132
+ .pp-label::before { content: counter(pp-chapter, decimal-leading-zero) " / "; color: var(--ink); }
133
+ .pp-chapter h2 {
134
+ font-size: clamp(var(--text-xl), 1rem + 2vw, var(--text-3xl)); font-weight: 600; line-height: 1.15;
135
+ letter-spacing: -0.025em; margin: 0 0 var(--s4); max-width: 22ch; text-wrap: balance;
136
+ }
137
+ .pp-chapter-body > p { margin: 0 0 var(--s4); max-width: 62ch; color: var(--ink-soft); font-size: var(--text-lg); line-height: 1.55; }
138
+ .pp-chapter-body > p:last-child { margin-bottom: 0; }
139
+
140
+ /* Two to four short points under a chapter: a ruled column each. */
141
+ .pp-points { list-style: none; margin: var(--s6) 0 0; padding: 0; display: grid; gap: var(--s5); grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
142
+ .pp-points > li { border-top: 2px solid var(--ink); padding-top: var(--s3); }
143
+ .pp-points h3 { margin: 0 0 6px; font-size: var(--text-lg); font-weight: 600; letter-spacing: -0.01em; }
144
+ .pp-points p { margin: 0; color: var(--ink-soft); }
145
+
146
+ /* A chapter's figure: a terminal, a screenshot, a small mock of the product. */
147
+ .pp-figure { margin: var(--s6) 0 0; }
148
+ .pp-figure > img { display: block; width: 100%; height: auto; border-radius: var(--radius-lg); border: 1px solid var(--line); box-shadow: var(--shadow-2); }
149
+ .pp-figure figcaption { margin-top: var(--s2); font-size: var(--text-sm); color: var(--ink-soft); }
150
+ .pp-terminal { margin: 0; overflow-x: auto; background: var(--card); color: var(--ink); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-1);
151
+ padding: var(--s5); font: var(--text-sm) / 1.7 var(--mono); }
152
+ .pp-terminal code { font: inherit; }
153
+
154
+ /* ── Close ─────────────────────────────────────────────────────────────
155
+ The primary action once more, with the room a form needs. */
156
+ .pp-close { padding: var(--s8) 0 var(--s9); border-top: 1px solid var(--line); scroll-margin-top: 64px; }
157
+ .pp-close > .pp-wrap { display: grid; gap: var(--s6); grid-template-columns: minmax(0, 1fr); }
158
+ @media (min-width: 960px) { .pp-close > .pp-wrap { grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); } }
159
+ .pp-close h2 { font-size: clamp(var(--text-xl), 1rem + 2vw, var(--text-3xl)); font-weight: 600; line-height: 1.15; letter-spacing: -0.025em; margin: var(--s4) 0 var(--s3); text-wrap: balance; }
160
+ .pp-close .pp-subhead { margin-bottom: 0; }
161
+ .pp-panel { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-lg); box-shadow: var(--shadow-2); padding: var(--s6); }
162
+ @media (max-width: 560px) { .pp-panel { padding: var(--s5) var(--s4); } }
163
+
164
+ /* ── Footer ────────────────────────────────────────────────────────────── */
165
+ .pp-footer { border-top: 1px solid var(--line); padding: var(--s6) 0 var(--s7); color: var(--ink-soft); font-size: var(--text-sm); }
166
+ .pp-footer > .pp-wrap { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--s4) var(--s6); }
167
+ .pp-footer p { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3); }
168
+ .pp-footer .pp-brand { font-size: var(--text-base); }
169
+ .pp-footer .pp-brand img, .pp-footer .pp-brand svg { width: 20px; height: 20px; }
170
+ .pp-footer nav { display: flex; flex-wrap: wrap; gap: var(--s4); }
171
+ .pp-footer nav a { color: var(--ink-soft); text-decoration: none; }
172
+ .pp-footer nav a:hover { color: var(--ink); }
package/tailwind.css CHANGED
@@ -23,6 +23,7 @@
23
23
  --color-line-control: var(--line-control);
24
24
  --color-surface-hover: var(--surface-hover);
25
25
  --color-surface-active: var(--surface-active);
26
+ --color-surface-sticky: var(--surface-sticky);
26
27
  --color-on-dark: var(--on-dark);
27
28
 
28
29
  --color-interactive: var(--i);
package/tokens.css CHANGED
@@ -119,6 +119,11 @@
119
119
  --card: light-dark(#ffffff, #17171a); /* n-0 / n-50 */
120
120
  --surface-hover: var(--n-100);
121
121
  --surface-active: var(--n-200);
122
+ /* Sticky chrome — a header or toolbar that content scrolls under, usually
123
+ with `backdrop-filter: blur()`. 92% of the surface it sits on: the card
124
+ in light, the page ground in dark, where cards are lighter than the
125
+ page. A token so consumers never color-mix() one (DESIGN.md §11). */
126
+ --surface-sticky: light-dark(color-mix(in srgb, #ffffff 92%, transparent), color-mix(in srgb, #0d0d0f 92%, transparent)); /* card / bg */
122
127
  --ink: var(--n-900);
123
128
  --ink-soft: var(--n-700);
124
129
  --slate: light-dark(#6b6b73, #8b8b93); /* n-600 / n-500 — text-safe: eyebrows, axis labels, captions */