---
phase: 2.1
slug: app-shell-navigation-search-foundation
status: draft
shadcn_initialized: false
preset: not applicable
created: 2026-05-08
revised: 2026-05-08
---

# Phase 2.1 — UI Design Contract

> Visual and interaction contract for the App Shell, Navigation & Search Foundation. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
>
> **Stack note:** This is a Kotlin Multiplatform + Compose Multiplatform mobile project (iOS-primary, Android secondary). shadcn is not applicable — the design system is built on Composables / Compose Unstyled primitives + a local `RecipeTheme` token scaffold + a `GlassSurface` primitive backed by Liquid → Haze → flat fallback chain.

---

## Design System

| Property | Value |
|----------|-------|
| Tool | none (Compose Multiplatform; shadcn is web-only) |
| Preset | not applicable |
| Component library | Composables / Compose Unstyled (renderless primitives, locally restyled by Recipe components) |
| Icon library | Compose Material Icons Outlined (`androidx.compose.material:material-icons-extended`) — Material Icons stays even though the visual layer leaves Material 3; outlined variants align with the calm Liquid-Glass aesthetic |
| Font | System default (`FontFamily.Default`) for v1; SF Pro on iOS / Roboto on Android via platform default. No custom font shipped this phase. Phase 10 may revisit. |
| Glass primitive | `GlassSurface` composable in `ui/components/glass/`, layered over Liquid (`io.github.fletchmckee.liquid:liquid`) → Haze (`dev.chrisbanes.haze:haze`) → flat translucent fallback |
| Theme entry | `dev.ulfrx.recipe.ui.theme.RecipeTheme { content }` providing a `LocalRecipeColors`, `LocalRecipeTypography`, `LocalRecipeSpacing`, `LocalRecipeShapes`, `LocalRecipeGlass` `CompositionLocal` set |

**Material 3 boundary:** Material 3 stays only as legacy auth-screen scaffolding (`PostLoginPlaceholderScreen`, login). New code in `ui/screens/{planner,recipes,pantry,shopping}` and `ui/components/` MUST NOT introduce `androidx.compose.material3.*` imports. Use `RecipeTheme` tokens.

---

## Spacing Scale

Declared values (all multiples of 4, all within the standard set {4, 8, 16, 24, 32, 48, 64}):

