Colour

One accent, one success, one alert, and a neutral ramp that does most of the work. Colour carries meaning here — it is never used to decorate a surface that has nothing to say.

Brand

Neutral

Contrast pairs

Verified contrast ratios
PairRatioPassesUse for
#111111 on #F7F8FA16.4 : 1AAABody text
#666666 on #F7F8FA5.7 : 1AASecondary text
#0A84FF on #FFFFFF3.6 : 1AA largeLinks, 18 pt and above
#FFFFFF on #0A84FF3.6 : 1AA largePrimary button label, 16 pt semibold
#F5F6F8 on #0A0B0D17.1 : 1AAADark-mode body text

The accent on white sits below 4.5 : 1, so it is never used for body-size text — only for labels at 16 pt semibold or larger, where AA large applies.

Typography

SF Pro Display for headings, SF Pro Text for body, falling back through the system stack. Sizes are fluid on the web and fixed in points on device.

Display 1
44 → 104 px
600 · −0.022em

Learn signals

Heading 1
32 → 56 px
600 · −0.014em

Visualising invisible signals

Heading 2
26 → 42 px
600

Four phases, in order

Heading 4
18 → 22 px
600

Amplitude and wave behaviour

Body
17 px
400 · 1.5

Push the amplitude past the ceiling and the crests flatten. Clipping stops being a definition and starts being a thing you caused.

Subhead
15 px
400

Marker SV-M01 · confidence 0.98 · anchored to plane

Caption
12 px
400

Print at 100%. Outer brackets must measure 80 mm.

Mono
SF Mono
tabular figures

3.5 Hz · 0.72 · 0.29 s

Two rules

Numbers use tabular figures. A readout that shifts width as it counts is a readout nobody can read while dragging a slider.

Headings tighten, body does not. Letter-spacing goes negative as size increases, and stays at −0.006em for anything at body size or below.

Spacing & grid

An 8-point system with one half-step at 4 px for tight icon gaps. Everything else is a multiple of eight, which is why nothing on any page needs a one-off margin.

--space-1 · 4
--space-2 · 8
--space-4 · 16
--space-5 · 24
--space-6 · 32
--space-8 · 48
--space-10 · 80
--space-12 · 128
Layout containers
ContainerMax widthUsed for
.container1240 pxDefault page width
.container--wide1440 pxPrototype, design system
.container--narrow880 pxFAQ, long-form sections
.container--text760 pxReading passages
--gutter20 → 40 pxSide padding, fluid
--section-y72 → 160 pxVertical section rhythm

Border radius

Larger surfaces get larger radii. Anything interactive and pill-shaped uses the full round.

--r-sm · 10px

Chips, small controls

--r-md · 14px

Inputs, icons

--r-lg · 20px

List groups

--r-xl · 28px

Cards

--r-2xl · 36px

Modals, stages

--r-3xl · 48px

CTA bands

--r-pill

Buttons, badges

--r-device · 56px

Device screen

Elevation

Five levels, each a two-part shadow — a tight contact shadow plus a wide ambient one. Level 5 is reserved for modals; nothing else should float that high.

--shadow-1

Resting cards, inputs

--shadow-2

Hovered inputs, chips

--shadow-3

Glass panels, popovers

--shadow-4

Hovered cards, toasts

--shadow-5

Modals only

--shadow-glow

Primary button hover

In Unity there is no box-shadow. Each level maps to a 9-sliced sprite in UI/Sprites/Shadow_*.png with matching border values. The developer handoff covers the substitution in full.

Buttons

Minimum height 48 px on the web, 54 px in the app. One primary action per view — if a screen appears to need two, the second one is secondary.

Default
Hover
Pressed
Focus
Loading
Disabled

Inputs

Errors appear on blur, never while typing — and clear the moment the field becomes valid. Nothing shouts at someone mid-sentence.

Switch, on
Switch, off

Cards

Four kinds. If a card needs a fifth variant, the layout is probably wrong.

Surface, hairline border, level-1 shadow. The workhorse.

Lifts 6 px on hover with a mouse-tracked spotlight. Hover this one.

No shadow, muted fill. For notes nested inside another card.

Blurred backdrop and an inset highlight. Only over imagery or the camera feed.

Feedback

Toasts confirm, alerts interrupt, skeletons wait. Each has exactly one job.

Target matched

2.6 s, bottom centre, never blocks input.

Shown after 300 ms of waiting, never before.

Determinate. Fills over 1.1 s on first view.

Indeterminate

Pop-in with a drawn check. Confetti is reserved for phase completion and the certificate — twice per course, so it still means something.

Charts & data

