Reference , tokens

Style guide

Every token on this site comes directly from tokens.css, so the page stays in sync with the system. There are two tiers: primitives hold the raw values, while semantics give components meaningful tokens to use. Everything is theme- and direction-aware. Use the controls in the navigation to see them resolve. Or press i and hover anything.

Primitives

The raw values: hex values, rem steps, font stacks, curve definitions. Nothing on the site references these directly except the semantic layer; if a component needs one, it asks through an alias.

Colour , Direction A (warm)

Both palettes ship. The Warm / Cool switch in the navigation flips the whole site between them, and every semantic token resolves against whichever set is live.

paper

  • --paper-50 #fcfaf5
  • --paper-100 #f7f4ed

cream

  • --cream-100 #f0ede5

ink

  • --ink-875 #21201a
  • --ink-900 #1c1b17
  • --ink-925 #1b1a16
  • --ink-950 #14130f

grey

  • --grey-300 #8c8a82
  • --grey-400 #ada89b
  • --grey-500 #6b675c
  • --grey-600 #5c5950
  • --grey-700 #4a4842

line

  • --line-100 #e7e2d6
  • --line-200 #d9d2c0
  • --line-700 #3e3b31
  • --line-800 #2b2922

rust

  • --rust-400 #c97a3f
  • --rust-600 #8c4d1f

Colour , Direction B (cool)

chalk

  • --chalk-50 #fafaf8
  • --chalk-100 #f4f4f2

frost

  • --frost-100 #ececec

carbon

  • --carbon-875 #1c1c1e
  • --carbon-900 #161616
  • --carbon-925 #161617
  • --carbon-950 #0f0f10

steel

  • --steel-300 #888888
  • --steel-400 #ababab
  • --steel-500 #676767
  • --steel-600 #5a5a5a
  • --steel-700 #484848

seam

  • --seam-100 #e4e4e2
  • --seam-200 #d2d2cf
  • --seam-700 #3c3c3f
  • --seam-800 #28282a

cobalt

  • --cobalt-400 #6b8cff
  • --cobalt-600 #2a4abd

Spacing scale

Nine steps, each drawn at true size. The semantic taxonomy assigns these to directions; the scale itself carries no meaning about where a gap goes.

  • --space-1 0.25rem , 4px
  • --space-2 0.5rem , 8px
  • --space-3 0.75rem , 12px
  • --space-4 1rem , 16px
  • --space-6 1.5rem , 24px
  • --space-8 2rem , 32px
  • --space-12 3rem , 48px
  • --space-16 4rem , 64px
  • --space-24 6rem , 96px

Type families

  • --font-sans Hanken Grotesk Variable Hanken Grotesk, the workhorse voice
  • --font-serif Season Serif Season Serif, the licensed display face, headlines
  • --font-mono-ui JetBrains Mono Variable JetBrains Mono: eyebrows, labels, table headers, key caps
  • --font-mono-data JetBrains Mono Variable JetBrains Mono: numerals, dates, token names, hex, code

Type sizes

  • --text-display clamp(2.25rem, 3.7vw, 4.25rem) , 36–68px
  • --text-heading clamp(1.875rem, 3vw, 3rem) , 30–48px
  • --text-body clamp(1.125rem, 0.75rem + 0.4167vw, 1.25rem) , 18–20px
  • --text-small clamp(0.9375rem, 0.75rem + 0.2083vw, 1rem) , 15–16px
  • --text-caption clamp(0.8125rem, 0.625rem + 0.2083vw, 0.875rem) , 13–14px

Weights

  • --weight-body 380 body, lead, small, meta
  • --weight-label 400 mono labels, captions, table headers
  • --weight-heading 400 serif headings (h1–h3)
  • --weight-action 450 buttons, segmented controls, metric numerals, list item names
  • --weight-strong 550 wordmark

Leading

  • --leading-tight 1.08 page headline (H1)
  • --leading-snug 1.12 headings (H2, H3), list item names
  • --leading-relaxed 1.6 body, lead, small

