# Saer design system: rules for agents

You are editing `packages/saer-web` (saer.app). Follow these rules exactly. They are enforced by `npm run lint`, `npm test` and by Saer itself. If a rule blocks you, add a token or ask; do not work around it.

## Before you write

1. Tokens live in `design-system/tokens.json`. `app/styles/tokens.css` is generated from it (`npm run tokens`). Never edit `tokens.css` by hand.
2. Components live in `app/components/ui` (primitives), `app/components/site` (header, footer), `app/components/visuals` (data visuals). Reuse one before writing markup.
3. Capability states (in the beta, in testing, in development, planned) come only from `lib/capabilities.ts`. Never describe what Saer does in your own words where a state is implied.

## Hard rules

- No raw colour anywhere outside `tokens.json`. Not hex, not rgb(), not named colours. Use a semantic role: `var(--color-fg)`, `var(--color-bg-raised)`, `var(--color-verified)` and so on.
- Components never use palette variables (`--green-400`, `--charcoal-900`). Use roles. Email and Open Graph images, which cannot read CSS variables, import `hex` from `lib/tokens.ts`.
- Every `font-size`, `font-weight`, `line-height` and `letter-spacing` is a token: `--text-*`, `--font-weight-*`, `--leading-*`, `--tracking-*`.
- Every padding, margin, gap and inset is a `--space-*` token (or `calc()` of one). No ad hoc pixels.
- Borders use `--border-width-hairline` or `--border-width-rule`. Radii use `--radius-none`, `--radius-sm` or `--radius-full`. Durations and easings use `--duration-*` and `--ease-*`.
- Media queries use the breakpoint tokens as literals, because a media query cannot read a custom property: `(min-width: 48rem)`, `(min-width: 64rem)`, and `47.99rem` / `63.99rem` for max-width. No other breakpoints.
- Font stacks end in `sans-serif` or `monospace`. Never serif. Never ship Futura (not licensed for the web).
- Copy: UK English, sentence case, no em dashes, no exclamation marks in UI. Never claim a planned or in-testing capability is shipped. No invented statistics, testimonials or comparisons.

## Colour means something

- Green (`--color-verified`, `--color-action`): verified, deterministic, in the beta, the primary action. On cream it is a fill, never text; green text uses `--color-verified-fg`.
- Lavender (`--color-inferred`): inferred, in testing, in development, intelligence. Lavender text uses `--color-inferred-fg`.
- Charcoal and cream are surfaces. To make a subtree charcoal, set `data-theme="dark"` on it (or use `<Section surface="charcoal">`). Every role remaps; do not special-case.
- The whole page follows `prefers-color-scheme` unless `<html data-theme>` is set by the footer switch. Never hard-code a light-only or dark-only colour; use roles. Highlight bands under display words use `--color-mark-verified` / `--color-mark-inferred`, never `--color-verified` / `--color-inferred`, because text crosses them.
- Shape repeats colour: squares for exact facts, circles for inference. Never rely on colour alone; say it in text too.
- One primary (green) button per view.

## Accessibility (required)

- Every interactive element shows the global `:focus-visible` ring. Never remove an outline without an equal replacement.
- Text pairs must pass 4.5:1, boundaries and focus 3:1, on both surfaces. Add any new pair to `contrast.pairs` in tokens.json; the test computes it.
- Pointer targets are at least 24 by 24 px; controls use `min-height: var(--space-7)` (48px).
- Any animation or transition must be harmless under `prefers-reduced-motion: reduce` (base.css cuts them to one frame; do not override that with `!important`).
- Form fields: a visible `<label>`, `aria-invalid` and an error message linked with `aria-describedby`. Consent boxes are never pre-ticked.
- Use landmarks (`main#main`, `nav` with a label), one H1 per page, headings in order.

## Data visuals

- Only real Saer output: `content/lunary-scan.json` (from `scripts/extract-scan.py`) and `content/dogfood.json` (from `scripts/extract-dogfood.py`). Never type a number that should come from a scan.
- Data colours (a swatch of a scanned hex) go in inline `style` from the data, never as literals.
- Every visual has a text alternative: `aria-label`, a caption, or a table.

## After you write

```
npm run lint    # design rules
npm test        # tokens, contrast, truth table, forms, SEO
npx tsc --noEmit
```

Then run Saer on the package and keep exact drift and off-scale values at zero:

```
node packages/saer-cli/bin/saer.js scan packages/saer-web --output /tmp/saer-site
python3 packages/saer-web/scripts/extract-dogfood.py /tmp/saer-site/system.json > packages/saer-web/content/dogfood.json
```
