# Phase 13: Design System Foundation - Research

**Researched:** 2026-05-16
**Domain:** Go + templ CSS design system port (tokens, component CSS, variant enums, dev catalog)
**Confidence:** HIGH

## Summary

Phase 13 is a CSS and Go enum porting task. All eleven templ component files already exist in `backend/internal/web/ui/`. No new templ components are being authored from scratch; the work is:

1. Replace `backend/internal/web/ui/base.css` (28-line stub) with the 223-line token vocabulary from `go-backend/internal/web/ui/base.css` verbatim.
2. Add ten missing CSS files (`modal.css`, `input.css`, `textarea.css`, `select.css`, `table.css`, `empty-state.css`, `icon-button.css`, `form-field.css`, `spacing.css`) ported from go-backend with page-level selectors stripped.
3. Update `backend/tailwind.input.css` to import all thirteen CSS files.
4. **Migrate `ButtonClass()` from the compound-class pattern** (`ui-button-solid-default-md`) **to go-backend's multi-class pattern** (`ui-button ui-button-solid ui-button-default ui-button-md`) — and update all call sites in templates that use the old compound class directly as string literals.
5. Add `ButtonVariantGhost` and `BadgeVariantPrimary` to `variants.go` (with normalizer updates).
6. Align Props structs with go-backend (Button needs `Icon string`, Card needs `Header/Body/Footer templ.Component` instead of children, several components need `Disabled bool`).
7. Add helper functions for components that currently lack them (`InputClass`, `TextareaClass`, `ModalClass`, etc.) — go-backend keeps these unexported; backend uses exported names.
8. Port `select_helpers.go` from go-backend (select component has significant helper logic).
9. Add a `UIIcon` templ function and a `SpaceX`/`SpaceY` component with `SpacingStep` enum.
10. Create `backend/internal/web/ui/catalog/` with the single-page `catalog.templ` + handler registered under `//go:build catalog`.
11. Extend `ui_test.go` to cover all eleven component types.

The critical migration risk is the **button class pattern change**: existing templates (`planning.templ`, `tasks.templ`, `events.templ`, `etapes.templ`) use compound class strings like `ui-button-solid-default-md` as raw HTML attribute values — these are NOT routed through `ButtonClass()`, so replacing `button.css` will silently break those templates unless the old compound selectors are kept or the templates are migrated. The plan must make this explicit.

**Primary recommendation:** Adopt go-backend's multi-class pattern in Phase 13. Replace `ButtonClass()` output, replace `button.css` entirely with go-backend's multi-class selectors, AND migrate all hardcoded compound class strings in templates to the new multi-class format. Do this in one atomic wave.

---

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions

**Token Values**
- D-T01: Port `go-backend/internal/web/ui/base.css` token values verbatim — no adjustments to brand palette, typography, or spacing scale before Phase 13 lands
- D-T02: Port ALL tokens — full 223-line vocabulary — not just the subset referenced by current component CSS files
- D-T03: Replace `backend/internal/web/ui/base.css` entirely (do not merge); go-backend version already includes the same box-sizing/reset foundation
- D-T04: Phase 13 ships the token vocabulary as CSS custom properties; wiring existing component CSS to consume `var(--…)` tokens is Phase 14–17 work — component CSS may still use hardcoded values in Phase 13

**app.css Scope**
- D-A01: `app.css` (the 1896-line shell) is deferred to Phases 14–17; Phase 13 ships component CSS only
- D-A02: `backend/tailwind.input.css` imports `base.css` + all component CSS files; no app shell import in Phase 13
- D-A03: When porting component CSS, strip page-level selectors (`body`, `:root` overrides, `.app-layout`); component CSS stays scoped to `.ui-*` class selectors only
- D-A04: Catalog page uses a hand-built fake shell (sidebar column + content area) for visual context, even though the real `app.css` shell is not yet in backend

**Catalog Route**
- D-C01: Catalog route is guarded by a Go build tag (`//go:build catalog`); production binary never includes the route — zero attack surface
- D-C02: Catalog page layout: sidebar/anchor nav with section links (`#buttons`, `#badges`, `#cards`, etc.) matching the fake shell layout
- D-C03: Each component section shows rendered variants + the templ call used to produce each variant (rendered + source, not rendered-only)
- D-C04: Static HTML only — no JavaScript interaction; all variants pre-rendered at page load

**Component API**
- D-CA01: Add `ButtonVariantGhost` to the `ButtonVariant` enum
- D-CA02: Add `BadgeVariantPrimary` to the `BadgeVariant` enum
- D-CA03: Review and align each component's Go-side Props struct with the go-backend equivalent — fill in missing fields
- D-CA04: Extend `ui_test.go` to cover class-generation logic for all 9+ component types

