Skip to content

System

This site is built as a small design system, and this page is that system documented from its own source. Every value below is read from the token files at build time. Nothing here is a screenshot, and nothing is typed out by hand.

Contrast

Contrast audit

Every foreground and background pairing the site uses, measured from the token values in both themes. 12 pairings are gated and 2 are documented as decorative, with a written reason each. If a gated pairing drops below its requirement the build fails, so this table cannot show a failure on a page that reached you.

Light theme. Every pairing is shown at its real colours, whichever theme you are reading in.
SpecimenPairingUsed forMeasuredResult
Sample text
colour-text-primaryon colour-surface-page#14161A on #FBFAF7Running prose, headings and navigation.
17.35
AAA4.50 needed
Sample text
colour-text-secondaryon colour-surface-page#5F6570 on #FBFAF7Metadata, captions and supporting notes.
5.61
AA4.50 needed
Sample text
colour-text-accenton colour-surface-page#1D4E6B on #FBFAF7Links in prose, section eyebrows and the end mark.
8.54
AAA4.50 needed
Sample text
colour-text-primaryon colour-surface-raised#14161A on #F4F2EDText inside a callout or a trade off panel.
16.18
AAA4.50 needed
Sample text
colour-text-secondaryon colour-surface-raised#5F6570 on #F4F2EDLabels inside a trade off panel.
5.24
AA4.50 needed
Sample text
colour-text-accenton colour-surface-raised#1D4E6B on #F4F2EDLinks inside a callout.
7.97
AAA4.50 needed
Sample text
colour-text-inverseon colour-surface-inverse#FBFAF7 on #14161AThe inverted footer block.
17.35
AAA4.50 needed
Sample text
colour-border-strongon colour-surface-page#14161A on #FBFAF7Section rules that mark the structure of a page.
17.35
AA3.00 needed
Sample text
colour-border-accenton colour-surface-page#1D4E6B on #FBFAF7The rule under an active navigation item.
8.54
AA3.00 needed
Sample text
colour-focus-ringon colour-surface-page#1D4E6B on #FBFAF7Keyboard focus indicator. WCAG 2.2 SC 2.4.11 and 1.4.11.
8.54
AA3.00 needed
Sample text
colour-focus-ringon colour-surface-raised#1D4E6B on #F4F2EDKeyboard focus indicator on a raised surface.
7.97
AA3.00 needed
Sample text
colour-audit-thresholdon colour-surface-page#5F6570 on #FBFAF7Threshold gridlines on the contrast ladder. These carry meaning, so they are held to 3:1.
5.61
AA3.00 needed
colour-border-subtleon colour-surface-page#D5D8DC on #FBFAF7Hairlines between list items.Separates blocks that heading structure and spacing already separate. Removing it loses no information.
1.37
Exemptdecorative
colour-audit-trackon colour-surface-page#D5D8DC on #FBFAF7The unfilled remainder of a contrast ladder track.The measured value is stated as a number and as a filled length. The empty track is a backdrop, not the datum.
1.37
Exemptdecorative

The axis is logarithmic, from 1:1 to 21:1, so the three thresholds that matter are readable rather than crushed into the left third. Results are words, not colours. Marking a contrast audit with red and green would be its own accessibility failure.

