Light moves. Elements don’t.
The interaction budget — enter, exit, press, spring — is inherited from the foundation unchanged. What v3 adds is atmosphere: three sanctioned light sweeps plus a handful of life signals that keep the scene breathing between interactions. The two systems don’t compete — sweeps run on the stage, the budget runs on what the user touches.
The foundation budget
Unchanged from the foundation. Four categories, one principle: premium motion is restraint plus precision, under 300ms, GPU-compositable properties only. Every button on this page obeys it — hover and press them.
The three sanctioned sweeps
GRAMMAR-v3 §6. Light moves; elements don’t. Nothing here translates, scales, or rotates a layout element — every sweep is a background-position, transform-on-a-pseudo-element, or opacity pass across something that stays put.
opacity 0.75 → 1 · line + soft under-glow
translateX(-140% → 160%) skewX(-18deg) on a pseudo-element, opacity of the surface never changes. Pairs with the 8° tilt / hover-flattens-to-3° / floor-shadow recipe — GRAMMAR-v3 §5.
One sweep dose per element, per GRAMMAR-v3 §1: a page section gets one phrase shine, a theater frame gets one sheen loop. Never stack sweeps on the same object.
Life signals
Ambient, not sweeping — these communicate state inside product surfaces, and the theater scene itself. A dead artifact (a panel with no teal pulse, no caret, no breathing scene) is a brand violation, not just a missed detail.
The hero-rise entrance
The only sanctioned entrance for marketing hero content. 26px rise + fade, staggered 120ms per element, 0.9s cubic-bezier(0.16,1,0.3,1). Page-local — not a token-file keyframe, so copy the recipe rather than importing a name.
.hero > * { animation: hero-rise 0.9s cubic-bezier(0.16,1,0.3,1) both; }
.hero > *:nth-child(1) { animation-delay: 0ms; }
.hero > *:nth-child(2) { animation-delay: 120ms; }
.hero > *:nth-child(3) { animation-delay: 240ms; }
@keyframes hero-rise {
from { opacity: 0; transform: translateY(26px); }
to { opacity: 1; transform: translateY(0); }
}
/* No parallax gimmicks, no scroll-jacking — GRAMMAR-v3 §6. */
Canonical keyframe names
These seven live in tokens-v3-gravity.css. The names are API — do not rename, do not redeclare locally. Everything on this page references them by name.
| Keyframe | Cycle | Motion | Used by |
|---|---|---|---|
| ox-shine | 7s --ox-shine-cycle | background-position 100%→0% | .phrase (sage/ink italic phrase) |
| ox-sheen | 12s --ox-sheen-cycle | translateX(-140%→160%) skewX(-18deg) | .frame__sheen span (product theater glass) |
| ox-ambient | 11s --ox-ambient-cycle | opacity 1→.65, scale 1→1.07 | .scene__ambient, .exhibit__ambient |
| ox-horizon | 10s | opacity .75→1 | .horizon::before / ::after (line + glow) |
| ox-teal-pulse | 1.2s / 1.6s | opacity .7→1, scale 1→1.25 | .dot-label--live::before (1.2s) · .chip--live::before (1.6s) |
| ox-caret | 1.1s steps(1) | opacity 1↔0 blink | .caret (typed verdict) |
| ox-breathe | 4s --ox-breathe-cycle | drop-shadow color cross-fade, blue→teal | .hero__mark / .mark-breathe |
hero-rise is deliberately absent from this table — GRAMMAR-v3 §9b files it as an implementation recipe (§03 above), not a token-file keyframe, because its stagger delays are per-element and set by the consuming page.
Where sweeps sit vs. the budget
The one question every implementer asks: does a sweep relax the interaction budget? No. They operate on different objects, at different frequencies, for different reasons.
Sweeps are ambient, marketing- and product-chrome motion — continuous, looping, felt rather than clicked, and never gated behind an interaction. They never block input and never appear on a table row, a form field, or anything read at 100+ views/day. Interactions — every button, drawer, modal, toast, and hover in the product — still obey the strict foundation budget: enter 220ms, exit 130ms, press 80ms. Sweeps don’t widen that budget; they run one layer above it, on the scene itself.
- · Continuous, infinite loop
- · Runs whether or not the user acts
- · Lives on hero copy, product frames, section dividers
- · One dose per object (§01)
- · One-shot, tied to a user action
- · Fires only on enter / exit / press
- · Lives on modals, drawers, toasts, buttons, cards
- · Frequency-gated — see §06
What never moves
Inherited from the foundation, unchanged. Premium legal software signals trust through restraint — animate less as usage frequency increases (Emil Kowalski’s frequency rule).
| Element | Frequency | Reason |
|---|---|---|
| Table row content, text in cells | 100+/day | Any delay feels like latency, not polish. |
| Keyboard shortcuts (⌘K, ⌘/, bulk actions) | 100+/day | 0ms is the only correct answer for power users. |
| Sidebar navigation items | dozens/session | No hover lift, no transition — used every session. |
| Filter / sort changes on lists | dozens/session | Data updates should feel instant. |
| Document viewer content (legal text) | reading mode | Motion competes with reading focus. |
| Error states and validation messages | urgency | Urgency signals should appear instantly. |
| Price and billing numbers (confirmed state) | trust context | Confirmed numbers must feel stable and certain. |
| Element | Allowed motion | Why it earns animation |
|---|---|---|
| Modal open | scale(0.97)→1, opacity 0→1, 220ms | First-time action per user intent — earns arrival. |
| Drawer / bottom sheet | translateY(100%)→0, 350ms spring | Spatial orientation — user needs to register origin. |
| Toast / notification | translateY(-100%)→0, 400ms ease | Rare event, needs to announce itself. |
| Card hover lift | translateY(-4px), 160ms | Affordance at browse frequency. |
| Button press | scale(0.97) / translateY(1px), 80ms | Tactile confirmation of an intentional action. |
| Credit balance change | spring ticker, ~300ms | Makes the cost of AI use tangible. |
Reduced-motion discipline
prefers-reduced-motion: reduce does not mean zero animation — it means no spatial movement. tokens-v3-gravity.css already collapses every animation and transition duration to 0.01ms globally, sweeps included; the guidance below is for anything you build outside that blanket rule (JS tickers, canvas, custom keyframes).
- translateY, translateX, rotate, tilt
- scale changes > 5% (frame tilt, hover lift)
- sheen sweep (replace with a static opacity pulse)
- ambient orb/tint drift
- page transitions with displacement
- opacity fades (state changes)
- color transitions (hover feedback)
- teal pulse dot (opacity-only variant)
- focus ring appearances
- skeleton pulse (opacity only, no shimmer)
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
/* Global, in tokens-v3-gravity.css — no per-component opt-out needed. */