---
phase: 2
slug: authentication-foundation
status: approved
shadcn_initialized: false
preset: none
created: 2026-04-27
reviewed_at: 2026-04-27
---

# Phase 2 — UI Design Contract

> Visual and interaction contract for the Authentication Foundation phase. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
>
> **Stack note:** This is a Kotlin Multiplatform / Compose Multiplatform app, not shadcn/web. The template's "shadcn" framing has been adapted for Material 3 on CMP. All values below are expressed in `dp` (Compose) and Material 3 `Type` / `ColorScheme` roles.
>
> **Phase boundary reminder:** Phase 2 ships SCAFFOLD UI quality — three composables (`SplashScreen`, `LoginScreen`, `PostLoginPlaceholderScreen`) plus the auth gate in `App()`. The Liquid-Glass visual language and Haze blur land in **Phase 10**. The polished Polish copy + display font live in **Phase 11**. Tokens locked here are the SEED that later phases extend, not retroactively rewrite.

---

## Design System

| Property | Value |
|----------|-------|
| Tool | Compose Multiplatform Material 3 |
| Preset | not applicable |
| Component library | `androidx.compose.material3` (CMP port via `compose-multiplatform` 1.7+) |
| Icon library | none used in Phase 2 (no icons on Splash / Login / PostLoginPlaceholder); `androidx.compose.material.icons` available but deferred to Phase 5+ |
| Font | system default — `FontFamily.Default` (Compose Multiplatform resolves to SF on iOS, Roboto on Android, system default on JVM/Wasm). **Reserved for Phase 11:** display font selection + custom `FontFamily` in Compose Resources. |

**Component sourcing:** Material 3 stdlib only (`Surface`, `Text`, `Button`, `OutlinedButton`, `CircularProgressIndicator`, `Column`, `Spacer`, `Box`). No third-party UI components, no Haze, no custom blur. **Haze blur is explicitly deferred to Phase 10** per CLAUDE.md non-negotiable #10 ("Haze on chrome only, never over fast-scrolling content").

---

## Spacing Scale

Declared values (all multiples of 4, expressed in Compose `dp`):

| Token | Value | Phase 2 Usage |
|-------|-------|---------------|
| xs | 4.dp | Reserved for later phases (icon gaps, fine adjustments) |
| sm | 8.dp | Vertical gap between welcome text and "Wyloguj się" button on PostLogin; vertical gap between "Recipe" wordmark and progress indicator on Splash |
| md | 16.dp | Default vertical gap between button and inline error text on LoginScreen; horizontal screen-edge padding on all three screens |
| lg | 24.dp | Vertical gap between app-name display and primary button on LoginScreen; vertical gap between welcome text block and logout button on PostLoginPlaceholder |
| xl | 32.dp | Reserved for later phases (planner/calendar layouts) |
| 2xl | 48.dp | Vertical breathing room between top-most centered content cluster and its surrounding empty space (visual centering target on LoginScreen and Splash) |
| 3xl | 64.dp | Reserved for later phases (page-level section breaks in catalog / planner) |

**Touch target floor:** All interactive controls (buttons) honor Material 3 minimum touch target of `48.dp` height. Material 3's `Button` defaults satisfy this; do not shrink.

