Milton design system
This document is the single source of truth for Milton’s visual language. It replaces the previous amber-on-warm-white palette entirely. Read it fully before making any changes.
1. What changed and why
The previous palette used a warm off-white base with an amber accent. Two problems:
- Amber is the universal warning colour. In a compliance product, “needs review” is one of the most important states in the UI. Amber cannot be both the brand colour and the review state — every button competed with every warning.
- The warm base + warm accent + near-black ink structure was not distinctive. The fix is structural: a cool neutral base and an accent that carries no status meaning.
The new system is achromatic-dominant. Colour is spent on status, not decoration.
Amber is not deleted. It is demoted from brand colour to the review status colour. This distinction drives most of the migration decisions below.
2. Colour tokens
Core
| Token | Light | Dark | Use |
|---|---|---|---|
ink | #14161A | #E8E9EC | Primary text, logotype, headings |
graphite | #2E323B | #9DA3AF | Secondary text, body copy at lower emphasis |
slate | #6B7280 | #6B7280 | Muted text, metadata, timestamps, placeholders |
rule | #D8DBE0 | #2C3038 | Hairline borders, dividers, table rules |
paper | #F2F3F5 | #111318 | Page background |
surface | #FFFFFF | #1A1D23 | Card, panel, modal background |
Accent — interaction only
| Token | Light | Dark | Use |
|---|---|---|---|
signal | #453C7A | #8B7FD4 | Primary buttons, links, active nav, focus rings |
signal-hover | #5D52A0 | #A196E0 | Hover and active states |
signal-tint | #ECEBF4 | #24222F | Selected rows, focus backgrounds, subtle highlights |
signal means “this is interactive.” It never means “this is good” or “this is a problem.” Do not use it for status, emphasis, illustration, or decoration.
Status — reserved vocabulary
| Token | Light | Dark | Tint (light) | Tint (dark) | Meaning |
|---|---|---|---|---|---|
compliant | #1B6B47 | #4FB587 | #E6F0EB | #16281F | Passing check, verified evidence |
review | #96610B | #E0A244 | #FAF0DE | #2A2113 | Needs human review, expiring evidence |
non-compliant | #B03A2E | #E07A6E | #F7E9E7 | #2B1A18 | Failing check, missing control |
pending | #8E92A0 | #8E92A0 | #EDEEF1 | #1F2228 | Queued, not yet evaluated |
Status colours appear only where they carry that meaning. They are never used as generic accent, chart series colour, or category colour.
Badges and pills use the tint as background and the solid as text. Never solid fill with white text — in a table of hundreds of rows, solid status fills turn the screen into a traffic light. Example: a compliant badge is #1B6B47 on #E6F0EB.
3. Typography
| Role | Family | Notes |
|---|---|---|
| UI and body | IBM Plex Sans | Everything by default |
| Data and identifiers | IBM Plex Mono | Document IDs, hashes, timestamps, version numbers, compliance codes, audit references, diffs |
The mono face is functional, not decorative. Anything that represents a tracked, immutable, or machine-generated value gets mono. This is a deliberate part of the identity — it gives the product a visual language for “this is a record.”
Weights: 400 regular, 500 medium, 600 semibold. Do not use 700 or above.
Install via @fontsource/ibm-plex-sans and @fontsource/ibm-plex-mono rather than a Google Fonts CDN link — self-hosting avoids a third-party request, which matters for a product sold to compliance-conscious buyers.
4. Migration rules
These are the rules that determine whether the migration is correct rather than merely complete.
4.1 Do not blind-replace hex values
Every old amber occurrence must be classified before it is replaced. The same old hex maps to different new tokens depending on what it meant:
| Old amber was used for | New token |
|---|---|
| Primary button, link, active nav item, focus ring | signal |
| Warning banner, “expiring soon”, “needs review” state | review |
| Logotype, wordmark, brand mark | ink — remove colour entirely |
| Chart series, illustration, decorative fill | slate or signal-tint — reassess case by case |
If a usage is ambiguous, flag it rather than guessing.
4.2 The logotype stays achromatic
The Milton wordmark and the scattered-blocks icon are pure ink on paper, or pure paper on ink for inverse. No accent colour. If you find a coloured logo variant in the codebase, convert it to ink and note it.
4.3 Warm neutrals become cool neutrals
The old base was warm (cream, tan, warm grey). Every warm neutral maps to its cool equivalent in the core ramp. Do not leave a warm off-white anywhere — a single cream surface next to #F2F3F5 reads as a bug.
4.4 Dark mode values are not derived
Do not compute dark mode by inverting or darkening light values. Use the dark column as given. signal in particular shifts substantially (#453C7A → #8B7FD4) because the light value is unreadable on a dark surface.
4.5 One accent per view
At most one signal-filled primary button per screen. Secondary actions use an outline style with rule border and ink text. If a screen has three filled purple buttons, two of them are wrong.
4.6 Contrast floor
All text meets WCAG AA: 4.5:1 for body, 3:1 for large text and UI boundaries. review at #96610B is the darkened variant specifically so small badge text passes — do not substitute the lighter #B4740E.
Milton is a compliance product. Shipping an interface that fails accessibility standards is a credibility problem, not just a quality one.
5. Definition of done
- No hardcoded hex values remain in component files — all colour flows through tokens
- No warm neutrals remain anywhere in the codebase
- Both fonts load and render; no fallback flash
- Mono face applied to IDs, timestamps, hashes, and version strings
- Light and dark mode both verified on every changed screen
- Logotype renders achromatic in both modes
- Status badges use tint background with solid text
- Focus states visible and use
signal - No screen has more than one filled primary button
6. Additions from the Phase 1 audit
These extend the system to cover two real cases the original spec didn’t anticipate, plus one clarification and one exemption.
6.1 Category / type differentiation
Where the UI distinguishes several types of the same object (e.g. block types) rather than status, do not invent a new colour ramp. Chroma is reserved for status (§2) and signal (§2.1) — a seven-hue category ramp turns the system back into the “rainbow SaaS” look the palette exists to avoid, and it fails colourblind users regardless of how carefully the hues are chosen.
Differentiate type with icon + label, not hue. All type badges share one neutral fill. If a block needs to carry both a type and a status (e.g. “Policy” type, “needs review” status), the status shows as a separate small indicator — a dot or left border — not a recolour of the type badge. Keep the two dimensions visually independent so they aren’t read as the same signal.
6.2 Surface elevation
paper and surface cover pages and cards, but real UIs need more steps for nested wells, hover rows, and overlays. Add these by purpose, not by number:
| Token | Purpose |
|---|---|
surface | Cards, panels — existing |
surface-sunken | Nested wells, input backgrounds, code blocks — recedes from surface toward paper |
surface-hover | Row/item hover and active list states |
surface-overlay | Modals, popovers, dropdowns, tooltips — pairs with a shadow, not just a fill change |
Derive exact values as consistent lightness steps per theme rather than eyeballing. In light mode this is mostly shadow-driven, since surface is already near-white. In dark mode expect four genuinely distinct fills. Map the current ramp’s actual call sites onto these four; anything that doesn’t fit cleanly, flag rather than force.
6.3 “Completed” is not “Compliant”
A workflow finishing is not the same fact as a control passing. Don’t badge “Completed” with a status colour by default — render it in ink / graphite as a neutral state. Only apply compliant / non-compliant / review where the badge reports the result of an evaluation, not that a process ran.
6.4 Exemptions
User-assigned colours (e.g. a colour picker for tagging the user’s own documents) are user content, not brand chrome, and sit outside this system entirely. Don’t constrain them to the palette.