### Claude's Discretion
- Exact migration of CSS variable names: if go-backend component CSS uses slightly different class names (`.ui-button` vs `.btn`), match go-backend exactly
- Exact fake-shell HTML in the catalog: should mimic the sidebar+main-content layout visible in go-backend's catalog but is not bound to pixel-perfect reproduction
- Order of anchor sections in the catalog sidebar: alphabetical by component name (per UI-SPEC)
- Exact test coverage depth: at minimum one test per component type for the happy path; error/edge cases at planner discretion

### Deferred Ideas (OUT OF SCOPE)
- `app.css` shell port (sidebar layout, topbar, nav shell, content area) — deferred to Phases 14–17
- Wiring component CSS to consume `var(--…)` tokens — deferred to Phases 14–17
- Dark mode tokens / `prefers-color-scheme` counterparts — Future requirement DARK-01
- Responsive sidebar and mobile layout — Future requirements RESP-01..03
</user_constraints>

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|------------------|
| DS-01 | CSS design tokens (colors, spacing, typography, shadows, gradients) defined in `backend/internal/web/ui/base.css` matching the go-backend token vocabulary | Replace 28-line stub with go-backend's 223-line `:root` block verbatim |
| DS-02 | Button component with primary, secondary, ghost, and danger variants | Add `ButtonVariantGhost` to variants.go; port go-backend button.css multi-class pattern; update existing templates |
| DS-03 | Input, Textarea, and Select form field components | Port input.css, textarea.css, select.css; add select_helpers.go; align Props structs with go-backend |
| DS-04 | Card component used across tablo list, detail, and content views | Align Card Props struct (Header/Body/Footer templ.Component); port go-backend card.css |
| DS-05 | Badge component supports semantic tones (primary, warning, success, danger) | Add `BadgeVariantPrimary` to variants.go; port go-backend badge.css (pill shape differs from current) |
| DS-06 | Modal component available for create/edit dialogs | Port go-backend modal.css (no modal CSS currently in backend); add ModalProps templ |
| DS-07 | Empty-state component available for zero-data views | Port go-backend empty-state.css; add EmptyStateProps templ |
| DS-08 | Table component available for list views | Port go-backend table.css; add TableProps templ |
| DS-09 | Icon-button component replaces inline icon-button patterns | Add IconButtonVariant/Tone enums; port icon-button.css; add UIIcon helper; add SpaceX/SpaceY |
</phase_requirements>

---

## Architectural Responsibility Map

| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| CSS token vocabulary | Static / Build | — | Compiled into `static/tailwind.css` by Tailwind CLI at `just generate` |
| Component CSS rules | Static / Build | — | Same pipeline as tokens |
| Go Props structs + variant enums | Backend Server | — | Compiled into the Go binary |
| templ components (HTML structure) | Backend Server | — | Rendered server-side, no client JS for structure |
| Select open/close toggle JS | Browser | — | Minimal inline script in select.templ manages `is-open` class; no framework |
| Catalog route handler | Backend Server | — | Build-tag gated; registered in router only under `-tags catalog` |
| Tailwind CSS compilation | Build toolchain | — | `./bin/tailwindcss -i tailwind.input.css -o static/tailwind.css` |

---

## Standard Stack

### Core (already in backend)

| Tool | Version | Purpose | Status |
|------|---------|---------|--------|
| Go templ | v0.3.1020 | Server-side HTML component authoring | Pinned in justfile |
| Tailwind CSS v4 standalone | v4.0.0 | CSS compilation from `.templ` + `.go` source scanning | Pinned in justfile |
| chi router | v5.2.5 | HTTP routing; catalog route registered with build tag | In go.mod |

### No New Dependencies

Phase 13 adds zero new Go modules or npm packages. All work is:
- CSS file creation/replacement
- Go source file edits (`variants.go`, Props structs)
- New Go files (`select_helpers.go`, `catalog/catalog.templ`, `catalog/catalog.go`, `catalog/examples.go`)

[VERIFIED: codebase inspection] — no missing toolchain dependencies

---

## Architecture Patterns

### System Architecture Diagram

