---
phase: 13
slug: design-system-foundation
status: draft
shadcn_initialized: false
preset: none
created: 2026-05-16
---

# Phase 13 — UI Design Contract

> Visual and interaction contract for the Design System Foundation phase.
> Generated by gsd-ui-researcher, verified by gsd-ui-checker.
>
> **Stack note:** This is a Go + HTMX project. There is no React, no shadcn, no
> node-based component library. The design system is implemented as:
> - CSS custom properties in `backend/internal/web/ui/base.css`
> - Component CSS files under `backend/internal/web/ui/`
> - Go templ components consumed by HTMX-driven server-rendered pages
> - Tailwind v4 utility classes alongside component CSS (no @apply)

---

## Design System

| Property | Value | Source |
|----------|-------|--------|
| Tool | none (Go + templ, not shadcn) | Project constraint |
| Preset | not applicable | n/a |
| Component library | Go templ components in `backend/internal/web/ui/` | Existing codebase |
| Icon library | Inline SVG via `UIIcon(name string)` templ helper | go-backend pattern |
| Font | `ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif` | go-backend `base.css` |

**Registry safety gate:** not applicable (no npm registry consumption).

---

## Token Vocabulary

All tokens are CSS custom properties declared in `backend/internal/web/ui/base.css`.
Phase 13 ports the full 223-line vocabulary from `go-backend/internal/web/ui/base.css` verbatim.
Decision D-T01 and D-T02 lock this: no adjustments before Phase 13 lands.

### Color Tokens (key values — from go-backend base.css)

| Group | Token | Value |
|-------|-------|-------|
| Brand | `--color-brand-primary` | `#804eec` |
| Brand | `--color-brand-primary-hover` | `#6d28d9` |
| Brand | `--color-brand-primary-active` | `#5b21b6` |
| Brand | `--color-brand-secondary` | `#a855f7` |
| Brand | `--color-brand-accent` | `#3b82f6` |
| Brand | `--color-brand-ink` | `#1e1b2e` |
| Text | `--color-text-primary` | `hsl(0 0% 9%)` |
| Text | `--color-text-secondary` | `#475467` |
| Text | `--color-text-muted` | `hsl(0 0% 43.5%)` |
| Text | `--color-text-faint` | `#9ca3af` |
| Text | `--color-text-inverse` | `#ffffff` |
| Text | `--color-text-brand` | `#804eec` |
| Text | `--color-text-disabled` | `#667085` |
| Surface | `--color-surface-page` | `hsl(0 0% 100%)` |
| Surface | `--color-surface-default` | `#ffffff` |
| Surface | `--color-surface-card` | `rgba(255,255,255,0.8)` |
| Surface | `--color-surface-subtle` | `hsl(0 0% 96.1%)` |
| Surface | `--color-surface-muted` | `#f3f4f6` |
| Surface | `--color-surface-brand-soft` | `#ede9fe` |
| Border | `--color-border-default` | `hsl(0 0% 90.9%)` |
| Border | `--color-border-strong` | `#d1d5db` |
| Border | `--color-border-subtle` | `#d0d5dd` |
| Focus | `--color-focus-ring` | `rgba(124,58,237,0.2)` |
| Focus | `--color-focus-ring-strong` | `rgba(139,92,246,0.16)` |
| Danger | `--color-status-danger-strong` | `#dc2626` |
| Danger | `--color-status-danger-foreground` | `#dc2626` |
| Success | `--color-status-success-strong` | `#16a34a` |
| Warning | `--color-status-warning-strong` | `#db9729` |

Full vocabulary: 223 custom properties — see `go-backend/internal/web/ui/base.css` for the complete list. Port verbatim; no value changes in Phase 13.

---

## Spacing Scale

Phase 13 defines spacing both as CSS custom-property-backed SpacingStep utility components and
as raw CSS values used inside component rules. All values are as declared in `go-backend/internal/web/ui/spacing.css`.

| Token | CSS Value | rem | SpacingStep const |
|-------|-----------|-----|-------------------|
| xs | 4px | 0.25rem | `SpacingStepXS` |
| sm | 8px | 0.5rem | `SpacingStepSM` |
| md | 12px | 0.75rem | `SpacingStepMD` |
| lg | 16px | 1rem | `SpacingStepLG` |
| xl | 24px | 1.5rem | `SpacingStepXL` |

