What's inside

§ 01.1

The system in three layers — hard tokens at the base, components built strictly from them, and the applied patterns that compose both. Each layer cites the one below it, never the reverse.

01 · Foundations08

Hard tokens

The tokens everything else is built from. Color, type, spacing, motion, and the rules that hold them together.

02 · Components39

Build blocks

The primitives I reach for when designing a product surface. Buttons, forms, cards, and the patterns that emerge from them.

03 · Applied07

In practice

How the system performs in practice: the presentation and case study templates, and the guidelines, from the principles to voice and accessibility.

The mark

§ 01.2

The symbol: three isometric cubes in motion forming a triangle, one cube per product. It ships as a fixed asset, and the shell renders it inside the brand tile.

Amaca logo
The symbol

Three isometric cubes forming a triangle, in motion.

Each cube is a product. The triangle is a system — components snapping together, breaking apart, reassembling. The motion is intrinsic, not applied: the mark is an animated SVG, one vector file describing every rotation and every transition. The logo follows the same logic as the system itself — written, not decorated.

New in 5.1MCP · Remote · Read-only

Connect Amaca to your AI, by URL.

One address, nothing to install. Your AI reads the components with their rules, the tokens and the spec straight from amaca.design, always at the published version. The Claude Code plugin brings it with the skill.

  1. Open your AI app’s connectors. In Claude: Settings → Connectors → Add custom connector.
  2. Fill in the three fields, Name, URL and Sign-in, and add.
  3. Ask in plain words: “Use Amaca to build a pricing card.”
NameAmaca
URLhttps://www.amaca.design/mcp
Sign-inNone
For agents and config files
{ "mcpServers": { "amaca": { "type": "http", "url": "https://www.amaca.design/mcp" } } }
claude mcp add --transport http amaca https://www.amaca.design/mcp

Pick your tool

§ 03.0

Start with the three on top, or jump to your stack. One download per tool — a small bundle with the file, the tokens/spec it needs, and a usage README inside.

Start here
Claude

Claude — skill + CLAUDE.md

The rules as a Claude skill (tokens bundled) + a CLAUDE.md for Claude Code.

amaca-frontend.skill
Figma

Figma — /amaca-figma skill

In-canvas custom skill: builds with Amaca Variables, audits what landed on canvas, repairs in a verify loop. Fetch-first — it never invents values.

amaca-figma.md
Google · Stitch

Google design.md

The spec in the Google Labs design.md standard — paste into Stitch's project box.

DESIGN.md
AI integrations
Plugin

Install it, don't unzip it

One package, two manifests, one skill — Agent Plugins 1.0 for Cursor, Codex, Copilot, VS Code and Kiro, plus the Claude Code manifest. /plugin marketplace add angelomacaione/amaca-design

amaca-plugin.zip
Repo root

AGENTS.md

One repo-root file read natively by 28+ agents (Codex, Cursor, Copilot, Windsurf, Aider…).

AGENTS.md
Chat

ChatGPT · Gemini

Self-contained paste-in with the token values inlined — no companion files.

AI-INSTRUCTIONS.md
IDE

Cursor · Copilot

Scoped rules — amaca-core.mdc always on, amaca-html.mdc and amaca-react.mdc on the files they govern — plus .github/copilot-instructions.md and the agent skill Cursor reads from .agents/skills/.

amaca-ide.zip
Builders

v0 · Lovable · Base44

Same self-contained paste-in — these builders have no filesystem.

AI-INSTRUCTIONS.md
Tokens & formats
Tailwind

Tailwind v4 @theme

Every token as a @theme block — generates bg-primary-500, p-4, rounded-lg.

theme.css
React / CSS

Raw CSS variables

tokens.css (vars + reset) + components.css — var(--token) everywhere, no build step.

tokens.css
DTCG

DTCG pipeline

Tokens as W3C DTCG JSON — stable 2025.10 format — for Style Dictionary, Tokens Studio, any DTCG-aware build.

tokens.dtcg.json

How to install

§ 03.1

Per-runtime. Pick the row that matches your tool — the first three mirror the hero cards above.

  1. 01 · COWORK / CLAUDE CODE
    ANY ANTHROPIC RUNTIME

    Install the plugin, or drop the .skill bundle in.

    In Claude Code: /plugin marketplace add angelomacaione/amaca-design, then /plugin install amaca-design@amaca; it installs the skill from amaca-plugin.zip with a SHA-256 pin. In the Claude app: Settings → Capabilities → Skills → upload amaca-frontend.skill. Invoke with amaca-frontend or just describe an Amaca task — the skill auto-triggers on context.

  2. 02 · FIGMA
    — AGENT SKILL

    Install it like the Claude skill, invoke /amaca-figma.

    In any Figma file: prompt box → attachment icon → Skills → Add skill → upload amaca-figma.md. Then /amaca-figma: it fetches the live spec first, generates with Amaca Variables, and self-audits in a verify loop — or audits a selection on canvas evidence. Code-side sync (tokens pushed as Variables over the Figma MCP) stays available through the amaca-frontend skill.

  3. 03 · GOOGLE STITCH
    — DESIGN.MD

    Paste the spec into the project box.

    Download DESIGN.md — the spec ships in the Google Labs design.md standard. On Stitch's Start with your project screen, paste it into the existing DESIGN.md box (or upload the file). Pick the most capable model for brand fidelity.

    Frontier-tier model required. On lighter models the spec is read as passive context, not instructions — see § 03.3 capability tiers.

  4. 04 · CURSOR / VS CODE / WINDSURF · CONTINUE / AIDER
    — EDITORS & ASSISTANTS

    Drop the spec at your tool's rules path.

    Where the editor installs Agent Plugins (Cursor, GitHub Copilot, VS Code, OpenAI Codex, Kiro), install amaca-plugin.zip. Otherwise unzip amaca-ide.zip at the repo root: core + per-target rules, attached only to the files they govern:

    • Cursor.cursor/rules/amaca-core.mdc · amaca-html.mdc · amaca-react.mdc
    • Cursor · Codex (skill).agents/skills/amaca-frontend/
    • VS Code (Copilot).github/copilot-instructions.md + instructions/amaca-*.instructions.md
    • Windsurf.windsurf/rules/amaca.md
    • AiderCONVENTIONS.md
    • Continuerules in config

    The runtime reads it as instructions and grounds on the spec.

    Model-gated — fidelity tracks the model behind the editor. See § 03.3.

  5. 05 · GEMINI ADVANCED
    — GEMS

    Spec as knowledge, instructions as system prompt.

    Create a new Gem. Paste AI-INSTRUCTIONS.md into Instructions: it is self-contained, with the token values inlined. Upload DESIGN.md as the Gem's knowledge file for the full component specs. Save. That Gem now speaks Amaca for any frontend task.

    Frontier-tier model required. On lighter models the spec is read as passive context, not instructions — see § 03.3 capability tiers.

  6. 06 · CHATGPT
    — CUSTOM GPT

    Same pattern as Gems.

    Create a Custom GPT, or a Project. AI-INSTRUCTIONS.md as instructions, DESIGN.md as knowledge file. Same result.

    Frontier-tier model required. On lighter models the spec is read as passive context, not instructions — see § 03.3 capability tiers.

  7. 07 · ONE-OFF
    ANY LLM CHAT

    Attach and prefix.

    Attach AI-INSTRUCTIONS.md to the chat: about seventy lines, every token value inlined, where DESIGN.md runs to thousands. Prefix your prompt with "Use this as the canonical reference for tokens, components, and voice — generate strictly compliant frontend code." Lighter than the skill but works when nothing else does.

    Frontier-tier model required. On lighter models the spec is read as passive context, not instructions — see § 03.3 capability tiers.

  8. 08 · REPLIT / LOVABLE / V0 / BOLT / BASE44
    — APP BUILDERS

    Feed the tokens, not the skill.

    Prompt-to-app builders impose their own stack (usually React + Tailwind + shadcn) and treat a pasted spec as soft context. Give them AI-INSTRUCTIONS.md as project knowledge and, where they take files, theme.css (the Tailwind @theme) as the project foundation — those travel; the enforcement workflow does not. Replit is closest (its Agent reads project files — drop a rules file). Base44 is the tightest (locked stack). Compliance is model- and platform-gated: a frontier model + tokens-as-foundation gets most of the way — verify with the § 03.3 scan.

  9. 09 · NPM · CDN
    — THE WHOLE SYSTEM

    Install the package, versioned.

    npm install amaca-design, then import amaca-design/tokens.css before amaca-design/components.css. With no install, link the same two files from jsDelivr: cdn.jsdelivr.net/npm/amaca-design@5/tokens.css. The package also carries theme.css (the Tailwind v4 @theme), tokens.dtcg.json and DESIGN.md. Satoshi loads from www.amaca.design; its files are not in the package.

  10. 10 · SHADCN
    — ONE COMPONENT

    Add one component, with its tokens.

    npx shadcn add https://www.amaca.design/r/button.json writes the component's CSS and an example into your project, with the tokens and the components it uses. No React and no components.json needed. Every item carries its contract in meta.amaca: usage, parts and their tokens, rules, variants. The index is /r/registry.json.

What's inside the spec

§ 03.2

The DESIGN.md the skill bundles, and the raw download serves, is organized in the Google design.md grammar:

BlockContents
## OverviewHow to use the file, quick reference, token roles in the YAML front-matter.
## ColorsEvery named hex, scale step, gradient, and the 85/10/5 proportion law.
## TypographyDisplay through caption — size, weight, leading, tracking. Family fallbacks.
## Layout · Shapes · ElevationThe 4-px scale, radius steps, shadow specs.
## MotionDurations, easings, the single curve everything rides on.
## ComponentsButtons, forms, cards, badges, navigation, accordion, chat, loader, diagrams, checkbox, select — props, states, do/don't.
## Do's and Don'tsThe five principles, the anti-patterns, and the rules for AI agents. Voice included.
## AccessibilityColor pairs with ratios, focus rules, reduced-motion contracts.
## ChangelogEvery release, top to bottom. Same as § 31.1.

Model capability tier

§ 03.3 · what to expect

The skill encodes a 7-step workflow and 14 verification checks. How rigorously a model runs them depends on its reasoning, how faithfully it follows instructions and how it reads files, more than on its name: model families change every few months, so this page names tiers, not models. Judge the output, not the label.

LARGEST MODELS · RECOMMENDED

Production-grade enforcement.

A provider’s most capable tier. Expect explicit tool calls to read DESIGN.md, syntactic token registry checks, gap-surfacing protocol invoked correctly, voice register maintained.

SMALL, FAST MODELS · BEST-EFFORT

Usable output, partial compliance.

A provider’s small, fast tier. Token references are mostly canonical, principles are cited, structure is sound. Expect two weaknesses:

  • 01Class name proliferation. New classes where canonical components would compose.
  • 02Micro-inconsistencies. Radius drift, occasional minor token substitution, edge cases on multi-component composition.
