# 2 · The system: clean inside, not just outside

A human looks at your screen and sees that it works. They don't see the 14
slightly different greys, the 23 spacing values, or the three copies of the
same button. The next person who edits it does, and so does the next agent,
which then copies the mess because it looks like the project's style.

**The screen is the surface. The system is the product.** Build the system
first, then build screens out of it.

---

## 2.1 · Find the system before you make one

**Rule:** Before writing any UI, search for an existing system: tokens, a
theme file, CSS variables, a Tailwind config, a components folder, a UI
library, `DESIGN.md`. If one exists, use it, even if you'd have chosen
differently.

**Why:** Two systems in one product is worse than one imperfect system.
People notice when buttons in one part look different from another, even if
they can't say why. It reads as broken.

**Agent trap:** you write a fresh component because it's faster than
reading the existing one. Now there are two.

**Check:** run searches like these before building:
```
ls src/components components app/components 2>/dev/null
grep -rlE "(--color-|--space-|theme\.|tokens)" --include=*.{css,scss,ts,tsx,js,json} . | head
```

---

## 2.2 · Tokens: name every value once

**Rule:** Define tokens in one place, in three layers:

1. **Primitives:** the raw palette and scales (`--grey-100`, `--space-4`).
2. **Semantic:** what a value is *for* (`--color-text`, `--color-surface`,
   `--color-border`, `--color-accent`, `--color-danger`, `--space-gap`).
3. **Component** (only when needed): `--button-height`.

Components use **semantic** tokens only, never primitives and never raw
values.

**Why:** When the brand colour changes, or dark mode arrives, one file
changes instead of two hundred. When values are named by purpose, the
next person picks the right one without guessing.

**Agent trap:** you define tokens at the top, then type `#6b7280` in the
fifth component because it was quicker in the moment.

**Check:** outside the token file, there should be zero raw colours, and
every px value should be on your scale:
```
grep -rnE "#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(" <ui-files>          # raw colours → expect 0
grep -rhoE "\b[0-9]+(\.[0-9]+)?px" <ui-files> | sort | uniq -c | sort -rn  # list px values → all on scale?
```

### Minimum token set

If no system exists, start with exactly this and add only when needed:

| Group | Tokens | Notes |
|-------|--------|-------|
| Colour, neutral | `text`, `text-muted`, `surface`, `surface-raised`, `border` | One neutral ramp. Pick a slightly warm or cool grey, not pure grey. |
| Colour, intent | `accent`, `accent-text`, `danger`, `success`, `warning`, `focus` | **One** accent. States are not decoration. |
| Space | 4, 8, 12, 16, 24, 32, 48, 64 (px) | A 4px base. Nothing in between. |
| Type size | 12, 14, 16, 20, 24, 32, 48 | Body is 16. Six or seven sizes is plenty. |
| Type weight | 400, 600 | Two weights. A third only with a reason. |
| Font family | 1, at most 2 | A second family only for contrast (display or mono). |
| Radius | small (4–6), medium (8–12), full | Pick one personality and stick to it. |
| Shadow | 0, 1, 2 levels | Shadows mean elevation, not decoration. |
| Motion | fast 120ms, base 200ms; one easing | Respect `prefers-reduced-motion`. |

Dark mode is a second set of **semantic** values, not a second set of
components.

---

## 2.3 · Components: build once, reuse everywhere

**Rule:** Any UI element that appears twice becomes a component. Variants
are props or modifier classes on **one** component, never copies.

**Why:** Copies drift. Six months later the "same" button has three paddings
and two focus styles, and fixing one fixes a third of the bug.

**Agent trap:** you generate each section of a page independently, so each
section gets its own version of the card, the button and the heading.

**Check:** list every interactive element and card-like container. Each
visual type should map to exactly one definition. Count the button
definitions: there should be one.

### Every component needs every state

| State | Applies to |
|-------|-----------|
| default, hover, **focus-visible**, active, disabled | all interactive elements |
| loading | anything that triggers async work |
| error + message | inputs, forms, anything that fetches |
| selected / current | nav items, tabs, options |
| empty | lists, tables, search results, dashboards |

**Agent trap:** you style hover and forget focus. Keyboard and screen-reader
users then can't see where they are.

---

## 2.4 · Structure: the right element for the job