Tracking

  • --tracking-tight -0.01em wordmark
  • --tracking-heading 0.02em serif headings (h1–h3)
  • --tracking-ui 0.04em sentence-case mono in the controls
  • --tracking-wide 0.08em mono labels and table headers

Border widths

  • --border-width-sm 1px
  • --border-width-md 2px
  • --border-width-lg 3px

Radii

  • --radius-sm 2px
  • --radius-md 4px
  • --radius-pill 999px fully rounded ends: pills, scrollbar track and thumb

Motion

Hover or focus a row and the dot crosses its track at that token’s duration, on the standard curve. The Reduce motion switch (and the OS preference) stills them.

  • --motion-fast 150ms hovers, small state changes
  • --motion-standard 300ms panels, disclosure
  • --motion-slow 450ms large moves, page-level shifts
  • --motion-press 80ms the press, and only the press: under --motion-fast so it lands under the finger
  • --ease-standard cubic-bezier(0.25, 0.1, 0.25, 1) the one easing curve, everywhere

Press

How far a control travels when you hold it down. Press and hold the square. The matching duration is --motion-press in the list above.

  • --press-scale 0.96 buttons, segments, icon buttons: anything thumb-sized

Density

--density is a unitless multiplier: 1 by default and 0.7 when the Compact switch in the navigation is on. Every semantic spacing token runs through it, which is why the whole page tightens with one switch. Flip it and watch the spacing bars in the Semantics view resize.

Semantics

Semantic tokens give components names they can rely on, like text-strong, instead of raw values. Change the theme and the tokens change with it, without changing the components.

Colour

Swatches use the live custom properties, so they always reflect the active theme and palette. The token name updates with them. Use the controls in the navigation to switch modes and see the colours change.

  • --color-surface-page → --paper-100
  • --color-surface-raised → --paper-50
  • --color-surface-band → --cream-100
  • --color-text-strong → --ink-900
  • --color-text-default → --grey-700
  • --color-text-subtle → --grey-500
  • --color-link-underline → --grey-400
  • --color-border-strong → --grey-500
  • --color-border-default → --line-200
  • --color-border-subtle → --line-100
  • --color-accent-default → --rust-600
  • --color-focus-ring → --rust-600

Spacing

Six concepts replace a flat spacing scale, in two families. inset is space inside a box and compiles to padding; gap is space between boxes and compiles to gap. Each family has a both-axis token and two axis-specific ones. Every token uses the primitive scale and responds to the density setting, so the bars below show the actual size. Read more about the approach on spacing tokens.

inset internal, both axes. Card padding.

  • --spacing-inset-xs = --space-1 × density
  • --spacing-inset-sm = --space-2 × density
  • --spacing-inset-md = --space-4 × density
  • --spacing-inset-lg = --space-6 × density

inset-block internal, vertical. Content spacing inside a panel.

  • --spacing-inset-block-xs = --space-1 × density
  • --spacing-inset-block-sm = --space-2 × density
  • --spacing-inset-block-md = --space-3 × density
  • --spacing-inset-block-lg = --space-6 × density
  • --spacing-inset-block-xl = --space-12 × density

inset-inline internal, horizontal. Left and right padding in buttons.

  • --spacing-inset-inline-xs = --space-2 × density
  • --spacing-inset-inline-sm = --space-3 × density
  • --spacing-inset-inline-md = --space-4 × density
  • --spacing-inset-inline-lg = --space-8 × density

gap external, both axes. The grid and flex gap shorthand.

  • --spacing-gap-xs = --space-1 × density
  • --spacing-gap-sm = --space-2 × density
  • --spacing-gap-md = --space-4 × density
  • --spacing-gap-lg = --space-8 × density
  • --spacing-gap-xl = --space-16 × density
  • --spacing-gap-2xl = --space-24 × density

gap-block external, vertical. Rows, stacked sections, list items.

  • --spacing-gap-block-xs = --space-1 × density
  • --spacing-gap-block-sm = --space-2 × density
  • --spacing-gap-block-md = --space-4 × density
  • --spacing-gap-block-lg = --space-8 × density
  • --spacing-gap-block-xl = --space-16 × density
  • --spacing-gap-block-2xl = --space-24 × density

