1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
/* Gloss design tokens, vendored from the Arbitrary Definitions design system. */
/* Gloss — color tokens. The stock is paper: #f2f1ee at the page level, with
surfaces going lighter rather than darker. There is a warm cast, but it is
held just off neutral — enough that the page is not screen-white, not
enough to read as cream or beige. Every neutral shares that one hue at
very low chroma, so the page reads as paper rather than as a tint. If a
surface ever looks tinted, the chroma is too high, not too low.
Hand-maintained; edit values directly. Palette ramps are reference
material. Components read the semantic aliases below, never a ramp step. */
:root {
--gl-neutral-0: #f2f1ee; /* page background */
--gl-neutral-50: #f9f8f6; /* card / surface stock */
--gl-neutral-100: #e8e7e3; /* recessed fill */
--gl-neutral-150: #deddd8; /* recessed fill, deeper */
--gl-neutral-200: #cdcbc6; /* hairline */
--gl-neutral-300: #a9a7a1; /* border, default */
--gl-neutral-500: #6a6862; /* muted-faint text (4.9:1 on stock) */
--gl-neutral-600: #4f4d48; /* muted / secondary text */
--gl-neutral-800: #2b2a27; /* strong text */
--gl-neutral-900: #161512; /* ink — primary text, near-black */
/* dark-mode neutral ramp — same hue, inverted */
--gl-neutral-dark-0: #0e0e0d;
--gl-neutral-dark-50: #151412;
--gl-neutral-dark-100: #1d1c1a;
--gl-neutral-dark-150: #252421;
--gl-neutral-dark-200: #2c2b28;
--gl-neutral-dark-300: #45443f;
--gl-neutral-dark-500: #8c8a85;
--gl-neutral-dark-600: #a5a39e;
--gl-neutral-dark-800: #d9d7d3;
--gl-neutral-dark-900: #eae8e4;
/* Sanctioned per-project accents — swap --gl-color-accent to one of these
to give a project its own identity without touching anything else.
All five read against the near-neutral stock; pick by project, not by
which one is loudest. */
--gl-accent-clay: #9a5a3c;
--gl-accent-ink-blue: #2b4a78;
--gl-accent-ochre: #93701f;
--gl-accent-plum: #6a3a5c;
--gl-accent-teal: #3a6a63;
/* Surfaces */
--gl-color-surface-page: #f2f1ee;
--gl-color-surface-card: #f9f8f6;
--gl-color-surface-fill: #e8e7e3;
--gl-color-surface-fill-2: #deddd8;
/* Borders */
--gl-color-border-hairline: #cdcbc6;
--gl-color-border-default: #a9a7a1;
/* The section rule is ink, not a border color: it is type-weight, not chrome. */
--gl-color-rule: var(--gl-color-text-primary);
/* Text. Tertiary clears 4.5:1 against the page stock; it is the faintest
text the system allows, and it is still readable. */
--gl-color-text-primary: #161512;
--gl-color-text-secondary: #4f4d48;
--gl-color-text-tertiary: #6a6862;
/* Accent — one per project, held constant. Never two in one view. */
--gl-color-accent: #9a5a3c;
--gl-color-accent-fg: #ffffff;
--gl-color-accent-ink: color-mix(in oklch, var(--gl-color-accent) 78%, black);
--gl-color-accent-soft: color-mix(in oklch, var(--gl-color-accent) 12%, white);
/* Status (fixed — never swapped like the accent) */
--gl-color-success: #3f7a4a;
--gl-color-success-bg: color-mix(in oklch, var(--gl-color-success) 12%, white);
--gl-color-danger: #ad3a2c;
--gl-color-danger-bg: color-mix(in oklch, var(--gl-color-danger) 10%, white);
}
/* Dark mode is a block of repointed aliases, not a second stylesheet.
Two entry points: the OS preference (overridable with data-theme="light")
and an explicit data-theme="dark". `.theme-dark` is kept as a class-based
equivalent for hosts that can't set an attribute on :root.
No glow, no colored shadow, no neon accent: a dark theme here is the same
flat ink relationship with the values swapped. */
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--gl-color-surface-page: #0e0e0d;
--gl-color-surface-card: #151412;
--gl-color-surface-fill: #1d1c1a;
--gl-color-surface-fill-2: #252421;
--gl-color-border-hairline: #2c2b28;
--gl-color-border-default: #45443f;
--gl-color-text-primary: #eae8e4;
--gl-color-text-secondary: #a5a39e;
--gl-color-text-tertiary: #8c8a85;
}
}
:root[data-theme="light"] {
color-scheme: light;
--gl-color-surface-page: #f2f1ee;
--gl-color-surface-card: #f9f8f6;
--gl-color-surface-fill: #e8e7e3;
--gl-color-surface-fill-2: #deddd8;
--gl-color-border-hairline: #cdcbc6;
--gl-color-border-default: #a9a7a1;
--gl-color-rule: var(--gl-color-text-primary);
--gl-color-text-primary: #161512;
--gl-color-text-secondary: #4f4d48;
--gl-color-text-tertiary: #6a6862;
}
:root[data-theme="dark"], .theme-dark {
color-scheme: dark;
--gl-color-surface-page: #0e0e0d;
--gl-color-surface-card: #151412;
--gl-color-surface-fill: #1d1c1a;
--gl-color-surface-fill-2: #252421;
--gl-color-border-hairline: #2c2b28;
--gl-color-border-default: #45443f;
--gl-color-text-primary: #eae8e4;
--gl-color-text-secondary: #a5a39e;
--gl-color-text-tertiary: #8c8a85;
}
/* Utopia fluid type scale — the config shared by quire, ketchup and domus:
viewport 320→1240, body 18→20px, ratio 1.20→1.25. Adopted verbatim. */
:root {
--gl-step--2: clamp(0.7813rem, 0.7736rem + 0.0341vw, 0.8rem); /* @kind other */
--gl-step--1: clamp(0.9375rem, 0.9119rem + 0.1136vw, 1rem); /* @kind other */
--gl-step-0: clamp(1.125rem, 1.0739rem + 0.2273vw, 1.25rem); /* @kind other */
--gl-step-1: clamp(1.35rem, 1.2631rem + 0.3864vw, 1.5625rem); /* @kind other */
--gl-step-2: clamp(1.62rem, 1.4837rem + 0.6057vw, 1.9531rem); /* @kind other */
--gl-step-3: clamp(1.944rem, 1.7405rem + 0.9044vw, 2.4414rem); /* @kind other */
--gl-step-4: clamp(2.3328rem, 2.0387rem + 1.3072vw, 3.0518rem); /* @kind other */
--gl-step-5: clamp(2.7994rem, 2.384rem + 1.8461vw, 3.8147rem); /* @kind other */
/* ── families ──────────────────────────────────────────────────────
No webfonts for now — the system UI face reads (headings, body, item
names, control labels, values) and the system mono files (section
rules, catalog labels, tags, buttons, dates, figures, status). Mono
is the ledger furniture, not the whole interface. Point these two
tokens at real faces later and nothing else changes.
--gl-font-serif stays as an opt-in editorial escape hatch. */
--gl-font-text: system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
--gl-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
--gl-font-serif: Georgia, "Times New Roman", serif;
--gl-font-read: var(--gl-font-text); /* semantic alias — prose */
--gl-font-ui: var(--gl-font-mono); /* semantic alias — chrome */
}
body { font-family: var(--gl-font-text); font-size: var(--gl-step-0); line-height: 1.6; }
/* Headings: medium, tightened. Keep measures at 65-75ch.
The scale steps by 1.2-1.25, which is barely a step. Skip one between a
heading and the text under it (h2 over body, not h3 over body) or the
hierarchy reads flat. */
.type-display { font: 500 var(--gl-step-4)/1.04 var(--gl-font-text); letter-spacing: -0.035em; }
.type-h1 { font: 500 var(--gl-step-3)/1.1 var(--gl-font-text); letter-spacing: -0.03em; }
.type-h2 { font: 500 var(--gl-step-2)/1.18 var(--gl-font-text); letter-spacing: -0.025em; }
.type-h3 { font: 500 var(--gl-step-1)/1.3 var(--gl-font-text); letter-spacing: -0.015em; }
.type-body { font: 400 var(--gl-step-0)/1.6 var(--gl-font-text); }
.type-body-sm { font: 400 var(--gl-step--1)/1.6 var(--gl-font-text); }
/* ── the section rule: this system's one repeated gesture ────────────
Every section head is a mono label sitting on a 1px ink rule that runs
the full measure. Ink is what makes it the rule; the hairlines elsewhere
are the same weight in a lighter colour. Use it for every section head
and nowhere else.
IMPORTANT: .type-section IS the heading. It never sits above another
heading as a kicker or eyebrow. A tracked uppercase label stacked over a
display headline is the single most common generated-UI hero, and this
class is one careless nesting away from producing it. */
.type-section {
display: block; width: 100%;
font: 500 var(--gl-step--2)/1 var(--gl-font-mono);
text-transform: uppercase; letter-spacing: 0.1em;
color: var(--gl-color-text-primary);
padding-bottom: var(--gl-space-2xs);
border-bottom: var(--gl-rule-w) solid var(--gl-color-rule);
}
/* Interface type: mono. Labels and meta are uppercase and tracked (catalog
tags); values and technical strings are sentence case. Uppercase plus
tracking is for three words or fewer. Running text is never uppercase and
never tracked past 0.05em: word shape is how people read. */
.type-label { font: 500 var(--gl-step--2)/1 var(--gl-font-mono); text-transform: uppercase; letter-spacing: 0.08em; color: var(--gl-color-text-secondary); }
.type-mono { font: 400 var(--gl-step--1)/1.5 var(--gl-font-mono); }
/* Figures in a ledger column: mono, lining, right-aligned by the column. */
.type-figure { font: 400 var(--gl-step--1)/1.5 var(--gl-font-mono); font-variant-numeric: tabular-nums; }
/* Utopia fluid space scale — same shared config as typography.css. */
:root {
--gl-space-3xs: clamp(0.3125rem, 0.3125rem + 0vw, 0.3125rem);
--gl-space-2xs: clamp(0.5625rem, 0.5369rem + 0.1136vw, 0.625rem);
--gl-space-xs: clamp(0.875rem, 0.8494rem + 0.1136vw, 0.9375rem);
--gl-space-s: clamp(1.125rem, 1.0739rem + 0.2273vw, 1.25rem);
--gl-space-m: clamp(1.6875rem, 1.6108rem + 0.3409vw, 1.875rem);
--gl-space-l: clamp(2.25rem, 2.1477rem + 0.4545vw, 2.5rem);
--gl-space-xl: clamp(3.375rem, 3.2216rem + 0.6818vw, 3.75rem);
--gl-space-2xl: clamp(4.5rem, 4.2955rem + 0.9091vw, 5rem);
--gl-space-3xl: clamp(6.75rem, 6.4432rem + 1.3636vw, 7.5rem);
}
/* One radius, 3px, everywhere. A scale of radii is a decision made three
times; this system makes it once. --gl-radius-full is only for true
pills (switch tracks). --gl-radius-sm/md/lg are kept as aliases so
older code keeps working — they all resolve to the same 3px. */
:root {
--gl-radius: 3px;
--gl-radius-sm: var(--gl-radius);
--gl-radius-md: var(--gl-radius);
--gl-radius-lg: var(--gl-radius);
--gl-radius-full: 999px;
/* Two line weights, each with exactly one job.
hairline (1px, --gl-color-border-hairline) — divides ledger rows.
rule (1px, ink) — under section heads.
Both are 1px: what separates them is ink versus hairline, not weight.
A heavier rule reads as a chart axis; this one reads as type.
Four-sided borders are not part of this system. */
--gl-border-w: 1px;
--gl-rule-w: 1px;
/* Elevation is not a language here: flat is the default and there is no
resting shadow. --gl-shadow-float is for the one thing genuinely
floating above the page (dialog, menu, toast); --gl-shadow-sm exists
only for the switch knob. */
--gl-shadow-sm: 0 1px 2px rgba(24, 20, 14, 0.14);
--gl-shadow-float: 0 2px 6px rgba(24, 20, 14, 0.07), 0 14px 32px -14px rgba(24, 20, 14, 0.22);
--gl-ease: cubic-bezier(0.2, 0, 0, 1); /* @kind other */
--gl-dur-fast: 0.12s; /* @kind other */
--gl-dur-base: 0.16s; /* @kind other */
}
@media (prefers-reduced-motion: reduce) {
:root {
--gl-dur-fast: 0ms; /* @kind other */
--gl-dur-base: 0ms; /* @kind other */
}
}
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: none; text-size-adjust: none; }
body { margin: 0; background: var(--gl-color-surface-page); color: var(--gl-color-text-primary); -webkit-font-smoothing: antialiased; }
a { color: var(--gl-color-accent-ink); text-underline-offset: 2px; }
a:hover { color: var(--gl-color-accent); }
[x-cloak] { display: none !important; }
/* ── the ledger ────────────────────────────────────────────────────────
Lists in this system are not stacks of cards. Content sits on ruled rows
that run the full width of their column, edge to edge, divided by a
single hairline and nothing else — no container border, no container
background, no gaps between rows. */
.gl-ledger { display: flex; flex-direction: column; }
.gl-ledger > * + * { border-top: var(--gl-border-w) solid var(--gl-color-border-hairline); }
.gl-ledger-row {
display: flex; align-items: center; gap: var(--gl-space-s);
padding: var(--gl-space-xs) 0;
transition: background var(--gl-dur-fast) var(--gl-ease);
}
/* Hover shifts the stock one step deeper, bled slightly past the measure so
the row still reads as full-bleed rather than as a highlighted box. */
.gl-ledger-row:hover { background: var(--gl-color-surface-fill); box-shadow: -8px 0 0 var(--gl-color-surface-fill), 8px 0 0 var(--gl-color-surface-fill); }