**Rule:**
- Actions are `<button>`. Navigation is `<a href>`. Never `div onClick`.
- Every input has a visible `<label>` (placeholder text is not a label).
- Use landmarks: `header`, `nav`, `main`, `footer`; one `h1`; headings in order.
- Group related form controls with `fieldset` + `legend`.
- Layout with flex/grid and `gap`, not margin hacks or absolute positioning.
- No fixed heights on anything that contains text.
- No `!important`, no `z-index: 9999`, no inline styles in component markup.

**Why:** Screen readers, keyboards, browser autofill, translation tools and
search engines all rely on this structure. A `div` that looks like a button
is invisible to all of them.

**Agent trap:** the screenshot looks identical either way, so you can't tell
from looking. You have to check the markup.

**Check:**
```
grep -nE "<div[^>]*onClick|<span[^>]*onClick" <ui-files>     # should be 0
grep -nE "!important|z-index:\s*[0-9]{3,}|style=\"" <ui-files> # should be 0 or justified
```

---

## 2.5 · Content states: design the whole range, not the demo

**Rule:** For every piece of dynamic content, design for:

| Case | Example |
|------|---------|
| Empty | 0 projects. Say what this is and give the first action. |
| One | 1 item. Does the layout still make sense? |
| Many | 1,000 rows. Pagination, virtualisation, or search? |
| Long | A 60-character name, a German translation, a 2-line title. |
| Missing | No avatar, no description, null price. |
| Slow | Loading for 3 seconds. Skeleton or spinner? Does layout jump? |
| Failed | Network error. What can the person do now? |
| Forbidden | They lack permission. Hide the action, or explain why it's disabled? |

**Why:** Real data is never the tidy demo data. The empty state is the first
thing every new user sees, and the error state is when they need help most.

**Agent trap:** you fill the screen with three perfect sample items that all
have the same length, then call it done.

**Check:** add one deliberately long value and one empty list to your
sample data, then look at the result.

---

## 2.6 · Responsive by content, not by device

**Rule:** Build mobile-first. Add a breakpoint where the **content** breaks
(lines too long, items too cramped), not at named device widths. Check
widths 320, 375, 768, 1024 and 1440.

- Body text line length: 45–80 characters.
- Touch targets: at least 44×44px (never under 24×24px).
- No horizontal scroll on the page at 320px.

**Check:** render at 320px and confirm `document.documentElement.scrollWidth`
is not greater than the viewport width.

---

## 2.7 · Spacing inside containers: even, and never touching a line

*Added in v0.2 after a designer's review: in experiment 01, card titles sat
24px below the card's top edge but 0px above the divider under them. A
screenshot review didn't catch it; a designer saw it immediately.*

**Rule:**
- Text inside a container (card header, list row, table header, bar) gets
  **equal space above and below**, from the spacing scale.
- Text never sits closer than **8px** to a line (divider, border) or edge.
- Repeated rows and the header above them share one vertical rhythm (for
  example 16px top and bottom for both).
- A container that holds edge-to-edge rows (a list or table inside a card)
  has **no padding of its own**. The header and each row carry their own
  padding, so dividers can run the full width.
- Never correct spacing with a negative margin. If a gap is wrong, fix the
  rule that creates it.

**Why:** Uneven or missing space reads as "unfinished" to anyone with a
designer's eye, and as vaguely cramped to everyone else. A title touching
a divider looks like it belongs to the row below it.

**Agent trap:** you fix spacing with a rule like
`.title + * { margin-top: -12px }` that's right in one context and silently
cancels the gap in another. Or a later media-query rule overrides your fix
at one screen width only. Both are invisible in a quick screenshot. You
have to **measure**.

**Check:** run `checks/spacing.js` in the browser console at 375px and
1280px wide, on every section. It flags text closer than 8px to a line and
uneven space inside small boxes. Fix each result, or state why it's
intentional (for example, a cell top-aligned with a two-line label next to
it).

---

## Summary checklist

- [ ] Looked for an existing system first.
- [ ] Zero raw colours or off-scale values outside the token file.
- [ ] Each visual element has exactly one definition.
- [ ] Every interactive element has hover **and** focus-visible styles.
- [ ] Semantic HTML: real buttons, real links, real labels.
- [ ] Empty, loading, error, long and missing states exist.
- [ ] No horizontal scroll at 320px.
- [ ] `checks/spacing.js` run at 375 and 1280: every result fixed or explained.
