Design System

Surfaces you can actually see.

This property is built from five grounds and two strokes. What makes a surface work is the measured step between it and the surface underneath it, so that is what we document here.

01 The ladder

Each ground is measured against the one it sits on, not against the text it carries.

Text contrast and surface contrast are different questions with different answers. A panel can carry body copy at 12:1 and still be invisible, because the eye is being asked to find the panel, not the words on it. Below the ladder is drawn as the thing it describes: every step is nested inside the ground it lifts off, with the measured figure printed at the boundary it measures.

--paper #101420 the page

--paper-raised #272F42 1.37:1 above the page

--wash #36415A · 1.31:1 above this panel
--wash-accent #1E4C7E · 1.52:1 above this panel

--paper-sunken #0B0E18 1.44:1 below a panel

The sunken ground is the honest exception. Against the page it measures 1.05:1, which is no step at all, so it only does its job inside a raised panel, where it reads at 1.44:1. That is why inputs, code blocks, and the browser chrome in the brand mockups all sit inside something. The footer is the one place it meets the page directly, and there a hairline draws the edge that the value cannot.

02 The two thresholds

The hexes on this page will drift. These two numbers are the part worth keeping.

  • Threshold 01

    A surface step reads from about 1.2:1

    Under that, a panel is implied rather than seen, and the design is asking the viewer to take its word for a boundary. Our raised panel had been sitting at 1.12:1 off the page and our chips at 1.15:1 off a panel, which is why the whole set read as one flat field. Both now clear the threshold.

  • Threshold 02

    A non-text boundary needs 3:1

    WCAG 2.1 SC 1.4.11 asks 3:1 of anything that identifies a control or its state. A chip stroke is exactly that, so it ships at 60% of the ink value and measures 4.53:1 against the chip it encloses, and 5.94:1 against the panel behind it. A 13% hairline reaches 1.48:1, which is a smudge at pill size, and that is the reason the stroke is drawn rather than implied.

03 Grounds and strokes
TokenValueSits onMeasured step
--paper#101420The pageReference ground
--paper-raised#272F42--paper1.37:1
--paper-sunken#0B0E18--paper-raised1.44:1
--wash#36415A--paper-raised1.31:1
--wash-accent#1E4C7E--paper-raised1.52:1
--line-chip--ink at 60%--wash, and --wash-accent4.53:1, 4.00:1
--line--ink at 13%Any ground1.48:1 on a panel

The two washes are solid values, not translucent tints. A tint inherits whatever is behind it, which is convenient right up to the moment two different grounds need the same chip to look the same. The strokes stay translucent on purpose, because a stroke is meant to track its ground.

The two washes are 1.16:1 apart, so they are told apart by hue and by their stroke, never by lightness. The accent wash carries the accent as its border at 4.35:1 against the panel, and its label takes --ink.

04 Text on each surface

Every pairing that holds. AA asks 4.5:1 for body text, and we treat that as the floor rather than the target, so secondary text is specified to land near 7:1 wherever the surface allows it.

Text token--paper--paper-raised--wash--wash-accent--paper-sunken
--ink16.84:112.25:19.34:18.05:117.66:1
--field-note12.75:19.27:17.07:16.10:113.37:1
--graphite11.19:18.14:16.21:15.35:111.73:1
--ink-212.91:19.39:17.16:16.17:113.53:1
--muted7.04:15.12:13.91:1, under AA3.37:1, under AA7.38:1
--accent5.98:14.35:1, under AA3.32:1, under AA2.86:1, under AA6.27:1
--accent-raised10.20:17.42:15.66:14.88:110.70:1
--ink-raised15.52:111.29:18.61:17.42:116.27:1
--fault-text9.97:17.25:15.53:14.77:110.45:1
--fault3.65:1, mark only2.66:1, mark only2.03:1, mark only1.75:1, mark only3.83:1, mark only
--sev-critical4.20:1, edge only3.05:1, edge only2.33:1, edge only2.01:1, edge only4.40:1, edge only
--sev-serious6.41:1, edge only4.66:1, edge only3.56:1, edge only3.07:1, edge only6.72:1, edge only
--sev-moderate9.08:1, edge only6.60:1, edge only5.04:1, edge only4.34:1, edge only9.52:1, edge only
--sev-minor5.98:1, edge only4.35:1, edge only3.32:1, edge only2.86:1, edge only6.27:1, edge only
--report-link9.54:16.94:15.29:14.56:110.00:1
--report-critical10.31:17.50:15.72:14.93:110.80:1
--report-warn9.81:17.14:15.44:14.69:110.29:1
--report-ok9.67:17.03:15.36:14.62:110.13:1
--report-info11.86:18.63:16.58:15.67:112.44:1

