Not a live product

The design system

Every component this product has, in every state it has, rendered from the same stylesheet every other surface loads. If the system breaks, this page breaks with it.

How to use this

Build a screen by choosing containers and components. If you find yourself writing a CSS rule, the honest question is whether what you need is a component this library is missing. Add it here, not on the page.

Its pictures. The homepage keeps about thirty lines for a cropped mark and two small charts, and that is correct: a bar chart of payroll months is not a design system concern. It owns no colour, no spacing and no type.

A <button> with no class renders as the browser's own grey control, because there is no bare button rule. That is deliberate: a missing class should look broken rather than look fine.

Grounds

The mechanism the whole palette hangs off. A ground is the only thing allowed to pair a foreground with a background, and it sets all of them at once: text, muted, borders, accent, status. Choosing a background therefore chooses everything that has to read on it.

This is not a convention to remember. A component never names two colours, so putting acid text on cream is not something you must avoid writing; it is something there is no way to write. Four grounds exist and there is no fifth: adding one means adding a row to GROUNDS in tokens.ts, and the contrast test then holds it to the same 4.5 as the other four.

Primary text on paper

Secondary text, which is what a paragraph inside a card is set in.

The muted tier: captions, hints, and the third rank of anything.

Same markup, four different sets of colours, no variant per ground.

Primary text on green

Secondary text, which is what a paragraph inside a card is set in.

The muted tier on green is full cream, because nothing quieter clears AA. Hierarchy here comes from size and weight.

Same markup, four different sets of colours, no variant per ground.

Primary text on ink

Secondary text, which is what a paragraph inside a card is set in.

The muted tier: captions, hints, and the third rank of anything.

Same markup, four different sets of colours, no variant per ground.

Primary text on brine

Secondary text, which is what a paragraph inside a card is set in.

The muted tier: captions, hints, and the third rank of anything.

Same markup, four different sets of colours, no variant per ground.

Measured contrast

Computed from the token values as this page renders, so it cannot go stale. test/design-system.test.mjs recomputes every one of these with its own implementation of the formula and fails the build below 4.5. Anything on this table reading anything other than AA or AAA is a bug.

PairingForegroundBackgroundRatioGrade
paper: primary text #0F2418 #FDFBF4 15.77 AAA
paper: secondary text #3B4A3E #FDFBF4 9.07 AAA
paper: muted text #5F6B60 #FDFBF4 5.40 AA
paper: text on a card #0F2418 #FBF7EC 15.26 AAA
paper: text on a raised card #0F2418 #FFFFFF 16.33 AAA
paper: button label on accent #FFFFFF #1E7A46 5.35 AA
paper: button label on accent hover #FFFFFF #196238 7.38 AAA
paper: text on the accent tint #1E7A46 #EAF5E3 4.75 AA
paper: urgent on its tint #A8412C #FBEDE8 5.32 AA
paper: attention on its tint #9A6412 #FDF4E2 4.57 AA
paper: good on its tint #1E7A46 #EAF5E3 4.75 AA
green: primary text #FBF7EC #1E7A46 4.99 AA
green: secondary text #FBF7EC #1E7A46 4.99 AA
green: muted text #FBF7EC #1E7A46 4.99 AA
green: text on a card #FBF7EC #17603A 7.08 AAA
green: text on a raised card #FBF7EC #155637 8.11 AAA
green: button label on accent #1E7A46 #FBF7EC 4.99 AA
green: button label on accent hover #1E7A46 #FFFFFF 5.35 AA
green: text on the accent tint #FBF7EC #155637 8.11 AAA
green: urgent on its tint #FBF7EC #155637 8.11 AAA
green: attention on its tint #FBF7EC #155637 8.11 AAA
green: good on its tint #FBF7EC #155637 8.11 AAA
ink: primary text #FBF7EC #0F2418 15.26 AAA
ink: secondary text #DAD9CE #0F2418 11.51 AAA
ink: muted text #B9BCB1 #0F2418 8.47 AAA
ink: text on a card #FBF7EC #16301F 13.28 AAA
ink: text on a raised card #FBF7EC #1D3A26 11.64 AAA
ink: button label on accent #0F2418 #8BD44F 9.04 AAA
ink: button label on accent hover #0F2418 #7CC441 7.64 AAA
ink: text on the accent tint #8BD44F #23402B 6.32 AA
ink: urgent on its tint #EFA894 #3A241E 7.37 AAA
ink: attention on its tint #EDC26E #39301B 7.78 AAA
ink: good on its tint #8BD44F #23402B 6.32 AA
brine: primary text #0F2418 #FFD84D 11.81 AAA
brine: secondary text #313D1F #FFD84D 8.35 AAA
brine: muted text #525627 #FFD84D 5.58 AA
brine: text on a card #0F2418 #FEDF73 12.46 AAA
brine: text on a raised card #0F2418 #FEE58C 13.07 AAA
brine: button label on accent #FFD84D #0F2418 11.81 AAA
brine: button label on accent hover #FFD84D #1B3A28 9.02 AAA
brine: text on the accent tint #0F2418 #F2C93F 10.27 AAA
brine: urgent on its tint #0F2418 #F2C93F 10.27 AAA
brine: attention on its tint #0F2418 #F2C93F 10.27 AAA
brine: good on its tint #0F2418 #F2C93F 10.27 AAA