**Exception:** The spacing component scale (xs=4, sm=8, md=12, lg=16, xl=24) is a verbatim port of go-backend's spacing utility components (locked by decision D-T01). The non-standard 12px (`md`) step is intentional and scoped exclusively to the SpaceX/SpaceY utility templ components — it is NOT used for page-level layout spacing. Layout spacing in all other components and the catalog shell uses standard Tailwind utilities (4, 8, 16, 24) directly.

Touch target exceptions: buttons and icon-buttons enforce `min-height: 44px` and icon-buttons enforce
`min-width: 44px` for WCAG 2.5.5 compliance. These override the spacing scale where needed.

---

## Typography

Sourced from `go-backend/internal/web/ui/base.css` body declaration and component CSS measurements.

**Declared font weights: 400 (regular) and 600 (semibold) only.** Headings rely on size contrast
(18px vs 16px body) rather than a third weight to establish hierarchy. Labels and form elements
share the 16px body size and are distinguished by weight (600 semibold) only — no separate size
step is introduced.

| Role | Size | Weight | Line Height | Token / Usage |
|------|------|--------|-------------|---------------|
| Body / label / form | 16px (1rem) | 400 regular / 600 semibold | 1.5 / 1.4 | Body prose at 400; form labels and button labels at 600 |
| Small / hint | 14px (0.875rem) | 400 regular | 1.4 | Form hints, error messages, badge text, table headers |
| Heading | 18px (1.125rem) | 600 semibold | 1.2 | Modal titles, empty-state titles, section headings |

Three declared sizes: 14px, 16px, 18px. Two declared weights: 400 and 600.

**Font stack (declared in base.css body rule):**
```
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif
```

No custom web font is introduced in Phase 13. The system stack is the final choice.

---

## Color Contract

60/30/10 split expressed in terms of the token vocabulary:

