Every decision a component is not allowed to make for itself. These are read out of tokens.css at build time and measured again in the browser, so what is printed here is what the page is actually running.
Source src/assets/css/tokens.cssAppearances Light and dark, defined in full in both
How the palette is arranged
Two layers. Underneath, a palette of literal values, declared once for the light appearance and again for the dark. Above it, a set of working names that every selector in wrenfield.css reaches for instead. Nothing in the component sheet names a colour; that is what lets one seed change the firm's whole appearance without touching a rule.
The two layers
A component asks for --accent. Only tokens.css knows that this is --brand, and only the palette knows that --brand is a particular green.
Palette
Literal values. --ink, --paper, --brand, --rule. Declared twice, once per appearance, so no value is ever derived at runtime and no appearance is an afterthought.
Working names
Aliases with jobs rather than colours. --bg, --text, --accent, --field. A component only ever names these.
The rest of the sheet
922 lines that between them mention no colour at all.
Ground and ink
The paper and what is written on it. Every surface in the site is one of these four.
6 tokens
Declared value on the left of the slash, dark on the right. Underneath, what the browser resolved it to just now.
tokens.css
--paper#FBFAF7 / #0B0B0B
--paper-2#F1EFE9 / #141414
--ink#14162E / #F0EFED
--ink-2#2C2F4A / #C9C7C3
--muted#4E4C46 / #B8B5AF
--muted-2#5C5A54 / #A8A5A0
The ratios are measured live against the ground each colour is written on, and they move when you switch appearance. Four bands: 7 and over is AAA, 4.5 is AA, 3 clears large text only and is marked amber, and anything under that is called a failure. That is how a palette change announces that it has broken something.
Brand
One green, and the colour that stays legible on it. The brand switcher reseeds these and nothing else.
3 tokens
Declared value on the left of the slash, dark on the right. Underneath, what the browser resolved it to just now.
Reseed the brand in the bar above and these four do not move. The switcher overrides the working names — --accent, --accent-text, --on-accent and the rest — and leaves the palette underneath alone, which is why --brand still reads as the firm's green while every button on the page has gone coral. See Reseeding.
Rules and frames
The site separates with a line far more often than with a box, so the rules carry a lot of the structure.
4 tokens
Declared value on the left of the slash, dark on the right. Underneath, what the browser resolved it to just now.
tokens.css
--rule#D9D5CB / #333333
--rule-2#E7E4DC / #262626
--card#FFFFFF / #181818
--glassrgba(241,239,233,.72) / rgba(20,20,20,.92)
The panel
The inverted band, dark in both appearances. Its own accent is literal, because the light brand is unreadable here.
7 tokens
Declared value on the left of the slash, dark on the right. Underneath, what the browser resolved it to just now.
tokens.css
--invert-bg#14162E / #1A1A1A
--invert-bg-2#1B1E38 / #242424
--invert-fg#F2F1FA / #F5F4F2
--invert-fg-2#C3C6E6 / #CFCCC7
--invert-rule#2F3358 / #3A3A3A
--panel-accent#9FDCC0
--hero-ground#02170E / #000F08
The ratios are measured live against the ground each colour is written on, and they move when you switch appearance. Four bands: 7 and over is AAA, 4.5 is AA, 3 clears large text only and is marked amber, and anything under that is called a failure. That is how a palette change announces that it has broken something.
Working names
Aliases. Every selector in wrenfield.css reaches for one of these, never for a palette value, which is what lets the palette move underneath it.
17 tokens
Declared value on the left of the slash, dark on the right. Underneath, what the browser resolved it to just now.
tokens.css
--bgvar(--paper)alias
--raisedvar(--paper-2)alias
--sunkenvar(--paper-2)alias
--textvar(--ink)alias
--accentvar(--brand)alias
--accent-textvar(--brand)alias
--on-accentvar(--on-brand)alias
--focusvar(--brand)alias
--sparkvar(--brand)alias
--fieldvar(--rule)alias
--errorvar(--brand)alias
--hero-btn-bgvar(--brand)alias
--hero-btn-fgvar(--on-brand)alias
--panelvar(--invert-bg)alias
--panel-textvar(--invert-fg)alias
--panel-mutedvar(--invert-fg-2)alias
--panel-rulevar(--invert-rule)alias
Corners
A law firm's corners, not an app's: tight enough that a card reads as a printed panel with the corner knocked off.
6 tokens
Declared value on the left of the slash, dark on the right. Underneath, what the browser resolved it to just now.
tokens.css
--radius-xs2px
--radius-sm3px
--radius-md4px
--radius-lg6px
--radius-chip3px
--radius-pill999px
Lift
Three shadows and no more. A card at rest, a card under the pointer, and the brand's own glow under a button.
3 tokens
Declared value on the left of the slash, dark on the right. Underneath, what the browser resolved it to just now.
Two durations and one curve. Everything that moves on the site moves on these.
3 tokens
Declared value on the left of the slash, dark on the right. Underneath, what the browser resolved it to just now.
tokens.css
--dur-fast140ms
--dur-base220ms
--easecubic-bezier(.2,.8,.2,1)
Voices
Three faces with three jobs: the serif thinks, the sans speaks, the mono labels.
3 tokens
Declared value on the left of the slash, dark on the right. Underneath, what the browser resolved it to just now.
tokens.css
Aa--serif'Literata',Georgia,serif
Aa--sans'Public Sans',system-ui,sans-serif
Aa--mono"IBM Plex Mono",ui-monospace,monospace
Measure
How wide a thing is allowed to get, and how tall the bar is. These were split across two files until the catalogue asked which one was true.
5 tokens
Declared value on the left of the slash, dark on the right. Underneath, what the browser resolved it to just now.
tokens.css
--measure46rem
--wide70rem
--shell76rem
--col34rem
--bar5.5rem
Reseeding the brand
The control beside the appearance switcher takes one hex and derives the whole accent layer from it in OKLCH, then runs a contrast gate that darkens derived text until it clears AA and reports every adjustment it made. It is the same engine the site ships; the catalogue is simply the right place to stand while using it.
A trust that was never funded does none of the work it was written to do.
Turn on Both and reseed again: the light pane and the dark pane each take their own derivation of the same seed, because a seed is only good if it holds in both. The engine returns both maps at once and the catalogue writes the one the root is not currently using into a stylesheet of its own.
<div class="speccol">
<div>
<span class="speclab">Buttons and links</span>
<div class="specrow"><a class="btn" href="#">Request a consultation</a><a class="btn btn-ghost" href="#">Read about probate</a><a href="#">An inline link</a></div>
</div>
<div>
<span class="speclab">A card, a chip, a quotation</span>
<div class="specrow" style="align-items:stretch">
<div class="card" style="flex:1 1 15rem"><h3>Probate</h3><p>Eight to eighteen months in California.</p></div>
<div style="flex:1 1 15rem;display:grid;gap:1rem;align-content:start">
<div class="filterchips"><button type="button" class="chip" aria-pressed="true">Everything</button><button type="button" class="chip" aria-pressed="false">Probate</button></div>
<div class="pull" style="margin:0"><p>A trust that was never funded does none of the work it was written to do.</p></div>
</div>
</div>
</div>
</div>
<p class="specnote">Turn on <b>Both</b> and reseed again: the light pane and the dark pane each take their own derivation of the same seed, because a seed is only good if it holds in both. The engine returns both maps at once and the catalogue writes the one the root is not currently using into a stylesheet of its own.</p>
Two gates run on this page and they do not always agree. That disagreement is the useful part.
brand-engine.js and ds.js
The engine's gate, in the panel
Runs at the moment a seed is applied. It corrects derived text upward until it clears AA against the surface it was derived for, reports every adjustment, and says how many tokens are still below.
The catalogue's measurement, on this page
Runs afterwards, on what the browser actually resolved, against the ground each colour is really written on. It has no opinion about how a value was arrived at.
Where they part
Seed the brand with Coral and the engine reports all pass, while --accent-text measures 4.37 against the paper — enough for a heading, not enough for the links and body-sized accent text this site actually sets. Read the amber marks in Working names before shipping a seed.
Type
Three faces with three jobs. The serif thinks — every heading on the site is set in an italic Literata, which is unusual and is the whole voice. The sans speaks: body copy, navigation, buttons. The mono labels: eyebrows, counts, numerals, anything that is a fact rather than a sentence.
Set on the page, not described. Headings clamp between a handset and a desktop, so drag the window and watch them move.
h1 h2 h3 .lede p .eyebrow .label .mono
h1 · serif italic clamp(2.7rem, 7vw, 4.8rem)
A plan your family can actually use
h2 · serif italic clamp(1.9rem, 4vw, 2.7rem)
What the first conversation covers
h3 · serif italic 1.4rem
Trust administration
.lede · serif 300 24px
Most people arrive having meant to do this for a year.
body · sans 19px / 1.65
The documents that decide who acts for you and who receives what, written to be used rather than filed. Nineteen pixels and a generous line, because the people reading this are frequently reading it in a difficult week.
.eyebrow · mono 16px / .16em
Estate planning
.label · mono 15px / .13em
Selected matters
.mono · mono inherits its size
310-555-0142
.muted · any voice --muted
A second rank of text, still above 4.5 against the paper.
<dl class="scale">
<div><dt>h1 · serif italic<br>clamp(2.7rem, 7vw, 4.8rem)</dt><dd><h1>A plan your family can actually use</h1></dd></div>
<div><dt>h2 · serif italic<br>clamp(1.9rem, 4vw, 2.7rem)</dt><dd><h2>What the first conversation covers</h2></dd></div>
<div><dt>h3 · serif italic<br>1.4rem</dt><dd><h3>Trust administration</h3></dd></div>
<div><dt>.lede · serif 300<br>24px</dt><dd><p class="lede">Most people arrive having meant to do this for a year.</p></dd></div>
<div><dt>body · sans<br>19px / 1.65</dt><dd><p>The documents that decide who acts for you and who receives what, written to be used rather than filed. Nineteen pixels and a generous line, because the people reading this are frequently reading it in a difficult week.</p></dd></div>
<div><dt>.eyebrow · mono<br>16px / .16em</dt><dd><p class="eyebrow">Estate planning</p></dd></div>
<div><dt>.label · mono<br>15px / .13em</dt><dd><span class="label">Selected matters</span></dd></div>
<div><dt>.mono · mono<br>inherits its size</dt><dd><p class="mono">310-555-0142</p></dd></div>
<div><dt>.muted · any voice<br>--muted</dt><dd><p class="muted">A second rank of text, still above 4.5 against the paper.</p></dd></div>
</dl>
How wide a line of text is allowed to get before it becomes hard to return from. The site keeps two: one for reading, one for a page.
--measure --wide
--measure
--wide
--bar
Prose sits on --measure. A section opener is allowed --wide, because a short line of large type reads as a caption rather than as a sentence.
<div class="ruler">
<div><b>--measure</b><i style="width:min(100%,var(--measure))"></i></div>
<div><b>--wide</b><i style="width:min(100%,var(--wide))"></i></div>
<div><b>--bar</b><i style="width:var(--bar)"></i></div>
</div>
<p class="specnote">Prose sits on <code class="mono">--measure</code>. A section opener is allowed <code class="mono">--wide</code>, because a short line of large type reads as a caption rather than as a sentence.</p>
Corners, lift and motion
The three things that decide whether the site reads as a printed firm or as an app. It is trying quite hard to read as a printed firm.
Six values, none of them soft. A card should read as a printed panel with the corner knocked off.
--radius-xs … --radius-pill
xs · 2px
sm · 3px
md · 4px
lg · 6px
chip · 3px
pill · 999px
--radius-pill is for the things that are genuinely circles: a bullet, a swatch, an avatar. Pill shaped controls take --radius-chip, so buttons, nav chips and hero tags move together when the corner changes.
<div class="specrow is-loose">
<div><span class="speclab">xs · 2px</span><span style="display:block;width:5rem;height:3.5rem;background:var(--accent);border-radius:var(--radius-xs)"></span></div>
<div><span class="speclab">sm · 3px</span><span style="display:block;width:5rem;height:3.5rem;background:var(--accent);border-radius:var(--radius-sm)"></span></div>
<div><span class="speclab">md · 4px</span><span style="display:block;width:5rem;height:3.5rem;background:var(--accent);border-radius:var(--radius-md)"></span></div>
<div><span class="speclab">lg · 6px</span><span style="display:block;width:5rem;height:3.5rem;background:var(--accent);border-radius:var(--radius-lg)"></span></div>
<div><span class="speclab">chip · 3px</span><span style="display:block;width:5rem;height:3.5rem;background:var(--accent);border-radius:var(--radius-chip)"></span></div>
<div><span class="speclab">pill · 999px</span><span style="display:block;width:5rem;height:3.5rem;background:var(--accent);border-radius:var(--radius-pill)"></span></div>
</div>
<p class="specnote"><code class="mono">--radius-pill</code> is for the things that are genuinely circles: a bullet, a swatch, an avatar. Pill shaped controls take <code class="mono">--radius-chip</code>, so buttons, nav chips and hero tags move together when the corner changes.</p>
Two durations and one curve, and everything that moves uses them. Hover the squares.
--dur-fast --dur-base --ease
fast · 140ms
base · 220ms
Fast is for a thing answering you — an underline drawing itself, a chevron turning. Base is for a thing arriving — a drawer, a scrim, a panel. Everything that moves is also written to stand still under prefers-reduced-motion.
<div class="specrow is-loose">
<div><span class="speclab">fast · 140ms</span><span class="dsmove" style="display:block;width:6rem;height:3.5rem;background:var(--accent);border-radius:var(--radius-sm);transition:transform var(--dur-fast) var(--ease)"></span></div>
<div><span class="speclab">base · 220ms</span><span class="dsmove" style="display:block;width:6rem;height:3.5rem;background:var(--accent);border-radius:var(--radius-sm);transition:transform var(--dur-base) var(--ease)"></span></div>
</div>
<p class="specnote">Fast is for a thing answering you — an underline drawing itself, a chevron turning. Base is for a thing arriving — a drawer, a scrim, a panel. Everything that moves is also written to stand still under <code class="mono">prefers-reduced-motion</code>.</p>
<style>.dsmove:hover{transform:translateX(2.5rem)}@media (prefers-reduced-motion:reduce){.dsmove{transition:none !important}}</style>