# 04 DESIGN SYSTEM

Palette direction: warm sand neutrals with a deep indigo-teal primary and a muted brass accent. Premium and textile-warm, not clinical white, and deliberately not the red-sale-badge look of a marketplace.

The brand name is not set yet, so these are the working tokens. When the logo and brand colours arrive, only the `--c-primary` and `--c-accent` families change. Everything else holds.

---

## 1. Tokens

```css
:root{
  /* Neutrals: warm sand base */
  --c-bg:            #FBF8F4;   /* page background */
  --c-surface:       #FFFFFF;   /* cards, sheets */
  --c-surface-alt:   #F3EDE4;   /* alternating bands, input fill */
  --c-border:        #E4DACD;   /* hairlines */
  --c-border-strong: #CBBCA9;   /* focused inputs, dividers */
  --c-ink:           #241D18;   /* headings, body */
  --c-ink-2:         #5A4E45;   /* secondary text */
  --c-ink-3:         #7D7166;   /* meta, captions */

  /* Primary */
  --c-primary:       #17453F;
  --c-primary-600:   #0F332E;   /* hover, pressed */
  --c-primary-100:   #E3EDEA;   /* tint background */

  /* Accent */
  --c-accent:        #B4643A;
  --c-accent-600:    #904924;
  --c-accent-100:    #F7E9E0;

  /* Semantic */
  --c-success:#1F6B45;  --c-success-bg:#E6F1EA;
  --c-warning:#8A5A05;  --c-warning-bg:#FBF0DC;
  --c-error:  #A32219;  --c-error-bg:  #FBE8E6;
  --c-info:   #17453F;  --c-info-bg:   #E3EDEA;

  /* Type */
  --f-head: 'Fraunces', Georgia, 'Times New Roman', serif;
  --f-body: 'Inter', 'Segoe UI', system-ui, -apple-system, 'Noto Sans', Arial, sans-serif;
  --fs-xs:.75rem;  --fs-sm:.875rem; --fs-base:1rem;   --fs-md:1.0625rem;
  --fs-lg:1.25rem; --fs-xl:1.5rem;  --fs-2xl:1.875rem;
  --fs-3xl:2.25rem; --fs-4xl:3rem;
  --lh-tight:1.15; --lh-snug:1.3; --lh-base:1.6;
  --fw-reg:400; --fw-med:500; --fw-semi:600; --fw-bold:700;
  --ls-tight:-0.02em; --ls-wide:.06em;

  /* Spacing, 4px base */
  --s-1:.25rem;  --s-2:.5rem;  --s-3:.75rem; --s-4:1rem;   --s-5:1.25rem;
  --s-6:1.5rem;  --s-8:2rem;   --s-10:2.5rem; --s-12:3rem; --s-16:4rem; --s-20:5rem;

  /* Radius */
  --r-xs:2px; --r-sm:4px; --r-md:8px; --r-lg:14px; --r-xl:22px; --r-full:999px;

  /* Shadow, warm-tinted, restrained */
  --sh-1:0 1px 2px rgba(36,29,24,.06);
  --sh-2:0 2px 8px rgba(36,29,24,.08);
  --sh-3:0 8px 24px rgba(36,29,24,.10);
  --sh-sticky:0 -2px 12px rgba(36,29,24,.12);

  /* Layout */
  --w-content:72rem;    /* 1152px, main grid */
  --w-wide:82.5rem;     /* 1320px, hero and galleries */
  --w-prose:38rem;      /* 608px, text blocks */
  --gutter:1rem;        /* 1.5rem at >=768px, 2rem at >=1200px */
  --tap:44px;

  --ease:cubic-bezier(.2,.6,.3,1);
  --dur:180ms;
}
```

**Breakpoints**, min-width: 360 base, 480 sm, 768 md, 1024 lg, 1200 xl, 1440 xxl.

## 2. Contrast, verified

WCAG terms, so nobody misapplies them: **normal text** needs 4.5:1 and means anything under 24px regular or 18.66px bold. **Large text** needs 3:1 and means 24px regular or 18.66px bold and above. Icons and UI boundaries need 3:1.

