src/styles/tokens.css (25277B)
1 /* Design tokens. Type, spacing and grid values are measured from two reference sites,
2 The Harvard Gazette and nicholas.carlini.com; the colour ladder is re-derived around a
3 crimson anchor. Single light theme — neither
4 reference site ships a dark mode. */
5
6 :root {
7 color-scheme: light;
8
9 /* Colour: five families x four steps. base sits at OKLCH L=.4701 with chroma at .901 of
10 the gamut maximum, both taken from the crimson anchor #a51c30.
11 dark -> --ppc-text (kickers)
12 base -> --ppc-ui (rules, quote bars, focus rings)
13 bright -> on black only
14 wash -> emphasis backgrounds
15 All 20 steps are contrast-checked against --page-bg, bright against black: dark >= 4.5,
16 base >= 3.0, bright >= 4.5 on black, wash <= 1.25. */
17 --crimson-dark: #771120; --crimson: #a51c30; --crimson-bright: #faa2a2; --crimson-wash: #f7ebeb;
18
19 /* Hover step for a link in running text, and the one colour here that is not a rung of
20 the ladder above — it sits between --crimson and --crimson-bright, which is why it is
21 declared apart rather than pretending to be a fifth step.
22
23 Derived from the reference rather than picked: its body links go rgb(160,32,32) at
24 rest to rgb(192,48,48) on hover, measured over CDP on its own article page, which is
25 a luminance increase of x1.572. The same factor applied to --crimson lands here.
26 Contrast: 5.30:1 on --page-bg, so it stays over the 4.5:1 normal text needs while
27 spending the headroom the way the reference spends it. Against the black body text
28 beside it this reads 3.84:1, where --crimson at rest is 2.81:1 — so the hovered link
29 is the one state that clears the 3:1 a colour-only link is asked for. */
30 --crimson-lift: #cb233b;
31 --blue-dark: #0e455f; --blue: #186285; --blue-bright: #70cafa; --blue-wash: #e6f0f7;
32 --ochre-dark: #553a0c; --ochre: #775315; --ochre-bright: #f6ae30; --ochre-wash: #f7ede0;
33 --purple-dark: #5e127b; --purple: #831daa; --purple-bright: #dda3fa; --purple-wash: #f3ebf7;
34 --slate-dark: #453f34; --slate: #605a4e; --slate-bright: #c4bdb0; --slate-wash: #f5eee0;
35
36 /* Neutrals */
37 --black: #000000;
38 --grey: #b9b8ad; /* hairlines only */
39 --sand: #faedda; /* surface: code plate, inline code, kbd, mark */
40 --sand-light: #fffbf4; /* page background */
41 --sand-dark: #eed6aa; /* borders */
42
43 /* Page roles. A section overrides the --ppc-* trio. */
44 --page-bg: var(--sand-light);
45 --page-fg: var(--black);
46 --page-meta: var(--slate);
47 --page-hairline: var(--grey);
48 --ppc-text: var(--crimson-dark);
49 --ppc-ui: var(--crimson);
50 --ppc-wash: var(--crimson-wash);
51
52 /* ── Type scale ────────────────────────────────────────────────────────────
53 Every fluid step is written as clamp(MIN, A vw + B rem, MAX) with
54
55 A = (MAX - MIN) x 1600 / 1168 (rem -> vw) B = MIN - 0.2 A
56
57 so the middle term equals MIN at exactly 320px and MAX at exactly 1488px,
58 rather than merely approaching them. Rounded to four decimals the residual at
59 either endpoint is under 0.002px on every row. Not every clamp in this file shares
60 these ends: --fs-code runs from 375 to 1440 and --root-pad has its own, each
61 explained at the token, and the spacing clamps (--sp-32 to --sp-120) keep theirs.
62
63 Derived from Lora's outlines rather than scaled by a constant off the
64 Newsreader/Inter ladder it replaced. Measured on the shipped subset
65 (public/fonts/latin-9bba8f25.woff2, wght 400, upem 1000): x-height .500em,
66 cap .700em, b-ascender .755em, p-descender -.255em. Inter's x-height is
67 .5459em, so an unchanged px size already draws 8.4% less lowercase on Lora;
68 a constant-factor rescale would bury that instead of spending it.
69
70 The anchor is apparent size, not px. The reference this design was pointed at
71 sets the same face at 14pt = 18.667px, which is 9.333px of x-height. The ladder
72 is cut against a 19px body = 9.500px of x-height, 1.8% above the reference and
73 13.0% below a 20px Inter (10.92px) - a 13% cut in what the eye measures out of a
74 5% cut in px. Running text itself is a flat 16px (see --fs-body); the headings
75 keep the 19px anchor.
76
77 The ladder is then a multiple of that anchor. The reference renders nothing above
78 1.50x body anywhere; ours has to carry a real page title on pages with no image
79 and no other display element, so at 1488 h1 is 2.0x the anchor (38px), h2 the
80 reference's own 1.50x (28.5px), and the rest interpolates. Against the 16px body
81 that ships:
82
83 h1 h2 h3 h4 h5 h6 body
84 1488px 38.0 28.5 24.5 22.0 20.0 17.0 16.0
85 2.38x 1.78x 1.53x 1.38x 1.25x 1.06x
86 320px 27.0 23.0 21.0 19.5 18.0 17.0 16.0
87 1.69x 1.44x 1.31x 1.22x 1.13x 1.06x
88
89 h6 is 17, not 18, so the ladder stays strictly monotone at 320px, where h5's
90 floor is 18. No post writes ##### or ######, but a post that contains an h1 has
91 every heading moved down one level (satteriProse), so its #### renders as h5.
92 .entry__title on the Writing index is --fs-h5 as well, so h5 is both the deepest
93 article heading and the index entry title: 20px at 1488 against the 16px body,
94 where the reference sets its own index entries at exactly body size.
95
96 --fs-small and --fs-medium sit outside the ladder. --fs-small sets the small print
97 (footer, byline, labels, the contents rail) and --fs-medium the secondary text
98 (captions, tables, footnotes, the inline contents list). Both already land at or
99 below body size, and cutting --fs-small would push the small print under 11px. They
100 keep the same 320-1488 shape, 11->14 and 12->16. The nav and the page name have their
101 own token, --fs-nav, and --fs-code has its own note at the token. */
102 --fs-small: clamp(0.6875rem, 0.2568vw + 0.6361rem, 0.8750rem); /* 11→14 */
103 --fs-medium: clamp(0.7500rem, 0.3425vw + 0.6815rem, 1.0000rem); /* 12→16 */
104
105 /* Body is a flat 16px, not a clamp, because that is what the reference is.
106 Measured on nicholas.carlini.com/writing/2025/regex-chess.html at a 1440px viewport:
107 body 16px, line-height 27.2px, paragraph box 928px — the same measure this site
108 uses. It never declares a font-size at all, so its body is the browser default and
109 does not move with the viewport. A body that grows with the viewport reads as too
110 large overall: a 16→19px ramp on the 320-1488 shape is already 18.9px at 1440, 18%
111 over the reference's 16.
112
113 Growth with the viewport is the thing avoided, so this is not a shallower clamp
114 either. The heading ladder above keeps its own clamps, which this line does not
115 touch; only its ratios to body differ from the 19px anchor it was cut against — h1
116 is 2.375x at 1488 rather than 2.00x — and the table above gives them against this
117 16px. */
118 --fs-body: 1rem; /* 16, fixed */
119
120 /* The header is measured from the reference's own header, not from body.
121 It sets its title at 150% and its nav at 120% of a 16px root: 24px and 19.2px. Hung
122 off --fs-body instead, with the nav at body size and the name at 1.25x of it, the
123 bar would read 20/16 against the flat 16px body. This token carries the nav size and .page-name takes 1.25x of it, which is the
124 reference's own 24:19.2 ratio.
125
126 It is a clamp where body is not, because the reference collapses its header into a
127 hamburger on a phone and this one wraps instead: a flat 19.2px nav wraps to a third
128 row at 320px, where 16px keeps the bar to two. */
129 --fs-nav: clamp(1.0000rem, 0.2740vw + 0.9452rem, 1.2000rem); /* 16→19.2 */
130 --fs-h6: 1.0625rem; /* 17, fixed */
131 --fs-h5: clamp(1.1250rem, 0.1712vw + 1.0908rem, 1.2500rem); /* 18→20 */
132 --fs-h4: clamp(1.2188rem, 0.2140vw + 1.1759rem, 1.3750rem); /* 19.5→22 */
133 --fs-h3: clamp(1.3125rem, 0.2997vw + 1.2526rem, 1.5313rem); /* 21→24.5 */
134 --fs-h2: clamp(1.4375rem, 0.4709vw + 1.3433rem, 1.7813rem); /* 23→28.5 */
135 --fs-h1: clamp(1.6875rem, 0.9418vw + 1.4991rem, 2.3750rem); /* 27→38 */
136
137 /* Unreferenced by any rule in src/. Kept because the names are part of the token
138 contract, and sized on the ladder's own 320-1488 shape so that anything which later
139 picks one up lands near the scale rather than on a display step far above it.
140 --fs-h1-large is also named in src/pages/writing/[year]/[slug].astro, where the
141 article title's note says why the title does not use it. */
142 --fs-xlarge: clamp(1.2500rem, 0.5137vw + 1.1473rem, 1.6250rem); /* 20→26 */
143 --fs-2xlarge: clamp(1.6250rem, 0.5137vw + 1.5223rem, 2.0000rem); /* 26→32 */
144 --fs-h1-large: clamp(2.0000rem, 1.1986vw + 1.7603rem, 2.8750rem); /* 32→46 */
145
146 /* Hooks for src/pages/writing/index.astro, which is written as a portable
147 component: it re-declares --fs-h1 / --fs-h5 / --fs-large from
148 var(--wp-*, <its own literal clamp>), and those literals are a larger ladder than
149 this one (34→64, 20→24 and 16→20). Without --wp-h5 the entry titles would set at
150 20→24 while article headings follow the ladder above - and every entry title on
151 the site is on that one page. --wp-h1 sizes only the index's .sr-only page title,
152 which is never drawn, and --wp-large only the empty-state line, which a build with
153 any post never renders. This is the same mechanism fonts.css uses to put the index
154 on the shipped face through --font-sans / --font-serif.
155
156 Written as literal clamps, not var(--fs-h1). Substitution happens at the
157 declaring element, so :root { --wp-h1: var(--fs-h1) } would in fact compute to
158 :root's --fs-h1 and inherit down cleanly - but the index assigns the result
159 back into --fs-h1 on .index, and handing a property to itself through an alias
160 is not a thing to be clever about when the failure mode is every entry title
161 losing its font-size. Keep --wp-h1 and --wp-h5 in step with --fs-h1 and --fs-h5.
162 --wp-large is not --fs-body: it is a 16→19 ramp, not the flat 16px. */
163 --wp-h1: clamp(1.6875rem, 0.9418vw + 1.4991rem, 2.3750rem); /* = --fs-h1 */
164 --wp-h5: clamp(1.1250rem, 0.1712vw + 1.0908rem, 1.2500rem); /* = --fs-h5 */
165 --wp-large: clamp(1.0000rem, 0.2568vw + 0.9486rem, 1.1875rem); /* 16→19, not --fs-body */
166
167 /* Line heights. Cut for Newsreader and kept for Lora on measurement, not by
168 oversight - the tempting number to react to is the wrong one.
169
170 Lora's metrics box is sTypoAscender + sTypoDescender = 1.006 + 0.274 = 1.280em,
171 against exactly 1.000em for the Newsreader this ladder was cut for, which looks
172 like every step below 1.28 has become an overlap. It has not. Lines collide when
173 ink meets ink, and ink is what barely moved: measured on the shipped subset,
174 Lora's lowercase and parentheses run -0.271em to +0.760em, a 1.031em band, against
175 1.033em for Newsreader, the face the ladder was set against (its 0.9745em band at the
176 106% size-adjust it used): the band is 0.2% narrower than the one these steps assume.
177
178 What the larger metrics box does is shift the half-leading. At --lh-tight the
179 half-leading is (1.1 - 1.280) / 2 = -0.090em, so the line box spans +0.916em to
180 -0.364em around the baseline and the ink sits inside it with room at both ends:
181 nothing clips, and consecutive lines clear by 1.1 - 1.031 = 0.069em. That is
182 2.6px on the 38px h1 - the clearance is a ratio, so a smaller size shrinks it
183 in px but never closes it. Korean headings are looser still - Hangul ink spans
184 at most 0.840em once KoBody's 94.29% is applied.
185
186 The one glyph that would break this is $, which tops out at 0.833em; it appears
187 only inside code, which sets --lh-base. */
188 --lh-tight: 1.1; /* h1 */
189 --lh-snug: 1.2; /* h2, h3 */
190 --lh-close: 1.25; /* h4 */
191 --lh-medium: 1.4; /* h5, h6 */
192 --lh-base: 1.5;
193 --lh-large: 1.6; /* body */
194
195 /* Spacing */
196 --sp-20: 0.44rem; --sp-30: 0.67rem; --sp-40: 1rem; --sp-50: 1.5rem;
197 --sp-60: 2.25rem; --sp-70: 3.38rem;
198 --sp-24: 1.5rem; /* heading-adjacent rhythm */
199 /* Between paragraphs, and the one spacing token deliberately written in em rather than
200 rem. The reference sets `p { margin-top: 0; margin-bottom: .7em }`, which is 11.2px
201 against its 16px body; --sp-24 is 24px, 2.1x that, and reads as too much air. A
202 custom property substitutes as a token stream, so the em resolves against the font
203 size of whichever element consumes it — which is what keeps the gap proportional to
204 the type instead of frozen at one pixel value. Read by three rules and no others:
205 `p + p` in base.css, `.prose > p + p` and `.prose blockquote p + p` in the article
206 template. It is NOT the paragraph-adjacent rhythm: a heading followed by a paragraph
207 keeps --sp-24, because the heading's job is to open a section rather than to continue
208 one. Korean check, where the risk is that the gap disappears into the leading: at
209 --lh-ko 1.75 the line box is 28px and Hangul's ink band fills 0.84em of it, so the
210 white between two lines is 14.6px and the white across a paragraph boundary is
211 25.8px — still 1.77x, which still reads as a boundary. */
212 --sp-para: 0.7em; /* between paragraphs */
213 --sp-32: clamp(1.5rem, 0.5vw + 1.4rem, 2rem);
214 --sp-48: clamp(2rem, 1vw + 1.8rem, 3rem); /* block rhythm */
215 --sp-64: clamp(2.5rem, 2.2vw + 2rem, 4rem);
216 --sp-80: clamp(2.5rem, 2.5vw + 2rem, 5rem);
217 --sp-120: clamp(4rem, 3.5vw + 3.3rem, 7.5rem);
218
219 /* ── Grid ──────────────────────────────────────────────────────────────────
220 --w-content is 928px: Carlini parity, chosen deliberately over a 728px column,
221 with the cost measured first and accepted.
222
223 Where the side whitespace comes from. Above the width at which the shell stops
224 growing (1024 + 2 x --root-pad, which --root-pad's own ramp puts at vw = 1063),
225 the distance from the viewport edge to the first prose character is
226
227 left = (vw - 96 - --w-content) / 2 + 96 right = (vw - 96 - --w-content) / 2
228
229 --root-pad cancels out of both: it is inside the shell's own max-width, so it
230 moves only the edge of main's own box, which paints nothing, never where the text
231 starts. At 1488px that is 328px left and 232px right whatever the padding does,
232 and --w-content is the only token on the page that can change it. A 728px column
233 would put it at 428 / 332, so 928 takes exactly 100px from each side.
234
235 What it costs. Measured over the paragraphs of the built articles, greedy-wrapped
236 on spaces only because word-break is keep-all, with Lora's advances and its 2,439
237 GPOS kern pairs, and Pretendard's uniform 1770/2048 Hangul advance at KoBody's
238 94.29%. cpl is the mean of the lines that actually wrap; the syllable figure is
239 what one full line holds:
240
241 728px / 16px English 94.5 cpl Korean 66.2 cpl 55.8 syllables
242 928px / 16px English 122.2 cpl Korean 85.4 cpl 71.2 syllables
243
244 The usual bands are 45-75 characters for English and 41-45 syllables for
245 Korean. Either width is past both at the 16px body; 928px holds more than half
246 again the Korean band's top, and English runs past 120 characters a line. The
247 column was chosen at a 19px body, where 928px held 59.9 syllables and about 100
248 English characters; the flat 16px body (see --fs-body) puts more on every line.
249 Article prose is about four fifths Hangul by letter count (see the KoBody note in
250 fonts.css), so that is the cost on most of the site, not an edge case. This block
251 records the trade. It is not an argument to reverse it, and no narrower fallback
252 is hiding below: 928px is the measure at every viewport wide enough to reach it.
253
254 Both pages hang off this token, and they do not share it. The Writing index
255 keeps its own --content-w, capped at min(100%, rail + gap + --content-w), so
256 the 100% branch follows a narrowing but nothing follows a widening. Were the
257 index to restate the width instead, moving this line alone would part the two:
258 with the index at 824px and the article at 1024px, the article title lands 100px
259 to the LEFT of the entry it was clicked from at 1488px and 1920px, and 81px left
260 at 1024px - the wider box is centred, so its left edge moves outward, not inward.
261 --content-w in src/pages/writing/index.astro therefore reads this token through
262 var() rather than re-stating it: a rem literal there matches only at a 16px root,
263 and at a 12px root the article <h1> sits 116px left of the entry title. Bound, the
264 two left offsets were compared in a browser at 320 / 768 / 1024 / 1488 / 1920 and
265 at 12 / 14 / 16 / 20 / 24px roots: identical to the pixel wherever the rail's band,
266 below, does not apply.
267
268 One bounded exception to that, named here because this is where a reader will
269 come looking for it. From 1440px, where the contents rail is shown, every article
270 pushes its page box right wherever a centred gutter cannot seat the rail's 240px
271 floor; the gutter is reserved whether or not the post has a rail - see the
272 ── the rail ── block in src/pages/writing/[year]/[slug].astro. Inside that band
273 the article title sits right of the index entry instead of matching it: measured
274 at a 16px root, 72px at 1440, 48 at 1488, 24 at 1536, and 0.00px from 1584 up and
275 below 1440. The band tracks the root size, because the floor is expressed in rem
276 along with the type it exists to fit - measured 0.00px at every width at a 12px
277 root, and at a 24px root it runs from 1440 to about 1912. Its worst case there is
278 195.91px at 1520, not the 156.80px it reads at the foot of the band: below 1521
279 the third, room term of --toc-rail-w is what binds, so --page-left tracks the
280 viewport and the divergence RISES with it until the 15rem floor takes over.
281 Sampling only the usual 1440 / 1488 / 1536 / 1584 columns misses that peak by 8px
282 and the foot of the band by 39px. That is the whole of the divergence: no width,
283 root size or article overflows horizontally, and the page box never loses its full
284 --w-page - measured 928px of measure and 0 horizontal overflow at 12 / 14 / 16 /
285 20 / 24px roots across 1400-2000px. */
286 --w-content: 928px;
287 /* Page box: the 72px date rail, its 24px gutter, and the measure. The shell, the
288 header and footer padding and the rail geometry read it; the index and the article
289 compute the same sum from --w-content, which is what puts their text on one left
290 edge. */
291 --w-page: calc(var(--w-content) + 4.5rem + 1.5rem); /* 1024px */
292 --w-wide: 1488px;
293 /* Outer gutter, 16 -> 24px. The reference site runs a flat 1em at every width;
294 this starts there and grows only to 24px rather than going fixed.
295
296 The shape is deliberate and differs from the type scale above: the middle term
297 is pinned at the 1rem floor for every viewport up to exactly 768px (1vw =
298 0.48rem there, so the bracket is zero and negative below it), and only then
299 ramps. A phone gutter must not ride a desktop ramp - the canonical
300 A vw + B rem form would put 19.1px of padding on a 768px screen and cost the
301 prose 6px it does not have. The factor sets where the cap lands: at 1.1111 the
302 term is exactly 1.5rem at 1488 ((0.93 - 0.48) x 1.1111 = 0.5), and the floor
303 holds 1rem at 320.
304
305 Above the width where the shell stops growing, the pad reaches neither the prose
306 nor the header's full-bleed rule: it sets the width of main's own box, 1024 + 2 x
307 --root-pad = 1072px at 1488, which nothing paints. The prose column does not move
308 at all - see the cancellation above. */
309 --root-pad: clamp(1rem, 1rem + ((1vw - 0.48rem) * 1.1111), 1.5rem); /* 16→24 */
310
311 /* ── Syntax highlighting ───────────────────────────────────────────────────
312 Astro builds the css-variables theme with `variablePrefix: '--astro-code-'`
313 and NO `variableDefaults` (@astrojs/internal-helpers/dist/shiki.js), and
314 @shikijs/core only emits a fallback when defaults are supplied
315 (dist/index.mjs: `return \`var(${prefix}${name})\``). So every token colour
316 ships as a bare var() with no fallback: leave one of these undefined and the
317 declaration is invalid at computed-value time, `color` falls back to the
318 inherited value, and the block renders as flat body text. They are not
319 optional — they ARE the palette.
320
321 Placement measured on the built DOM (dist, 52 plates, 2,230 token spans):
322 foreground 24.3% · string 21.3% · string-expression 14.8% · keyword 14.1%
323 punctuation 12.3% · function 6.3% · comment 3.9% · constant 3.1% · parameter 0.04%
324 Contrast recomputed against both surfaces, sand #faedda first (the real plate):
325 black 18.19/20.36 · purple-dark 9.84/11.01 · blue-dark 8.94/10.00
326 crimson-dark 9.65/10.80 · ochre-dark 9.11/10.19 · slate 5.93/6.63. */
327 --astro-code-background: transparent; /* figure.code paints the plate */
328 --astro-code-foreground: var(--black);
329 --astro-code-token-string: var(--black); /* bash: unquoted argument, not a string */
330 --astro-code-token-keyword: var(--purple-dark);
331 --astro-code-token-string-expression: var(--blue-dark);
332 --astro-code-token-punctuation: var(--slate);
333 --astro-code-token-function: var(--crimson-dark);
334 --astro-code-token-comment: var(--ochre-dark);
335 --astro-code-token-constant: var(--blue-dark);
336 --astro-code-token-parameter: var(--black);
337 --astro-code-token-link: var(--blue-dark);
338
339 /* Code body. The top of the ramp is 0.9 x the fixed --fs-body, not 1.0: a
340 monospace face at the same em reads larger than the text face beside it, because
341 every glyph carries the width of the widest one, so code set at body size looks
342 like the loudest thing in the article. 14.4px is the correction, and it is a
343 correction rather than a redesign - one notch, still above the 13px the reference's
344 own <pre> inherits from the UA monospace default against its 16px body.
345
346 It buys line width as well as quiet. The plate's inner box is 928 - 2*16 = 896px,
347 and a line's width is linear in the size, so 14.4 rather than 16 is +10% of column.
348 Measured over the built corpus - 52 plates, longest rendered line each, Consolas:
349 at 16.0px 4 plates overflow 896px
350 at 14.4px 3 plates overflow 896px
351 The one that stops scrolling is the widest line any author here would want kept
352 whole, a 110-column jsonc route table: 955px at 16, 859px at 14.4, 37px of slack.
353 Of the three that remain, one is a 132-column wget command that would need 12.4px,
354 under the 13px floor, and two are dumps, not code: a 701-column line of sysmon XML
355 config (2.3px) and a 1,639-column syslog line holding a raw sysmon XML event
356 (1.0px). They keep the scroller, or take ```<lang> wrap.
357
358 How many characters 896px is still depends on the face: 104 at 0.600em (JetBrains
359 Mono, Menlo), 113 at 0.5498em (Consolas, measured off the built page - a 132-column
360 line renders 72.57px per 1px of font-size). No single character count holds until
361 the site ships a monospace face of its own.
362
363 The ramp runs 13px at 375 to 14.4px at 1440, and the floor is 13px because a phone
364 gains little below it: at 375 the plate's inner box is about 300px (311 on a phone,
365 296 beside a classic scrollbar), some 40 columns at 13px, and half the plates run
366 wider than that, most of them far wider, so a smaller size would not stop them
367 scrolling. */
368 --fs-code: clamp(0.8125rem, 0.1315vw + 0.7817rem, 0.9rem); /* 13→14.4 */
369 --code-pad: var(--sp-40); /* 16px */
370 }
371
372 /* Per-section palette, swapped by a class on <body>. */
373 .is-writing { --ppc-text: var(--crimson-dark); --ppc-ui: var(--crimson); --ppc-wash: var(--crimson-wash); }
374 .is-code { --ppc-text: var(--blue-dark); --ppc-ui: var(--blue); --ppc-wash: var(--blue-wash); }
375 .is-talk { --ppc-text: var(--ochre-dark); --ppc-ui: var(--ochre); --ppc-wash: var(--ochre-wash); }
376 .is-papers { --ppc-text: var(--purple-dark); --ppc-ui: var(--purple); --ppc-wash: var(--purple-wash); }
377 .is-slate { --ppc-text: var(--slate-dark); --ppc-ui: var(--slate); --ppc-wash: var(--slate-wash); }