The pairings that are not available

Listed because knowing why acid is never a text colour on a light page is what stops somebody reintroducing it. Acid and brine are for grounds, marks and fills. They carry text on ink, and nowhere else.

PairingForegroundBackgroundRatioGrade
acid on cream #8BD44F #FBF7EC 1.69 fail
brine on cream #FFD84D #FBF7EC 1.29 fail
acid on green #8BD44F #1E7A46 2.96 fail
green on brine #1E7A46 #FFD84D 3.87 AA large

Scales

Type, spacing, radius and motion. One definition each, in tokens.ts, referenced everywhere.

Type

--text-display Business insurance for when things get sticky
--text-1 Ridgeline Roofing Ltd
--text-2 Statement of demands and needs
--text-3 Asset register
--text-body-l The lede, at 16.5px, for the sentence under a heading.
--text-body Body, at 15px. Everything a person actually reads.
--text-small Small, at 13.5px. Captions, hints, table cells.
--text-tiny Tiny, at 12px. Footnotes and the disclaimer.
--text-micro

Spacing, on a 4px base

--space-1
--space-2
--space-3
--space-4
--space-5
--space-6
--space-7
--space-8

Radius

Three steps and a pill, and they are load-bearing rather than decorative: 9px for a chip or an input, 14px for a card, 18px for a panel. Three steps is how nesting reads as nesting.

--radius-sm
--radius-md
--radius-lg
--radius-pill

Motion

Three durations, and a hard rule about what they may touch: colour, opacity, and a one-pixel press. Nothing in this system animates layout, position or size. Two effects that did were built and removed from this product already, one because it produced a repaint artefact and one because it failed to fire for four of seven sections and would have shipped a homepage that rendered blank below the hero. Everything here also stops under prefers-reduced-motion.

--dur-1160msState feedback: hover, focus, press.
--dur-2260msSomething arriving: a log row, a toast.
--dur-3320msA value changing: a meter, a progress bar.

Button

default · hover · focus · active · disabled · loading

The component the whole workstream turns on. There is no button { } rule anywhere in this system: a control looks like a button because it says class="btn". The old stylesheet painted every button with the accent fill, so a tab strip made of buttons had a hovered tab turn dark green and the tab component carried three background: transparent declarations purely to undo it. Variants here are custom properties rather than rules, so there is one hover rule for all five and nothing depends on source order. Loading is aria-busy="true", which is both the hook the styling reads and the thing a screen reader announces.

Fields

default · hover · focus · disabled · readonly · invalid · compact

Styled by element rather than by class, and the distinction from the button is variance, not element type: a <button> is a tab, a chip, a link and an action, while an <input type="text"> is one thing. Invalid keys off aria-invalid for the same reason loading keys off aria-busy: a field cannot be visibly red and silently fine.

Reads the filed record at Companies House.
A company number is eight characters, or two letters and six digits.

Table

comfortable · dense · sticky · clickable · selected · empty · too much data

Too much data is a designed state here, not an overflow. The practice console lists a portfolio and the register lists every tool a trade owns; both are unbounded, and a table that shows fewer rows than it holds has to say so with the real total. Silent truncation reads as "this is everything", which is the one thing it is not. The empty case keeps the header so the columns still say what is missing.

Portfolio