| Token | Value | Usage |
|-------|-------|-------|
| `xs` | 4dp | Icon-to-label gap inside dock pill; chip internal padding |
| `sm` | 8dp | Compact inline spacing; gap between dock and floating search/action button; floating dock vertical offset above the bottom safe-area; dock vertical padding; inter-tab gap inside dock; empty-state icon-to-headline gap |
| `lg` | 16dp | Default screen content padding; empty-state headline-to-subline gap; search pill horizontal padding |
| `xl` | 24dp | Section padding; horizontal screen edge inset for empty-state body |
| `2xl` | 32dp | Layout-level gaps; vertical breathing room above empty-state block |
| `3xl` | 48dp | Large vertical separators (e.g. between top safe-area and an empty-state's icon when centered visually rather than mathematically) |

**Revision note (revision 1, 2026-05-08):** CONTEXT D-14 originally locked the scale as `4/8/12/16/24/32`. The 12dp step (`md`) was retired during UI-SPEC verification because no usage in this phase required 12dp specifically — every prior 12dp reference was remapped to 8dp (tighter chrome read more like a native iOS dock cluster). The scale extends upward with `2xl` (32dp) and `3xl` (48dp) so empty-state vertical rhythm has expressive headroom. Re-introduce a 12dp token in a later phase if a real geometric need surfaces in execution; the rest of the system can absorb that without churn.

**Exceptions:**
- iOS safe-area insets are added on top of these tokens via `WindowInsets.safeContent` — never hardcode status-bar or home-indicator padding.
- Touch target minimum: 44dp on iOS, 48dp on Android. Dock tab cells and the floating search button MUST satisfy this even if visual padding is smaller — use a transparent expansion via `Modifier.minimumInteractiveComponentSize()` or equivalent.
- Dock geometry: 56dp expanded height, 44dp collapsed height. These are absolute pixel values driven by touch-target ergonomics, not spacing-scale tokens.

---

## Typography

Four named text styles, two weights (Regular 400, Semibold 600). Use system default font family; let the platform pick SF Pro / Roboto.

| Role | Size | Weight | Line Height | Letter Spacing | Usage |
|------|------|--------|-------------|----------------|-------|
| `display` | 28sp | 600 (Semibold) | 1.2 (≈34sp) | -0.2sp | Empty-state headline (the calm, anticipatory line) |
| `title` | 20sp | 600 (Semibold) | 1.2 (≈24sp) | 0sp | Inline tab title at top of each screen body (no top app bar — D-04) |
| `body` | 16sp | 400 (Regular) | 1.5 (≈24sp) | 0sp | Empty-state subline; search input value text; default screen body copy |
| `label` | 13sp | 600 (Semibold) | 1.2 (≈16sp) | 0.1sp | Dock tab labels (always shown, both active + inactive — D-02); chip text |

**Scale enforcement:** No raw `TextStyle(fontSize = ...)` in screen code. All text styles come from `RecipeTheme.typography.{display,title,body,label}`. The `title` role is the only header style this phase ships — there is no `headline` / `h1..h6` cascade because there's no top app bar (D-04) and screens don't yet have multi-level content hierarchy.

**Polish-language readiness:**
- All four roles must render Polish diacritics (ą, ć, ę, ł, ń, ó, ś, ź, ż) without clipping. Line-height ratios above (1.2 / 1.5) leave headroom for `ą` and `Ż` accents.
- Long Polish tab labels constrain the `label` role: `Spiżarnia` is the longest (9 chars including diacritic). Dock label cells must accommodate this without truncation at default font scale; with system font scaling at 1.3× the dock may compress label visibility (active-only) — this is acceptable in v1 and revisited in Phase 10.

---

## Color

Light + dark schemes are both defined this phase (CONTEXT D-15) and follow the system setting. The mockup palette is reference, not ported. Tokens are exposed as semantic roles (CONTEXT D-14), never raw hex in screen code.

### Semantic roles (60/30/10 + supporting)

| Role | Light value | Dark value | Usage (60/30/10 mapping) |
|------|-------------|-----------|--------------------------|
| `background` | `#F7F5F1` (warm off-white) | `#0F1113` (near-black warm) | **Dominant 60%** — full-screen background behind every tab |
| `surface` | `#FFFFFF` | `#1A1D21` | **Secondary 30%** — solid card / sheet / search-pill substrate when glass is unavailable (flat fallback) |
| `surfaceGlass` | `#FFFFFF @ 60% alpha` | `#1A1D21 @ 55% alpha` | Tint layer composited inside `GlassSurface` (dock, search pill, floating action button); the Liquid/Haze blur reads through this |
| `content` | `#0F1113` | `#F1EFEA` | Primary text on `background` and `surface` |
| `contentMuted` | `#6B6E73` | `#9AA0A6` | Empty-state subline, inactive tab label, secondary captions |
| `accent` | `#D97757` (warm terracotta) | `#E48A6E` | **Accent 10%** — see "Accent reserved for" below |
| `separator` | `#E5E1DA` | `#2A2D31` | Hairline dividers (1dp); inter-tab separators inside dock if used |
| `borderCard` | `#E5E1DA @ 60% alpha` | `#FFFFFF @ 8% alpha` | Outline on glass surfaces (dock, search pill) for depth in light mode and edge clarity in dark mode |
| `destructive` | `#C0392B` | `#E57368` | Reserved — no destructive actions exist in this phase, but the token is declared so feature phases (sign-out confirmation, plan-entry deletion) inherit it |

### Accent reserved for

The `accent` color (warm terracotta, 10% of pixel real estate target) is used **only** for:

1. **Active dock tab** — the wider, emphasized active tab cell uses `accent` at full opacity for its icon + label color, on a `surfaceGlass` substrate. Inactive tabs use `contentMuted`.
2. **Search input caret + selection highlight** — the cursor in the open search pill, and any text-selection range.

Accent is NOT used for:
- Dividers, borders, separators
- Empty-state icons (those use `contentMuted` per D-10 — calm, low-saturation)
- The dock substrate itself (that is `surfaceGlass`, not `accent`)
- Standard body text

This list is exhaustive for this phase. Future phases extend it — primary CTA buttons (Phase 5+), shopping-list checked items (Phase 9), etc.

### 60/30/10 audit (this phase only)

- 60% `background` — yes; the four tab screens are predominantly empty (empty states), so the warm off-white / near-black background dominates.
- 30% `surface` / `surfaceGlass` — yes; the dock pill, the floating search button, and the search pill are the only substantial non-background surfaces in the shell.
- 10% `accent` — yes; only the active tab and the search caret carry accent. Quantitatively below 10%, which is correct for a calm shell.

---

## Copywriting Contract

All strings go through Compose Resources (`composeResources/values/strings.xml` or per-locale equivalents). No literal Polish strings in `.kt` files. Resource keys are namespaced by feature: `shell_*`, `empty_*`, `search_*`. Polish copy is the v1 ship language; the resource catalog is multi-locale-ready for Phase 11.

### Tab labels (CONTEXT D-03 — order: Planer, Przepisy, Spiżarnia, Zakupy)

| Resource key | Polish copy | English placeholder (not shipped) |
|--------------|-------------|-----------------------------------|
| `shell_tab_planner` | `Planer` | Planner |
| `shell_tab_recipes` | `Przepisy` | Recipes |
| `shell_tab_pantry` | `Spiżarnia` | Pantry |
| `shell_tab_shopping` | `Zakupy` | Shopping |

### Empty states (CONTEXT D-10, D-11 — anticipatory tone, icon + headline + subline, no CTA)

| Tab | Icon (Material Outlined) | Headline (display) | Subline (body) |
|-----|--------------------------|--------------------|----------------|
| Planer | `Icons.Outlined.CalendarMonth` | `Twój plan tygodnia czeka` | `Wkrótce zobaczysz tu zaplanowane posiłki.` |
| Przepisy | `Icons.Outlined.MenuBook` | `Tu pojawi się Twoja książka kucharska` | `Po dodaniu pierwszych przepisów zobaczysz je w tym miejscu.` |
| Spiżarnia | `Icons.Outlined.Inventory2` | `Spiżarnia jest jeszcze pusta` | `Wkrótce zobaczysz tu wszystko, co masz pod ręką.` |
| Zakupy | `Icons.Outlined.ShoppingCart` | `Lista zakupów czeka na Twój plan` | `Gdy zaplanujesz tydzień, zobaczysz tu, czego brakuje.` |

Resource keys: `empty_planner_title` / `empty_planner_subtitle`, `empty_recipes_title` / `empty_recipes_subtitle`, `empty_pantry_title` / `empty_pantry_subtitle`, `empty_shopping_title` / `empty_shopping_subtitle`.

**Tone rules:**
- Forward-looking: "Wkrótce", "Po dodaniu", "Gdy zaplanujesz" — signal the feature is real, not broken.
- No "Brak danych", no chatty onboarding ("Witaj!"), no exclamation marks.
- Subline ends with a period. Headline does not.
- No CTA buttons (CONTEXT D-12). The `EmptyState` composable's `action` slot is reserved unused this phase (D-13).

**Phase 11 caveat:** copy may be tuned during the localization pass. Resource keys above are the contract; copy strings are best-current.

### Search affordance (CONTEXT D-06 through D-09)

| Resource key | Polish copy | Purpose |
|--------------|-------------|---------|
| `search_open_a11y` | `Otwórz wyszukiwanie` | Content description for the floating search-icon button (icon-only) |
| `search_close_a11y` | `Zamknij wyszukiwanie` | Content description for the collapsed dock toggle when search is open (D-05) |
| `search_clear_a11y` | `Wyczyść` | Content description for the clear button inside the search pill (visible when query is non-empty) |
| `search_placeholder_recipes` | `Szukaj przepisów…` | Search pill placeholder on Przepisy tab |
| `search_placeholder_pantry` | `Szukaj w spiżarni…` | Search pill placeholder on Spiżarnia tab |

Search body content: **none** (CONTEXT D-07). No "no results" copy this phase. Phase 5 wires real result rendering. Empty `SearchSurface` body renders an empty `Box` matched to `background`.

### Error / sign-out (out of scope for this phase but tokens reserved)

This phase introduces no error surfaces (auth errors are Phase 2 territory; sync errors are Phase 4+) and no destructive actions. The `destructive` color and a future `confirm_signout_*` resource family are NOT defined here — they ship with their owning phase.

### CTA / primary action

This phase has **no primary CTA button**. The shell is navigation chrome and empty surfaces. The `accent` color contract above declares accent reservation; the first real primary CTA ships in Phase 5 (recipe browse).

---

## Component Inventory (this phase)

Composables introduced in `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/`:

| Composable | Path | Built on | Visual contract |
|-----------|------|----------|-----------------|
| `RecipeTheme` | `ui/theme/RecipeTheme.kt` | CompositionLocal scaffold | Provides `RecipeColors`, `RecipeTypography`, `RecipeSpacing`, `RecipeShapes`, `RecipeGlass` to descendants |
| `GlassSurface` | `ui/components/glass/GlassSurface.kt` | Liquid → Haze → flat | Single primitive consumed by dock, search pill, floating buttons. Same token API across all three backends (color, opacity, radius). Compile-time backend selection per target; debug-build runtime toggle (CONTEXT D-16, D-17) |
| `AppShell` | `ui/screens/shell/AppShell.kt` | Compose Unstyled `Scaffold`-equivalent | Auth-gated root: hosts root NavHost + the bottom dock + the floating search/action surface. Renders `background` color edge-to-edge under safe-area insets. |
| `DockBar` | `ui/components/dock/DockBar.kt` | Compose Unstyled `TabGroup`-equivalent + GlassSurface | Floating bottom pill, 4 tabs (icon + label always — D-02), active tab wider with `accent` foreground; collapses to single circular icon-only toggle when `searchOpen == true` (D-05). Capsule shape: full-pill (height/2 corner radius). Height: 56dp; collapsed height: 44dp. |
| `FloatingSearchButton` | `ui/components/dock/FloatingSearchButton.kt` | Compose Unstyled `Button` + GlassSurface | 44dp circular glass button, search icon (`Icons.Outlined.Search`) tinted `content`. Adjacent to dock with `sm` (8dp) gap. Visible only on Przepisy + Spiżarnia tabs (D-06). Hidden when `searchOpen == true`. |
| `SearchPill` | `ui/components/search/SearchPill.kt` | Compose Unstyled `TextField` (renderless) + GlassSurface | Inline bottom search pill (D-09). Capsule shape. Holds: leading search icon, text input (placeholder per tab), trailing clear button (visible when query non-empty). Substrate: `surfaceGlass`. Body content behind it stays visible. Height: 44dp. |
| `EmptyState` | `ui/components/empty/EmptyState.kt` | Plain Compose | Reusable `EmptyState(icon: ImageVector, title: String, subtitle: String, action: (@Composable () -> Unit)? = null)` — D-13. Vertical center on screen. Icon 48dp tinted `contentMuted`. Spacing: icon → 8dp (`sm`) → headline (`display`) → 16dp (`lg`) → subline (`body`, color `contentMuted`). `action` slot is below subline at 24dp (`xl`) gap when present; unused this phase. |
| `Screen scaffolds` | `ui/screens/{planner,recipes,pantry,shopping}/{Tab}Screen.kt` | `RecipeTheme` + `EmptyState` | Each: inline tab title at top in `title` style + `lg` padding, then centered `EmptyState`. Background: `RecipeColors.background`. |

**Renderless primitive boundary:** Where Compose Unstyled provides a renderless primitive (button, text field, tab group), Recipe components MUST consume it and apply local styling, not implement the gesture/a11y semantics from scratch. This is the explicit project decision (PROJECT.md § Components: Composables / Compose Unstyled).

---

## Interaction Contracts

### Dock state machine (CONTEXT D-05)

States:
- `Expanded` — default. 4-tab pill, all icons + labels visible, active tab wider with `accent` foreground.
- `Collapsed` — when `searchOpen == true`. Single circular cell showing only the active tab's icon, no label, height 44dp (vs 56dp expanded).

Transition: **single coordinated animation** (not two independent ones — explicit user intent in CONTEXT specifics). Suggested duration: 250ms with a standard easing (e.g. `FastOutSlowInEasing`); planner picks final curves and Phase 10 tunes on real device.

Tapping the collapsed dock = `setSearchOpen(false)` = re-expand + close search.

### Search affordance (CONTEXT D-06 through D-09)

- Visible only on `Przepisy` + `Spiżarnia` tabs.
- `FloatingSearchButton` tap → `searchOpen = true` → `SearchPill` slides up / fades in, `DockBar` collapses, `FloatingSearchButton` hides. Coordinated with the dock-collapse animation as one motion.
- Closing: tap collapsed dock OR system back gesture → `searchOpen = false` AND `query = ""` (D-08). Re-opening starts blank.
- Query state lives in the per-tab `SearchViewModel` (one for Recipes, one for Pantry); no persistence across close, tab-switch, or app launch.
- Body of search surface: **renders nothing** this phase (D-07). The `SearchPill` overlays the existing tab body; the body remains visible behind it.

### Tab navigation (UI-03 / CONTEXT D-03)

- Default landing tab on first sign-in: `Planer` (D-03 — departs from REQ listing order, which research confirmed non-binding).
- Tab order in dock (left→right): Planer / Przepisy / Spiżarnia / Zakupy.
- Each tab owns an independent nested `NavHost` (CONTEXT D-03 + research ARCHITECTURE recommendation), so future detail screens preserve back stacks per tab.
- Tab switch preserves the destination tab's back stack; selecting an already-active tab pops to its root (standard mobile pattern).
- No tab-bar hide-on-scroll behavior this phase (deferred — CONTEXT § Deferred).

### Accessibility

- Each dock tab cell: `Modifier.semantics { role = Role.Tab; selected = isActive; contentDescription = "$tabLabel${if (isActive) ", aktywna" else ""}" }`.
- `FloatingSearchButton`: `contentDescription = stringResource(Res.string.search_open_a11y)`.
- Collapsed dock toggle: `contentDescription = stringResource(Res.string.search_close_a11y)`.
- Search pill clear button: `contentDescription = stringResource(Res.string.search_clear_a11y)`; visible only when query is non-empty.
- Touch targets: dock tab cells and the floating search button MUST be ≥ 44dp on iOS, ≥ 48dp on Android.
- Focus order when search opens: search input field receives focus on open; soft keyboard appears; the collapsed dock toggle is in the tab order after the clear button.
- Empty-state regions: `Modifier.semantics(mergeDescendants = true) { ... }` so VoiceOver reads the headline + subline as one announcement, not two.

---

## Glass / Liquid contract

`GlassSurface` is the only entry point to glass effects this phase. Direct calls to Liquid or Haze APIs from screen code are forbidden — those only live inside `GlassSurface`'s internal backend selection.

### Backend selection

| Backend | When engaged | Notes |
|---------|--------------|-------|
| Liquid | Default on iOS + Android where Liquid 1.1.x compiles cleanly for the target | Pixel-sampling refractive approximation; matches PROJECT decision and CLAUDE.md convention #10 |
| Haze | Compile-time fallback if Liquid does not ship for a target, OR runtime debug-toggle override | Plain blur; no refraction |
| Flat | Compile-time fallback if neither Liquid nor Haze is available, OR debug-toggle override | Solid translucent surface using `surfaceGlass` token; no blur |

Selection mechanism (CONTEXT D-17):
- **Compile-time per target:** the build picks the backend at build time. No runtime branch in production binaries.
- **Runtime debug toggle (debug builds only):** stored via `multiplatform-settings`, surfaced through a hidden settings entry or build flag. Lets the developer switch backends on-device for visual comparison.

### Surface parameters

The dock, search pill, and floating search button all consume the same token API:

| Parameter | Value | Notes |
|-----------|-------|-------|
| Tint color | `surfaceGlass` (light: white@60%, dark: dark@55%) | Composited inside the glass effect |
| Corner radius | 28dp for the dock pill (full-pill at 56dp height); 22dp for the collapsed dock toggle (full-pill at 44dp); 22dp for the search pill (full-pill at 44dp); 22dp for the floating search button (full-circle at 44dp) | All chrome elements are pill / circle, never rectangular |
| Border | 1dp `borderCard` outline | Provides edge clarity especially in dark mode |
| Elevation / shadow | Soft drop shadow: y-offset 8dp, blur 24dp, opacity 12% in light mode; opacity 0% (no shadow, just border) in dark mode | Applied via `Modifier.shadow()` outside the glass clip |
| Blur radius (Liquid + Haze) | Initial value: 24dp. Phase 10 tunes on real device. Planner may pick library-specific equivalent. |
| Refraction (Liquid only) | Library default initially; tune in Phase 10. |

**Chrome-only constraint (CLAUDE.md #10 + PITFALLS Pitfall 5):** Glass surfaces are applied to dock, search pill, and floating search button only. NEVER over scrolling content. The empty-state area, tab body, and any future list rows are flat — no `GlassSurface` wraps them.

### Fallback test plan (informational)

Each backend must render visually distinct but functionally identical chrome. Acceptance: switching the debug toggle between Liquid / Haze / flat keeps the dock, search pill, and floating button in the same geometry, with the same content positioning, only the substrate effect changes.

---

## Layout & Safe Area

- Root container: full-screen, edge-to-edge. `WindowInsets.statusBars` is consumed by tab body content (top inset added to the inline tab title's top padding). `WindowInsets.navigationBars` + iOS home-indicator inset are consumed by the dock's bottom offset.
- The dock floats `sm` (8dp) above the bottom safe-area inset. The search pill and floating search button sit at the same vertical baseline as the dock when active.
- iOS keyboard avoidance: when the search input has focus, the search pill animates above the soft keyboard via `imeAnimationSource` / `imePadding()`. The dock's collapsed toggle rides up with it (single coordinated motion).
- No top app bar (D-04). The inline tab title sits at the top of each screen body with `xl` (24dp) top padding above the status-bar inset, then `lg` (16dp) below before screen content (or before the empty-state vertical centering region).

---

## Registry Safety

| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none — not applicable (Compose Multiplatform stack) | not required |
| Compose Unstyled (`composables.com`) | renderless primitives (Button, TextField, TabGroup-equivalent) — locally restyled by Recipe components | not required (first-party renderless library; no third-party code lifted into the project) |
| Liquid (`io.github.fletchmckee.liquid:liquid`) | consumed as a Gradle dependency, not as copied source | dependency review passed — date 2026-05-08; no source code lifted |
| Haze (`dev.chrisbanes.haze:haze`) | consumed as a Gradle dependency, not as copied source | dependency review passed — date 2026-05-08; no source code lifted |

No third-party shadcn registries declared. No source-code blocks vended into the repo. Standard Gradle dependency review applies.

---

## Out-of-Scope Boundaries (this UI-SPEC)

These intentionally have no contract here and are owned by later phases:

- Recipe list rendering, grid spec, card style — Phase 5
- Real planner grid, day cells, slot cells — Phase 6
- Pantry inventory rows, category headers — Phase 8
- Shopping list rows, checked-state styling, category groupings — Phase 9
- Theme polish (final color palette tuning, custom font) — Phase 10
- Animation curves and durations beyond the dock-collapse 250ms default — Phase 10 tunes on real device
- Real-device Liquid parameter tuning (refraction strength, specular highlights) — Phase 10
- Polish copy final pass — Phase 11
- Profile / settings / sign-out chrome placement — Phase 3 onward (no top bar exists yet — D-04)

---

## Pre-Population Audit

| Field | Source |
|-------|--------|
| Tab order, default landing | CONTEXT D-03 |
| Tab labels (Polish) | CONTEXT D-03 + REQUIREMENTS UI-03 |
| Dock shape, label visibility | CONTEXT D-01, D-02 |
| Top app bar absence | CONTEXT D-04 |
| Dock-collapse-on-search transition | CONTEXT D-05 + user verbatim |
| Search affordance scope (which tabs) | CONTEXT D-06 |
| Search behavior this phase | CONTEXT D-07, D-08, D-09 |
| Empty-state pattern + tone + no CTA | CONTEXT D-10, D-11, D-12 |
| `EmptyState` composable signature | CONTEXT D-13 |
| Theme scaffold scope | CONTEXT D-14 |
| Light + dark schemes | CONTEXT D-15 |
| GlassSurface fallback chain | CONTEXT D-16 |
| Compile-time + debug toggle | CONTEXT D-17 |
| Compose Unstyled foundation | PROJECT.md Key Decisions + CLAUDE.md tech stack |
| Liquid first / Haze fallback | PROJECT.md + CLAUDE.md #10 |
| Strings externalized | CLAUDE.md #9 + REQUIREMENTS UI-01 |
| Material 3 boundary | PROJECT.md + CONTEXT discretion default |
| Material Icons Outlined | CONTEXT discretion default |
| Spacing scale 4/8/16/24/32/48 | CONTEXT D-14 (12dp step retired during UI-SPEC verification — see Spacing § Revision note) |
| Typography 4 styles, 2 weights | gsd-ui-researcher recommendation aligned with CONTEXT D-14 named scale |
| Color hex values | gsd-ui-researcher recommendation (mockup is reference, not ported — CONTEXT D-15) |
| Empty-state copy strings | gsd-ui-researcher recommendation; subject to Phase 11 copy pass |
| Touch target minimums | iOS HIG / Material guidelines + accessibility default |
| 250ms transition duration | gsd-ui-researcher reasonable default; CONTEXT discretion + Phase 10 tunes |

No user questions asked this round — CONTEXT.md, PROJECT.md, REQUIREMENTS.md, and CLAUDE.md collectively answered every load-bearing decision. Discretionary defaults (color hex values, typography sizes, copy strings, animation duration) are recorded above and revisitable in Phase 10/11.

---

## 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