```
backend/tailwind.input.css
  └── @import base.css             [223-line token vocabulary — DS-01]
  └── @import button.css           [multi-class pattern — DS-02]
  └── @import badge.css            [DS-05]
  └── @import card.css             [DS-04]
  └── @import input.css            [DS-03 — NEW]
  └── @import textarea.css         [DS-03 — NEW]
  └── @import select.css           [DS-03 — NEW]
  └── @import modal.css            [DS-06 — NEW]
  └── @import empty-state.css      [DS-07 — NEW]
  └── @import table.css            [DS-08 — NEW]
  └── @import icon-button.css      [DS-09 — NEW]
  └── @import form-field.css       [DS-03 wrapper — NEW]
  └── @import spacing.css          [DS-09 utility — NEW]
         |
         v
  ./bin/tailwindcss → static/tailwind.css
         |
         v
  Go template <link rel="stylesheet" href="/static/tailwind.css">

backend/internal/web/ui/
  variants.go          [ButtonVariant, BadgeVariant, IconButtonVariant, SpacingStep enums]
  helpers.go           [mergeAttrs — unchanged]
  tokens.go            [semantic token constants — minor additions]
  button.templ         [ButtonProps — add Icon field]
  badge.templ          [BadgeProps — unchanged API, no CSS class change needed]
  card.templ           [CardProps — BREAKING: switch from children to Header/Body/Footer fields]
  input.templ          [InputProps — add Disabled, Required fields]
  textarea.templ       [TextareaProps — add Disabled, Required fields — NEW FILE]
  select.templ         [SelectProps — complex: multi-value, placeholder — NEW FILE]
  select_helpers.go    [selectPlaceholder, selectOptionSelected, etc. — NEW FILE]
  modal.templ          [ModalProps — Title/Body/Actions — NEW FILE]
  empty_state.templ    [EmptyStateProps — NEW FILE]
  table.templ          [TableProps — Head/Body — NEW FILE]
  icon_button.templ    [IconButtonProps + UIIcon helper — NEW FILE]
  space.templ          [SpaceX/SpaceY — NEW FILE]
  form_field.templ     [FormFieldProps — NEW FILE]
  ui_test.go           [extended to cover all 11+ components]

  catalog/
    catalog.templ      [single-page layout with sidebar + anchor nav — NEW]
    catalog.go         [//go:build catalog handler + router registration — NEW]
    examples.go        [typed Example structs, one per component variant — NEW]
```

### Multi-Class Button Pattern (Critical Migration)

[VERIFIED: codebase inspection] The current backend generates compound classes:

```go
// Current backend/internal/web/ui/variants.go
func ButtonClass(variant ButtonVariant, tone ButtonTone, size Size) string {
    return "ui-button ui-button-" + string(t) + "-" + string(v) + "-" + string(s)
    // Produces: "ui-button ui-button-solid-default-md"
}
```

The go-backend generates separate classes:

```go
// go-backend/internal/web/ui/variants.go
func buttonClass(variant ButtonVariant, tone ButtonTone, size Size) string {
    return "ui-button ui-button-" + string(normalizedButtonTone(tone)) +
           " ui-button-" + string(normalizedButtonVariant(variant)) +
           " ui-button-" + string(normalizedSize(size))
    // Produces: "ui-button ui-button-solid ui-button-default ui-button-md"
}
```

Go-backend's `button.css` uses compound CSS selectors that match the space-separated output:

```css
/* go-backend pattern */
.ui-button-solid.ui-button-default {
    background: var(--color-brand-primary);
}
.ui-button-solid.ui-button-default:hover {
    background: var(--color-brand-primary-hover);
}
```

**Migration scope for template hardcodes:** These files use the OLD compound class pattern directly as HTML attribute strings (not through `ButtonClass()`):

- `backend/templates/planning.templ` — uses `ui-button-soft-neutral-md`, `ui-button-solid-default-md`
- `backend/templates/tasks.templ` — uses `ui-button-soft-neutral-md`, `ui-button-soft-danger-md`
- `backend/templates/events.templ` — uses `ui-button-soft-neutral-md`
- `backend/templates/etapes.templ` — uses `ui-button-soft-neutral-md`, `ui-button-soft-danger-md`

[VERIFIED: codebase grep] Each occurrence must be updated to the multi-class pattern:
- `ui-button-solid-default-md` → `ui-button ui-button-solid ui-button-default ui-button-md`
- `ui-button-soft-neutral-md` → `ui-button ui-button-soft ui-button-neutral ui-button-md`
- `ui-button-soft-danger-md` → `ui-button ui-button-soft ui-button-danger ui-button-md`

Also: existing tests in `ui_test.go` assert the compound string `"ui-button ui-button-solid-default-md"` — these must be updated to the multi-class assertion.

### Card Props API Change (Breaking)

[VERIFIED: codebase inspection] The current backend `Card` accepts children via `templ.WithChildren`:

```go
// Current backend — Card(attrs templ.Attributes) — children via context
templ Card(attrs templ.Attributes) {
    <section class="ui-card" { attrs... }>
        { children... }
    </section>
}
// Called as: @ui.Card(nil) { <p>content</p> }
```

The go-backend uses typed `Header/Body/Footer templ.Component` fields:

```go
// go-backend — Card(props CardProps)
type CardProps struct {
    Header templ.Component
    Body   templ.Component
    Footer templ.Component
}
// Called as: @ui.Card(ui.CardProps{Body: myBodyComponent})
```