Computed from the token values above:

| Pair | Ratio | Verdict |
|---|---|---|
| `--c-ink` on `--c-bg` | 15.69:1 | AAA, all sizes |
| `--c-ink-2` on `--c-bg` | 7.34:1 | AA, all sizes |
| `--c-ink-3` on `--c-bg` | 4.48:1 | **fails AA for normal text.** Large text and non-text UI only. Do not use it for captions or meta copy. |
| `--c-primary` on `--c-bg` | 10.12:1 | AA, all sizes |
| white on `--c-primary` | 10.72:1 | AA, all sizes |
| `--c-accent` on `--c-bg` | 4.11:1 | **fails AA for normal text at every size.** Large text, icons and borders only. |
| white on `--c-accent` | 4.35:1 | fails. Accent buttons use `--c-accent-600` as the background, which passes. |
| white on `--c-accent-600` | 7.03:1 | AA, all sizes |
| `--c-error` on `--c-error-bg` | 6.35:1 | AA |
| `--c-success` on `--c-success-bg` | 5.58:1 | AA |

Two consequences, and they are binding:

- `--c-ink-3` cannot carry body, caption or meta text. Use `--c-ink-2` for anything a customer has to read. `--c-ink-3` is for decorative separators and disabled states.
- `--c-accent` is a decorative and large-display colour. It never carries small text. Where accent-coloured text is needed at normal size, use `--c-accent-600`.

`--c-border` never carries text.

Re-run these checks with a contrast checker after the brand palette replaces the working primary and accent, and record the results in `docs/PROGRESS.md`.

## 3. Typography

Two families, five faces total.

- **Fraunces**, variable, headings only, weights 500 and 600, Latin subset
- **Inter**, weights 400, 500, 600, Latin subset

Both self-hosted as WOFF2 in the theme. Do not load from the Google Fonts CDN, the extra DNS and connection cost hurts on a Pakistani 4G connection.

`font-display: swap`. Preload only Inter 400 and Fraunces 600. Total font payload under 60 KB.

The hero headline must render in the fallback stack first and swap. No web font blocks the LCP element.

## 4. Component inventory

Thirty five components, all built from the tokens above. Target: CSS under 45 KB minified, JS under 15 KB.

**Layout and chrome**: header (sticky, condensed on scroll), mobile drawer nav, search overlay, cart bubble with count, breadcrumb, footer, section header (eyebrow, title, link).

**Actions**: button in four variants (primary solid, secondary outline, ghost, full-width sticky bar), quantity stepper, sticky mobile action bar.

**Product**: product card (one template reused in grid, carousel, related and cross-sell), category tile, price block (regular, sale with real saved amount, from-price for variable), rating stars as an SVG sprite (never an icon font), badge (New, Only a few left, Out of stock, Verified Purchase, COD), swatch group (colour circles and size pills), gallery with thumbnail rail and swipe track, accordion, spec table, review card, review summary bar.

**Commerce**: cart line item, order summary panel, filter drawer with active-filter chip row, sort select, pagination.

**Forms**: form field (label, input, hint, error slot), select, textarea, checkbox, radio card.

**Feedback**: notice and toast in one style with four semantic colours, empty state, skeleton block for AJAX inserts, trust strip.

Discipline rule: build these eight well and refuse one-off styles for everything else. Button, card, form field, table, badge, drawer, accordion, notice.

## 5. Product imagery

**Card aspect ratio 4:5 portrait.** Bedding photographs better vertically, and portrait cards fit more per mobile screen. Set `aspect-ratio: 4/5` on the wrapper so there is zero layout shift before the image lands.

**Gallery aspect ratio 4:5**, the same crop as the card, so one photograph serves both and the product shoot only has to produce one framing. This corrects an earlier note that said 1:1; the registered gallery sizes below are 4:5 and they are correct.

Registered WordPress image sizes, and no others:

| Name | Size | Use |
|---|---|---|
| `bc-card` | 400 x 500 | grid card |
| `bc-card-2x` | 800 x 1000 | grid card, high DPI |
| `bc-gallery` | 900 x 1125 | product gallery |
| `bc-zoom` | 1600 x 2000 | desktop zoom only |

