# The design system

2026-08-06

Ernest wants Convoro to feel like years of commercial development. The front end
has had five design passes and looks it. The admin has had none. But the deeper
problem is that neither is built on a system — they are built on judgement,
applied one screen at a time, and judgement does not survive four hundred more
screens.

This note defines the system, and the check that keeps it.

## The diagnosis

Counted, not guessed:

| | front end | admin |
|---|---|---|
| lines of CSS | 7,561 | 1,304 |
| rules | 1,177 | — |
| distinct font sizes | **46** | 16 |
| distinct padding values | **131** | 39 |
| distinct gap values | 34 | — |
| distinct border radii | 33 | 8 |
| hardcoded hex colours | 36 distinct, 106 uses | 40 distinct, 103 uses |

Forty-six font sizes is not a type scale, it is forty-six decisions. Nobody
chose `0.82rem` over `0.83rem` for a reason — both exist because two screens
were built on different days. That is precisely the difference a person reads as
"unfinished" without being able to name it.

Two structural findings matter more than the counts.

**Colour is tokenised; space and type are not.** There are forty `--c-*`
colour tokens and zero spacing or type tokens. So colour stayed coherent through
five design passes and everything else drifted. The fix is not discipline, it is
giving space and type the same treatment colour already has.

**The admin has its own parallel colour system.** `--admin-bg`, `--admin-text`,
`--admin-border`, `--admin-card-bg` and friends duplicate `--c-bg`, `--c-text`,
`--c-border`, `--c-surface` with different values, hardcoded, light only. That
is why the admin cannot be themed, cannot go dark, and cannot follow the
operating system the front end now follows. It is also why a button looks like
one thing on the front end and another in the ACP — the most legible signal that
two different products got stapled together.

## What we are copying, and what we are not

Ernest's read is right: Invision's product is more polished than ours. What is
worth being exact about is that *the polish is not theirs*.

Their feature set, their information architecture, the fact that a moderation
action happens inline rather than on a separate page — all of that is function,
and function is ours to match freely. Their icon artwork, their CSS, their
setting labels and error copy, their palette, their name: those are expression,
and none of it comes near this codebase. Their source is not available to us at
all and is not the input to anything here.

What actually makes their product read as commercial is generic craft:

1. One spacing scale and one type scale, everywhere, no exceptions
2. Real information density — more per screen than a cautious designer allows
3. Every state designed: empty, loading, error, forbidden, one row, five hundred
4. One control vocabulary across the whole product, admin included
5. Nothing shifts, flashes, or renders half-styled

Every one of those is a thing we do, not a thing we take. A pixel-clone would be
the one move that is both legally exposed (trade dress) and commercially
pointless — nobody migrates to a worse-looking copy of what they already run.

## The system

### Space

One scale, based on 4px, exposed as tokens and used for every padding, margin
and gap.

```
--s-1:  0.25rem   4px   hairline gaps, icon to label
--s-2:  0.5rem    8px   inside a chip, between tight controls
--s-3:  0.75rem  12px   between controls in a row
--s-4:  1rem     16px   the default. Card padding, list gaps
--s-5:  1.25rem  20px   a control's horizontal padding
--s-6:  1.5rem   24px   between blocks inside a panel
--s-7:  2rem     32px   between panels
--s-8:  2.5rem   40px
--s-9:  3rem     48px   page rhythm, section separation
--s-10: 4rem     64px   the top of a page, an empty state
```

The first draft of this skipped 1.25rem and 2.5rem, which are perfectly good
4px steps. That is not a tighter scale, it is a scale that forces correct values
off the grid — churn with nothing at the end of it.

Anything not on the scale needs a comment saying why. Optical corrections are
real — a 15px value to make an icon sit right is legitimate; a 15px value
because nobody checked is not.

### Type

Eight sizes, not forty-six. Named by role, so a template asks for what a thing
*is* rather than how big it should be.