**Call-site impact:** The existing test `TestCard_RendersChildren` uses `templ.WithChildren` and asserts `<section class="ui-card"`. Migrating to go-backend's API requires updating this test AND any templates calling `Card` with children. Currently no templates use `Card` with content (the component is stubbed in Phase 1 and not yet wired into pages). The test must be rewritten.

### Catalog Route — Build Tag Pattern

[VERIFIED: codebase inspection] The go-backend approach is a separate `cmd/designsystem/main.go` that generates static HTML files. The CONTEXT.md specifies a different approach for backend: a live HTTP handler gated by `//go:build catalog`.

The canonical Go build tag pattern for a catalog handler file:

```go
//go:build catalog

package web

import "net/http"

func RegisterCatalogRoute(mux chi.Router) {
    mux.Get("/ui-catalog", catalogHandler)
}
```

The router calls `RegisterCatalogRoute(r)` only in the catalog build. In the default build, the function is either absent (linker drops it) or provided by a stub file:

```go
//go:build !catalog

package web

func RegisterCatalogRoute(mux chi.Router) {} // no-op
```

This pattern requires TWO files — one for each build tag — so the function signature is always available to the compiler, but only the catalog build wires up the handler.

**Alternative (simpler):** Include the catalog handler in a file tagged `//go:build catalog` and call `RegisterCatalogRoute` unconditionally in `router.go` only if a second stub file satisfies the symbol in non-catalog builds. This is the standard Go build-tag gating pattern.

### Select Component — Inline JavaScript

[VERIFIED: codebase inspection] Go-backend's `select.templ` embeds a self-initializing `<script>` block directly in the templ component. This is the approved pattern for the select (per CONTEXT.md interaction contract: "JS adds `is-open` class"). The script uses `window.__uiSelectInitAll` to avoid double-registration across HTMX swaps and re-initializes on `htmx:afterSwap` events.

The backend `select.templ` must include this same inline script verbatim — it is part of the component contract, not an external JS file.

### Ghost Button Variant — New CSS Required

[VERIFIED: codebase inspection] Go-backend's `button.css` does NOT include a ghost variant. `ButtonVariantGhost` is being ADDED (D-CA01). The ghost CSS rules must be authored following the multi-class pattern:

```css
/* New ghost rules to add to button.css */
.ui-button-ghost {
    background: transparent;
    color: var(--color-brand-primary);
}
.ui-button-ghost:hover {
    background: var(--color-surface-brand-soft);
}
.ui-button-ghost:focus-visible {
    box-shadow: 0 0 0 3px var(--color-focus-ring);
    outline: none;
}
```

Ghost does not interact with tone (solid/soft) — it is its own variant class. UI-SPEC confirms: `ui-button-ghost` is a standalone class, not combined with a tone class.

### Badge Primary Variant — New CSS Required

[VERIFIED: codebase inspection] Go-backend's `badge.css` does not include `ui-badge-primary`. Per UI-SPEC:

```css
.ui-badge-primary {
    background: var(--color-surface-brand-soft);
    border-color: rgba(128, 78, 236, 0.3);  /* --color-brand-primary at 30% opacity */
    color: var(--color-text-brand);
}
```

### textarea.css — Missing from go-backend base.css

[VERIFIED: codebase inspection] Go-backend's `textarea.css` is a short file. Textarea does not have a separate CSS class helper — it uses a static `class="ui-textarea"` in the templ. The CSS mirrors input styling with `min-height: 7rem` and `resize: vertical`.

---

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Select dropdown open/close | Custom JS state machine | Inline script in select.templ (go-backend pattern) | Already solved with HTMX re-init support |
| Icon SVG delivery | Icon font, external CDN | Inline SVG in UIIcon templ switch | Zero-dependency, go-backend pattern |
| Component token wiring | Replace all hex values with var() | Leave hardcoded in Phase 13 (D-T04) | Token wiring is Phase 14–17 work |
| Catalog HTML generation | Static site generator | Live HTTP handler (-tags catalog) | Simpler, server-rendered |

---

## Common Pitfalls

### Pitfall 1: Button Class Pattern Mismatch — Silent Visual Break
**What goes wrong:** Port go-backend's `button.css` (which uses `.ui-button-solid.ui-button-default { }` selectors) without updating the old compound class strings in templates. The buttons render with no styling.
**Why it happens:** Templates hardcode `class="ui-button ui-button-soft-neutral-md"` directly as attribute strings — these bypass `ButtonClass()` and are invisible to the Go type system.
**How to avoid:** Grep ALL `.templ` files for `ui-button-` before replacing button.css. Update each hardcoded string to the multi-class equivalent.
**Warning signs:** `just generate` succeeds but buttons appear unstyled in the browser.

