@bitbaum/design-tokens 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mao Nakamoto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @bitbaum/design-tokens
2
+
3
+ The design **single source of truth** for OrangeCat, FleetCrown and Solon.
4
+
5
+ One file — [`tokens.css`](./tokens.css) — defines every colour, typeface, weight,
6
+ tracking, radius and shared primitive the three products use. They import it.
7
+ Nothing is copied.
8
+
9
+ ## Why
10
+
11
+ These tokens used to be hand-copied into three repos, and they drifted. Solon's
12
+ `globals.css` carried a comment claiming its tokens matched OrangeCat's while
13
+ sharing **zero** names or values with it — so the three products looked like
14
+ three different companies. Copies drift; imports cannot.
15
+
16
+ ## Retheming the entire stack
17
+
18
+ 1. Edit the block marked `▼▼▼ THE KNOBS ▼▼▼` in `tokens.css`. Nothing else.
19
+ 2. Tag a release: `git tag v1.2.0 && git push --tags`
20
+ 3. Bump `@bitbaum/design-tokens` in the three apps. Their CI auto-merges.
21
+
22
+ Changing the display typeface is **one line**:
23
+
24
+ ```css
25
+ --font-display: "Instrument Serif", Georgia, serif;
26
+ --font-display-weight: 400; /* the face's weight lives WITH the face */
27
+ --tracking-display: -0.015em; /* and so does its tracking */
28
+ ```
29
+
30
+ ## Install
31
+
32
+ ```jsonc
33
+ // package.json
34
+ "@bitbaum/design-tokens": "github:bitbaum/design-tokens#v1.1.0"
35
+ ```
36
+
37
+ ```tsx
38
+ // app/layout.tsx — import BEFORE the app's own globals.css
39
+ import "@bitbaum/design-tokens/tokens.css";
40
+ import "./globals.css";
41
+ ```
42
+
43
+ ```js
44
+ // tailwind.config.js (v3)
45
+ module.exports = {
46
+ presets: [require("@bitbaum/design-tokens/tailwind-preset")],
47
+ content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
48
+ };
49
+ ```
50
+
51
+ Tailwind v4 apps consume `tokens.css` through `@theme`; the variable names are
52
+ identical, so both paths stay in sync.
53
+
54
+ ## The contract
55
+
56
+ Components declare **intent**, never mechanics.
57
+
58
+ | Do | Don't | Why |
59
+ |---|---|---|
60
+ | `className="font-display"` | `font-['Instrument_Serif']` | the face is not a component's business |
61
+ | `className="font-display"` | `font-display font-bold` | the display face has **one weight**; a synthesized bold looks cheap |
62
+ | `className="bg-surface-base"` | `bg-[#1c1c1c]` | retheming must not require finding hex codes |
63
+ | `className="text-accent"` | `text-[#ff5c00]` | one action colour, one definition |
64
+ | `bg-accent text-on-accent` | `bg-accent text-white` | white on the accent is **3.10:1** and fails AA; the paired ink is 6.10:1 |
65
+ | `className="tabular"` | — | numbers that change in place must not jitter |
66
+ | `font-display` at ≥`text-2xl` | `font-display text-lg` | below 24px the high-contrast serif reads *lighter* than the body sans and hierarchy inverts |
67
+ | `className="wordmark"` | `font-display` on the logo | uppercase + open tracking is the one small size a display serif survives |
68
+
69
+ Enforce it in each repo's design gate so drift fails the build rather than
70
+ shipping quietly.
71
+
72
+ ## Typefaces
73
+
74
+ Self-hosted (latin + latin-ext), no runtime requests to Google:
75
+
76
+ | Role | Face | Weights | Carries |
77
+ |---|---|---|---|
78
+ | display | Instrument Serif | 400 only | headlines, wordmarks |
79
+ | sans | Inter | 400–700 variable | all UI and body text |
80
+ | mono | IBM Plex Mono | 400, 500 | addresses, signatures, hashes, tallies |
81
+
82
+ All three are SIL Open Font License.
83
+
84
+ ### Why a serif
85
+
86
+ Every AI and infrastructure company competes inside the same dark-canvas
87
+ grotesque convention, where the winner is whoever has the largest design budget.
88
+ A high-contrast serif exits that comparison: it reads institutional rather than
89
+ startup, which is exactly right for a stack whose product is *legitimacy* —
90
+ signed votes, verifiable treasuries, an append-only audit trail.
Binary file
Binary file
Binary file
Binary file
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@bitbaum/design-tokens",
3
+ "version": "1.1.0",
4
+ "engines": {
5
+ "node": ">=20"
6
+ },
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "provenance": true
10
+ },
11
+ "description": "Design SSOT for OrangeCat, FleetCrown and Solon — tokens, self-hosted faces, and the Tailwind preset that maps them.",
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/bitbaum/design-tokens.git"
16
+ },
17
+ "files": [
18
+ "tokens.css",
19
+ "tailwind-preset.js",
20
+ "fonts"
21
+ ],
22
+ "exports": {
23
+ "./tokens.css": "./tokens.css",
24
+ "./tailwind-preset": "./tailwind-preset.js",
25
+ "./fonts/*": "./fonts/*"
26
+ },
27
+ "sideEffects": [
28
+ "*.css"
29
+ ],
30
+ "devDependencies": {
31
+ "@eslint/js": "^10.0.1",
32
+ "@types/node": "^26.4.0",
33
+ "eslint": "^10.9.1",
34
+ "globals": "^17.12.0",
35
+ "prettier": "3.9.6",
36
+ "typescript": "^6.0.3"
37
+ },
38
+ "scripts": {
39
+ "lint": "eslint .",
40
+ "typecheck": "tsc --noEmit",
41
+ "test": "node --test test/*.test.js",
42
+ "verify": "pnpm run format:check && pnpm run lint && pnpm run typecheck && pnpm test",
43
+ "format": "prettier --write .",
44
+ "format:check": "prettier --check ."
45
+ }
46
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * @bitbaum/design-tokens — Tailwind v3 preset.
3
+ *
4
+ * Maps utility names onto the CSS custom properties defined in tokens.css.
5
+ * Every value here resolves to a var — never a literal. That is the whole
6
+ * point: retheming edits tokens.css, and nothing in any app changes.
7
+ *
8
+ * Usage (tailwind.config.js):
9
+ * module.exports = {
10
+ * presets: [require('@bitbaum/design-tokens/tailwind-preset')],
11
+ * content: [...],
12
+ * }
13
+ *
14
+ * Tailwind v4 apps (FleetCrown) consume tokens.css directly via @theme instead;
15
+ * the variable names are identical, so the two paths stay in sync.
16
+ */
17
+
18
+ // HSL channel triplets, so `bg-surface-raised/50` composes alpha correctly.
19
+ /**
20
+ * @param {string} token CSS custom property name, e.g. `--text-primary`.
21
+ * @returns {string} Tailwind colour value carrying the alpha placeholder.
22
+ */
23
+ const withAlpha = (token) => `hsl(var(${token}) / <alpha-value>)`;
24
+
25
+ module.exports = {
26
+ theme: {
27
+ extend: {
28
+ colors: {
29
+ fg: {
30
+ primary: withAlpha('--text-primary'),
31
+ secondary: withAlpha('--text-secondary'),
32
+ tertiary: withAlpha('--text-tertiary'),
33
+ muted: withAlpha('--text-muted'),
34
+ inverted: withAlpha('--text-inverted'),
35
+ },
36
+ surface: {
37
+ public: withAlpha('--surface-public'),
38
+ page: withAlpha('--surface-page'),
39
+ base: withAlpha('--surface-base'),
40
+ raised: withAlpha('--surface-raised'),
41
+ overlay: withAlpha('--surface-overlay'),
42
+ modal: withAlpha('--surface-modal'),
43
+ drawer: withAlpha('--surface-drawer'),
44
+ hover: withAlpha('--surface-hover'),
45
+ },
46
+ border: {
47
+ subtle: withAlpha('--border-subtle'),
48
+ DEFAULT: withAlpha('--border-default'),
49
+ strong: withAlpha('--border-strong'),
50
+ interactive: withAlpha('--border-interactive'),
51
+ },
52
+ // Lets components write `border-default` alongside `border-subtle`.
53
+ default: withAlpha('--border-default'),
54
+ accent: {
55
+ DEFAULT: 'var(--public-accent)',
56
+ hover: 'var(--accent-hover)',
57
+ muted: 'hsl(var(--accent-muted))',
58
+ text: 'hsl(var(--accent-text))',
59
+ },
60
+ // The label colour for text ON the accent. `bg-accent text-white` is
61
+ // 3.10:1 and fails AA; `bg-accent text-on-accent` is 6.10:1. Paired
62
+ // with the background it belongs to so nobody has to remember which
63
+ // ink an orange button takes.
64
+ 'on-accent': withAlpha('--on-accent'),
65
+ // Bitcoin's own orange — means "this is Bitcoin", never an action.
66
+ bitcoin: 'var(--bitcoin-orange)',
67
+ status: {
68
+ positive: withAlpha('--status-positive'),
69
+ warning: withAlpha('--status-warning'),
70
+ negative: withAlpha('--status-negative'),
71
+ neutral: withAlpha('--status-neutral'),
72
+ // The -subtle tokens already carry their own alpha, so they are used
73
+ // as-is rather than through withAlpha.
74
+ 'positive-subtle': 'hsl(var(--status-positive-subtle))',
75
+ 'warning-subtle': 'hsl(var(--status-warning-subtle))',
76
+ 'negative-subtle': 'hsl(var(--status-negative-subtle))',
77
+ },
78
+ },
79
+ fontFamily: {
80
+ sans: ['var(--font-sans)'],
81
+ display: ['var(--font-display)'],
82
+ mono: ['var(--font-mono)'],
83
+ },
84
+ letterSpacing: {
85
+ display: 'var(--tracking-display)',
86
+ label: 'var(--tracking-label)',
87
+ caps: 'var(--tracking-caps)',
88
+ },
89
+ borderRadius: {
90
+ control: 'var(--radius-control)',
91
+ surface: 'var(--radius-surface)',
92
+ pill: 'var(--radius-pill)',
93
+ },
94
+ fontSize: {
95
+ // Fluid display sizes. Line heights ride with them so a headline never
96
+ // needs a per-breakpoint leading override.
97
+ 'display-1': ['var(--text-display-1)', { lineHeight: '1.04' }],
98
+ 'display-2': ['var(--text-display-2)', { lineHeight: '1.12' }],
99
+ 'display-3': ['var(--text-display-3)', { lineHeight: '1.2' }],
100
+ },
101
+ maxWidth: {
102
+ shell: 'var(--shell-max)',
103
+ lede: 'var(--measure-lede)',
104
+ copy: 'var(--measure-copy)',
105
+ },
106
+ spacing: {
107
+ nav: 'var(--public-nav-height)',
108
+ section: 'var(--section-py)',
109
+ 'section-tight': 'var(--section-py-tight)',
110
+ },
111
+ },
112
+ },
113
+ };
package/tokens.css ADDED
@@ -0,0 +1,428 @@
1
+ /**
2
+ * @bitbaum/design-tokens — the design SSOT for OrangeCat, FleetCrown and Solon.
3
+ *
4
+ * ┌──────────────────────────────────────────────────────────────────────────┐
5
+ * │ TO RETHEME THE ENTIRE STACK, EDIT THE "KNOBS" BLOCK BELOW AND NOTHING │
6
+ * │ ELSE. Then tag a release and bump the dependency in the three apps. │
7
+ * └──────────────────────────────────────────────────────────────────────────┘
8
+ *
9
+ * Why this package exists: the same tokens used to be hand-copied into three
10
+ * repos. They drifted — Solon's globals.css carried a comment claiming it
11
+ * matched OrangeCat while sharing zero values with it, and the three products
12
+ * looked like three companies. Copies drift; imports cannot.
13
+ *
14
+ * Rules for consumers:
15
+ * - Components use ROLES (.font-display, bg-surface-base, text-fg-secondary).
16
+ * They never name a typeface, a weight, or a hex value.
17
+ * - A face's required weight and tracking are declared HERE, next to the face,
18
+ * so swapping a single-weight display serif for a variable grotesque is one
19
+ * edit and zero component changes.
20
+ * - Colours are HSL channel triplets so Tailwind composes alpha:
21
+ * `hsl(var(--surface-raised) / 0.5)` via `bg-surface-raised/50`.
22
+ */
23
+
24
+ /* ══════════════════════════════════════════════════════════════════════════
25
+ FACES — self-hosted, latin + latin-ext. No external requests at runtime.
26
+ ══════════════════════════════════════════════════════════════════════════ */
27
+
28
+ /* Instrument Serif — display. SINGLE WEIGHT (400): there is no bold. Asking for
29
+ 700 makes the browser synthesize a fake bold, which looks cheap; that is why
30
+ --font-display-weight exists and why components must not set a weight. */
31
+ @font-face {
32
+ font-family: 'Instrument Serif';
33
+ font-style: normal;
34
+ font-weight: 400;
35
+ font-display: swap;
36
+ src: url(./fonts/instrument-serif-latin.woff2) format('woff2');
37
+ unicode-range:
38
+ U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
39
+ U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
40
+ }
41
+ @font-face {
42
+ font-family: 'Instrument Serif';
43
+ font-style: normal;
44
+ font-weight: 400;
45
+ font-display: swap;
46
+ src: url(./fonts/instrument-serif-latin-ext.woff2) format('woff2');
47
+ unicode-range:
48
+ U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
49
+ U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
50
+ U+A720-A7FF;
51
+ }
52
+ @font-face {
53
+ font-family: 'Instrument Serif';
54
+ font-style: italic;
55
+ font-weight: 400;
56
+ font-display: swap;
57
+ src: url(./fonts/instrument-serif-italic-latin.woff2) format('woff2');
58
+ unicode-range:
59
+ U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
60
+ U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
61
+ }
62
+
63
+ /* Inter — UI/body. Variable 400–700. */
64
+ @font-face {
65
+ font-family: 'Inter';
66
+ font-style: normal;
67
+ font-weight: 400 700;
68
+ font-display: swap;
69
+ src: url(./fonts/inter-latin.woff2) format('woff2');
70
+ unicode-range:
71
+ U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
72
+ U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
73
+ }
74
+ @font-face {
75
+ font-family: 'Inter';
76
+ font-style: normal;
77
+ font-weight: 400 700;
78
+ font-display: swap;
79
+ src: url(./fonts/inter-latin-ext.woff2) format('woff2');
80
+ unicode-range:
81
+ U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
82
+ U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
83
+ U+A720-A7FF;
84
+ }
85
+
86
+ /* IBM Plex Mono — load-bearing strings: Bitcoin addresses, signatures, hashes,
87
+ tallies. A hash must look identical on all three sites. */
88
+ @font-face {
89
+ font-family: 'IBM Plex Mono';
90
+ font-style: normal;
91
+ font-weight: 400;
92
+ font-display: swap;
93
+ src: url(./fonts/plex-mono-400-latin.woff2) format('woff2');
94
+ unicode-range:
95
+ U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
96
+ U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
97
+ }
98
+ @font-face {
99
+ font-family: 'IBM Plex Mono';
100
+ font-style: normal;
101
+ font-weight: 500;
102
+ font-display: swap;
103
+ src: url(./fonts/plex-mono-500-latin.woff2) format('woff2');
104
+ unicode-range:
105
+ U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
106
+ U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
107
+ }
108
+
109
+ :root {
110
+ /* ════════════════════════════════════════════════════════════════════════
111
+ ▼▼▼ THE KNOBS ▼▼▼ Everything below this line is derived from these.
112
+ ════════════════════════════════════════════════════════════════════════ */
113
+
114
+ /* — Typeface roles. Swap a face here and the whole stack changes. ——————— */
115
+ --font-display: 'Instrument Serif', Georgia, serif;
116
+ --font-sans: 'Inter', system-ui, -apple-system, sans-serif;
117
+ --font-mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
118
+
119
+ /* The weight and tracking the DISPLAY FACE needs. These belong to the face,
120
+ not to the component — that is what makes a face swap a one-line change.
121
+ Instrument Serif has one weight (400) and, being a high-contrast serif,
122
+ wants far less negative tracking than a grotesque does. */
123
+ --font-display-weight: 400;
124
+ --tracking-display: -0.015em;
125
+
126
+ /* — The one action colour across all three products. Theme-independent. — */
127
+ --public-accent: #ff5c00;
128
+
129
+ /**
130
+ * The label colour for text sitting ON --public-accent.
131
+ *
132
+ * White on #ff5c00 is 3.10:1 — below the 4.5 AA floor — so every primary
133
+ * CTA in the fleet was failing at once: Solon's "Open the Dashboard",
134
+ * OrangeCat's "Get Started", FleetCrown's "Launch agent". Measured, not
135
+ * guessed. Near-black on the same orange is 6.10:1.
136
+ *
137
+ * The orange is deliberately NOT darkened to rescue white text: it would
138
+ * have to reach #c74600 to clear the floor, and that is a duller, muddier
139
+ * colour. The accent IS the brand; the label is what gives way.
140
+ *
141
+ * Theme-independent for the same reason --public-accent is. The accent does
142
+ * not flip between themes, so a label that flipped with the theme would be
143
+ * unreadable in one of them — which is exactly why no existing text token
144
+ * could be reused here (--text-primary inverts, this must not).
145
+ */
146
+ --on-accent: 0 0% 8%;
147
+
148
+ /**
149
+ * — The marketing band ——————————————————————————————————————————————————
150
+ * Always near-black, in BOTH themes, so a CTA reads the same regardless of
151
+ * the surrounding app theme. Deliberately not part of the ramp below.
152
+ */
153
+ --surface-public: 0 0% 4%;
154
+
155
+ /* ── LIGHT THEME ────────────────────────────────────────────────────────
156
+ :root is light and `.dark` overrides it — the same mechanism OrangeCat
157
+ uses, so an app can toggle themes without translating anything. An app
158
+ that is dark-only (Solon, FleetCrown's public surface) pins it by putting
159
+ `class="dark"` on <html> and never removing it. */
160
+
161
+ /* Surfaces — page < base < raised < overlay < modal < drawer */
162
+ --surface-page: 0 0% 97%;
163
+ --surface-base: 0 0% 100%;
164
+ --surface-raised: 0 0% 96%;
165
+ --surface-overlay: 0 0% 94%;
166
+ --surface-modal: 0 0% 100%;
167
+ --surface-drawer: 0 0% 98%;
168
+ --surface-hover: 0 0% 94%;
169
+
170
+ /* Text ramp */
171
+ --text-primary: 0 0% 10%;
172
+ --text-secondary: 0 0% 40%;
173
+ --text-tertiary: 0 0% 58%;
174
+ --text-muted: 0 0% 72%;
175
+ --text-inverted: 0 0% 98%;
176
+
177
+ /* Borders */
178
+ --border-subtle: 0 0% 94%;
179
+ --border-default: 0 0% 82%;
180
+ --border-strong: 0 0% 78%;
181
+ --border-interactive: 0 0% 30%;
182
+
183
+ /* Accent */
184
+ --accent-primary: var(--public-accent);
185
+ --accent-hover: #e44d00;
186
+ --accent-muted: 0 0% 10%;
187
+ --accent-text: 0 0% 10%;
188
+
189
+ /* Status — the only chromatic colours permitted outside the accent */
190
+ --status-positive: 145 60% 35%;
191
+ --status-positive-subtle: 145 60% 35% / 0.08;
192
+ --status-warning: 38 92% 50%;
193
+ --status-warning-subtle: 38 92% 50% / 0.08;
194
+ --status-negative: 0 65% 50%;
195
+ --status-negative-subtle: 0 65% 50% / 0.08;
196
+ --status-neutral: 0 0% 58%;
197
+
198
+ /* — Geometry —————————————————————————————————————————————————————————— */
199
+ --radius-control: 0.375rem;
200
+ --radius-surface: 0.5rem;
201
+ --radius-pill: 9999px;
202
+ --shell-max: 80rem;
203
+ --public-nav-height: 76px;
204
+
205
+ /* — Secondary tracking roles —————————————————————————————————————————— */
206
+ --tracking-label: 0.1em;
207
+ --tracking-caps: 0.16em;
208
+
209
+ /**
210
+ * — Vertical rhythm ————————————————————————————————————————————————————
211
+ * Pace is one decision, not a `py-20 sm:py-28` guess repeated on every
212
+ * section — that is how a page ends up with six slightly different rhythms.
213
+ * Fluid, so spacing that reads as generous at 1440 does not strand a phone
214
+ * with half a screen of emptiness.
215
+ */
216
+ --section-py: clamp(5rem, 9vw, 9rem);
217
+ --section-py-tight: clamp(3.5rem, 6vw, 6rem);
218
+
219
+ /**
220
+ * — Display scale ——————————————————————————————————————————————————————
221
+ * Fluid for the same reason: one confident size that resolves itself per
222
+ * viewport, rather than three breakpoint variants per heading. Note that
223
+ * display-3 bottoms out at 1.5rem — exactly the display face's size floor,
224
+ * so every step of this scale is safe to set in the display face.
225
+ */
226
+ --text-display-1: clamp(2.75rem, 5.4vw, 4.5rem);
227
+ --text-display-2: clamp(2rem, 3.4vw, 3rem);
228
+ --text-display-3: clamp(1.5rem, 2vw, 1.875rem);
229
+
230
+ /**
231
+ * — Measures for running text ——————————————————————————————————————————
232
+ * Headlines take their width from the column they sit in; a hero h1 with its
233
+ * own max-width as well ends up with two rules where only one ever binds.
234
+ */
235
+ --measure-lede: 46ch;
236
+ --measure-copy: 68ch;
237
+
238
+ /* ════════════════════════════════════════════════════════════════════════
239
+ ▲▲▲ END KNOBS ▲▲▲
240
+ ════════════════════════════════════════════════════════════════════════ */
241
+
242
+ /* Bitcoin's own orange. Not a brand colour — it means "this is Bitcoin", so
243
+ it is deliberately NOT --public-accent and must not be used for actions. */
244
+ --bitcoin-orange: #f7931a;
245
+ }
246
+
247
+ /* ══════════════════════════════════════════════════════════════════════════
248
+ DARK THEME
249
+ Only the values that actually differ. Faces, weights, tracking, geometry,
250
+ rhythm, measures, --public-accent and --surface-public are theme-independent
251
+ and are NOT repeated here — repeating them is how a theme drifts from its
252
+ own light counterpart.
253
+ ══════════════════════════════════════════════════════════════════════════ */
254
+
255
+ .dark {
256
+ /* Surfaces */
257
+ --surface-page: 0 0% 7%;
258
+ --surface-base: 0 0% 11%;
259
+ --surface-raised: 0 0% 14%;
260
+ --surface-overlay: 0 0% 17%;
261
+ --surface-modal: 0 0% 11%;
262
+ --surface-drawer: 0 0% 9%;
263
+ --surface-hover: 0 0% 13%;
264
+
265
+ /* Text ramp */
266
+ --text-primary: 0 0% 92%;
267
+ --text-secondary: 0 0% 65%;
268
+ --text-tertiary: 0 0% 48%;
269
+ --text-muted: 0 0% 32%;
270
+ --text-inverted: 0 0% 10%;
271
+
272
+ /* Borders */
273
+ --border-subtle: 0 0% 13%;
274
+ --border-default: 0 0% 20%;
275
+ --border-strong: 0 0% 24%;
276
+ --border-interactive: 0 0% 60%;
277
+
278
+ /* Accent */
279
+ --accent-primary: var(--public-accent);
280
+ --accent-hover: #ff7a33;
281
+ --accent-muted: 0 0% 92% / 0.08;
282
+ --accent-text: 0 0% 92%;
283
+
284
+ /* Status */
285
+ --status-positive: 145 55% 50%;
286
+ --status-positive-subtle: 145 55% 50% / 0.12;
287
+ --status-warning: 38 92% 60%;
288
+ --status-warning-subtle: 38 92% 60% / 0.12;
289
+ --status-negative: 0 75% 60%;
290
+ --status-negative-subtle: 0 75% 60% / 0.12;
291
+ --status-neutral: 0 0% 48%;
292
+ }
293
+
294
+ /* ══════════════════════════════════════════════════════════════════════════
295
+ SHARED PRIMITIVES — the chrome all three products present.
296
+ ══════════════════════════════════════════════════════════════════════════ */
297
+
298
+ body {
299
+ color: hsl(var(--text-primary));
300
+ background: hsl(var(--surface-page));
301
+ font-family: var(--font-sans);
302
+ }
303
+
304
+ /**
305
+ * The display treatment. Owns family + weight + tracking together so that a
306
+ * component only ever declares INTENT ("this is display type"), never the
307
+ * mechanics. Do not add font-bold/tracking-* alongside this class: the current
308
+ * face has no bold, and a synthesized one looks cheap.
309
+ */
310
+ .font-display {
311
+ font-family: var(--font-display);
312
+ font-weight: var(--font-display-weight);
313
+ letter-spacing: var(--tracking-display);
314
+ /* Balance the line lengths so a headline never strands its last word alone on
315
+ a second line. Browsers cap this at a few lines, which is exactly the range
316
+ a display heading occupies; longer running text is unaffected. */
317
+ text-wrap: balance;
318
+ }
319
+
320
+ /**
321
+ * SIZE FLOOR — the display face is for DISPLAY sizes only: 1.5rem (24px) and up.
322
+ *
323
+ * Instrument Serif is high-contrast: its thin strokes thin out further as the
324
+ * type shrinks, so an 18px serif heading reads LIGHTER than the 16px sans
325
+ * paragraph beneath it and the hierarchy silently inverts. Below the floor,
326
+ * headings use the sans at 600. Each repo's design gate enforces this by
327
+ * rejecting `font-display` next to text-xs/sm/base/lg/xl.
328
+ */
329
+
330
+ /**
331
+ * The wordmark. A high-contrast serif goes weak at small sizes in mixed case,
332
+ * but holds up in uppercase with open tracking — which is exactly what a
333
+ * wordmark is. This is the ONLY sanctioned use of the display face below the
334
+ * size floor described above.
335
+ */
336
+ .wordmark {
337
+ font-family: var(--font-display);
338
+ font-weight: var(--font-display-weight);
339
+ text-transform: uppercase;
340
+ letter-spacing: 0.08em;
341
+ }
342
+
343
+ /**
344
+ * Numbers that sit in columns or change in place — tallies, balances, counts.
345
+ * Tabular figures stop the layout jittering as values update.
346
+ */
347
+ .tabular {
348
+ font-variant-numeric: tabular-nums;
349
+ font-feature-settings: 'tnum' 1;
350
+ }
351
+
352
+ /**
353
+ * Section rhythm. Marketing sections are full-bleed and own their inner
354
+ * container — a root layout must NOT wrap pages in one, or every hero renders
355
+ * as a box floating in a gutter instead of reaching the edges of the screen.
356
+ */
357
+ .section-shell {
358
+ margin-inline: auto;
359
+ width: 100%;
360
+ max-width: var(--shell-max);
361
+ padding-inline: 1rem;
362
+ }
363
+
364
+ @media (min-width: 640px) {
365
+ .section-shell {
366
+ padding-inline: 1.5rem;
367
+ }
368
+ }
369
+
370
+ @media (min-width: 1024px) {
371
+ .section-shell {
372
+ padding-inline: 2rem;
373
+ }
374
+ }
375
+
376
+ /** One accent glow and a masked grid over the near-black public surface. */
377
+ .public-backdrop {
378
+ position: relative;
379
+ isolation: isolate;
380
+ background: hsl(var(--surface-public));
381
+ --public-glow-accent: rgb(255 92 0 / 0.1);
382
+ --public-grid-stroke: rgb(255 255 255 / 0.04);
383
+ --public-grid-size: 80px;
384
+ }
385
+
386
+ .public-backdrop::before {
387
+ content: '';
388
+ position: absolute;
389
+ inset: 0;
390
+ z-index: -1;
391
+ background: radial-gradient(60rem 30rem at 50% -10%, var(--public-glow-accent), transparent 70%);
392
+ }
393
+
394
+ .public-backdrop::after {
395
+ content: '';
396
+ position: absolute;
397
+ inset: 0;
398
+ z-index: -1;
399
+ background-image:
400
+ linear-gradient(to right, var(--public-grid-stroke) 1px, transparent 1px),
401
+ linear-gradient(to bottom, var(--public-grid-stroke) 1px, transparent 1px);
402
+ background-size: var(--public-grid-size) var(--public-grid-size);
403
+ mask-image: radial-gradient(closest-side at 50% 35%, black, transparent);
404
+ }
405
+
406
+ /** The eyebrow pill that opens a marketing page on all three products. */
407
+ .eyebrow {
408
+ display: inline-flex;
409
+ align-items: center;
410
+ gap: 0.5rem;
411
+ border-radius: var(--radius-pill);
412
+ border: 1px solid hsl(var(--border-default));
413
+ background: hsl(var(--surface-base));
414
+ padding: 0.375rem 0.875rem;
415
+ font-size: 0.75rem;
416
+ font-weight: 500;
417
+ letter-spacing: var(--tracking-caps);
418
+ text-transform: uppercase;
419
+ color: hsl(var(--text-secondary));
420
+ }
421
+
422
+ .eyebrow::before {
423
+ content: '';
424
+ width: 0.375rem;
425
+ height: 0.375rem;
426
+ border-radius: var(--radius-pill);
427
+ background: var(--public-accent);
428
+ }