Dark theme. Every pairing is shown at its real colours, whichever theme you are reading in.
SpecimenPairingUsed forMeasuredResult
Sample text
colour-text-primaryon colour-surface-page#EDECE8 on #121417Running prose, headings and navigation.
15.60
AAA4.50 needed
Sample text
colour-text-secondaryon colour-surface-page#9BA1AC on #121417Metadata, captions and supporting notes.
7.10
AAA4.50 needed
Sample text
colour-text-accenton colour-surface-page#7FB2D4 on #121417Links in prose, section eyebrows and the end mark.
8.10
AAA4.50 needed
Sample text
colour-text-primaryon colour-surface-raised#EDECE8 on #1B1E23Text inside a callout or a trade off panel.
14.13
AAA4.50 needed
Sample text
colour-text-secondaryon colour-surface-raised#9BA1AC on #1B1E23Labels inside a trade off panel.
6.43
AA4.50 needed
Sample text
colour-text-accenton colour-surface-raised#7FB2D4 on #1B1E23Links inside a callout.
7.33
AAA4.50 needed
Sample text
colour-text-inverseon colour-surface-inverse#121417 on #EDECE8The inverted footer block.
15.60
AAA4.50 needed
Sample text
colour-border-strongon colour-surface-page#EDECE8 on #121417Section rules that mark the structure of a page.
15.60
AA3.00 needed
Sample text
colour-border-accenton colour-surface-page#7FB2D4 on #121417The rule under an active navigation item.
8.10
AA3.00 needed
Sample text
colour-focus-ringon colour-surface-page#7FB2D4 on #121417Keyboard focus indicator. WCAG 2.2 SC 2.4.11 and 1.4.11.
8.10
AA3.00 needed
Sample text
colour-focus-ringon colour-surface-raised#7FB2D4 on #1B1E23Keyboard focus indicator on a raised surface.
7.33
AA3.00 needed
Sample text
colour-audit-thresholdon colour-surface-page#9BA1AC on #121417Threshold gridlines on the contrast ladder. These carry meaning, so they are held to 3:1.
7.10
AA3.00 needed
colour-border-subtleon colour-surface-page#2A2E35 on #121417Hairlines between list items.Separates blocks that heading structure and spacing already separate. Removing it loses no information.
1.35
Exemptdecorative
colour-audit-trackon colour-surface-page#2A2E35 on #121417The unfilled remainder of a contrast ladder track.The measured value is stated as a number and as a filled length. The empty track is a backdrop, not the datum.
1.35
Exemptdecorative

The axis is logarithmic, from 1:1 to 21:1, so the three thresholds that matter are readable rather than crushed into the left third. Results are words, not colours. Marking a contrast audit with red and green would be its own accessibility failure.

Colour

Semantic colour

Components consume these names and never the primitives beneath them. All 14 resolve to a different primitive in dark theme, which is the point of the layer: the accent reads at 8.55:1 on paper and would sit at roughly 2:1 on the dark surface, so dark theme points the same name at a lighter step of the same ramp. No component learns about it.

Semantic tokenLightDark
--colour-surface-page
#FBFAF7colour.neutral.50
#121417colour.neutral.950
--colour-surface-raised
#F4F2EDcolour.neutral.100
#1B1E23colour.neutral.800
--colour-surface-inverse
#14161Acolour.neutral.900
#EDECE8colour.neutral.150
--colour-text-primary
#14161Acolour.neutral.900
#EDECE8colour.neutral.150
--colour-text-secondary
#5F6570colour.neutral.500
#9BA1ACcolour.neutral.400
--colour-text-accent
#1D4E6Bcolour.steel.600
#7FB2D4colour.steel.300
--colour-text-inverse
#FBFAF7colour.neutral.50
#121417colour.neutral.950
--colour-text-on-accent
#FFFFFFcolour.neutral.0
#121417colour.neutral.950
--colour-border-subtle
#D5D8DCcolour.neutral.200
#2A2E35colour.neutral.700
--colour-border-strong
#14161Acolour.neutral.900
#EDECE8colour.neutral.150
--colour-border-accent
#1D4E6Bcolour.steel.600
#7FB2D4colour.steel.300
--colour-focus-ring
#1D4E6Bcolour.steel.600
#7FB2D4colour.steel.300
--colour-audit-track
#D5D8DCcolour.neutral.200
#2A2E35colour.neutral.700
--colour-audit-threshold
#5F6570colour.neutral.500
#9BA1ACcolour.neutral.400

Type

Type

Three faces, self hosted, variable, Latin subset, about 85KB together. Each role below is rendered at the size it is actually used. Roles carry family, weight, size, line height and tracking together, so a component adopts a whole role in two declarations rather than assembling one from five tokens.

Archivo, display

Omnibus TypeWeight 100 to 900SIL OFL 1.1

Headings and the name. Set tight and heavy.

Instrument Sans, body

Rodrigo Fuenzalida and Jordan EgstadWeight 400 to 700SIL OFL 1.1

Running prose. The default for everything that is read, not scanned.

Red Hat Mono, utility

MCKLWeight 300 to 700SIL OFL 1.1

Dates, token values, contrast ratios and section labels.

Adoption is the only measure that matters

--type-displaydisplayclamp(2.5rem, 1.35rem + 5vw, 4.5rem)line height 1weight 800tracking -0.03em

The name on the home page. Once per site.

Adoption is the only measure that matters

--type-heading-1displayclamp(1.9375rem, 1.15rem + 3.4vw, 3.25rem)line height 1.1weight 700tracking -0.015em

The single h1 on every page.

Adoption is the only measure that matters