gap-inline external, horizontal. Gaps between adjacent controls.

  • --spacing-gap-inline-xs = --space-2 × density
  • --spacing-gap-inline-sm = --space-3 × density
  • --spacing-gap-inline-md = --space-6 × density
  • --spacing-gap-inline-lg = --space-12 × density

Text styles

Each type role in type.css combines the font family, size, weight, line height, letter spacing and colour into one class, much like a Figma text style. The examples use the actual type styles, so you can see exactly what each one looks like. Apply a .type-* class directly, or use one of the combined selectors.

Page headline

.type-display

Inner-page hero on Work, About, Contact, Thinking, and both homepage heroes (the full-bleed one caps the size against viewport height).

Font
var(--font-serif) , Season Serif
Size
var(--text-display) , clamp(2.25rem, 3.7vw, 4.25rem) , 36–68px
Weight
var(--weight-heading) , 400
Line height
var(--leading-tight) , 1.08
Tracking
var(--tracking-heading) , 0.02em
Ligatures
font-feature-settings: 'dlig' 1 , ff fi fl
Colour
var(--color-text-strong)
Applies to
.page-head h1, .hero h1, .hero-full h1

Section heading

.type-heading

Section headings in prose pages, and the article title on a homepage Thinking piece.

Font
var(--font-serif) , Season Serif
Size
var(--text-heading) , clamp(1.875rem, 3vw, 3rem) , 30–48px
Weight
var(--weight-heading) , 400
Line height
var(--leading-snug) , 1.12
Tracking
var(--tracking-heading) , 0.02em
Ligatures
font-feature-settings: 'dlig' 1 , ff fi fl
Colour
var(--color-text-strong)
Applies to
.prose h2, .idea h2

Subheading

.type-subheading

Sub-section headings within prose. Body size on purpose: the serif family and heading weight mark it, not a size step.

Font
var(--font-serif) , Season Serif
Size
var(--text-body) , fluid on large screens , 18–20px
Weight
var(--weight-heading) , 400
Line height
var(--leading-snug) , 1.12
Tracking
var(--tracking-heading) , 0.02em
Ligatures
font-feature-settings: 'dlig' 1 , ff fi fl
Colour
var(--color-text-strong)
Applies to
.prose h3

Name , list item title

.type-name

The name of one item in a set, sitting over its own description: client names in the contracts lists, the audit-framework phase titles, the token names in the spacing-tokens article. Sans is what separates it from Subheading. Both are body size, so family and weight are all either style has to work with.

Font
var(--font-sans) , Hanken Grotesk
Size
var(--text-body) , fluid on large screens , 18–20px
Weight
var(--weight-action) , 450
Line height
var(--leading-snug) , 1.12
Tracking
normal
Colour
var(--color-text-strong)
Applies to
.rows .client, .phases h3

Lead / intro

.type-lead

The standfirst paragraph under a page headline.

Font
var(--font-sans) , Hanken Grotesk
Size
var(--text-body) , fluid on large screens , 18–20px
Weight
var(--weight-body) , 380
Line height
var(--leading-relaxed) , 1.6
Colour
var(--color-text-default)
Applies to
.page-head .intro

Body

.type-body

Default running text. Reading measure caps at 38em.

Font
var(--font-sans) , Hanken Grotesk
Size
var(--text-body) , fluid on large screens , 18–20px
Weight
var(--weight-body) , 380
Line height
var(--leading-relaxed) , 1.6
Colour
var(--color-text-default)
Applies to
body, p

Detail / UI text

.type-detail

Nav links, list-row descriptions, secondary controls.

Font
var(--font-sans) , Hanken Grotesk
Size
var(--text-small) , fluid on large screens , 15–16px
Weight
var(--weight-body) , 380
Line height
var(--leading-relaxed) , 1.6
Colour
var(--color-text-default)
Applies to
nav a, .rows p (buttons use var(--weight-action))

Eyebrow / label

.type-label

Mono caption above headlines and table headers. The editorial caption voice.