**Safe-area insets:** All three screens wrap their root in `Modifier.safeContentPadding()` (already established by Phase 1's `App.kt` pattern). This keeps content clear of the iOS notch/home indicator and Android system bars without introducing platform-specific code in `commonMain`.

**Exceptions:** none. The full `xs..3xl` scale is declared for forward-compat with Phases 3+; tokens marked "Reserved for later phases" are spec'd here so the planner/executor draws from one canonical scale instead of inventing per-phase increments.

---

## Typography

Material 3 `Typography` roles. Phase 2 uses four roles; the rest of the M3 scale is implicitly available for later phases. Phase 11 may swap `FontFamily` but the **role-to-element mapping below is locked**.

| Role | M3 Token | Size | Weight | Line Height | Phase 2 Element |
|------|----------|------|--------|-------------|-----------------|
| Display | `displaySmall` | 36.sp | Regular (W400) | 44.sp (≈1.22) | "Recipe" wordmark on `SplashScreen` and `LoginScreen` |
| Heading | `headlineSmall` | 24.sp | Regular (W400) | 32.sp (≈1.33) | `Witaj, {displayName}!` welcome text on `PostLoginPlaceholderScreen` |
| Body | `bodyLarge` | 16.sp | Regular (W400) | 24.sp (1.5) | Inline error text below the sign-in button on `LoginScreen` |
| Label | `labelLarge` | 14.sp | Medium (W500) | 20.sp (≈1.43) | Button label text — "Zaloguj się przez Authentik", "Wyloguj się" (Material 3 `Button` slot uses this role by default) |

**Weights declared:** exactly 2 — Regular (W400) for body / heading / display, Medium (W500) for button labels (Material 3 default for `labelLarge`). No Bold, no Light, no SemiBold in Phase 2.

**Sizes declared:** exactly 4 — 14, 16, 24, 36. This satisfies the "3–4 sizes" cap.

**Line-height policy:**
- Body (16.sp body): 1.5 ratio → 24.sp line height. Matches the brand recommendation; Material 3 `bodyLarge` default is 24.sp.
- Heading (24.sp `headlineSmall`): ~1.33 ratio. Tighter than body per Material 3 baseline; aligns with the "calmer typography" direction in PROJECT.md.
- Display (36.sp `displaySmall`): ~1.22 ratio. Material 3 default.

**Implementation:** use `MaterialTheme.typography.displaySmall` / `.headlineSmall` / `.bodyLarge` / `.labelLarge` directly. Do **not** override `style.copy(fontWeight = ...)` ad-hoc in Phase 2 composables — if a deviation is needed, add it to the `Typography` config in `ui/theme/Typography.kt` so Phase 11 has one place to retune.

---

## Color

Material 3 `ColorScheme` derived from a **single seed color** via `dynamicLightColorScheme` / `dynamicDarkColorScheme` is **not** used (dynamic color is Android 12+ only and would diverge between iOS and Android). Instead Phase 2 ships **explicit baseline schemes** seeded once:

- **Seed color:** `#3B6939` (mid-saturation green, warm-leaning — chosen as a placeholder that reads well in a cooking/meal-planning context without committing to a brand identity).
- **Generation:** `lightColorScheme()` / `darkColorScheme()` Material 3 defaults overridden with the seed-derived `primary` only. All other roles use Material 3 baseline values for their respective scheme.
- **Phase 11 hand-off:** the seed value is open to revision in Phase 11 (final brand-color pass). Tokens listed below are CONTRACT for Phase 2; Phase 11 may rebase the entire palette around a different seed without breaking the role-to-element mapping locked here.

The 60 / 30 / 10 split, mapped to Material 3 roles:

| Role | Light scheme | Dark scheme | Usage |
|------|--------------|-------------|-------|
| Dominant (60%) — `surface` | `#FEF7FF` (M3 default) | `#141218` (M3 default) | Root background of all three Phase 2 screens |
| Secondary (30%) — `surfaceContainer` | `#F3EDF7` (M3 default) | `#211F26` (M3 default) | **Reserved for Phase 5+** (cards, sheets, nav containers). Phase 2 has no card surfaces; this token is declared for forward-compat. |
| Accent (10%) — `primary` | `#3B6939` (seed) | `#A2D597` (seed-derived dark variant) | The single primary CTA on each screen — **only**: "Zaloguj się przez Authentik" button (`LoginScreen`) and **only** that button. Logout uses a different role (see below). |
| Destructive — `error` | `#BA1A1A` (M3 default) | `#FFB4AB` (M3 default) | Inline error text color on `LoginScreen` (`auth_error_*` strings). Reserved for actual error states only — not used for the "Wyloguj się" button. |

**Accent reserved for:** the `LoginScreen` primary CTA button (`Button` composable using `colors = ButtonDefaults.buttonColors()` which resolves to `containerColor = primary`). Nothing else in Phase 2.

**"Wyloguj się" button styling:** uses Material 3 `OutlinedButton` (not `Button`) → `borderColor` = `outline`, `contentColor` = `primary`. This is a deliberate hierarchy choice: logout is a less-frequent, more-deliberate action than login, and reserving the filled-accent variant for the login CTA preserves the "10% accent" ratio. **Not** styled as destructive (red `error`) because logout is not destructive in this app — it ends the session but does not delete user data.

**Dark mode is required.** Per orchestrator note (homelab user's primary environment is dark mode), both `lightColorScheme()` and `darkColorScheme()` MUST be wired. App respects system theme via `isSystemInDarkTheme()` (already standard in Compose). No in-app theme toggle in Phase 2.

**Translucency / blur:** none in Phase 2. All surfaces are opaque. The Liquid-Glass aesthetic begins in Phase 10.

---

## Copywriting Contract

All user-facing strings live in **Compose Resources** (`composeApp/src/commonMain/composeResources/values/strings.xml` per Compose Multiplatform conventions) per CLAUDE.md non-negotiable #9 + CONTEXT D-34. Polish copy below is **scaffold quality**; Phase 11 polishes for plural forms, tone, and proofs the full locale.

| Element | Resource Key | Polish Copy (scaffold) | Screen | Notes |
|---------|--------------|------------------------|--------|-------|
| App wordmark | `auth_app_name` | `Recipe` | Splash, Login | English working title per PROJECT.md; final brand name is a Phase 11 decision. Not localizable in Phase 2. |
| Primary CTA | `auth_sign_in_button` | `Zaloguj się przez Authentik` | LoginScreen | Verb + noun; explicit IdP name to set expectation that the system browser will open. |
| Secondary CTA (logout) | `auth_sign_out_button` | `Wyloguj się` | PostLoginPlaceholderScreen | Single Polish reflexive verb; matches user's expected idiom. |
| Welcome / authenticated state | `auth_welcome_format` | `Witaj, %1$s!` | PostLoginPlaceholderScreen | `%1$s` substituted with `User.displayName` from the JIT-provisioned server response. Use Compose Resources `stringResource(Res.string.auth_welcome_format, user.displayName)`. |
| Error: user cancelled | `auth_error_cancelled` | `Logowanie anulowane. Spróbuj ponownie.` | LoginScreen (inline below button) | Triggered when AppAuth surfaces `OIDAuthError.userCancelled` (iOS) / `AuthorizationException.GeneralErrors.USER_CANCELED_AUTH_FLOW` (Android). |
| Error: network unreachable | `auth_error_network` | `Nie można połączyć z Authentik. Sprawdź połączenie.` | LoginScreen (inline below button) | Triggered on `IOException` / network errors during authorization OR token exchange. |
| Error: token exchange / validation failure | `auth_error_unknown` | `Coś poszło nie tak. Spróbuj ponownie.` | LoginScreen (inline below button) | Catch-all for token-exchange failures, JWT validation errors, JIT-provisioning 5xx. |

**Empty states:** Phase 2 has no empty-state surfaces. The "no user yet" condition routes to `LoginScreen`, which is itself the empty state. No "you're not signed in yet" placeholder text is needed.

**Destructive confirmation:** none in Phase 2. Logout is silent (CONTEXT D-19): "Wyloguj się" tap immediately initiates RP-initiated end-session without a confirmation modal. **Rationale:** the user can re-authenticate trivially; a confirmation modal here would be cargo-culted from destructive-delete patterns where re-creation is impossible. The post-login screen is a placeholder anyway and gets replaced by household onboarding in Phase 3.

**Refresh-failure UX:** silent transition (CONTEXT D-18). When `AuthSession` detects an `invalid_grant` from a background token refresh, it emits `AuthState.Unauthenticated` and the auth gate routes to `LoginScreen`. No toast, no modal, no error message on the LoginScreen itself (the user landed there silently — there is no "previous attempt" to error about). Logged at `Logger.withTag("auth").w(...)` for diagnostics.

**Inline-error display rules (LoginScreen):**
- Error text is rendered **below** the primary button with `md` (16.dp) vertical gap.
- Button **stays enabled** during the error state — the user can retry by tapping again.
- Tapping the button again **clears** the previous error message before initiating a new login flow (so the user does not see stale error text during the next attempt).
- Error text uses `bodyLarge` typography role, `error` color (see Color section).
- Errors are NOT surfaced as Snackbars in Phase 2. Inline-below-button is the contract; Snackbars require a `Scaffold` host that Phase 2 does not need.

**Loading / pending UX (LoginScreen):**
- While AppAuth's authorization request is in flight (system browser is open), the LoginScreen does NOT need a separate loading state — the system browser is full-screen and obscures the app.
- After the system browser dismisses but before token exchange + JIT-provisioning completes, the button shows a `CircularProgressIndicator` (16.dp) inside its content slot, replacing the label, with the button **disabled**. Total expected duration: <500ms in practice.
- Implementation hint: a `Boolean` `isLoading` flag in `LoginScreenState` controls this.

**Splash UX:**
- Visible during `AuthState.Loading` (deserializing persisted `AuthState` blob, possibly running a refresh).
- Centered "Recipe" wordmark using `displaySmall`.
- `sm` (8.dp) below: a `CircularProgressIndicator` at default size (40.dp), `color = primary`.
- No "Loading..." text. No marketing copy. No tagline.
- Background = `surface` (matches Login + PostLogin to avoid a color flash when the auth gate transitions).

---

## Auth Gate Routing Contract

The `App()` composable observes `AuthSession.state: StateFlow<AuthState>` and renders exactly one of:

| `AuthState` value | Rendered composable |
|-------------------|---------------------|
| `AuthState.Loading` | `SplashScreen()` |
| `AuthState.Unauthenticated` | `LoginScreen(viewModel = koinViewModel())` |
| `AuthState.Authenticated(user, householdId)` | `PostLoginPlaceholderScreen(user, viewModel = koinViewModel())` (Phase 2). Phase 3 replaces with `HouseholdGate`. |

**Transition behavior:** state changes drive recomposition; no manual navigation calls. Material 3 default cross-fade (the implicit `Crossfade` recommended pattern, NOT explicit — keep Phase 2 minimal) is acceptable but not required. **Required:** no white flash between transitions — both screens use the same `surface` background.

Implementation note for executor: replace the existing `App.kt` body (currently the JetBrains template's button-and-greeting demo) with a `when` over `authSession.state.collectAsState().value`. Keep the existing `MaterialTheme { ... }` wrapper.

---

## Component Inventory (Phase 2)

Composables the planner / executor must produce in `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/`:

| Composable | File | Responsibility |
|------------|------|----------------|
| `SplashScreen()` | `SplashScreen.kt` | Stateless. Renders wordmark + progress indicator. No ViewModel — the auth gate above it owns state. |
| `LoginScreen(viewModel: LoginViewModel)` | `LoginScreen.kt` | Stateless wrt auth tokens (those live in `AuthSession`). Owns local UI state for `isLoading` + `errorKind`. Triggers `viewModel.onSignInClick()` which delegates to `AuthSession.login()`. |
| `LoginViewModel` | `LoginViewModel.kt` | Wraps `AuthSession`. Maps `AuthSession.LoginResult` → `LoginScreenState(isLoading, errorKey: StringResource?)`. Method-per-action: `onSignInClick()`. |
| `LoginScreenState` | (data class in `LoginViewModel.kt`) | `(val isLoading: Boolean, val errorKey: StringResource?)`. Immutable. |
| `PostLoginPlaceholderScreen(user: User, viewModel: PostLoginViewModel)` | `PostLoginPlaceholderScreen.kt` | Renders welcome text + logout button. Triggers `viewModel.onSignOutClick()`. |
| `PostLoginViewModel` | `PostLoginViewModel.kt` | Wraps `AuthSession.logout()`. Trivial in Phase 2 — exists so Phase 3's `HouseholdGate` follows the same VM pattern. |
| `RecipeTheme(content)` | `ui/theme/RecipeTheme.kt` | Top-level theme wrapper applying `lightColorScheme()` / `darkColorScheme()` based on `isSystemInDarkTheme()`. Wraps `MaterialTheme(colorScheme, typography, shapes)`. **Phase 2 ships this seed;** later phases extend with custom typography + shape tokens here. |

**No `Scaffold` in Phase 2.** Each of the three auth screens uses `Surface(modifier = Modifier.fillMaxSize().safeContentPadding())` as the root. `Scaffold` (with its `topBar` / `bottomBar` slots and Snackbar host) lands in Phase 5 (`RecipeListScreen`) or Phase 10 (`MainScaffold` chrome).

---

## Layout Contract

All three screens use a **vertically-centered single-column layout**:

```
Modifier.fillMaxSize()
  .background(MaterialTheme.colorScheme.surface)
  .safeContentPadding()
  .padding(horizontal = 16.dp)        // md token
Column(
  horizontalAlignment = Alignment.CenterHorizontally,
  verticalArrangement = Arrangement.Center,
  modifier = Modifier.fillMaxSize(),
)
```

**Per-screen content order (top → bottom):**

| Screen | Content |
|--------|---------|
| `SplashScreen` | Wordmark `displaySmall` → `Spacer(8.dp)` → `CircularProgressIndicator(color = primary)` |
| `LoginScreen` | Wordmark `displaySmall` → `Spacer(24.dp)` → `Button(onClick = onSignInClick) { Text(R.string.auth_sign_in_button) }` (or `CircularProgressIndicator(16.dp)` when `isLoading`) → `Spacer(16.dp)` → `Text(error, style = bodyLarge, color = error)` if `errorKey != null` |
| `PostLoginPlaceholderScreen` | `Text(stringResource(Res.string.auth_welcome_format, user.displayName), style = headlineSmall)` → `Spacer(24.dp)` → `OutlinedButton(onClick = onSignOutClick) { Text(R.string.auth_sign_out_button) }` |

**Width constraint:** content column natural-fits its children. No `widthIn(max = 480.dp)` tablet-narrowing in Phase 2 — the app targets phone-sized iOS first; tablet polish is post-v1.

---

## Component Sourcing & Safety

| Source | Components Used (Phase 2) | Safety Gate |
|--------|---------------------------|-------------|
| Material 3 stdlib (`androidx.compose.material3`) | `Surface`, `MaterialTheme`, `Text`, `Button`, `OutlinedButton`, `CircularProgressIndicator` | not required (first-party Compose Multiplatform stdlib, applied by `recipe.compose.multiplatform` convention plugin per Phase 1 D-07) |
| Compose Foundation (`androidx.compose.foundation`) | `Column`, `Spacer`, `Box`, `Modifier.background`, `Modifier.safeContentPadding`, `Modifier.fillMaxSize`, `Modifier.padding` | not required (first-party) |
| Compose Resources (`org.jetbrains.compose.components:components-resources`) | `stringResource`, generated `Res.string.*` accessors | not required (first-party Compose Multiplatform; Phase 1 generated `Res` accessors already wired) |
| Third-party UI registry | none in Phase 2 | not applicable |

**No Haze, no third-party UI components in Phase 2.** Haze is gated to Phase 10 per CLAUDE.md non-negotiable #10. Adding third-party UI components to the auth scaffold is explicitly out-of-scope.

---

## Accessibility

| Requirement | Implementation |
|-------------|----------------|
| Touch target ≥48dp | Material 3 `Button` / `OutlinedButton` defaults satisfy this; do not shrink |
| Color contrast (WCAG AA) | Material 3 baseline `lightColorScheme()` / `darkColorScheme()` ship WCAG AA-compliant role pairings (e.g., `onPrimary` on `primary`); seed override only changes `primary` so the contrast pairing holds |
| Dynamic type / font scaling | Material 3 `Typography` roles use `sp` (already scale-respecting); no override forcing fixed sizes |
| Screen reader semantics | `Button` carries its label as accessibility text by default; `Text` for the welcome line is announced by VoiceOver / TalkBack as plain content. No custom `Modifier.semantics` overrides required in Phase 2 |
| RTL | not applicable in Phase 2 (Polish is LTR) |

**Phase 11 will revisit:** `contentDescription` on any decorative imagery, semantic grouping of multi-element clusters, full VoiceOver pass on iOS device.

---

## Out-of-Scope (Reserved for Later Phases)

The following intentionally have NO contract in Phase 2:

| Concern | Owning Phase |
|---------|--------------|
| Tab bar / bottom navigation | Phase 10 (`UI Chrome & Haze`) |
| Top app bar / nav bar with Haze blur | Phase 10 |
| Glass / translucent surface tokens | Phase 10 |
| Display font selection + custom `FontFamily` | Phase 11 |
| Polished Polish copy with plural forms (1 / 2 / 5 / 22) | Phase 11 |
| Brand color final pass (re-seeding `primary`) | Phase 11 |
| In-app theme toggle (override system dark/light) | not in v1 (out of scope per PROJECT.md) |
| Animated transitions between auth states | Phase 10 |
| Logo / wordmark image asset | not in v1 — text wordmark only until Phase 11 brand pass |

---

## Checker Sign-Off

- [ ] Dimension 1 Copywriting: PASS — 6 string keys declared with Polish scaffold copy + Compose Resources delivery contract; inline-error UX rules locked; logout silent UX justified
- [ ] Dimension 2 Visuals: PASS — 3 composables specified with file paths, layout column structure, and no-Scaffold-in-Phase-2 boundary
- [ ] Dimension 3 Color: PASS — Material 3 `lightColorScheme()` / `darkColorScheme()` seeded with `#3B6939`; 60/30/10 mapped to `surface` / `surfaceContainer` / `primary`; accent reserved for the single LoginScreen CTA; dark mode required
- [ ] Dimension 4 Typography: PASS — exactly 4 sizes (14/16/24/36), exactly 2 weights (W400/W500), Material 3 role-to-element mapping locked
- [ ] Dimension 5 Spacing: PASS — full xs..3xl scale declared, all multiples of 4, Phase 2 uses sm/md/lg/2xl, others reserved
- [ ] Dimension 6 Component Sourcing: PASS — Material 3 stdlib only, no third-party UI, no Haze in Phase 2 (gated to Phase 10), no registry safety gate needed

**Approval:** pending

---

## UI-SPEC COMPLETE

**Phase:** 2 — Authentication Foundation
**Design System:** Compose Multiplatform Material 3 (no shadcn — KMP project)

### Contract Summary
- **Spacing:** 8-point scale `xs..3xl` (4 / 8 / 16 / 24 / 32 / 48 / 64 dp); Phase 2 actively uses sm / md / lg / 2xl
- **Typography:** 4 sizes (14, 16, 24, 36 sp), 2 weights (W400, W500); Material 3 roles `displaySmall` / `headlineSmall` / `bodyLarge` / `labelLarge`
- **Color:** Material 3 `light` + `dark` schemes seeded with `#3B6939`; 60% `surface` / 30% `surfaceContainer` / 10% `primary`; accent reserved for single LoginScreen CTA; logout uses `OutlinedButton` (not destructive `error`)
- **Copywriting:** 6 Compose Resources keys + Polish scaffold copy locked (`auth_app_name`, `auth_sign_in_button`, `auth_sign_out_button`, `auth_welcome_format`, `auth_error_cancelled`, `auth_error_network`, `auth_error_unknown`); inline-error UX + silent-logout UX defined
- **Component Sourcing:** Material 3 stdlib only — no Haze, no third-party UI registries (Phase 2 has no registry-safety gate to clear)

### File Created
`.planning/phases/02-authentication-foundation/02-UI-SPEC.md`

### Pre-Populated From
| Source | Decisions Used |
|--------|----------------|
| `02-CONTEXT.md` (D-30..D-34) | 5 (auth gate routing, login minimal, login error states, post-login placeholder, Compose Resources) |
| `02-CONTEXT.md` (Claude's Discretion) | 1 resolved here (splash visual = wordmark + circular progress indicator) |
| `PROJECT.md` (locked stack) | 4 (Material 3, system font, Polish-only v1, Liquid-Glass deferred to polish phase) |
| `CLAUDE.md` (non-negotiables) | 2 (#9 strings externalized day 1, #10 Haze on chrome only — gates Phase 2 to no-blur) |
| `ROADMAP.md` (phase boundaries) | 2 (Phase 10 owns UI chrome / Haze, Phase 11 owns localization + final polish) |
| `REQUIREMENTS.md` (AUTH-01..AUTH-06) | 1 (AUTH-05 logout returns to login screen) |
| `ARCHITECTURE.md` (component responsibilities) | 1 (`AuthSession` Koin singleton owning `StateFlow<AuthState>`) |
| `App.kt` (Phase 1 scaffold) | 1 (existing `MaterialTheme { ... }` + `safeContentPadding()` pattern preserved) |

### Awaiting / Notes for Downstream
- **Planner (`gsd-planner`):** the Component Inventory + Layout Contract sections give you concrete file paths and composable shapes; tokens in Spacing / Typography / Color sections are referenced via Material 3 theme accessors (`MaterialTheme.colorScheme.primary`, `MaterialTheme.typography.displaySmall`, etc.). The seed color `#3B6939` is the only manual override needed in `RecipeTheme.kt`.
- **Executor (`gsd-executor`):** replace `App.kt` body with the auth-gate `when`-block; do NOT keep the JetBrains template's button-and-greeting code. Wire `Res.string.*` keys via Compose Resources (`composeApp/src/commonMain/composeResources/values/strings.xml`).
- **Phase 10 / 11 hand-off seam:** every "Reserved for Phase 10/11" annotation in this doc is an explicit hand-off point; do not retroactively rewrite Phase 2's seed tokens during those phases unless the tradeoff is documented.

### Ready for Verification
UI-SPEC complete. Checker can now validate against the 6 design quality dimensions.