--type-heading-2display1.5remline height 1.25weight 700tracking -0.015em

Section headings within a page or case study.

Adoption is the only measure that matters

--type-heading-3display1.25remline height 1.25weight 600tracking 0em

Subsections, and the title of a component entry on the system page.

Adoption is the only measure that matters

--type-leadbody1.25remline height 1.5weight 400tracking 0em

The opening paragraph of a page. One per page at most.

Adoption is the only measure that matters

--type-bodybody1.0625remline height 1.65weight 400tracking 0em

Running prose. The default for everything unless there is a reason.

Adoption is the only measure that matters

--type-body-smallbody0.9375remline height 1.5weight 400tracking 0em

Supporting notes and captions. Not for long passages.

Adoption is the only measure that matters

--type-eyebrowmono0.75remline height 1.5weight 500tracking 0.14em

Uppercase label above a section rule. Never a heading on its own.

Adoption is the only measure that matters

--type-metamono0.8125remline height 1.5weight 400tracking 0em

Dates, periods, counts and other metadata.

Adoption is the only measure that matters

--type-mono-valuemono0.9375remline height 1.5weight 500tracking 0em

Token values and contrast ratios, where figures must align.

Space

Spacing scale

A four pixel base, named for the job each step does rather than the size it happens to be. Bars are drawn at the real value.

--space-stack-tight0.5rem
--space-inline0.75rem
--space-stack1rem
--space-rule1.25rem
--space-page-inline1.5rem
--space-block2rem
--space-gutter2.5rem
--space-section-tight4rem
--space-section6rem

Components

Components

11 primitives, each used by at least one page. These descriptions are the same note files that sit beside the components, so they cannot drift. A test fails if any component is missing a note, a stylesheet or a test.

Button

An action. Renders a real button element and guarantees the WCAG 2.2 minimum target size regardless of the type size it inherits.

When not to use itNever use it to navigate. If it changes the URL it is a Link, however it looks. The pressed style is driven by aria-pressed, so do not fake selection with a class.

  • --size-target
  • --button-padding-block
  • --button-padding-inline
  • --colour-border-strong
  • --colour-surface-inverse
  • --colour-text-inverse

Card

A linked record in a list: a hairline, a title, a dotted leader and a period. Used for case studies on the work index.

When not to use itIt is not a raised surface and never gains a shadow, a radius or a fill. If content is not a link to somewhere else, it is not a Card. Do not put a second link inside it: the title link already covers the whole block.

  • --colour-border-subtle
  • --type-heading-3
  • --type-meta
  • --size-measure
  • --space-stack

Grid

The page grid. `gutter` gives a narrow label column beside a content column. `pair` gives two equal columns.

When not to use itNot a general layout tool. Do not reach for it to place two items side by side inside a paragraph or a card, and do not nest it inside itself.

  • --size-gutter
  • --space-gutter
  • --space-stack

Link

Every link on the site. Routes internal links through next/link and renders external ones as plain anchors with an end mark.

When not to use itDo not use it as a button. If the thing performs an action rather than navigating, use Button. Do not rely on the quiet tone in running prose, where colour and underline together are the affordance.

  • --colour-text-accent
  • --colour-text-primary
  • --duration-state
  • --easing-standard

Logo

Alan's monogram, drawn inline so the letters inherit the current text colour and the full stop takes the accent token. Correct in both themes from one asset.

When not to use itDo not use it as a decorative flourish or repeat it beside the wordmark on the same screen. It carries no accessible name by default, so give it one only when it is not already inside a link or heading that names it.

  • --colour-text-primary
  • --colour-text-accent

Prose

Wraps long form text and gives plain HTML its typography. Caps the line length at a readable measure.

When not to use itDo not wrap interface text in it. It styles descendants it does not own, which is right for MDX output and wrong everywhere else. One per page, around the body copy only.

  • --size-measure
  • --type-body
  • --type-heading-2
  • --type-heading-3
  • --colour-text-accent
  • --space-stack

Section

A region of a page: a hairline rule, an optional uppercase label in the gutter, and content. Carries the one motion treatment on the site.

When not to use itUse the wide variant only for content the gutter would crush, such as a table or a diagram. Reaching for it to make a paragraph longer defeats the measure. Do not repeat the label as a heading below it: pass labelAs to promote the label itself. If the region needs to appear in the document outline, put a real heading inside it and pass its id to labelledBy.

  • --colour-border-subtle
  • --colour-border-strong
  • --colour-text-accent
  • --type-eyebrow
  • --space-rule
  • --duration-reveal
  • --easing-reveal