Font
var(--font-mono-ui) , JetBrains Mono
Size
var(--text-caption) , fluid on large screens , 13–14px
Weight
var(--weight-label) , 400
Tracking
var(--tracking-wide) , 0.08em
Case
uppercase
Colour
var(--color-text-default)
Applies to
.label, .token-table th

Meta / mono data

.type-meta

Dates and tabular values in list rows. Tabular figures keep columns aligned.

Font
var(--font-mono-data) , JetBrains Mono
Size
var(--text-caption) , fluid on large screens , 13–14px
Weight
var(--weight-body) , 380
Numerics
tabular-nums
Colour
var(--color-text-default)
Applies to
.rows .meta

Metric

.type-metric

The big accent numeral in a stats row: case-study outcomes, the About page facts.

Font
var(--font-mono-data) , JetBrains Mono
Size
var(--text-heading) , clamp(1.875rem, 3vw, 3rem) , 30–48px
Weight
var(--weight-action) , 450
Numerics
tabular-nums
Colour
var(--color-accent-default)
Applies to
.metrics .num, .facts .num

Wordmark

.type-wordmark

The studio name in the header.

Font
var(--font-sans) , Hanken Grotesk
Size
var(--text-small) , fluid on large screens , 15–16px
Weight
var(--weight-strong) , 550
Tracking
var(--tracking-tight) , -0.01em
Colour
var(--color-text-strong)
Applies to
.wordmark (header layers its own primitive/semantic colour)

Interaction states

Four states, each carried by a different property, so they can't be confused. Rest uses the component's own colour. Hover uses the accent (--color-accent-default) because here it signals a change of state. Focus is the ring, and nothing else. Pressed is geometry: the control scales inward on --press-scale, over --motion-press.

Two things don't scale. Links and text-only controls have no box to scale, and moving text reads as a rendering fault, so they keep the underline and accent on hover. Cards have the opposite problem: even a small scale reads as the page reflowing, so they use a directional cue instead.

Reduced motion keeps the press but removes the animation. The control still responds; it just arrives rather than travelling. Less movement doesn't mean less feedback.

Every control below is the real one, out of global.css. Press and hold any of them. The segmented control and the switch are the exceptions, and the table says why.

Switch

Every pressable on the site uses the same mechanism. It lives in one place: the pressable class in global.css. Components use that class unless they need different timing, in which case they use the same two lines and the same tokens.

Control Hover Pressed
Button .button Fills accent, text drops to the page surface. Scales to --press-scale.
Segmented control .seg button The unselected segments take the accent as text. Never the raised surface with strong text: that was the selected state's own two colours, swapped. Nothing, deliberately. A segment sits inside a shared border, so scaling it opens a gap between its fill and the frame. And the selection lands on the same event. By the time your finger is up the segment has inverted and the page has changed, so a press state would be decoration in front of feedback that has already arrived.
Toggle switch .wt-switch button Off, the border firms to the strong neutral and the knob darkens with it, not the accent, which on this control already means 'on'. On, that same neutral ring is invisible against the fill (1.2:1 in light mode), so the pill itself deepens instead, mixed off the accent toward the text colour so it holds in every theme. Either way the knob doesn't move: on a switch, movement means the state changing, and hover hasn't changed anything. The knob leans 0.2rem toward the end the release will send it to, on --motion-press. Not a scale: a squash on something this small read as a third state. The lean is the knob doing early what it is about to finish, so it completes the travel from 0.2rem further along, on its own slower ramp.
Card link .cs-pager__next Border warms to accent. The arrow goes right. A card is too wide to scale usefully, so the press moves the cue it already carries, in the direction the link goes: out on --motion-press, drifting back on the resting ramp, so a tap pulses.
Carousel arrow .gallery-arrow Border firms to --color-text-strong. Scales to --press-scale, and takes the same border. On touch there is no hover to have fired first.
Carousel dot .gallery-dot None. Grows half way toward the pill it becomes when selected. The press is the first half of the state change: let go and it finishes, move away and it goes back. A 10px dot cannot usefully scale by 3%.