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.md wins.

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. signal means “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 surfacesurface-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-lg to rounded-2xl by size. The bigger the box, the larger the radius.
  • Badges are the one pill. rounded-full at 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. xs 1.25rem · sm 1.5rem · lg 2rem, plus square icon variants.
  • Primary: signal fill. At most one per screen (DESIGN-SYSTEM.md §4.5).
  • Outline: transparent with a rule hairline 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-visible sets the border to --ring and 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 --input border, surface-sunken fill.
  • Focus: border becomes --ring, plus a 2px ring.
  • Error: aria-invalid drives 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/10 instead of a border, 1rem padding (0.75rem at size="sm"), 1rem internal gap.
  • Interactive cards add .surface-interactive: on hover the border shifts to signal and Elevation 1 appears.
  • Never nest a card in a card. Use surface-sunken.

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). Raw ink/graphite/slate are theme-invariant and fail contrast as text in dark mode (§6.5).
  • Do keep signal to 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-motion by 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 signal for 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-blur as 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.