# Storefront Design System

## Scope

`../ux/05-storefront-flows.md` deliberately deferred "visual design of every page," design tokens for a public sales surface, and storefront componentry to "a dedicated storefront design phase." This document is that phase. It covers the **reusable design language** — tokens and components every storefront page will be built from. It does not lay out Home, PDP, Cart, or Checkout page-by-page; that's the next phase, built against what's specified here (the same relationship `00-overview.md` describes between the admin design docs and Phase 5+).

The admin design docs (`01`–`06`) remain the source of truth for the **operator tool**. This document does not replace them — it defines a sibling system for the **public sales surface**, which has a different job: an operator tool optimizes for density and speed of repeated tasks; a storefront optimizes for browsing, trust, and a frictionless path from discovery to checkout. Where a rule from `01-design-principles.md` still applies unchanged (rule 5, motion; rule 8, no artificial friction) it's inherited, not restated. Where the storefront needs to diverge, the divergence is explicit below.

## Reference input

Two sets of screenshots were reviewed as pattern references — Apple's homepage/Store/Shop Mac pages, and Nike's homepage. **Neither is cloned.** No layout, copy, imagery, or exact visual treatment from either is reused. What's extracted is the underlying UX pattern, then re-expressed in CommerceDex's own visual language (brand purple accent, Inter type, the token set below).

### Patterns taken from the Apple reference

- **Editorial rhythm**: every promotional section follows the same beat — headline, one-line subhead, a small CTA row (1–2 buttons), then the product visual. Predictable rhythm across sections is what makes a long scrolling page feel calm rather than noisy.
- **Restraint**: overwhelmingly grayscale/neutral surfaces with a single accent color reserved for links and primary buttons. Color is spent on product imagery, not chrome.
- **Asymmetric bento grids**: 2-up and 2x2 tile grids of unequal visual weight (one wide hero tile, two half-width tiles) instead of a rigid uniform grid — used for "shop by category" style modules.
- **Two-tier CTAs**: a filled pill button for the primary action ("Buy") paired with a quieter pill-outline or plain-text link for the secondary one ("Learn more"), never two equally loud buttons.
- **Dense, trust-heavy footer**: many short columns of links plus a legal/fine-print block — signals a mature, established commerce operation.

### Patterns taken from the Nike reference

- **Expressive hero type**: large, confident, condensed display type for hero/promo moments — a deliberate contrast to body copy, used sparingly (hero + section headers only).
- **Immersive dark modules**: full-bleed photographic promo tiles with a dark scrim and white overlay text — used to break up an otherwise light page and give key moments more visual weight.
- **Circular badge navigation grid**: a horizontal row of small circular icons/logos ("choose your team") as a browsing entry point distinct from the main nav — a pattern worth generalizing into "shop by category" for CommerceDex.
- **Minimal, image-forward product card**: the product photo does almost all the work; card chrome is reduced to a name, one short meta line, and a price/tag — no borders, no heavy shadow.
- **Utility bar + primary nav split**: a slim top-of-page utility strip (store/help/account links) sits above the main nav bar, keeping wayfinding and browsing visually separate.

## Storefront principles

Checkable rules, in the spirit of `01-design-principles.md`, tuned for a public sales surface:

1. **One accent, spent on intent — not chrome.** Brand purple (`--color-accent-500`) marks the one primary action per screen and links. It never fills a card background or a large surface area; product imagery carries color, not UI chrome. Inherited from admin rule 1, unchanged.
2. **Two typographic registers.** A body register (reused from the admin scale, tuned larger — already in `index.css`) for anything read at length, and an expressive display register (`--type-hero-*`, `--type-display-*`, new below) for hero/section moments only. A page should never have more than one hero-register block in the same viewport.
3. **Pill-shaped actions.** Primary and secondary buttons are fully rounded (`--radius-pill`), not boxy — this is the single clearest visual signal that separates the storefront from the admin tool's rectangular, task-dense controls. Reserve sharp/rectangular controls for dense in-page utilities (quantity steppers, filter chips) where a pill would look ornamental.
4. **Light is the default surface; dark is a deliberate accent.** Most of the storefront is white/neutral-50, matching the Apple reference's restraint. A promo module may opt into the dark surface (`--color-ink`) to create weight for a specific moment (a flagship launch, a brand story), the way Nike's reference uses it — never as the default background.
5. **Every card variant shares one anatomy.** `ProductCard`, `CollectionCard`, and promo tiles all follow *image → short text block → one action* — so a page mixing them (a home feed of products and collections) reads as one coherent system, not several unrelated widgets.
6. **Borders over shadows, still.** Static surfaces (cards in a grid) use no shadow or a hairline border; `--shadow-md`/`--shadow-lg` are reserved for things genuinely floating above the page (the search overlay, a modal, a toast) — inherited from admin rule 2, unchanged.
7. **Every list has a real empty state and a real loading state.** No bare "Loading…" text, no blank space when a collection/search/cart/wishlist is empty — `EmptyState` and `Skeleton` (below) are the only sanctioned ways to represent "nothing here yet" and "still fetching."
8. **Timeless over trendy, still.** No glassmorphism, no gradient meshes, no oversized bubble UI — inherited from admin rule 7, unchanged. Expressive hero type and the dark promo surface are the storefront's two intentional departures from admin's austerity; nothing else is added on top of them.

