Internal — not linked from primary nav
Verified, not decorated.
The design system for taly.dev. Every number on this page is
computed, not asserted — the contrast ratios below are the actual
WCAG relative-luminance output for these tokens, run against the
real hex values in styles.css, not eyeballed.
The idea
One idea, not ten effects — chosen before any of this was coded.
Every real claim on this site already carries a measured number: 5,400+ tests, a dated Lighthouse score, a specific commit. The system makes that ethos a formal rule instead of a habit — monospace figures for anything measured (stats, dates, contrast ratios, spacing values), set against a calm, restrained type hierarchy for narrative text, on a strict 8px grid. Motion only ever confirms something the user just did — a hover, a focus, a section entering view — never motion running on its own.
That is why this page's own contrast table isn't a static screenshot: it's the same numbers computed live from the same tokens the rest of the site uses.
Type scale
Named by role, not size. --text-h1 through --text-h4 are the exact curves already live on Home and About — formalized here, not changed. --text-display is the one new role, used only on this page.
Display
Heading 1
Heading 2
Heading 3
Heading 4 / label
Body large — the lede under every hero.
Body — running text, paragraphs, list items.
Body small — meta rows, tag lists, table cells.
Caption / eyebrow
2026-08-11 · 6.09:1 · 5,400+
Spacing scale
8px base grid. Every value already existed somewhere in the CSS before this system named it — --space-9 is the section padding you're reading right now.
Color, with contrast verified — not asserted
Computed with the WCAG 2.x relative-luminance formula against the real hex values in
:root, run for both themes. Full method and script in
docs/contrast-verification.md in this PR. WCAG 2.1 AA requires 4.5:1 for
normal text and 3:1 for large text (≥24px, or ≥19px bold) and for non-text UI
boundaries like input borders.
| Pair | Light | Dark | Needs |
|---|---|---|---|
| Body text on page background | 17.77:1 AAA | 16.59:1 AAA | 4.5:1 |
| Muted text on page background | 7.38:1 AAA | 9.15:1 AAA | 4.5:1 |
| Muted text on raised background (cards) | 6.77:1 AA | 8.38:1 AAA | 4.5:1 |
| Accent text on page background | 6.09:1 AA | 10.50:1 AAA | 4.5:1 |
| Button text on accent fill | 6.09:1 AA | 10.91:1 AAA | 4.5:1 |
| "Live" status text on its pill | 5.36:1 AA | 9.27:1 AAA | 4.5:1 |
| "Active build" status text on its pill | 7.95:1 AAA | 8.33:1 AAA | 4.5:1 |
| Shipped-version status text on its pill | 4.64:1 AA | 8.60:1 AAA | 4.5:1 |
| Error text on page background | 5.59:1 AA | 7.26:1 AAA (fixed — was 3.33:1) | 4.5:1 |
| Decorative border (dividers, card edges) | 1.37:1 n/a — not a UI boundary | 1.52:1 n/a | — |
| Interactive border (inputs, secondary button) | 3.33:1 pass (new token) | 3.70:1 pass (new token) | 3:1 |
Two real fixes came out of running the numbers, not the aesthetic pass: dark-mode form
error text was inheriting the light-mode red at 3.33:1 on the dark background — below
AA for text — and secondary-button / input borders were using the decorative
--border token at ~1.4:1, below the 3:1 WCAG 1.4.11 threshold for
interactive-element boundaries. Both now have their own verified tokens
(--error per theme, --border-interactive).
Motion
--ease-standard is a no-overshoot cubic-bezier — nothing on this site
bounces. Three durations only: --duration-fast (120ms, hover/focus),
--duration-base (200ms, cards), --duration-slow (420ms,
section reveal on scroll). Every animated rule on the site is silenced under
prefers-reduced-motion: reduce from one global rule — hover a card or
button below, or reload with reduced motion on, to compare.
[data-reveal] plus an IntersectionObserver in
script.js, gated so the page is fully visible by default if JavaScript
never runs.
States
Default, hover, focus, active, disabled, loading — shown together, not just described.
Tab to the field below to see the focus ring; it uses --focus-ring, distinct from the accent so it stays visible against accent-colored elements too.
Loading & empty states
No async content ships on the site yet — the contact list is a static link, not a form-and-response. These exist so the next thing that fetches or submits has somewhere to land instead of inventing a spinner under deadline.
Components already live on the site
Rendered here with the same classes Home, About, and the case study use — this page doesn't have its own component language.
-
Component sample
Active buildThis is the exact
View real ones →.project-cardmarkup from the Home page — hover it.
Used in the case study for a measured, slightly cautionary note — border-left accent, not a full color fill, so it doesn't compete with real status pills.