# Storefront Motion & Micro-interactions

## Scope

`05-motion-and-microinteractions.md` set the motion philosophy for the *admin* dashboard: instant navigation, static skeletons, ease-out only, nothing slower than 240ms. That philosophy is right for an operator tool where speed of repeated action matters most. The storefront is a shopping experience, not an operator tool — the brief that opened this phase of work asked for it to feel "calm like Apple, fast like Nike," and the one gap left after the purchasing-journey work was that resolved content *popped in* abruptly the instant a loading skeleton finished, and a button click gave no tactile feedback beyond an opacity dim. This phase closes that gap.

**What doesn't change:** page-to-page navigation is still instant — clicking a link never gets a transition animation, for the same reason `05` gives (animating the single most frequent interaction would slow down exactly the thing that most needs to feel fast). What's new here is entirely about the moment *within* a page where a loading skeleton resolves into real content, or a popover/dropdown mounts — motion that communicates "this settled into place," not motion that delays getting there.

## New primitives (`index.css`)

- `@keyframes fade-in-up` + a bare `.enter` utility class (same idiom as the existing bare `container`/`grid12` utility classes) — opacity 0→1 with an 8px upward settle, `--motion-slow` (240ms), `--motion-easing` (the existing ease-out token). Both declared once in the global `index.css`, applied by adding the plain `enter` class name alongside a component's own CSS-module class in its JSX — not by writing `animation: fade-in-up` inside a `.module.css` file. That distinction matters: CSS Modules auto-scopes any bare identifier used in an `animation`/`animation-name` value, so a reference to a keyframes name declared in a *different* file (like the global `fade-in-up`) silently resolves to nothing. Keeping the class and the keyframes in the same unscoped global stylesheet sidesteps that entirely.
- A global `prefers-reduced-motion: reduce` fallback collapses every animation/transition in the app to a single ~0.01ms frame. This isn't a per-component add-on — it's one rule covering everything, present and future. The skeleton shimmer (`Skeleton.module.css`, pre-existing) lands on its resting frame under this rule rather than flashing, which happens to reproduce `05`'s "static skeleton" rule exactly, for users who need it.

## Where it's applied

**Resolved-content entrance** — the global `enter` class added alongside the root element's existing class in:
- `ProductCard`, `CollectionCard`, `CategoryRow`, `PromoModule` — the four building blocks behind nearly every skeleton→content swap in the app (Homepage modules, category/collection/search results, Cart's cross-sell rail, PDP's related-products/recently-viewed rails, Order Confirmation's cross-sell rail all compose from these, so one edit each covers all of them).
- `ProductDetail`'s two-column `.layout` — previously this page's loading state was a bare "Loading…" string; it now gets a real skeleton (gallery-shaped rect + title/description/price/button-shaped lines matching the eventual layout, same convention `ProductCardSkeleton` already established) that resolves into the real layout with the same fade-up.
- `TrackOrder`'s result summary panel and `OrderConfirmation`'s content wrapper.

**Popovers and overlays** — `Modal` (backdrop fade + dialog fade-scale 98%→100%, matching `05`'s documented modal pattern exactly), `SearchOverlay` (same backdrop/panel treatment), `MiniCart` (a shorter dropdown-style fade + 4px settle, anchored under the trigger rather than centered), and `CompareBar` (slides up from off-screen the moment a second product is selected, rather than appearing instantly at the bottom edge).

**Button press feedback** — every `Button` (all variants, since this lives in the one shared component) now scales to 97% on `:active`, alongside the existing hover-opacity dim. This is the "the click registered" feedback `05` already prescribes for admin, ported here since the storefront's buttons had the hover state but no press state at all.

## Deliberately unchanged

- `EmptyState` stays static — animating a screen a shopper may sit on while deciding what to do next fails the same "purposeful, not decorative" test `05` applies to admin's empty states.
- Table/list row hovers (there are none of consequence on the storefront the way there are in admin) aren't a concern here.
- No spring/bounce easing anywhere — same `--motion-easing` ease-out token throughout, no exceptions.
