Skip to content

Design system

Style Guide

Every value on this page is read from tokens.json. Nothing here is a literal. One palette, no dark mode — the token file holds a single set of values.

01

Colour

Each name is a job, not a value. The -on-contrast keys switch as a set.

Surfaces

  • surface-body
  • surface-subtle
  • surface-contrast

Marks

  • ink
  • ink-muted
  • border

Action

  • action
  • action-hover
  • on-action
  • focus

Status

  • danger
  • success
  • warning

Danger. Text uses danger on danger-bg.

Success. Same pattern.

Warning. Same pattern.

02

Inverted band

One per page. Ground and all three ink tokens switch together, and the class declares color so descendants cannot keep the light ink.

03

Type

Two ramps. Each step ships size, leading and tracking tuned together — use them as a set, never recombine across steps.

Title ramp

title-display

Fired earth

title-xl — h1

Fired earth

title-lg — h2

Fired earth

title-md — h3

Fired earth

title-sm

Fired earth

title-xs

Fired earth

title-2xs — the eyebrow step

Fired earth

Text ramp

text-xl

The kiln holds its heat for a day after the fire goes out.

text-lg — lead

The kiln holds its heat for a day after the fire goes out.

text-md — body default

The kiln holds its heat for a day after the fire goes out.

text-sm

The kiln holds its heat for a day after the fire goes out.

text-xs

The kiln holds its heat for a day after the fire goes out.

Font slots

  • --font-text DM Sans — running copy
  • --font-title Stack Sans Notch — headings
  • --font-display Stack Sans Notch — hero only
  • --font-mono JetBrains Mono — code and data

04

Space

One ramp for padding, margin and gap alike. Lower half inside a component, 2xl and up for layout.

  • 3xs
  • 2xs
  • xs
  • sm
  • md
  • lg
  • xl
  • 2xl
  • 3xl
  • 4xl

05

Radius, border, shadow

radius-none
radius-sm
radius-md
radius-lg
radius-full
shadow-sm
shadow-md
shadow-lg

06

Buttons

The primary action gets button treatment. Secondary actions may stay as text links, but only after one primary is visually distinct.

Tab to any control to see the focus ring, which reads from --color-focus.

07

Card

The base grid unit. The footer link is pinned with margin-top: auto, so links align across a row whatever the body length.

Base

Lacquer cutting

Short body copy, one or two sentences.

Read more

Subtle

One step off the ground

The same card on the subtle surface. A longer body, to show the link still lines up with its neighbours.

Read more

Base

No footer link

The link is optional. Without it the card simply ends after the body.

08

Blockquote

A rule on the inline start edge instead of decorative quote glyphs. The display variant moves that rule to the block start, because a centred column with a left rule reads as misaligned.

We stopped asking what the page should look like and started asking what the next page would inherit from it.

Marta Reinhold Principal, Reinhold Security

A literal is invisible in review and breaks the moment the palette moves.

Display variant Centred, rule on top

09

Prose

Running copy sits at text-md and is constrained to the layout-content measure. A paragraph inside .section__inner takes ink-muted, so it steps back from the headings without dropping below the contrast floor.

An inline link takes the action colour. Only unclassed links are targeted, so a button inside prose keeps its own label colour.


The rule above is the --rule composed property: one hairline width, one border colour, declared once.

10

Section head

The size follows the heading level, not a class. An h1 here takes the xl step and the wider cap; an h2 takes lg. That is what separates a page title from the section titles under it.

01 — Capability

Six things, done properly.

A short lead, two lines at most. It qualifies the title rather than repeating it.

11

Feature cell

Not the card. This is a borderless cell that expects a ruled grid around it — cells owns every hairline. The link is pinned with margin-top: auto, so links align across a row whatever the body length.

12

Step cell

One movement in a sequence. The command line is the heading of the cell but it is a p, not an h3 — a run of shell commands in the heading outline is noise.

13

Stat bar

Four cells, fixed. The count is tied to the nth-child border rules — three or five leaves a stray hairline at one breakpoint. The figure takes the title ramp while the label stays mono, and that contrast is the point.

14

Person

The role sits above the name, so the eye can scan roles down a row without reading names. It carries the shared eyebrow class alongside person__role — drop either and it is half styled. The alt text belongs to the instance, never to the recipe.

15

Badge

An instance is one badge. A set sits in a ul.badges, which carries no margin of its own — the block around it decides the space above. One or two words: a badge holding a sentence stops reading as a badge.

  • Cloud and identity
  • Detection rules
  • badge--action

The third is badge--action, for the one badge in a set that is the point of the set. Colour every badge and none of them reads as marked.

16

Checklist

Three or four assurances that qualify an offer, not running content. The list draws a top rule and every item draws a bottom one, so the last rule is deliberate — it closes the block against whatever follows. Text takes full ink, because these lines are the point rather than support.

  • One short assurance per line.

  • Three is the usual count.

  • Four at most before it reads as a feature list.

17

Form field

The unit every form on the site is built from. The control edge uses --color-field-border, not the layout hairline: at 1.19:1 that rule fails the 3:1 WCAG 1.4.11 asks of anything a person operates.

One line saying what to put here, when the label cannot say it alone.

That does not look like an email address.

The invalid state and the select are marked data-delta-component-opt-out: both are deliberate variants, and the swapped tag or the extra class would otherwise report as diverged on every conformance run. A textarea is the same case. The checkbox carries no marker at all — it is field__check, a row rather than the stacked grid, so it is not an instance of this component.

18

Intake form

Static by design. The script validates and reports; nothing is sent. Wiring it to a real endpoint means replacing that script, not adding to it. Submit it empty to see the error state, or fill it to see the message.

kiln@intake:~$ new-engagement

Rough estate size, sector, and anything that has already gone wrong.

Encrypted in transit. We keep intake notes for ninety days, then delete them.

Field ids are prefixed here because this page carries a second form above. The script binds by class across every .intake, delegated at the document, so two forms on one page are safe — but two sets of matching id and for values are not.

19

Tabs

The markup ships with no hidden attribute on any panel, so with Javascript off every panel stays readable. The script adds hidden on load. Roving tabindex: only the selected tab is reachable with Tab, and Left, Right, Home and End move between them.

First panel. The row never wraps — it scrolls sideways instead, so a set that does not fit stays on one line.

Second panel. The selected state is the tab's own bottom border in the action colour, sitting on the container rule.

Third panel. Ids are per instance: two sets on one page sharing ids would drive each other.

20

Ticker

Decorative, and the whole strip is aria-hidden — the text is duplicated, so a screen reader would otherwise read every fragment twice. Exactly two runs with identical content: the track shifts by half its width, so a third run or a differing copy makes the loop jump. Hover pauses it, and under prefers-reduced-motion the animation stops and the second run is hidden.