## Tokens

`storefront/src/index.css` is the single source of truth for every literal value — this section is a map of what's there and why, not a duplicate list to keep in sync by hand.

### Grid & container

- `--container-max: 1280px` — the max width of any page's content column, centered with auto margins. Wide promo modules may bleed full-width behind the container (background image/color spans the viewport) while their text/CTA content still aligns to the container edge, matching both references' "full-bleed background, aligned content" pattern.
- `--container-pad`: responsive inline padding (24px mobile → 64px desktop, see Responsive rules) so content never touches the viewport edge.
- `.container` (global utility class in `index.css`): `max-width: var(--container-max); margin-inline: auto; padding-inline: var(--container-pad)`. Every header, footer, and page section uses this instead of ad hoc max-widths.
- A 12-column CSS grid (`--grid-columns: 12`, `gap: var(--space-6)` desktop / `var(--space-4)` mobile) backs collection/category modules; product grids instead use `repeat(auto-fill, minmax(...))` so card count per row adapts to viewport without breakpoint-specific column counts.

### Typography

Body-register sizes (`--type-body-*` through `--type-caption-*`) are unchanged from the existing storefront tokens. New expressive/display register, added for hero and section-header moments:

- `--type-hero-size` / `--type-hero-line` — largest size, one per page maximum (a true hero banner headline).
- `--type-display-size` / `--type-display-line` — existing token, now explicitly scoped to section headings ("You might also like", collection titles) rather than page heroes.
- Both registers share `--font-family` (Inter) — no second typeface. Expressiveness comes from size/weight/line-height, not a display font, per storefront principle 8.

### Color

Existing neutral/accent/semantic scale is unchanged (see `index.css` header comment for the AA-contrast rationale on `#6d28d9`). Added for promo modules and elevated surfaces:

- `--color-ink`: the dark promo-surface background (aliases `--color-neutral-900`, named separately so component CSS reads as an intentional surface choice, not "the darkest gray").
- `--color-ink-contrast`: text/icon color on `--color-ink` (aliases `--color-neutral-0`).
- `--color-overlay-scrim`: a semi-transparent black gradient token for text-over-image legibility on promo tiles, so every module using an image+text overlay computes contrast the same way instead of each component inventing its own rgba value.

### Shape, elevation, motion

- `--radius-pill: 999px` — new, for buttons and pill-shaped tags/badges.
- `--radius-sm/md/lg` — unchanged, used for cards and inputs.
- `--shadow-sm/md/lg` — `lg` is new, reserved for the search overlay and any modal per principle 6.
- `--z-sticky-header`, `--z-overlay`, `--z-modal`, `--z-toast` — new z-index scale so the sticky header, search overlay, and cart drawer never fight over stacking order.
- Motion tokens (`--motion-fast/default/slow`, easing) are unchanged and reused as-is.

## Components

Each is a real component under `storefront/src/components/`, not a spec-only description — implementation is the deliverable, per this phase's brief.

### Buttons (`Button.tsx`)

Variants: `primary` (filled, accent), `secondary` (outline), `dark` (filled ink — for use on light surfaces needing more visual weight than `secondary`, e.g. "Buy" on a white hero), `ghost` (no border/fill, for a tertiary action beside a primary), `link` (text-only, underline on hover — the "Learn more" role). Sizes: `sm`/`md`/`lg`. All pill-shaped (`--radius-pill`) per principle 3. A screen follows the two-tier CTA pattern: one `primary` (or `dark`) plus at most one quieter variant beside it — never two `primary`s in the same row.

### Forms (`Input.tsx`)

