Governance
Universal quality gates that apply to ALL archetypes — Harbor, Sentinel, Current, Lattice, Meridian. Paste this before any archetype spec. Two pastes per prompt, non-negotiable.
Accessibility Gates — WCAG 2.1#
Contrast ratio requirements
- Body text: ≥ 4.5:1 (AA minimum), aim for ≥ 7:1 (AAA) on primary body copy
- Large text (18pt+ or 14pt bold): ≥ 3:1 (AA)
- Non-text UI elements (icons, focus rings, chart elements): ≥ 3:1
- Decorative-only elements: exempt from contrast requirements
- Failing combinations must be flagged "FAIL — do not use" with an alternative pairing suggested
Accessibility Verification Table
Every spec page MUST include this table computed from actual token values. Do not omit. Do not estimate.
| Pair name | FG | BG | Ratio | AA body (4.5:1) | AA large (3:1) | AAA (7:1) |
|---|---|---|---|---|---|---|
| Body text on page bg | #1A1B1E |
#FFFFFF |
18.1:1 | Pass | Pass | AAA |
| Muted text on page bg | #495057 |
#FFFFFF |
7.4:1 | Pass | Pass | AAA |
| Accent link on page bg | #0066CC |
#FFFFFF |
5.9:1 | AA | Pass | Fails AAA |
| Faint text on page bg | #868E96 |
#FFFFFF |
3.0:1 | FAIL — body use | Large only | Fails AAA |
| White on accent (#0066CC) | #FFFFFF |
#0066CC |
5.9:1 | AA | Pass | Fails AAA |
| Error text on error bg | #C92A2A |
#FFF5F5 |
6.1:1 | AA | Pass | Fails AAA |
Keyboard Navigation#
- Every interactive element reachable via Tab in DOM order
- Focus indicator visible: 2px outline, offset 2px, using
--color-focus-ring(defined per archetype) - Use
:focus-visible, NOT:focus— suppress ring for pointer users - Focus trapping required for modal dialogs, drawers, command palettes — Esc returns focus to trigger
- Skip-to-content link required at the top of every page
Standard key bindings — full map
| Key | Action | Context |
|---|---|---|
| Enter / Space | Activate element | Buttons, toggles, links, checkboxes, radio buttons |
| Esc | Close / dismiss | Dialogs, drawers, popovers, menus, command palettes |
| Arrow Keys | Navigate within widget | Tabs, radio groups, menus, listboxes, data grids, sliders |
| Home / End | Jump to first / last item | Lists, menus, sliders, tab rows |
| Tab | Move focus forward | All focusable elements in DOM order |
| Shift+Tab | Move focus backward | All focusable elements in reverse DOM order |
| Page Up / Page Down | Large-step increment | Sliders, scrollable regions, data tables |
| F6 | Move between panes | Multi-pane layouts (Lattice especially) |
| Ctrl/Cmd+K | Open search / command palette | Any product with global search (required in Meridian) |
Screen-Reader Requirements#
- All icon-only buttons must have
aria-label - All form fields must have programmatic labels:
label[for]oraria-labelledby— never placeholder-as-label - Live regions for toasts, async results, validation errors:
aria-live="polite"default;"assertive"only for errors blocking task completion - Decorative SVGs:
aria-hidden="true". Meaningful SVGs:role="img"+<title>
Test matrix
| Screen reader | Browser | Platform | Required for |
|---|---|---|---|
| VoiceOver | Safari | macOS / iOS | All components |
| NVDA | Firefox | Windows | All components (minimum) |
| TalkBack | Chrome | Android | Any component shipped to mobile (Current archetype) |
| JAWS | Chrome / Edge | Windows | Enterprise products (Sentinel archetype) |
Motion & Animation#
prefers-reduced-motion: reduce→ fall back to fade-only or no animation. Never just shorten duration — remove transform animation entirely.- No autoplay video or animated backgrounds without explicit user consent
- Vestibular safety: avoid parallax > 20% offset, scroll-jacking, spinning loaders > 2s without a text label
prefers-reduced-motion rule applies to ALL animations — CSS transitions, JavaScript animations, Lottie files, CSS transforms. Shortening duration is not an acceptable fallback; the transform must be removed entirely for vestibular safety.Token Hygiene#
- Zero raw hex in component CSS — every color, size, and shadow goes through
var(--token-name) - Components reference SEMANTIC tokens, never primitives
- Both light and dark mode tokens defined for every semantic token, even if a mode is unshipped
- Tokens declared in a single
:root+[data-theme="dark"]override block — never inside component scope
color: var(--color-link);
background: var(--color-surface);
border: 1px solid var(--color-border);
color: var(--color-blue-500);
background: #F8F9FA;
border: 1px solid rgba(0,0,0,0.1);
Component State Coverage#
Every interactive component must implement these 8 states — visually distinct, not just functionally present.
Form field additional states
Form fields additionally implement: empty, filled, required, read-only.
| State | Visual treatment | ARIA attribute |
|---|---|---|
| empty | Placeholder text at --color-text-faint | — |
| filled | Value text at --color-text | — |
| required | Label indicator (per archetype); "(required)" text preferred over asterisk-only | required |
| read-only | --color-surface-2 background; no focus ring | readonly / aria-readonly="true" |
| error | --color-error border; error message below | aria-invalid="true" + aria-describedby |
Error Message Formula#
Every error message follows What → Why → How.
Why: [the cause]
How: [recovery action — concrete, doable]
Banned error patterns
- Stack traces in user-facing copy
- Error codes without explanation (e.g. "Error 403" with no context)
- Generic "Oops!" / "Whoops!" / "Uh-oh!" messaging in product UI
Copy Hygiene#
- Sentence case for headings, buttons, labels — not Title Case, not ALL CAPS (except overline/eyebrow style)
- No emoji in UI copy — SVG icons only (emoji acceptable in user-generated content rendering)
- Contractions allowed (we'll, don't, can't) — softer voice
- Verb-first CTAs: "Save changes" not "Changes can be saved"
Banned copy patterns
Iconography Floor#
- SVG only. Inline SVG preferred for color inheritance
fill="currentColor"orstroke="currentColor"— never hardcoded hex on icon pathsaria-hidden="true"for decorative icons;<title>for meaningful icons- Consistent grid: 24px standard; archetype may override (Lattice uses 16px dense)
- Consistent stroke width within a family — never mix stroke widths on the same surface
- Never mix icon families on the same surface
Banned icon patterns
- Emoji used as icon in product UI
- Raster (.png) icons — not theme-aware, not scalable
- Animated GIF icons — motion not controllable by user
Versioning — SemVer#
All design system releases follow MAJOR.MINOR.PATCH semantic versioning.
Changelog Template#
Every release entry follows this canonical structure. A11y improvements are called out separately.
## [VERSION] — YYYY-MM-DD
### Added
- [new components, tokens, variants]
### Changed
- [modifications to existing surfaces]
### Deprecated
- [old → new migration path, removal version]
### Removed
- [breaking removals — MAJOR only]
### Fixed
- [bug fixes]
### A11y
- [accessibility improvements — call out separately]
Quality-Gate Checklist#
All 12 items are PR / release blocking. A release cannot ship until every gate is green.
- Contrast ratios verified (AA minimum, AAA for primary body)
- Keyboard navigable (Tab / Space / Enter / Esc / Arrows all wired)
- Screen-reader tested (VoiceOver + NVDA minimum)
-
prefers-reduced-motionhonored (transform animations removed, not just shortened) - Both light + dark mode tokens defined for every semantic token
- Storybook (or equivalent) story per variant + state
- No raw hex in component CSS (all colors via semantic tokens)
- No emoji in product UI (SVG icons only)
- Component implements all 8 states: default / hover / focus-visible / active / disabled / loading / error / success
- Error messages follow What/Why/How formula
- Visual-regression snapshot committed and passing
- Changelog entry filed under correct SemVer bump
Spec-Page Output Requirements (Prompt 49)#
Every generated spec page must contain these sections in this order, even if the archetype spec doesn't enumerate them explicitly. Prompt 49 derives missing details from archetype rules.
| Section | Name | Notes |
|---|---|---|
| §1.1 | Color system + Accessibility verification table | Must enumerate every text/background pairing with computed ratios |
| §1.2 | Typography | Scale, weights, line-heights — all from tokens |
| §1.3 | Spacing scale | All 8–12 steps with use-case notes |
| §1.4 | Elevation + shadows | Use case per token |
| §1.5 | Shape & borders | Radii with use guidance per component category |
| §1.6 | Motion | Durations, easings, reduced-motion fallback |
| §1.7 | Iconography | Family, grid, stroke, visual sample grid |
| §2 | Voice & content | Do/Don't pairs (minimum 6 pairs) |
| §3 | Component library | Foundation → composite → domain progression |
| §4 | Patterns | Form layout, empty state + archetype-specific patterns |
| §5 | Accessibility | WCAG conformance target, keyboard map, SR notes |
| §6 | Versioning + Governance | Quality-gate checklist + changelog template |
| Footer | Version · date · mode · WCAG target · archetype name | Required on every generated page |
Performance Budgets New#
Performance is a design system concern. Slow systems erode every other quality choice. These are the universal floor targets — every archetype must meet them at a minimum.
Universal targets (every archetype, every page)
on 4G mobile
on 4G mobile
universal
on 4G mobile
Per-archetype tightening (overrides to universal floor)
| Archetype | Override | Rationale |
|---|---|---|
| Current | Initial JS < 70KB | Mobile networks; battery constraints; launch speed is UX for native-feel apps |
| Lattice | Initial JS < 50KB; data-grid virtualization mandatory | Dense data views must stay fast at scale; virtualization prevents DOM bloat |
| Meridian | Web font < 60KB; first paint shows body text within 1s | Reading products: body text is the LCP element — it must appear immediately |
| Harbor | Hero image < 150KB; font subset to display set only | Marketing first impressions; large unoptimized heroes destroy perceived quality |
| Sentinel | Initial JS < 100KB (workhorse default) | Enterprise desktop apps have more headroom but must stay auditable |
Internationalization (i18n) New#
Every component must work across writing directions, character ranges, and locale-sensitive data formats. i18n failures are a11y failures.
Writing direction
| Direction | Languages | Requirement |
|---|---|---|
| LTR (default) | English, French, German, Spanish, most European | Default layout; baseline requirement |
| RTL | Arabic, Hebrew, Persian, Urdu | Every component must declare logical CSS properties so RTL flips correctly without per-language overrides |
margin-inline-start instead of margin-left, padding-block-end instead of padding-bottom, inset-inline-end instead of right. This is the mechanism that enables RTL without separate stylesheets.Character ranges
- Latin (basic + extended): default font stack covers this
- CJK (Chinese, Japanese, Korean): taller line-heights needed (1.6+ minimum); fonts may need to swap to region-appropriate (Noto Sans CJK). Never assume Latin character widths.
- Cyrillic, Greek, Devanagari: support per product market reach; verify font coverage before shipping
Locale-sensitive tokens — use the Intl API
// Date/time — never hardcode "MM/DD/YYYY"
new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(date);
// Numbers — handles locale separators, currency, units
new Intl.NumberFormat(locale, { style: 'currency', currency: 'USD' }).format(1234.56);
// → en-US: $1,234.56 | de-DE: 1.234,56 $ | ja-JP: $1,234.56
// Sort order — use Intl.Collator for string comparison
new Intl.Collator(locale, { sensitivity: 'base' }).compare(a, b);
// Pluralization — use ICU MessageFormat, not string concatenation
// BAD: "You have " + count + " items"
// GOOD: new Intl.PluralRules(locale).select(count)
// → MessageFormat("{count, plural, one {# item} other {# items}}")
Banned i18n antipatterns
- Hardcoded English strings in components — use i18n keys
- Concatenated strings ("You have " + count + " items") — use ICU plurals
- Fixed-width buttons that truncate German text (German averages 30%+ longer than English)
- Direction-aware icons (chevrons, arrows, play buttons) without RTL flip
- Assuming Latin characters in regex / validation patterns
- ASCII-only password rules (blocks non-Latin users from using their native characters)
Form Validation Strategy New#
Validation behavior is a tonal choice — get it wrong and the form feels nagging or surprises the user at submit time.
When to validate
| Trigger | Policy | Use cases |
|---|---|---|
| On blur | Default | All general input fields — user has completed their thought before feedback arrives |
| On submit | Required — always runs | Final validation pass on all fields; catches anything missed by blur validation |
| On change (real-time) | Allowed for specific cases only | Password strength meters, username availability (debounced API), password confirm match |
| On every keystroke | Banned for general fields | Feels nagging; user is mid-thought; breaks flow for slower typists and screen reader users |
What to validate
- Required field empty
- Format mismatch (email, URL, phone, date)
- Range / length violations (min/max characters, min/max values)
- Server-side rules (uniqueness, conflict, capacity limits)
How to display validation errors — the canonical pattern
<!-- What: field border turns error color -->
<!-- Why: aria-invalid tells screen readers the value is wrong -->
<!-- How: error message below with icon + text = redundant encoding -->
<label for="email">Email</label>
<input
type="email"
id="email"
aria-invalid="true"
aria-describedby="email-error"
style="border-color: var(--color-error);"
>
<div id="email-error" role="alert" aria-live="polite">
<!-- Icon (color + icon = colorblind-safe redundant encoding) -->
<svg aria-hidden="true">...error icon...</svg>
<!-- What / Why / How message -->
Email format isn't recognized. Try name@example.com.
</div>
<!-- Submit stays ENABLED — disabling hides the form's validation
state from the user (WCAG SC 3.3.4 violation) -->
<button type="submit">Create account</button>
Display rules
- Error message replaces helper text below the input (same slot)
- Field gets
--color-errorborder + 3px focus ring when focused - Error message follows the What/Why/How formula (§7)
- Error icon to the left of message — color + icon = redundant encoding for colorblind accessibility
- Submit button stays enabled — disabling submit hides validation state from the user
Per-archetype overrides
| Archetype | Override |
|---|---|
| Sentinel | Inline validation on blur; "(required)" parenthetical text, not asterisk-only |
| Current | Keyboard type matches input semantic: inputMode="numeric" / "email" / "tel" |
| Meridian | Forms are rare; when present, generous spacing (--space-4 between fields), clear labels above |
| Lattice | Number-field formatting uses mono + tabular figures; Tab key advances within table-grid forms |
| Harbor | Labels always above (never floating); error tone consistent with editorial voice — no exclamation marks |
Loading-State Strategy New#
Choose the right loading affordance — wrong choice feels broken. Operation duration drives the decision, not developer preference.
Decision tree: operation duration → preferred loading state
Per-archetype tightening
| Archetype | Override |
|---|---|
| Lattice | Prefer NO loading state for sub-100ms ops; never hide chrome behind a loader (keep keyboard shortcuts + nav functional at all times) |
| Current | Pull-to-refresh with haptic on threshold; skeleton required for any list waiting > 500ms |
| Meridian | Skeletons banned (motion ban applies); use opacity-static placeholders OR no loader at all (most docs render statically) |
| Harbor | Skeletons subtle (--color-surface-2 base, opacity pulse); never shimmer animation (too feature-heavy for editorial) |
| Sentinel | Text label required next to spinner if wait > 1s — ambiguity is not acceptable in productivity tools |
Banned loading antipatterns (universal)
- Spinner with no text after 1s — ambiguous: is it stuck or working?
- Full-screen blocker for partial-page loads
- Skeleton shimmer in Meridian / editorial contexts (violates motion policy)
- Hide nav / sidebar / chrome behind a loading state — keyboard navigation must remain functional
- Disabling Submit button "to prevent double-click" — use debounce + button loading state instead
Hybrid Products New#
Most real products are hybrids. One archetype is rarely sufficient for an entire product surface. These are the five most common patterns.
Do not force one tokens.css to cover both archetypes. Each archetype owns its own separate: semantic layer (Layer 2 token names), typography scale, density scale, component priority order, and banned patterns.
- Brand primary hex (the anchor color)
- Brand voice principles (broad guidelines — not specific voice rules, which vary per archetype)
- Universal governance gates (this file)
Archetype Overrides — Aggregated Summary New#
Each archetype inherits this governance spec in full and adds its own overrides. Below is the canonical aggregated summary. Updated whenever any archetype's overrides change.
- Warm oklch shadows — any cold rgba shadow fails CI
- Single accent enforced — any secondary accent ramp fails lint
-
Typography is fluid clamp() — any fixed-px
--text-*fails lint - Serif italic display reserved for editorial emphasis — no italic body class
- Floating label class banned — fails component lint
- Indigo/purple banned in any token — fails token lint
- Avatar component defaults to initials variant — stock-photo variant fails review
- One primary CTA per section enforced at template level
- Hero variants limited to template 01 (Hero Editorial) — no carousel hero
- Two-layer token architecture — component referencing Layer 1 directly fails lint
- Accent-CTA rule — white text on accent-500 fails CI contrast check
-
Tabular numerals required on all numeric columns (
font-feature-settings: "tnum" 1) -
Typography is fixed px modular — any
clamp()on--text-*fails lint -
Sans-serif only — any
--font-serifdeclaration fails lint - Cold rgba shadows enforced — any oklch shadow fails lint
- Dark mode tokens defined for every Layer 2 semantic token
- WCAG verification table required in spec page (Prompt 49 must enumerate every pairing)
- Color-only encoding banned — every indicator combines color + icon + text label
- "(required)" parenthetical enforced — asterisk-only fails form lint
- 44px minimum touch target — any hit area < 44×44px fails lint
- Body font-size ≥ 16px on inputs — prevents iOS zoom on focus
- Native font stack required — custom fonts are opt-in via brand override only
- Safe-area insets honored on every fixed-position element
- Hover-only interactions banned — every :hover state requires :focus or pressed equivalent
- Hamburger nav banned as primary navigation
- Tab bar capped at 5 items
- Haptic feedback paired with visible state change — haptic alone fails a11y review
- Modal dialogs require bottom-sheet alternative consideration
-
keyboardTypematches input semantic (inputMode="numeric","email","tel") - Both light + dark mode required — no "dark mode v2" deferrals
- Two-color-system architecture — chrome and data palettes cannot share tokens
-
Monospace + tabular figures required on any numeric column (
font-feature-settings: "tnum" 1) - Every interactive element has a documented keyboard shortcut — icon-buttons without shortcut metadata fail lint
- Animation on data updates banned (cell flash, row pulse — motion-fatigue)
- Shadows on default surfaces banned — panes, cells, rows have no shadow
- Confirmation dialogs banned for reversible actions — use undo snackbar pattern
- Pagination banned for data tables — must virtualize
- Color-only encoding banned — every status combines color + icon + text
- Body type ≥ 11px in chrome (10px only for axis labels, micro-metadata)
- Pane resizability required — any non-resizable splitter fails workspace lint
- Reading width hard-capped at 72ch on body content
-
Body type ≥ 17px — any smaller
--text-bodyfails lint -
Line-height ≥ 1.7 on body — tighter
--leading-bodyfails lint -
Animation on body content banned — no transitions on
.article-bodyor descendants - Sepia mode + dark mode tokens defined (sepia is Meridian-specific variant)
-
Print stylesheet required — any product without
@media printrules fails build - All headings (h2+) auto-id'd with anchor link — heading without id fails lint
-
External links flagged with icon —
a[href^=http]without external-link indicator warns -
Code blocks have copy button — any
pre>codewithout copy affordance fails lint - Search shortcut Cmd/Ctrl+K wired — any docs product without it fails review
- Marketing hype words blocked in body ("powerful", "seamless", "revolutionary" etc. fail copy lint)