--- 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` 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`) | | `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.