```
--t-micro:  0.7rem    uppercase labels, table headers, meta on a chip
--t-small:  0.8rem    secondary text, timestamps, hints
--t-body:   0.9rem    the default. List rows, form fields, controls
--t-read:   1rem      prose meant to be read at length — post bodies
--t-lead:   1.15rem   a subtitle, an intro paragraph
--t-h3:     1.25rem   panel and card titles
--t-h2:     1.6rem    page titles
--t-h1:     2.1rem    the one hero on a page, if it has one
```

Weights collapse to three: `400` body, `600` emphasis and headings, `750` the
one number or title carrying a screen.

### Radius

Already tokenised and mostly consistent. Formalised as three, all derived from
the theme's `--radius` so a site that sets 0 gets square corners everywhere:

```
--radius        the control radius: inputs, buttons, small surfaces
--radius-card   panels and cards (--radius * 1.35)
--radius-pill   999px, for anything capsule-shaped
```

The bare `6px`, `8px` and `4px` values in the stylesheet resolve to these.

### Colour

One set of tokens, `--c-*`, for the whole product. **`--admin-*` is deleted and
the admin adopts the front-end tokens.** That single change gives the ACP
theming, dark mode and OS-following for free, and makes a button in the ACP the
same object as a button on the front end.

The admin's chrome — the dark sidebar — stays dark by intent in light mode. That
is a deliberate, named choice (`--c-chrome-bg`), not an accident of a separate
palette.

Roles, not shades. A template asks for `--c-danger-soft`, never `#fef2f2`. The
semantic tint work already done for the System option is the pattern: hue,
strength, and use — soft/line/ink/chip.

### Control vocabulary

One button, three emphases (primary, default, quiet) and one destructive
variant. One input. One select. One table. One panel. One empty state. Each
defined once, in the shared stylesheet, and used by both front end and admin.
A control that exists only in the ACP is a bug unless there is a reason written
next to it.

### States

Every list, table and panel ships five states or it is not finished:

- **empty** — with the action that would fill it, not just "No results"
- **loading** — if it can take longer than a paint
- **error** — what went wrong and what to do, no apology
- **forbidden** — said plainly, not a 404 pretending
- **full** — pagination, truncation, and what happens at five hundred rows

This is the item most likely to be skipped and the one that most reads as
"finished software" when it is not.

## Keeping it

🚨 A design system nobody enforces decays back to forty-six font sizes. Every
standing rule in this project has a mechanised check behind it — the mobile
audit, the gutter check, the colour scheme matrix — because that is the only
kind of rule that survives.

So: `tests/design-audit.js`, run alongside the others, failing on

- a `font-size` not on the type scale
- a `padding`, `margin` or `gap` not on the space scale
- a raw hex colour outside the token definitions
- a `border-radius` that is not one of the three
- any surviving `--admin-*` token

with a per-rule allowance for a commented exception, because optical corrections
are real and a check that cannot be argued with gets disabled.

The number it reports is a ceiling, not a target. Going down is always fine and
quietly lowers the ceiling; going up fails the check, because the only way it
goes up is somebody writing a new hardcoded value, which is the drift that
produced forty-six font sizes in the first place. `--accept` moves the ceiling
deliberately.

**The front end is not being migrated, and that is a decision.** Its 846
off-scale values were placed across five design passes and the screens are
right; snapping them would move things that somebody meant. The admin had no
passes at all, which is why its turn came first and cost nothing. So the system
governs everything new, the admin is fully on it, and the front end is held
where it is rather than churned to satisfy a counter.

## Order of work

1. Tokens and the check, reporting only — the ruler before the cutting
2. The shared control vocabulary — button, input, select, table, panel, empty
3. The admin onto `--c-*`, deleting `--admin-*`
4. The nineteen admin templates onto the shared controls, screen by screen
5. The front end's off-scale values, which are fewer and less visible

The admin is where the work shows. It is the surface that says "years of
development" to anybody evaluating Convoro, and it is currently the surface that
says the opposite.