### Pitfall 2: Existing Tests Assert Old Compound Class
**What goes wrong:** `ui_test.go` has `wantClass := "ui-button ui-button-solid-default-md"` — this will fail after `ButtonClass()` is updated.
**Why it happens:** Tests were written against the old pattern.
**How to avoid:** Update test assertions alongside `ButtonClass()` in the same commit.

### Pitfall 3: Card API Change Breaks Test
**What goes wrong:** `TestCard_RendersChildren` uses `templ.WithChildren(context.Background(), child)` — this breaks when Card switches from `{ children... }` to typed `Header/Body/Footer` fields.
**Why it happens:** The go-backend Card API is structurally different.
**How to avoid:** Rewrite the card test to use `CardProps{Body: child}` when migrating the templ.

### Pitfall 4: Build Tag File Pair — Linker Error Without Stub
**What goes wrong:** `router.go` calls `RegisterCatalogRoute(r)` unconditionally. When built without `-tags catalog`, the function is undefined → linker error.
**Why it happens:** Go requires all referenced symbols to be defined in the build graph.
**How to avoid:** Provide a `catalog_stub.go` with `//go:build !catalog` that defines `RegisterCatalogRoute` as a no-op. Both files must be in the same package.

### Pitfall 5: templ Generate Overwrites Hand-Edited `*_templ.go`
**What goes wrong:** Editing a `*_templ.go` file directly — `just generate` (which runs `templ generate`) will overwrite it on the next run.
**Why it happens:** `*_templ.go` files are machine-generated from `.templ` source.
**How to avoid:** Only edit `.templ` files. Run `just generate` after every `.templ` change. Never touch `*_templ.go`.

### Pitfall 6: select.templ Inline Script and HTMX Re-Init
**What goes wrong:** Omitting the `htmx:afterSwap` listener from the select's inline script means selects inside HTMX-swapped fragments do not initialize.
**Why it happens:** Inline scripts run once on page load; HTMX replaces DOM without re-running scripts.
**How to avoid:** Port the full `__uiSelectInitAll` + `htmx:afterSwap` listener block verbatim from go-backend.

### Pitfall 7: Modal Rendered Without Backdrop in Catalog
**What goes wrong:** Catalog renders `ui-modal-backdrop` with `position: fixed; inset: 0` — this overlays the entire catalog page.
**Why it happens:** The backdrop class uses fixed positioning for actual use; in the catalog it obscures everything.
**How to avoid:** In the catalog modal section, render only `ui-modal-panel` directly (skip the backdrop wrapper), or render the modal in a relative-positioned container with an override. UI-SPEC says: "Modal section renders an open modal panel (without the backdrop toggle)".

### Pitfall 8: Missing `ui-button-ghost` CSS for NormalizedButtonVariant
**What goes wrong:** `NormalizedButtonVariant` defaults unknown variants to `ButtonVariantDefault`. If `ButtonVariantGhost` is not added to the switch, ghost falls back to default (silent).
**Why it happens:** The normalizer exhaustively lists known variants.
**How to avoid:** Add `case ButtonVariantGhost: return variant` to `NormalizedButtonVariant`.

---

## Code Examples

### ButtonClass Multi-Class Output (Updated)

```go
// Source: go-backend/internal/web/ui/variants.go (verified)
// Updated backend pattern to match:
func ButtonClass(variant ButtonVariant, tone ButtonTone, size Size) string {
    v := NormalizedButtonVariant(variant)
    t := NormalizedButtonTone(tone)
    s := NormalizedSize(size)
    return "ui-button ui-button-" + string(t) + " ui-button-" + string(v) + " ui-button-" + string(s)
}
// Output: "ui-button ui-button-solid ui-button-default ui-button-md"
```

### Ghost Variant in NormalizedButtonVariant

```go
// Add to variants.go normalizer:
func NormalizedButtonVariant(variant ButtonVariant) ButtonVariant {
    switch variant {
    case ButtonVariantNeutral, ButtonVariantWarning, ButtonVariantSuccess, ButtonVariantDanger, ButtonVariantGhost:
        return variant
    default:
        return ButtonVariantDefault
    }
}
```

### IconButtonClass (New — from go-backend)

```go
// Source: go-backend/internal/web/ui/variants.go (verified)
func IconButtonClass(variant IconButtonVariant, tone IconButtonTone) string {
    normalizedVariant := NormalizedIconButtonVariant(variant)
    switch NormalizedIconButtonTone(tone) {
    case IconButtonToneGhost:
        return "borderless-icon-button ui-icon-button-ghost ui-icon-button-" + string(normalizedVariant)
    default:
        return "ui-icon-button ui-icon-button-solid ui-icon-button-" + string(normalizedVariant)
    }
}
```

### SpaceX/SpaceY Class Functions (New)