4 clients
ClientSignalUninsured
Ridgeline Roofing LtdPayroll since March£24,100
Calder Electrical LtdSum insured set in 2018£4,292
Selected rowSubcontractor payments£11,880
Hollow Lane JoineryPlant on hire£7,400
ClientSignalUninsured
Client 1 LtdTools bought since renewal£1,200
Client 2 LtdTools bought since renewal£1,517
Client 3 LtdTools bought since renewal£1,834
Client 4 LtdTools bought since renewal£2,151
Client 5 LtdTools bought since renewal£2,468
Client 6 LtdTools bought since renewal£2,785
Client 7 LtdTools bought since renewal£3,102
Client 8 LtdTools bought since renewal£3,419
Client 9 LtdTools bought since renewal£3,736
Client 10 LtdTools bought since renewal£4,053
Client 11 LtdTools bought since renewal£4,370
Client 12 LtdTools bought since renewal£4,687
Client 13 LtdTools bought since renewal£5,004
Client 14 LtdTools bought since renewal£5,321
ItemBoughtCostReplacement
Power tools from Screwfix Direct Cardiff
Power tools · no serial number
14 Feb 2026 £1,240 £1,271
Makita 18v LXT 6-piece kit
Power tools · serial MAK-8841207
3 Nov 2025 £890 £925
Narrow the window past 560px. The header goes to screen readers only and every cell carries its column name from data-label, except the first, which is the row's own title rather than a field. A wide table that is the whole point of its screen cannot answer a phone with a sideways scroll: on the register that squeezed the item name to 93px.
ClientSignalUninsured
Ridgeline Roofing LtdPayroll since March£24,100
Calder Electrical LtdSum insured set in 2018£4,292
Showing 2 of 412. Ranked by uninsured value.
ClientSignalUninsured
No clients need attention this week

The header stays so the columns still say what would have been here. Nothing has changed in any connected ledger since Friday.

Card and panel

default · quiet · inset · raised · accent · interactive · selected · toned

A card is a bordered block at 14px; a panel is the container a card sits inside, at 18px, with a header row. Never a shadow. .card-interactive is only for a card that is genuinely a link or a button: a hover state on something nobody can click is a promise the page does not keep.

.card

.card-quiet, sitting on the ground

.card-inset, recessed

.card-raised, white on cream

.card-accent, the one that carries the answer

.card-interactive, which really is clickable

.card-selected

.card-urgent

.card-attention

Panel

head

The body, at 24px, holding whatever the panel is for.

And a foot, for a count or a caveat.

Badge and pill

neutral · good · attention · urgent · info · outline

Five tones and no more, because the point of a tone is that a reader learns what it means. The audit page has domain vocabulary of its own (a statutory basis, a contractual one, a bare exposure, three confidence levels) and maps those onto these five in audit-page.ts. Those names used to be tones in the shared stylesheet, which is how a design system ends up knowing what compulsory Employers' Liability is. Status tones are tuned for the paper and ink grounds only.

neutralgoodattentionurgentinfooutline
neutralgoodattentionurgentinfo
Not a live product The one marker with fixed colours, because it is a statement about the whole prototype rather than a state of anything on the page.
neutralgoodattentionurgentinfo

Tabs

default · hover · selected · focus · disabled

Live: click them. Selection is aria-selected rather than a class, so a tab cannot look selected while announcing that it is not. The strip scrolls sideways rather than wrapping, because a wrapped strip changes height when the selection moves and shifts everything under it.

The first panel. Hover the strip: nothing turns green, because no rule reaches into a bare button.

Disclosure

closed · open · hover · focus

A <details>, so it works with no client script at all. That is not a preference: the register page ships no JavaScript on purpose, because a sum insured should never be computed in a browser, and it is the page with the most folded content in the product.

A column is wrong. Correct it

The caret is drawn in CSS so it inherits the text colour on every ground and rotates on open. The browser's own triangle is removed rather than restyled, because Safari and Chrome disagree about what it is.

Quiet, for a row inside a table

Open by default, to show the state.

Notice, finding and disclaimer

info · attention · urgent · good

A notice is part of the document: it stays, it can be long, and it is what the engine uses to say that a file did not state which way the money went. A toast is transient feedback about a click. This product is server-rendered and almost every message it has is a fact about the result, which is why notices are everywhere here and toasts are rare. A finding is the same tokens with no fill, for a list where tinting every row would turn the tone into wallpaper by about the third one.

Nothing in this file says which way the money went, so every row has been read as money out.

The website and the filed record disagree