The accent is a mark, a border, and a link color on the page ground. It is not a text color on a panel or a chip, and the table says so in the only way that settles an argument.

Two of these exist because the rule above kept being broken. --accent-raised is what anything reads as the accent ON a panel; --fault-text is what a failure reads as in words. --fault is a brand-book value and stays the dot, the cross, and the mark. It never carries copy, at any size, on any ground on this site.

The four --sev-* values are edges and never words, which is why every figure in their rows says so. Critical reads 3.05:1 as text on a panel, under AA, and each of them clears the 3:1 a boundary needs on the grounds a card actually uses. A severity is carried by the stroke down the side of a card and named in the label beside it, so the color is never the only thing saying which one it is.

The five --report-* values are the bright tier, built for text on a raised card where a base hue falls short. They started inside the embedded reports and are now the answer anywhere a hue has to carry a word.

One name here is a role rather than a color. --tick points at whatever the accent is in its context: the page blue normally, and the light value inside each brand mockup card, which is why it has no row of its own. A role has no single ratio to measure, so it takes the ratio of whatever it currently names.

05 The type scale

Three sizes, and a floor. The site was carrying nineteen different text sizes under nineteen pixels, nine of them on a half-pixel, which is one decision made nineteen slightly different ways rather than nineteen decisions. Headings are not in this table on purpose: they already scale with the viewport and were never the problem.

TokenValueReplacesUsed for
--fs-caption13px9.5 through 13.5Labels, chips, eyebrows, table heads, and meta
--fs-body16px14 through 16Paragraphs and descriptions
--fs-lead18px16.5 through 19Section heads and claims
headingsclamp()Already fluid, seventeen of themAnything that should grow with the viewport

The floor is the point rather than a side effect. Thirty-one declarations sat below twelve pixels, which is the floor this site holds the embedded reports to, and the smallest was nine and a half. Nothing a person reads is under thirteen now, and a token added below it fails a test rather than a review.

Nine declarations are exempt and neither is a matter of taste. The radar label is SVG, where the number is a user unit rather than a pixel: it reads ten and renders at seventeen once the drawing scales, so changing the number would make it larger, not smaller. The rest are inside the package, the card, the billboard, and the browser chrome on the brand page, which are drawings of physical objects and already hold that exemption for their heights.

06 The spacing scale

Four steps and one fluid step. Before this existed the site carried thirteen raw margin values across eighty-four inline declarations, which is drift rather than thirteen decisions. Every one of them sat within four pixels of a step below.

The scale governed those inline declarations and nothing else for six weeks. The stylesheet went on carrying a hundred and fifty-three raw margins across twenty-five sizes, because a scale written down is a scale that applies to whatever someone remembers to apply it to. On 21 September they came onto the steps in the table below, and the ones that stayed off are listed as exceptions in a test rather than left to be noticed.

TokenValueReplacesUsed for
--space-18px8, 10A subtitle under the line it belongs to
--space-216px14, 16, 18A note under a section head
--space-324px20, 22, 24, 26A block against the block above it
--space-434px28, 34, 38A section against a section
--space-flowclamp(20px, 3.5vw, 34px)The one fluid step already in useSpace that should breathe with the viewport