| Role | Token / Value | Usage |
|------|---------------|-------|
| Dominant (60%) | `--color-surface-page` (#ffffff) | Page background, form field backgrounds |
| Secondary (30%) | `--color-surface-muted` (#f3f4f6) | Card backgrounds, icon-button hover states, empty-state icon container, select chips |
| Accent (10%) | `--color-brand-primary` (#804eec) | Primary button background, focus rings, brand text, selected option highlight |
| Destructive | `--color-status-danger-strong` (#dc2626) | Danger button variant only |

**Accent reserved for:**
- Solid default button (`ui-button-solid-default-*`)
- Focus ring on all interactive elements (`--color-focus-ring`)
- Brand text (`--color-text-brand`)
- Soft default button background (`--color-surface-brand-soft`)

**Accent is NOT used for:** neutral buttons, card borders, table rows, form field borders at rest, badge backgrounds (badges use status-color tokens).

---

## Component Inventory

All 11 component types must be present as both a templ component and a CSS file.
The catalog page must render every variant listed here.

### Button (DS-02)

Class pattern: `ui-button ui-button-{tone}-{variant}-{size}`

| Axis | Values | CSS classes |
|------|--------|-------------|
| Variant | default, neutral, warning, success, danger, **ghost** (add) | `ui-button-default`, `ui-button-neutral`, `ui-button-warning`, `ui-button-success`, `ui-button-danger`, `ui-button-ghost` |
| Tone | solid, soft | `ui-button-solid`, `ui-button-soft` |
| Size | sm, md, lg | `ui-button-sm`, `ui-button-md`, `ui-button-lg` |

**Ghost variant:** `ButtonVariantGhost` added to `variants.go`. Class combo `ui-button-ghost` renders with transparent background + brand-colored text, hover shows `--color-surface-brand-soft`. CSS mirrors go-backend ghost rules.

Props struct (go-backend parity):
```go
type ButtonProps struct {
    Label    string
    Variant  ButtonVariant
    Tone     ButtonTone
    Size     Size
    Type     string
    Icon     string          // icon name for UIIcon helper
    Disabled bool            // add if missing in backend
    Attrs    templ.Attributes
}
```

Interaction states: default → hover (background shift) → active (darker) → focus-visible (3px focus ring `--color-focus-ring`) → disabled (opacity 0.5, cursor not-allowed).

### Badge (DS-05)

Class pattern: `ui-badge ui-badge-{variant}`

| Variant | Background token | Border token | Text token |
|---------|-----------------|--------------|------------|
| info | `--color-status-info-soft-bg` | `--color-status-info-soft-border` | `--color-status-info-foreground` |
| warning | `--color-status-warning-soft-bg` | `--color-status-warning-soft-border` | `--color-status-warning-foreground` |
| success | `--color-status-success-soft-bg` | `--color-status-success-soft-border` | `--color-status-success-foreground` |
| danger | `--color-status-danger-soft-bg` | `--color-status-danger-soft-border` | `--color-status-danger-foreground` |
| **primary** (add) | `--color-surface-brand-soft` | `--color-brand-primary` at 30% opacity | `--color-text-brand` |

**Primary variant:** `BadgeVariantPrimary` added to `variants.go`. Uses brand-purple tones to signal "active" or "primary" status (distinct from info blue).

Props struct (go-backend parity):
```go
type BadgeProps struct {
    Label   string
    Variant BadgeVariant
}
```

### Card (DS-04)

Class: `ui-card` wrapping `ui-card-header`, `ui-card-body`, `ui-card-footer`

- Border radius: 1rem
- Border: 1px solid `--color-border-default`
- Shadow: `--shadow-surface-md`
- Section padding: `1.25rem 1.5rem`
- Header/footer have a 1px divider at `--color-border-default`

Props struct (go-backend parity):
```go
type CardProps struct {
    Attrs templ.Attributes
}
```
Header, Body, Footer are separate templ components taking `templ.Component` children.

### Input (DS-03)

Class: `ui-input`

- Border radius: 0.75rem
- Min-height: 44px (touch target)
- Width: 100%
- Placeholder color: `--color-text-faint`
- Focus: border-color `--color-brand-focus`, box-shadow `0 0 0 3px --color-focus-ring-strong`, outline none

Props struct:
```go
type InputProps struct {
    Name        string
    ID          string
    Type        string         // defaults to "text"
    Placeholder string
    Value       string
    Disabled    bool
    Required    bool
    Attrs       templ.Attributes
}
```

### Textarea (DS-03)

Class: `ui-textarea`

- Border radius: 0.75rem
- Min-height: 7rem
- Width: 100%, resize: vertical
- Same placeholder and focus behavior as input

Props struct:
```go
type TextareaProps struct {
    Name        string
    ID          string
    Placeholder string
    Value       string
    Rows        int
    Disabled    bool
    Required    bool
    Attrs       templ.Attributes
}
```

### Select (DS-03)

Class: `ui-select` wrapping `ui-select-control`, `ui-select-menu`, `ui-select-option`

- Control: min-height 44px, border-radius 0.75rem, chevron arrow that rotates 180deg when `is-open`
- Menu: absolute positioned, max-height 16rem, overflow-y auto, border-radius 0.9rem
- Option selected state: `is-selected` class → `--color-status-info-soft-bg` background + `--color-text-brand` text

Props struct:
```go
type SelectOption struct {
    Value    string
    Label    string
    Selected bool
    Disabled bool
}

type SelectProps struct {
    Name     string
    ID       string
    Options  []SelectOption
    Disabled bool
    Attrs    templ.Attributes
}
```

### Modal (DS-06)

Class: `ui-modal-backdrop` → `ui-modal-panel` → `ui-modal-header` / `ui-modal-body` / `ui-modal-actions`

- Backdrop: `position: fixed`, `inset: 0`, `background: --overlay-backdrop-default`, flex center
- Panel: max-width 32rem, border-radius 1rem, shadow `--shadow-surface-lg`
- Header: `font-size: 1.125rem`, `font-weight: 600`, bottom border divider
- Actions: flex row, justify end, gap 0.75rem, top border divider
- HTMX pattern: backdrop rendered server-side into `#modal-slot`; close button sets `hx-delete` or swaps empty fragment

Props struct:
```go
type ModalProps struct {
    Title string
    Attrs templ.Attributes
}
```
Body and Actions are passed as `templ.Component` children.

### Empty State (DS-07)

Class: `ui-empty-state` wrapping `ui-empty-state-icon`, `ui-empty-state-title`, `ui-empty-state-description`

- Border: 1px dashed `--color-border-subtle`
- Border radius: 1rem
- Icon container: 4rem × 4rem circle, `--color-surface-muted` background, `--color-text-faint` icon
- Icon SVG: 2rem × 2rem
- Title: `font-size: 1.125rem`, `font-weight: 600`
- Description: max-width 32rem, `--color-text-muted`

Props struct:
```go
type EmptyStateProps struct {
    Icon        string
    Title       string
    Description string
    Attrs       templ.Attributes
}
```
CTA button (optional) passed as `templ.Component` child.

### Table (DS-08)

Class: `ui-table-shell` → `ui-table`

- Shell: `overflow-x: auto`, `width: 100%`
- Table: `border-collapse: collapse`, `min-width: 100%`
- Header cells: 14px, weight 600, `--color-text-secondary`, uppercase tracking (Tailwind utilities)
- Body rows: 16px, `--color-text-primary`, hover `--color-surface-subtle`
- Borders: 1px `--color-border-default` on row bottoms

Props struct:
```go
type TableProps struct {
    Attrs templ.Attributes
}
```
Head, Body, Row, Cell, HeaderCell are separate templ sub-components.

### Icon Button (DS-09)

Class patterns:
- Solid: `ui-icon-button ui-icon-button-solid ui-icon-button-{variant}`
- Ghost: `borderless-icon-button ui-icon-button-ghost ui-icon-button-{variant}`

- Min-height: 44px, min-width: 44px (touch targets)
- Variants: neutral, warning, success, danger
- Tones: solid, ghost
- Ghost: transparent background, no border, SVG 1rem × 1rem

Props struct:
```go
type IconButtonProps struct {
    Icon    string
    Variant IconButtonVariant
    Tone    IconButtonTone
    Type    string
    Label   string  // aria-label
    Attrs   templ.Attributes
}
```

### Form Field (DS-03 wrapper)

Class: `ui-form-field` wrapping `ui-form-label`, input/textarea/select, `ui-form-hint`, `ui-form-error`

- Label: `font-size: 1rem`, `font-weight: 600`, `--color-text-primary`
- Hint: `font-size: 0.875rem`, `--color-text-muted`
- Error: `font-size: 0.875rem`, `--color-status-danger-foreground`
- Grid layout with `gap: 0.5rem`

Props struct:
```go
type FormFieldProps struct {
    Label    string
    Hint     string
    Error    string
    Required bool
    ForID    string
}
```
Input/textarea/select passed as `templ.Component` child.

---

## Spacing Component (utility — not layout)

The `SpaceX` / `SpaceY` templ components render sized whitespace blocks for vertical/horizontal rhythm.
They are distinct from the CSS spacing scale used in layout.

| Step | Width/Height |
|------|-------------|
| xs | 0.25rem (4px) |
| sm | 0.5rem (8px) |
| md | 0.75rem (12px) |
| lg | 1rem (16px) |
| xl | 1.5rem (24px) |

---

## Catalog Page Contract

**Route:** `GET /ui-catalog`
**Build gate:** `//go:build catalog` — compiled only with `-tags catalog`
**Template location:** `backend/internal/web/ui/catalog/catalog.templ`

**Focal point:** The component grid in the main content area is the primary focal point. The sidebar
provides navigation only — it carries no visual weight beyond a subtle border-right at
`--color-border-default`. Each section opens with a bold section heading (`{ComponentName} — {DS-req}`)
that anchors the viewer's eye before the variant grid. Within each section, the default/md size
variant is rendered first (top-left) as the canonical reference.

### Layout

```
+--sidebar (240px, fixed)--+--main content (fluid)--+
| [Brand logo]             | [Section heading]       |
| #buttons                 | [Component grid]        |
| #badges                  | [templ call annotation] |
| #cards                   | ...repeated per section |
| #inputs                  |                         |
| #textarea                |                         |
| #select                  |                         |
| #modal                   |                         |
| #empty-state             |                         |
| #table                   |                         |
| #icon-button             |                         |
| #form-field              |                         |
+--------------------------+-------------------------+
```

The fake shell uses hand-written Tailwind utility classes (flex, min-h-screen, w-60, etc.) — no import of the real `app.css` which is deferred to Phases 14–17.

### Section Structure

Each section follows this pattern:

```
## Button — DS-02

[Rendered variant grid]

Each cell:
  [Rendered component]
  ─────────────────────
  ui.Button(ui.ButtonProps{
    Label:   "Save",
    Variant: ui.ButtonVariantDefault,
    Tone:    ui.ButtonToneSolid,
    Size:    ui.SizeMD,
  })
```

Section order in sidebar: alphabetical by component name (badge, button, card, empty-state, form-field, icon-button, input, modal, select, table, textarea).

Each section header includes the DS requirement number: `Button — DS-02`.

### Static only

No JavaScript on the catalog page. All variants pre-rendered at page load. Modal section renders an open modal panel (without the backdrop toggle) so all states are visible without interaction.

---

## Copywriting Contract

This phase is infrastructure (CSS tokens + component library), not a user-facing feature.
The only user-visible surface is the dev-only catalog page.

| Element | Copy |
|---------|------|
| Catalog page title | `Component Catalog` |
| Catalog nav heading | `Components` |
| Each section subheading | `{ComponentName} — {DS-req}` (e.g. `Button — DS-02`) |
| Empty state demo title | `Nothing here yet` |
| Empty state demo body | `Add your first item to get started.` |
| Empty state demo CTA | `Create item` |
| Table demo empty row | `No data to display` |
| Form error demo | `This field is required.` |
| Form hint demo | `Enter a value between 1 and 100.` |
| Modal demo title | `Confirm action` |

---

## Go-Side API Contract

### Variants to add in `backend/internal/web/ui/variants.go`

```go
// Add to ButtonVariant enum:
ButtonVariantGhost ButtonVariant = "ghost"

// Add new BadgeVariantPrimary:
BadgeVariantPrimary BadgeVariant = "primary"
```

`NormalizedButtonVariant` and `NormalizedBadgeVariant` must include the new cases.

### Class generation functions to add/update

```go
// ButtonClass already exists — update normalizedButtonVariant to include Ghost
// BadgeClass already exists — update normalizedBadgeVariant to include Primary

// Add for new component types:
func CardClass() string
func InputClass(disabled bool) string
func TextareaClass(disabled bool) string
func ModalClass() string
func EmptyStateClass() string
func TableClass() string
func FormFieldClass() string
func IconButtonClass(variant IconButtonVariant, tone IconButtonTone) string  // already in go-backend
func SpaceXClass(step SpacingStep) string  // already in go-backend
func SpaceYClass(step SpacingStep) string  // already in go-backend
```

### Test coverage (`backend/internal/web/ui/ui_test.go`)

At minimum one smoke test per component type verifying class string output:

| Component | Test function | Assertion |
|-----------|--------------|-----------|
| Button | `TestButtonClass` (extend) | Ghost variant → contains `ui-button-ghost` |
| Badge | `TestBadgeClass` (extend) | Primary variant → contains `ui-badge-primary` |
| Card | `TestCardClass` | Returns `ui-card` |
| Input | `TestInputClass` | Returns `ui-input` |
| Textarea | `TestTextareaClass` | Returns `ui-textarea` |
| Select | `TestSelectClass` | Returns `ui-select-control` |
| Modal | `TestModalClass` | Returns `ui-modal-panel` |
| EmptyState | `TestEmptyStateClass` | Returns `ui-empty-state` |
| Table | `TestTableClass` | Returns `ui-table` |
| IconButton | `TestIconButtonClass` | Ghost neutral → contains `borderless-icon-button` |
| FormField | `TestFormFieldClass` | Returns `ui-form-field` |

---

## CSS File Manifest

After Phase 13, `backend/tailwind.input.css` imports:

```css
@import "tailwindcss";

@source "./templates/**/*.templ";
@source "./internal/web/**/*.templ";
@source "./internal/web/**/*.go";

@import "./internal/web/ui/base.css";
@import "./internal/web/ui/button.css";
@import "./internal/web/ui/badge.css";
@import "./internal/web/ui/card.css";
@import "./internal/web/ui/input.css";
@import "./internal/web/ui/textarea.css";
@import "./internal/web/ui/select.css";
@import "./internal/web/ui/modal.css";
@import "./internal/web/ui/empty-state.css";
@import "./internal/web/ui/table.css";
@import "./internal/web/ui/icon-button.css";
@import "./internal/web/ui/form-field.css";
@import "./internal/web/ui/spacing.css";
```

Each CSS file is ported from `go-backend/internal/web/ui/{name}.css` with page-level selectors stripped (D-A03). Component CSS stays scoped to `.ui-*` class selectors only.

---

## Class Naming Convention

Go-backend uses two different class-assembly patterns. The backend must match go-backend exactly:

| Component | Pattern | Example |
|-----------|---------|---------|
| Button | `ui-button ui-button-{tone} ui-button-{variant} ui-button-{size}` | `ui-button ui-button-solid ui-button-default ui-button-md` |
| Badge | `ui-badge ui-badge-{variant}` | `ui-badge ui-badge-info` |
| Icon button (solid) | `ui-icon-button ui-icon-button-solid ui-icon-button-{variant}` | `ui-icon-button ui-icon-button-solid ui-icon-button-neutral` |
| Icon button (ghost) | `borderless-icon-button ui-icon-button-ghost ui-icon-button-{variant}` | `borderless-icon-button ui-icon-button-ghost ui-icon-button-danger` |
| Space X | `ui-space-x ui-space-x-{step}` | `ui-space-x ui-space-x-md` |
| Space Y | `ui-space-y ui-space-y-{step}` | `ui-space-y ui-space-y-md` |

**Note:** The existing `backend/internal/web/ui/variants.go` uses a DIFFERENT pattern:
`ui-button ui-button-{tone}-{variant}-{size}` (hyphen-joined). This diverges from go-backend.
The planner must decide whether to align backend to go-backend's space-separated pattern or keep the existing joined pattern and update the CSS to match. This is a **pre-planning decision required before Plan 01.**

---

## Interaction Contract

All interactive states are implemented in CSS only (no JavaScript for state). JavaScript is permitted only for:
- The select component dropdown open/close toggle (adding/removing `is-open` class and `hidden` attribute on the menu)
- Modal show/hide via HTMX swap (no JS state machines)

| State | Mechanism |
|-------|-----------|
| Button hover | CSS `:hover` pseudo-class |
| Button active | CSS `:active` pseudo-class |
| Button focus | CSS `:focus-visible`, 3px ring `--color-focus-ring` |
| Button disabled | HTML `disabled` attribute + CSS `[disabled]` or `.is-disabled` |
| Input focus | CSS `:focus`, border-color change + 3px ring |
| Select open | JS adds `is-open` class; CSS rotates chevron, shows menu |
| Modal open | HTMX swaps backdrop+panel into `#modal-slot` |
| Modal close | HTMX swaps empty fragment into `#modal-slot` |
| Icon button focus | CSS `:focus-visible`, 3px ring `--color-focus-ring` |

---

## Accessibility Contract

- All interactive elements must have `min-height: 44px` (touch target — WCAG 2.5.5)
- Icon buttons must have an `aria-label` attribute (no visible text)
- Form fields must use `<label for="...">` wired to input `id`
- Modal must trap focus when open (HTMX pattern: autofocus first interactive element in swapped fragment)
- Focus rings must use `--color-focus-ring` (purple 20% opacity) — never remove focus visible styles
- Color is never the sole differentiator for status (badge labels carry semantic meaning)
- `.visually-hidden` utility class is ported from go-backend base.css for screen-reader-only text

---

## Registry Safety

| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| npm / shadcn | none | not applicable — Go project |
| third-party CSS | none | not applicable — port from own go-backend |

No external CSS frameworks or component registries are introduced in Phase 13.

---

## Pre-Planning Decision Required

Before Plan 01 can be written, one ambiguity must be resolved:

**Class assembly pattern mismatch:**
- `go-backend/variants.go` generates: `ui-button ui-button-solid ui-button-default ui-button-md` (four separate classes)
- `backend/variants.go` (current) generates: `ui-button ui-button-solid-default-md` (one compound class)

The CSS selectors in go-backend's `button.css` use the multi-class pattern:
```css
.ui-button-solid.ui-button-default { ... }
```

The planner must align the backend `ButtonClass()` function and `button.css` selectors to use go-backend's multi-class pattern. This is a breaking change to `button.css` but button.css is already being replaced in Phase 13, so the cost is zero.

**Recommended resolution (Claude's discretion per D-CA03):** Adopt go-backend's multi-class pattern in Phase 13. Update `ButtonClass()` in `variants.go` to emit four separate classes, and port `button.css` with go-backend's multi-class selectors.

---

## Checker Sign-Off

- [ ] Dimension 1 Copywriting: PASS
- [ ] Dimension 2 Visuals: PASS
- [ ] Dimension 3 Color: PASS
- [ ] Dimension 4 Typography: PASS
- [ ] Dimension 5 Spacing: PASS
- [ ] Dimension 6 Registry Safety: PASS

**Approval:** pending
