Motion.
Quieter than it looks: one entrance (fade-rise), a couple of ambient loops, smooth in-page anchors. Everything honours prefers-reduced-motion. The product motion tokens live at /foundations/motion.
The standard section entrance. An IntersectionObserver toggles `data-reveal` → `data-revealed`; an optional `delay` staggers siblings.
[data-reveal] { opacity: 0; transform: translateY(16px); transition: opacity/transform 900ms var(--ease-out); } [data-reveal][data-revealed='true'] { opacity: 1; transform: none; } JS adds [data-revealed='true'] on intersection; stagger via per-item delay. Reduced motion Render in the final state (opacity 1, no transform); skip the transition.
Subtle continuous motion — orbit/float for decorative elements, pulse/spin for live dots. Desync per item so they never march in lockstep.
CSS keyframes with a long duration and per-item `animation-delay`; keep amplitude small (a few px / a few degrees). Reduced motion animation: none — hold the static frame.
Smooth scroll to a target, offset for the sticky header.
el.scrollIntoView({ behavior: 'smooth' }); target gets `scroll-margin-top: <sticky-header-height>`. Wire in a small island, guard against double-bind, re-run on `astro:page-load`. Reduced motion Use behavior: 'auto' (instant jump) — still offset by scroll-margin-top.
- Global `@media (prefers-reduced-motion: reduce)` plus a per-animation `animation: none` / final-state fallback.
- Honour it for loops, count-ups, feature-tab rotation, and smooth scroll.
- Reveal is the default entrance — do not invent per-section bespoke entrances.
- Feature-tab auto-rotation: the CSS keyframe duration MUST equal the JS advance timeout, or the progress fill desyncs from the tab switch.
- Re-bind scroll/animation islands on `astro:page-load` (Astro view transitions) and guard against double-binding.
- Drive any displayed figure (bars, counters) from state — never hardcode values that imply they update.