```go
// Source: go-backend/internal/web/ui/variants.go (verified)
func SpaceXClass(step SpacingStep) string {
    return "ui-space-x ui-space-x-" + string(NormalizedSpacingStep(step))
}
func SpaceYClass(step SpacingStep) string {
    return "ui-space-y ui-space-y-" + string(NormalizedSpacingStep(step))
}
```

### Build Tag File Pair for Catalog Route

```go
// File: backend/internal/web/catalog_route_catalog.go
//go:build catalog
package web

import "github.com/go-chi/chi/v5"

func RegisterCatalogRoute(r chi.Router) {
    r.Get("/ui-catalog", catalogPageHandler())
}
```

```go
// File: backend/internal/web/catalog_route_stub.go
//go:build !catalog
package web

import "github.com/go-chi/chi/v5"

func RegisterCatalogRoute(r chi.Router) {} // no-op in production builds
```

### tailwind.input.css Final State

```css
/* Source: 13-UI-SPEC.md CSS File Manifest (verified) */
@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";
```

### Catalog Handler Pattern

```go
// Source: go-backend/cmd/designsystem/main.go (adapted for live HTTP)
// The catalog handler renders the single-page catalog via templ.
// Fake shell uses Tailwind utility classes — no import of real app.css.
```

---

## Component Delta Table

This table shows exactly what changes are needed per component — the planner can turn each row into one or more tasks.

| Component | CSS Status | Props Status | Variants | Helpers | Test Coverage |
|-----------|-----------|--------------|----------|---------|---------------|
| Button | REPLACE (multi-class migration) | Add `Icon string`, `Disabled bool` | Add `Ghost` | Update `ButtonClass()` normalizer | Update existing tests |
| Badge | REPLACE (pill shape, token-based colors) | No change needed | Add `Primary` | Update `BadgeClass()` normalizer | Extend existing tests |
| Card | REPLACE (token-based, add header/footer sections) | BREAKING: replace children with `Header/Body/Footer templ.Component` | — | Add `CardClass()` → returns `"ui-card"` | Rewrite existing test |
| Input | NEW FILE (port from go-backend) | Add `Disabled bool`, `Required bool` | — | Add `InputClass(disabled bool)` | New test |
| Textarea | NEW FILE (port from go-backend) | New file: add `Disabled bool`, `Required bool` | — | Add `TextareaClass()` | New test |
| Select | NEW FILE (port from go-backend) | New file: complex (Values []string, Multiple bool, Placeholder) | — | New `select_helpers.go` (port verbatim) | New test |
| Modal | NEW FILE (port from go-backend) | New file: `Title/Body/Actions templ.Component` | — | Add `ModalClass()` | New test |
| Empty State | NEW FILE (port from go-backend) | New file: `Icon templ.Component` (not string) | — | Add `EmptyStateClass()` | New test |
| Table | NEW FILE (port from go-backend) | New file: `Head/Body templ.Component` | — | Add `TableClass()` | New test |
| Icon Button | NEW FILE (port from go-backend) | New file: `IconButtonProps` + `UIIcon(kind string)` | `IconButtonVariant`, `IconButtonTone` | Add `IconButtonClass()` | New test |
| Form Field | NEW FILE (port from go-backend) | New file: `Label/Hint/Error/ForID` | — | Add `FormFieldClass()` | New test |
| Space X/Y | NEW FILE (port from go-backend) | New file: `SpaceProps{Size SpacingStep}` | `SpacingStep` enum | Add `SpaceXClass()`, `SpaceYClass()` | New test |

---

## Props Struct Alignment Detail

### Button — add `Icon string` and `Disabled bool`

Current `backend` ButtonProps is missing `Icon string` (go-backend has it). `Disabled bool` is mentioned in UI-SPEC but go-backend does NOT have it — go-backend relies on `Attrs` pass-through for `disabled`. **Do not add `Disabled bool` to Button** — use `Attrs: templ.Attributes{"disabled": true}` for now. Only add `Icon string`.

[VERIFIED: go-backend/internal/web/ui/button.templ — no Disabled field in go-backend]

### Input — `Disabled bool` and `Required bool`

Go-backend `InputProps` does NOT have `Disabled bool` or `Required bool` — these are passed via `Attrs`. UI-SPEC defines them as explicit fields. Since this is Claude's discretion for Props alignment (D-CA03), and the UI-SPEC is authoritative for the backend design contract, add them as explicit fields in the backend Props with corresponding `if props.Disabled { disabled }` conditionals in the templ.

### EmptyState — `Icon templ.Component` not `Icon string`

Go-backend's `EmptyStateProps.Icon` is `templ.Component` (a rendered component, not an icon name string). This means callers pass `ui.UIIcon("grid3x3")` directly. The backend port must match: `Icon templ.Component`.

### Card — Children to Typed Fields (Breaking)