A value off the scale is allowed, and it should look like a decision rather than a default. The test is whether it can be named: the brand mockups set their own heights and gaps because they are drawings of physical objects, and the swatches on this page carry a background each because showing a different color is the entire point of them.

Two exceptions survived the sweep and both can be named. Fifteen margins sit below --space-1, at one to five pixels, for the gap inside a tight pair: a label against the figure it names, a legend against its first field. There is no step under eight pixels and inventing one for eye-level nudges would make a worse scale.

The second is a real gap. Twenty-four margins sit between --space-1 and --space-2, and twelve pixels alone accounts for eighteen of them, so the site has a half-step in daily use that this table does not name. Whether it becomes a fifth token or gets pushed onto the two steps either side is a decision still to make, and it is capped where it was found so the answer stays a decision rather than a habit.

07 What the ladder rules out
  • A hex hand-tuned to sit just above whatever the panel happens to be today. It inverts the moment the panel moves, and the selected state ends up reading as the recessed one.
  • --muted on either wash, at 3.91:1 and 3.37:1. It belongs on the page and on the sunken ground, where it reads 7.04:1 and 7.38:1.
  • The accent as label text on an accent chip, at 2.86:1. The accent draws the border, the label takes --ink at 8.05:1.
  • A 13% hairline as the only edge of a chip or a control. At 1.48:1 it is decoration, and SC 1.4.11 wants 3:1.
  • --paper-sunken as a step against the page, at 1.05:1. It needs a panel above it or a rule beside it.
  • A severity hue as words. They are edge colors: critical reads 3.05:1 as text on a panel and fails outright. The edge carries the hue; the label carries --ink and says which severity it is.
  • A translucent tint standing in for a nested surface, when the same chip has to look the same on two different grounds.
08 Two recipes, not twelve

The ladder settles color. This settles construction. Almost everything on this site that holds content is one of two shapes, and a new block that restates either one in its own words is a second place for the same decision to drift.

RecipeBuilt fromThe rule
The card--paper-raised, a --line hairline, a 10–16px radius, and an optional 3px left or top edge in the accent or a severity hueEvery panel, callout, finding, and result block on the site is this. A new block states its edge and its padding and inherits the rest.
The status bar--wash-raised, a --line-chip hairline, a 16px top radius, and a 5px left edge in --labs-ready or a severity hueThe tool panel's own header, joined to the card below it and stuck under the nav while a visitor works. It carries state, so the left edge is the state and the dot is the state; the words are never the only signal. It shipped carrying its surface as a literal colour, which is the rule above broken by a component that was new — the same shape as Contract Corpus below.
The chip--wash, a --line-chip stroke, a 999px radius, mono uppercase, --ink labelWritten once as .pill, with modifiers for severity, frequency, and confirmation. Anything pill-shaped is this, including the ones a script builds at runtime.

Six tokens carry no swatch because they are not colors. --sans and --mono are the typefaces, --wrap and --tool-measure are the page and reading measures, and --space-1-5 is the half step the scale needed once. --labs-ready is the one green on the site, used only to say a tool is free. The heuristics report then carries a scoped theme of 48 --hx-* names. Twenty-three derive from the tokens above, and twenty-five carry values of their own, and until 2026-09-23 a second type scale that set body at 15.5px where this one sets 16. The four sizes with an equivalent here now derive from it, so they cannot drift again; head and display stay scoped, because this site does not tokenize heading sizes at all.

The test is whether a change lands everywhere it should. Contract Corpus arrived carrying its own fifth copy of the card and its own sixth copy of the chip, and the chip it drew colored its label with a severity hue at 3.05:1, a rule this page had already written down, broken again because the component was new.

Closing

A surface nobody can see is a surface we did not build.

Every figure on this page is a computed WCAG 2 contrast ratio between the two named values, measured surface to surface for grounds and text to surface for type. The values will move again. The rule that a step under about 1.2:1 is not seen, and that anything identifying a control needs 3:1, is what survives the next round.