Remove core's `medium_large`, `1536x1536` and `2048x2048`, plus WooCommerce's unused sizes, with `intermediate_image_sizes_advanced`. Each removed size saves disk, inodes, and the regeneration passes that time out on shared hosting.

`srcset` with `sizes="(min-width:1200px) 25vw, (min-width:1024px) 33vw, 50vw"`. Note the 1024 breakpoint, not 768: the grid is two columns until 1024, so a 768 breakpoint here would serve tablets an image a third too small. WebP at quality 78, served by LiteSpeed rewrite so no `<picture>` markup is needed. Cap uploads at 1600px via a `wp_handle_upload` filter so the client cannot upload 6 MB phone photos.

Alt text pattern: `{Product Name} {Colour} {Size} bedsheet` for image one, then `{Product Name} close up fabric detail`, then `{Product Name} on bed`. Never repeat the same alt across a gallery.

## 6. Variation swatch behaviour

Colour circles at 36px with a 44px tap target via padding, wrapped in a row. Selected state is a 2px `--c-primary` ring plus a checkmark. **Never colour alone**, that fails for colour-blind users and is unreadable on a bright phone screen outdoors.

Size is pills, not a select.

Unavailable combinations: struck through, `aria-disabled`, still tappable, and tapping shows the out-of-stock message rather than doing nothing.

## 7. LCP protection

- Homepage hero and the first product gallery image: `fetchpriority="high"`, `loading="eager"`, `decoding="async"`, plus `<link rel="preload" as="image" imagesrcset=...>` in the head
- Everything below the fold: `loading="lazy"`
- Critical CSS, roughly 6 KB per template type, inlined in the head, with the main stylesheet loaded via `rel="preload" as="style" onload`
- Dequeue WooCommerce's `wc-blocks-style`, `woocommerce-layout.css`, `woocommerce-smallscreen.css`, `woocommerce-general.css`, plus the `zoom`, `flexslider` and `photoswipe` gallery scripts. That is roughly 100 KB saved before a single line of custom CSS is written.
- Also dequeue `wp-block-library`, `classic-theme-styles` and `global-styles` on the front end.

## 7b. Icon system

One set, one source, one treatment. Do not mix.

- **Lucide**, outline style, downloaded as individual SVGs and committed to `assets/img/icons/`. Do not load an icon font and do not load an icon library at runtime.
- 24px nominal, 1.5px stroke, `currentColor` fill so icons inherit text colour.
- Inlined into the markup by `bc_icon( $name, $size = 24 )`, which reads from a whitelist array. An unknown name returns nothing rather than a broken glyph.
- The full MVP set, and no more: menu, close, search, cart, user, chevron-down, chevron-right, chevron-left, star, star-filled, check, plus, minus, trash, truck, banknote, shield-check, refresh-ccw, message-circle, phone, map-pin, filter, arrow-up-down, alert-circle, info, external-link.
- Decorative icons get `aria-hidden="true"`. Icons that carry meaning on their own get an `aria-label`.

## 8. Motion

Restrained. One duration (`--dur`, 180ms) and one easing curve (`--ease`) for almost everything. Longer only for the drawer and the filter panel, at 240ms.

No scroll-triggered reveals, no parallax, no animated counters. They cost INP and the brief explicitly asks to avoid excessive animation.

Respect `prefers-reduced-motion: reduce` by dropping every transition to `0.01ms`.

## 9. Accessibility floor

- WCAG 2.1 AA contrast, verified in section 2
- 44px minimum tap target everywhere
- Visible focus ring, `2px solid var(--c-primary)` with a 2px offset, never `outline: none` without a replacement
- Every form input has a real `<label>`, not a placeholder standing in for one
- Errors linked with `aria-describedby` and marked with `aria-invalid`
- Gallery, accordion, drawer and filter panel are keyboard operable and trap focus where appropriate
- Skip-to-content link
- Semantic landmarks: `header`, `nav`, `main`, `footer`
