# Motion system — Deck Hearth Reference for the motion-token surface and the `prefers-reduced-motion` contract. Motion tokens are defined in `styles/globals.css` alongside the [`design-tokens`](DESIGN_TOKENS.md) and consumed as `var(--motion-duration-…)` / `var(--motion-ease-…)`. ## Why bother Motion shapes how an app *feels* more than any other surface. Loose animations (random durations, jittery easings, no reduced-motion respect) read as amateur. The 4-tier taxonomy here exists so every new animation lands in one of four buckets — never an arbitrary duration — and every animation is reduced-motion safe by default. ## Duration taxonomy Four discrete durations + an instant escape hatch. Pick the bucket that matches the *meaning*, not the visual feel. | Token | Value | Use | Examples | | --- | --- | --- | --- | | `--motion-duration-instant` | `0ms` | Theme/route transitions where any duration is wrong; hover state for keyboard-only users | Color flips on `data-theme` change | | `--motion-duration-quick` | `150ms` | Hover, focus, micro-feedback. Snappy enough that users don't perceive it as animation | Button hover scale, focus-ring fade-in | | `--motion-duration-default` | `250ms` | Default for state transitions. The token most CSS `transition:` blocks should reach for | Card hover, link color, glass-surface opacity | | `--motion-duration-slow` | `400ms` | Entering/exiting layout surfaces. Slow enough to read; fast enough not to feel sluggish | Modal scale/opacity, sidebar drawer slide, popover open | | `--motion-duration-deliberate` | `600ms` | Hero moments, celebration, onboarding. Use sparingly — feels heavy if overused | Empty-state illustrations, scanner success confetti | **Rule of thumb:** if you can't justify why a duration is *not* `default`, use `default`. ## Easing taxonomy Three curves. Two ease-outs (one calm, one playful) plus linear for progress indicators. | Token | Curve | Use | | --- | --- | --- | | `--motion-ease-out` | `cubic-bezier(0.16, 1, 0.3, 1)` | Default for entrances + state changes. Decelerates softly — reads as "settling." | | `--motion-ease-spring` | `cubic-bezier(0.34, 1.56, 0.64, 1)` | Slight overshoot for delight (button press, modal open). Use only when the motion is the *point* — sparingly. | | `--motion-ease-linear` | `linear` | Progress indicators (spinners, loading bars). Any non-linear curve here implies state change, which is wrong for a progress affordance. | ## Composition recipes | Recipe | CSS | | --- | --- | | Default hover | `transition: all var(--motion-duration-quick) var(--motion-ease-out);` | | Default state change | `transition: var(--motion-duration-default) var(--motion-ease-out);` | | Modal enter | `transition: opacity var(--motion-duration-slow) var(--motion-ease-out), transform var(--motion-duration-slow) var(--motion-ease-spring);` | | Sidebar slide | `transition: transform var(--motion-duration-slow) var(--motion-ease-out);` | | Loading spinner | `animation: spin var(--motion-duration-deliberate) var(--motion-ease-linear) infinite;` | ## The `prefers-reduced-motion` contract WCAG 2.2 SC 2.3.3 Level AAA: provide a mechanism for users to disable non-essential motion. Operating systems already expose this preference; `@media (prefers-reduced-motion: reduce)` reads it and we honor it. The site-wide rule in `styles/globals.css` collapses every animation + transition to `0.01ms` when the user has reduced motion enabled. End-states are preserved (vs `animation: none` which can flicker). ### When motion is *essential* Some motion is essential to communicate state — a loading spinner indicating in-flight work, an actively-scanning camera reticle. For those, add the `.motion-essential` class to the animating element (or to a parent — children inherit via `.motion-essential *`): ```html