`TextField`, `SelectField`, `Checkbox` — labeled, larger touch targets than the admin equivalents (`--space-3` + 4px vertical padding vs admin's tighter fields), rectangular (not pill — principle 3's exception for dense utilities extends to form fields, since a pill-shaped multi-line-capable input reads oddly). Shares the admin convention of label-above-input, inline helper/error text.

### Header & navigation (`Header.tsx`)

Two-tier structure per the Nike reference: a slim utility row (store name/logo, currency selector, help/account links) and a primary bar (category nav + search trigger + wishlist + cart, all icon-labeled via `lucide-react`). Sticky (`position: sticky`, `--z-sticky-header`) so search and cart stay reachable on a long scrolling page. Category links come from `GET /storefront/categories` (`useSiteCategories.ts`), admin-controlled per category via a "show in nav" toggle, nested up to 3 levels (top nav item → mega-menu column → column's item list).

**Mega-menu spacing**: deliberately compact, not editorial-spacious — small font, tight vertical rhythm, per direct merchant feedback after an initial pass over-spaced it (eyeballing pixel positions in a reference screenshot turned out to be an unreliable way to derive these numbers; don't repeat that — treat a screenshot as a qualitative direction, not a source of exact pixel values, unless it can actually be measured). Each column is `min-width: 160px`; columns sit in a row with `gap: var(--space-6)` (24px) between them and `padding-block: var(--space-6)` (24px) on the panel. Within a column, the heading and its `<ul>` are the only two flex children, so `.megaMenuColumn`'s `gap: var(--space-3)` (12px) is the heading-to-list gap; the `<ul>` itself carries its own `gap: var(--space-2)` (8px) for item-to-item spacing — these are two distinct rules and must stay distinct, since collapsing them into one uniform column-level gap (an earlier bug here) leaves the item list's own gap at whatever `.megaMenuList` was last set to, invisibly wrong until a column has 2+ items. Heading: `--type-small-size` (14px), `--weight-semibold`, `--color-neutral-900`, no underline except on hover. Item: `--type-caption-size` (13px), `--weight-regular`, `--color-neutral-700`, `--color-neutral-900` on hover.

Apple's own mega-menu (e.g. Watch) uses a different, incompatible structure — a bold "hero" link column at a larger size (~20px) plus a secondary muted-label link group beneath it, versus small uniform links in its other columns. That hierarchy depends on a "this link is featured" content distinction our category tree has no concept of (every category is the same kind of thing) — replicating it would mean fabricating that distinction, not matching a reference, so it's deliberately not applied here. If a merchant-curated "featured category" concept is wanted later, that's a data-model decision, not a CSS one.

Two easy-to-miss gotchas confirmed against real, multi-item category trees (a single test item per column hides both):
- **Reset `<ul>`'s default margin.** The browser UA stylesheet gives `<ul>` a ~16px top/bottom block margin; left in place, it stacks on top of `.megaMenuColumn`'s heading-to-list flex gap, silently inflating an intended 32px gap to 48px. `.megaMenuList` must carry `margin: 0` alongside its own `list-style: none`.
- **Backdrop behind the open panel.** The dropdown is `position: absolute`, so it never pushes page content down — on a short page (e.g. a category listing whose sidebar sits right under the header), the dropdown's own height can reach down into where that content starts, and without anything acknowledging "the rest of the page is behind an overlay right now," it reads as a rendering bug rather than a floating panel. `.megaMenuBackdrop` is a sibling of `.header` (not a child — this lets it compare z-index directly against `.header`, which always wins and stays undimmed, instead of getting tangled in `.header`'s own child stacking order), `position: fixed; inset: 0`, a flat `rgba(10, 8, 14, 0.4)` dim (not `--color-overlay-scrim` — that token is a top-to-bottom gradient for image-legibility, wrong shape for a uniform page dim), shown via `.header:has(.navItemWithMenu:hover) ~ .megaMenuBackdrop` (and the `:focus-within` equivalent).

### Search (`SearchOverlay.tsx`)

A full-width overlay (not a dedicated page) triggered from the header's search icon, `--z-overlay`, `--shadow-lg` per principle 6. Contains the search input plus a results/empty-state slot; wraps the storefront's existing `listProducts({ q })` capability rather than introducing new search infrastructure. Escape-to-close and click-outside-to-close, matching the admin `Modal`'s interaction contract for consistency of feel across the two apps.

### Product cards (`ProductCard.tsx`)

image → name → price, unchanged anatomy, extended with: an optional `Badge` (New/Sale/Low stock/Sold out) pinned to the image's top-left corner, an optional compare-at price (strikethrough beside the current price, for discounted items), and a wishlist-heart affordance on hover/focus (top-right of the image). No border; the image's own edge is the card boundary, matching the Nike reference's "the photo is the card" restraint.

### Collection cards (`CollectionCard.tsx`)

New. image background → overlaid title (bottom-aligned, on a `--color-overlay-scrim` gradient for legibility) → optional one-line description. Used for "shop by collection/category" grids. Sized to sit inside the 12-column grid at 4, 6, 8, or 12 columns so a merchant's category tiles can express the Apple reference's asymmetric bento layout (one 8-col hero tile + one 4-col tile, or three 4-col tiles) without a bespoke layout per page.

### Category modules (`CategoryRow.tsx`)

New. A horizontal, scrollable row of circular icon tiles (image + short label), generalizing the Nike "choose your team" badge grid into a brand-neutral "shop by category" pattern. Distinct from `CollectionCard`'s bento grid — this is for a larger number of smaller, equally-weighted entry points (a full category list) where the bento grid is for a curated handful of featured collections.

### Promotional modules (`PromoModule.tsx`)

New. The hero/banner component. Props: `variant` (`light` | `dark`), `layout` (`center` | `split`), headline, subhead, up to two CTAs (rendered via `Button`, following the two-tier pattern above), and an image. `light` variant matches the Apple reference's restrained editorial hero; `dark` variant (using `--color-ink` + `--color-overlay-scrim`) matches the Nike reference's immersive moment — a merchant/page chooses per-section, per principle 4 ("dark is a deliberate accent, not the default").

### Recommendation modules (`ProductRail.tsx`, with `RelatedProducts.tsx` now a thin wrapper)

New generalized component: heading + optional "See all" link + a horizontally scrollable rail of `ProductCard`s. The existing `RelatedProducts` ("You might also like") is refactored to call it, so every future recommendation surface (trending, recently viewed, cross-sell) shares one implementation instead of each being a bespoke grid.

### Footer (`Footer.tsx`)

New, replacing the four-link bar currently in `Layout.tsx`. Multi-column link groups (props-driven — `groups: { heading: string; links: {label, href}[] }[]`) plus the store's address/legal line, matching the density of both references' footers, which is a legitimate trust signal for a public storefront (an admin tool has no equivalent need). Column count and content are still supplied by the page/store, not hardcoded brand copy.

### Empty states (`EmptyState.tsx`)

New. Icon slot (via `lucide-react`) + heading + one-line description + optional CTA `Button`. Covers empty cart, empty wishlist, and zero search/collection results — the concrete implementation of principle 7.

### Loading states (`Skeleton.tsx`)

New. Shimmering placeholder rectangles, plus a `ProductCardSkeleton` composition matching `ProductCard`'s exact proportions so a loading grid doesn't visually jump when real cards resolve in. Reused wherever a list currently renders bare "Loading…" text.

## Responsive rules

Breakpoints (referenced by component media queries, not exposed as CSS custom properties since media query values can't be tokens): `480px` (mobile/tablet split), `768px` (tablet/small-desktop), `1024px` (primary desktop breakpoint — nav switches from hamburger to full horizontal), `1280px` (container max engages, extra edge padding stops growing).

- **Header**: full category nav + search-as-input below 1024px collapses to a hamburger + icon-only search trigger; utility row hides secondary links (keeps store name + cart) below 768px.
- **Product/collection grids**: `auto-fill, minmax(220px, 1fr)` for product grids so column count is a function of viewport width, not a hand-maintained breakpoint table; collection bento grids collapse their column-span props to full-width (12/12) below 768px, stacking tiles rather than shrinking them illegibly.
- **`ProductRail`/`CategoryRow`**: horizontal scroll with hidden scrollbar + edge fade is the mobile behavior at every width — this is a deliberate exception to "collapse to a grid," since a scrollable rail is the more natural touch interaction and avoids the component needing two different internal layouts.
- **Promo modules**: `split` layout (image beside text) stacks to image-above-text below 768px; `center` layout is unaffected (already single-column).
- **Touch targets**: every interactive element (nav links, cart/wishlist icons, form fields) maintains a minimum 44px hit area below 1024px, per standard mobile touch-target guidance — larger than the admin tool needs, since storefront traffic is majority mobile.