SkipLink

Lets a keyboard user jump past the navigation to the main landmark. First focusable element on every page.

When not to use itDo not hide it with display none or visibility hidden, which removes it from the tab order and defeats the point. It moves with a transform so it stays focusable while off screen.

  • --skip-link-inset
  • --layer-skip-link
  • --colour-border-accent
  • --size-target

Stack

Sets the vertical space between a group of siblings. The only component that owns the gap between things.

When not to use itDo not use it for a single child, and do not add margins to its children to adjust spacing. Change the gap, or use a different gap step.

  • --space-stack-tight
  • --space-stack
  • --space-block
  • --space-section

Tag

A short, non interactive label for a discipline or category. Set in mono under a hairline.

When not to use itIt is not a filter, a button or a link, and it must never become one: nothing about it signals that it can be pressed. If a category needs to be selectable, that is a different component with a different affordance.

  • --type-meta
  • --colour-text-secondary
  • --colour-border-subtle
  • --border-hairline

ThemeToggle

Swaps between light and dark. The operating system decides until the button is first pressed, so nothing is stored for a visitor who never touches it.

When not to use itDo not label it with the current theme. The icon and the accessible name both describe what pressing it will do, and the two are chosen in CSS rather than in JavaScript so they are correct before hydration.

  • --size-target
  • --colour-text-secondary
  • --colour-text-primary
  • --duration-state

Case studies

Components for writing

5 components available inside a case study without importing anything. TradeOff is the one that matters: it renders a decision as options considered, what I chose, and what it cost. The build fails if the chosen option is not in the list of options, because a choice that was never an option means the list is decoration.

Callout

An aside inside a case study: a labelled block on a raised surface. The quiet tone carries the confidentiality notice.

When not to use itNot for emphasis. If a sentence matters, put it in the prose and make it the first sentence. A page with three callouts has no callouts, only paragraphs in boxes.

  • --colour-surface-raised
  • --colour-text-accent
  • --type-eyebrow
  • --type-body

Figure

A diagram with a required caption. Built for inline SVG, so diagrams inherit the theme rather than shipping two image files.

When not to use itNot for decoration or screenshots of interfaces. The caption is required because a figure nobody can describe is not carrying an argument. Mark it decorative only when it genuinely repeats the prose.

  • --colour-border-subtle
  • --type-body-small
  • --colour-text-accent
  • --size-measure

MetricRow

A row of figures with their labels, rendered as a description list so each value stays tied to what it measures.

When not to use itDo not use it for numbers that are not measurements, and do not pad it to fill the row. Three real figures read as evidence. Six invented ones read as a pitch deck.

  • --type-heading-2
  • --type-eyebrow
  • --colour-border-subtle
  • --space-block

Timeline

A sequence of events in order, rendered as an ordered list because the order carries the meaning.

When not to use itNot a substitute for the narrative. If the sequence needs explaining, explain it in prose and use this to summarise. Not for a list of unordered facts.

  • --type-meta
  • --type-body
  • --colour-text-accent
  • --colour-border-subtle

TradeOff

Renders a decision as three labelled parts: options considered, what I chose, and what it cost. The most important structure on the site, because trade off reasoning is what senior product hiring screens for.

When not to use itDo not use it for a decision that had no cost, because there was no trade off and the empty field will say so. The chosen option must appear in the options list, and the build fails if it does not: a choice that was never an option means the list is decoration.

  • --colour-surface-raised
  • --colour-text-accent
  • --type-heading-3
  • --type-eyebrow
  • --size-measure

Layer 3

Component tokens

Only where a component genuinely needs local variation. This layer is short on purpose: a long component token layer usually means the semantic layer is not saying enough. Each entry carries the reason it exists.

TokenPoints atWhy it exists
--button-padding-block--space-stack-tightButtons and links must reach the 24px target minimum independently of the type size they inherit.
--button-padding-inline--space-inlinePaired with the block padding so the two stay in step.
--skip-link-inset--space-stackThe skip link is positioned against the viewport, not a layout parent, so it cannot use page spacing.
--focus-ring-offset--space-stack-tightThe focus ring needs to clear the element without colliding with adjacent hairlines.
--audit-row-height--space-blockThe contrast ladder sets real specimens at real sizes, so its row height is driven by the tallest specimen rather than by page rhythm.