# Design contract

For designers working on Localintel — in-house or contract. It is short on purpose; you should be
able to read it in fifteen minutes and go back to designing.

## What this is, and what it is not

This constrains the **vocabulary**, not the design. Which greys exist, which type sizes exist, what a
breakpoint is allowed to change — that sort of thing. Nothing here tells you what to make, and none of
it is a review checklist you have to satisfy before anyone will look at your work.

The reason it exists: on our last redesign, the parts that cost the most were never the interesting
parts. They were a 13.5px font that had to become a hand-written rule, a card track drawn against a
wider canvas than ours that silently lost a column, and a mobile layout that regrouped controls in a
way that forced the same buttons to be built twice. All of it avoidable, none of it visible in the
design file. This is that list, written down.

Everything below is live and inspectable in the **boutique** — one URL, the real components, the real
tokens. This document is published there too, so if you are reading it on the web the links below
already work; if you are reading the file, ask for the boutique link. Where it says
[/tokens](/tokens), that is a page you can open, read values off, and inspect in DevTools.

## 1. The deliverable

**Keep sending HTML prototypes.** They are worth more to us than any static mockup, because we can
read exact values out of them and see hover and open states without asking. Please carry on.

Two things make them much cheaper to implement:

- **Annotate values with token names** rather than raw numbers where you know them —
  `--color-heading-2` instead of `#0D1420`, `text-heading-3` instead of `17.5px`. See §2.
- **Say which parts are real and which are placeholder.** Charts, maps, tables and KPI blocks in your
  file are almost certainly standing in for our live components, which bring their own layout. A
  one-line note per region — *"real component, art indicative"* versus *"design this"* — removes a
  whole class of guessing. Last time we had to ask.

## 2. Design on our scales

A value that is not on our scale is not slightly more expensive — it is a different kind of thing. On
the scale, it is one word. Off it, it is a bespoke CSS rule that then has no dark mode and no
responsive behaviour of its own, and that nobody else can reuse.

Concretely, from real cases: **17.5px cost nothing** (it is a step, `text-heading-3`), and **13.5px
cost a hand-written rule** for a difference nobody could see next to 14px. Same for radii — 10px and
22px are steps; 19px is not, and on a short control what you almost certainly mean is a full pill,
which *is* a step.

See [/tokens](/tokens) for every value, with light and dark shown together.

- **Type** — 23 steps. Size, weight and line height travel together under one name, so ask for a step
  rather than three numbers.
- **Radius** — Tailwind's scale plus two named tokens (`panel` 10px, `sheet` 22px).
- **Spacing** — a 4px scale. Any multiple of four is free; anything between two steps is not.
- **Colour** — see below.

### Colour has two halves, and they are used differently

This one is not obvious and it has bitten us:

- **Theme tokens** — surfaces, primary, error, success, the control colours. Around fifty, and they
  generate real utilities.
- **`:root` variables** — a larger set that includes *every* text-role colour: `heading-1` …
  `heading-5`, `subtitle`, `action`, `filter-option`, `date`. These have no utility behind them and
  are referenced differently in code.

Practically, for you: name the role, not the value. `--color-heading-2` for a page subtitle,
`--color-heading-5` for a caption. Pick by **role**, not by how dark it looks — heading-1 is the page
title and heading-5 is a caption, and the darkness relationship between them inverts in dark mode.

**Never specify a hex.** A token carries a light and a dark value and survives a per-client re-theme.
A hex gets you neither, and forces us to guess which of three near-identical greys you meant.

## 3. Both modes, always

The app runs in light and dark. Every colour needs both, and dark is not an inversion — contrast and
accent legibility have to be decided, not derived.

If designing both doubles your work, the alternative that costs you nothing: **name tokens instead of
values** (§2) and dark comes free, because the token already carries it.

Our last reference was light-only, which meant every colour was decided twice — once by you and once
by whoever implemented it. Every specimen in the boutique is shown in both modes side by side for
exactly this reason.

## 4. Breakpoints: one rule

There is one breakpoint that matters: **`md`, at 768px**. Below it is "mobile", above it is "desktop".

> **Across breakpoints, keep the same elements in the same containers.**
> Change size, direction, order and visibility as freely as you like.
> Do not re-parent an element, and do not split one group into two.

Size, direction, order and visibility are nearly free — a row becomes a column, a control grows,
something hides, the order shuffles. All four are a line of CSS.

**Re-parenting is not free.** If a control moves from one container to another between breakpoints,
the platform cannot move it — so it gets built twice, once in each place, with one copy hidden. Two
copies drift. That is a real cost you can spend deliberately, but you should know you are spending it.

Two examples from our own work, both on [/patterns](/patterns) with the numbers:

- *Cheap.* The index toolbar. One row at desktop; below md the row becomes a column, search goes full
  width, the filters become a scrolling strip, the view switch hides. Four changes, nothing moved
  containers, an afternoon.