CHECK THE OUTPUT After any generation, ask the model to run check_compliance from the MCP server on what it wrote. It flags hex literals (#XXXXXX instead of var(--color)), raw px (16px instead of var(--s-4)), raw durations and easings (ratified continuous loops are the only exception), --ease-spring on effect properties (color, background, opacity, shadow — § 06.3 RIGID), classes outside the § 3 registry and a missing @media (prefers-reduced-motion: reduce) override, each with the token that fixes it. Without the MCP server, grep the code for the same things. Either way it catches most drift, whatever the model.
A11Y FLOOR · § 06 Brand-fill text contrast: text on primary-500 (#F051D5) defaults to obsidian-950 — 6.5:1, AA. One ratified exception (v2.6.0, § 6.3): .btn-primary, .badge-solid and .card-feature-tab carry obsidian-050 at ≈2.8:1 — kept on perceptual grounds, scoped to those three selectors. Every other magenta fill — chips, tinted badges, non-button CTAs — stays dark-text.
RUNTIME ARCHITECTURE CAVEAT

Beyond model size, the runtime's file-grounding architecture matters. Agentic tools that read files through explicit tool calls (Claude Code, Cursor, Continue) read what you give them — what you upload is what gets read. Consumer chat tools that abstract file uploads as context embeddings may produce performative compliance: the model announces reading DESIGN.md but the actual grounding is shallow.

Wherever the app supports it, the most reliable route is the MCP server, https://www.amaca.design/mcp: the model reads the components, the tokens and the spec by tool call, at the published version, and checks its own output. Without MCP, use the platform’s custom-assistant wrapper — Gemini Gems, ChatGPT custom GPTs — where instructions and knowledge files load for the whole session, not per message.

React — native

§ 03.4

Tailwind v4 @theme is native: tokens become first-class utilities (bg-primary-500, rounded-md) straight from tokens.css — no Google spec, no build hack. React is first-class in amaca-frontend v1.1.

UTILITIES
bg-primary-500 · text-obsidian-950 · rounded-md

Every token resolves to a utility class against the same var() chain the CSS surface uses. No CSS modules, no translation layer.

IMPORT
@import "amaca-design/theme.css";

One line wires the full token scale into a Tailwind v4 project. theme.css is a @theme projection of tokens.css — var() aliases, no duplicated literals.

OPTIONAL PROJECTION
design.md / W3C DTCG
tokens.json

The Google design.md / W3C DTCG export stays available as an optional, generated projection for lint, diff, and cross-tool interop — not the thing that unlocks React. Same one-way model: derived from tokens.css, not authored separately.

Versioning & license

§ 03.5

Two SemVer streams, one rule each.

VERSIONING — SPEC

Pinned to the system release — the version in the site header is the version of the spec this page ships. Strict SemVer from v1.2.0 onward — minor bumps for additions, major for breaking renames.

VERSIONING — SKILL

Decoupled from spec version. The skill's own SemVer covers workflow changes — new posture rules, verification checks, deploy targets. v1.2.2 re-bundles on the current spec — multi-target across HTML, React, and Figma — and since v1.2.0 it verifies its own output before handing it over. The in-canvas Figma skill (amaca-figma) versions separately.

CURRENT · v1.3.1 → NEXT · v1.3
LICENSE — MIT

Use it in client work, internal tools, your own systems. Attribution appreciated, not required.

Brand · signal

§ 01.1

The magenta range. Use for primary actions, links, data emphasis, and gradients. Never for body text.

Primary 100#FFE3F8Surface · tint
Primary 200#FFB5ECSoft · hover bg
Primary 300#FF85DFLink hover
Primary 400#F868D8Accent · eyebrow
Primary 500#F051D5Brand · actions
Primary 600#C93BB0Pressed
Primary 700#9A2D87Deep · shade
Grad · Signal500 → 300Brand · gradient

Obsidian · neutrals

§ 01.2

The working surface. Twelve-step scale from page bedrock to bone. Body text lives at 100; metadata at 300; edges at 700.

Obsidian 950#07090BPage
Obsidian 900#0B0E12Surface
Obsidian 850#10141AElevated
Obsidian 800#161B22Card
Obsidian 700#1E242DBorder
Obsidian 600#2A313BDivider
Obsidian 500#3A4451Edge
Obsidian 400#5B6573Disabled
Obsidian 300#8A94A3Secondary
Obsidian 200#B8C0CBTertiary
Obsidian 100#E3E7ECBody text
Obsidian 050#F4F6F8Bone

Secondary · electric cyan

§ 01.3

High-voltage cyan. Use for focus accents, highlighted states, and secondary emphasis — never as a second primary.

Secondary 100#CCFFFCTint
Secondary 300#33FFF5Hover
Secondary 500 — Electric#00FFF2Accent
Secondary 700#009992Deep

Tertiary · petrol

§ 01.4

Deep petrol teal. Used for default badges, quiet tags, and neutral chips that shouldn’t read as brand but still need color.

Tertiary 100#CEEAF1Tint
Tertiary 300#4FA9BFText on dark
Tertiary 500 — Default#0C6078Badge stroke
Tertiary 600#084C61Deep

Semantic · signals

§ 01.5

Reserved for feedback only — success, warning, danger, info. Never use as decoration.

Success#3AFFC7Confirm · saved
Warning#F6C65BCaution · pending
Danger#FF5B5BError · destructive
Info#5CC8FFInfo · neutral state

Tints · alpha

§ 01.6

A colour with an alpha is a token colour times a tint: color-mix(in srgb, var(--hue) calc(var(--tint-*) * 100%), transparent). Seven steps, shown on --primary-500; an opacity is 0, 1 or a token. A value between two steps snaps to the nearest, the smaller at equal distance.

Faint--tint-faint · 0.04A wash · open row
Fill--tint-fill · 0.08Tinted label fill
Fill deep--tint-fill-deep · 0.12Deep hue fill · card light
Glow--tint-glow · 0.2Glow · hairline
Edge--tint-edge · 0.3Tinted label edge
Edge deep--tint-edge-deep · 0.4Deep hue edge
Strong--tint-strong · 0.6Veil · pulse ring
Disabled--opacity-disabled · 0.4Every disabled control

Usage matrix

§ 01.7

One row per color token: value, sanctioned use, and the misuse to refuse. The 85/10/5 law decides the proportions; this matrix decides the placements.

TokenValueUseDon't
--primary-500#F051D5Primary CTA, focus ring, emphasisBody text, large backgrounds
--grad-signalmagenta 500→300Headlines, accents, data highlightsMore than one per screen
--obsidian-950#07090BPage background onlyCard surfaces
--obsidian-100#E3E7ECBody text on darkPure white — never use #FFF
--danger#FF5B5BDestructive actions, form errorsWarnings or neutral states
Use it when
  • Long-form text left-aligned, at most 80 characters per line.
Use something else when
  • Body text centred or wider than 80 characters → left-align it and cap the measure.

Display scale

§ 02.1

The display end of the scale, display to h6, one Swatch type tile each (§ 3.36): the sample in its class at full size, cropped at the end of the line, then size / leading / tracking and weight. Negative tracking is part of the voice at this size, not a tweak.

Reshape.

Display--t-display112 / 1.05 / -4%Marketing hero only · Black 900

Design that compounds.

Heading 1--t-h176 / 1.05 / -4%Page title (one per page) · Bold 700

A case for quiet precision.

Heading 2--t-h252 / 1.05 / -4%Section title · Bold 700

Systems outlive the work

Heading 3--t-h338 / 1.2 / -2%Subsection title · Bold 700

Evidence over opinion

Heading 4--t-h430 / 1.2 / -2%Card title · Bold 700

The obvious answer, made inevitable

Heading 5--t-h524 / 1.2 / -2%Small heading · Semibold 600

A label that leads a group

Heading 6--t-h620 / 1.2 / 0Eyebrow heading · Semibold 600

Text scale

§ 02.2

The text scale, lede to micro: the steps running copy actually uses, one Swatch type tile each. Each tile declares its size and leading — reference the token, never the number.

Lede paragraph. Sits under page titles, once per page, to set the register before body text takes over.

Lead--t-lead18 / 1.65 / 0Page lede / intro paragraph · Regular 400

Body copy. Reads comfortably at 62–72 characters per line. Default for paragraphs, long-form notes and case studies.

Body--t-body15 / 1.65 / 0Default body text · Regular 400

Small text. Captions, table rows, secondary UI copy.

Small--t-small13 / 1.45 / 0Captions, table cells · Regular 400

Caption · image credit, footnote, timestamp.

Caption--t-caption12 / 1.45 / 0Smallest readable text · Regular 400

Micro · mono labels, eyebrows, kbd keys.

Micro--t-micro10 / 1.45 / 0Mono labels, eyebrows, kbd keys · Regular 400

Weights available

§ 02.3

The Satoshi weights the system ships. One typeface carries every register — the mono voice is Satoshi too, tracked and upper-cased, never a second family.

Light 300
Satoshi-Light
Regular 400
Satoshi-Regular
Medium 500
Satoshi-Medium
Bold 700
Satoshi-Bold
Black 900
Satoshi-Black
Italic
Satoshi-BoldItalic

Pairing rules

§ 02.4

Numbered rules for combining the scale: tracking bounds, weight pairings, the register mixes that hold. Pair inside them.

Rule 01Headlines always negative track. Never tighter than −4%, never looser than −2%.
Rule 02Mono is metadata only. Timestamps, token names, labels, status — never prose.
Rule 03Italic is for emphasis in running text. Never for UI labels or headlines.

Scale

§ 03.1

The spacing scale: a 4px grid from --s-0 to --s-32, with --s-0-5 (2px) under the first step, one Swatch space tile each (§ 3.36): the bar at 1:1 on the 4px grid. Every gap, inset, and rhythm in the system is one of these steps.

Space 0--s-00px
Space 0.5--s-0-52px
Space 1--s-14px
Space 2--s-28px
Space 3--s-312px
Space 4--s-416px
Space 5--s-520px
Space 6--s-624px
Space 8--s-832px
Space 10--s-1040px
Space 12--s-1248px
Space 16--s-1664px
Space 20--s-2080px
Space 24--s-2496px
Space 32--s-32128px

Where to reach

§ 03.2

Where to reach on the scale for the common cases — icon gaps, button padding, card rhythm, section breath. The fastest route from a layout decision to its token.

ContextTokenNotes
Inline icon–label gap--s-28px. Always.
Button padding (inline)--s-624px on the default button; --s-4 small, --s-8 large. No block padding: the height is a min-height of --s-8 · --s-10 · --s-12.
Card interior padding--s-624px standard. --s-8 on hero cards.
Subsection vertical rhythm--s-2080px between subsections.
Page top padding--s-1248px below top bar.
Section → section--s-2496px breathing room on long pages.
Use it when
  • Rectangles inside the 8–12px working range; --r-xl for the hero of a case study, --r-2xl for a hero block.
  • --r-full as a shape, not a size: it makes a pill or a circle (button, chip, switch, radio, avatar, status dots, slider track and thumb, the segmented control's track and thumb, skeleton circle, the key-point dot of a case study, the floating navigation bar).
Use something else when
  • A small rectangle past 16px → it reads as a toy: stay in the working range, or make it a pill.

Radius scale

§ 04.1

The radius scale, --r-none to --r-full, one Swatch radius tile each (§ 3.36): the shape at the token’s radius, 1:1. Sub-4px hairlines are a ratified closed list (§ Shapes in the contract), not scale members.

None--r-none · 0
Extra-small--r-xs · 2px
Small--r-sm · 4px
Medium--r-md · 8pxWorking range
Large--r-lg · 12pxWorking range
X-large--r-xl · 16pxCase study hero
2X-large--r-2xl · 24pxHero block
Full--r-full · 999pxPill · circle
Use it when
  • Depth: an element that sits above another, on the elevation scale.
Use something else when
  • Emphasis → type weight or colour, never a shadow.

Elevation

§ 05.1

Level 0 to 4, the menu shadow, the two brand glows and the three focus rings, one Swatch shadow tile each (§ 3.36): a surface on a section ground, where cards and popovers sit. Elevation on this ground is tonal — an inset highlight over a soft drop — never a hard outline; the rings exist only for focus and are never used as depth.

Level 0--sh-0
Level 1--sh-1Subtle
Level 2--sh-2Card
Level 3--sh-3Popover
Level 4--sh-4Modal
Glow--sh-glowPrimary hover
Glow soft--sh-glow-soft
Menu--sh-menuSelect menu · picker
Ring halo--ring-haloFocus · pressables
Ring field--ring-fieldFocus · inputs
Ring field danger--ring-field-dangerFocus · invalid

Durations × easings

§ 06.1

The motion vocabulary at a glance: five easings and where each belongs, one Swatch motion tile each (§ 3.36). Time runs across, position up; the ball on the right rides the curve at the dot’s height. Decel, accel and ease in-out run on the same --d-base so the curves can be compared; spring rides --d-slow, the overshoot needs the room, and soft rides it too, as a page’s entrance does. Each tile plays three times and rests on the end. The pairing rules and the durations each job usually rides are in § 09.3.

Decel--ease-decel · 350 mscubic-bezier(.16,1,.3,1)Entrances
Accel--ease-accel · 350 mscubic-bezier(.7,0,.84,0)Exits
Ease in-out--ease-standard · 350 mscubic-bezier(.45,0,.55,1)State changes
Spring--ease-spring · 600 mscubic-bezier(.34,1.56,.64,1)Delight · reveal
Soft--ease-soft · 600 mscubic-bezier(.12,.23,.5,1)Reading entrances

Click to re-trigger animations.

Duration tokens

§ 06.2

Six duration tokens, each named for a job. Stat count-up is a RIGID rule: numeric stats and KPIs compose from zero to their target riding --d-draw · --ease-decel and land exactly on the final value; under reduced motion they jump straight to it.

TokenmsUse
--d-instant100Hover state, tap feedback
--d-quick200Button states, focus rings
--d-base350Default — most UI transitions
--d-slow600Page enters, panel slides
--d-scene900Hero reveals, orchestrated sequences
--d-draw1200Chart draw-on, count-up, progressive data reveals

Easing tokens & pairing

§ 06.3

The trigger decides the pairing: user-triggered states ride --d-quick · --ease-standard; system-triggered transitions — content entering, leaving, or moving without a direct pointer cause — ride --d-base · --ease-decel. Spatial vs Effects is a RIGID rule: every animated property has a type, and the type gates the easing. Spatial properties — transform, position, size, shape — may use any easing, including --ease-spring. Effect properties — color, background, opacity, shadow — use no-overshoot curves only; --ease-spring on an effect property is off-system, and greppable per line.

TokenCurveUse
--ease-decelcubic-bezier(0.16, 1, 0.3, 1)Default for system-triggered transitions — entrances, reveals. The system signature
--ease-accelcubic-bezier(0.7, 0, 0.84, 0)Exits, dismissals
--ease-standardcubic-bezier(0.45, 0, 0.55, 1)Ease in-out, symmetric: user-triggered state changes — hover, press, focus, selected
--ease-springcubic-bezier(0.34, 1.56, 0.64, 1)Pop-in, delight, overshoot — spatial properties only
--ease-softcubic-bezier(0.12, 0.23, 0.5, 1)Reading entrances: a page’s blocks as it opens, a card coming into view. A gentle out on --d-slow over --s-8

Motion per component

§ 06.4

Every component that moves documents its motion in one fixed schema — same four columns everywhere, no prose specs. The Motion column accepts token pairs only; the Property column declares the type (spatial / effect) so the rule above is checkable on the row itself; the Reduced motion column uses a closed vocabulary — instant · fade-only · none — recording only how the component lands when the global kill-switch (§ 24.3) fires. Seeded with the site-canonical patterns; every new component ships its rows here — and in DESIGN.md § Motion — in the same release. Continuous loops are a ratified class: they run while the component is mounted, on per-component literal clocks — the --d-* scale describes perceived transitions, not idle rhythm. Mechanical loops ride linear (or steps), never an easing curve; every loop is ratified by its own loop row above, and a loop literal without a row is drift — the raw-duration grep exception covers exactly this list, nothing else.

ComponentTriggerProperty (type)MotionReduced motion
Entrance reveal ([data-fade])enter viewportopacity (effect) · translateY 12px→0 (spatial)--d-slow · --ease-decelinstant
Page shellsection activatethe section's direct children, as three blocks: opacity (effect) · translateY --s-8→0 (spatial); .page-header after --d-quick, .usage one --d-quick later, the rest together one --d-quick after the block before it--d-slow · --ease-softnone — the page is at rest
Buttonpressbackground · border · colour (effect)--d-quick · --ease-standardinstant
Button (primary)pressscale 1 → .97 (spatial)--d-quick · --ease-springnone — no scale
Chart cardenter viewportopacity (effect) · translateY --s-8→0 (spatial), 80ms apart in reading order; a stat starts its count-up on the same frame--d-slow · --ease-softnone — shown at rest
Chart cardstat revealcount-up (effect · text)--d-draw · --ease-decelnone — jumps to the value
Plotplot revealbaseline clip width (spatial)--d-scene · --ease-decel · delay --d-basenone — drawn
Plotplot revealline stroke-dashoffset (spatial)--d-draw · --ease-decel · delay --d-base + --d-slownone — drawn
Plotplot revealarea opacity (effect)--d-slow · --ease-soft · delay --d-base + 2 × --d-slownone — drawn
Plotplot revealpoint radius (spatial)--d-base · --ease-spring · delay --d-base + 2 × --d-slow + --d-quick × inone — drawn
Plotplot revealbar width (spatial) · value label count-up (effect · text)--d-scene · --ease-decel · delay 2 × --d-base + --d-instant × inone — drawn
Plotplot revealcolumn height (spatial)--d-slow · --ease-decel · delay 2 × --d-base + --d-instant × inone — drawn
Plotplot revealdonut slice sweep (spatial) · legend % count-up (effect · text)--d-slow · --ease-decel · delay --d-slow + --d-instant × inone — drawn
Plotplot revealdonut total count-up (effect · text)--d-draw · --ease-decel · delay 2 × --d-basenone — jumps to the value
Timelineplot revealbar width (spatial)--d-scene · --ease-decel · delay 2 × --d-base + --d-instant × inone — drawn
Timelineplot reveallabels · today (effect · spatial)the entrance reveal: --d-slow · --ease-decelnone — shown
Maphover · focuscluster glow (effect)--d-quick · --ease-standardinstant
Maphover on grey landcountry fill + faint glow (effect)--d-quick · --ease-standardinstant
Mapopenthe rest of the land dims (effect)--d-quick · --ease-standardinstant
Pielegend hoverborder-color · background (effect)--d-quick · --ease-standardinstant
Accordionopenpanel grid-rows (spatial)duration by distance · --ease-decelinstant
Accordionclosepanel grid-rows (spatial)duration by distance · --ease-standardinstant
Accordionopenchevron rotate 90° (spatial)--d-base · --ease-decelinstant
Accordionclosechevron rotate 0° (spatial)--d-base · --ease-standardinstant
Accordionopenpanel body opacity (effect)--d-quick · --ease-decel · delay 80msinstant
Accordionhover / open statetrigger + chevron color (effect)--d-quick · --ease-decelinstant
Tabsselectindicator transform + width (spatial)--d-base · --ease-decelinstant
Tabspanel switchpanel opacity (effect) · translateY 6px→0 (spatial)--d-quick · --ease-standardinstant
Segmented controlselectthumb transform + width (spatial)--d-base · --ease-decelinstant
Segmented controlhover · selectsegment label color (effect)--d-quick · --ease-standardinstant
Segmented controlpanel switchpanel opacity (effect) · translateY 6px→0 (spatial)--d-quick · --ease-standardinstant
Cardhoverborder-color · box-shadow (effect)--d-quick · --ease-standardinstant
Cardhover · focus-withinmedia child scale 1→1.05 (spatial)--d-slow · --ease-decelnone — no zoom
Cardhover ends · focus leavesmedia child scale 1.05→1 (spatial)--d-slow · --ease-decelnone — no zoom
Cardenter viewportopacity (effect) · translateY --s-8→0 (spatial), 80ms apart in reading order--d-slow · --ease-softnone — shown at rest
Lightboxopenbackdrop + content opacity (effect) — no scale-in--d-quick · --ease-decelinstant
Lightboxclosebackdrop + content opacity (effect)--d-quick · --ease-accelinstant
Modalopenpanel opacity (effect)--d-quick · --ease-decelinstant
Modalopenpanel translateY --s-2→0 (spatial)--d-base · --ease-decelinstant
Modalopenscrim background (effect)--d-quick · --ease-decelinstant
Modalclosepanel opacity + translateY (effect · spatial) · scrim background (effect)--d-quick · --ease-accelinstant
Avatarhover · press (interactive)border-color (effect)--d-quick · --ease-standardinstant
Listhover (interactive row)background (effect)--d-quick · --ease-standardinstant
Paginationhover · focus (link)background · color (effect)--d-quick · --ease-standardinstant
Sliderhover · focus (thumb)box-shadow (effect)--d-quick · --ease-standardinstant
Chatmessage enter · container (.chat-bubble, .chat-status, .chat-avatar, .chat-meta)scale 0.92→1 (spatial)--d-base · --ease-springinstant
Chatmessage enter · containeropacity (effect)--d-base · --ease-standardinstant
Chatmessage enter · content (.chat-bubble-content)opacity (effect) · translateY 3px→0 (spatial)--d-quick · --ease-standard · delay 80msinstant
Chatroll-off (.is-out)opacity (effect) · translateY −8px (spatial)--d-quick · --ease-accelinstant — still hides
Chatroll-off (.is-out)max-height + margin collapse (spatial)--d-base · --ease-accelinstant — still hides
Chattyping dots (loop)translateY 0→−4px→0 (spatial) · opacity 0.3→1→0.3 (effect)--d-slow · --ease-standard · stagger 0/160/320msnone — the dots hold still at full opacity
Chatsend idle → readybackground + color (effect)--d-quick · --ease-standardinstant
Loadersuccesscheck scale-in (spatial)--d-base · --ease-springinstant
Loadersuccess.loader-anim opacity → 0 (effect) · scale → 0.7 (spatial)--d-base · --ease-springinstant
Loaderskeleton handofffull-page loader → .skeleton-stack swapafter a --d-scene waitinstant swap, shimmer disabled
Diagramsenter viewport · nodesopacity (effect)--d-base · --ease-decel · stagger ~70msinstant — straight to resolved
Diagramsenter viewport · edges + clustersopacity (effect)--d-base · --ease-decel · after nodes, +40ms eachinstant — straight to resolved
Stat / KPIenter viewporttext content count-up 0→target (effect)--d-draw · --ease-decelinstant — jump to final value
Loaderwait (loop)brand mark SVG (spatial)media clock (SVG SMIL, 4.5 s loop) · infinitehidden — fallback ring shown
Loaderwait (loop)fallback ring rotate (spatial)1400ms · linear · infiniteratified exception — keeps spinning
Loaderlabel cycle (loop)label opacity crossfade (effect)2400ms cycle · 200ms crossfadeinstant swap
Skeletonwait (loop)shimmer sweep (effect)1600ms · linear · infinitedisabled — static fill
Popoveropenopacity (effect) · translateY −4px→0 (spatial)--d-quick · --ease-decelinstant
Popovercloseopacity (effect) · translateY 0→−4px (spatial)--d-quick · --ease-accelinstant
Draweropenslide in from its edge (spatial) · scrim fade (effect)--d-base · --ease-decelinstant
Drawercloseslide out to its edge (spatial) · scrim fade (effect)--d-base · --ease-accelinstant
Stepperrevealdot scale 0→1 (spatial) · fade (effect)--d-slow · --ease-spring for scale, --ease-decel for opacity · 520ms + 80ms per stepinstant
Stepperrevealtext rise 8px (spatial) · fade (effect)--d-slow · --ease-decel · 520ms + 80ms per stepinstant
Stepperrevealconnector draw, done or dashed (spatial) · appear (effect)--d-slow · --ease-decel · 580ms + 80ms per step; appears on --d-quickinstant
Steppercurrent (loop)ring scale 1→1.33 (spatial) · fade (effect)2400ms · --ease-standard · infinitestill ring
Progressreveal · value changefill draw (spatial)--d-scene · --ease-decel · 700ms on revealinstant — at its value
Navigation · top baritem hover / presscolor (effect)--d-quick · --ease-standardinstant
Navigation · sidebaritem hover / presscolor (effect)--d-quick · --ease-standardinstant
Breadcrumbcrumb hover / presscolor (effect)--d-quick · --ease-standardinstant
Tablelink row hover / pressbackground (effect)--d-quick · --ease-standardinstant
Inputhoverborder-color (effect)--d-quick · --ease-standardinstant
A11y caret (demo)idle (loop)caret opacity blink (effect)1s · steps(1) · infinitestopped by the kill-switch
Date pickeropenpopover opacity (effect) · translateY −4px→0 (spatial)--d-quick · --ease-decelinstant
Date pickerclosepopover opacity (effect) · translateY 0→−4px (spatial)--d-quick · --ease-accelinstant
Date pickerday hoverbackground · color (effect)--d-quick · --ease-standardinstant
Date pickeryear modetitle chevron rotate 180° (spatial)--d-quick · --ease-standardinstant
Time inputopenpopover opacity (effect) · translateY −4px→0 (spatial)--d-quick · --ease-decelinstant
Time inputclosepopover opacity (effect) · translateY 0→−4px (spatial)--d-quick · --ease-accelinstant
Checkbox · Radiocheck / uncheckborder-color · background (effect)--d-quick · --ease-standardinstant
Switchtoggleknob left 2px↔16px (spatial)--d-base · --ease-standardinstant
Switchtoggletrack background · knob background (effect)--d-quick · --ease-standardinstant
Toastenteropacity (effect) · translateX 24px→0 (spatial)--d-base · --ease-decelinstant
Toastdismiss (.is-out)opacity (effect) · translateX 0→24px (spatial) — retraces the entry--d-quick · --ease-accelinstant — still hides
Toastdismiss (.is-collapsing)max-height · padding · border · margin collapse (spatial) — repositions the stack--d-base · --ease-decelinstant — still hides
Alertenter viewportopacity (effect) · translateY 12px→0 (spatial)--d-slow · --ease-decel · stagger 90msinstant
Tooltipopen on hover / focusopacity (effect) · translateY 2px→0 (spatial)--d-quick · --ease-standardinstant
Tooltipclose (another opens · Escape · scroll)opacity (effect) · translateY 0→2px (spatial)--d-quick · --ease-standardinstant
Chiphover (pressable)border-color (effect)--d-quick · --ease-standardinstant
Chipselectborder-color · color (effect)--d-quick · --ease-standardinstant
Chipdeselectborder-color · color (effect)--d-quick · --ease-standardinstant
Chipdismissopacity (effect) · scale 1→0.92 (spatial)--d-quick · --ease-accelinstant — still removes
Case studyentersection eyebrow, title, text: a 24px rise (spatial) · fade (effect)--d-slow · --ease-decel, 0 / 80 / 160msinstant
Case studyenteroutcome number count-up (effect)--d-draw · --ease-decelthe final number, no count
Case studyhover · focusfigure border and shadow (effect)--d-quick · --ease-standardinstant
Case studyhover · focusfigure hint (effect)--d-quick · --ease-standardinstant
Case studyhover · focusmore-case picture scale 1→1.05 (spatial)--d-slow · --ease-decelnone
Use it when
  • Naming an action in an icon-only button, or reinforcing a label beside it: add, close, search, replay.
  • Telling a state apart from its neighbours without colour alone: the warning triangle in an alert, the check in a done step.
Use something else when
  • The picture is the content: a photo, a logo, a map → an image, never an icon.
  • The meaning cannot be guessed from the drawing and there is room → a text label, with or without the icon.
  • A status standing alone → Badge, § 3.14.

Album

§ 07.1

Every icon as a Swatch icon tile (§ 3.36): the drawing 4× on its 16 grid with the 14 live area dashed, then at 16 and 24. The drawing is copied into the markup as it is; a drawing that is not here is not in the system. Component spec: § 3.1 in the contract.

Actions

plus16 · 24Add, new, expand a group
x16 · 24Close, dismiss, clear
check16 · 24Done, selected, success
edit16 · 24Edit
download16 · 24Download
trash16 · 24Delete
play16 · 24Play
pause16 · 24Pause
rotate-ccw16 · 24Replay, try again
paperclip16 · 24Attach
maximize16 · 24Open larger

Arrows and chevrons

arrow-up16 · 24Up, send
arrow-down16 · 24Down
arrow-left16 · 24Left, back
arrow-right16 · 24Forward, go to
arrow-up-right16 · 24External link
arrow-up-left16 · 24Up and left
arrow-down-right16 · 24Down and right
arrow-down-left16 · 24Down and left
chevron-up16 · 24Collapse
chevron-down16 · 24Open a list, expand
chevron-left16 · 24Previous
chevron-right16 · 24Next, disclosure
sort-ascending16 · 24Sorted low to high; a column that sorts and does not drive
sort-descending16 · 24Sorted high to low

Navigation and panels

menu16 · 24Open the navigation
kebab16 · 24More actions
panel-left16 · 24Sidebar
panel-right16 · 24Side panel
search16 · 24Search
settings16 · 24Settings
configurations16 · 24Configure, adjust, filter
component16 · 24A component
code16 · 24Code view

Status and content

info16 · 24Neutral information
triangle-alert16 · 24Needs attention: warning, error
clock16 · 24Time
calendar16 · 24Date
eye16 · 24Show
eye-off16 · 24Hide
image16 · 24Image, placeholder

People and chat

user16 · 24A person, no picture
message16 · 24A message, a new chat
messages16 · 24A conversation
message-plus16 · 24New chat

Shapes

triangle16 · 24Shape
triangle-fill16 · 24Shape
diamond16 · 24Shape
diamond-fill16 · 24Shape
square16 · 24Shape
square-fill16 · 24Shape
pentagon16 · 24Shape
pentagon-fill16 · 24Shape
hexagon16 · 24Shape
hexagon-fill16 · 24Shape

Various

coin16 · 24A credit
crown16 · 24Admin
wrench16 · 24Tester, tools

Sizes

§ 07.2

Four steps from the size tokens. The stroke is 1.5 on the 16 grid, so it grows with the size: 1.3px at 14, 1.5 at 16, 1.9 at 20, 2.3 at 24.

xs · 14
sm · 16
md · 20
lg · 24

Colour

§ 07.3

The icon takes the colour of its container. The only role is muted, --obsidian-300, for decorative icons inside a field.

inherits text
.icon-muted
set by container
set by container

In context

§ 07.4

Beside a label, alone in a button that carries the name, and trailing. In a button the icon is 14.

Do and don't

§ 07.5

One rule: copy it from the album.

✓Do

Copy the drawing from the album, hide it from the screen reader when a label is beside it, and let the container give it the colour.

✕Don't

Redraw an icon, set a stroke width or a colour on it, or fill an outline icon.

The grid

§ 08.1

The page grid — twelve columns compressing to eight, then four. Columns are page anatomy; the component grid presets in § 15.3 handle everything inside a section.

≥ 1024px·24px gutter
1
2
3
4
5
6
7
8
9
10
11
12
720 – 1023px·20px gutter
1
2
3
4
5
6
7
8
< 720px·16px gutter
1
2
3
4

Breakpoints

§ 08.2

The four breakpoints with their columns and gutters. Layout changes at these widths and nowhere else.

NameMin widthColumnsGutter
Mobile0416px
Tablet720820px
Desktop10241224px
Wide14401224px, capped at 1180 content

Common layouts

§ 08.3

The compositions the system ships — sidebar and content, hero three-up, editorial split — and how each reflows down the breakpoints.

≥ 1024px·3-column composition
Sidebar · contentDefault
Hero · three-upLanding
Editorial · splitCase study
720 – 1023px·sidebar collapses, hero stays 3-up
Sidebar · contentStacked
Hero · three-upLanding
Editorial · splitCase study
< 720px·everything stacks
Sidebar · contentDrawer
Hero · three-upStacked
Editorial · splitStacked

Browser chrome

§ 08.4

Four surfaces the browser paints are styled: the scrollbars, the selection, the caret. They are not components, and everything else the browser paints stays native. Scroll the box both ways and select its text.

A long table scrolls both ways: the corner where the bars meet stays transparent.

The thumb lightens to --obsidian-600 under the pointer.

Select this text: the selection is magenta with near-white type.

Firefox draws its own dark bar instead; nothing here asks it otherwise.

Everything else the browser paints stays native.

A long table scrolls both ways: the corner where the bars meet stays transparent.

The thumb lightens to --obsidian-600 under the pointer.

Select this text: the selection is magenta with near-white type.

Firefox draws its own dark bar instead; nothing here asks it otherwise.

Everything else the browser paints stays native.

A long table scrolls both ways: the corner where the bars meet stays transparent.

The thumb lightens to --obsidian-600 under the pointer.

Select this text: the selection is magenta with near-white type.

Firefox draws its own dark bar instead; nothing here asks it otherwise.

Everything else the browser paints stays native.

SurfaceTreatment
::-webkit-scrollbar10px on both axes
::-webkit-scrollbar-tracktransparent: the page shows through
::-webkit-scrollbar-thumb--obsidian-700, --r-sm, a --bw-2 border in --obsidian-950 that sets it in
::-webkit-scrollbar-thumb:hover--obsidian-600
::-webkit-scrollbar-cornertransparent, where the two bars meet
::selection--primary-500 fill, --obsidian-050 text
caret-color--primary-500 on input and textarea

Do and don’t

§ 08.5

One shell, one active section; the grid lays out and does nothing else.

✓Do

Keep one .app per document and one active section, switched by the controller.

✗Don’t

Toggle .active by hand in the markup, or drop min-width: 0 from .main.

✓Do

Lay panels out on the presets, with the --s-4 gap.

✗Don’t

Give the grid a surface or a padding, or change its gap: stop and ask.

Use it when
  • The affirmative action of the screen, as the one .btn-primary.
  • Any action on the current page: submit, save, open, apply.
Use something else when
  • Going to another page or resource → Link.
  • An icon-only button whose glyph is not universal and has no tooltip → a labelled button.

Anatomy and code

§ 09.1

The primary button, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

1Fill--primary-500pressed --primary-600
2Label--obsidian-050--t-body 500 · --tr-snug
3Edge--bw-1 --primary-500pressed --primary-600
4Boxmin-height --s-10padding 0 --s-6 · gap --s-2
5Shape--r-full
6Shadow--sh-2hover --sh-glow + --sh-3 · pressed --sh-1
7Focus--ring-halooutline --focus-width --obsidian-100 · offset --focus-offset
8Motion--d-quick · --ease-standardtransform --d-quick · --ease-spring
9Disabledopacity var(--opacity-disabled)cursor not-allowed

Rules

§ 09.2

What the contract holds the button to, word for word from its section of DESIGN.md.

  • One .btn-primary per screen. Everything else recedes.
  • Touch. On a coarse pointer every .btn grows to at least 44px tall and every .btn-icon 44px wide — the § 6 floor. .btn-lg already clears it; dense desktop keeps 32.
  • Focus: dual-ring (white outline + --ring-halo). Never remove outline without re-implementing focus visibility.
  • States: § 3.0.1. default · hover · active · focus-visible · disabled · loading, values in the state index. loading is .is-loading on the button: the label turns transparent but keeps the width, and the loader sits at the centre — its composition is § 3.19's. disabled is the native attribute — <button disabled> — which the CSS reads as :disabled; no class produces it.
  • Pressed (active). A coloured fill darkens toward --obsidian-950: the primary lands on --primary-600 (the palette's pressed step, 4.09 : 1 for its label), the danger fill on 85% --danger. A neutral fill goes one step past its hover; the outline takes the link's pressed magenta; the floating button keeps its translucent fill and darkens its edge. Only the primary moves: it gives to scale(.97) on --d-quick · --ease-spring (L8), and under reduced motion it does not move. Never a lighter fill on press (§ 6.3).
  • Primary label is near-white (--obsidian-050) on --primary-500 — a ratified exception to the § 6 floor (§ 6.3, ≈ 2.8 : 1), kept on perceptual grounds. Scoped to .btn-primary; rest and hover are bounded to --primary-500 (no lighten); pressed darkens to --primary-600. The only other light label on magenta is .badge-solid (§ 6.3); never elsewhere.
  • Label weight is Medium 500 — ratified 2026-06-12 after a 400 / 500 / 700 comparison. Under APCA stroke weight is a contrast input and 700 scores strongest, but at 15px the bold label shifts the button's voice; 500 keeps the register, and the legibility budget is carried by the § 6.3 pairing. Don't bold the primary label for emphasis; don't drop below 500.

Variants

§ 09.3

The button variants, one job each. One primary per screen; everything else supports it.

Floating

§ 09.4

A button that stays on screen while the content under it scrolls: Back in a long page, such as a case study. The fill is translucent and blurred, so the text reads through it, and it takes no height of its own. Scroll inside the frame.

Three months of observation came before a single screen. Every interaction was logged for six weeks, then read against eye-tracking data from the research partners.

The map that came back was clear: most distracted-driving events clustered around three flows. Those three became the targets, and everything else waited until they had shipped and been measured.

Every screen had to read in one glance at arm's length. Type grew a step, labels lost a word, and every state that mattered got its own colour and its own word.

The center stack, the cluster and the companion app read the same tokens, so a change shipped once and landed everywhere.

Sizes

§ 09.5

Three sizes — 32, 40, 48 — and the default is the middle one. Size signals density, not importance: the variant does that.

With icons

§ 09.6

Icon-and-label compositions, leading and trailing, and the icon-only square for toolbars where the meaning is universal. The icon-only button is named by its aria-label; the icon itself is hidden from assistive tech.

Do and don’t

§ 09.7

One primary per screen; the variant says how much it matters.

✓Do

Keep one .btn-primary per screen and let the rest recede; give an icon-only button an aria-label that names the action.

✗Don’t

Bold the label, lighten the primary on hover, or remove the outline without a focus ring in its place.

✓Do

Pick the size for density (32 · 40 · 48) and let the variant carry importance.

✗Don’t

Use a button to go to another page: that is a Link.

Use it when
  • Many of many; the value applies on submit.
  • A single opt-in, such as accepting terms.
Use something else when
  • The change applies immediately → Switch.
  • Only one option can be chosen → Radio.

Anatomy and code

§ 11.1

The checkbox, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

1Box--surface-field --bw-1 --border-input --r-xs --s-4checked --primary-500
2Mark--bw-2 --obsidian-950
3Label--obsidian-100 --t-smalldisabled --obsidian-400
4Motion--d-quick --ease-standard
5Focus--focus-width --obsidian-100 --focus-offset --ring-halo

Rules

§ 11.2

What the contract holds the checkbox to, word for word from its section of DESIGN.md.

  • Checkbox and radio are told apart by shape — square with a tick, circle with a dot. Never a tick in a circle, never a dot in a square.
  • The switch is the only one of the three that commits on change. If the change needs a Save button, it is a checkbox.
  • The whole <label> is the hit target; on mobile it must reach 44×44 (§ 6 floor #7) — pad the label, don't grow the box.
  • The focus ring lives on the input, not the label, so the halo traces the control.
  • A radio group has exactly one checked member at all times. A group with none is a defect, not a state.
  • State matrix: § 3.0.1. Motion: § 06.4.

States

§ 11.3

A square with a tick: many of many. The value applies when the form is submitted. Shape carries the meaning before colour does. What separates the three choice controls is when they commit.

Checkbox — many of many

Do and don’t

§ 11.4

Shape first: a square with a tick.

✓Do

Make the whole label the target, at least 44×44 on a phone, and keep the focus ring on the input.

✗Don’t

Put a tick in a circle, or use a checkbox for a change that applies at once: that is a Switch.

Use it when
  • One of many, always in a named group with a default.
Use something else when
  • Several options can be chosen → Checkbox.
  • Four or more options and little room → Select.

Anatomy and code

§ 12.1

The radio, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

1Circle--surface-field --bw-1 --border-input --r-full --s-4checked --primary-500
2Dot--primary-500 --r-full
3Label--obsidian-100 --t-smalldisabled --obsidian-400
4Motion--d-quick --ease-standard
5Focus--focus-width --obsidian-100 --focus-offset --ring-halo

Rules

§ 12.2

What the contract holds the radio to, word for word from its section of DESIGN.md.

  • Checkbox and radio are told apart by shape — square with a tick, circle with a dot. Never a tick in a circle, never a dot in a square.
  • The switch is the only one of the three that commits on change. If the change needs a Save button, it is a checkbox.
  • The whole <label> is the hit target; on mobile it must reach 44×44 (§ 6 floor #7) — pad the label, don't grow the box.
  • The focus ring lives on the input, not the label, so the halo traces the control.
  • A radio group has exactly one checked member at all times. A group with none is a defect, not a state.
  • State matrix: § 3.0.1. Motion: § 06.4.

States

§ 12.3

A circle with a dot: one of many, always in a named group with a default. Like the checkbox, it applies on submit.

Radio — one of many

Do and don’t

§ 12.4

One of many, and always one checked.

✓Do

Keep exactly one option checked in a named group, with a default.

✗Don’t

Put a dot in a square, or leave a group with nothing checked.

Use it when
  • One of two, applied immediately: a setting, not a form value.
  • Turning a preference on or off with instant save.
Use something else when
  • The change needs a Save button → Checkbox.
  • More than two options → Radio or Select.

Anatomy and code

§ 13.1

The switch, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

1Track--obsidian-700 --r-full --s-8 --s-4 --s-0-5on --primary-500
2Knob--obsidian-200 --r-fullon --obsidian-950
3Label--obsidian-100 --t-small
4Motion--d-base --ease-standard --d-quick
5Focus--focus-width --obsidian-100 --focus-offset --ring-halo

Rules

§ 13.2

What the contract holds the switch to, word for word from its section of DESIGN.md.

  • Checkbox and radio are told apart by shape — square with a tick, circle with a dot. Never a tick in a circle, never a dot in a square.
  • The switch is the only one of the three that commits on change. If the change needs a Save button, it is a checkbox.
  • The whole <label> is the hit target; on mobile it must reach 44×44 (§ 6 floor #7) — pad the label, don't grow the box.
  • The focus ring lives on the input, not the label, so the halo traces the control.
  • A radio group has exactly one checked member at all times. A group with none is a defect, not a state.
  • State matrix: § 3.0.1. Motion: § 06.4.

States

§ 13.3

A track with a knob: the only choice control that applies immediately, on change.

Switch — applies immediately
Switch or checkbox?
If the change needs a Save button, it is a checkbox. The switch is the only one of the three that commits on change.

Do / Don't

§ 13.4

The fill carries the state, never a stroke.

✓Do

Let the fill carry the switch’s state: the quiet track when off, magenta when on.

✕Don't

Put a stroke on the switch track. The fill carries the state; a border makes it read as a checkbox that lost its tick.

Use it when
  • Choosing a number, or a range of two, where the rough position matters as much as the exact value: volume, price, opacity.
Use something else when
  • Fewer than about ten steps → Radio or Select.
  • More than a few hundred steps, or an exact value to type → a number in a Text input.
  • A date or a time → Date picker, Time input.

Anatomy and code

§ 14.1

The slider, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

40
0100
1Label--obsidian-300 --font-mono --t-micro --tr-mono
2Value--obsidian-100 --t-small
3Track--primary-500 --obsidian-600 --r-full --s-1
4Thumb--obsidian-100 --bw-2 --obsidian-950 --r-full --s-5 --sh-1hover --ring-field
5Scale--obsidian-300 --t-caption
6Focus--ring-halo

Rules

§ 14.2

What the contract holds the slider to, word for word from its section of DESIGN.md.

  • Count the steps, not the values. The unit is the step: (max − min) ÷ step is the number of positions. Fewer than about ten → Radio or Select. More than a few hundred, or a value the reader has to hit exactly → a number typed in a Text input. 0 to 100 in steps of 5 is 20 positions.
  • Native underneath. Arrow keys move one step, Page Up and Page Down a larger one, Home and End go to the ends. The value goes with the form under the input's name.
  • A range starts on a selection, not on the whole scale. Each thumb is its own input with its own aria-label ("Minimum price", "Maximum price"), and the pair is a role="group" named by the visible label. The lower thumb cannot pass the upper one; they can meet.
  • The value is always shown. In .slider-value, beside the label; the minimum and the maximum sit in .slider-scale. A slider with no visible value is a guess.
  • The fill is the script's. --p on the input (single), --lo and --hi on the pair (range), as percentages of the scale. Without the script the control still works and the fill is absent.
  • No error state. The value is always inside the scale. A slider that cannot be changed is disabled.
  • Targets (WCAG 2.5.8). The thumb is 20px and the whole 32px row takes the pointer, so a click on the track moves the nearest thumb.
  • Motion. The ring on the thumb changes on --d-quick · --ease-standard, none under reduced motion (§ 06.4).

Single

§ 14.3

A label, the value beside it, the scale under the track. The fill runs from the start to the handle. Arrow keys move one step, Page Up and Page Down ten, Home and End go to the ends. The last one is disabled.

Volume
0100
Disabled
0100

Range

§ 14.4

Two handles on one track, each its own range input with its own name. The lower cannot pass the upper; they can meet. A range starts on a selection inside the scale, not on the whole of it.

Price range
Price range
€0€200
Handles together
Price range
€0€200

Do and don’t

§ 14.5

Count the steps, not the values.

✓Do

Set a step that gives between ten and a few hundred positions, show the value beside the label, and name each handle of a range.

✕Don’t

Use a slider for three choices or for a value the reader has to hit exactly, or start a range on the whole scale.

Use it when
  • Free text on a single line: a name, a search, a URL.
  • Always with a persistent label; the placeholder is never the label.
Use something else when
  • The answer runs over several lines → the textarea.
  • The answer comes from a known list → Select or Radio.
  • A time or a date → Time input, Date picker.

Anatomy and code

§ 15.1

The text input, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

1Label--obsidian-300 --font-mono --t-micro --tr-mono
2Field--surface-field --bw-1 --border-input --r-md --s-10hover --obsidian-300 · focus --primary-500 · error --danger
3Value--obsidian-100 --font-sans --t-small
4Placeholder--obsidian-300
5Help--obsidian-300 --t-caption
6Focus ring--ring-fielderror --ring-field-danger
7Motion--d-quick --ease-standard

Rules

§ 15.2

What the contract holds the text input to, word for word from its section of DESIGN.md.

  • Labels are mono-uppercase, --t-micro, --obsidian-300 — the neutral mono register (§ Typography); they took it first, for legibility over dark fields, and v5.2.0 gave it to every neutral label. Always persistent — placeholder is not a label.
  • Height --s-10, declared. min-height: var(--s-10), block padding --s-2, type --t-small: a single-line field is 40px, the height of the default button beside it (§ 3.6). The height is the token, not a sum of paddings.
  • Helper text is .help; the error variant is .help.error.
  • Error is marked with aria-invalid="true" — the border and the helper text both follow from it. Colour alone never says "error" (§ 6 floor #1).
  • States: § 3.0.1. default · hover · focus-visible · error · disabled · readonly, values in the state index (hover is the field's outline, one step lighter, and the .input-action's; error is not a search state). No state is described in prose here.
  • Icon left. A .icon as a direct child of .input-wrap: decorative (aria-hidden), --icon-sm, --obsidian-300, no click. The field pads --s-8 on the left to clear it.
  • Action right. .input-action: a <button type="button"> last in the .input-wrap, 32px square, --r-sm, --icon-sm, --obsidian-300 and --obsidian-100 on hover. The field pads --s-10 on the right. It carries an aria-label: it has no text. The date picker and the time picker use this same part (§ 3.13).
  • Clear. An .input-action with data-input-clear and the x of § 3.1. It is visible only while the field has a value (:placeholder-shown, so the field needs a placeholder) and hidden when the field is disabled or readonly. Clicking it empties the field, fires input and gives the focus back to the field.
  • Search. The field sits in <form role="search"> with type="search", so Enter submits natively. The browser's own clear button is removed: there is one. A search has no error state: no results is a result and is shown where the results go, never on the field.

Fields

§ 15.3

Text, search and textarea, each with a persistent label and the helper text below. States live in the contract’s state matrix; error is aria-invalid, never color alone.

Shown in case study index cards.
Slug must be lowercase with hyphens.

Search

§ 15.4

The icon on the left says what the field is for; the clear button on the right appears once there is something to clear. Enter submits the form. Type in the first one to try it. A search has no error state: no results is a result, shown where the results go.

Do and don’t

§ 15.5

A label that stays; an error said in words.

✓Do

Give every field a persistent label, and say an error in the helper text as well as in the border.

✗Don’t

Use the placeholder as the label, or let colour alone say "error".

✓Do

Show the clear button only while the field has a value, and show "no results" where the results go.

✗Don’t

Give a search field an error state, or keep the browser's own clear button beside ours.

Use it when
  • Native for a fixed, short list; the custom listbox only when native cannot carry the requirement.
  • Four or more options, or little room on screen.
  • A number set by feel rather than typed, such as a price → the Slider variant.
  • Actions on an item that do not fit on screen → the Menu variant.
Use something else when
  • Two or three options → Radio, all of them visible.
  • A value the person must hit exactly → Text input.
  • A place to go → Navigation or Link.

Anatomy and code

§ 16.1

The select, its menu laid open apart from the field, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

Status
1Label--obsidian-300 --font-mono --t-micro --tr-mono
2Trigger--surface-field --bw-1 --border-input --r-md --s-10hover --obsidian-300 · open --primary-500 · error --danger
3Value--obsidian-100
4Menu--obsidian-850 --bw-1 --obsidian-700 --r-md --sh-menu --s-1
5Option--obsidian-100 --font-sans --t-small --s-2hover --obsidian-800 · selected --primary-400
6Check--icon-xs

Rules

§ 16.2

What the contract holds the select to, word for word from its section of DESIGN.md.

  • aria-expanded on .select-trigger toggles "true"/"false"; open state activates the magenta border + glow (same focus treatment as .input:focus).
  • Keyboard focus on the trigger is the pressable ring, not the field glow: the .input .textarea .select | focus-visible row of the state index does not apply to .select-trigger, which has its own row — a trigger is pressed, not typed into. The magenta border stays, from .select:focus.
  • .select-menu placement is viewport-aware: open below the trigger by default; flip above only when the menu's height does not fit below and above offers more room; max-height = min(240px, room on the chosen side), with internal scroll beyond it — measure the menu's natural height with getBoundingClientRect() rounded up (not scrollHeight, which rounds and skips the border), and scroll only when the menu is really shortened; clamp horizontally to the viewport. The menu never covers adjacent content (e.g. a chat composer the control sits above). Use position:fixed measured from the trigger's rect (.select-menu.is-fixed) when the control may sit near a viewport edge or above important content. A transformed ancestor becomes the containing block of a fixed element, so measure where (0,0) lands and subtract it; position:absolute; top:calc(100% + 4px); left:0; right:0 is acceptable only when there is always room below. Background --obsidian-850 (or --obsidian-900 for an elevated overlay); hidden attribute used to dismiss (do not toggle display directly).
  • The panel never transitions. Its box is measured and written by script at open; under reduced motion the global 0.01ms rule gives every property a transition, and a script that reads the start of one places the panel off screen. .select-menu carries transition:none inside the reduced-motion override (v5.0.0).
  • .select-option[aria-selected="true"] is rendered in --primary-400, with a check after the label (.select-check, the album's check, 14px) in the colour of the text. The check's room is reserved on every row, so labels never move when the choice changes. Hover/focus background is --obsidian-800.
  • Only one menu open at a time — opening one closes any other open .select-menu.
  • Enter/Space/↓ on trigger opens.
  • ↓/↑ navigate options; Home/End jump to the first and last; no wrap from the last to the first. First open lands on the currently selected option (aria-selected="true"), or the first if none.
  • A printable character jumps to the next option that starts with the letters typed (typeahead, 500ms).
  • Enter/Space on focused option commits + closes; trigger receives focus back.
  • Esc closes without committing; trigger receives focus back.
  • Tab from open menu closes and proceeds.
  • Click outside the wrapper closes the menu.
  • A fixed-position (viewport-aware) menu also closes on resize, and on a page scroll that moves the trigger. Its anchored coordinates would otherwise drift from the trigger. Scrolling inside the menu never closes it.
  • Window blur does not auto-close (browser-quirk; leave the menu, let the next click handle it).

Variants

§ 16.3

The native <select> for short lists, the custom listbox when options need a look of their own. The chosen option keeps the magenta text and gains a check after its label; a disabled option stays visible and the arrow keys skip it. Both open below the trigger and flip only when there is no room.

Slider

§ 16.4

A number set without typing it. The field is the same; the panel holds one Slider, with one handle or two, and the field shows the value while it moves. Esc puts back the value the panel opened with.

Max price
Price

Menu

§ 16.5

Actions behind a labelled button or a kebab. The same panel and row, with an icon in front and a shortcut after; the destructive action comes last, after a divider. A kebab at the end of a row opens inward.

  • Spring field notesEdited 2 days ago
  • Harbour survey, draft 3Edited last week

Groups

§ 16.6

A long list splits into named groups: a heading in the grammar of the label, a full-bleed divider between groups. Typing a letter jumps to the next option that starts with it.

Do and don’t

§ 16.7

A choice stays, an action runs, a number slides. The panel finds its own room.

✓Do

Open the panel below the field and above it only when the bottom edge would clip it; open a kebab at the end of a row inward, aligned to its end. Put a destructive action last, after a divider, and one Slider at most in a panel.

✕Don’t

Pin the panel below a field at the bottom of the screen, or let it cover what the person still needs, such as a composer. Use a Menu to hold a value, mix a Slider with options, or put two Sliders in one panel.

Use it when
  • A time of day, HH:MM in 24 hours, formatted as it is typed.
Use something else when
  • A date and a time together → pair it with the Date picker.

Anatomy and code

§ 17.1

The time input, its wheel laid open apart from the field, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

1Label--obsidian-300 --font-mono --t-micro --tr-mono
2Field--surface-field --bw-1 --border-input --r-md --s-10focus --primary-500 · error --danger
3Clock button--obsidian-300 --r-sm --s-8hover --obsidian-100
4Popover--obsidian-900 --bw-1 --obsidian-700 --r-lg --sh-3 --s-3
5Wheel item--obsidian-300 --font-sans --t-smallcurrent --obsidian-100
6Band--primary-500 --tint-fill --bw-1 --tint-glow --r-sm
7Separator--obsidian-300 --font-sans --t-lead
8Motion--d-quick --ease-accelopen --ease-decel

Rules

§ 17.2

What the contract holds the time input to, word for word from its section of DESIGN.md.

  • maxlength="5" and inputmode="numeric" are required.
  • Optional by default; a context can make it required.
  • No type="time": the native picker does not follow the system.
  • Focus and error follow § 3.10.

Time input

§ 17.3

HH:MM, 24h. Auto-formats as you type — the colon is inserted after the second digit, and each position is bounded so an invalid time is impossible to enter. No type="time": the native picker doesn't respect the system.

Type or pick. Arrow Down also opens the picker.
Required when the field is mandatory and left empty on submit.
DIGIT-BY-DIGIT RULES
H1max 2 H2max 3 if H1 = 2, else 0–9 M1max 5 M20–9
CONTRACT
  • maxlength="5" + inputmode="numeric" are required.
  • The : is inserted automatically — users only type digits.
  • Optional by default; can be marked required per context.
  • Focus & error states inherit from § 11.1.

Do and don’t

§ 17.4

Typed, and formatted as it is typed.

✓Do

Let the field format HH:MM as it is typed, with inputmode="numeric" and maxlength="5".

✗Don’t

Use type="time": the native picker does not follow the system.

Use it when
  • A single date or a range, typed or picked from the calendar.
  • Dates near today, where the relation to other days matters.
Use something else when
  • A date the reader already knows, such as a birth date → typed Text inputs, no calendar.

Anatomy and code

§ 18.1

The date picker, its calendar laid open apart from the field, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

1Label--obsidian-300 --font-mono --t-micro --tr-mono
2Field--surface-field --bw-1 --border-input --r-md --s-10focus --primary-500 · error --danger
3Calendar button--obsidian-300 --r-sm --s-8hover --obsidian-100
4Popover--obsidian-900 --bw-1 --obsidian-700 --r-lg --sh-3 --s-3
5Month--obsidian-100 --font-sans --t-small --tr-snug
6Arrows--obsidian-300 --r-smhover --obsidian-100 --obsidian-800
7Weekdays--obsidian-500 --font-mono --t-micro --tr-mono
8Day--obsidian-100 --font-sans --t-small --r-sm --s-8disabled --obsidian-600

Rules

§ 18.2

What the contract holds the date picker to, word for word from its section of DESIGN.md.

  • The trigger is a real <input> plus a labelled <button> — never a bare div. The popover is role="dialog" with an accessible name.
  • .is-today and .is-selected are visually distinct on purpose: today is a ring, selection is a fill. When a range end lands on today, the ring is dropped (the fill already says it).
  • Range corners are geometric, not decorative: square in the middle, rounded at the ends, fully rounded when start and end are the same day.
  • Disabled days are <button disabled>, not styled-out divs — keyboard users must be able to tell.
  • State matrix: § 3.0.1. Motion: § 06.4.

Date input

§ 18.3

DD/MM/YYYY, single date. Typed input is digit-by-digit masked (slashes auto-insert) and the trailing icon opens a calendar popover that stays in sync. No native type="date" — the browser picker doesn't respect the system. Optional min/max attributes constrain both the typed value and the picker.

Type or pick. Arrow Down also opens the calendar.
Bound to 2026-05-01 → 2026-12-31.
Not a real date — calendar validation runs on blur, after the mask.
DIGIT-BY-DIGIT RULES
D1max 3 D2max 1 if D1 = 3; if D1 = 0, must be 1–9; else 0–9 M1max 1 M2max 2 if M1 = 1; if M1 = 0, must be 1–9; else 0–9 Y1–40–9
CONTRACT
  • maxlength="10" + inputmode="numeric" required.
  • Slashes are inserted automatically — users only type digits.
  • Real-date check (e.g. 31/02 rejected) runs on blur, not while typing.
  • data-min & data-max in ISO YYYY-MM-DD; both optional.
  • Trailing icon & ↓ on the input both open the picker.
  • Locale: English month/day labels, week starts Monday.
  • Single date only. For range selection, see § 11.4.

Date range

§ 18.4

Two coupled inputs sharing one popover. Two months side-by-side on desktop, stacked on mobile. Click 1 sets the start, click 2 sets the end — clicking before start swaps. Footer presets cover the analytics defaults; the helper line auto-counts inclusive days.

Select start and end. Range count appears here.
SELECTION RULES
  • Click 1 → start. Hover preview shows tentative range.
  • Click 2 ≥ start → commits end.
  • Click 2 < start → swaps (becomes new start).
  • Same-day click twice → single-day range.
  • Esc → cancel pending selection & close.
PRESETS
Todaystart = end = today Last 7 daystoday − 6 → today (inclusive) Last 30 daystoday − 29 → today Last monthfull previous calendar month
CONTRACT
  • Two inputs: data-range-start & data-range-end inside a data-range-group wrapper.
  • Same digit-by-digit mask as § 11.3.
  • Popover anchors to the start input. 580px wide, collapses to 280px stacked under 480px viewport.
  • Helper auto-renders N days (inclusive count) on commit.
  • data-min & data-max on either input bound the whole range.

Do and don’t

§ 18.5

A real input and a labelled button; today a ring, the selection a fill.

✓Do

Pair a real <input> with a labelled button; today is a ring, the selection a fill.

✗Don’t

Build the trigger from a div, style disabled days out instead of disabling them, or open a calendar for a date the reader already knows, such as a birth date.

Use it when
  • A status the system computed, next to the thing it describes.
  • The solid variant: the one featured accent of the viewport.
Use something else when
  • A value a person chose → Chip.
  • It needs hover or focus → Button or Chip.

Anatomy and code

§ 19.1

The badge, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

Published
1Pill--tint-hue --tint-fill --bw-1 --tint-edge-deep --tint-edge --r-md --s-2 --s-4 --s-1
2Label--primary-400 --font-sans --t-small --tr-snug
3Dot--r-full --s-1

Rules

§ 19.2

What the contract holds the badge to, word for word from its section of DESIGN.md.

  • The badge's own register. --t-small, weight 500, --tr-snug; padding --s-2 --s-4, gap --s-1; corner --r-md. Line height and case come from the context. It is taller than the Chip on purpose: the badge reports, the chip is handled.
  • A tint is a hue at two strengths, a fill and an edge, text on the full token. The variants use the Chip's pair, --tint-fill / --tint-edge. The default's deep hue, --tertiary-500, uses the heavier --tint-fill-deep / --tint-edge-deep. Never a hand-written rgba.
  • One line, always. A badge never wraps (white-space: nowrap) and never shrinks in a flex row (flex: none): when room runs out, the text beside it wraps instead.
  • The dot (.dot, --s-1 square, --r-full, currentColor) leads every status badge. The solid badge carries none.
  • The solid badge takes the light label, --obsidian-050 on --primary-500: the second ratified exception of § 6.3, bounded like the first.
  • States: § 3.0.1. default only: a badge is static — no hover, no focus. If it needs one, it is a button or a chip.

Status variants

§ 19.3

The badge variants: semantic color plus the dot. Shape and label carry the state; the tint reinforces it.

Default Live Shipped Pending Blocker Archive Featured Info

In context

§ 19.4

Badges in running context — rows, cards, headers — where they annotate a noun rather than announce a state.

Arc Automotive HMI
2024–2025 · 18 months
Shipped
Project Orion — IoT platform
2025–2026 · ongoing
Pending
Field observability
2023 · 6 months
Archive

Do / Don't

§ 19.5

A badge is a label read at a glance; it holds its line.

✓Do

Keep it to a word or two, the status first, on one line beside what it describes.

✕Don't

Let it break onto two lines. When room runs out the text beside it wraps; the badge never does.

Use it when
  • A value someone entered, chose or can take back: a tag, a facet, a recipient.
  • Pressable chips narrow a set.
Use something else when
  • A status the system computed → Badge.
  • Pressable and dismissible at once → a list row.
  • A link to another page → Link.

Anatomy and code

§ 20.1

The chip, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

Design systems
1Pill--tint-hue --tint-fill --bw-1 --obsidian-700 --tint-edge --r-full --s-1 --s-3
2Label--primary-400 --font-sans --t-small --tr-snug --lh-snug
3Dot--r-full --s-1
4Focus--focus-width --obsidian-100 --focus-offset --ring-halo

Rules

§ 20.2

What the contract holds the chip to, word for word from its section of DESIGN.md.

  • The variant is a tint, at the Badge's two strengths. Background at --tint-fill (0.08), border at --tint-edge (0.3) — tokens since v4.3.0 — text on the full token — the same pair § 3.14 already uses, so the two small labelled objects of the system read as one family. The neutral chip is the exception and the majority case: --obsidian-800 on --obsidian-700, the card surfaces. A filled chip — solid magenta behind white — is off-system: one filled accent per viewport is the whole 85 / 10 / 5 budget, and a filter row would spend it six times over.
  • A chip may be static. A chip that is neither pressable nor dismissible is legal and is the common case — a tag, a category, a value shown but not editable here. What makes it a chip rather than a § 3.14 Badge is who owns the value: a chip carries something a person entered or chose, a badge reports state the system computed. The affordance follows from the context, not from the definition.
  • --r-full, always. A chip with a rectangular radius is a badge wearing a chip's label. The radius is how the two are told apart at a glance, so it is not a variant.
  • The label rides --t-small. --t-micro is the dense step, legal inside a table cell or a toolbar and nowhere else. No other step is on-scale for a chip.
  • A chip carries at most one affordance, never two (RIGID). Pressable or dismissible, not both. Both roles want the 44×44 target and two of them do not fit inside one 28px object without one swallowing the other. A filter chip is a <button> and the whole object is the target; a tag that leads to the list of its tag is an <a>, pressable the same way, with no aria-pressed and no disabled state; a dismissible chip is static and the dismiss button is the only target. A chip that is both is off-system — that pattern is a list row, not a chip. Carbon reaches the same rule from the other side: "avoid using tags with multiple functions to prevent confusion and reduce accidental clicks". It is a decision, not a law — Calcite and Fluent ship the combination deliberately.
  • The touch target grows, the chip does not. At --t-small the rendered box is shorter than the 44×44 floor (§ 6), so the hit area is extended with a transparent inset pseudo-element. Growing the chip to reach 44px instead makes the densest component in the system the tallest one.
  • A filter chip declares aria-pressed, and a group of them is a role="group" with an accessible name. Selection lands on the border and the text (--primary-500 · --primary-400), never on a magenta fill: a filter row is exactly the place where one filled accent becomes six.
  • The dismiss names its value. aria-label="Remove Motion", never a bare × and never "Remove". Removing a chip changes a collection the reader can't otherwise see change, so the field's own live region announces it — not a toast (§ 3.17 is for things that happened somewhere else).
  • Never colour alone (§ 6 floor #1). The label carries the meaning; the variant is emphasis on top of it. There is no icon-only chip — a chip without a label is a dot.
  • Chips wrap, they never scroll and never truncate. A collection long enough to need either collapses behind a trailing +N chip, which is itself a .chip and a button. A horizontally scrolling chip row hides values from a keyboard and from a screen reader alike.

Chip

§ 20.3

A compact label for one value that belongs to something else — a tag on a post, a facet on a result set, a recipient already entered. Where a badge reports what the system computed, a chip carries what a person chose. Six variants on the badge's tint, an optional dot; most chips are neutral and static.

Design systems Motion Amaca Resolved Needs review Broken link Auto-imported

Filter chips

§ 20.4

Pressable chips narrow a set. Each is a button that holds its state in aria-pressed, and the row is a named group. Selection lands on the border and the label, so a row of six selections still spends no fill.

Removable chips

§ 20.5

Values entered into a field. The chip itself stays static; the dismiss is the only target and its label names the value it removes. The field announces the removal in its own live region, and a long collection collapses behind a trailing count.

Recipients
Lena Ortiz Design team Reviewers

Do / Don't

§ 20.6

The chip is the one object here the reader owns: they put the value there, so they can take it back.

✓Do

Use a chip for a value someone entered, chose or can take back. It may be pressable or removable — one of the two, never both.

✕Don't

Use a chip to report status the system computed: that is a badge, and the giveaway is that nobody can remove it. Or let it break onto two lines: the text beside it wraps instead.

Use it when
  • A condition that applies to the whole surface, inline, as long as it is true.
Use something else when
  • The reader must be able to dismiss it → Toast.
  • It concerns one field → the field's .help.error.
  • Two alerts on one surface → merge them into one.

Anatomy and code

§ 21.1

The alert, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

Draft published
The changes are live for every reader.
1Box--obsidian-850 --r-md --s-3 --s-4
2Side--bw-1 --obsidian-800 --bw-2 --primary-500
3Icon--success
4Title--obsidian-100
5Body--obsidian-200 --t-small
6Positioninlinein the flow of the page, above what it talks about

Rules

§ 21.2

What the contract holds the alert to, word for word from its section of DESIGN.md.

  • The fill never changes. Every variant sits on --obsidian-850; semantics ride the 2px left rule and the icon. A tinted alert surface would spend the 5% semantic budget on furniture (§ Law, L1).
  • The icon is mandatory and aria-hidden — it is the shape half of "colour never alone" (§ 6 floor #1). The text carries the meaning.
  • role="status" for informative, role="alert" for danger. Nothing else.
  • An alert is not dismissible, at all. Not on a timer (§ 6 floor #6) and not by the reader either: it clears when the condition clears. A close button would let someone hide a state that is still true. If the reader must be able to make it go away, the message is transient and belongs in § 3.17 Toast — that is the whole difference between the two components.
  • Entrance is the system's canonical reveal — [data-alert-fade] + .is-in, opacity plus 12px from below on --d-slow · --ease-decel, the same ride as [data-fade]. Staggered when several land together. Under reduced motion they are simply there.

Alert

§ 21.3

Inline, in flow, addressed to the whole surface. The fill never changes — semantics ride the 2px left rule and the icon, so the accent budget stays spent on actions, not on furniture. An alert is never dismissible — not on a timer, and not by the reader either.

System voice
The default. No semantic claim, just the system talking.
Tokens synced 4 minutes ago
A neutral fact the reader didn't ask for.
Bundle re-baked
Something completed, and stayed completed.
Draft not published
Attention needed, nothing broken. Changes are saved locally until you publish.

Click to re-trigger animations.

Do / Don't

§ 21.4

An alert states a condition; it clears when the condition clears.

✓Do

Let an alert clear when the condition clears. If the reader has to be able to make the message go away, it was never an alert — it was a toast.

✕Don't

Give an alert a close button. It lets someone hide a state that is still true, and the next reader inherits a screen that lies.

✓Do

Carry the semantics on the left rule and the icon. The surface stays --obsidian-850 in every variant, so the accent budget stays spent on actions.

✕Don't

Tint the fill red or green to say "error" or "success". That spends the 10% band on furniture, and colour alone never carries meaning.

Use it when
  • An event that just happened somewhere else.
Use something else when
  • A fourth message in the stack → Alert.
  • It is the only record of the information → keep it somewhere durable.
  • A critical message that needs an action → Modal.

Anatomy and code

§ 22.1

The toast, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

Draft published
Live on the blog.
1Box--obsidian-850 --bw-1 --obsidian-700 --bw-2 --primary-500 --r-md --sh-3 --s-4
2Icon--success --icon-sm
3Title--obsidian-100
4Body--obsidian-200 --t-small
5Action--primary-400 --font-mono --t-micro --tr-monohover --primary-300
6Dismiss--obsidian-400 --r-smhover --obsidian-100
7Motion--d-base --ease-decel
8Positionfixed · bottom right--s-6 from the bottom and the right edge, in .toast-region

Rules

§ 22.2

What the contract holds the toast to, word for word from its section of DESIGN.md.

  • Auto-dismiss is opt-in and bounded (§ 6 floor #6). data-autohide is permitted only when the toast carries no action and is not .toast-danger; anything with an Undo or an error stays until dismissed. Hover and focus pause any countdown.
  • The region is aria-live="polite"; a .toast-danger carries role="alert" on the toast itself.
  • Newest enters at the bottom of the stack. Three visible at most — a fourth means the surface should be showing an § 3.16 Alert instead.
  • A toast is never the only place a result appears. If losing it loses the information, it wasn't a toast.
  • Same grid as § 3.16: icon and title on the first row, body returning to the icon's left edge below.
  • A toast enters and leaves on the same side it lives on — 24px from the right, opacity with it. Coming in from below would read as the page growing; coming in from the edge reads as something arriving from outside the page, which is what it is. Exit retraces the entry exactly: same axis, same distance, opposite direction.
  • The stack closes the gap, it never snaps. Dismissal is two moves on two curves, and they are not the same move. The toast fades and slides out on --d-quick · --ease-accel — that is the exit. Its box then collapses on --d-base · --ease-decel — and that is not an exit at all: it is the repositioning of the toasts below, which are not leaving, they are settling into a new place. Accel on the collapse would tell the survivors they are being dismissed too. Same roll-off shape as the chat message (§ 3.35). Removing the node outright drops everything below it by a full toast height in one frame — that reads as a glitch, not a dismissal.
  • The two rides differ in speed, not in path. In: .is-in, --d-base · --ease-decel — the system announcing itself. Out: .is-out, --d-quick · --ease-accel — the reader dismissed it, get out of the way. Remove the node after the exit resolves; under reduced motion it goes at once.
  • The toast keeps its close button. It is the dismissible half of the pair: § 3.16 Alert has none by contract.
  • Motion: § 06.4.

Toast

§ 22.3

What an alert becomes when the message is about an action that just happened somewhere else. Transient, stacked bottom-right, three visible at most. Auto-dismiss is opt-in and bounded: only for a toast with no action that is not an error, and hover or focus pauses the countdown.

Draft published
Live at amaca.design/blog.
Figma variables updated
128 variables written to the Amaca library.

Click to re-trigger animations.

Types

§ 22.4

Four types, one rule on the left and one icon: the default for a neutral event, success, warning and danger. The fill never changes.

Figma variables updated
128 variables written to the library.
Draft published
Live on the blog.
Storage almost full
92% of the workspace is in use.

Do / Don't

§ 22.5

A toast reports an event, and something durable keeps the record.

✓Do

Mirror every toast somewhere durable. A toast is a notification of a result, never the record of it.

✕Don't

Auto-hide a toast that carries an Undo or an error. The countdown races the reader; hover and focus must pause it.

Use it when
  • A wait with a known end, longer than about three seconds.
Use something else when
  • A short or unknown wait → Loader.
  • Content arriving into a known layout → Skeleton.
  • Steps the reader takes → Stepper.

Anatomy and code

§ 23.1

The progress bar, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

Upload
60%
1Label--obsidian-300 --font-mono --t-micro --tr-mono
2Value--obsidian-100 --t-small
3Track--obsidian-700 --r-full --s-2
4Fill--grad-progress --sh-glow-soft
5Motion--d-scene --ease-decel

Rules

§ 23.2

What the contract holds the progress bar to, word for word from its section of DESIGN.md.

  • Two heights. 8px by default; .progress-sm, 4px, where the bar sits inside a row or a card. Nothing between: 6px is not on the scale.
  • Named and valued. The track carries role="progressbar", aria-valuenow, aria-valuemin, aria-valuemax and a name; the value is also written as text.
  • Known end only. An unknown wait is a Loader; a bar that moves without a value is not a Progress.
  • Motion. On view the fill draws from zero on --d-scene · --ease-decel from 700ms and lands exactly on its value; a new value moves the fill on the same curve. Reduced motion: the fill is at its value.

Progress bar

§ 23.3

The linear meter: draws from zero to its value on view and lands exactly. The value is stated in text beside the bar. Two heights: 8px by default, 4px with .progress-sm where the bar sits inside a row or a card.

Checkout · completion.progress · 8px
Progress
60%
Checkout · completion.progress-sm · 4px
Progress
60%

Click to re-trigger animations.

Do and don’t

§ 23.4

A known amount of work, with its value written.

✓Do

Write the value beside the bar and name the bar.

✗Don’t

Use it for an unknown wait, or stack two bars for one task.

Use it when
  • A real wait; the large size for full-page waits, the extra-small inline and in buttons.
Use something else when
  • A wait under 200 ms → show the result, no loader.
  • A wait past 800 ms → Skeleton, when the shape of the content is known.
  • Progress is known → Progress.

Anatomy and code

§ 24.1

The loader, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

1Size--loader-lg
2Marklogo-loader-fast.svgthe Amaca logo, drawn and animated in the file
3Motion--d-quick --ease-standard

Rules

§ 24.2

What the contract holds the loader to, word for word from its section of DESIGN.md.

  • Show the loader within 100ms of the action that triggered the wait. Late loaders read as glitches, not feedback.
  • A 200ms wait deserves no loader at all — show the result.
  • One full-page loader per route at a time. After 800ms hand off to a Skeleton.
  • Don't stack a section loader inside a card that already sits inside a full-page loader.
  • Resolve to the success state when a wait completes green. Let the loader keep spinning after the wait is over and you've shipped a bug surface, not a state.
  • Never use the loader as a decorative animation. The mark is in motion because something is loading.

Measurements

Tokenized spacing between logo and label, padding on overlay variants, alpha on the backdrop. Each measurement maps to a system token — no raw px in component code.

Generating tokens…
01
Logo box (MD)
--loader-md (48)
02
Gap between logo and label
--s-3 (12)
03
Label · body 15, tracking snug
--obsidian-200
04
Overlay backdrop alpha
0.72 · blur 2px
05
Success check · scale-in spring
--ease-spring
06
Reduced-motion ring · stroke
--primary-500 · 1.4s
07
ARIA · live announcement
role=status

Variants

§ 24.3

Five shapes for five waits. The standalone is the floor; everything else composes from it. Success swaps the mark for a check on --ease-spring when the wait resolves green.

Standalone
Generating tokens…
With label
Inside button
Skeleton handoff
Success state

Scale

§ 24.4

Four sizes, four tokens. The SVG scales linearly — the file is vector. Hit-area follows the box exactly; no extra padding.

--loader-xs 16 PX
--loader-sm 24 PX
--loader-md 48 PX
--loader-lg 96 PX

Compositions

§ 24.5

Four real-world placements. Click any Trigger to start the wait, watch the loader hold the surface, then resolve.

17.3 / 01 Inside primary button · submitting

A submit handler is in flight. The button stays the same size; the label crossfades to the loader so layout never shifts.

17.3 / 02 Inline · async validation
Checking availability…
17.3 / 03 Full-page · app boot
Loading workspace…
17.3 / 04 Chat composer · bot generating (cross-component)
Amaca is generating a response…
Send another message…

Do / Don't

§ 24.6

A loader has a job. If it isn't communicating a real wait, it's decoration — and decoration on a brand mark dilutes the mark.

✓Do

Show the loader within 100ms of the action that triggered the wait. Late loaders read as a glitch, not feedback.

✕Don't

Pop the loader after the response already returned. A 200ms wait deserves no loader at all — show the result.

✓Do

One full-page loader per route at a time. After 800ms hand off to skeleton frames so the page reads as structurally present.

✕Don't

Stack a section loader inside a card that already sits inside a full-page loader. The user can't tell which wait is which.

✓Do

Pair every loader with an accessible name — aria-label on the loader root or a visible label. Screen readers announce role=status politely.

✕Don't

Use the loader as a decorative animation in the corner of a hero. The mark is in motion because something is loading — never because the page is.

✓Do

Resolve to the success state when a wait completes green — the checkmark crossfades in over the logo on --ease-spring and the live region announces.

✕Don't

Let the loader keep spinning after the wait is over. A loader that won't stop is a bug surface, not a state.

Use it when
  • The shape of the content is known and the wait is past 800 ms: a list, a card, a profile.
  • The content replaces it in the same place and at the same size.
Use something else when
  • The shape is unknown → Loader.
  • A wait under 800 ms → show the result, no skeleton.
  • Progress is known → Progress.

Anatomy and code

§ 25.1

The skeleton, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

1Line--obsidian-700 --r-sm --s-3
2Circle--obsidian-700 --r-full --size
3Shimmer--primary-500 --tint-fill
4Stack--s-3

Rules

§ 25.2

What the contract holds the skeleton to, word for word from its section of DESIGN.md.

  • The same size as the content. The shapes take the height and the width of what will arrive, so the swap moves nothing. If the size is not known, the shape is not known: use a Loader.
  • Visible inside a card. The fill is --obsidian-700. --obsidian-800 is the colour of the card and the shapes would disappear.
  • Quiet. The shimmer is a magenta tint at --tint-fill over 1600 ms, linear, in a loop. No high-contrast sweep.
  • Not the only signal. The region carries aria-busy="true" and a name (aria-label). When the content lands, aria-busy goes away and a live region (role="status") says so.
  • Silent shapes. Every shape is aria-hidden="true". A skeleton is never focusable and never a link.
  • Reduced motion. The shimmer is off and the fill stays still (§ 06.4).

Shapes

§ 25.3

A line for text, a circle for a person, a block for an image. The fill is --obsidian-700, so it stays visible inside a card; the shimmer is a quiet magenta tint and stops under reduced motion.

Text
Person
Card

Loading, then loaded

§ 25.4

The region is aria-busy while the shapes are there, the shapes are aria-hidden, and a live region says when the content arrives. The two states have the same height.

Do and don’t

§ 25.5

A skeleton promises a shape; keep the promise.

✓Do

Draw the shapes at the size of the content and announce the end of the wait.

✗Don’t

Use a skeleton for a wait of a few hundred milliseconds, for content whose shape you do not know, or as the only sign that something is loading.

Use it when
  • Details about one element that carry a link or a button, and do not need to leave the view.
  • A short form or a choice tied to one control: a rename, a share link.
Use something else when
  • Short text with nothing to click → Tooltip.
  • The content is wider than 360 px, or the person must answer before going on → Modal.
  • A list of actions → a menu.
  • A menu or text that needs the full height of the screen → Drawer.

Anatomy and code

§ 27.1

The popover, open in place, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

Rename

1Surface--obsidian-900 --bw-1 --obsidian-700 --r-lg --sh-3 --s-32 --s-4
2Head--s-4
3Title--t-body --lh-snug
4Body--obsidian-200 --s-3
5Foot--s-2
6Motion--d-quick --ease-accel --s-1

Rules

§ 27.2

What the contract holds the popover to, word for word from its section of DESIGN.md.

  • A click opens it, never a hover. Hover belongs to the Tooltip. The trigger is a real button with popovertarget, so the browser sets aria-expanded and the link to the panel.
  • Two kinds of close. A read-only popover is popover (auto): a click outside or Escape closes it. A popover with a form is popover="manual": it closes only on its own buttons and on Escape, so a stray click does not lose the field. Escape on a manual one is handled by the page controller, and the focus goes back to the trigger.
  • One at a time, never nested. An auto popover closes the one that is open. A popover inside a popover is a Modal.
  • Up to 360 px. Wider, or taller than the view, is a Modal. No horizontal scroll.
  • A name. With a title or a form it has role="dialog" and aria-labelledby; a text-only one has aria-label. It is not modal: Tab can leave it.
  • No arrow. The 4px gap and the anchoring point to the trigger, as in the Tooltip.
  • It is placed by CSS. From the invoker, with position-area and a flip when there is no room; no script measures anything. The one exception is the Map (§ 3.43): its popover stays inside the map and beside its cluster, so the map's script places it.
  • Motion. It fades in and rises --s-1 on --d-quick; out, the same, faster. Reduced motion: instant (§ 06.4).

Forms

§ 27.3

A read-only popover is auto: a click outside or Escape closes it. One with a form is manual: it closes only on its own buttons and on Escape, so a stray click does not lose the field. The trigger is a button with popovertarget.

Text and a link
With a title
Form, manual

Under 800 ms the result usually lands before the eye finds the shape, so a skeleton only flickers.

Read the Skeleton rule

Placement

§ 27.4

Below the trigger, aligned to its start, 4px away. .popover-top and .popover-end change the side; whatever the side, it flips when there is no room. There is no arrow.

Above
At the end

Above the trigger, for a control near the bottom edge.

At the end side, for a control in a column.

Do and don’t

§ 27.5

A popover is a click away, not a hover away.

✓Do

Open it with a button and give it a name, and let a form popover close only on its own buttons.

✗Don’t

Open it on hover, put a popover inside a popover, or use it for content wider than 360 px.

Use it when
  • A menu or a few groups of links that open from the edge: the main navigation on a small screen.
  • Text or details of one item that need the full height: release notes, the facts of a row.
Use something else when
  • A decision to answer before going on, or a short form → Modal.
  • A long form, or work that deserves its own address → a page.
  • A panel that stays beside the page → a layout, not a Drawer.
  • Short text tied to one button → Popover.

Anatomy and code

§ 28.1

The drawer, leaning on the right edge as it does on the page, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

Release notes

Two new tokens, one renamed. The full list is below.

1Surface--obsidian-800 --sh-3 --pad
2Head--pad --s-4
3Title--t-h6 --lh-snug
4Body--obsidian-200 --t-body --pad --s-4
5Foot--bw-1 --obsidian-700 --s-4 --pad --s-2
6Backdrop--scrim --blur-veil
7Motion--d-base --ease-accel

Rules

§ 28.2

What the contract holds the drawer to, word for word from its section of DESIGN.md.

  • A modal, anchored. The modal's surface, padding and scrim; no radius, since it sits flush with the edge. The shadow is one step below the modal's (--sh-3 against --sh-4), so it takes no edge.
  • Open it with showModal(). As the modal: focus is trapped, the page behind is inert and does not scroll, Esc closes, and focus returns to the button that opened it. It has the same data-modal-open and data-modal-close hooks.
  • The title is the name. aria-labelledby points at .drawer-title; the menu is a nav with an aria-label, and the current page carries aria-current="page".
  • Closing. Esc always closes, and so does a click on the scrim, except when the drawer holds data not yet saved. The close button is always present. A click on a link of the menu closes it too.
  • The width is a line. 45 characters plus the padding for text, 30 for a menu, never wider than the screen; at 480px or less it takes the full width.
  • A menu uses the List. The rows are a.list-item of the List, at --t-lead; groups are named with .drawer-label, two levels at most.
  • Not for a form. A short form is a Modal, a long one a page. The drawer is for reading and for choosing where to go.
  • Never stacked, no bottom. A drawer opened from a drawer wants a page; one from the bottom is a different component.
  • A menu opens on the side of its toggle. A toggle at the end of the bar opens it at the end (the default); one at the start uses .drawer-start.
  • Motion. It slides in from its edge on --d-base · --ease-decel and the scrim fades with it; out, the same on --ease-accel. Reduced motion: instant (§ 06.4).
  • No state matrix beyond default and current. The panel is never focused or pressed (§ 3.0.1); the rows carry the List's states.

Try it

§ 28.3

A real <dialog> opened with showModal(), like the modal: focus is trapped, the page behind is inert, Esc or a click on the scrim closes it, and focus returns to the button. A link of the menu closes it too.

Menu

Release notes

v5.0.0 re-architects the components one at a time: each one is approved on a render before anything is written to the repo.

Slider, Divider, Skeleton, Popover and Drawer are new. Text input gains an icon and an action.

Breaking

The date and time pickers lose .dp-icon-btn in favour of .input-action.

Migration guide

The wide drawer holds text with longer lines: a guide, a diff, a table of renamed classes. It keeps the same edge, the same scrim and the same motion; only the width grows, to 60ch plus the padding.

Read it top to bottom, then close it and the page is where you left it.

Do and don’t

§ 28.4

The drawer is for reading and for choosing where to go.

✓Do

Use it for a menu or a text the person reads with the page still behind it, and name it with its title.

✗Don’t

Put a form in it, stack one drawer on another, or make it a panel that stays on the page.

Use it when
  • A label for something already on screen.
  • Only on interactive elements, such as an icon-only button.
Use something else when
  • The information is needed to finish the task → .help or Alert.
  • It needs a title → Alert.
  • It holds a link or a button → Popover.

Anatomy and code

§ 29.1

The tooltip, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

Pushes local tokens to Figma
1Bubble--obsidian-800 --bw-1 --obsidian-700 --r-sm --sh-2 --s-1 --s-2
2Label--obsidian-100 --font-sans --t-caption --lh-snug
3Motion--d-quick --ease-standard

Rules

§ 29.2

What the contract holds the tooltip to, word for word from its section of DESIGN.md.

  • Opens on hover and on focus. A tooltip reachable only by pointer fails § 6 floor #5.
  • At most one tooltip is open at a time (RIGID). Opening one closes every other — two bubbles on screen overlap, and an overlapped label is a label that can't be read. Escape closes; scrolling closes. This cannot be expressed in CSS (:hover has no memory of the others), so the open state is the .is-open class and a controller owns it. A tooltip that opens on a bare :hover rule is off-system.
  • The trigger references it with aria-describedby; the bubble is role="tooltip". A tooltip with no trigger relationship is decoration.
  • No interactive content. No links, no buttons, no form controls — there is no accessible path to reach them.
  • Never the sole carrier of meaning (§ 6 floor #1). If the information is required to complete the task, it belongs in .help or in an § 3.16 Alert.
  • No arrow. The 6px offset and the anchoring do the pointing; an arrow adds a shape to maintain at every edge case.
  • Plain text only, one or two lines. A tooltip that needs a title is an alert.

Tooltip

§ 29.3

A label for something already visible. Never a container for content, never the sole carrier of meaning. Opens on hover and on focus — a tooltip reachable only by pointer fails the accessibility floor. The last one opens below and aligns its end to its anchor: near the top edge and the end edge, the bubble stays on screen.

Pushes local tokens to the Figma library Rebuilds every download bundle that embeds a changed file Pending Waiting on a review before it can ship

Do / Don't

§ 29.4

A tooltip labels a thing already on screen.

✓Do

Keep one tooltip open at a time, opened on hover and on focus. Two bubbles on screen overlap, and an overlapped label cannot be read.

✕Don't

Put a link, a button, or anything required inside a tooltip. There is no accessible path to reach it, and it disappears the moment the pointer leaves.

Use it when
  • Related views of one page, read one at a time.
Use something else when
  • Moving between pages → Navigation.
  • A linear process → Stepper.
  • The reader needs several sections open at once → Accordion.

Anatomy and code

§ 31.1

The tabs, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

1Rail--bw-1 --obsidian-800 --s-4
2Tab--obsidian-300 --font-sans --t-small --s-3 --s-4hover --obsidian-100 · selected --obsidian-100
3Indicator--primary-500 --d-base --ease-decel
4Focus--focus-width --obsidian-100 --focus-offset --ring-halo

Rules

§ 31.2

What the contract holds the tabs to, word for word from its section of DESIGN.md.

  • Panel swap is fade only: never crossfade, never slide both. The leaving panel is removed (display:none) before the new one fades in.
  • Keyboard: Tab reaches each trigger; Enter/Space activates.
  • One indicator. One .tab-indicator per tablist, 2px of --primary-500, moved with transform and width; never an underline per tab.
  • Views of one page. Tabs switch what one page shows; going to another page is Navigation.

Tabs

§ 31.3

Tabs with one shared indicator that travels — 2px of magenta, transform-driven. The panel swaps as the indicator lands.

Tab content sits in the active panel. Indicator is a 2px magenta underline that slides between tabs. Inactive tabs stay at tertiary weight.
The process panel. Three months of in-vehicle observation, instrumentation of the legacy HMI, and correlation with eye-tracking data before a single screen was drawn.
The outcome panel. −41% task time, +12pt retention, 94% token adoption across 7 surfaces in 18 months.
The reflection panel. Under‑invested in the companion mobile app for the first 6 months. Correcting course late cost us a quarter of rework.

Do and don’t

§ 31.4

One view at a time, one indicator that travels.

✓Do

Fade the new panel in once the old one is gone, under one magenta indicator that travels.

✗Don’t

Crossfade or slide both panels, or use tabs to move between pages: that is Navigation.

Use it when
  • Two or three views of the same content, switched in place: the anatomy and the code of a component, a chart and its table.
Use something else when
  • Views of different content, or more than three → Tabs.
  • Moving between pages → Navigation.
  • A value the reader submits with a form → Radio.

Anatomy and code

§ 32.1

The segmented control, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

1Track--surface-sunken --bw-1 --border-subtle --r-full --s-1
2Item--obsidian-300 --font-sans --t-small --s-4 --s-2hover --obsidian-100 · selected --obsidian-100
3Thumb--obsidian-700 --bw-1 --border-input --r-full --sh-1
4Motion--d-base --ease-decel
5Focus--focus-width --obsidian-100 --focus-offset --ring-halo

Rules

§ 32.2

What the contract holds the segmented control to, word for word from its section of DESIGN.md.

  • States: § 3.0.1. default · hover · active · selected · focus-visible, values in the state index.
  • The thumb is the selection. Its edge, --border-input on --surface-sunken, reads at 3.27 : 1 (WCAG 1.4.11); without the edge the fill alone is 1.24 : 1. The label of the selected segment is --obsidian-100 (12.57 : 1 on the thumb), the others --obsidian-300 (6.30 : 1 on the track).
  • One stop on Tab. The selected segment carries tabindex="0", the others -1. Arrow Left and Right select the neighbour and move the focus to it, Home and End go to the ends.
  • Two or three segments, each an icon and a label. The label stays: an icon alone does not say which view it opens.
  • The thumb travels under 120px between neighbouring segments, so it rides --d-base (Motion, duration by distance).

Full width

§ 32.3

With .segmented-fill the segments share the width of their container in equal parts, for a narrow column or a phone.

The anatomy of the component: the track, two segments with an icon and a label, and the thumb under the selected one.

Segmented control

§ 32.4

Two views of one component, its anatomy and its code. The thumb travels to the segment you pick and the panel fades in as it lands; the arrow keys move between segments. Component spec: § 3.28 in the contract.

The anatomy of the component: the track, two segments with an icon and a label, and the thumb under the selected one.

Do and don’t

§ 32.5

Two or three views of one thing, each named.

✓Do

Keep two or three segments, each with an icon and a label, under one thumb that travels.

✗Don’t

Drop the labels, put more than three views in it, or use it for a value a form submits: that is a Radio.

Use it when
  • Pages of similar items too long for one page: search results, a list of case studies.
  • The rows of a table that do not fit on the screen, in the compact form below it.
Use something else when
  • A linear journey such as a form → a Continue button and a Back link.
  • Only one page → show nothing.
  • Content that sits on one page without slowing it → no pagination.

Anatomy and code

§ 34.1

The pagination, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

1Page--obsidian-300 --t-small --bw-1 --r-md --s-8hover --obsidian-100 --obsidian-700 · current --obsidian-100 --obsidian-700 --obsidian-500
2Gap--obsidian-300 --t-small
3Row--s-1
4Focus--focus-width --obsidian-100 --s-px --ring-halo

Rules

§ 34.2

What the contract holds the pagination to, word for word from its section of DESIGN.md.

  • What to show. The first page, the last page, the current page and one on each side; a gap stands for the rest. A gap never sits at an end, and a gap for a single page shows that page instead. Under --bp-sm use the compact form; the numbered one still fits there, as icons without their words, but it is the fallback.
  • The current page is marked, not hidden. aria-current="page" on its link, with the fill and the border of § 3.0.1. It stays a link.
  • Name every link (WCAG 2.5.3). A number carries aria-label="Page 6". Previous and Next carry rel="prev" and rel="next", and their arrows are decorative.
  • The ends stay in place. On the first page Previous is disabled, on the last page Next is: aria-disabled="true" and no href, so the row does not move and the link is not a target.
  • Pages are addresses. Each page is a URL that loads without script. The page number goes in the document title ("Search results, page 6 of 12"), and a page that no longer exists goes to the first.
  • Not for a journey, and not scrolling forever. A form or any linear journey takes a Continue button and a Back link. Infinite scroll takes the keyboard away from the reader; it is not a replacement.
  • Motion. Hover and focus change the background and the colour on --d-quick · --ease-standard, none under reduced motion (§ 06.4).

Numbered

§ 34.3

First, last, the current page and one on each side; a gap stands for the pages in between. Previous and Next stay in place and go disabled at the ends, so the row does not move.

First page
A middle page

Compact

§ 34.4

Previous, where you are, Next. For narrow spaces, under --bp-sm, and for the footer of a table. The status is text, not a link.

Compact

Do and don’t

§ 34.5

One set of pages, one nav.

✓Do

Mark the current page with aria-current, name every number (“Page 6”), and put the page number in the page title.

✕Don’t

Use it for a form or any linear journey, show it for a single page, or replace it with infinite scroll.

Use it when
  • A linear flow: forms, onboarding, checkout.
  • Three or more steps.
Use something else when
  • Fewer than three steps → a Next button.
  • Steps that can be done in any order → Tabs.

Anatomy and code

§ 35.1

The stepper, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

1Dot--obsidian-800 --bw-1 --obsidian-300 --r-full --stp-dotdone --primary-500 · current --grad-progress --sh-glow --stp-cur
2Number--obsidian-300 --t-caption
3Pulse--bw-1 --primary-500 --r-full
4Connector--obsidian-300 --s-1 --s-2 --bw-2done --grad-progress
5Label--obsidian-300 --t-smalldone --obsidian-100 · current --primary-200
6Meta--obsidian-300 --font-mono --t-micro --tr-monocurrent --primary-200

Rules

§ 35.2

What the contract holds the stepper to, word for word from its section of DESIGN.md.

  • Horizontal while the labels fit. Three to five steps with short labels, from 480px of the stepper's own width. Below 480px it turns vertical by itself (a container query); .stepper-vertical forces the column for long labels or more steps.
  • One current step. Exactly one aria-current="step"; done steps before it, upcoming steps after.
  • Done is drawn, upcoming is dashed. The connector after a done step is --grad-progress at --bw-2; the one after the current and upcoming steps is a dashed --obsidian-300 line, 4px dashes and 4px gaps (--s-1) (5.6 : 1 on a card).
  • Contrast. Upcoming labels and every meta line are --obsidian-300 (5.6 : 1 on --obsidian-800); the number and the check on a filled dot are --obsidian-950 (6.5 : 1 on --primary-500).
  • Not a control. The steps are not links: going back is the flow's own Back button.
  • Motion. On view, every dot scales in on --d-slow · --ease-spring (opacity on --ease-decel) and its text rises 8px, from 520ms plus 80ms per step; each connector draws to the next dot on --d-slow · --ease-decel from 580ms plus 80ms per step, done or dashed alike, so the choreography holds for any number of steps. The current dot breathes: a 1px ring scales to 1.33 and fades, 2400ms, in a loop. Reduced motion: everything in place, the ring still.

Horizontal

§ 35.3

Done steps carry a check and a drawn connector, the current step breathes, upcoming steps stay outlined. The current step is announced: aria-current="step" and a hidden word for each state.

Checkout · order flowStep 3 of 5

Click to re-trigger animations.

Vertical

§ 35.4

The same steps in a column. It is automatic below 480px of the stepper’s own width, and forced with .stepper-vertical anywhere.

Checkout · order flow.stepper-vertical

Click to re-trigger animations.

Do and don’t

§ 35.5

Horizontal while every label fits on one line; vertical when they don’t. One ladder for the reveal, whatever the state.

✓Do

Use the horizontal stepper for three to five steps with short labels, from 480px of width up; let it turn vertical below, or force the vertical one for long labels or more steps.

✕Don’t

Squeeze a horizontal stepper until its labels wrap, hide the labels to make it fit, or show more than one current step.

✓Do

Reveal it on one ladder: dot n scales in from 520ms + 80ms·n, the connector that leaves it draws from 580ms + 80ms·n, both on --d-slow. Done or upcoming only changes the paint, so one more step is one more rung.

✕Don’t

Time the done and the upcoming steps as two phases, hold one back until the other ends, or start the ladder from the current step: the reveal would change every time the person moves on.

Use it when
  • One project or one stat: micro-header, title, body, at most one action.
  • A concept the reader scans and acts on as one unit.
  • A list of items, each opening its own page → the row card.
  • The one item a page leads with → the feature card, once per page.
Use something else when
  • Secondary information shown on demand → Popover or Modal.
  • A gradient background → never; the surface stays on the obsidian ramp. The light of the feature card is a glow behind the content, not a fill.

Anatomy and code

§ 36.1

The card; the badge and the button inside it have their own pages, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

CS-017 · 2025Live

Atlas · token pipeline

One source for every surface. 14 months, 3 platforms.

1Surface--obsidian-800 --bw-1 --obsidian-700 --r-lg --s-6hover --obsidian-600 --sh-2
2Meta--obsidian-300 --font-mono --t-micro --tr-mono
3Actions--s-3
4Motion--d-quick --ease-standard

Rules

§ 36.2

What the contract holds the card to, word for word from its section of DESIGN.md.

  • Background: --obsidian-800. Border: 1px solid --obsidian-700. Radius: --r-lg.
  • Every card carries a micro-header (.card-meta) with project code, date, or index. Mono, --t-micro, --obsidian-300.
  • Card rise: a card fades up as it comes into view: opacity 0→1 over --s-8 of rise, --d-slow · --ease-soft, the page's own entrance (§ Page shell). A card already in view when the page opens rides with its page block. Cards that show later, together, go in reading order, 80ms apart (capped at eight steps), and the content rides with its card, it does not enter on its own. A stat's count-up starts on the same frame as its card or block. A card inside a card moves with its parent. Under reduced motion the cards are at rest and every number shows its value.
  • Hover: border shifts to --obsidian-600, shadow --sh-2.
  • At most one action per card, on its own row at the end; it is .btn-primary only when the card is the screen's single primary act.

Project card

§ 36.3

The project card: micro-header, media, title, body, and at most one action. Surfaces ride the card ramp; the media scales on hover, the card itself never lifts.

CS-017 · 2025Live

Arc · Automotive HMI

Rethinking the center stack for a next-gen EV. 18 months, 4 platforms.

CS-019 · 2026Pending
CONFIDENTIAL

Project Orion — IoT platform

Pending state card. This payload is merely illustrative.

CS-015 · 2024Archive

Field · observability app

On-call dashboards for a SaaS infra company.

Click to re-trigger animations.

Stat card

§ 36.4

The stat card is a chart with no plot, so it lives with the charts: Charts › Stat · one number (§ 44.3) holds the demo, the classes and the count-up. The surface is this page’s card ramp.

Row card

§ 36.5

One line of a list: a square media, the micro-header, a title and one clipped line, then a chevron. The whole card is the link, so it holds no button; it hovers like any card and takes the focus ring of a pressable.

Feature card

§ 36.6

The one item a page leads with: media and tags on one side, title, text, reading time and the action on the other, a magenta light from the top right corner and a tab that names why it leads. Once per page; one column under the medium breakpoint.

Latest project
CS-020 · 2026

One source, four platforms: a token pipeline for a fleet console

How a single token file reached web, iOS, Android and the in-car cluster without a hand-copied value, and what the team stopped arguing about.

Reading time · 8 min

Do and don’t

§ 36.7

One micro-header, at most one action.

✓Do

Give every card a micro-header and at most one action, last on its own row.

✗Don’t

Put a gradient on a card, or a .btn-primary unless the card is the screen's single primary act.

✓Do

Let the media zoom inside its clip, on hover and on focus-within.

✗Don’t

Grow the card itself, or invent a spacing the anatomy does not have: stop and ask.

✓Do

Make the whole row card the link, and use the feature card once per page.

✗Don’t

Put a button inside a row card, or give the feature card’s light, tab or surface another colour.

Use it when
  • Dense reference content: headers scanned first, read selectively.
  • Multi-open sparingly, when answers are compared side by side.
Use something else when
  • Content the reader will read in full → a page with headings.
  • Switching quickly between sections → Tabs.

Anatomy and code

§ 37.1

The accordion, one item open, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

Every release keeps the old names working for one minor, then removes them.

1Item--bw-1 --obsidian-800
2Trigger--s-4 --s-6
3Chevron--obsidian-400open --primary-400
4Label500
5Number--obsidian-500 --font-mono --t-caption --tr-monoopen --primary-400
6Body--obsidian-300 --t-small --s-6 --s-4
7Motion--d-slow --ease-standardopen --ease-decel
8Focus--focus-width --obsidian-100 --ring-halo

Rules

§ 37.2

What the contract holds the accordion to, word for word from its section of DESIGN.md.

  • Single-open by default.
  • Chevron rotates 90° on expand.
  • Keyboard-operable: aria-expanded toggled, Enter/Space opens/closes.
  • States: § 3.0.1. default · hover · active · expanded · focus-visible on the trigger, values in the state index. Focus is the system's pressable ring, drawn inside the row.

Each part and its rule

Each part of the accordion with the rule that governs it — hit target, single control, chevron direction, duration by measured height.

PartRule
.acc-triggerFull-row hit target — minimum 56px tall. Entire row is a single button; never put a separate toggle inside.
.acc-chevronRotates from 0° (closed) to 90° (open) over 320ms on the standard ease. Color shifts to primary-400 on open. No swap-icon.
.acc-numOptional. Mono index in the form NN / NN. Use when the set has a fixed, ordered count; omit on FAQ-style lists.
.acc-panelAnimates via grid-template-rows 0fr → 1fr — no JS height measurement. Body fades in 80ms after the row starts opening.
aria-expandedRequired on every trigger. Toggled in JS in lockstep with .is-open. The panel carries role="region" and an id referenced by aria-controls.
Reduced motionUnder prefers-reduced-motion: reduce, the row swaps instantly — no rotation, no fade, no grid transition.

Default · single-open

§ 37.3

Used inside case-study briefs and project requirements lists. Opening one panel closes the others — keeps the eye anchored. First item is open on mount so the surface never reads as inert.

Clean informative text that is displayed only when someone wants to deep dive more. Crucial to cluster topics under the same group.

Ship the platform as a living surface, not a release. Feature-flag everything; let production teams roll out capabilities to fleets in cohorts without redeploying the console.

Workstations run 10–14h shifts under variable lighting. Type stays at 16px minimum; primary actions clear AAA contrast on every surface; error states never rely on hue alone.

Every OTA action — operator, target VIN range, package hash, timestamp — is logged and exportable. The interface surfaces this trail in-context, not in a separate compliance tool.

Multi-open · FAQ

§ 37.4

When users compare answers side-by-side. Add data-multi on the wrapper — items toggle independently. Use sparingly; default to single-open.

Yes — typically as a 12–16 week engagement with embedded engineering pairing. I don't ship throwaway redesigns; the system has to outlive my involvement.

Two weeks for a discovery sprint — research, audit, a short written direction. Anything shorter doesn't leave room to disagree with the brief, which is usually where the work is.

Remote-first, with on-site weeks for kickoff and major milestones. I'm based in Milan and travel comfortably across EU.

Do and don’t

§ 37.5

Headers scanned first, one panel open.

✓Do

Open one panel at a time by default, each header a real button with aria-expanded.

✗Don’t

Fold away content the reader will read in full: that is a page with headings.

Use it when
  • A change of topic inside one container: two blocks of a card, a modal or a menu.
  • Groups side by side that need a boundary: the groups of a toolbar.
Use something else when
  • The rows of a List or a Table → the row's own hairline.
  • Space already separates the blocks → no line.
  • The sections of a page → a heading and space.

Anatomy and code

§ 38.1

The divider with a label, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

1Line--bw-1 --obsidian-700
2Label--obsidian-300 --t-caption --s-3
3Line beside label--bw-1 --obsidian-700

Rules

§ 38.2

What the contract holds the divider to, word for word from its section of DESIGN.md.

  • A break, not decoration. The horizontal divider is an hr: a change of topic, with the implicit role separator. A line that only decorates is not a Divider: the rows of a List and a Table already carry their own hairline.
  • The vertical one names itself. A div has no meaning, so it carries role="separator" and aria-orientation="vertical". The parent has a definite height or is a flex row.
  • The labelled one is content. It has no role="separator": that role makes its children presentational and the word would be lost to the screen reader. The two lines are pseudo-elements.
  • The space belongs to the container. No margin on the divider: the gap on both sides is the parent's (--s-4 to --s-6), like the icon's.
  • One weight, one colour. --obsidian-700, 1px. --border-subtle (--obsidian-800) vanishes inside a card, which is --obsidian-800; a heavier line is a different component, not a thicker divider.
  • Static. No hover, no focus, no motion.

Forms

§ 38.3

Horizontal is an hr; vertical is a div with role="separator" and aria-orientation; the labelled one is two lines around a word and is not a separator, so the word stays readable.

Horizontal
Project settings
Name, owner and visibility.

Danger zone
Archive or delete this project.
Vertical
With a label
or

Do and don’t

§ 38.4

A line is a break, not decoration.

✓Do

Draw it where the topic changes, and let the container give the space on both sides.

✕Don’t

Put one between the rows of a List or a Table, stack lines to make a heavier one, or add a margin to the divider itself.

Use it when
  • Conversational interfaces: a chatbot, an assistant.
Use something else when
  • A one-way notice from the system → Toast or Alert.
  • Reference content the reader consults, not a dialogue → a page or Accordion.

Anatomy and code

§ 39.1

A message from the assistant and a reply, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

Amaca10:42
Which tokens changed?
Two, and one renamed.
1Avatar--obsidian-800 --bw-1 --obsidian-700 --primary-400 --font-mono --t-caption --tr-mono
2Name--obsidian-200 --t-caption --tr-snug
3Time--obsidian-300 --font-mono --t-micro --tr-mono
4Bubble--r-lg --s-3 --s-4 --t-body --tr-snug --lh-normal
5Bot bubble--obsidian-800 --bw-1 --obsidian-700
6Own bubble--primary-700 --obsidian-050
7Stack--s-1

Rules

§ 39.2

What the contract holds the chat to, word for word from its section of DESIGN.md.

  • Own bubbles fill with --primary-700 — the deep brand accent.
  • The brighter --primary-500 is not used here. It stays reserved for CTAs and focus rings, so the 85/10/5 budget holds in long conversations.
  • A previous "hot last" rule (paint only the latest own bubble primary-500) was tried and discarded — the bright last bubble started reading as a CTA, stealing weight from the actual primary action on the page.
  • One avatar per streak. The rest of the streak uses .chat-avatar-spacer for column alignment, never repeats the avatar.
  • Bot reply replaces the typing bubble in place (mutate the existing .chat-stack, swap .chat-bubble content) so layout doesn't shift.
  • After ~3 exchanges, roll older messages off the top so the surface never overcrowds — under reduced motion .is-out still hides (instantly), per the motion table.

Motion spec

Every transition resolves to a token, in the canonical § 09.4 grammar. Two-stage bubble entry: the container scales from 0.92 with an --ease-spring overshoot (spatial only — the opacity rides --ease-standard), then the inner content fades up 80ms behind.

TriggerProperty (type)MotionReduced motion
message enter · container .chat-bubblescale 0.92→1 (spatial)--d-base · --ease-springinstant
message enter · containeropacity (effect)--d-base · --ease-standardinstant
message enter · content .chat-bubble-contentopacity (effect) · translateY 3px→0 (spatial)--d-quick · --ease-standard · delay 80msinstant
roll-off .chat-msg.is-outopacity (effect) · translateY −8px (spatial)--d-quick · --ease-accelinstant — still hides
roll-off .chat-msg.is-outmax-height + margin collapse (spatial)--d-base · --ease-accelinstant — still hides
typing dots .chat-dot (loop)translateY 0→−4px→0 (spatial) · opacity 0.3→1→0.3 (effect)--d-slow · --ease-standard · stagger 0/160/320msnone — the dots hold still at full opacity
send idle → ready .chat-composer-sendbackground + color (effect)--d-quick · --ease-standardinstant

Composer

§ 39.3

Textarea expands from 36px to 132px before scrolling. Send is dim while empty; lights up magenta on first keystroke. Attach is ghost-styled, hover only. Enter sends; ⇧ Enter inserts a newline.

Empty · resting
Enter Send · ⇧ Enter Newline
Ready · text staged
Send button activates on first keystroke

Live demo

§ 39.4

Three scripted exchanges with a chatbot. Each turn: typing indicator first, then the bubble lands. After the third exchange completes, older messages roll off the top so the surface never overcrowds. Hit replay to restart.

§ 19.2 · Amaca assistant
Amaca · assistant
Online

Do / Don't

§ 39.5

Anti-patterns surfaced from early chatbot builds. Most are 85/10/5 violations dressed up as enthusiasm.

✓Do

Own bubbles fill with --primary-700 — the deep brand accent. Bright --primary-500 stays reserved for CTAs and focus rings.

✕Don't

Paint own bubbles with --primary-500. The bubble starts reading as a CTA and the actual primary action on the page loses its weight.

✓Do

Anchor the typing indicator inside a bot bubble — same shape, same surface, same column. The reader's eye stays where the next message will land.

✕Don't

Float "Amaca is typing…" as a gutter caption below the conversation. For a single-bot UI it's noise; the avatar already says who's typing.

✓Do

Show the avatar only on the first bot bubble of a streak. The rest of the streak gets a 32px invisible spacer so the column stays aligned.

✕Don't

Repeat the avatar on every bot bubble. It hijacks the eye and steals the budget from content.

✓Do

Roll older messages off the top after the conversation passes ~3 exchanges. Fade up + collapse in --d-quick on --ease-accel.

✕Don't

Let the demo grow unbounded. A demo that fills the viewport buries the motion you're trying to document.

Use it when
  • A label names a colour: a legend key, a token in a table.
  • A palette shows its values, one tile per step.
  • An easing token shows its curve and the duration it rides, one tile per token.
  • A foundation page shows its scale: type, space, radius, shadow, the icon album, one tile per token or drawing.
Use something else when
  • The colour stands for a state, with a word → Badge or Chip.
  • The shape carries the meaning, not the colour → the Diagrams legend.
  • The user picks a colour → off-system: stop and ask.

Anatomy and code

§ 40.1

The swatch tile, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

Primary 500#F051D5Brand
1Tile--obsidian-900 --bw-1 --obsidian-800 --r-md
2Fill--s-24
3Meta--s-3 --s-4 --s-0-5
4Name--obsidian-100 --t-small
5Hex--obsidian-200 --font-mono --t-caption --tr-mono
6Role--obsidian-300 --font-mono --t-micro --tr-mono

Rules

§ 40.2

What the contract holds the swatch to, word for word from its section of DESIGN.md.

  • Static. No hover, no focus: a swatch is not a button and not a picker. Choosing a colour is off-system. The colour forms never move.
  • The motion tile moves because the motion is its value. Time across, position up, as in CSS Easing L1 and MDN: a curve copied from the tile is the curve it shows. The dot rides the curve and the ball on the rail shares its height. The move lasts exactly --d, inside a cycle of 2.5 × --d; three cycles and it rests on the end, under 5 s even on --d-slow (WCAG 2.2.2). It plays when it comes into view, and again each time it comes back. A Replay button under the tiles plays them again; a loop for ever is not an option (WCAG 2.2.2 would then ask for a pause control). Reduced motion shows the end.
  • The curve is set on the element. animation-timing-function: var(--e) on each mover; a var() inside @keyframes is not read and every curve falls back to ease.
  • The radius tile is 1:1, the icon tile is 4×. A radius is judged at its size; an icon is judged on its grid, so the zoom shows where the drawing sits in the 14 live area, and the 16 and 24 beside it show it at size. The zoom is the album drawing in an .icon, never a redrawing.
  • A type tile prints what its class renders. Size, leading, tracking and weight are read from the .ty-* class the sample wears, not typed beside it.
  • A row of examples thins, it does not wrap. .swatch-grid-4 holds four tiles in one row and drops the last ones as its own width shrinks (container query on the bp tokens): 4, then 3 under 900px, 2 under 720px, 1 under 480px.
  • The value is a token. --sw is set to var() of a token, never a pasted hex; --e and --d to var() of an easing and a duration. --sw-r to var() of a radius token, --sw-s to var() of a spacing token, --sw-sh to var() of a shadow or ring token. On a tile the printed hex, radius, curve and milliseconds are that token's value in tokens.css, and the drawn path is its curve.
  • Always bordered. An inset 1px of --border-input, so the size does not change and a dark colour still has an edge: 3.38, 3.27 and 3.13 : 1 on --surface, --surface-sunken and --surface-field (WCAG 1.4.11). On --surface-raised it is 2.93 : 1, so on a card the label is what carries the colour.
  • Before its label. The gap is the container's (a legend row --s-3, a token in a table --s-1); swatch and label are one unit that never breaks across lines.
  • The word carries the meaning. A swatch never stands for a state or a series alone: the label names it (§ 6 floor #1).
  • Named once. Beside a visible label it is aria-hidden="true"; alone, role="img" with an aria-label that names the colour.
  • The Diagrams legend is not a swatch. Its key shows a node's shape, with a border in the colour of the state (§ 3.44).

Forms

§ 40.3

Inline, 12px, before its label; the label names the colour, so the swatch is hidden from the screen reader, and the gap is the container’s. Tile, one step of a palette with its name, the token’s value and its role. Both carry an inset 1px of --border-input. Type tile and space tile, horizontal: a row of their scale, the sample or the bar at 1:1, a divider, the specs. Radius and shadow tiles: the shape at the token’s radius, a surface with the token’s shadow. Motion tile, one easing token on the standard graph: time across, position up, the ball on the rail at the dot’s height; three plays, then it rests on the end (WCAG 2.2.2). Icon tile, an album drawing 4× on its grid.

Inline
  • Direct48%
  • Organic27%
  • Referral15%
obsidian-100 on obsidian-950
Tile
Secondary 500#00FFF2Accent
Obsidian 950#07090BPage
Type tile

A case for quiet precision.

Heading 2--t-h252 / 1.05 / -4%Section title · Bold 700

Body copy. Reads comfortably at 62–72 characters per line. Default for paragraphs, long-form notes and case studies.

Body--t-body15 / 1.65 / 0Default body text · Regular 400
Space tile
Space 2--s-28px
Space 4--s-416px
Space 8--s-832px
Space 16--s-1664px
Radius tile
Small--r-sm · 4px
Medium--r-md · 8pxWorking range
X-large--r-xl · 16pxCase study hero
Full--r-full · 999pxPill · circle
Shadow tile
Level 1--sh-1Subtle
Level 3--sh-3Popover
Glow--sh-glowPrimary hover
Ring halo--ring-haloFocus · pressables
Motion tile
Decel--ease-decel · 350 mscubic-bezier(.16,1,.3,1)Entrances
Accel--ease-accel · 350 mscubic-bezier(.7,0,.84,0)Exits
Ease in-out--ease-standard · 350 mscubic-bezier(.45,0,.55,1)State changes
Spring--ease-spring · 600 mscubic-bezier(.34,1.56,.64,1)Delight · reveal

Click to re-trigger animations.

Icon tile
search16 · 24Search
settings16 · 24Settings
calendar16 · 24Date
configurations16 · 24Configure, adjust, filter

Do and don’t

§ 40.4

A swatch shows a value; the word next to it carries the meaning.

✓Do

Put it before its label, let the label name the colour, and take the fill, the curve and the duration from tokens.

✕Don’t

Make it a button, let the colour carry the meaning alone, paste a hex or a cubic-bezier where a token exists, loop a motion tile for ever, or stretch the hex swatch out of a regular hexagon.

Use it when
  • Showing who a person, team or account is, beside their name or standing for it: a comment, a list row, a menu header, a member list.
  • Several people share one item and a count is enough: a stack of up to five.
Use something else when
  • A picture of a thing rather than of a person or account: a product, a place → an image in a Card.
  • A count or a status standing alone → Badge.
  • An icon that stands for an action → an icon-only Button, § 3.6.

Anatomy and code

§ 41.1

The avatar with its status, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

1Circle--obsidian-700 --bw-1 --obsidian-600 --r-full --size
2Initials--obsidian-100
3Status--bw-2 --obsidian-950 --dc --r-full --dot

Rules

§ 41.2

What the contract holds the avatar to, word for word from its section of DESIGN.md.

  • Always a circle (RIGID). There is no square variant, and the shape never means "person" or "organisation". A team or an account is told apart by its image or its initials, not by its corners.
  • The name sits on the wrapper (RIGID). role="img" with aria-label="Anna Rossi"; the image has alt="" and the initials are aria-hidden. Next to the name in text the whole avatar is aria-hidden="true", so the name is read once. An avatar with no name and no text beside it is a defect.
  • Image, then initials, then the glyph. The image fails over to the initials by itself (onerror="this.remove()" in the markup, no script of the system). With no name there is the user icon of § 3.1 at 60% of the side, --obsidian-300. Initials are at most two letters.
  • Neutral by default. Every avatar starts on --obsidian-700. The five .avatar-hue-* tints are for telling people apart in a long list, assigned by the caller from a stable key such as the id, never by position and never reshuffled. --danger is not one of them: it reads as an error.
  • A 1px edge on every variant, the image included, so a dark photo and a light one weigh the same on a dark page.
  • Status is never colour alone (WCAG 1.4.1). The offline dot is a hollow ring, and the status goes in the accessible name: "Anna Rossi, online". The ring around the dot is the surface the avatar sits on: set --avatar-ring on a card (--obsidian-800); the default is the page, --obsidian-950.
  • Groups. Overlap is a quarter of the side, and each avatar is cut out of the one before by --bw-2 of the surface, so it works on any background. The last avatar sits on top. Show at most five slots; past that the fifth is "+N", the same size, labelled "N more people".
  • Interactive only when it goes somewhere. An <a> or a <button> carries .avatar and its label names the destination ("Open Anna Rossi's profile"). Hover moves the edge to --primary-400, pressed to --primary-500, focus is the pressable ring. Any other avatar does not react.
  • Motion. The edge colour on --d-quick · --ease-standard — a row in the § 06.4 index; reduced motion is instant.
  • No other states. The avatar is not disabled, selected or loading; a pending picture shows the initials.

Fallback and colour

§ 41.3

The image gives way to the initials by itself, and with no name there is the glyph. Every avatar starts neutral; the five tints are for telling people apart in a long list.

image fails
image
initials
no name
hue 1
hue 2
hue 3
hue 4
hue 5

Sizes

§ 41.4

Five steps, 32 by default. The initials follow the type scale at about 0.4 of the side. The image variant is the brand mark; a person's photo uses the same circle. Component spec: § 3.37 in the contract.

Image
24
32
40
48
64
Initials
24
32
40
48
64
No name
24
32
40
48
64

Status

§ 41.5

A dot at the bottom right, ringed in the surface the avatar sits on. The offline dot is hollow; the status is always in the accessible name.

online
away
busy
offline
online
away
busy
offline

Interactive

§ 41.6

Only an avatar that goes somewhere reacts: the edge turns to the brand colour on hover and press, and the focus is the pressable ring.

Group

§ 41.7

Overlapped by a quarter of the side, each avatar cut out of the one before. At most five slots; the fifth becomes the count.

+3
+2
+2
+2
+2
+2

Do and don't

§ 41.8

One rule: an avatar names someone.

✓Do

Give the avatar a name the screen reader can say, on the wrapper, and hide it when the name is already beside it.

✕Don't

Use an avatar as decoration, or for a thing that is not a person or an account. A picture of a product is an image in a card.

Use it when
  • Many things of one kind, each a line or two, to scan or open: projects, settings, people.
  • The facts of one item as label and value pairs, with an action beside a value to change it.
Use something else when
  • Several columns to compare or sort → Table, § 3.39.
  • A choice from a fixed set → Select or Radio.
  • Bullets inside running text → a plain ul or ol in the prose, no component.

Anatomy and code

§ 42.1

The list, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

1Row--bw-1 --obsidian-700
2Item--s-3 --s-4
3Title--obsidian-100 --t-small
4Meta--obsidian-300 --t-caption
5End--s-2
6Motion--d-quick --ease-standard
7Focus--focus-width --obsidian-100 --ring-halo

Rules

§ 42.2

What the contract holds the list to, word for word from its section of DESIGN.md.

  • A row is one thing. Every row of a list has the same parts in the same order; a row that needs more columns is a Table row, § 3.25. A list is not nested inside a row.
  • Static or interactive, by the element. A div row has no hover and no pointer. An a or a button row has hover, focus-visible and disabled (aria-disabled="true"), values in § 3.0.1. The whole row is the one target: no link around a row that holds a button.
  • Name every action (WCAG 2.5.3). An icon-only action in a row carries an aria-label that says which row (More actions for Arc). A text action keeps its visible word at the start of its name (Change owner).
  • Summary semantics. dl with a div per row is valid HTML; each dt has at least one dd. A fact without a value is not a row.
  • The hairlines stay. They tie each row to its action and help the reader who zooms. Drop them only in a list with no actions, and then for the whole list.
  • One density. One row height; there is no small or large list. A list that has to be denser is a Table.
  • Motion. Hover changes the background on --d-quick · --ease-standard, none under reduced motion (§ 06.4).

Items

§ 42.3

One row per thing: a lead (icon or avatar), a title with its meta line, and an end (badge, action, chevron). A static row is a div; a row that opens something is an a or a button and gets hover and focus. The last row here is disabled.

Static rows
  • Arc · Automotive HMICS-017 · 2025Live
  • Lumen · Retail kioskCS-016 · 2024Draft
  • Orbit · Fleet consoleCS-014 · 2024Archive

Summary

§ 42.4

The facts of one item: a key in mono, a value, and an action beside the values that can change. A key is never empty and neither is a value. Under 720px the three columns stack.

One project
Name
Arc · Automotive HMI
Owner
Anna Rossi
Status
Live
Last updated
12 Aug 2026

Do and don’t

§ 42.5

One row is one thing.

✓Do

Keep the same parts in every row of a list, and give an icon-only action in a row its own accessible name.

✕Don’t

Put columns in a row to compare them (that is a Table), nest a list in a row, or wrap a whole row in a link and put a button inside it.

Use it when
  • Content, not chrome.
  • Comparing values across rows and columns.
Use something else when
  • Laying out a page → Grid.
  • Key facts about one item → List.

Anatomy and code

§ 43.1

The table, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

TokenValue
--s-416
--s-624
--s-832
1Header--obsidian-300 --font-mono --t-micro --tr-mono --obsidian-850 --bw-1 --obsidian-800 --s-3 --s-4
2Cell--bw-1 --obsidian-800 --s-3 --s-4
3Code--primary-400 --font-mono --t-caption --tr-mono --obsidian-850 --r-sm

Rules

§ 43.2

What the contract holds the table to, word for word from its section of DESIGN.md.

  • No zebra striping. The row rule carries the scan; a second device would double the noise.
  • Left-align everything, including numbers, unless the column is a pure quantity — then use font-variant-numeric: tabular-nums and align right.
  • The header is a label, not a heading: mono-uppercase, same register as .label (§ 3.10).
  • A table wider than its container scrolls in a wrapper, .table-scroll (overflow-x: auto) — never shrink the type to fit.
  • Tables are content, not chrome: a row has no state. A row that is a link holds one .table-row-link, the anchor in its first cell, whose hit area covers the row: the row then hovers --obsidian-850, presses --obsidian-800 and takes the pressable ring. One link per row and nothing else pressable in it: a second target would sit under the first.
  • Sorting: one driver (RIGID). A column that sorts holds a .table-sort button in its header. One header drives the order and carries aria-sort; it sits a step lighter and shows its direction. Every other sortable header shows sort-ascending, the order its first click gives.
  • A click never clears the order. On the driver it flips ascending and descending; on another column it makes that column the driver, ascending. The table opens sorted, its driver declared in the markup.
  • The whole header is the target. The button fills the cell, so the target is the header's height and width, never the icon alone. Focus is the pressable halo with the outline inside the cell (offset -2px, as the date picker's days): the frame clips the halo on the table's outer edges, the outline always shows.

Table

§ 43.3

The plainest data surface in the system, and the one this documentation leans on most. No zebra striping — the row rule carries the scan, and a second device would double the noise. The header is a label, not a heading: mono-uppercase, the same register as a field label.

PartTreatmentNote
Frame--obsidian-800 border, --r-mdOverflow hidden, so the radius clips the first and last rows
HeaderMono 10px on --obsidian-850Uppercase, --obsidian-300, weight 500
Cell13px, padding --s-3 --s-4Left-aligned, including numbers
Row rule1px solid --obsidian-800The last row drops it
Inline code.table codeMono 12px, --primary-400
Secondary cell.mutedThis column, throughout

Pure quantities are the one exception to left alignment: align them right, set tabular-nums.

Sortable

§ 43.4

A column that sorts holds a button in its header, and the whole header is the target. One header drives the order: it carries aria-sort, sits a step lighter and shows its direction. Every other sortable header shows the ascending icon, the order its first click gives. A second click on the driver flips it; the table opens sorted and a click never clears the order.

Atlas12 OctMarta
Dune15 OctLeo
Borea3 OctIvo
Cirro28 SepAda

Linked rows

§ 43.5

A row that opens a page is the link: one anchor in its first cell, whose hit area covers the row. The row lightens on hover, a step more while pressed, and takes the focus ring as a whole. A row that is not a link has no state.

ProjectScopeYear
OrionFleet console2025
NovaOnboarding2025
AtlasToken pipeline2026
FieldObservability2024

Do and don't

§ 43.6

One rule for sorting: there is always a driver. One target per row.

✓Do

Open a sortable table already sorted, with its driver named by aria-sort and a step lighter.

✕Don't

Let a click clear the order, put the sort on the icon alone, or mark the driver by its colour only.

✓Do

Make a row that opens a page one link, its anchor in the first cell covering the row.

✕Don't

Highlight a row that is not a link, or put a second link or button in a linked row.

Use it when
  • Bars for comparison, labelled at the value; the donut for composition.
  • Lines for trends over time and for several series.
  • Areas for cumulative totals.
  • One number that matters, with no plot: the stat.
Use something else when
  • Several time series → lines, not bars.
  • Five points or fewer → bars, not a line.

Anatomy and code

§ 44.1

The chart card, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

Adoption by regionOrion · beta
EMEA91%AMER71%APAC58%LATAM29%
1Card--obsidian-800 --bw-1 --obsidian-700 --r-lg --s-6
2Title--t-body
3Source--obsidian-300 --font-mono --t-caption --tr-mono
4Bars--primary-300 --primary-500 --primary-600 --primary-700the sequential palette, low to high · track --obsidian-800
5Axis label--obsidian-200
6Value label--obsidian-100

Rules

§ 44.2

What the contract holds the chart card to, word for word from its section of DESIGN.md.

  • Text is HTML, never inside the svg (RIGID). Every label of a plot (axes, values, a donut's centre) is an element over the drawing, positioned in % of its viewBox and sized by a token, so it reads the same at every width. A <text> inside a plot scales with the drawing and is never a token: 10 units were 14px on a desktop card and 4px on a phone.
  • Two drawings, one shown. A plot whose proportions or points do not hold on a phone carries a second drawing, .chart-plot-sm, with its own viewBox (344 wide against 700), the same data and points at data-r 8; under --bp-md it replaces .chart-plot-lg, labels included. The small one is aria-hidden: the chart has one accessible name. The baseline clip reads its width from data-clip-w.
  • Bars keep two columns down to --bp-md (.chart-grid): one column at 900 made each bar chart twice its size.
  • The plot is not interactive. No hover, no focus, no state but default (state index). A value the reader needs is written as text: the bar label, the legend percentage, the stat.
  • Choreography E (the line and area plot). Each part starts when the one before it reads, not when it ends: the baseline at --d-base, the line --d-slow later, area and points --d-slow after the line starts, the points --d-quick apart. Under reduced motion every part lands drawn.

Stat · one number

§ 44.3

The stat: one number with no plot, on a quiet data surface on the card ramp. One number, one accent at most, and the delta keeps its sign — color never carries direction alone.

Retention · D30CS-017
0%
▲ 12pt vs. baseline
Task timeCS-017
0%
vs. legacy HMI
NPSCS-019
0
▲ 8 from Q4
Error rateCS-015
0.0%
▲ from 0.1%

Click to re-trigger animations.

Bars · comparison

§ 44.4

Bars for comparison, labeled at the value. One accent series against neutrals — 85/10/5 applies to data too.

Adoption by regionOrion · beta
EMEAAMERAPACLATAM91%71%58%29%
Sessions by daylast 14d
1.7k
  • Previous 7 days
  • Last 7 days

Click to re-trigger animations.

Pie · composition

§ 44.5

Composition as a donut: slices from the magenta ramp, the legend as the labelling surface, percentages counting in lockstep with the slices. Component spec: § 3.41 in the contract.

Traffic sourcesLast 30d · %
0.0VISITS · K
  • Direct0%
  • Organic0%
  • Referral0%
  • Social0%
Adoption · Q1–Q4Tokens shipped
0TOKENS
  • Color0%
  • Spacing0%
  • Typography0%
  • Motion0%

Click to re-trigger animations.

Line · area

§ 44.6

Line and area: one series lit, the rest recede. The draw-on traces once on reveal — data motion is information, not decoration.

Task time · Arc HMI
Median, seconds to complete priority flow
2026 baseline
4530150Q1Q2Q3Q4
  • Baseline
  • After redesign

Click to re-trigger animations.

Palette ramps

§ 44.7

The two data palettes: a sequential magenta ramp for density, a closed categorical set for series, in this order — and nothing outside them. The categorical set borrows the semantic hues: in a chart they name series, so no colour in the same view may also stand for a state (L1).

Sequential · magentaHeatmaps · density
020406080100
CategoricalUp to 6 series
Primary
Success
Info
Warning
Danger
Neutral

Do and don’t

§ 44.8

Text over the drawing, timing from tokens, nothing to press.

✓Do

Write every label of a plot as HTML over the drawing, sized by a token, with the same margin on all four sides of the canvas.

✗Don’t

Put a <text> inside the svg, or let a label touch the edge of the canvas.

✓Do

Give a plot that does not hold on a phone a second drawing, and keep the bars in two columns down to 720px.

✗Don’t

Let a phone shrink the desktop drawing until its points and labels cannot be read.

✓Do

Start each part of the reveal when the one before it reads, with every duration, curve and delay from a token.

✗Don’t

Type a duration or a delay by hand, or give the plot a hover or a focus.

✓Do

Give every slice its legend row, with its percentage written.

✗Don’t

Let colour carry a slice alone, or paste a hex where a palette token exists.

Use it when
  • Tasks on one shared time axis, so overlap and sequence read at a glance.
Use something else when
  • Dates with no duration and no overlap → List or Table.
  • Steps done in order, one at a time → Stepper.

Timeline · gantt

§ 45.1

The gantt: workstreams across weeks. Time reads left to right on the neutral grid; state reads from the fill. Under the width a lane needs, the plot scrolls sideways and its labels keep their size.

Project · Orion launch
Five workstreams across twelve weeks. The dashed line is today.
5 lanes
W1W3W5W7W9W11DiscoveryDesignEngineeringQALaunchRES · 2DES · 4ENG · 6QA · 3OPS · 2ResearchWireframe → Hi-fiBuild · API · IntegrationBug bashShipMar 03Mar 31Apr 28May 26Today

Click to re-trigger animations.

Do and don’t

§ 45.2

It scrolls; it never shrinks.

✓Do

Let the plot scroll sideways under the width its lanes need, keep the labels at their tokens, and end the last date on the last gridline.

✗Don’t

Shrink the gantt to fit a phone, write its labels inside the svg, or let a date run off the canvas.

Use it when
  • The pattern in the data is geographic.
  • A few places carry a state; the rest of the land is context.
Use something else when
  • Data for only a few areas → a bar chart.
  • Geographic data with no geographic pattern → another chart.
  • Exact values to compare place by place → Table.

Geographic · hex map

§ 46.1

The land is Natural Earth 1:110m (public domain), baked into the page as a grid of hexes: no request at run time. A coastal cell draws smaller, by how much of it is land. A cluster of cells is one place with nodes: it takes the colour of its worst state, and the rest of the land stays grey. Hover, focus or a click act on the whole cluster, never on one hex: it glows in its colour, and grey land lights its country. The sea never takes a colour. On a wide screen a click opens the cluster's Popover inside the map, beside the cluster, and the rest of the land dims; under 720px a tap opens its Drawer.

Deployments · worldwide
Edge nodes by status · 2026 Q2
Live
  • Primary hub
  • High load
  • Active incident

Brazil

Primary hub · 3 nodes

  • EDGE-20123.5°S 46.6°W · p95 38 ms · 99.97%Primary hub
  • EDGE-20215.8°S 47.9°W · p95 41 ms · 99.95%Primary hub
  • EDGE-2033.7°S 38.5°W · p95 52 ms · 99.91%Primary hub

France

Primary hub · 2 nodes

  • EDGE-31148.9°N 2.4°E · p95 24 ms · 99.98%Primary hub
  • EDGE-31243.3°N 5.4°E · p95 29 ms · 99.96%Primary hub

Italy

Primary hub · 2 nodes

  • EDGE-32145.5°N 9.2°E · p95 27 ms · 99.97%Primary hub
  • EDGE-32241.9°N 12.5°E · p95 31 ms · 99.94%Primary hub

Mozambique

High load · 2 nodes

  • EDGE-41126.0°S 32.6°E · p95 118 ms · 99.42%High load
  • EDGE-41219.8°S 34.8°E · p95 64 ms · 99.88%Primary hub

Kazakhstan

Active incident · 2 nodes

  • EDGE-51143.2°N 76.9°E · p95 412 ms · 96.10%Active incident
  • EDGE-51251.1°N 71.4°E · p95 73 ms · 99.81%Primary hub

Indonesia

High load · 3 nodes

  • EDGE-6116.2°S 106.8°E · p95 141 ms · 99.37%High load
  • EDGE-6127.3°S 112.7°E · p95 58 ms · 99.90%Primary hub
  • EDGE-6135.1°S 119.4°E · p95 61 ms · 99.89%Primary hub

Do and don’t

§ 46.2

The cluster is the unit; the sea is never coloured.

✓Do

Act on the whole cluster: it glows in its colour on hover and focus, and a click opens its Popover.

✗Don’t

Scale the cells, light one hex alone, or colour the sea.

✓Do

Keep a few clusters on land that stays grey, each in the colour of its worst state and named in the legend.

✗Don’t

Colour every place, or let a colour carry a state with no legend.

✓Do

Open the Popover inside the map, beside its cluster, and dim the rest of the land while it is open. Under 720px, open the Drawer instead.

✗Don’t

Let the popover leave the map or cover its cluster, or open a popover on a phone.

Use it when
  • Flowcharts and system architecture, box and line with grouping.
Use something else when
  • Sequence, tree or ER diagrams → out of scope.

Anatomy and code

§ 47.1

The diagram, part by part, with the tokens that draw each part. Switch to Code for its HTML and the rules of components.css that draw it, ready to copy or download.

FIG-01 · Token gate
1Shell--obsidian-900 --bw-1 --obsidian-700 --r-lg --s-6 --s-5
2Canvas140px
3Caption--obsidian-300 --font-mono --t-micro --tr-mono
4Index--primary-400

Rules

§ 47.2

What the contract holds the diagram to, word for word from its section of DESIGN.md.

  • One diagram per figure; the source lives in <template class="diagram-src"> and never renders as raw text.
  • Theme only from tokens read at runtime. A hex literal in themeVariables is a regression (§ 9).
  • Exactly one magenta focus node per diagram (:::focus). More than one = decorating (§ 1.4).
  • Semantic state only when a node encodes a real state — color + shape + label + legend (§ 6 #1).
  • Orthogonal routing, soft elbows. Never diagonal. Never CSS transforms on SVG <g> (§ 7.3).

Flowchart · node-edge

§ 47.3

Decisions and paths. Rectangles are steps, diamonds are gates, dashed edges are loops. One magenta node — the entry — anchors where to start reading. Everything else recedes to obsidian.

FIG-01 · Token-resolution path

System architecture · grouped

§ 47.4

Box-and-line with grouping. Subgraphs become clusters — --obsidian-850 fill, --obsidian-700 border — so related boxes read as one zone. The one magenta node is the source of truth every surface derives from.

FIG-02 · Multi-deploy · one token, three surfaces

Semantic states · legend

§ 47.5

The one case for color beyond magenta. When a node encodes a real state, it carries the semantic hue — but never alone: every state node also changes shape and spells the state in its label (§ 6 floor #1). A legend maps each pairing.

FIG-03 · Release gate · color + shape + label

Token map

§ 47.6

A <figure class="diagram"> wraps a .diagram-canvas[role="img"] (holding the rendered SVG) and a .diagram-caption. Surface, border, and radius are coherent with .card. Semantic-state diagrams add a .diagram-legend.

ClassRole
.diagramFigure shell. --obsidian-900 surface, 1px --obsidian-700 border, --r-lg, --s-6 padding — the .card frame.
.diagram-canvasRender target. Carries role="img" + a descriptive aria-label; the library injects the SVG here. A textual source sits in a <template class="diagram-src"> until render.
.diagram-captionMono micro, --obsidian-300, FIG-NN · title — echoes .card-meta. The index tints --primary-400.
.diagram-legendOptional. Only on semantic-state diagrams. Each row pairs a shape swatch + a written label — color never carries meaning alone.

Token map — applied through themeVariables populated from getComputedStyle(:root), plus classDef strings built from the same tokens at runtime. The theming config holds zero hex literals.

ElementToken
Canvas / background--obsidian-900
Node fill--obsidian-800
Node border · text--obsidian-600 · --obsidian-100
Edge line + arrowhead--obsidian-500
Edge label--obsidian-300 on --obsidian-900
Cluster fill · border--obsidian-850 · --obsidian-700
Node radius · font--r-md · Satoshi (--font-sans)
Focus node border--primary-500 · one per diagram
State nodes--success · --warning · --danger

Motion spec

§ 47.7

Entrance reuses the system's established reveal patterns — nothing new, in the canonical § 09.4 grammar. An IntersectionObserver fires once (threshold 0.15, rootMargin 0px 0px -8% 0px, unobserve after fire); nodes cascade in, then edges and clusters. Opacity only: transforms on the generated SVG <g> groups compose with the cascade and break the auto-layout positioning. The hook runs after the async render resolves, not on first paint.

TriggerProperty (type)MotionReduced motion
enter viewport · nodesopacity (effect)--d-base · --ease-decel · stagger ~70msinstant — straight to resolved
enter viewport · edges + clustersopacity (effect)--d-base · --ease-decel · after nodes, +40ms eachinstant — straight to resolved

Do / Don't

§ 47.8

A diagram is structure, not decoration. Most of these are 85/10/5 violations or auto-layout breakers.

✓Do

Keep every node neutral obsidian and spend the magenta once — on the entry or the narrative focus. One accent per diagram is the budget.

✕Don't

Color nodes by category with a per-type rainbow palette. The hues stop meaning anything and the surface goes loud.

✓Do

Read tokens from :root at runtime and feed them to themeVariables and classDef. The source of truth stays tokens.css.

✕Don't

Paste hex literals into themeVariables. A copied #0B0E12 drifts the moment a token moves — it's a regression on day one.

✓Do

Animate the entrance with opacity only, gated on an IntersectionObserver that fires once after render resolves.

✕Don't

Apply CSS transform to the SVG <g> groups. It composes with the auto-layout transforms and the diagram falls apart — set position via the SVG attribute, animate opacity only.

✓Do

When a node encodes a real state, pair the semantic color with a distinct shape and a written label, and add a legend.

✕Don't

Fill nodes with gradients or bleed a diffuse magenta glow down the edges. Depth comes from token borders, never from decoration.

Use it when
  • Slide decks on the system, advanced by hand.
Use something else when
  • Auto-advance → never (accessibility floor #6).

The deck

§ 48.1

A worked 8-slide deck — the Arc HMI case study, told on-system. Navigate with the controls, the dots, or the ← / → arrow keys. No auto-advance (a11y floor #6); position is remembered across reloads.

01 / 08 Cover

Layouts

§ 48.2

The eight templates, authored at full 1920×1080 and scaled to fit. Each is a single .slide with a layout modifier; the chrome (brand, footer, page number) is shared across all of them.

TPL-01 · Cover16:9
Amaca
Case study · CS-017

Rebuilding Arc's in-car interface.

A next-generation automotive HMI — one glance at a time.

Amaca · Case study01 / 08

Opening slide. Title carries the single accent word; eyebrow and footer set context.

TPL-02 · Section divider16:9
Amaca
Section

Brief

Amaca · Case study02 / 08

Chapter break. The oversized numeral is ghosted obsidian — a graphic, not an accent.

TPL-03 · Content16:9
Amaca
01 · Brief

The legacy stack made drivers work.

The previous HMI borrowed mobile patterns — deep hierarchies, tap-heavy flows — and dropped them into a 70mph context.

The constraint: every primary task in under 3 interactions, under 10 seconds, eyes-on-road.

Amaca · Case study03 / 08

Title + body. Two short paragraphs, one bolded key line. Copy capped near 44ch.

TPL-04 · Two-column16:9
Amaca
Dossier

Scope, fixed before a screen was drawn.

RoleLead Product Designer
Team3 design · 14 eng · 2 research
PlatformStack · cluster · companion
Window18 months
Amaca · Case study04 / 08

Heading left, metadata list right. Hairline rows echo the case-file dossier.

TPL-05 · Big stat16:9
Amaca
Outcome
0%
Task time, priority flows
38s → 22s median · measured over 6 weeks
Amaca · Case study05 / 08

One number, full weight. The magenta gradient is the slide's single accent.

TPL-06 · Data16:9
Amaca

Adoption by region.

EMEA0%
AMER0%
APAC0%
LATAM0%
Amaca · Case study06 / 08

Chart slide. One hue, descending weight — the Data Viz vocabulary (§ 22) at slide scale.

TPL-07 · Full-bleed image16:9
Amaca
Fig. 01

Six weeks in the cabin, 42 participants.

Amaca · Case study07 / 08

Media fills the frame; a bottom scrim keeps the caption and chrome legible. Drop a real photo in place of the placeholder.

TPL-08 · Closing16:9
Amaca

Let's build.

Maila@amaca.design
Webamaca.design
Amaca · Case study08 / 08

Sign-off. Statement line plus contact in mono — no CTA fights the message.

Anatomy

§ 48.3

Every slide is authored in a 1920×1080 coordinate space, then scaled to whatever container holds it. The chrome and safe margins are constant so a deck reads as one object.

PartSpec
.slide canvas1920×1080, 16:9, surface --obsidian-950. Scaled via transform: scale(), never reflowed.
Safe margin120px left/right, 88px top/bottom (--pad-x / --pad-y). Content lives inside.
.slide-brandTop-left lockup — logo mark + “Amaca” wordmark. Present on every slide.
.slide-footFooter rail — project · date (left), page NN / total (right). Mono, --obsidian-300; the live page index tints --primary-400.
TypeIts own optical scale — the UI scale (§ 05) is tuned for an 1180px column, not a 1920px canvas. Titles 64–150px, body 30px, mono chrome 20–24px.
AccentOne primary moment per slide — one keyword, one numeral, or the eyebrow rule. The rest is obsidian (85/10/5, § 52).

Layout modifiers — each is a single class on .slide:

ClassUse
.slide--coverTitle / opening
.slide--dividerSection divider with ghosted numeral
.slide--contentTitle + body copy
.slide--splitTwo-column — heading + metadata list
.slide--statOne oversized metric
.slide--dataChart slide (Data Viz vocabulary)
.slide--imageFull-bleed media + scrim + caption
.slide--closingSign-off / contact

Rules

§ 48.4

The presentation rules: one magenta moment per slide, neutrals for everything else, evidence over adjectives.

✓Do

Spend one magenta moment per slide — a keyword, a numeral, or the eyebrow rule. Everything else stays obsidian (85/10/5).

✕Don't

Flood a slide background with a magenta gradient or tint more than one element. That reads as decoration, not signal.

✓Do

Keep the 1920×1080 canvas fixed and scale it. Nav controls live outside the scaled frame so they stay usable at any size.

✕Don't

Auto-advance the deck or loop decorative motion. Navigation is manual — arrow keys, controls, or dots (a11y floor #6).

Use it when
  • The starting frame for every case study.
  • A project told as a narrative: the hero with its reading time and tags, the facts in a dossier, numbered sections with figures and points, the outcome in numbers, a reflection, more cases.
Use something else when
  • A project shown in a grid among others → Card (project).

2024–2025 · 18 months

Rebuilding Arc's in-car interface, one glance at a time.

A next-generation automotive HMI for a connected EV — reshaping how drivers interact with their vehicle at 70mph and at a standstill.

Reading time8 minutes
Tags
  • Product design
  • Automotive
  • HMI
Role
Lead Product Designer
Team
3 designers, 14 engineers, 2 researchers
Platform
Center stack · cluster · companion
Outcome
−41% task time · +12pt retention

01 · Brief

The legacy stack made drivers work.

Arc's previous HMI borrowed patterns from mobile — deep hierarchies, tap-heavy flows — and dropped them into a 70mph context. Drivers spent 38 seconds on average on the top-three priority tasks. That number needed to move.

We started with a constraint: every primary task completes in under 3 interactions, under 10 seconds, eyes-on-road. Everything else followed.

"The design system was the outcome. Every shipped screen was a byproduct of the tokens."

02 · Process

Measure twice, cut once.

Three months of in-vehicle observation before a single screen was drawn. We logged every interaction on the legacy HMI over six weeks, then matched it to eye-tracking data from our research partners.

Fig. 01Distracted-driving events by flow, 6 weeks, 42 participants.

The map that came back was unambiguous: 73% of distracted-driving events clustered around three flows. Those three became our design targets.

03 · What changed

Three decisions carried the rest.

Three flows, not thirty

The observation data pointed at three flows. We redesigned those first and left the long tail on the legacy patterns until the new ones had shipped and been measured.


Glanceable by default

Every screen had to read in one glance at arm's length. Type grew a step, labels lost a word, and every state that mattered got its own colour and its own word.

Anything that failed the one-glance test moved to the companion app.


One token set, three surfaces

The center stack, the cluster and the companion app read the same tokens, so a change shipped once and landed everywhere.

04 · Outcome

Measured, shipped, maintained.

  • Task time
    −0%
    38s → 22s median
  • Eyes-off-road
    −0%
    Below the 12s rule
  • Retention
    +0pt
    D30, 72 → 84%

Shipped to 2 vehicle platforms in Q4 2024. The token system that emerged from this project became the foundation for Arc's whole product design language.

05 · Reflection

What I'd do differently.

We underinvested in the companion app for the first 6 months, treating it as a delivery vehicle for the in-car experience rather than its own surface. By the time we corrected course, three flows had to be rebuilt. The lesson — surfaces are equal citizens from day one — is now how I scope every multi-platform project.

NoteFigures are rounded; the vehicle platforms are not named under the client's agreement.

Do and don’t

§ 49.1

A story in a fixed order, closed by more cases.

✓Do

Open with the hero, its reading time and tags, and close on more cases as Cards.

✗Don’t

Add a call-to-action button of the template's own, or repeat Back at the bottom of the page.

01 · CLARITY BEFORE CLEVERNESS

The obvious answer, on purpose.

If a user has to decode an icon or guess at a label, we failed. Metaphors earn their keep or get cut.

In
  • A labelled button
  • A verbose variable name
  • Four explicit lines over one trick
Out
  • An icon-only button whose glyph is not universal
  • A metaphor the reader has to decode
02 · EVIDENCE OVER OPINION

Every decision shows its work.

Case studies document the path, not just the destination. Sketches, data, dead ends — the version that didn't ship is part of the file too.

In
  • The answer to “why” as a principle, a token or a commit
  • A one-line comment on a non-obvious choice
Out
  • “Because it looks better”
03 · MOTION IS A MATERIAL

Interfaces are not static. Neither is this document.

The way something arrives, settles, responds carries meaning. Motion in this system is consistent and intentional — every transition follows the same rhythm, so the whole document reads as one instrument.

Every entrance in this system breathes on the same curve — CUBIC-BEZIER(0.16, 1, 0.3, 1), the signature ease, living at --ease-decel since v2.0.0 — so the whole document feels like a single instrument, played deliberately.

In
  • Motion that reports a change of state: enter, exit, focus, error
  • Entrances on --ease-decel, exits on --ease-accel, state changes on --ease-standard, --ease-spring for emphasis
Out
  • Motion with no state change behind it
  • A decorative loop
04 · PRECISION IS A FEELING

Rigor the eye picks up before the mind does.

Spacing, timing, typographic rhythm — when they're locked, the work reads as considered before a single word is parsed. The rules are in Foundations.

In
  • Every value on a scale: var(--s-3)
Out
  • padding: 14px
  • A value between two steps
05 · QUIET, THEN LOUD

Restraint is what makes accents land.

Most of the surface stays neutral. Color, motion, weight — they only show up where the work needs them, never as decoration.

In
  • Neutral surfaces, one accent where the work needs it
  • One primary action per screen
Out
  • Primary on more than one element per viewport
  • Accent borders, glowing dividers, tinted furniture

The registry

§ 51.1

Each convention carries a severity and the kind of check that can hold it, from a closed catalogue of check kinds. The full code rules live in DESIGN.md § Code conventions.

IdConventionSeverityCheck
C1Token names: --{family}-{step} for primitives, --{role}-{variant} for semantics; a closed vocabulary of modifiers; no value inside a role name
§ Colors · semantic
errornone
C2Classes BEM-ish and pragmatic: .card, .card-meta; no deep nesting; state on ARIA or a pseudo-class when one exists, otherwise .is-*
§ Code conventions · CSS
warnnone
C3data-* for state hooks (data-fade, data-reveal, data-replay)
§ Code conventions · HTML
warnnone
C4Canonical, semantic HTML: every tag closed, every attribute double-quoted, <button> for actions, <a> for navigation, never <div onclick>; ARIA only when HTML can't say it
§ Code conventions · HTML
errorlint
C5Files: tokens.css → components.css, a third file only past 150 lines; kebab-case, named after the role (card.css, not box.css)
§ Code conventions · CSS, File naming
warnnone
C6Media queries mobile-first, scoped inside the component block, only at the --bp-* widths
§ Code conventions · CSS · check 53
errorlint
C7!important only on the closed list
§ Code conventions · CSS · check 50
errorcount
C8Browser chrome: four surfaces styled, everything else native
§ Code conventions · Browser chrome
errorcount
C9Code patterns by name: replay, staggered reveal, anchor-link routing, SVG attribute transforms, runtime-token anti-drift, collapse-to-zero
§ Code conventions · JavaScript
infonone
C10One number: a site section and its DESIGN.md § 3 entry share it
§ Components
infoparity

The 85/10/5 law

§ 52.1

The proportion rule for every screen, slide, and case study. Obsidian is the ground; brand and signal are accents.

≥ 85%
Obsidian PAGE, SURFACES, TYPE
≤ 10%
Magenta + Cyan CTA, FOCUS, EMPHASIS
≤ 5%
Semantic FEEDBACK ONLY
RULE

≥ 85% Obsidian, ≤ 10% Magenta + Cyan combined, ≤ 5% semantic. If a layout violates this rule, it's decoration, not hierarchy.

The registry

§ 52.2

error stops the output; warn is flagged in review. The check column names the harness check or the verifier that holds the rule.

IdRuleSeverityCheck
L185 / 10 / 5: ≥ 85% of any surface is --obsidian-*; ≤ 10% is --primary-* and --secondary-* (cyan) together; ≤ 5% is semantic
Quiet, then loud · § Colors · § Principles
warnamaca-frontend verifier (budget)
L2The accessibility floor, all seven items: a component that can't meet all seven doesn't ship
Clarity before cleverness · § Accessibility
erroramaca-frontend verifier · review
L3One primary action per screen; a landing may carry one per section
Quiet, then loud · § Principles · Quiet, then loud
warnamaca-frontend verifier
L4Tokens only, on the scale: no hex, px, cubic-bezier or duration literal in component code
Precision is a feeling · § For the AI agent #1–2 · § Code conventions
errorchecks 01, 02, 03, 07, 51
L5A composite value is reproduced verbatim: every stop, layer and family
Precision is a feeling · § For the AI agent #9
errorcheck 38
L6No emoji in the interface
Clarity before cleverness · § Iconography · § Voice & tone
errorcheck 16
L7prefers-reduced-motion: reduce is honoured by every transition
Motion is a material · § For the AI agent #7 · § Motion
errorcheck 49
L8Motion grammar: six durations and five easings, no others. --ease-decel for entrances, --ease-soft for reading entrances (a page's blocks, cards coming into view), --ease-accel for exits, --ease-standard for state transitions, --ease-spring for emphasis and relevant interactions such as pressing a primary, on spatial properties only
Motion is a material · § Motion
errorchecks 05, 06, 07, 08
L9Light text on primary only on the closed list of ratified exceptions
Clarity before cleverness · § Accessibility · Ratified exceptions
errorreview

Voice attributes

§ 53.1

The voice attributes, each with the grammar that enforces it — active voice, subject before verb, claims tied to evidence.

01

Direct

Active voice. Subject before verb. Say what happened, not what was happening.

02

Specific

Numbers when you have them, scope when you don't.

03

Honest

Name the tradeoffs. Name the dead ends. No mythologizing.

04

Considered

No filler, no flourish. Every sentence earns its place.

Do / Don't

§ 53.2

Voice by example: the same message said in-system and off-system, side by side.

✓Do

"This pattern didn't work in early testing. Users read 'send' as destructive, so we changed the label to 'queue for review."

✗Don't

"Through iterative exploration and rigorous testing, we arrived at an optimal labeling solution that better aligned with user mental models."

✓Do

"Three weeks of work, two releases, one major refactor. The token system is cleaner; the components are not."

✗Don't

"Through a holistic, iterative process leveraging cross-functional collaboration, we achieved a more streamlined and impactful system."

✓Do

"This is v0.1. The Don'ts list is short because it grows with use."

✗Don't

"Excited to share v0.1 of our latest design system initiative — stay tuned for what's next!"

Vocabulary

§ 53.3

The lexicon: the word to use, the words it replaces, and why. Concrete beats inflated everywhere copy ships.

UseInstead ofBecause
ship"roll out", "launch"Concrete, not inflated
cut"reduce", "optimize"Action, not abstraction
wrong"non-optimal"Be willing to be wrong out loud
we learned"key insights surfaced"Who, not what
the version that didn't ship"alternative explorations"Honest about process
it didn't work"results were mixed"The reader can tell either way

For AI agents

§ 53.4

If you're a language model generating copy for surfaces that use this system, four hard constraints:

✓Do · 01

When in doubt, prefer the shorter sentence and the lower register. Restraint is the system's voice. Quiet, then loud.

✗Don't · 02

Write nominal sentence fragments as headlines or pay-offs. Use complete sentences.

✗Don't · 03

Use always, never, or no exceptions as rhetorical emphasis. Use them only when literally true.

✗Don't · 04

Use the words delight, world-class, journey, unlock, supercharge, holistic, synergy, leverage (as verb), or seamless. None of them carry meaning.

Contrast · pairs

§ 54.1

Ratios measured against the actual text/surface pair each token forms in production. Body text clears AAA; brand and signal colors clear AA normal or better. One pair sits below AA by ratified exception — the primary button label and the solid badge, documented below and in the v2.6.0 release.

PairRatioLevelUse
obsidian-100 on obsidian-95016.1:1AAABody text, headings
obsidian-200 on obsidian-90010.5:1AAALede, supporting copy
obsidian-300 on obsidian-9006.3:1AAMetadata, caption, subsection desc
obsidian-050 on primary-500≈2.8:1EXC*Primary button label (ratified v2.6.0)
obsidian-950 on primary-5006.5:1AADefault for every other magenta fill
primary-400 on obsidian-9007.3:1AAALink rest, eyebrow, accent text
primary-300 on obsidian-9008.9:1AAALink hover
secondary-500 on obsidian-95015.8:1AAAFocus ring, secondary signal
success on obsidian-90015.0:1AAAPositive badge, delta up, "do" label
warning on obsidian-90012.1:1AAACaution badge, draft state
danger on obsidian-9006.3:1AAError text, destructive label, "don't"
info on obsidian-90010.3:1AAAInfo badge, neutral signal
obsidian-400 on obsidian-8503.1:1AAControl outline: field, checkbox, radio (WCAG 1.4.11, non-text 3:1)

* Ratified exception, § 6.3: below the 4.5:1 AA bar, kept on perceptual grounds — on high-chroma magenta, near-white separates more cleanly than the higher-contrast dark label. Scoped to .btn-primary and .badge-solid. Under APCA (the WCAG 3 candidate method) the ranking inverts: the light label scores Lc 55.8 vs 47.4 for the dark one — the perceptual model agrees.

Focus · keyboard

§ 54.2

Every interactive element has a visible focus state. The system ships two patterns: a dual-ring for pressable elements (outline + brand halo), and an inline glow for text inputs. Verified against components.css.

SelectorTreatmentContrast on obsidian-950
.btn, .nav-item,
.chip
outline: 2px solid obsidian-100; offset: 3px + --ring-halo16.1:1
.input, .textarea,
.select
Border shifts to primary-500 + --ring-field; --ring-field-danger when invalid6.5:1
.cs-imgBorder primary-500 + wide 48px glow (case-study imagery)6.5:1
.skip-linkOff-screen at top: -100px; jumps to top: 12px on focus6.5:1
.btn:focus-visible
same ruleset
emailyou@studio
.input:focus

Motion · safety

§ 54.3

Under prefers-reduced-motion: reduce, the system ships a global kill-switch plus per-component overrides for hover-transforms that would otherwise feel jumpy. No parallax, no auto-advance, anywhere in the system.

RuleScopeEffect
*, *::before, *::afterGlobal (all elements)animation-duration + transition-duration → 0.01ms !important
[data-fade]Section revealsSkip fade-in — final state painted immediately
.card-mediaMedia tiles on hover and focus-withinStrip transform: scale on both triggers — no zoom
.cs-imgCase-study imageryStrip caption transform: scale on hover/focus
.tab-indicator,
.tab-panel
Tab switcherNo indicator slide, no panel translate — instant swap
.lightboxImage overlayNo transition on open/close
window.__motion gateMotion One — cards, data viz, countersJS animations jump to final state; counters land on target

The floor

§ 54.4

Non-negotiable rules. Any component that can't meet all seven doesn't ship.

01

Color never carries meaning alone. Every status uses color + shape + label.

02

No text below 12px. No body text below 14px. Line height never under 1.4 for running text.

03

Every form field has a persistent label. Placeholder is not a label.

04

Every image has alt. Decorative images carry alt="" explicitly.

05

Focus order follows reading order. tabindex is used for fixes, never for flow.

06

Auto-advancing content is forbidden. Users drive timing — no carousels, no timed dismissals.

07

Touch hit area targets 44×44 on mobile, 32×32 minimum on dense desktop. Extend beyond the visual when needed.

Releases

§ 24.1

Latest first. Each entry documents what was added, refined, or fixed — and where unchanged, says so. Click a row to expand.

MINOR. Pages that arrive gently, a search for the whole system, and an MCP server that checks what a model wrote.

Added

  • check_compliance, the sixth tool of the MCP server. Give it HTML and CSS, or React with Tailwind: it flags colour literals, raw px, durations and easings, --ease-spring on an effect property, classes that look like Amaca and are not in the registry, a second primary and a missing reduced-motion override, each with the token or class that fixes it. The server’s instructions ask a model to run it before handing code over.
  • --ease-soft (0.12, 0.23, 0.5, 1), the curve for reading entrances. It takes the place of --ease-linear, which leaves the set (see Removed); § 06 swaps the Linear tile for Soft.
  • The page entrance (§ Page shell). An active section arrives in three blocks, header, usage and the rest, each rising --s-8 on --d-slow · --ease-soft, one --d-quick apart. It replaces the 8px fade of the whole section.
  • Card rise (§ Card). A card coming into view rises the same way; cards that show together go 80ms apart in reading order, and a stat counts from the frame its card starts.
  • Charts › Stat · one number (§ 44.3). The stat card lives with the charts, and Card § 36.4 points to it. Bars and what follows renumber to § 44.4–44.8.
  • ⌘K / Ctrl+K search on the site. Every section of the sidebar, ranked as you type, from a Search button in the top bar (an icon on phones). Site chrome, not a component: the package does not carry it.
  • Code conventions › CSS: a host’s unlayered reset outranks @layer base. Inside a host, restate the body unlayered, by token. amaca-frontend 1.3.1 teaches it (HTML.md, check H11).
  • Checks 108 and 109. No active text on --obsidian-400; the author’s name only as credit.

Changed

  • Neutral mono labels, placeholders and the chip’s dismiss move to --obsidian-300 so they read as text, not as decoration; --obsidian-400 stays for disabled states and glyphs.
  • The case study Outcome uses the Chart card stat. Each stat is a .chart-card with .chart-big and .chart-delta, one accent at most. .cs-stat, .cs-stat-label, .cs-stat-value and .cs-stat-note are removed and .cs-stats keeps the grid: a copied template replaces the four classes. .chart-big takes --lh-tight and tabular figures.
  • Documentation leads with the MCP server. A card at the top gives the name, the URL and the sign-in for a custom connector; the shadcn MCP server is no longer suggested. § 03.3 names model tiers, not models, and sends the check to check_compliance.
  • npm publishes through Trusted Publishing. GitHub Actions exchanges an OIDC token for each publish: no stored token. The actions run on v7 and Node 24.

Fixed

  • Demos use invented names; the author’s name appears only as credit.
  • § 03.4: the utilities line wraps on phones instead of being clipped.
  • The Anatomy and code card keeps one height: the Anatomy view sets it and Code scrolls inside, so switching views no longer moves the page.
  • The example in the Anatomy and code card is inert: no hover, focus or click changes it, so it always matches the tokens named beside it.

Removed

  • --ease-linear. One use in the whole system, the Plot’s area fade, which now rides --ease-soft. A continuous loop (the Loader, the Skeleton shimmer) keeps the linear keyword, as it already did: a loop with no curve is not a choice of curve. An outside stylesheet that reads var(--ease-linear) falls back to the browser’s default timing: write --ease-soft for a fade, linear for a loop.

Unchanged

  • No other token changes its value.
RELEASED · 2026-10-19 LIVE · amaca.design SPEC · design.md COMPARE · v5.1.0…v5.2.0

MINOR. Amaca as a remote MCP server: a chat app that takes a server only by URL can now read the system.

Added

  • A remote MCP server at https://www.amaca.design/mcp. Streamable HTTP, stateless, read-only, no key. Five tools: list_components; get_component, the contract in meta.amaca, the CSS and one example; get_tokens, tokens.css or the DTCG file, whole or one family; get_design_section, one section of DESIGN.md by title or § number; get_install, the tool guide. It answers from the files of the deploy that serves it, so it is always the published release. Add it as a custom connector in Claude, with claude mcp add --transport http in Claude Code, or by URL in Cursor and VS Code; the Claude Code plugin now declares it, so installing the plugin brings the skill and the server together. The shadcn MCP server stays the way to install components into a project.
  • § 03.1 step 12, “Connect any agent, by URL”, and its line in the tool guide of llms.txt and llms-full.txt.
  • Check 107: the server’s tests pass (tests/mcp.test.js, run with node --test), Vercel bundles every file it reads, and the site gives its URL.

Unchanged

  • No token, component or rule changes. The stylesheets are the v5.0.0 files.
RELEASED · 2026-10-19 LIVE · amaca.design SPEC · design.md COMPARE · v5.0.0…v5.1.0

MAJOR. One page per component, every value on a scale, and a system an agent can install: the contract, the site and the packages now say the same thing in the same order.

Breaking

  • **--magenta-* is renamed --primary-*, no alias.** The Tailwind utilities follow (bg-magenta-500 → bg-primary-500). A computed-style diff against v4.3.2 on 10,165 elements found no paint change.
  • The case study template changes its markup. Hero title and lede, section title and body, figure, stat cards, note and More cases carry their own classes (.cs-hero-title, .cs-section-body, .cs-figure, .cs-stat*, .cs-note*, .cs-card*); the 16:9 placeholder figure and .cs-img-caption are gone.
  • Section numbers changed. The site reads in menu order, one section per component, § 3 after it; a "§ N.M" reference written against v4.3.2 points to the same content under its new number.

Added

  • Components: Link, Modal, Avatar, Icon (an album of 50, outline and solid), List, Pagination, Slider and range, Divider, Skeleton, Popover, Drawer, the top bar and the floating bar, Menu and Slider in Select, Swatch, Stepper and Progress as HTML, Segmented control, row and feature Cards, sortable Table headers, a pressed state on every Button.
  • A page per component, in one order: Usage, the Anatomy and code card (written by release/kit.py), Rules copied from the contract (release/rules.py), the variants by axis, Do and don't. Every component section carries a Rules. block and a Variants. block.
  • Install paths: the npm package amaca-design (also on jsDelivr), published on the release tag; a shadcn registry with one item per component, its contract in meta.amaca; MCP through the shadcn MCP server with the @amaca namespace.
  • AI-readable: llms.txt and llms-full.txt open with "Which file for which tool", and a model asks for the tool before recommending a file.
  • Tokens: --s-px and --s-0-5, border widths, focus, icon and target sizes, fluid headings, surface and border roles, three breakpoints, three materials, --primary-950, --ease-linear; a size index and a state index with an Exception column.
  • Guidelines: Usage per component, ranked principles, the law and conventions registries; Browser chrome on the Layout page, the scrollbars included.
  • Tints and opacity: --tint-faint, --tint-glow, --tint-strong, --opacity-disabled and --light-hero; every alpha in components.css is a token colour times a tint.
  • Checks 51-106. Among them: every value a token (51), breakpoints (53), the kit card (99), the Rules (100), the shadcn registry (101), every declared variant shown (102), the npm package (103), the tool guide (104), the Overview counts (105), no rgba() or loose opacity in the stylesheet (106).

Changed

  • --ease-standard is an ease in-out (0.45, 0, 0.55, 1) for state changes; it was an ease-out.
  • Charts: data palettes on tokens, labels in HTML over the drawing, a second drawing for small screens, choreography on tokens; Chart card and Diagrams get their Anatomy card.
  • The case study template follows the portfolio that runs it: one reading column, real figures under a caption, outcome stat cards, a closing note, More cases as linked Cards; the animated mark stays as the hero's placeholder.
  • How to install recommends the plugin for Claude Code and the editors, and AI-INSTRUCTIONS.md for chats and app builders; the Tailwind install is npm install amaca-design with @import "amaca-design/theme.css".
  • amaca-frontend 1.3.0 and amaca-figma 1.6 read the Rules and Variants blocks.
  • The feature card’s tab takes the light label (--obsidian-050 on --primary-500), the third selector of the ratified pair; Color shows the tint scale.

Fixed

  • Control outlines reach 3:1 on --border-input (fields, checkbox, radio were 1.18:1 and 1.41:1).
  • The law card and the sidebar indicator move on tokens only; a raw cubic-bezier in components.css now fails check 07.
  • No page scrolls sideways; the sidebar indicator travels from the last position across groups; the typing dots are written once; the motion index carries every row word for word.
  • The lightbox is an overlay like the others: the page behind it on --scrim and --blur-veil, the figure on --sh-4, its close button on the obsidian ramp; a case study figure glows on --sh-glow, and its Expand hint keeps the icon size.
RELEASED · 2026-10-18 LIVE · amaca.design SPEC · design.md COMPARE · v4.3.2…v5.0.0

PATCH. The anatomy, the example and the zoom the site already draws, now in the contract — and the trigger reduced motion had missed.

Added

  • § Card › media — .card-media between micro-header and title: --s-2 / --s-5 margin, --r-md, overflow: hidden, one full-width child.
  • The § Card example carries the media, as the first § 12.1 card does.
  • Motion rows for the media zoom — hover and focus-within, scale 1→1.05 and back on --d-slow · --ease-decel, none under reduced motion. The zoom stays inside the clip.
  • Checks 49 and 50 — a reduced-motion override covers every trigger of a transform; every !important is in a reduced-motion override or in the ratified list.

Changed

  • Four !important exceptions ratified — .card-media > *, .doc-row, .spec-card, .btn.is-loading, each with what it outranks.

Fixed

  • Reduced motion stops the keyboard zoom too — tabbing to a card’s action no longer scales its media when motion is reduced. Nothing changes for anyone else.
RELEASED · 2026-09-26 LIVE · amaca.design SPEC · design.md COMPARE · v4.3.1…v4.3.2

PATCH, with a visible change taken by decision. The badge contract says everything the demo draws, and the last raw values in the controls go on the scale: the nearest token, and at equal distance the smaller.

Changed

  • No raw-by-design list — fields, select options and choice controls move from 14px to --t-small, their 10px to --s-2, the 36px reserves to --s-8. A field keeps its 40px, now declared as min-height: var(--s-10) like the button. The badge takes --s-2 --s-4 padding, a --s-1 gap and a --s-1 dot; the chip dot follows.

Added

  • An Info badge in § 13.1 — declared, never shown until now.
  • Checks 47 and 48 — the § Badge example matches its variant table, and every demo badge has a state-index row.

Fixed

  • The § Badge example is the § 13.1 demo, badge for badge, and the default row names padding, gap and dot.
RELEASED · 2026-09-24 LIVE · amaca.design SPEC · design.md COMPARE · v4.3.0…v4.3.1

MINOR. A reader of the whole file, set side by side with the CSS, read v4.2.1 and listed nineteen places where the file disagreed with itself or with its CSS. The Select ones had already shipped in v4.2.2. The rest follow here, under one rule: where the contract and the site disagree, the site wins and the contract is rewritten.

Changed

  • The badge contract describes the badge this site draws — its own register, Live on brand, Archive on the default, the dot on every status badge. Not one pixel of the badge changes.
  • § 6.3 names .badge-solid beside .btn-primary — the solid badge has always worn the light label on magenta; the ratified exception now covers it by name.

Added

  • Four tint tokens — --tint-fill / --tint-edge for the variants, --tint-fill-deep / --tint-edge-deep for the default badge. Thirteen hand-written rgba pairs are gone, at the same values.
  • State-index rows for the card and every badge variant.
  • checked and expanded join the closed state vocabulary, with check 42 holding the table to the list.
  • Examples a generator can read — helper and error text, an input with an icon, a textarea, a native select, and a card with a badge and an action.

Fixed

  • The status demo lost three inline styles left from v1.1.4: Default now shows its class, Pending the warn fill.
  • The error demo followed its own rule — aria-invalid and a linked helper, no inline paint.
  • The switch track carried a stroke in the state index it never had on the site — the family’s default row now splits, .switch is fill-only. The checkbox corner and the focus outline now match the CSS.
  • A Select menu that fits no longer scrolls — its height is measured and rounded up.
  • The cache-key check reads only <link> tags.
  • The contract now says what the site draws — select trigger focus, chip tints, danger border, menu surface, badge-solid in § 6.3; textarea placeholder follows the field. Checks 44–46 hold the state index to the stylesheet.
RELEASED · 2026-09-24 LIVE · amaca.design SPEC · design.md COMPARE · v4.2.2…v4.3.0

PATCH. No token and no component contract changed. The Dropdown / Select contract has asked for a viewport-aware menu since v2.8.0, and the demo on this page never did it.

Fixed

  • The custom listbox follows its placement rule — fixed to the trigger, below by default, flipped above only when it does not fit and above has more room, height clamped to the space with internal scroll, width clamped to the viewport. The first open lands on the selected option, Tab closes the menu, and a page scroll or a resize closes it while an internal scroll does not. Every rule was measured in the browser.
  • A transformed ancestor broke position:fixed — the section's entrance animation leaves a transform, so the first fixed menu opened hundreds of pixels away. The script now corrects for it, and the contract names the trap.
  • The native variant had no demo — § 11.1 now shows a native select beside the custom one.

Added

  • The native picker follows the same rule where the platform allows — with appearance: base-select (Chromium) it opens below and flips above only when clipped. Elsewhere the OS popup stays.
  • Home / End in the listbox, and check 41: every lettered variant has a demo, and the Select demo carries the rules its contract names.
RELEASED · 2026-09-24 LIVE · amaca.design SPEC · design.md COMPARE · v4.2.1…v4.2.2

PATCH. No token, component or rule changed. The site was loading the new stylesheets under old URLs.

Fixed

  • v4.2.0 shipped under the v4.1.0 cache keys — this page still loaded components.css?v=82 and tokens.css?v=27 after both files had changed. A returning visitor kept the old hover. A browser holding the old tokens.css next to the new components.css had the three ring tokens undefined, so the focus rings disappeared. A fresh browser has no cache, so no render could show it.
  • ?v is the file’s content hash — the first 8 hex of its sha256, written by release.py bust. It changes exactly when the bytes change, and check 40 recomputes it, so a stale key cannot ship.
RELEASED · 2026-09-24 LIVE · amaca.design SPEC · design.md COMPARE · v4.2.0…v4.2.1

MINOR, with one rendered change shipped by decision. A rendering of the contract showed the Button’s loading dot unlit. That was faithful to the source: this site rendered a button in loading, and the contract never named the state. Reading the state index to fix it turned up a second problem. The primary’s hover ring read magenta bloom, a phrase with no value behind it, while the CSS drew a ring the spec had never stated. The grammar said tokens only, and no check read the cells.

Added

  • Focus rings are tokens — --ring-halo, --ring-field, --ring-field-danger. They carry the same values as the two literals named in v3.4.0, plus the error twin, which was never named, so nothing renders differently. The literals had been re-typed in twenty CSS rules, nine agent snippets and two skill references; all of them now use var(). Tailwind reaches them as shadow-ring-*, and the DTCG projection carries them.
  • loading is a declared Button state — it gets a row in the state index, a place in the Button’s state list and a loading button in its example. The composition stays with the Loader, and the Button points to it.
  • Checks 37–39 — check 37: every value cell of the state index is a value, not a description. Check 38: no file re-types a ring or glow token, or draws the same ring at another alpha. Check 39: the states a component lists match the index rows and the .is-* states the CSS renders. Each was mutation-tested and finds v4.1.0’s drift.

Changed

  • The primary hover is --sh-glow over --sh-3 — it replaces an undeclared 4px ring at 0.18, so the hover renders differently. It ships in a minor by decision, on the v4.1.0 precedent: nothing had been declared for a consumer to depend on. The order was measured. The first shadow paints on top, and with --sh-3 first its drop dimmed the bloom to about half.
  • The state grammar names its value cells — a value cell holds a token, —, native, none or transparent, and never a description. The danger hover’s (brightened) now reads filter: brightness(1.05) under Other.

Fixed

  • The error demos drew a different ring than the CSS — three inline styles and two runtime handlers painted the error glow at 0.12 against the 0.15 the CSS draws. They now read the token.
  • The skill follows — amaca-frontend v1.2.2 references the ring tokens, and its React verification rule no longer tolerates an rgba() halo.
RELEASED · 2026-09-24 LIVE · amaca.design SPEC · design.md COMPARE · v4.1.0…v4.2.0

MINOR. A projection of the contract rendered this system with the Chip cell empty — correctly, because the contract said site-only. An empty cell in a projection is the cheapest way to learn that a component the system needs was never declared. Writing the spec then surfaced two more things the file was wrong about: a state it used without naming, and a class it taught after removing it. Three surfaces, one failure — the inventories were guarded, the sentences about them were not.

Added

  • Chip, a component at last — the registry taught that .chip was doc-site chrome, “a colour swatch on the doc site, not a chip/tag component” — right about the class, wrong about the system. It is now a contract: six variants on the Badge’s semantic set and its two tint strengths, an optional dot, an optional dismiss, states from the state grammar, motion rows in the index. The doc-site swatch is renamed .swatch-chip. A chip carries a value a person put there; status the system computed stays a .badge. At most one affordance — pressable or dismissible, never both, because two 44×44 targets do not fit in a 28px object.
  • The state vocabulary gains selected — legal where an element carries a persistent choice the reader made, always mirrored by aria-selected or aria-pressed, never a synonym for active. Two releases late: the state index already carried it for the date-picker day and the select option, so the sentence claiming a closed list was false about its own table.
  • Composite values are reproduced verbatim — AI-agent rule #9 and check 29. A gradient, a shadow or a font stack is a value with parts; copying its shape instead of its content invents a value rather than simplifying one.
  • Icon-only button, .btn-icon — a square at its size’s height, aria-label mandatory, a tooltip when the glyph is not universal. The icon-and-label anatomy the site has drawn in § 10.3 is declared with it.

Changed

  • Button sizes are declared, and rendered: 32 · 40 · 48 — the spec said 32 and 44, this site labelled 32, 40 and 48, and the CSS rendered 35, 51 and 60. Heights now sit on --s-8, --s-10 and --s-12, and every button reaches 44 on a coarse pointer. A rendered change shipped in a minor by decision: no stated size matched the rendered one, so none was there to depend on.

Fixed

  • The changelog slice reads the heading, not its citation — the harness anchored the changelog with a naive string find that matched the citation inside the release checklist, so three releases of structural drift were validated instead of caught.
  • The Chip shipped without a demo — canonical in the contract and in the CSS, absent from this site: not one chip on the page. It now has three demos in § 13, renamed Chips & Feedback — variants, filter chips, removable chips — and the inset that was meant to grow the touch target carried pointer-events: none, so it grew nothing. Only the targets grow now: the pressable chip and the dismiss. Check 33 holds every canonical component to a demo.
  • A sidebar label fits on one line — Badges, Chips & Feedback wrapped to two lines in the nav. The rule is now in the contract: at most two nouns joined by &, at most 16 characters — the length of the longest label that already fit. Check 34 holds every label to it.
  • The Button section never pointed to its states — the state index carried .btn | disabled and the CSS implemented it, but the component section stopped at variants, sizes and focus. A reader of that section alone — a generator — concluded the disabled button did not exist. It now points to the state grammar as Input does, and its example carries a disabled button. No value changes.
  • The snippets agents copy verbatim had drifted from the CSS they quote — every “reuse these verbatim” block in the rules files and llms-full.txt disagreed with components.css: a --r-md button the CSS draws round, a --magenta-600 hover the ratified exception forbids, no disabled state, the wrong input surface, and .field-label, a name v4.0.0 removed from one copy and not the other seven. The snippets now quote the CSS rule for rule; check 30 holds them to it.
  • The skill taught a different button — amaca-frontend’s React, HTML and Figma references drifted from the state index on the disabled fill, the label, the hover, the radius and the focus halo, and listed states the index does not carry. Realigned, with the restated lists replaced by pointers to the index.
  • The skill’s version went backwards — v1.2.0 shipped with v4.0.0 and the README never recorded it, so this release first stamped v1.1.11 while the site still read v1.1.9. The skill ships as v1.2.1; check 31 holds its five stated versions to one, and check 24 now sees the copy the IDE zip carries unpacked, which sat one version behind.
  • tokens.css erased Tailwind on every button — its element reset sat unlayered, and an unlayered rule beats every layered one, so a button built from theme.css utilities rendered transparent and borderless. The base rules now live in @layer base; the site renders identically, measured element by element. Check 32 keeps them there.
  • The CSS never carried its version — the versioning rules said both stylesheets stamp it in their leading comment; neither ever did. The rule now says what is true: the release tag versions the CSS.
  • .info-strip leaves the contract — v4.0.0 removed the class and said so, while the registry kept its row and the Alert section still told the reader it works — and components.css still shipped the rule. It is gone now, as announced; check 10 reads only the registry, which also added the missing .dp-range row.
RELEASED · 2026-09-23 LIVE · amaca.design SPEC · design.md COMPARE · v4.0.0…v4.1.0

MAJOR. The first real run of the installed plugin put the contract in front of a generator: it declared two divergences between the Card spec and the CSS, and a human eye found the third — an anatomy the spec did not have, so the generator invented spacing. A DTCG audit the day before had found seven tokens missing from the projection. Same lacuna from three angles in two days: the inventories were guarded, the projections and declarations were not.

Breaking

  • The card renders the surfaces the spec always declared — background --obsidian-800, border --obsidian-700, hover --obsidian-600 with --sh-2. The token table already said --obsidian-800 → Card; the CSS had shipped one ramp step darker, shadowless, since the section was written. Decided on a measured render, not on token names.
  • .card-header becomes .card-meta, no alias — the spec taught a class the CSS did not know, so a generator following the contract emitted an unstyled div. The spec name wins, cleanly.
  • The mono-label register collapses to --t-caption — a de facto 11px register lived off-scale in 21 CSS rules and 46 site spots. Chart-head titles and the brand name (14px) join --t-body; the chart card adopts the card ramp; skip-link and pie-legend geometry collapse to scale steps.
  • .info-strip is removed — the v3.4.0 promise, removal scheduled for v4.0.0, is kept.

Added

  • Seven promotions — Page shell, Grid system, Breadcrumb, Skip link, Chart card, Pie, Brand mark: every css-only row cleared at its milestone, each promotion an audit, and each audit found something — a sticky topbar on a raw z-index, a brand glow in a color no token declares, a skip jump that did not clear the topbar it exists to skip, a section entrance running under reduced motion. Fixed with the promotion that found them.
  • The debt came due, and cleared — 126 exact-parity px literals now read their token; six rules left their demo captions and became framing prose, verbatim; 41 subsections gained the prose that says what the thing is for. Zero findings in declared debt, for the first time since the mechanism exists.
  • The DTCG projection is contractualized — the seven z-* tokens enter tokens.dtcg.json, the file stamps its own release, dtcg joins deploy_targets, and the spec declares the direction: a generated projection of tokens.css, never edited by hand.
  • Checks 25–28 — the DTCG round-trip; every class the spec emits exists in the CSS and declared surfaces match it; every id unique; every rgba speaks a token’s color. Each born from the drift it now prevents; all four mutation-tested before first use.
  • The skill verifies its own output — amaca-frontend v1.2.0 ships scripts/verify-output.py: deterministic, stdlib-only, reading the closed lists from the bundle’s own spec copies. Output is deliverable only on exit 0. A control described in prose is not a control; now it is executable.

Fixed

  • Dead names the spec taught — .badge-live/.badge-draft and .field-label, examples and live site spans rendering unstyled, one of them styled in llms-full.txt by a rule the CSS never had. All aligned to the shipped names; check 26 makes the class unrepeatable.
  • The stat count-up rode a curve the rule does not name — hardcoded 1200ms and easeOutCubic against a RIGID rule declaring --d-draw · --ease-decel. It now reads the duration off :root at run time and solves the declared bezier exactly.
  • The pie demos pasted hex — 24 attribute literals, all token values written by hand, now var() styles with role="img" on both charts.
  • id="main-content" was duplicated — the skip target was ambiguous, and the jump did not clear the sticky topbar. One id, on the content wrapper; check 27 guards uniqueness.
RELEASED · 2026-08-29 LIVE · amaca.design SPEC · design.md COMPARE · v3.5.0…v4.0.0

MINOR. No token changed. Cursor started reading agent skills in January and the IDE download never caught up, so it shipped the constraints without the procedure. Fixing that led to three pieces of drift in this repo that every gate reported green: the version line the spec calls canonical, a download button named after a file deleted three releases ago, and a debt mechanism whose deadlines never arrived.

Added

  • The agent skill reaches the IDE — since Cursor 2.4 the Agent Skills standard is a first-class input, read from .cursor/skills/, .agents/skills/ and, for compatibility, .claude/skills/ and .codex/skills/. amaca-ide.zip now carries amaca-frontend unpacked under .agents/skills/amaca-frontend/ — the vendor-neutral path, so one folder serves Cursor and Codex alike. The spec is not duplicated inside it: the bundle already ships DESIGN.md at root, and a second copy is a second thing that can drift. The three wirings compose rather than compete — the paste-in, the scoped .mdc layer, and the skill, which carries the multi-target workflow a rules file structurally cannot.
  • Check 21 — the document's own version line — check 14 is called one version, five places. The spec calls a sixth place the source of truth, and no check covered it. Now one does, comparing the version and its date against the frontmatter.
  • The skill becomes installable — amaca-plugin.zip — Agent Plugins 1.0 was published on 6 August by Amazon, Cursor, Microsoft, OpenAI, Vercel and Google: one vendor-neutral way to package agent skills and MCP servers, read by Cursor, OpenAI Codex, GitHub Copilot, VS Code and Kiro. Anthropic is not at that table and Claude Code keeps its own format — but the two manifests live at different paths and never collide, so this is one package with two manifests and a single skill inside, not two packages. Verified, not assumed: claude plugin validate --strict passes clean on the emitted artifact, and it does not see the root plugin.json. The spec ships inside the skill here, in one copy — unlike amaca-ide.zip, whose root is your repo; a plugin root is an install directory nobody browses. CLAUDE.md is deliberately absent: Claude Code fails --strict on it, and says why — context belongs in a skill.
  • The repo is now a plugin marketplace — /plugin marketplace add angelomacaione/amaca-design, then /plugin install amaca-design@amaca. The entry installs from the published zip over HTTPS with a SHA-256 pin — no npm, no clone. The pin is not decoration: Agent Plugins defines a package format and leaves distribution, provenance and trust explicitly outside it, so verifiable integrity is the publisher's job. Check 22 recomputes the digest against the bytes on disk, because a hash that nobody re-checks is a hash that goes stale.

Fixed

  • The contract declared itself two minors behind, for two releases — the version line read 3.2.0 while the frontmatter and the whole site read 3.4.0. Every stamp the harness could see was correct; the only one it could not see was the one the spec calls canonical.
  • Declared debt never came due — the gate suppressed a check whenever a debt was declared, and never compared the release it named to the current version. A debt dated v3.5.0 would have stayed silent at v9.0.0. The mechanism described as declared and dated was only declared — the date was decorative, and 71 open findings would never have surfaced on their own. Deadlines now expire: at or past the release it names, a debt blocks like anything else. Deferring stays legitimate; it has to be done on purpose, by moving the date.
  • The release checklist stated its own number of checks, and the number was wrong — step 0 read nineteen against a harness of twenty-one, and this release added three more, so any figure written there would have rotted again within the same version. Step 0 no longer states a count: the harness prints it.

Changed

  • The IDE download card and its button — the card read .cursor/rules/amaca.mdc and the button was labelled amaca.mdc, a file replaced in v3.1.0 by the core / html / react split. The site had been offering a download named after a file that had not existed for three releases.
  • llms.txt indexed three AI-tool entry points out of seven — CLAUDE.md, the paste-in, the amaca-frontend skill and the IDE bundle were all missing. An index that omits four of the surfaces it exists to advertise teaches a retrieving agent that they do not exist.
  • Debt re-dated to v3.6.0 — the seven css-only registry rows, the teaching-grammar note, and checks 03, 17 and 18. Deliberate deferral, not drift: with the expiry fixed, the new date is a real deadline.

Trigger

A competitive watch found that Cursor had changed its primary input shape in January and nobody noticed, because every run re-read the page about rules — which by construction cannot mention a format arriving beside it. Checking what we actually emit against what Cursor actually reads deflated the alarm and left one real gap. Pointing the same reflex at this repo — verify the artefact, not the description of it — found the other three. All of them had the shape this system knows best: no symptom, and a gate reporting green.

RELEASED · 2026-08-26 LIVE · amaca.design SPEC · design.md COMPARE · v3.4.0…v3.5.0

MINOR. The token rule works because the token list is closed. Components had no equivalent list, so a model couldn't tell "not covered" from "not yet written" — it invented. This release closes the component inventory, gives states the same fixed grammar motion got in v3.3.0, and documents seven components that were either shipped-but-invisible or missing entirely.

Added

  • Component registry (§ 3.0 of the spec) — the closed inventory of components, with a four-state vocabulary: canonical (spec written) · css-only (shipped in CSS, spec owed, milestone named) · off-system (stop and ask) · site-only (documentation chrome, not the contract). A component not in the table is off-system. This is what makes the token rule executable one level up — a generator can now detect non-coverage instead of inventing.
  • State grammar — one fixed six-column schema for every interactive component (state · background · border · foreground · ring · other) with a closed state vocabulary and two RIGID rows: focus-visible is mandatory for anything focusable, error is mandatory for every form control. The state index aggregates every ratified row.
  • Stacking scale — seven named layers, from --z-sticky to --z-max. The values name the system as built — nothing was renumbered. A raw z-index of 10 or more is now greppable as a violation.
  • Duration by distance (RIGID) — perceived velocity is distance ÷ duration, so a component whose content varies by an order of magnitude cannot have one honest duration. Three buckets, three existing tokens, no computed values: under 120px rides --d-base, 120–600px --d-slow, over 600px --d-scene.
  • Teaching grammar — the closed list of the four surfaces a component is documented on: framing prose, Do / Don't, demo caption, contract card. Written after an audit found five competing grammars and no rule saying which to use — 41 of 90 subsections with no framing prose, Do / Don't in 6 sections of 24, and the demo caption doing two incompatible jobs. RIGID: a rule never lives in a demo caption, and a demo with no choreography gets no replay button.
  • Browser chrome — scrollbar, ::selection and caret-color promoted from the CSS into the contract, with a closure clause and one ratified exception. The scrollbar had been specified since v1 and had zero occurrences in the spec.
  • Date picker — the most complex component in the system, shipped since v2 and invisible in the contract until now — full anatomy, the closed day-flag set, range with two months, presets, year jump.
  • Table, Checkbox, Switch — shipped in CSS, never documented. Now canonical, with state matrices.
  • Radio, Alert, Toast, Tooltip — four components that existed nowhere. Radio completes the choice-control family (square with a tick, circle with a dot — shape carries meaning before colour). Alert never tints its fill, and is not dismissible at all — not on a timer and not by the reader: a close button would let someone hide a state that is still true. If the reader must be able to make it go away, it is a toast. That is the line between the two components. The icon labels the title, so a titled alert drops its body to a second row aligned with the icon rather than hanging indented off a short label. Toast auto-dismisses only when it carries no action and no error, and enters and leaves on the side it lives on — 24px from the right, the exit retracing the entry on the same axis, differing in speed and not in path. Tooltip opens on hover and on focus, holds no interactive content, and is never the sole carrier of meaning — and at most one is open at a time: opening one closes every other, since two overlapping bubbles make both unreadable.
  • .btn-outline — the fifth button variant, shipped in v2 and never listed in the variant table.

Changed

  • Input states — three prose bullets replaced by a state matrix. Error is marked with aria-invalid="true" and the border follows from it.
  • Focus rings are now named — pressable halo and field glow, so the state grammar cites them instead of re-typing the rgba literal on every row. The choice controls join the pressable list.
  • .info-strip → .alert — the un-specced ancestor becomes a deprecated alias. Still works; removal scheduled for v4.0.0.

Fixed

  • --font-mono was never a monospaced stack — tokens.css carried a copy of --font-sans while the spec and the DTCG file both declared a real mono stack, a correction applied to the export in v3.1.0 and never propagated to the CSS. The system ran on two typographic identities: 134 token references rendering Satoshi, 38 hardcoded stacks rendering real mono. The CSS was right — Amaca is a single-typeface system and the mono register is Satoshi plus letter-spacing, uppercase and tabular figures. Spec, DTCG and the 38 stacks all realigned; a real monospaced stack is now off-system.
  • One duration across a 34× range of distances — the § 15 accordion panel is 60px, the § 24.1 changelog panel is 2037px, and both rode 600ms from the same rule: 100 px/s against 3395 px/s. They read as different components because they were moving at different speeds. Accordion panels now pick their duration token by measured height (§ 08.3) — 206 px/s and 1299 px/s, from the same closed scale. Only the choice is computed; the value is always a token.
  • The loader inside .btn-primary was dark while the label was light — the dark silhouette was ratified when the primary label was still dark; § 6.3 flipped the label to near-white two releases later and the loader was never revisited, so the two halves of the same button carried opposite treatments. The rule behind it is now written: anything composed inside .btn-primary wears the label's colour.
  • 704 <code> elements ran on the browser's monospaced default — only two were ever styled, so every other inline fragment fell back to a real monospaced typeface. The § 11.2 rules card showed both, a line apart. code, kbd, samp, pre now ride the mono register in the reset.
  • The composer's staged state was two rows tall for one line of text — rows="2" left the text at the top and pushed attach and send to the bottom, so the two side-by-side demos didn't match. Shipped that way since the composer landed.
  • var(--brand) does not exist — 13 uses — the digit labels in the time and date input rule cards referenced an undeclared token. The fallback was a plausible grey, so it never read as broken.
  • 153 raw px in inline styles — swapped to tokens at exact parity. 115 off-scale literals remain, scheduled with the teaching-grammar debt.
  • The input error state had no CSS — the spec has promised a danger border since v2.0.0; only the helper text was ever implemented.
  • Checkbox and switch had no focus ring — both shipped without :focus-visible — an accessibility floor violation in the two most-used form controls after the text input.
  • Raw hex, eight survivors — including two #fff that contradicted the no-pure-white rule fixed for ::selection in v3.3.0. components.css now contains zero raw hex.
  • Motion token pairs, seven survivors — transitions carrying a duration with no easing half. The two transition: all on the choice controls are now explicit property lists, so the Spatial-vs-Effects rule is checkable per line.

Trigger

An audit against v3.3.0 found three classes of failure with one root cause. Spec that existed but was invisible in the contract (the scrollbar, specified in CSS, zero mentions in the spec). Components shipped in CSS and never documented (Date picker, Table, Checkbox, Switch) — the model reinvented them though a canon already existed. Components that existed nowhere, so the model invented from scratch. Writing the registry surfaced the second finding: the audit that closes the inventory is also the audit that finds the drift — eight raw hex, seven half-written token pairs, two missing focus rings, and an error state documented for four versions but never implemented.

RELEASED · 2026-08-05 LIVE · amaca.design SPEC · design.md COMPARE · v3.3.0…v3.4.0

MINOR. The motion documentation becomes homogeneous: one fixed grammar for every component that moves, two new RIGID rules, and the anti-drift net extended to catch what it couldn't see. Shaped by a comparative research pass across M3 Expressive, Figma, Carbon, Atlassian, Fluent 2, Polaris and the DTCG standard. No token value changes.

New · Motion grammar (§ 08.3–08.4)

  • Spatial vs Effects (RIGID) — every animated property has a type, and the type gates the easing: spatial (transform, position, size, shape) may ride any curve including --ease-spring; effect (color, background, opacity, shadow) never overshoots. Mechanically greppable, per line.
  • Trigger-based pairing — user-triggered states ride --d-quick · --ease-standard; system-triggered transitions ride --d-base · --ease-decel. The trigger decides, not taste.
  • Per-component motion schema — one fixed four-column table (trigger · property+type · token pair · reduced-motion, closed vocabulary instant/fade-only/none) replaces prose specs. Card, Accordion, Tabs, Lightbox, Chat, Loader and Diagrams migrated; § 16.4 and § 18.5 converted to the canonical columns.
  • Motion index — the aggregate of every ratified per-component spec, in § 08.4 and DESIGN.md § Motion: the system's whole choreography in one table.
  • Continuous loops (ratified class) — loops run while mounted, on per-component literal clocks, linear/steps only, each ratified by its own index row (brand mark, fallback ring, label cycle, shimmer, caret). The raw-duration grep exception is now a closed list.
  • § 08.3 Easing tokens — the four curves get their own table on the site (they lived only in DESIGN.md and the demo), each with intent annotation on the row.

Changed · Enforcement

The grep audit, the anti-patterns table and AI-agent rule #6 now catch raw ms durations (ratified loops excepted) and --ease-spring on effect properties. Propagated to every download surface: AI-INSTRUCTIONS, CLAUDE.md, AGENTS.md, Copilot/Cursor rules, llms-full.txt; amaca-figma reaches v1.4 (check 8 gains the Spatial/Effects RIGID); the amaca-frontend bundle re-bakes with the pairing section and new checks (H8, R10, F9–F10), and FIGMA.md v1.1 maps the motion scale to Figma's native Timing and Easing variable types.

Fixed

  • Vercel Web Analytics — the tracking snippet (PR #9, May 12) was lost in a later index.html rewrite; the site shipped without it and the dashboard read zero. Re-added before </body>.
  • Token drift, three survivors — [data-fade] rode a raw 600ms → --d-slow; the law-card bar rode a raw bezier → --ease-standard and a 1100ms clip-path → --d-draw, closing the last off-scale draw-on literal the v2.7.0 audit had missed.
  • Changelog renumbered to strict SemVer — v2.4.1 → v2.4.0, v1.2.6 → v1.2.0, v1.1.4 → v1.1.0: the first release of each line is now X.Y.0, consolidation notes and COMPARE links updated. The public sequence has no skipped numbers.
  • § Overview metadata stamp — read Version 3.1.0 · Jul 6 while the system shipped 3.2.0: realigned, and the release checklist in DESIGN.md § Versioning now names it explicitly so it can't drift again.

Trigger

The foundations lived in § 08 but per-component motion rules were scattered across component sections with no uniform grammar — the exact pathology a machine-readable system can't afford. A verified research pass over how the leading systems structure motion (spring schemes, composite tokens, intent annotations, kill-switch contracts) shaped the schema; the strongest patterns — Atlassian's intent bundles, M3's spatial/effects taxonomy, Polaris's per-token guidance — landed as Amaca rules.

RELEASED · 2026-07-17 LIVE · amaca.design SPEC · design.md COMPARE · v3.2.0…v3.3.0

MINOR. The system reaches the Figma canvas: an in-canvas custom skill for the Figma agent, on the Agent Skills single-file standard. No token, value, or component contract changed.

New · Figma agent skill — /amaca-figma

  • Two entry points — generate builds on canvas binding Amaca-named Variables; audit lints a selection on canvas evidence: bindings, token existence, the 85/10/5 budget, contrast pairs, the component registry, type and spacing scale.
  • Verify loop — audit runs after every generate; repairs target only the flagged nodes, capped at two passes. A finding that survives a repair is treated as a system gap and routed to the gap protocol, not another loop.
  • Fetch-first, fail-stop — the skill reads /llms-full.txt before any action and refuses to generate from memory. It never invents a hex, px, or easing.
  • Install — in a Figma file: prompt box → Skills → Add skill → upload amaca-figma.md → invoke /amaca-figma. Ships as downloads/amaca-figma.md + zips/amaca-figma.zip.

New · Motion rule — stat count-up (RIGID)

Numeric stats and KPI values count up from zero to the target on entrance (--d-draw · --ease-decel) and land exactly on the final value; under reduced motion they jump straight to it. Already the site's counter behavior — now normative in § 08 for every deploy target, taught by the amaca-frontend targets (v1.1.9) and audited by /amaca-figma (check 10).

Changed · downloads grid (§ 03.0)

  • Hero swap — the Figma card takes the second hero slot; AGENTS.md moves to the top of the AI integrations column. Its bundle is unchanged.
  • Builders card — Figma Make hands off to the dedicated Figma card; the row now reads v0 · Lovable · Base44. Same paste-in bundle (amaca-appbuilder.zip), contents unchanged.
  • § 03 instructions reconciled — the install rows now mirror the hero cards: the Figma row teaches the in-canvas skill (install like Claude, invoke /amaca-figma) and a Google Stitch row is added; the IDE row reflects the v3.1.0 core + per-target layout; the spec-contents table speaks the design.md grammar; versioning cards refreshed (CURRENT v1.1.9); subsection numbering closed (03.1–03.5, was 03.2–03.6).
  • tokens.dtcg.json → DTCG stable 2025.10 — colors as colorSpace/components objects (hex kept as fallback), dimensions and durations as value+unit objects, easings as cubicBezier arrays, shadows structured, font stacks as arrays. Repairs in passing: two gradient tokens re-typed from cubicBezier to other, font-mono corrected to the ui-monospace stack, em letter-spacing kept as strings (dimension is px|rem only). The draft-format file remains in git history.
  • llms-full.txt version stamp — the self-contained context file now declares the spec version and release date it was baked from, so fetch-first consumers (this skill included) can echo what they loaded.

Trigger

Post-Config 2026 the Figma agent accepts custom skills on the same Agent Skills standard this repo already targets — the deliverable is loadable as-is. Occupying the in-canvas surface extends multi-deploy to where designers work. The skill is advisory by nature (the agent is non-deterministic); the deterministic gate remains the amaca-frontend bundle.

RELEASED · 2026-07-14 LIVE · amaca.design SKILL · amaca-figma.md COMPARE · v3.1.0…v3.2.0

MINOR. Two amaca.design-side parity moves — no token, value, or component contract changed.

New · IDE download — core + per-target rules

  • Cursor split — the single .cursor/rules/amaca.mdc is replaced by amaca-core.mdc (alwaysApply: true — token discipline, the laws, the a11y floor) plus glob-scoped amaca-html.mdc (CSS/HTML) and amaca-react.mdc (JSX/TSX, Tailwind v4 @theme). The core stays loaded everywhere; the heavy target rules attach only to the files they govern.
  • Copilot per-target — .github/copilot-instructions.md (repo-wide) plus .github/instructions/amaca-{html,react}.instructions.md with applyTo globs.
  • Canonical targets — html · react · figma. Figma is a Variables deploy, not a glob target, so it carries no .mdc.

New · site /llms.txt

  • Curated AI index — /llms.txt points agents at the docs worth reading (DESIGN.md, tokens.css, theme.css, tokens.dtcg.json, AGENTS.md) by absolute URL, plus a self-contained /llms-full.txt with the rules and full token table inlined. Read by Cursor, Copilot, and Claude/Perplexity in retrieval.
  • robots.txt + sitemap.xml — permissive crawl policy that names the AI/LLM crawlers (GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-Web, PerplexityBot, Google-Extended) as allowed so /llms.txt is never moot, plus a sitemap listing / and /DESIGN.md. Independent layers from llms.txt (access + search vs inference-time index).

Changed · downloads

  • App-builder bundle split out — the App-builder card (Figma Make · v0 · Lovable · Base44 · Bolt) now downloads its own amaca-appbuilder.zip instead of sharing amaca-chat.zip, so the two no longer collide as one filename. Both ship the same self-contained AI-INSTRUCTIONS.md paste-in, baked from one source via build-paste-in-bundles.sh — edit once, both stay in sync.

Trigger

Reviewing the downloads against what the AI tools read surfaced two gaps: the IDE download was a single rule file where core + per-target scopes the rules better, and the site had no hosted llms.txt. Closing those pulled in the adjacent AI-surface hygiene (robots/sitemap) and a download-naming dedupe (App-builder).

RELEASED · 2026-07-06 LIVE · amaca.design SPEC · design.md COMPARE · v3.0.0…v3.1.0

MAJOR. A document-grammar break, not a token change. DESIGN.md is rewritten to the Google Labs design.md open standard — the eight canonical sections in order plus a structured front-matter token block — so any conforming consumer (Stitch, an agent, a conformance lint) reads it as a valid design.md by construction, not by omission. No token, value, or component contract changed; every screen renders identically to v2.8.0.

Changed

  • Eight canonical sections — Overview · Colors · Typography · Layout · Elevation & Depth · Shapes · Components · Do's and Don'ts, in that order. The old numbered sections fold in: Principles / Voice / 85·10·5 / Anti-patterns → Do's and Don'ts; the token sub-sections → Colors / Typography / Layout / Elevation & Depth / Shapes; component specs → Components.
  • Structured front-matter — Machine-readable colors: (with an explicit primary: #F051D5 role + the full obsidian/magenta scales), typography:, spacing:, rounded:. The primary role is the load-bearing bit: without it a consumer can regenerate the palette and ignore the brand.
  • Extension sections, preserved — Motion, Iconography, Accessibility, Code conventions, Multi-deploy and IDE integration are kept verbatim as marked extension sections after the canonical eight — the richness the standard has no slot for, lint-safe.

Migration

Section numbers changed. In-prose cross-references written as "§ N.M" now point to the same content under its new home (e.g. former § 3.10 Dropdown → Components → Dropdown / Select; § 6.3 → Accessibility → Ratified exceptions; § 2.8 → Motion). A migration note in the spec changelog carries the mapping.

Trigger

The Google design.md grammar is the shape agents and tools now read — so the hand-authored spec was restructured to the same standard, written at its best. The site is now the maximal-compliant instance, not a bespoke doc the lint can't read.

Also shipped in 3.0.0

  • New component · Presentations — slide-deck templates (a fixed 1920×1080 canvas scaled to 16:9, eight layouts), with a live demo deck + layout gallery in Applied. Slide pieces enter on the system's staggered-reveal motion: numeric values count up, listed rows choreograph one at a time, data bars wipe in.
  • Downloads · "Pick your tool" — Amaca packaged for every tool: a Claude skill + CLAUDE.md, the universal AGENTS.md, Google design.md, IDE rules (Cursor .mdc / Copilot), a self-contained paste-in for ChatGPT/Gemini/builders, and the token formats (Tailwind @theme, raw CSS, DTCG). One turnkey bundle per tool, each with a usage README inside.
  • Data Viz · interactive hex world map (§ 19.8) — the public-domain Natural Earth 1:110m land rasterised into a token-styled honeycomb (d3-geo + world-atlas), status nodes placed at true longitude/latitude, click-to-inspect node card, labeled legend.
RELEASED · 2026-06-22 LIVE · amaca.design SPEC · design.md COMPARE · v2.8.0…v3.0.0

MINOR. One component refinement: the custom listbox now places itself so it never covers the content it sits above.

Changed · Dropdown / Select

  • Viewport-aware placement (variant B) — The custom listbox opens below the trigger and flips above when there isn't room, clamps its max-height to the available space (internal scroll) and clamps horizontally — so the menu never covers adjacent content (e.g. a chat composer the control sits above). position:fixed measured from the trigger's rect when near a viewport edge; the old position:absolute; top:calc(100% + 4px) stays valid only when there's always room below.
  • Native <select> (variant A) discouraged directly above content — The browser/OS renders the option popup over the control and can cover what's beneath it; near a viewport edge or above must-see content, reach for variant B.
  • Dismissal extended — A fixed-position menu also closes on scroll and resize (its anchored coordinates would otherwise drift from the trigger).

Trigger

A native <select> sitting just above a chat composer covered the input with its option popup. The fix is a placement contract, ratified into § Components → Dropdown / Select, that any control near a viewport edge or above must-see content follows.

RELEASED · 2026-06-21 LIVE · amaca.design SPEC · design.md COMPARE · v2.7.2…v2.8.0

PATCH. A ratified no-change: the primary button label was evaluated at three weights and stays Medium 500 — documented so the decision has a date and a rationale, not just a habit.

Ratified

  • § 3.1 · Primary label weight — Compared Regular 400, Medium 500 and Bold 700 in context (15px and 13px scales). Under APCA stroke weight is a contrast input and 700 scores strongest — but the bold label shifts the button's voice, and the legibility budget is already carried by the § 6.3 pairing. Medium 500 holds: don't bold the primary label for emphasis, don't drop below 500. Noted in the § 6.3 APCA evidence so the question doesn't reopen.
  • System constraint, recorded — Satoshi ships no SemiBold cut: font-weight: 600 silently resolves to Bold 700. The real choice on this system is 500 vs 700.

Skill

Bundle re-baked as v1.1.5 on this spec; HTML.md's a11y note carries the weight rule.

Trigger

Follow-up to the v2.7.1 APCA work: would a semibold label improve accessibility? WCAG 2 is weight-blind, APCA rewards weight, the system has no 600 cut — a side-by-side evaluation closed the question in Medium's favor.

RELEASED · 2026-06-12 LIVE · amaca.design SPEC · design.md COMPARE · v2.7.1…v2.7.2

PATCH. Documentation and one token-value refinement. The floor's only ratified exception (§ 6.3, primary button label) gets quantified with the WCAG 3 candidate method — and the perceptual model agrees with the choice.

Fixed

  • § 6.3 · APCA evidence — Under APCA the ranking inverts: --obsidian-050 on --magenta-500 scores Lc 55.8 vs 47.4 for the dark label. WCAG 2.x under-estimates light text on saturated mid-tone fills (the "orange button" failure mode). Added with it: the ceiling note (4.5:1 is unreachable on #F051D5 — pure white tops at 3.07:1) and a compliance mode for AA-contractual contexts (dark-label variant, on request, never default).
  • Ratio honesty — The declared ≈3.0:1 was the measured 2.83. Corrected to ≈2.8:1 in every living section (§ 3.1, § 6.1, § 6.3 spec · § 03.4, § 21.1 site). Historical changelog untouched.
  • § 21.1 · EXC badge — Dropped the warning tint: the asterisk already marks the exception, and semantic colors are feedback-only (§ 04.5) — a documentation badge reading as an alarm was off-doctrine.
  • ::selection literal — tokens.css carried color:#fff, contradicting the no-#FFF rule and § 6.3's "near-white" description of the precedent. Now var(--obsidian-050).
  • § 7.3 · Runtime-token anti-drift, codified — Live in two domains (Mermaid theming § 18, the window.__motion helper) but missing as a general rule. Now canonical in code conventions: tokens read off :root at every JS boundary; a hand-copied value is a regression.

Skill

Bundle re-baked as v1.1.4 on this spec; HTML.md's a11y note carries the APCA evidence and the measured ratio. Release rule, now standing: every relevant DS change ships into the skill in the same release.

Trigger

An accessibility question — is there an accessible non-pure white on magenta-500? — led to measuring the exception with both models. No light color can pass WCAG 2 on this fill; APCA inverts the ranking in the light label's favor. The floor's only exception is now its most evidence-based section.

RELEASED · 2026-06-12 LIVE · amaca.design SPEC · design.md COMPARE · v2.7.0…v2.7.1

MINOR. A motion audit (2026-06-11) found the system's animations running on six engines with two structural leaks: Motion One blocks duplicated easing values as literals and escaped the reduced-motion kill-switch, and chart draw-on choreography lived off-scale. This release closes both — one new token, zero new engines.

Added · tokens

  • --d-draw · 1200ms — Sixth duration. Data draw-on: chart tracing, count-up, progressive data reveals. The scale topped out at --d-scene (900ms, hero reveals), but a line tracing itself or a number counting up is a different semantic category — data revealing, not UI entering — and lives naturally at 1.2s. Projected to Tailwind as duration-draw via theme.css.

Fixed · motion integrity

  • Motion tokens, one source — The Motion One module now reads --d-* and --ease-* off :root at runtime and hands them to consumers (same pattern § 18 uses for color). The hand-copied easing arrays in cards (§ 12) and data viz (§ 19) are gone — if a token moves, the JS follows. The v2.0.0 lesson, finally encoded.
  • Reduced-motion gate for Motion One — The § 21.3 CSS kill-switch can't reach WAAPI/JS animation. Every animate() call is now gated: under prefers-reduced-motion: reduce it jumps straight to the final state and counters land on their target. Cards and data viz — the most animated surfaces in the system — were the only blocks without a JS check.
  • Durations snapped to scale — Data viz literals (0.5 / 0.7 / 0.75 / 0.9 / 1.2 / 1.4s) now resolve to tokens: draw-on and count-up ride --d-draw, baseline sweeps ride --d-scene, bar and ramp reveals ride --d-slow. Choreography delays and staggers stay literal by design — they are composition, not vocabulary.
  • § 08 demo fallback — playMotion fell back to cubic-bezier(0.2, 0.8, 0.2, 1) — a curve that exists nowhere in the system — and 240ms, the pre-v2.0.0 base. Now reads --ease-standard and --d-quick off :root.

Trigger

An external motion audit read every animation path in the codebase and found the virtuous pattern — tokens read off :root at runtime — already canonized in § 18, while the Motion One blocks next to it still carried hand-copied values. Extending the system's own best pattern to its most animated surfaces is the whole release.

RELEASED · 2026-06-12 LIVE · amaca.design SPEC · design.md COMPARE · v2.6.1…v2.7.0

PATCH. Documentation-only. The accessibility exception ratified in v2.6.0 (§ 3.1 primary button label) is now reflected everywhere the old rule survived. No token changes, no component API breaks — every screen renders identically to v2.6.0.

Fixed

  • § 03.4 · A11Y floor — Rewritten. The old note required obsidian-950 on every magenta fill and called light text a failure; it now states the dark-text default and documents the single ratified exception, scoped to .btn-primary.
  • § 21.1 · Contrast pairs — The obsidian-900 on magenta-500 row described the pre-v2.6.0 button. Replaced with two rows: obsidian-050 on magenta-500 (≈3.0:1, ratified exception, primary button label) and obsidian-950 on magenta-500 (6.5:1, AA, default for every other magenta fill). Section lede updated — "nothing falls below AA" is no longer literally true, so it no longer says it.
  • § 02 · Principles heading — "Five rules. No exceptions." violated § 20.4 (no exceptions as rhetorical emphasis) and, after v2.6.0, was literally false. Retitled: "Five rules. Exceptions earn their keep."
  • § 02 · Signature curve copy — Still claimed every surface rides cubic-bezier(0.16, 1, 0.3, 1); since v2.0.0 that curve lives at --ease-decel and --ease-standard is the Framer ease. The copy now says what the tokens say.
  • § 03 · Skill references — The docs described the skill as v1.1.0, 6 steps, 13 checks; the shipped bundle is two releases ahead — 7 steps and 14 checks since v1.1. All § 03 references reconciled, and the bundle re-baked as v1.1.2 on the v2.6.1 spec so the download matches the page that describes it.
  • styles/theme.css — v2.5.0 declared it shipped; it never reached the repo. It now exists: a Tailwind v4 @theme projection of tokens.css — var() aliases, zero duplicated literals, same-name tokens (--font-*, --ease-*) declared @theme inline to avoid circular aliases. Verified against a real Tailwind v4 build: 21/21 utilities generate. One spec correction landed with it: the duration namespace in § 13.1 is --transition-duration-*, not --duration-* — the generated utility (duration-quick) is unchanged.

Trigger

An external audit (2026-06-11) read the live document end to end and found three sections still describing the system as it was before v2.0.0 and v2.6.0. A spec that sells enforcement can't disagree with itself — reconciling the copy to the ratified decisions is the whole release.

RELEASED · 2026-06-11 LIVE · amaca.design SPEC · design.md COMPARE · v2.6.0…v2.6.1

MINOR. A new component: auto-layout flowcharts and system-architecture diagrams, rendered from a textual source and themed entirely from tokens. Additive — tokens.css is untouched and no component API breaks.

Added · components

  • § 18 · Diagrams — Flowcharts / node-edge graphs and system architecture (box-and-line with grouping), via an auto-layout library (Mermaid, theme base) laid out with the ELK engine for orthogonal, FigJam-style right-angle edges (dagre fallback). The theming config holds zero hardcoded hex: tokens are read off :root with getComputedStyle at runtime and passed into themeVariables, and the focus / state classDef strings are built from the same tokens at render time — the source of truth stays tokens.css. 85/10/5 holds: everything neutral obsidian, exactly one --magenta-500 focus node per diagram; semantic states (--success / --warning / --danger) only when a node encodes a real state, always with a distinct shape + label + a .diagram-legend — never color alone. Anatomy: <figure class="diagram"> → .diagram-canvas[role="img"] holding the SVG → .diagram-caption (FIG-NN · title), coherent with .card.

Motion

  • On-scroll entrance — Reuses the established reveal patterns. An IntersectionObserver (threshold 0.15, rootMargin '0px 0px -8% 0px', unobserve after fire) cascades nodes in (~70ms stagger), then edges and clusters. Opacity only — no CSS transform on the generated SVG <g> groups; they compose with the cascade and break the auto-layout positioning. Hooked after the async render resolves. prefers-reduced-motion: reduce renders straight to rest.

Refined

  • § 3.1 · Button (primary) — Primary label moves dark → near-white: --obsidian-050 on --magenta-500. A ratified accessibility exception (§ 6.3): the pair is ≈ 3.0 : 1, below the 4.5 : 1 AA bar, kept on perceptual grounds — on the high-chroma magenta, near-white separates more cleanly than the higher-contrast dark label. Scoped to .btn-primary; rest and hover are bounded to --magenta-500 (no lighten), fill and dual-ring focus unchanged. The CTA stays --magenta-500, so the magenta doctrine and chat reservation are untouched.
  • Section numbering — Diagrams lands in the Components group as § 18. The Applied and Meta groups shift by one — Data Viz 18→19, Voice 19→20, Accessibility 20→21, Case Study 21→22, Changelog 22→23. Sidebar nav, in-page anchors, and cross-references all updated; no external links break.

Trigger

The multi-deploy docs (v2.5.0) needed a system map and there was no on-system way to draw one. The open question — how to theme an auto-layout library without pasting hex into its config — is closed by reading tokens off :root at runtime. That is the rationale for § 18.

RELEASED · 2026-06-06 LIVE · amaca.design SPEC · design.md COMPARE · v2.5.0…v2.6.0

MINOR. React and Figma move from roadmap to shipped. One canonical token now projects to three surfaces — CSS, Tailwind v4, Figma Variables — defined in a new § 13 and emitted by styles/theme.css. Additive: tokens.css and every component spec are untouched; existing HTML output is byte-for-byte identical.

Added

  • § 13 · Multi-deploy compatibility — One token, three surfaces. var(--magenta-500) ↔ bg-magenta-500 (React / Tailwind v4) ↔ the Figma Variable color/magenta/500, all resolving to #F051D5. The direction is one-way: tokens are the source, every target derives.
  • styles/theme.css — A Tailwind v4 @theme projection of tokens.css. var() aliases, no duplicated literals. Wire it with @import "amaca-design/styles/theme.css" and the full token scale becomes utilities.
  • DESIGN.md YAML frontmatter — Machine-parseable header: version, updated, last_synced, deploy_targets. The markdown body is unchanged below it.
  • amaca-frontend skill v1.1.0 — Multi-target. Reads HTML.md / REACT.md / FIGMA.md for the surface you request and ingests theme.css for React output. HTML, React via Tailwind v4 @theme, and Figma Variables all ship.

Refined

  • § 03.5 · Multi-deploy export — Reframed from roadmap to shipped. The section now documents the three-surface model and the theme.css import. The Google design.md open-spec dependency is dropped — the React/Tailwind path is Tailwind v4 @theme native, not generated by an external tool. The DTCG export is restated as Amaca's own roadmap, fully decoupled.
  • Version reconciliation — Spec, skill, header badge, and overview meta all reconciled to v2.5.0 / skill v1.1.0. § 03.1 and § 03.6 reflect the multi-target skill and the spec ↔ skill version split.

Trigger

The amaca-frontend skill reached v1.1 and its reference docs cited a § 13 and a Tailwind v4 @theme projection that did not yet exist — the skill shipped ahead of its contract. § 13 and theme.css close that gap: the contract the skill already references is now real and canonical.

RELEASED · 2026-06-02 LIVE · amaca.design SPEC · design.md SKILL · amaca-frontend.skill COMPARE · v2.4.0…v2.5.0

PATCH on top of a documentation refresh. The § 03 Meta surface is rewritten end-to-end: two delivery formats (skill bundle + raw spec), per-runtime install guide, model capability tier, and the Tailwind / DTCG export roadmap. No token changes, no component API breaks — every screen in the system renders identically to v2.3.0.

Added · documentation

  • § 03.1 · Choose your fit — Two formats, two postures. The amaca-frontend.skill bundle (and universal .zip mirror) for Cowork and Anthropic-stack runtimes — a 6-step workflow, 13 verification checks, gap-surfacing protocol. The raw DESIGN.md for any chat model that takes a system prompt. Both wrap the same spec; the skill enforces it at generation time.
  • § 03.2 · How to install — Per-runtime instructions for Cowork / Claude Code, Cursor / Continue / Windsurf / Aider, Gemini Gems, ChatGPT Custom GPT, and one-off chat attachment. The five rows replace the previous three generic workflows.
  • § 03.4 · Model capability tier — New section. Frontier vs lightweight expectations, with measured scores (8.5/10 overall, 9.5/10 on gap handling across N=4 scenarios on the Anthropic stack), the 30-second post-generation scan checklist, and the runtime architecture caveat for consumer chat tools that abstract file grounding.
  • § 03.5 · Tailwind export — Roadmap. When YAML frontmatter lands in DESIGN.md v2.5.0+, three tooling integrations unlock via the Google design.md open spec: lint (token references, WCAG AA contrast, orphans, section order), diff (token-level regression detection), and CSS export to Tailwind v4 @theme or W3C DTCG tokens.json. v1.1 of the skill adds React-via-Tailwind generation on top.
  • § 03.6 · Versioning & license — Two SemVer streams, documented. The spec follows the system release; the skill versions independently and ships with whichever DESIGN.md is current. License stays MIT.

Refined

  • Page-level framing — § 03 retitled from "packaged for machines" to "two ways to drop it into your stack." The lede now names the spec/skill split up front instead of pitching a single Markdown file.
  • § 03.3 · Components row — The "what's inside" table now states 13 components explicitly (buttons, forms, cards, badges, navigation, accordion, chat, loader, checkbox, …) rather than the earlier abbreviated list. Cross-reference to Changelog bumped to § 23 to match current numbering.

Fixed

  • Stray markup in versioning copy — The old § 03.4 versioning card had a duplicated </p> closing tag mid-sentence (a paste artifact from an earlier draft). Replaced by the rewritten § 03.6 block.
  • Spacing token in § 03.1 picker cards — Initial pass used var(--s-7), which is not part of the 4-px scale (the system skips 7) and resolved to 0px. Cards now use var(--s-8) for full padding.

Trigger

Operating the system across multiple AI runtimes — Cowork, Cursor, ChatGPT, Gemini — surfaced a single point of friction: the docs assumed one delivery mode (paste the spec) when in practice two exist, with very different enforcement guarantees. v2.4.0 closes that gap and pre-loads the v2.5.0+ Tailwind roadmap so the next minor release lands against a documented surface, not a blank one.

RELEASED · 2026-05-25 LIVE · amaca.design SPEC · design.md SKILL · amaca-frontend.skill COMPARE · v2.3.0…v2.4.0

MINOR release. Two new components ship — a chat & messaging set built for chatbot interfaces, and a brand-logo loader at four scales with five composition variants. The 85/10/5 law card gets an entrance choreography. The Applied and Meta groups renumber by one to keep Components contiguous (16 Chat, 17 Loader); no token, no API breaks.

Added · components

  • § 16 · Chat & Messaging — Bot and own bubbles at uniform --r-lg (no tail), avatar on the first bubble of a bot streak, two-stage entry choreography (container scale(0.92→1) on --ease-spring at --d-base, inner content fades up 80ms behind on --ease-standard), typing indicator with a 3-dot wave loop on --d-slow, scripted live demo with replay, full composer specimen (textarea + send + attach). Own bubbles fill with --magenta-700 so --magenta-500 stays reserved for CTAs and focus.
  • § 17 · Loader — Brand mark (Lottie/GIF) at four scales — --loader-xs 16 / --loader-sm 24 / --loader-md 48 / --loader-lg 96. Five variants: standalone, with-label (cycling phrases on a 2.4s interval), inside-button (replaces label when .is-loading), skeleton handoff, and success state with a check that scales in on --ease-spring. Four real-world compositions: form submit, async input validation, full-page boot in a device frame, and a cross-component placement inside the chat composer. Under prefers-reduced-motion: reduce the brand mark steps aside and a single magenta CSS ring spinner takes its place.

Added · tokens

  • --loader-xs / sm / md / lg — Component-specific size scale at 16 / 24 / 48 / 96px. Lives in tokens.css alongside layout tokens.

Refined

  • § 04.7 · 85/10/5 law card — Entrance choreography added. On scroll into view, the proportion bar wipes left-to-right via clip-path: inset(0 100% 0 0) → inset(0 0 0 0) on --ease-decel, then the three legend rows and the rule block fade-and-rise in sequence (stagger 180ms). IntersectionObserver fires once at threshold 0.25; reduced-motion users see the resolved state instantly.
  • Chat own-bubble color — Discarded the "hot last" rule (latest own message tinted bright magenta). Unified all own bubbles on --magenta-700 (deep brand) with --obsidian-050 text — 6.26:1 contrast, near-AAA at body size. The brighter --magenta-500 stays reserved for CTAs and focus rings, so the 85/10/5 budget holds in long conversations.
  • Loader inside .btn-primary — Cyan-on-magenta is off-system. Resolved via filter: brightness(0) on .btn-primary .loader-anim, collapsing the cubes to a near-black silhouette without introducing a second source file. Applies at rest and while loading.
  • Section numbering — Applied (Data Viz, Voice, Accessibility, Case Study) and Meta (Changelog) groups shift by one — 16→18 / 17→19 / 18→20 / 19→21 / 20→22 — to make room for the two new Components entries. Sidebar nav, in-page anchors, and cross-references all updated; no external links break.

Accessibility

  • Chat conversation — The scroll region carries role="log" aria-live="polite" aria-relevant="additions"; each bubble includes a visually-hidden sender prefix ("Amaca said:" / "You said:") so the read order survives when the avatar is suppressed on streak follow-ups; the typing bubble carries aria-label="Amaca is typing"; the composer textareas get a persistent aria-label.
  • Focus visibility — Dual-ring focus state (--obsidian-100 outline + magenta halo) added to .chat-composer-attach, .chat-composer-send, and .chat-demo-replay. Touch hit areas on composer buttons bump from 36 to 44px below 720px wide.
  • Loader semantics — Every loader carries role="status" with a contextual aria-label. The cycling label variant updates the host's aria-label in lockstep with the visible phrase, so screen readers hear the real wait rather than a generic "Loading".
RELEASED · 2026-05-22 LIVE · amaca.design SPEC · design.md COMPARE · v2.2.0…v2.3.0

MINOR release. Three new components ship: a calendar-popover date input, a two-month date-range picker with analytics presets, and an iOS-style scroll-wheel time picker. The release also closes two spec gaps surfaced by a real implementation (pipeline-dashboard, 2026-05-12): Tabs were under-specified and the dropdown / select pattern was missing entirely.

Added · components

  • § 11.2 Time picker — iOS-style scroll wheel. 24h, configurable minute step (default 5), 7 visible items, infinite loop via triple-buffer + auto-rewind, native scroll-snap-type: y mandatory. Magenta band overlay + top/bottom fade gradient. Click on item snaps with smooth scroll; ↑/↓ on a focused wheel step ±one. ↓ on the input opens the popover.
  • § 11.3 Date input — DD/MM/YYYY single date. Digit-by-digit masked (D1≤3, D2 depends on D1, M1≤1, M2 depends on M1, slashes auto-insert). Trailing calendar icon opens a popover with year-jump view (decade grid 4×3), Today shortcut, optional data-min/data-max bounds. Real-date validation (31/02 rejected) fires on blur.
  • § 11.4 Date range — Two coupled inputs (data-range-start / data-range-end) sharing one popover anchored to start. Two months side-by-side; collapses to stacked vertical under 600px. Click 1 sets start, hover preview, click 2 commits end (swaps if before start). Four analytics presets in footer: Today, Last 7 days, Last 30 days, Last month. Helper auto-renders N days (inclusive count).

Refined · spec gaps closed

  • § 3.7 Tabs — Expanded from a single line to a full spec: canonical HTML, required classes, the indicator-positioning JS pattern (getBoundingClientRect on button + parent, transform: translateX + width interpolation, resize listener, double requestAnimationFrame for initial position), prefers-reduced-motion handling, panel fade-in with opacity + 6px translateY, and the "never crossfade, never slide both" rule.
  • § 3.10 Dropdown / Select — New section. Two variants: native enhanced (<select class="select"> with appearance:none + inline SVG chevron, canonical stroke %238A94A3, 36px right-pad, hover/focus borders) and custom listbox (.select-wrap > .select-trigger + .select-menu > .select-option) with full keyboard nav, focus-trap, click-outside dismissal, and guidance on when to prefer each.

Fixed

  • Date picker hover flicker — The :hover rule on .dp-day now excludes every range state (is-in-range, is-range-preview, is-range-start, is-range-end); previously obsidian-800 was overpainting the magenta range fills frame-by-frame.
  • Range second-click reliability — The hover preview no longer re-renders DOM (was destroying the cell under the cursor before click could register). Cells are cached on grid._cells and only their state classes get repainted via paintRange(). Commit moved from click to mousedown with preventDefault.
  • Stepper label drift on replay — The reset used a flag shared with dots (_baseT, never set on labels) and re-parsed cy from the already-offset attribute, accumulating +10px per replay. Replaced with a dedicated _labelInit flag and a clean translate(cx, cy) reset.
  • Replay button capture phase — Listener moved to capture phase so the chart reset survives any downstream stopPropagation.

Compatibility

AreaStatus
All tokens (color, type, spacing, radius, shadow, motion)Unchanged
Existing component APIsUnchanged
New components: Time picker, Date input, Date rangeAdded
§ 3.7 Tabs, § 3.10 DropdownSpec expanded — no markup changes required

Trigger

A real implementation — pipeline-dashboard, 2026-05-12 — surfaced two under-specified areas. Closing those gaps is the rationale for the spec-side updates in § 3.7 and § 3.10.

RELEASED · 2026-05-12 LIVE · amaca.design SPEC · design.md COMPARE · v2.1.0…v2.2.0

MINOR release. Adds the first new component since v2.0.0 — a fully-masked time input — alongside deterministic landing-page routing and a stepper transform refactor that fixes a long-standing replay bug. design.md bumped to v2.1.0; drop the new spec into your LLM rules.

Added

  • § 11.2 · Time input — HH:MM, 24h. Digit-by-digit masked: H1 max 2, H2 max 3 if H1=2 else 0–9, M1 max 5, M2 0–9. The colon inserts automatically after the second digit. No native type="time" — the browser picker doesn't respect the system. maxlength="5" + inputmode="numeric" are required. Optional by default; can be marked required per context. Focus & error states inherit from § 11.1.

Refined

  • Anchor-link routing — landing on amaca.design (no hash) now always resolves to #overview. The previous localStorage fallback that remembered the last-visited section across visits has been removed — fresh visits are deterministic. URL is rewritten via history.replaceState to avoid extra history entries.
  • Stepper SVG transforms — refactored to use the SVG transform attribute as the single source of truth (instead of CSS transform-box: fill-box, which v2.0.1 had documented). A generic [data-fade]{transform: translateY(12px)} rule was silently composing with the group transforms via cascade and breaking dot positioning. Replay now drives the SVG attribute directly.
  • .law-rule mobile padding — uniform 20px padding on small viewports with a 24px left-pad to compensate for the magenta border. Internal gap reduced from 32px to 12px for compact stacking.

Documented

  • § 3.9 — Time input full spec (rules, contract, reference implementation).
  • § 7.3 — SVG group transforms guidance rewritten: prefer SVG attribute over CSS. Stepper choreography updated. Anchor-link routing pattern added.

Compatibility

AreaStatus
All tokens (color, type, spacing, radius, shadow, motion)Unchanged
Existing component APIsUnchanged
New component: Time inputAdded

Note on history

v2.1.0 follows v2.0.0 directly. The internal v2.0.1 patch (stepper hardening, outcome card stat refactor, staggered text reveals) was never publicly tagged — its contents are folded into this release alongside the new additions. From v2.1.0 onward the system continues to follow strict SemVer.

LIVE · amaca.design SPEC · design.md COMPARE · v2.0.0…v2.1.0

First major release. Motion vocabulary recalibrated end to end — easing curves redistributed semantically, durations restretched across the full scale. Everything else stays. One reason to bump the major: one set of changes worth knowing about.

Breaking changes

This release shifts the values of the motion system. Token names are unchanged. If you reference Amaca tokens in another project, audit motion before upgrading.

Easing values reassigned

The four easing tokens kept their names. The curves under them moved to better match what the names mean.

TokenBeforeAfter
--ease-standardcubic-bezier(0.16, 1, 0.3, 1)cubic-bezier(0.22, 1, 0.36, 1)
--ease-decel[previous curve]cubic-bezier(0.16, 1, 0.3, 1)
--ease-accelcubic-bezier(0.7, 0, 0.84, 0)unchanged
--ease-springcubic-bezier(0.34, 1.56, 0.64, 1)unchanged

The curve cubic-bezier(0.16, 1, 0.3, 1) — the system signature — now lives where it semantically belongs: --ease-decel. Use it for entrances and reveals, where the slow tail in the curve has always been doing decel work. --ease-standard is now the Framer ease, sized for everyday UI state changes (hover, focus, button press).

Migration: if a component was relying on --ease-standard to behave like the old signature curve, switch it to --ease-decel. If it was using --ease-standard for generic UI feedback, leave it — the new curve does the job.

Duration values rebalanced

The five duration tokens have been restretched. Names unchanged — only values shift. The new scale stretches the slower end (more breathing room on entrances and orchestrated reveals) and grounds the faster end on round numbers.

TokenBeforeAfter
--d-instant80ms100ms
--d-quick160ms200ms
--d-base240ms350ms
--d-slow400ms600ms
--d-scene640ms900ms

Migration: no code changes required if you reference durations via var(--d-*). If any duration is hardcoded in milliseconds, search for the old values and confirm intent.

Refined

  • Motion — Additional animations integrated across components. The new easing/duration pairings applied throughout.
  • § 7 · Grid & Layout — Grid system extended to tablet and mobile breakpoints. Column counts, gutters, and content widths now documented for every viewport.
  • § 14 · Voice & Tone — Section rewritten for sharper register. Do/Don't examples expanded. Vocabulary table tightened.
  • design.md — Spec file regenerated for v2.0.0. Drop the new version into LLM system prompts to keep AI-generated work in sync with the live system.

Fixed

  • Mobile responsiveness — Multiple layout fixes across breakpoints. Long titles, dense tables, and stat cards now behave correctly on narrow viewports.

Compatibility

AreaStatus
Color tokensUnchanged
Typography tokensUnchanged
Spacing scaleUnchanged
Radius & shadowUnchanged
Component APIsUnchanged
Motion easing namesUnchanged
Motion easing valuesBreaking — curves redistributed
Motion duration namesUnchanged
Motion duration valuesBreaking — five values rebalanced

Note on history

v2.0.0 follows v1.2.0 directly. Internal iterations between (drafts at v1.2.1 and beyond) have been consolidated into this release. From v2.0.0, the system follows strict SemVer with no skipped numbers in public releases.

RELEASED · 2026-05-07 LIVE · amaca.design SPEC · design.md COMPARE · v1.2.0…v2.0.0

First substantive expansion since v1.0. The system gains three new components, a full accessibility posture, a public changelog, deep-linkable sections, and a downloadable spec for AI-assisted design.

Added · new components

  • § 15 · Accordion — Collapsible content blocks for FAQs, dense documentation, and progressive disclosure. Single-open and multi-open variants. Keyboard navigation built in.
  • § 19.5 · Timeline (Gantt) — Horizontal time-based visualization for project phases, roadmaps, and case study chronologies. Date-aware, with milestone markers.
  • § 19.6 · Stepper — Linear progress indicator for multi-step flows (forms, onboarding, case study walkthroughs). Horizontal and vertical variants.

Added · new sections

  • § 21 · Accessibility — Color pairs with documented contrast ratios, prefers-reduced-motion handling, focus & keyboard order, and hard rules that gate every component decision.
  • § 23 · Changelog — Version history, in public. What changed, when, and why.
  • § 03 · Documentation — Single Markdown file (design.md) containing every token, component spec, principle, and rule. Built to feed into LLMs as context for AI-assisted design work. Updated alongside the system — every release ships a fresh spec.

Added · navigation

  • Anchor links — Every section header now carries a hash. Deep-link any paragraph: amaca.design/#13-accordion, amaca.design/#16-accessibility. The system became citable.

Refined

  • § 04 · Colors — Visual presentation refined. Token names, hex values, and intended use unchanged — no breaking changes to consumers.
  • § 10 · Buttons — Vertical padding tuned for better optical balance across sizes.
  • Copy — Several section intros and microcopy tightened for rhythm.

Added · meta

  • Favicon — Amaca mark serves across browser tabs and bookmarks.
  • Open Graph image — Link previews on social and chat render the system's identity instead of a generic screenshot.

Unchanged

No token renames. No component API shifts. No breaking changes.

Note on history

This release consolidates internal iterations after v1.1.0 that were not shipped publicly. From v1.2.0 onward, the system follows strict SemVer.

RELEASED · 2026-04-28 LIVE · amaca.design SPEC · design.md COMPARE · v1.1.0…v1.2.0

Maintenance release. Small fixes, one meaningful polish pass on donut charts.

Fixed

  • § 04 · Colors — Primary token #F051D5 was mislabeled "Cube Cyan" in the palette reference. Corrected to "Magenta" — its actual value for the last nine months.
  • § Overview — Page metadata stamp updated (version + date drift).
  • Responsiveness — Layout fixes across mobile breakpoints.
  • Interactions — Added states for tabs, images, and thumbs.

Refined · § 19.3 Donut charts

Slice geometry rebuilt from stroked arcs to true ring-segment paths.

  • All four corners of each slice now render with a real 2px radius (matches rx=2 on bars in § 19.2).
  • Inter-slice gap halved from ~6px to ~3px of circumference for tighter visual rhythm.
  • Draw-on animation preserved via angular sweep.
  • Before — half-circle round caps (13px radius — too soft) or butt caps (sharp — inconsistent with bars).
  • After — 2px on all 4 edges. Matches the rest of the data viz vocabulary.

Unchanged

Everything else. No breaking changes, no token renames, no component API shifts.

Infrastructure

  • Repository — Cleanup: removed duplicate nested folders and orphaned font/logo files from root.
  • Documentation — Added README and LICENSE to public repo.
  • Branch protection — Ruleset activated on main (PR-only merges).
RELEASED · 2026-04-24 LIVE · amaca.design

Initial public release. Prima pubblicazione pubblica del design system Amaca, live su amaca.design.

Added · Foundations

  • Color — Magenta brand range, obsidian neutrals, electric cyan secondary, petrol tertiary, semantic signals.
  • Typography — Satoshi as primary typeface across all weights (Light to Black).
  • Spacing, radius & shadow — Core layout tokens.
  • Motion — 5 durations × 4 easings, all synchronized on cubic-bezier(0.16, 1, 0.3, 1).
  • Grid & layout — Foundational structure across breakpoints.

Added · Components

  • Buttons — Primary set with full state coverage.
  • Inputs & forms — Field, label, and validation patterns.
  • Cards — Container variants for content surfaces.
  • Badges — Compact status and category markers.
  • Navigation — Top-level wayfinding patterns.

Added · Applied

  • Data visualization — Charting vocabulary aligned to brand tokens.
  • Voice & tone — Editorial direction across surfaces.
  • Case study template — Reusable narrative structure for project work.

Principles

Five principles — clarity before cleverness · evidence over opinion · precision is a feeling · quiet then loud · motion is a material.

Infrastructure

  • Hosting — Static site on Vercel.
  • DNS — Cloudflare.
  • Repository — Public on GitHub, MIT License.
  • Versioning — SemVer adopted.
RELEASED · 2026-04-23 LIVE · amaca.design