Design System: MILTON ReactClient
Colour and typography live in
../DESIGN-SYSTEM.md, which is the single source of truth for both. This document covers what that one does not: layout, elevation, shape, component anatomy, and the interaction rules. Where the two ever disagree about a colour or a typeface,DESIGN-SYSTEM.mdwins.This file previously carried its own palette (a warm “brass and bone” system) and its own type ramp. Both were replaced wholesale in the 2026-08 migration and have been removed rather than updated, so there is only one place to look.
Overview
MILTON is a compliance documentation tool, and the interface is built to be read for hours rather than admired for seconds. It is achromatic-dominant: colour is spent on status, never on decoration. Chrome recedes; the document and its structure are the only things allowed to look important.
The interface is dense on purpose. Controls are small (a 1.75rem button, a 0.7rem label), spacing is tight inside a group and generous between groups.
Key characteristics
- One accent, and it is never a status.
signalmeans “this is interactive” and nothing else. - Chroma is reserved. Status colours carry their four meanings; everything else is neutral.
- Type is differentiated by icon and label, not hue — see
DESIGN-SYSTEM.md§6.1. - Mono means “this is a record.” IDs, hashes, timestamps, versions, pipeline references.
- Four purposeful surfaces, not a numbered ramp — see §6.2. Nesting steps through
surface→surface-sunken; shadows are for things that genuinely leave the page. - Compact controls. Default button and input height is 1.75rem. This is a tool for people who are in it all day.
Layout
Two layout registers, and they behave differently.
Standard routes sit inside the app shell: a collapsible sidebar (16rem, 3rem collapsed), a
sticky 4rem header, and a content column capped at max-w-400 with 1.5rem padding rising to
2rem at lg. Vertical rhythm comes from two composition classes — .page-stack (gap: 2rem,
between major sections) and .section-stack (gap: 1rem, within one).
The template editor deliberately leaves the shell: a full-height, non-scrolling three-column workspace (structure 24% · canvas 52% · preview 24%) built on resizable panels, each collapsible to zero from a control mounted on the seam itself. Its own header carries the back affordance the app sidebar would otherwise provide. Zen mode drops to canvas plus preview — it hides chrome, never the deliverable.
Spacing uses the --space-1…9 scale (0.25rem → 6rem). Inside a group, steps 1–3; between groups,
4–6; between page sections, 6–7.
Grids collapse at Tailwind defaults: card grids run 1 → md:2 → xl:3; editor panels stack below lg.
Elevation & Depth
Depth is tonal first, shadow second. Structure comes from the surface tokens; a shadow means the
element has genuinely left the page. In light mode this is mostly shadow-driven, because surface
is already near-white; in dark mode the four surface fills do most of the work.
Shadow vocabulary
Shadows are cool ink (rgba(20, 22, 26, …)), matching the neutral base. Four steps plus an inset:
- Elevation 1 — resting cards, small controls that should read as pressable.
- Elevation 2 — raised surfaces, dropdowns, the primary action in a header.
- Elevation 3 — dialogs and sheets.
- Elevation 4 — toasts and tooltips, the top of the stack.
- Inset 1 — a single highlight line along the top edge of a raised surface.
Exact values live in src/index.css; reference the --shadow-elevation-* tokens rather than
copying the rules.
Named rules
The Tonal-First Rule. If two containers are nested, they differ by surface token, not by shadow.
.template-depth-frame → .template-depth-card-strong → .template-depth-card steps through
surfaces and uses no shadow to do it.
The One-Or-The-Other Rule. A 1px border and a ≥16px-blur shadow never appear on the same
element. Modals carry the shadow and drop the border; resting cards carry the border (or a ring-1)
and drop the shadow.
Shapes
Corners are modest and derive from one root: --radius: 0.45rem, scaled to sm 0.27 / md 0.36 /
lg 0.45 / xl 0.63 / 2xl 0.81 / 3xl 0.99 rem.
- Controls (buttons, inputs, selects, menu items) are
rounded-md— 0.36rem. Small radii on small controls; anything rounder reads as consumer software. - Containers (cards, panels, dialogs) run
rounded-lgtorounded-2xlby size. The bigger the box, the larger the radius. - Badges are the one pill.
rounded-fullat 1.25rem height, used inline as data — never as a decorative capsule around a heading. - Strokes are 1px. The only 2px rule in the system is the tree indent guide in the section structure panel, which is a structural rail rather than an accent stripe.
Components
Buttons
- Shape and size:
rounded-md(0.36rem), 1.75rem default height, 0.5rem horizontal padding,text-xs/relaxed, weight 500.xs1.25rem ·sm1.5rem ·lg2rem, plus squareiconvariants. - Primary:
signalfill. At most one per screen (DESIGN-SYSTEM.md§4.5). - Outline: transparent with a
rulehairline and ink text. This is the default for secondary actions. - Ghost: no chrome; hover fills with
muted. - Destructive: tinted, never a solid red slab.
- Link:
signal, underline on hover. - Focus:
focus-visiblesets the border to--ringand adds a 2px solid ring at full opacity — a 30%-alpha ring measures under the 3:1 minimum for non-text UI. - Press:
translate-y-px. One pixel, no scale, no bounce.
Badges
- Shape:
rounded-full, 1.25rem tall,text-label. - Neutral variants:
neutral,outline,secondary,ghost— for type, metadata and pipeline state. - Status variants:
compliant,review,nonCompliant,pending. Tint background, solid text. Never a solid fill with white text. These are only for reporting the result of an evaluation (§6.6). - Block-type badges share one neutral fill and differentiate by icon (§6.1).
Inputs / fields
- Style:
rounded-md, 1.75rem tall, 1px--inputborder,surface-sunkenfill. - Focus: border becomes
--ring, plus a 2px ring. - Error:
aria-invaliddrives the destructive border and ring, so the visual state and the accessible state cannot drift apart. - Placeholder:
--muted-foreground. Never a lighter grey.
Cards
- Shape:
rounded-lg,ring-1 ring-foreground/10instead of a border,1rempadding (0.75rematsize="sm"),1reminternal gap. - Interactive cards add
.surface-interactive: on hover the border shifts tosignaland Elevation 1 appears. - Never nest a card in a card. Use
surface-sunken.
Navigation (app sidebar)
16rem expanded, 3rem icon-collapsed, on --sidebar with a hairline separator. Items are 2.25rem
tall at the body step (0.875rem), weight 500. The active item takes a signal-tint fill — active
navigation is interaction, which is what signal is for. Chevrons nudge 2px on hover.
Section tree (signature component)
The template editor’s structure panel is a real role="tree": every row is a treeitem carrying
aria-level, aria-setsize, aria-posinset and aria-expanded, with roving tabindex so the tree
is one tab stop and j/k/arrows move within it. Each row shows its hierarchical number (1.2.3)
beside the title, because a V-Model section is cited by number. The drag grip is aria-hidden and
untabbable — its only function is a pointer drag, and the keyboard path is the row’s context menu.
Toasts
Sonner, at Elevation 4, themed from the popover tokens. richColors is off — sonner’s built-in
green/amber/red would paint over the reserved status vocabulary. Every destructive action ships an
Undo action on its success toast, backed by a deep-clone snapshot of the section tree. Success
copy is a short declarative past-tense sentence (“Template saved.”, “Source removed.”). Failure copy
names the status and the recovery, never “please try again” as a catch-all.
Do’s and Don’ts
Do
- Do route text colour through the semantic aliases (
text-primary,text-secondary,text-muted,pending-ink). Rawink/graphite/slateare theme-invariant and fail contrast as text in dark mode (§6.5). - Do keep
signalto interaction only, and to one filled primary button per screen. - Do render pipeline state and evaluation verdict as two separate things (§6.6).
- Do use
text-label(0.7rem) as the smallest type on any surface. - Do build nesting from the four surface tokens and leave resting surfaces flat.
- Do set mono on anything that is a tracked, immutable or machine-generated value.
- Do measure contrast against the actual surface token behind the text, not against white.
- Do pair every destructive action with a snapshot and an Undo action on its toast.
- Do state what a destructive action will destroy, with a count, before it runs.
- Do give long-running work a named, cancellable, resumable surface — waiting is a designed state in this product.
- Do honour
prefers-reduced-motionby substituting a cross-fade rather than deleting the feedback; progress indicators keep turning.
Don’t
- Don’t use a status colour as a category, series or accent colour.
- Don’t use
signalfor status, emphasis, illustration or decoration. - Don’t differentiate types of the same object by hue — icon and label, one neutral fill (§6.1).
- Don’t badge “Completed” with a status colour. A process finishing is not a control passing.
- Don’t write a colour literal in a component. Every colour is a custom property.
- Don’t wrap a hex custom property in
hsl().hsl(#453c7a / 0.65)is invalid CSS and fails silently. - Don’t put a 1px border and a ≥16px-blur shadow on the same element.
- Don’t use
backdrop-bluras decoration, or a zero-offset coloured halo as a shadow. - Don’t put a kicker or eyebrow label above a heading. The heading carries itself.
- Don’t print instructions on a component to explain an interaction — instructions-as-UI means the interaction is wrong.
- Don’t use celebratory badges, sparkle icons, gradients, or gradient text.
- Don’t use a font weight above 600.
- Don’t ship a
role="button"div where a<button>belongs, and don’t leave a custom widget in the tab order when its only function needs a pointer. - Don’t expose an internal id in the interface. Sections are addressed by number, documents by title.