- *Expensive.* The preview modal. Desktop has two independently scrolling columns; mobile is one
  scroll with a pinned header and action bar, and the desktop side rail's five actions become a
  three-button bar plus an overflow menu. Both were rebuilds rather than restyles. Worth it here —
  but a design that kept one scroll region at both sizes would have cost nothing.

Also:

- **Design mobile at 375px**, and sanity-check 320px. Our last mobile file was drawn wider than a real
  phone, so chips that fitted the canvas overflowed the device.
- **When a row is too wide, say what happens**: scroll, wrap, or truncate. All three are buildable.
  Guessing between them is not.

## 5. Widths and grids

State the canvas width you drew against. Our content column caps at **1240px**, with a page gutter of
**36px** (18px below md).

**Specify a grid as a rule, not a track.** *"Four across at desktop, one on a phone, 22px and 30px
gutters"* is something we can satisfy at any width. A fixed 290px column only works on the canvas it
was drawn on: at 1340px it gives four across, and in our 1240px container it quietly gives three.
That happened, and the fix was arithmetic nobody could do from the file alone.

## 6. States, not just the happy path

The single most common gap. A screen in one state is one sixth of a screen.

For every **control**: default, hover, active, focus, disabled, loading.
For every **list or collection**: loading, empty, error, one item, many items.
For every **field that can be missing**: what shows instead — we use an em dash.

Why this one matters more than it sounds: a menu row in our app was *correctly* disabled in code for
weeks but looked identical to a live row — same ink, same hover — because no design had ever shown a
disabled menu row, so nobody had styled one. It read as broken rather than as not-yet-available.

## 7. Real content, including the worst case

Design with the longest plausible string in every field, and say what should happen when it does not
fit.

A live example: an "Owner" row was designed showing `You`. The backend actually returns an email
address, and a long one pushed the whole panel into a horizontal scrollbar. Both are one-line fixes
*if the question is asked* — wrap, truncate, or scroll — and a guess otherwise.

Please also don't hand us dummy imagery to reproduce. Where a region shows real content, we will use
the real content; a placeholder state ("No preview image") is worth designing, and we will use that.

## 8. What we add after you

Not gaps in your work — deliberately ours, so you don't have to carry them:

- **Accessibility.** Focus rings, ARIA roles, keyboard navigation, contrast checks. A design that
  omits them is silent on the subject, not arguing against them, and we never remove existing
  accessibility to match a picture.
- **Two consequences worth leaving room for**, because they are layout, not decoration: a focus ring
  needs about **2px of clearance** around a control, so avoid placing focusable controls flush against
  a clipping edge; and mobile hit targets grow toward **44px**, so a 34px control on a phone will
  probably get taller.
- **Safe areas.** Where your mobile frame reserves space at the top and bottom for a notch or a home
  indicator, we measure the device rather than hardcoding your numbers. Design the padding; we will
  translate it.

## 9. Build from the inventory

Browse the components at [/buttons](/buttons), [/forms](/forms) and [/overlays](/overlays) — with their
variants, states, and limits. The exact inventory and count are in
[/design-system.json](/design-system.json), which is generated from the code and cannot go stale.

- Composing existing components: free.
- **Extending** one with a new variant: usually minutes. A checkbox gained an indeterminate state and
  a stronger outline in about that.
- A genuinely **new** component: real work, and worth flagging as such in your handover so it can be
  planned rather than discovered.

One current limit worth knowing, since it shapes mobile menus: every dropdown in the app is an
anchored panel that hangs off the control that opened it. **There is no bottom-sheet presentation
yet.** A mobile design built on sheets is asking for new work — which may well be the right call, but
say so rather than assuming it exists.

## 10. When we deviate

Sometimes we will not match your design exactly, on purpose: when the difference is imperceptible and
the exact version would cost a bespoke token, a new component, or a fragile override. Our own rule is
*match exactly where it is cheap, approximate where exact is expensive, and never silently.*

So every deliberate deviation gets written down and sent back to you — what we changed, and why. You
can accept it or push back. What we are trying to avoid is not deviation; it is drift nobody
mentioned.

## Pre-flight

Six questions. If they are all yes, the handover will go smoothly.

1. Are colours named as tokens rather than hexes, and does every colour have a dark value?
2. Are type sizes, radii and spacing on our scales — or, where they are not, is that a deliberate ask?
3. Does every breakpoint change amount to size, direction, order or visibility only? If something is
   re-parented or regrouped, is that called out?
4. Is the mobile view drawn at 375px, and does every over-wide row say whether it scrolls, wraps, or
   truncates?
5. Are grids expressed as *N across at width W*, and is your canvas width stated?
6. Does every control show its disabled and loading states, and every list its empty and loading
   states?

And the two annotations that save the most time: **which regions are real components**, and **what the
longest realistic string is** in each text field.
