diff --git a/apps/storybook/src/Foundations.stories.tsx b/apps/storybook/src/Foundations.stories.tsx
index 76c476d..112c54a 100644
--- a/apps/storybook/src/Foundations.stories.tsx
+++ b/apps/storybook/src/Foundations.stories.tsx
@@ -562,48 +562,345 @@ function Elevation() {
/* ------------------------------------------------------------------- Motion */
-const EASES = ['standard', 'emphasized', 'decelerate'];
-const DURATIONS = ['fast', 'base', 'slow'];
+const DURATIONS = ['micro', 'fast', 'base', 'slow', 'slower'] as const;
+const EASES = ['standard', 'emphasized', 'decelerate', 'spring', 'spring-strong'] as const;
+
+// Each shipped `animate-*` preset, mapped to the duration + ease tokens it composes
+// (mirrors packages/roots/preset-motion.css). `spring-strong` has no preset by design.
+// Full literal class strings (NOT template-built) so Tailwind v4's scanner emits each
+// utility - the same rule the Radii/Elevation sections note. The `cls` is applied verbatim
+// on a key-remounted element so clicking Play replays the real Tailwind animation.
+const PRESETS: {
+ name: string;
+ cls: string;
+ duration: string;
+ ease: string;
+ anim: string;
+ note?: string;
+}[] = [
+ {
+ name: 'pop-in',
+ cls: 'animate-pop-in',
+ duration: 'base',
+ ease: 'spring',
+ anim: 'pop-in',
+ },
+ {
+ name: 'pop-out',
+ cls: 'animate-pop-out',
+ duration: 'fast',
+ ease: 'standard',
+ anim: 'pop-out',
+ },
+ {
+ name: 'shake',
+ cls: 'animate-shake motion-reduce:animate-none',
+ duration: 'slow',
+ ease: 'standard',
+ anim: 'shake',
+ note: 'A repeated translate is a vestibular trigger, so gating this preset behind prefers-reduced-motion is mandatory - not optional polish.',
+ },
+ {
+ name: 'fade-in',
+ cls: 'animate-fade-in',
+ duration: 'base',
+ ease: 'standard',
+ anim: 'fade-in',
+ },
+ {
+ name: 'fade-out',
+ cls: 'animate-fade-out',
+ duration: 'fast',
+ ease: 'standard',
+ anim: 'fade-out',
+ },
+];
+
+const mono: React.CSSProperties = {
+ fontFamily: 'var(--font-mono)',
+ fontSize: 'var(--text-xs)',
+ color: 'var(--color-text-subtle)',
+};
+
+const playBtn: React.CSSProperties = {
+ fontFamily: 'var(--font-sans)',
+ fontSize: 'var(--text-xs)',
+ fontWeight: 600,
+ color: 'var(--color-primary-foreground)',
+ background: 'var(--color-primary)',
+ border: 'none',
+ borderRadius: 'var(--radius-md)',
+ padding: '0.25rem 0.75rem',
+ cursor: 'pointer',
+};
+
+const th: React.CSSProperties = { padding: '6px 16px 6px 0', textAlign: 'left' };
+const td: React.CSSProperties = { padding: '8px 16px 8px 0', verticalAlign: 'middle' };
+
+// Durations - each row's bar travels the track for exactly its own duration. Clicking Play
+// bumps a counter used as the bar's React `key`, remounting it so the CSS transition restarts.
+function DurationRow({ name }: { name: string }) {
+ const [tick, setTick] = useState(0);
+ return (
+
+
duration-{name}
+
{tokenVal(`duration-${name}`)}
+
+
+
+
+
+
+
+
+
+ );
+}
+
+// Easings - a dot travels a fixed duration (--duration-slow) using each curve. Clicking the
+// track (or the Play button) bumps a counter used as the dot's React `key`, remounting it so
+// the CSS animation restarts. The track is sized so the spring curves' OVERSHOOT (past the
+// target, then settle back) stays fully inside it - see the geometry note in the CSS below.
+function EaseRow({ name }: { name: string }) {
+ const [tick, setTick] = useState(0);
+ const replay = () => setTick((t) => t + 1);
+ return (
+
+ );
+}
+
+// A preset player - clicking Play remounts the sample with the literal `animate-*` class so
+// the ACTUAL Tailwind utility runs. `animationName` is the preset's label, also used as the
+// badge text; the remount (key bump) restarts the "out" presets so they visibly animate away.
+function PresetPlayer({
+ cls,
+ animationName,
+}: {
+ cls: string;
+ animationName: string;
+}) {
+ const [tick, setTick] = useState(0);
+ return (
+
- Hover a bar to play its easing/duration token.
+
Motion - durations, easing & presets
+
+ All motion composes the --duration-* and --ease-* tokens - the
+ durations and curves below, and the shipped animate-* presets, all read their
+ values from Roots. Consumers gate presets with motion-reduce:animate-none.
+ Each bar travels the track for exactly its token duration. Click Play to replay.
+
+
+
+
+
Token
+
Value
+
Track
+
+
+
+
+ {DURATIONS.map((d) => (
+
+ ))}
+
+
+
+
Easings
+
+ A dot crosses using each curve over --duration-slow. Click the track or the
+ Play button to replay. The spring and spring-strong curves
+ overshoot the target and settle back.
+
+
+
+
+
Token
+
Value
+
Track
+
+
+
+
+ {EASES.map((e) => (
+
+ ))}
+
+
+
+
Presets (animate-* utilities)
+
+ Each preset runs the real Tailwind utility on a sample; Play remounts it to replay. The
+ composition line is derived from the tokens, so it can't drift. Gate every preset with{' '}
+ motion-reduce:animate-none.
+
);
}
diff --git a/docs/feedback/0016-verify-motion-in-flight-not-at-rest.md b/docs/feedback/0016-verify-motion-in-flight-not-at-rest.md
new file mode 100644
index 0000000..c4d015a
--- /dev/null
+++ b/docs/feedback/0016-verify-motion-in-flight-not-at-rest.md
@@ -0,0 +1,37 @@
+# 0016 - A static screenshot verifies layout, not motion
+
+## Symptom
+
+The Storybook motion page (spec 0034) shipped its first pass with two motion defects that passed a
+build + a full-page screenshot: (1) the easing track (`360px`, `overflow:hidden`) clipped
+`ease-spring-strong`'s overshoot - the dot's peak (~392px) was cut off by ~30px, hiding the exact
+bounce the row exists to demonstrate; and (2) a "hover the track to play" affordance was a CSS
+no-op (the `:hover` rule re-declared the same `animation-name`, which does not restart a CSS
+animation). Both were caught by the designer + engineer review, not by the pre-PR verification.
+
+## Root cause
+
+The change was verified by a single static full-page screenshot taken after load. A settled
+screenshot captures the animation's REST state, so a mid-flight clip (or an overshoot that never
+fit) is invisible - the dot had already returned inside the track by capture time. And the hover
+"replay" was never actually watched firing; re-declaring an identical `animation-name` on `:hover`
+produces no restart, so the affordance looked plausible in code but did nothing.
+
+## Fix
+
+- Size an overshoot/spring stage for the PEAK, not the rest position: peak = travel x
+ (max bezier y). Widened the track 360 -> 480px so the 392.5px peak clears with room.
+- Verify motion at its EXTREMES: RAF-sampled the running animation to catch the max right edge, and
+ additionally froze the dot at the computed peak and screenshotted that - not just a settled frame.
+- Replaced the dead `:hover` with a real click-to-replay (remounts via the state key), and only
+ claimed the affordance after watching it fire.
+
+## Learning
+
+When you verify MOTION, capture it IN FLIGHT, not at rest. A single settled screenshot proves
+layout, not animation - it cannot see clipping, overshoot, or a mid-flight glitch. For any
+spring/overshoot demo, size the container for the PEAK position (compute it, add clearance) and
+prove the extreme is in-bounds by frame-sampling the live animation or freezing at the computed
+peak. And never ship a motion affordance you have not watched trigger - a CSS `:hover` that
+re-declares an identical `animation-name` is a no-op. Generalizes past this page to any motion demo
+or animated component this design system builds.
diff --git a/docs/overview/features.md b/docs/overview/features.md
index 044598a..9e2b748 100644
--- a/docs/overview/features.md
+++ b/docs/overview/features.md
@@ -61,7 +61,10 @@ theme only; the dark remap + runtime switching are 0004.
(`p-4` = 1rem) derive from a single `--spacing: 0.25rem` base.
- **Foundations stories** - Storybook `Foundations` section renders ramps + semantic swatches,
the Figtree specimen + type scale + weights + leading, spacing, radii, elevation, motion, and
- a WCAG AA contrast table. The visual lock surface.
+ a WCAG AA contrast table. The visual lock surface. **Motion** documents the durations and
+ easings tables (literal values from `tokens`, with replayable bars and overshooting spring
+ tracks) plus interactive pop / shake / fade preset players that run the real `animate-*`
+ utilities and show each preset's token composition; `shake` is gated `motion-reduce:animate-none`.
- **Contrast** - all primary text roles meet WCAG AA (≥ 4.5:1) on their intended surfaces;
`text-subtle` (tertiary) meets AA-large (≥ 3:1), documented as for large/non-essential text.
Guarded by an **executable contrast test** (`tokens.test.ts`) that resolves each role to its
diff --git a/docs/overview/learnings.md b/docs/overview/learnings.md
index 4f14f84..efc8881 100644
--- a/docs/overview/learnings.md
+++ b/docs/overview/learnings.md
@@ -521,3 +521,17 @@ encode the test that **fails** if it regresses (e.g. an occurrence-count guard),
presence check can't see a duplicate. This recurs with
[keyboard/controlled coverage (0014)](../feedback/0014-interactive-component-test-coverage.md): the
shared root is testing the happy representative instead of the whole contract.
+
+## Verify motion in flight, not at rest
+
+The Storybook motion page (0034) shipped a first pass where the easing track clipped
+`spring-strong`'s overshoot and a "hover to play" affordance was a dead CSS no-op - both passed a
+build and a full-page screenshot, because a settled screenshot captures the REST state and the
+overshoot had already returned in-bounds by capture time. Caught by review. See
+[`docs/feedback/0016-verify-motion-in-flight-not-at-rest.md`](../feedback/0016-verify-motion-in-flight-not-at-rest.md).
+
+**Apply it:** a static screenshot proves layout, not animation - it cannot see clipping, overshoot,
+or a mid-flight glitch. For any spring/overshoot demo, size the stage for the **peak** (compute
+`travel x max-bezier-y`, add clearance) and prove the extreme is in-bounds by frame-sampling the
+live animation or freezing at the computed peak. Never ship a motion affordance you have not
+watched fire (a CSS `:hover` re-declaring an identical `animation-name` restarts nothing).
diff --git a/docs/specs/0034-motion-foundations-page.md b/docs/specs/0034-motion-foundations-page.md
new file mode 100644
index 0000000..9d64e6d
--- /dev/null
+++ b/docs/specs/0034-motion-foundations-page.md
@@ -0,0 +1,78 @@
+# 0034 - Motion foundations page (tokens table + interactive presets)
+
+## Problem
+
+Canopy's Storybook Foundations has a `Motion` story, but it predates spec 0033: it shows only the
+old three easings (`standard/emphasized/decelerate`) and three durations (`fast/base/slow`), paired
+1:1, with no literal-value table and no demo of the animation presets. Since 0033 the token set is
+larger (5 easings incl. `spring`/`spring-strong`, 5 durations incl. `micro`/`slower`) and now ships
+ready-made `animate-*` presets (`pop-in/out`, `shake`, `fade-in/out`) - none of which are
+documented or shown in motion. A designer or consumer has no single page to read the literal values
+AND feel the motion.
+
+Who it is for: designers picking a curve/duration, and engineers choosing a preset - both want the
+numbers and the motion side by side, in the living Storybook.
+
+## Outcome
+
+The Foundations `Motion` story becomes a complete motion reference, driven by the generated Roots
+tokens (no hardcoded values), reading correctly in light and dark:
+
+- **Durations** - a table of all five (`micro` 80ms, `fast` 120ms, `base` 200ms, `slow` 320ms,
+ `slower` 480ms) with the literal value pulled from `tokens`, each with a replayable bar that
+ travels for exactly that duration.
+- **Easings** - a table of all five (`standard`, `emphasized`, `decelerate`, `spring`,
+ `spring-strong`) with the literal `cubic-bezier(...)` value from `tokens`, each with an
+ interactive track (click/hover to send a dot across using that curve) so the overshoot of the
+ spring curves is visible.
+- **Presets** - each `animate-*` preset (`pop-in`, `pop-out`, `shake`, `fade-in`, `fade-out`)
+ shown with: a Play button that runs the ACTUAL Tailwind utility on a sample element (re-triggered
+ via a React key remount), and its composition (e.g. `animate-pop-in = pop-in . duration-base .
+ ease-spring`) read from the token values. The `shake` demo is gated with
+ `motion-reduce:animate-none` and carries a visible note that this gating is mandatory (a
+ vestibular trigger), modelling correct consumer usage.
+- A short intro paragraph noting all motion composes the `--duration-*` / `--ease-*` tokens and
+ that consumers gate presets with `motion-reduce`.
+
+## Scope
+
+In:
+
+- Rewrite the `Motion` component in `apps/storybook/src/Foundations.stories.tsx` (still the
+ `Foundations` / `Motion` story - this IS the core-tokens page; no new story file).
+- Use FULL literal class names for the `animate-*` utilities (`animate-pop-in`, ... ) so Tailwind
+ v4's source scanner emits them (same rule the file already notes for radii/shadows).
+- Values come from the typed `tokens` export (already imported), matching the other Foundations
+ sections; nothing hardcoded.
+
+Out:
+
+- No new Roots tokens or preset changes (0033 shipped those; this only documents them).
+- No changes to other Foundations sections (colours, type, spacing, radii, elevation, contrast).
+- `spring-strong` has no shipped `animate-*` preset (by design); it appears in the easings table +
+ track, not as a preset player. Not adding a preset just to demo it.
+- No visual-regression/Playwright test committed to the repo (rendering is verified during build).
+
+## Approach
+
+Mirror the existing Foundations idioms exactly: the `wrap`/`h2` styles, `tokenVal()` reader, a
+scoped `