Current backend uses `{ children... }` with `templ.WithChildren`. Go-backend uses `Header/Body/Footer templ.Component` struct fields. The switch is breaking at the call site. Since no production template currently calls `Card` with content (it was introduced in Phase 1 but not yet used in pages), the only impact is the test `TestCard_RendersChildren` which must be rewritten.

---

## File Creation Order (Recommended Wave Structure)

The planner should structure waves to ensure CSS is available before catalog tests run:

**Wave 1 — Token and enum foundation (no visual output yet)**
- Replace `base.css` with go-backend version (D-T01/T02/T03)
- Update `variants.go`: add Ghost/Primary variants, add IconButtonVariant/Tone, SpacingStep enums
- Update `tokens.go`: no substantive changes needed (existing constants are compatible)

**Wave 2 — Migrate existing components to go-backend APIs**
- Update `button.css` (multi-class selectors) + `ButtonClass()` (multi-class output)
- Update compound class strings in templates (planning.templ, tasks.templ, events.templ, etapes.templ)
- Update `badge.css` (pill shape, add primary variant)
- Update `card.templ` (Props API migration) + `card.css` (token-based)
- Update `ui_test.go` for Button (multi-class assertions) and Card (typed Props)

**Wave 3 — Port new CSS + templ files**
- Add `input.css`, `input.templ` (update existing stub)
- Add `textarea.css`, `textarea.templ` (new file)
- Add `modal.css`, `modal.templ` (new file)
- Add `select.css`, `select.templ`, `select_helpers.go` (new files — complex)
- Add `empty-state.css`, `empty_state.templ` (new file)
- Add `table.css`, `table.templ` (new file)
- Add `icon-button.css`, `icon_button.templ` (new file — includes UIIcon)
- Add `form-field.css`, `form_field.templ` (new file)
- Add `spacing.css`, `space.templ` (new file)
- Update `tailwind.input.css` with all new @import entries
- Extend `ui_test.go` for all new components

**Wave 4 — Catalog**
- Create `backend/internal/web/ui/catalog/catalog.templ`
- Create `backend/internal/web/ui/catalog/examples.go`
- Create `backend/internal/web/catalog_route_catalog.go` (build tag: catalog)
- Create `backend/internal/web/catalog_route_stub.go` (build tag: !catalog)
- Wire `RegisterCatalogRoute(r)` into `NewRouter` in `router.go`
- Add justfile target: `catalog: just generate && go run -tags catalog ./cmd/web`
- Manual visual sign-off checkpoint before Phase 14

---

## Validation Architecture

### Test Framework

| Property | Value |
|----------|-------|
| Framework | Go's built-in testing + testify (none — stdlib only) |
| Config file | none (go test ./...) |
| Quick run command | `go test ./internal/web/ui/...` |
| Full suite command | `just test` (runs `just generate && go test ./...`) |

### Phase Requirements → Test Map

| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| DS-01 | Token custom properties present in base.css | Manual (visual) + file content check | `grep -c 'color-brand-primary' backend/internal/web/ui/base.css` | ✅ (replace) |
| DS-02 | ButtonClass ghost variant emits `ui-button-ghost` | Unit | `go test ./internal/web/ui/... -run TestButton` | ✅ (extend) |
| DS-03 | Input/Textarea/Select render correct class attributes | Unit | `go test ./internal/web/ui/... -run "TestInput\|TestTextarea\|TestSelect"` | ❌ Wave 3 |
| DS-04 | Card renders ui-card-header/body/footer sections | Unit | `go test ./internal/web/ui/... -run TestCard` | ✅ (rewrite) |
| DS-05 | BadgeClass primary variant emits `ui-badge-primary` | Unit | `go test ./internal/web/ui/... -run TestBadge` | ✅ (extend) |
| DS-06 | Modal renders ui-modal-backdrop and ui-modal-panel | Unit | `go test ./internal/web/ui/... -run TestModal` | ❌ Wave 3 |
| DS-07 | EmptyState renders ui-empty-state with title | Unit | `go test ./internal/web/ui/... -run TestEmptyState` | ❌ Wave 3 |
| DS-08 | Table renders ui-table-shell wrapping ui-table | Unit | `go test ./internal/web/ui/... -run TestTable` | ❌ Wave 3 |
| DS-09 | IconButton ghost neutral emits borderless-icon-button | Unit | `go test ./internal/web/ui/... -run TestIconButton` | ❌ Wave 3 |

### Wave 0 Gaps (tests needed before implementation)

- [ ] `backend/internal/web/ui/ui_test.go` — rewrite `TestCard_RendersChildren` → `TestCard_RendersTypedRegions`
- [ ] New test functions for all 8 components added in Wave 3 (see UI-SPEC test coverage table)
- Framework already installed — no setup needed

