motion
!checklist init ${CLAUDE_SKILL_DIR} --force
An atelier is a craftsman's studio — the place where judgment is applied to material, where a decision about how something moves is as considered as a decision about how it looks. motion is the feel lens of the atelier suite: where the other skills decide what is true and what it looks like, motion decides what it does when you touch it — and makes that a system, not a pile of ad-hoc durations and default curves. Its product is a written motion language: every animation named to a purpose, dynamics derived from physics, gesture conflicts resolved by a declared arbitration policy, and a re-tuned reduced-motion path that preserves meaning. It runs across gated stages and will not advance past a GATE until the checklist tool clears it — order enforced, substance yours.
The governing fact: motion without a purpose is noise; noise is the fingerprint of agent-generated work. The agent animates because it can: transition: all 0.3s ease applied to every state change, a spring dropped in for "feel," layout properties animated because they were convenient, and no prefers-reduced-motion path because no one asked for one. Each choice is locally defensible; the sum is jank, wasted attention, and an accessibility failure. The craft is the same subtraction the rest of the suite preaches: name the purpose first; delete what has none; derive the dynamics from physics; make the platform carry the work. Do that and two things happen — the surface communicates (a well-choreographed state transition tells users where they are; a precise spring on release tells them the object has real weight) and it performs (compositor-only properties stay off the main thread; a declared touch-action replaces a non-passive event handler).
This is where the agent era bites:
- The agent animates decoration. It reaches for
transition: all 0.3s or drops a spring on a button fade. Neither has a job; both add noise. The test is merciless: name the purpose or delete the animation.
- The agent confuses spring and curve. A hover uses
framer-motion; a modal fade uses a Bézier. Both "work" until the user grabs the modal mid-animation — the Bézier restarts from scratch; the spring would have carried the velocity. Using a curve where physics is needed, or a spring where a simple discrete 200ms ease-out would do, is a category error the agent makes constantly.
- The agent forgets gesture arbitration. A draggable panel is placed inside a scrollable list with no
touch-action, no overscroll-behavior, and no axis-lock — so the finger fights the scroll on every open. The platform primitives exist; they are rarely set.
- The agent ships no
prefers-reduced-motion path. The media query exists, it is never checked, and a user with vestibular sensitivity gets large-translation parallax on every scroll.
Read references/motion-purpose-and-physics.md first — the suite's motion spine; everything from easing direction through spring parameter tiers and orchestration patterns lives here. Load at the start, re-check at every gate.
Speak the user's language. The decisions are the user's — which animations earn their place, how much bounce the brand personality warrants, where the surface archetype (data tool vs marketing) changes the motion budget. Read their fluency and gloss a term on first use (purpose taxonomy, spring / damping ratio ζ, easing curve / cubic-bezier, compositor vs layout properties, FLIP / View Transitions API, scroll-driven animation, overscroll-behavior / touch-action, prefers-reduced-motion, stagger / orchestration). A motion system the user can't explain to a colleague is style imposed, not shared.
The reference library
The depth lives in references/. Open each when a stage sends you there — not all upfront.
- references/motion-purpose-and-physics.md — the heart: the seven-purpose taxonomy (causality / spatial continuity / feedback / progress / attention / depth / expression); the easing-vs-spring decision tree and why they can never be swapped; direction rules (ease-out for entrances, ease-in for exits); duration as a function of distance; spring parameter tiers by element weight; orchestration patterns (shared-element, stagger, container transform, enter/exit coordination, Z-axis lift); the 12 animation principles that UI actually uses; the ethics / attention test; and restraint as the default posture.
- references/gesture-arbitration.md — nested gesture conflict resolution: the core arbitration law (inner scroller owns until boundary); axis lock and activation threshold; mid-gesture handoff without a jump; platform primitives (
touch-action, overscroll-behavior, Pointer Events, setPointerCapture, the passive-listener trap); named conflict patterns and recipes (panel-wraps-scroll, horizontal carousel, swipe-to-delete, pull-to-refresh, edge-swipe drawer, same-axis nested, map/pinch-zoom); and platform convention differences (iOS / Android / Web).
- references/the-feel-css-deep-water.md — the CSS mechanics that separate "knows how to animate" from "knows why it works": the rendering pipeline and where each property falls; compositor-only properties and why
will-change is temporary; FLIP and the View Transitions API; CSS scroll-driven animations (animation-timeline: scroll()/view()); spring numerical integration and why semi-implicit Euler; the stacking-context and containing-block traps that break fixed overlays; modern CSS that replaces JS hacks (:has(), container queries, anchor positioning, @property); and how to use DevTools to diagnose frame drops, paint flashing, and cascade failures.
- references/reduced-motion-and-performance.md — the non-negotiables as technique: the correct
prefers-reduced-motion contract (re-tune, don't strip; replace vestibular triggers — translation/scale/parallax/rotation — with cross-fade or instant cut; gestures keep working); flicker ≤ 3×/sec (WCAG 2.3.1); every gesture needs a non-gesture alternative; 60fps / 16.7ms budget and how to hold it; the transform/opacity-only rule and its exceptions (filter); latency masking and the < 100ms first-frame rule; state machine framing for complex interactions; color animation in oklch/oklab.
The arc is one motion system. Three stages — purpose & physics · gesture & intent · compositor, performance & reduced-motion — turn a working surface into one that feels right: animations earn their place and get the right dynamics; fingers are never fighting the scroll; and a user who prefers reduced motion gets a re-tuned experience, not silence.
Greenfield or audit? Decide the entry. On a new surface, walk STAGE 0→2 in order. On an existing surface, inventory first: every animation (CSS transition, animation, JS-driven), its duration and property, and its apparent purpose — cluster by whether the purpose is nameable, whether it animates layout properties, and whether a reduced-motion path exists. That inventory is not a deliverable to perfect; it is the raw material the gates consume. The technique for finding where the un-named animations hide (DevTools Animations panel, a transition: all grep, a will-change audit) is in references/the-feel-css-deep-water.md. The taste call stays a gate: for each cluster you still decide which animations earn their place and what the physics should be.
STAGE 0 — Purpose & physics (name every animation to a job; set the physics)
Open references/motion-purpose-and-physics.md. Establish the purpose taxonomy and the motion tokens before anything is tuned.
- Every animation maps to a purpose — or is deleted. Before touching timing or curve, classify into the seven jobs: causality / origin story (show where a thing came from — the button that triggered the panel); spatial continuity (state/route change that preserves the mental map — shared-element morph); feedback (confirm input received, first frame < 100ms); progress / status (loading, determinate vs indeterminate); attention / hierarchy (staggered entrance, what to look at first); depth / spatial model (modal forward, drawer from edge); expression / brand (the emotional layer — spend it on one signature moment). Anything that doesn't map is noise. Delete it. Restraint is the default posture; over-animation is an agent-era tell.
- Physics by role — spring or curve, never confused. The decision is mechanical: discrete, state-to-state, known duration, not interruptible → easing curve (CSS
transition / Web Animations API). Gesture-driven, interruptible, needs "physics," must carry velocity → spring (stiffness / damping / mass; library-driven). A Bézier curve restarted mid-flight jumps; a spring carries current position and velocity — that round-trip is the fingerprint of native feel. Easing direction: entrance → ease-out (fast start, settle gently — responding to a user action must start instantly); exit → ease-in (accelerate away, get out of the way); on-screen A→B → ease-in-out. Pure ease-in on an entrance is forbidden — slow start reads as stuck. Commit curve tokens (cubic-bezier(0, 0, 0.2, 1) ease-out · cubic-bezier(0.4, 0, 1, 1) ease-in · cubic-bezier(0.4, 0, 0.2, 1) ease-in-out · an asymmetric expressive curve for signature moments). linear is reserved for genuinely constant motion: infinite spinner, progress fill, 1:1 drag tracking.
- Spring parameters from the damping ratio ζ, not by feel. Design in the two derived quantities: ω₀ = √(k/m) (speed) and ζ = c / (2·√(k·m)) (character). ζ < 1 = underdamped (bounce); ζ = 1 = critical (fastest no-bounce settle); ζ > 1 = overdamped (sluggish, avoid). Design interface:
response (≈ perceived duration, 2π/ω₀) and bounce/dampingFraction (≈ ζ). Tiers by element weight: crisp/precise controls ≈ ζ 1.0 / response 0.2–0.3s; standard UI / cards / panels ζ 0.8–1.0 / 0.3–0.5s; modals / large surfaces ζ 0.9–1.0 / 0.4–0.6s; small delight elements ζ 0.5–0.7 / 0.3–0.5s; signature bounce only ζ 0.35–0.5 / 0.4–0.6s. Bounce budget: most of the site ζ ≥ 0.8 (overshoot ≤ ~1.5%); visible bounce is reserved for the few light/signature elements. Overshoot formula: exp(−ζπ/√(1−ζ²)). Commit spring tokens to match the curve tokens.
- Intensity calibrated to the surface archetype. The purpose taxonomy is constant everywhere; the motion budget is set by what the surface is for. A marketing/brand surface may spend signature, expressive motion; a data/tool/dashboard surface earns motion only for orientation (state change), causality (what just happened), and latency masking — runs crisp and short, and a re-fire under streaming data must never restart or distract. Decide the budget as a taste gate; the taxonomy enforces it.
- Duration as a function of distance and size. Duration grows sub-linearly with travel and element size. Exit ≈ 0.6–0.8× entrance. Perceptual bands: < ~100ms = not perceived as motion; most functional UI = 150–400ms; > ~400–500ms starts to feel sluggish (signature/hero excepted). First frame of feedback < 100ms even when the full animation is longer. Stagger: 20–60ms between adjacent items, total duration capped (never linearly stagger 50 items — cap visible count or use a decay curve).
GATE — clear before GESTURE
checklist check purpose purpose-and-physics-committed
checklist verify purpose
STAGE 1 — Gesture & intent arbitration (scroll vs drag; conflict resolution)
Open references/gesture-arbitration.md. Every gesture conflict must be declared and resolved before a finger is confused.
- Axis lock and activation threshold. Commit only after displacement > ~8–10px (a tap must not be misread as a drag). Assign by the displacement vector's angle: within ±~30° of an axis belongs to that axis; the middle is a diagonal ambiguity zone — pick one policy (dominant-component-first or a small deadzone) and hold it site-wide. First move locks the axis for the rest of the gesture; the other axis is ignored (kills sideways jitter in a vertical scroll).
- The core arbitration law. The inner scroller consumes vertical gestures; unless it has already hit its boundary in the gesture's direction, at which point the gesture hands off to the outer panel. For a full-height panel containing a list scrolled to top: drag down with
scrollTop=0 → inner can't scroll → hand to panel; drag up → list scrolls, panel still. Once the panel starts dragging, lock the inner scroll until the panel returns to full-height and the gesture reverses.
- Mid-gesture handoff — zero-jump is the hard constraint. At handoff, the taker must start from the exact current position and consume only the overflow past the boundary — otherwise the element jumps by the overflow amount. Velocity transfer is a design choice held consistently: chain the residual fling velocity to the outer (iOS-style flick keeps going) or absorb it (stop at the edge, require a deliberate re-pull). Use boundary tolerance
scrollTop ≈ 0 / ≈ maxScroll (~1px).
- Platform primitives — declarative first,
preventDefault last. overscroll-behavior: contain stops scroll-chaining to ancestors (a modal's list at bottom no longer scrolls body) — first tool to reach for. touch-action: pan-y declares browser owns vertical scroll, horizontal gestures go to JS (mandatory for horizontal carousel inside vertical page); pan-x reverse; none = all to JS; manipulation = no double-tap zoom — the declarative workhorse for axis arbitration, and the agent almost never sets it. Pointer Events + setPointerCapture — build for pointer, not mouse-only. Passive-listener trap: touchmove is passive at document level by default (preventDefault silently no-ops); intercepting scroll needs {passive:false} — that hurts scroll perf, so anything solvable with touch-action/overscroll-behavior must NOT use non-passive preventDefault.
- Named conflict patterns, declared. Vertical page + horizontal carousel → page
touch-action: pan-y, carousel handles pan-x, angle threshold on diagonal. Swipe-to-delete row in scroll list → angle test, lock once horizontal commits. Pull-to-refresh → fire only at scrollTop=0 + downward + displacement threshold + rubber-band resistance; overscroll-behavior-y: contain. Edge-swipe drawer over horizontal content → reserve first ~16–20px for drawer. Same-axis nested scroll → inner to boundary then chains to outer via overscroll-behavior. Map/pinch-zoom inside scroll → touch-action: none, take over everything in JS.
- Gesture physics on release. 1:1 during drag — no easing, no spring, direct position tracking. At release: measure velocity over last ~50ms window (EMA of Δx/Δt), never the whole-gesture average (the average badly underestimates a "slow-then-flick"). Project the momentum endpoint:
current + v₀·rate/(1−rate) (v₀ in px/ms, rate ≈ 0.998 normal / 0.99 fast), find nearest snap point to the projection, spring to it carrying v₀. Flick-to-next threshold: |v₀| > ~500 px/s → ignore position, go to next detent in fling direction. Rubber-band: Apple formula b(x,d,c) = (1 − 1/(x·d/c + 1))·c with d ≈ 0.55, c ≈ container dimension; or simplified displacement × 0.2–0.5.
- Interruptibility as a requirement. A spring mid-flight must be grababble: capture current position + velocity, return to 1:1 gesture tracking; on re-release, feed position + velocity back to the spring. This round-trip is the entire secret of "it is one continuous physical object." Model complex draggable interactions as a state machine:
idle → dragging → settling → snapped — explicit legal transitions instead of flag juggling.
GATE — clear before REDUCED-MOTION
checklist check gesture gesture-arbitration-declared
checklist verify gesture
STAGE 2 — Reduced-motion, compositor-only & 60fps
Open references/reduced-motion-and-performance.md and references/the-feel-css-deep-water.md. These are non-negotiables — not taste decisions.
prefers-reduced-motion is a re-tuned path, not an off switch. Replace the vestibular triggers — large translation, scale, parallax, rotation — with a cross-fade or instant cut; keep the necessary feedback (the user still needs to know a panel opened). For gestures: swap momentum/bounce for a fast fade or instant snap — the gesture still works, only the physics flourish is gone. Flicker ≤ 3×/sec (WCAG 2.3.1). Critical information must never be conveyed by motion alone. Every gesture needs a non-gesture alternative (a swipe-to-dismiss panel also has a close button; keyboard scroll; focusable controls). Persons using switch control or voice control cannot perform gestures — gesture must never be the only path.
- Compositor-only properties. The rendering pipeline is parse → style → layout → paint → composite. Animating
width/height/top/left/margin triggers layout reflow and drops frames. Animate transform and opacity only; filter is permitted but paint-heavy. To animate a layout change cheaply: FLIP (First/Last measure → Invert with a counter-transform so it looks unmoved → Play the transform to 0) or the View Transitions API (document.startViewTransition() + view-transition-name — automatic FLIP-style + shared-element morph; works for both SPA state changes and MPA cross-document navigation; prefer it over hand-rolled FLIP). will-change is a temporary hint — set just before the animation, removed after; leaving it on costs GPU memory and may harm more than help. Animate color in oklch/oklab to avoid the sRGB gray dead-zone.
- 60fps held under load. 60fps = 16.7ms/frame; 120fps = 8.3ms/frame — the animation must stay on the compositor thread. Don't stack heavy animations on scroll or first-paint. JS-driven heavy animation blocks the main thread and hurts INP. Quantified check: only
transform/opacity animated · no dropped frames in the Rendering panel · single-interaction main-thread work < ~50ms. For springs: prefer semi-implicit (symplectic) Euler integration over closed-form analytic x(t) — the analytic solution is cheaper but awkward when the target changes mid-flight; per-frame integration handles interruption and target changes natively (why most libraries use it). Use fixed step / sub-stepping at low frame rates to avoid divergence.
- Mask latency to preserve the causal story. The user's action must visibly, immediately cause the result; when the real result is slow, show the optimistic/skeleton response inside the < 100ms feedback window so the cause→effect link is never broken.
transform-origin is the mechanical implementation of causality: a tooltip scaling from its anchor, a menu expanding from the trigger corner, a modal growing from the button that opened it — set the origin to the causal source, not the element's center.
- CSS scroll-driven animations.
animation-timeline: scroll() / view() — no JS, no IntersectionObserver; compositor-friendly when only transform/opacity are animated. Prefer over JS-driven scroll effects.
FINAL GATE
checklist check compositor compositor-only-reduced-motion-and-60fps
checklist verify compositor
checklist show — confirm all three stages passed.
checklist done — clear this run's state.
The thread through all of it
motion is the suite's kinetic conscience — the place where taste about feel is gated, where the agent's default of "animate everything, animate layout, ignore reduced-motion" is refused. It runs after the surface has its visual system, and its motion language is what lookout later measures as felt performance and what bulwark must keep from regressing. The through-line is the suite's own — push correctness into structure — applied to movement: an animation derived from a purpose taxonomy and a physics system can't drift the way a hand-typed 0.3s ease can. The line that keeps motion honest: the document holds the encodable technique (how ζ controls overshoot, how touch-action routes a gesture axis, how FLIP does layout animation without layout cost); the taste — which animations earn their place, how much bounce the brand warrants, where the surface archetype sets the motion budget — stays a gate the user clears. Blur that line and the skill becomes a recipe book; hold it and it stays a lens.
Anti-patterns (use as a pre-flight checklist)
transition: all 0.3s — never. Name the property, name the duration, name the curve. all animates layout properties, triggers reflow, hides intent, and fires on properties you didn't mean to animate.
- Spring where a curve belongs — hover/fade/discrete open: easing curve. Touch/interruptible/gesture-driven: spring. The test: can the animation be grabbed mid-flight? If yes, spring.
- Curve where a spring belongs — a Bézier restarted mid-animation jumps; a spring carries current position and velocity. Re-triggerable gestures always need a spring.
- Animating layout properties —
width, height, top, left, margin, padding animate on the main thread. Use FLIP or View Transitions API instead.
will-change left on permanently — it promotes the layer, costs GPU memory, and may hurt compositing of nearby elements. Set before the animation, remove after.
- No
prefers-reduced-motion path — the media query is mandatory. Replace vestibular triggers with cross-fade or instant cut; keep necessary feedback; never strip motion entirely and leave the user without state information.
- Motion only path — a swipe-to-dismiss must also have a close button; a drag-and-drop must also have keyboard ordering. Gesture as the only path is an accessibility failure.
- Animating color in sRGB/HSL — interpolation crosses the gray dead-zone; use
oklch/oklab.
- Purpose-less animation — if you cannot name the purpose from the seven-job taxonomy, delete it. "It looks nice" is not a purpose.
- Uniform stagger on long lists — cap total duration; use a decay curve so later items accelerate rather than linearly staggering all fifty items.
- Whole-gesture average for velocity — the average underestimates a "slow-then-flick." Always EMA over the last ~50ms window.
preventDefault on passive listeners — silently no-ops at document level; use touch-action and overscroll-behavior instead; only drop to {passive:false} when declaration can't cover it, and document why.
- No activation threshold on drag — a tap becomes a drag at the first pixel. Require > ~8–10px before committing to a drag decision.
- Mid-gesture handoff with a jump — the incoming owner must start from the exact boundary point and consume only the overflow, not reset position to zero.
- Skipping a GATE — and remember: every animation should trace to a purpose and a physics decision; if it doesn't, it was invented by hand.
1---2name: motion3description: The motion, gesture & feel lens for a frontend build — where every animation earns its place and the feel IS the product. Use after the build works, when adding interaction physics, when a UI feels sluggish / janky or the gesture fights the scroll, or auditing animations. The one shift: every animation maps to a PURPOSE and its dynamics come from physics (spring vs easing, never confused); the feel is the product; compositor- only properties + a real reduced-motion path are non-negotiable — the agent animates because it can, feeling none of the jank, so motion must be gated. Triggers on "animation / transition / motion / spring / damping / bounce / easing / physics", "gesture / drag / scroll / swipe / scroll-vs-drag", "feels laggy / disconnected / jank / stiff / floaty", "prefers-reduced-motion / a11y", "FLIP / View Transitions / scroll-driven", "60fps / compositor / layout thrashing / will-change", "feels native / hand-feel / touch / stagger / orchestration / shared element / morph".4---56<!--7Copyright 2026 IamK77 — Licensed under the Apache License, Version 2.0.8See ./LICENSE and ./NOTICE · https://www.apache.org/licenses/LICENSE-2.09-->1011# motion1213!`checklist init ${CLAUDE_SKILL_DIR} --force`1415An *atelier* is a craftsman's studio — the place where judgment is applied to material, where a decision about how something *moves* is as considered as a decision about how it *looks*. `motion` is the feel lens of the `atelier` suite: where the other skills decide *what is true* and *what it looks like*, `motion` decides *what it does when you touch it* — and makes that a **system**, not a pile of ad-hoc durations and default curves. Its product is a written **motion language**: every animation named to a purpose, dynamics derived from physics, gesture conflicts resolved by a declared arbitration policy, and a re-tuned reduced-motion path that preserves meaning. It runs across gated stages and will not advance past a **GATE** until the `checklist` tool clears it — order enforced, substance yours.1617**The governing fact: motion without a purpose is noise; noise is the fingerprint of agent-generated work.** The agent animates because it can: `transition: all 0.3s ease` applied to every state change, a spring dropped in for "feel," layout properties animated because they were convenient, and no `prefers-reduced-motion` path because no one asked for one. Each choice is locally defensible; the sum is jank, wasted attention, and an accessibility failure. The craft is the same subtraction the rest of the suite preaches: **name the purpose first; delete what has none; derive the dynamics from physics; make the platform carry the work.** Do that and two things happen — the surface *communicates* (a well-choreographed state transition tells users where they are; a precise spring on release tells them the object has real weight) and it *performs* (compositor-only properties stay off the main thread; a declared `touch-action` replaces a non-passive event handler).1819This is where the agent era bites:20- **The agent animates decoration.** It reaches for `transition: all 0.3s` or drops a spring on a button fade. Neither has a job; both add noise. The test is merciless: name the purpose or delete the animation.21- **The agent confuses spring and curve.** A hover uses `framer-motion`; a modal fade uses a Bézier. Both "work" until the user grabs the modal mid-animation — the Bézier restarts from scratch; the spring would have carried the velocity. Using a curve where physics is needed, or a spring where a simple discrete 200ms ease-out would do, is a category error the agent makes constantly.22- **The agent forgets gesture arbitration.** A draggable panel is placed inside a scrollable list with no `touch-action`, no `overscroll-behavior`, and no axis-lock — so the finger fights the scroll on every open. The platform primitives exist; they are rarely set.23- **The agent ships no `prefers-reduced-motion` path.** The media query exists, it is never checked, and a user with vestibular sensitivity gets large-translation parallax on every scroll.2425**Read [references/motion-purpose-and-physics.md](references/motion-purpose-and-physics.md) first** — the suite's motion spine; everything from easing direction through spring parameter tiers and orchestration patterns lives here. Load at the start, re-check at every gate.2627**Speak the user's language.** The decisions are the user's — which animations earn their place, how much bounce the brand personality warrants, where the surface archetype (data tool vs marketing) changes the motion budget. Read their fluency and gloss a term on first use (*purpose taxonomy*, *spring / damping ratio ζ*, *easing curve / cubic-bezier*, *compositor vs layout properties*, *FLIP / View Transitions API*, *scroll-driven animation*, *overscroll-behavior / touch-action*, *prefers-reduced-motion*, *stagger / orchestration*). A motion system the user can't explain to a colleague is style imposed, not shared.2829## The reference library3031The depth lives in `references/`. Open each when a stage sends you there — not all upfront.3233- **[references/motion-purpose-and-physics.md](references/motion-purpose-and-physics.md)** — the heart: the seven-purpose taxonomy (causality / spatial continuity / feedback / progress / attention / depth / expression); the easing-vs-spring decision tree and why they can never be swapped; direction rules (ease-out for entrances, ease-in for exits); duration as a function of distance; spring parameter tiers by element weight; orchestration patterns (shared-element, stagger, container transform, enter/exit coordination, Z-axis lift); the 12 animation principles that UI actually uses; the ethics / attention test; and restraint as the default posture.34- [references/gesture-arbitration.md](references/gesture-arbitration.md) — nested gesture conflict resolution: the core arbitration law (inner scroller owns until boundary); axis lock and activation threshold; mid-gesture handoff without a jump; platform primitives (`touch-action`, `overscroll-behavior`, Pointer Events, `setPointerCapture`, the passive-listener trap); named conflict patterns and recipes (panel-wraps-scroll, horizontal carousel, swipe-to-delete, pull-to-refresh, edge-swipe drawer, same-axis nested, map/pinch-zoom); and platform convention differences (iOS / Android / Web).35- [references/the-feel-css-deep-water.md](references/the-feel-css-deep-water.md) — the CSS mechanics that separate "knows how to animate" from "knows why it works": the rendering pipeline and where each property falls; compositor-only properties and why `will-change` is temporary; FLIP and the View Transitions API; CSS scroll-driven animations (`animation-timeline: scroll()/view()`); spring numerical integration and why semi-implicit Euler; the stacking-context and containing-block traps that break `fixed` overlays; modern CSS that replaces JS hacks (`:has()`, container queries, anchor positioning, `@property`); and how to use DevTools to diagnose frame drops, paint flashing, and cascade failures.36- [references/reduced-motion-and-performance.md](references/reduced-motion-and-performance.md) — the non-negotiables as technique: the correct `prefers-reduced-motion` contract (re-tune, don't strip; replace vestibular triggers — translation/scale/parallax/rotation — with cross-fade or instant cut; gestures keep working); flicker ≤ 3×/sec (WCAG 2.3.1); every gesture needs a non-gesture alternative; 60fps / 16.7ms budget and how to hold it; the `transform`/`opacity`-only rule and its exceptions (`filter`); latency masking and the < 100ms first-frame rule; state machine framing for complex interactions; color animation in `oklch`/`oklab`.3738> **The arc is one motion system.** Three stages — purpose & physics · gesture & intent · compositor, performance & reduced-motion — turn a working surface into one that *feels* right: animations earn their place and get the right dynamics; fingers are never fighting the scroll; and a user who prefers reduced motion gets a re-tuned experience, not silence.3940> **Greenfield or audit? Decide the entry.** On a new surface, walk STAGE 0→2 in order. On an existing surface, inventory first: every animation (CSS `transition`, `animation`, JS-driven), its duration and property, and its apparent purpose — cluster by whether the purpose is nameable, whether it animates layout properties, and whether a reduced-motion path exists. That inventory is not a deliverable to perfect; it is the raw material the gates consume. The technique for finding where the un-named animations hide (DevTools Animations panel, a `transition: all` grep, a `will-change` audit) is in **[references/the-feel-css-deep-water.md](references/the-feel-css-deep-water.md)**. The taste call stays a gate: for each cluster you still decide which animations earn their place and what the physics should be.4142---4344## STAGE 0 — Purpose & physics (name every animation to a job; set the physics)4546Open **[references/motion-purpose-and-physics.md](references/motion-purpose-and-physics.md)**. Establish the purpose taxonomy and the motion tokens before anything is tuned.4748- **Every animation maps to a purpose — or is deleted.** Before touching timing or curve, classify into the seven jobs: *causality / origin story* (show where a thing came from — the button that triggered the panel); *spatial continuity* (state/route change that preserves the mental map — shared-element morph); *feedback* (confirm input received, first frame < 100ms); *progress / status* (loading, determinate vs indeterminate); *attention / hierarchy* (staggered entrance, what to look at first); *depth / spatial model* (modal forward, drawer from edge); *expression / brand* (the emotional layer — spend it on one signature moment). Anything that doesn't map is noise. Delete it. **Restraint is the default posture; over-animation is an agent-era tell.**49- **Physics by role — spring or curve, never confused.** The decision is mechanical: *discrete, state-to-state, known duration, not interruptible* → **easing curve** (CSS `transition` / Web Animations API). *Gesture-driven, interruptible, needs "physics," must carry velocity* → **spring** (stiffness / damping / mass; library-driven). A Bézier curve restarted mid-flight jumps; a spring carries current position and velocity — that round-trip is the fingerprint of native feel. Easing direction: **entrance → ease-out** (fast start, settle gently — responding to a user action must start instantly); **exit → ease-in** (accelerate away, get out of the way); **on-screen A→B → ease-in-out**. Pure ease-in on an entrance is forbidden — slow start reads as stuck. **Commit curve tokens** (`cubic-bezier(0, 0, 0.2, 1)` ease-out · `cubic-bezier(0.4, 0, 1, 1)` ease-in · `cubic-bezier(0.4, 0, 0.2, 1)` ease-in-out · an asymmetric expressive curve for signature moments). `linear` is reserved for genuinely constant motion: infinite spinner, progress fill, 1:1 drag tracking.50- **Spring parameters from the damping ratio ζ, not by feel.** Design in the two derived quantities: **ω₀ = √(k/m)** (speed) and **ζ = c / (2·√(k·m))** (character). ζ < 1 = underdamped (bounce); ζ = 1 = critical (fastest no-bounce settle); ζ > 1 = overdamped (sluggish, avoid). Design interface: **`response`** (≈ perceived duration, `2π/ω₀`) and **`bounce/dampingFraction`** (≈ ζ). Tiers by element weight: crisp/precise controls ≈ ζ 1.0 / response 0.2–0.3s; standard UI / cards / panels ζ 0.8–1.0 / 0.3–0.5s; modals / large surfaces ζ 0.9–1.0 / 0.4–0.6s; small delight elements ζ 0.5–0.7 / 0.3–0.5s; signature bounce only ζ 0.35–0.5 / 0.4–0.6s. **Bounce budget: most of the site ζ ≥ 0.8** (overshoot ≤ ~1.5%); visible bounce is reserved for the few light/signature elements. Overshoot formula: `exp(−ζπ/√(1−ζ²))`. Commit spring tokens to match the curve tokens.51- **Intensity calibrated to the surface archetype.** The purpose taxonomy is constant everywhere; the motion budget is set by what the surface is *for*. A marketing/brand surface may spend signature, expressive motion; a data/tool/dashboard surface earns motion only for *orientation* (state change), *causality* (what just happened), and *latency masking* — runs crisp and short, and a re-fire under streaming data must never restart or distract. Decide the budget as a taste gate; the taxonomy enforces it.52- **Duration as a function of distance and size.** Duration grows sub-linearly with travel and element size. **Exit ≈ 0.6–0.8× entrance.** Perceptual bands: < ~100ms = not perceived as motion; most functional UI = 150–400ms; > ~400–500ms starts to feel sluggish (signature/hero excepted). First frame of feedback < 100ms even when the full animation is longer. Stagger: 20–60ms between adjacent items, total duration capped (never linearly stagger 50 items — cap visible count or use a decay curve).5354### GATE — clear before GESTURE551. `checklist check purpose purpose-and-physics-committed`562. `checklist verify purpose`5758---5960## STAGE 1 — Gesture & intent arbitration (scroll vs drag; conflict resolution)6162Open **[references/gesture-arbitration.md](references/gesture-arbitration.md)**. Every gesture conflict must be declared and resolved before a finger is confused.6364- **Axis lock and activation threshold.** Commit only after displacement **> ~8–10px** (a tap must not be misread as a drag). Assign by the displacement vector's angle: within **±~30°** of an axis belongs to that axis; the middle is a diagonal ambiguity zone — pick one policy (dominant-component-first or a small deadzone) and hold it site-wide. **First move locks the axis** for the rest of the gesture; the other axis is ignored (kills sideways jitter in a vertical scroll).65- **The core arbitration law.** The inner scroller consumes vertical gestures; *unless* it has already hit its boundary in the gesture's direction, at which point the gesture hands off to the outer panel. For a full-height panel containing a list scrolled to top: drag down with `scrollTop=0` → inner can't scroll → hand to panel; drag up → list scrolls, panel still. Once the panel starts dragging, lock the inner scroll until the panel returns to full-height and the gesture reverses.66- **Mid-gesture handoff — zero-jump is the hard constraint.** At handoff, the taker must start from the *exact current position* and consume only the **overflow past the boundary** — otherwise the element jumps by the overflow amount. Velocity transfer is a design choice held consistently: *chain* the residual fling velocity to the outer (iOS-style flick keeps going) or *absorb* it (stop at the edge, require a deliberate re-pull). Use boundary tolerance `scrollTop ≈ 0 / ≈ maxScroll` (~1px).67- **Platform primitives — declarative first, `preventDefault` last.** `overscroll-behavior: contain` stops scroll-chaining to ancestors (a modal's list at bottom no longer scrolls `body`) — first tool to reach for. `touch-action: pan-y` declares browser owns vertical scroll, horizontal gestures go to JS (mandatory for horizontal carousel inside vertical page); `pan-x` reverse; `none` = all to JS; `manipulation` = no double-tap zoom — the declarative workhorse for axis arbitration, and the agent almost never sets it. Pointer Events + `setPointerCapture` — build for pointer, not mouse-only. **Passive-listener trap:** `touchmove` is passive at document level by default (`preventDefault` silently no-ops); intercepting scroll needs `{passive:false}` — that hurts scroll perf, so anything solvable with `touch-action`/`overscroll-behavior` must NOT use non-passive `preventDefault`.68- **Named conflict patterns, declared.** *Vertical page + horizontal carousel* → page `touch-action: pan-y`, carousel handles pan-x, angle threshold on diagonal. *Swipe-to-delete row in scroll list* → angle test, lock once horizontal commits. *Pull-to-refresh* → fire only at `scrollTop=0` + downward + displacement threshold + rubber-band resistance; `overscroll-behavior-y: contain`. *Edge-swipe drawer over horizontal content* → reserve first ~16–20px for drawer. *Same-axis nested scroll* → inner to boundary then chains to outer via `overscroll-behavior`. *Map/pinch-zoom inside scroll* → `touch-action: none`, take over everything in JS.69- **Gesture physics on release.** 1:1 during drag — no easing, no spring, direct position tracking. At release: measure velocity over **last ~50ms window (EMA of Δx/Δt), never the whole-gesture average** (the average badly underestimates a "slow-then-flick"). Project the momentum endpoint: `current + v₀·rate/(1−rate)` (v₀ in px/ms, rate ≈ 0.998 normal / 0.99 fast), find nearest snap point *to the projection*, spring to it carrying v₀. **Flick-to-next threshold:** |v₀| > ~500 px/s → ignore position, go to next detent in fling direction. **Rubber-band:** Apple formula `b(x,d,c) = (1 − 1/(x·d/c + 1))·c` with d ≈ 0.55, c ≈ container dimension; or simplified `displacement × 0.2–0.5`.70- **Interruptibility as a requirement.** A spring mid-flight must be grababble: capture current *position + velocity*, return to 1:1 gesture tracking; on re-release, feed position + velocity back to the spring. This round-trip is the entire secret of "it is one continuous physical object." Model complex draggable interactions as a **state machine**: `idle → dragging → settling → snapped` — explicit legal transitions instead of flag juggling.7172### GATE — clear before REDUCED-MOTION731. `checklist check gesture gesture-arbitration-declared`742. `checklist verify gesture`7576---7778## STAGE 2 — Reduced-motion, compositor-only & 60fps7980Open **[references/reduced-motion-and-performance.md](references/reduced-motion-and-performance.md)** and **[references/the-feel-css-deep-water.md](references/the-feel-css-deep-water.md)**. These are non-negotiables — not taste decisions.8182- **`prefers-reduced-motion` is a re-tuned path, not an off switch.** Replace the vestibular triggers — large translation, scale, parallax, rotation — with a cross-fade or instant cut; *keep* the necessary feedback (the user still needs to know a panel opened). For gestures: swap momentum/bounce for a fast fade or instant snap — the gesture still works, only the physics flourish is gone. Flicker **≤ 3×/sec** (WCAG 2.3.1). Critical information must never be conveyed by motion alone. Every gesture needs a **non-gesture alternative** (a swipe-to-dismiss panel also has a close button; keyboard scroll; focusable controls). Persons using switch control or voice control cannot perform gestures — gesture must never be the only path.83- **Compositor-only properties.** The rendering pipeline is parse → style → **layout → paint → composite**. Animating `width/height/top/left/margin` triggers layout reflow and drops frames. Animate **`transform` and `opacity` only**; `filter` is permitted but paint-heavy. To animate a layout change cheaply: **FLIP** (First/Last measure → Invert with a counter-transform so it *looks* unmoved → Play the transform to 0) or the **View Transitions API** (`document.startViewTransition()` + `view-transition-name` — automatic FLIP-style + shared-element morph; works for both SPA state changes and MPA cross-document navigation; prefer it over hand-rolled FLIP). **`will-change` is a temporary hint** — set just before the animation, removed after; leaving it on costs GPU memory and may harm more than help. Animate color in **`oklch`/`oklab`** to avoid the sRGB gray dead-zone.84- **60fps held under load.** 60fps = 16.7ms/frame; 120fps = 8.3ms/frame — the animation must stay on the compositor thread. Don't stack heavy animations on scroll or first-paint. JS-driven heavy animation blocks the main thread and hurts INP. **Quantified check:** only `transform`/`opacity` animated · no dropped frames in the Rendering panel · single-interaction main-thread work < ~50ms. For springs: prefer **semi-implicit (symplectic) Euler** integration over closed-form analytic `x(t)` — the analytic solution is cheaper but awkward when the target changes mid-flight; per-frame integration handles interruption and target changes natively (why most libraries use it). Use fixed step / sub-stepping at low frame rates to avoid divergence.85- **Mask latency to preserve the causal story.** The user's action must *visibly, immediately* cause the result; when the real result is slow, show the optimistic/skeleton response inside the < 100ms feedback window so the cause→effect link is never broken. `transform-origin` is the mechanical implementation of causality: a tooltip scaling from its anchor, a menu expanding from the trigger corner, a modal growing from the button that opened it — set the origin to the causal source, not the element's center.86- **CSS scroll-driven animations.** `animation-timeline: scroll()` / `view()` — no JS, no IntersectionObserver; compositor-friendly when only `transform`/`opacity` are animated. Prefer over JS-driven scroll effects.8788### FINAL GATE891. `checklist check compositor compositor-only-reduced-motion-and-60fps`902. `checklist verify compositor`913. `checklist show` — confirm all three stages passed.924. `checklist done` — clear this run's state.9394---9596## The thread through all of it9798`motion` is the suite's **kinetic conscience** — the place where taste about feel is gated, where the agent's default of "animate everything, animate layout, ignore reduced-motion" is refused. It runs after the surface has its visual system, and its motion language is what `lookout` later measures as felt performance and what `bulwark` must keep from regressing. The through-line is the suite's own — *push correctness into structure* — applied to movement: an animation derived from a purpose taxonomy and a physics system can't drift the way a hand-typed `0.3s ease` can. The line that keeps `motion` honest: **the document holds the encodable technique (how ζ controls overshoot, how `touch-action` routes a gesture axis, how FLIP does layout animation without layout cost); the taste — which animations earn their place, how much bounce the brand warrants, where the surface archetype sets the motion budget — stays a gate the user clears.** Blur that line and the skill becomes a recipe book; hold it and it stays a lens.99100## Anti-patterns (use as a pre-flight checklist)101102- **`transition: all 0.3s`** — never. Name the property, name the duration, name the curve. `all` animates layout properties, triggers reflow, hides intent, and fires on properties you didn't mean to animate.103- **Spring where a curve belongs** — hover/fade/discrete open: easing curve. Touch/interruptible/gesture-driven: spring. The test: can the animation be grabbed mid-flight? If yes, spring.104- **Curve where a spring belongs** — a Bézier restarted mid-animation jumps; a spring carries current position and velocity. Re-triggerable gestures always need a spring.105- **Animating layout properties** — `width`, `height`, `top`, `left`, `margin`, `padding` animate on the main thread. Use FLIP or View Transitions API instead.106- **`will-change` left on permanently** — it promotes the layer, costs GPU memory, and may hurt compositing of nearby elements. Set before the animation, remove after.107- **No `prefers-reduced-motion` path** — the media query is mandatory. Replace vestibular triggers with cross-fade or instant cut; keep necessary feedback; never strip motion entirely and leave the user without state information.108- **Motion only path** — a swipe-to-dismiss must also have a close button; a drag-and-drop must also have keyboard ordering. Gesture as the only path is an accessibility failure.109- **Animating color in sRGB/HSL** — interpolation crosses the gray dead-zone; use `oklch`/`oklab`.110- **Purpose-less animation** — if you cannot name the purpose from the seven-job taxonomy, delete it. "It looks nice" is not a purpose.111- **Uniform stagger on long lists** — cap total duration; use a decay curve so later items accelerate rather than linearly staggering all fifty items.112- **Whole-gesture average for velocity** — the average underestimates a "slow-then-flick." Always EMA over the last ~50ms window.113- **`preventDefault` on passive listeners** — silently no-ops at document level; use `touch-action` and `overscroll-behavior` instead; only drop to `{passive:false}` when declaration can't cover it, and document why.114- **No activation threshold on drag** — a tap becomes a drag at the first pixel. Require > ~8–10px before committing to a drag decision.115- **Mid-gesture handoff with a jump** — the incoming owner must start from the exact boundary point and consume only the overflow, not reset position to zero.116- **Skipping a GATE** — and remember: every animation should trace to a purpose and a physics decision; if it doesn't, it was invented by hand.