@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 +21 -0
- package/README.md +90 -0
- package/fonts/instrument-serif-italic-latin.woff2 +0 -0
- package/fonts/instrument-serif-latin-ext.woff2 +0 -0
- package/fonts/instrument-serif-latin.woff2 +0 -0
- package/fonts/inter-latin-ext.woff2 +0 -0
- package/fonts/inter-latin.woff2 +0 -0
- package/fonts/plex-mono-400-latin.woff2 +0 -0
- package/fonts/plex-mono-500-latin.woff2 +0 -0
- package/package.json +46 -0
- package/tailwind-preset.js +113 -0
- package/tokens.css +428 -0
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
|
|
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
|
+
}
|