Filed as a builder, the site reads as a roofer. Confirming this changes four cover lines.

Employers' liability is required by law from March and no premium appears in this ledger.

Read without assumptions. Every column mapped on its own header.

urgentNo employers' liability premium appears in this ledger
Payroll starts in March. A ledger cannot show the absence of insurance, only the absence of a premium leaving the account.
attentionTools cover has not moved since 2018
£3,000 insured against £7,292 of replacement value.
goodPublic liability held at £5m
Premium seen monthly for eleven of the last twelve months.

The disclaimer, which travels with every rendered audit and looks the same on every surface including inside a partner's frame.

Toast and modal

info · good · attention · urgent

Neither may be the only place something is said. A lot of this product gets shown on a projector, and a message that appears for four seconds in a corner is a message half the room missed. The modal is a <dialog>, so focus trapping, Escape and the backdrop are the browser's job. The toast fades and does not move: a toast that slides reflows the corner of the screen while a presenter is pointing at it.

Item added
Makita DHP484, serial recorded.
Two rows were skipped: no readable date.
That upload is over the 5MB this prototype accepts.

Progress, meter and figure

determinate · indeterminate · under · met · over

A progress bar shows how far along a task is: it only goes up, and full is good. A meter shows a value against a target: it can sit anywhere, and full is not necessarily good. This product needs the second far more, because its central argument is a shortfall. Both are divs with an inline width, because <progress> and <meter> cannot be styled consistently across browsers. The width is data, not design, and aria-valuenow carries the same number to a reader.

Tools cover against replacement value41%
£3,000 insured£7,292 held
Public liability100%
Stock cover, against stock held240%
Paying for cover that cannot be claimed on
£11.40 to £18.60 / month
Spinner, the only indefinite animation in the product.

Log

pending · ok · failed

A sequence of attempted steps, each with an outcome and how long it took. Watch it fill in: the rows are already on the page at 30% opacity and the script clears them one at a time, so a dead script leaves a faint list rather than an empty box. This product had a hide-until-JavaScript reveal fail to fire for four of seven sections, which would have shipped a homepage that rendered blank below the hero, and that is why this one is visible-by-default.

Companies House09876543, active, incorporated 2019241 ms
WebsiteRead 4,102 words, found a company number318 ms
!LedgerSkipped: no transactions were supplied

Transcript

user · assistant · tool call

An exchange with the tool calls shown. The tool block is the reason it is a component at all: most chat styling is a bubble, and the thing that matters here is that a reader can see the tool name and the payload, because the claim the agent page makes is that a real tool returned a real answer.

Business owner

What insurance does Ridgeline Roofing actually need?

Agent

Reading the filed record and the site.

gather_facts({ "query": "Ridgeline Roofing Ltd" })

Chip, brand chip, avatar and code

default · hover · selected · disabled · off

The chip is the second component the button trap bit, after the tab: it arrived painted solid green and had to be repainted back. The brand chip's off state is the one that matters, because three of the five ways into this product are not built and each has to say so on its own face.

FAFreeAgent XOXero, not connected
{
  "product": "employers_liability",
  "basis": "legal",
  "triggered_by": ["payroll_seen_2026-03"]
}
https://itspickle.app/mcp

Confirm card

open · confirmed

Domain-shaped rather than generic, and that is the right call: this is a design system for one product, and turning an extracted fact into a declared one is the move the whole engine is built around. The "changes" line is not decoration. A confirm card that does not say what it affects is asking somebody to answer a question with no stated consequence.

Do you work at height?
Found: yes, from "flat roofing and gutter replacement" on your site
From Website, medium confidence. Changes: Public liability, Employers' liability, Tools cover
Do you pay subcontractors?
Found: yes, £41,200 across the year
Declared by the customer. The needs above are now based on a stated fact.

Key and value

two column · stacked

The most-used component in the product, because the engine's discipline is that every fact carries its source and its confidence. Two variants, and the second exists because of a real failure: with a long label the two-column form wraps every value into a column two words wide. Below 560px the first becomes the second on its own.

Company number
09876543
Incorporated
4 Feb 2019
Turnover
£150,001 to £200,000 (Ledger, medium)
Does anyone other than a director draw a wage from the business?
Yes, since March 2026
Is the plant on site owned or hired in?
Hired in, three invoices seen

Layout

wrap · section · stack · row · spread · grid · prose