One waveform renderer serves the marketing pages, the in-app workbench and the Unity build. Grid lines sit at 5% opacity so they read as structure, not content.

Sine · 2 HzDefault
Composite · 3 harmonicsPhase 4
ModulatedEnvelope
NoisyLow SNR

Every canvas registers with one shared requestAnimationFrame loop and pauses when it scrolls out of view or the tab is hidden. Twelve waveforms on a page cost roughly what one costs.

Icons

A single stroke set: 24 × 24 box, 1.8 px stroke, round caps and joins, no fills. Sixty-eight glyphs, all inheriting currentColor.

signal

qr

ar

target

gauge

graduation

medal

face

Motion

Motion explains where something came from. If an animation does not answer that question, it is decoration and it goes.

Duration and easing tokens
TokenValueUse for
--dur-instant90 msPress feedback
--dur-fast180 msHover, focus rings
--dur-base280 msMost state changes
--dur-slow460 msScreen transitions
--dur-slower720 msScroll reveals
--dur-cinema1100 msHero and headline reveals
--ease-outcubic-bezier(.22,1,.36,1)Default — things arriving
--ease-springcubic-bezier(.34,1.56,.64,1)Overshoot on press and pop-in
--ease-in-outcubic-bezier(.65,0,.35,1)Looping ambient motion

28 px rise, 6 px blur, 720 ms. Fires once at 12% visibility and then unobserves — nothing re-animates on scroll back.

70 ms between siblings. Beyond about eight items the last one arrives too late to feel connected, so groups stay small.

Push, pop, modal, fade and zoom — 420 ms each. The outgoing screen shifts 28% and dims rather than sliding fully away.

Reduced motion is a real path, not a stub

With prefers-reduced-motion or the in-app toggle, transitions collapse to 0.01 ms rather than being removed. Every transitionend callback still fires, so nothing gets stuck half-mounted — a failure mode that is easy to ship and hard to notice.

Component states

Six states, defined once, applied everywhere.

State definitions
StateVisual changeDuration
DefaultBase tokens
Hover−2 px translate, elevation +1, colour interpolates280 ms
Focus3 px ring at --accent-ring, 3 px offset180 ms
Pressed0.97 scale, elevation −190 ms
LoadingSpinner replaces the label, pointer events off
Disabled0.42 opacity, pointer events off, no focus

Focus rings use :focus-visible, so they appear for keyboard users and stay out of the way for mouse users. They are never removed — a focus ring you cannot see is an interface a keyboard user cannot navigate.

Light & dark

Dark mode follows the operating system until the visitor chooses otherwise, and that choice persists. It is a full palette, not an inverted one.

Signal frequency

3.5 Hz · period 0.29 s

Accent Success

Signal frequency

3.5 Hz · period 0.29 s

Accent Success

Three things that change beyond colour

Shadows deepen. The light-mode shadows disappear on a dark ground, so opacity roughly triples.

Borders become light. A hairline at 9% white replaces the solid grey; a dark border on dark is invisible.

Grain increases. Noise opacity goes from 3.5% to 5% because dark gradients band far more visibly.

Responsive behaviour

Four breakpoints, and type that scales fluidly between them rather than jumping.

Breakpoints
BreakpointWidthWhat changes
Large desktop1441 px +Container caps at 1440; whitespace grows, content does not
Desktop1081–1440 pxFull navigation, three-column prototype layout
Tablet769–1080 pxNav collapses to the drawer; prototype rails move below the device
Mobile≤ 768 pxSingle column, device scales to 0.72, custom cursor disabled

Fluid type uses clamp() keyed to viewport width, so a heading at 1200 px and one at 1400 px differ by a few pixels rather than a whole step. Nothing in the layout depends on a specific font size having been chosen.

Accessibility

WCAG 2.2 AA, checked rather than claimed.

  • Contrast — body text at 16.4 : 1, secondary at 5.7 : 1. The accent never carries body-size text.
  • Keyboard — every interactive element reachable in DOM order, with a visible :focus-visible ring. Skip link first in the tab order.
  • Targets — 44 × 44 px minimum for anything tappable, including the icon-only buttons in the navigation.
  • Semantics — landmarks on every page, one h1, headings that never skip a level, and ARIA only where HTML has no equivalent.
  • Live regions — toasts announce politely; form errors use role="alert" and are tied to their field with aria-describedby.
  • Colour is never the only signal — quiz results pair colour with an icon, and the clipping warning changes the readout text, not just its colour.
  • Reduced motion — respected from the OS and available as an in-app toggle, with every callback still firing.
  • AR parity — every workbench readout is mirrored as a live text region, so values are announced as they change.