### Sampling Rate
- **Per commit:** `go test ./internal/web/ui/...`
- **Per wave merge:** `just test` (includes templ generate)
- **Phase gate:** `just test` green + manual catalog visual sign-off at `/ui-catalog`

---

## Open Questions (RESOLVED)

1. **Ghost button — which size rules apply?**
   - What we know: `ButtonVariantGhost` is a new variant not in go-backend's button.css. The size rules (`ui-button-sm`, `ui-button-md`, `ui-button-lg`) are defined as standalone classes in go-backend and apply to ALL variants.
   - What's unclear: Should ghost buttons have padding/sizing from the size classes, or use a custom minimal padding?
   - Recommendation: Inherit the standard size classes (`.ui-button-md { padding: 0.7rem 1rem; }`). Ghost variant only overrides background/color, not sizing.

2. **Catalog route vs. router.go signature**
   - What we know: `NewRouter(...)` in `router.go` has a fixed signature with many named deps. Adding catalog registration changes `router.go`.
   - What's unclear: The build-tag stub approach keeps the function call in `router.go` unconditional — this is safe because the stub satisfies the symbol.
   - Recommendation: Use the two-file build-tag approach (catalog + !catalog stub) with an unconditional call in `router.go`. Zero diff to router.go signature.

3. **`auth-provider-*` CSS in button.css — keep or remove?**
   - What we know: Current `button.css` has `auth-provider-button`, `auth-provider-separator`, etc. appended from Phase 8. Go-backend does not have these.
   - What's unclear: Phase 13 replaces button.css entirely — these auth selectors would be lost.
   - Recommendation: Move auth-provider CSS to a separate `auth.css` file BEFORE replacing button.css. Import it in `tailwind.input.css`. This is a prerequisite sub-task in Wave 2.

---

## Assumptions Log

| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | Ghost button inherits size class padding from `.ui-button-sm/md/lg` | Component Delta Table | Ghost renders with wrong sizing if size classes don't apply |
| A2 | No templates currently call `Card` with children in production pages | Card Props section | Card change breaks rendered pages (not just tests) |
| A3 | `auth-provider-*` CSS from Phase 8 must be preserved | Open Question 3 | Login page loses styling if auth CSS is dropped |

---

## Environment Availability

| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
| Go | Build | ✓ | 1.26.0 (go.mod) | — |
| templ CLI | `just generate` | ✓ | v0.3.1020 (justfile) | `go install github.com/a-h/templ/cmd/templ@v0.3.1020` |
| tailwindcss standalone | `just generate` | ✓ | v4.0.0 (justfile) | `just bootstrap` |
| just | Task runner | ✓ | system | Run commands manually |

[VERIFIED: justfile inspection] — toolchain versions pinned.

---

## Security Domain

> `security_enforcement` not set to false — section required.

### Applicable ASVS Categories

| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | no | Phase 13 is CSS only — no auth changes |
| V3 Session Management | no | No session changes |
| V4 Access Control | yes — catalog route | Build tag `//go:build catalog` ensures catalog is never in production binary |
| V5 Input Validation | no | Component CSS and static Props structs, no user input validated |
| V6 Cryptography | no | No crypto operations |

### Known Threat Patterns

| Pattern | STRIDE | Standard Mitigation |
|---------|--------|---------------------|
| Catalog route exposed in production | Information Disclosure | `//go:build catalog` + `//go:build !catalog` stub; production binary built without `-tags catalog` |
| XSS via templ text content | Tampering | templ auto-escapes `{ variable }` interpolations — no raw HTML injection via Props |

---

## Sources

### Primary (HIGH confidence — verified by codebase inspection)

- `backend/internal/web/ui/` — all existing files read directly
- `go-backend/internal/web/ui/` — all source CSS and templ files read directly
- `go-backend/internal/web/ui/catalog/` — catalog.templ, examples.go, catalog_test.go, pages.go read directly
- `backend/tailwind.input.css` — current import manifest verified
- `backend/internal/web/router.go` — router structure verified
- `backend/justfile` — build targets verified (tailwind_version = v4.0.0)
- `backend/templates/*.templ` — compound class hardcodes verified by grep

### Secondary (HIGH confidence — from project planning artifacts)

- `.planning/phases/13-design-system-foundation/13-CONTEXT.md` — locked decisions
- `.planning/phases/13-design-system-foundation/13-UI-SPEC.md` — visual contract
- `.planning/REQUIREMENTS.md` — DS-01 through DS-09

---

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH — verified by direct codebase inspection
- Architecture: HIGH — both source and target codebases fully read
- Pitfalls: HIGH — compound class migration risk verified by grep, card API break verified by test inspection
- Props struct alignment: HIGH — all templ files in both repos read

**Research date:** 2026-05-16
**Valid until:** 2026-06-16 (stable — no external dependencies, pure codebase-internal work)