Six containers, and between them they replaced almost every inline style= on two surfaces. Spacing is never set on a child; it is set by whichever container the child sits in. That is why a new screen needs no new CSS: you are choosing containers, not writing margins. .prose exists because the same max-width:78ch had been written eleven times.

.spread

heading and its controls
.rowwraps, centred

.grid-3

auto-fit

no media query per surface

.prose caps a paragraph at 70 characters, which is the difference between body copy and a wall. A line that runs the full width of a 1080px page loses the reader at the end of every line, and every surface here had been solving that with an inline style of its own.

The states everyone forgets

empty · loading · error · too much data

The deliverable, not an afterthought. All four are the same three things: a short statement of what is true, one line of why, and at most one action. Never an illustration, never a heading that says "No results", and never a dead end. An empty state with no action is a page telling somebody it cannot help them.

Nothing on the register yet

Connect an accounting feed and the tools already in the ledger arrive with dates and prices. Or add the first one by hand.

No findings against what was read

Every check the sources could answer came back clear. That is not the same as being well covered: a ledger can show what is held and never the absence of anything.

Reading Companies House and the website
That file does not hold text

"accounts.csv" is named as a CSV but is most likely a spreadsheet or a PDF that was renamed rather than exported.

No company by that name

Companies House returned nothing for "Ridgline Rooofing". Check the spelling, or search by company number.

Showing the 25 largest of 412 items. The rest are under £40 each.

Item 1: a row in a list long enough that the box scrolls and the bottom edge fades, so the cut is visible rather than implied.

Item 2: a row in a list long enough that the box scrolls and the bottom edge fades, so the cut is visible rather than implied.

Item 3: a row in a list long enough that the box scrolls and the bottom edge fades, so the cut is visible rather than implied.

Item 4: a row in a list long enough that the box scrolls and the bottom edge fades, so the cut is visible rather than implied.

Item 5: a row in a list long enough that the box scrolls and the bottom edge fades, so the cut is visible rather than implied.

Item 6: a row in a list long enough that the box scrolls and the bottom edge fades, so the cut is visible rather than implied.

Item 7: a row in a list long enough that the box scrolls and the bottom edge fades, so the cut is visible rather than implied.

Item 8: a row in a list long enough that the box scrolls and the bottom edge fades, so the cut is visible rather than implied.

Item 9: a row in a list long enough that the box scrolls and the bottom edge fades, so the cut is visible rather than implied.

Item 10: a row in a list long enough that the box scrolls and the bottom edge fades, so the cut is visible rather than implied.

Item 11: a row in a list long enough that the box scrolls and the bottom edge fades, so the cut is visible rather than implied.

Item 12: a row in a list long enough that the box scrolls and the bottom edge fades, so the cut is visible rather than implied.

Item 13: a row in a list long enough that the box scrolls and the bottom edge fades, so the cut is visible rather than implied.

Item 14: a row in a list long enough that the box scrolls and the bottom edge fades, so the cut is visible rather than implied.

Item 15: a row in a list long enough that the box scrolls and the bottom edge fades, so the cut is visible rather than implied.

Item 16: a row in a list long enough that the box scrolls and the bottom edge fades, so the cut is visible rather than implied.

Item 17: a row in a list long enough that the box scrolls and the bottom edge fades, so the cut is visible rather than implied.

Item 18: a row in a list long enough that the box scrolls and the bottom edge fades, so the cut is visible rather than implied.

The rules, and what enforces them

Each of these is a test in test/design-system.test.mjs, not a convention.

One place holds a colour

No hex anywhere in src/ except ui/tokens.ts, and partner/widget.ts, which renders in a partner's brand on a partner's page and cannot use ours.

Every token used is defined

Every custom property read anywhere in the product resolves to something a ground or a scale actually sets. This is the check that catches a rename that missed a surface, which otherwise ships as a silently unstyled page.

Contrast is recomputed, not remembered

Every ground's text, muted tier, accent pair and status tints are measured against 4.5 on every run, with an implementation independent of the one this page prints from.

No bare button rule, ever again

Nothing in the shared stylesheet may put a background or a colour on button as an element.

No page-specific CSS in shared files

The register's edit grid, the source log rows and the claim transcript were in the shared stylesheet. Two became components; the rest went back to their surface.

This page stays complete

Every component in the library must appear here. Adding one without a specimen fails the build, which is what stops this page becoming a document about the product it used to be.