--- phase: 02.1 plan: 02 type: execute wave: 1 depends_on: ["02.1-01"] files_modified: - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeColors.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTypography.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeSpacing.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeShapes.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeGlass.kt autonomous: true requirements: [UI-04, UI-09] tags: [kotlin, compose-multiplatform, theme, design-tokens, composition-local] must_haves: truths: - "RecipeTheme exposes colors / typography / spacing / shapes / glass via @ReadOnlyComposable getters backed by CompositionLocals" - "Light + dark color schemes follow system setting (D-15)" - "MaterialTheme(...) wrapper preserved so legacy auth screens (LoginScreen, PostLoginPlaceholderScreen, SplashScreen) keep resolving MaterialTheme.colorScheme.* / MaterialTheme.typography.*" - "Spacing scale is xs/sm/lg/xl/2xl/3xl with values 4/8/16/24/32/48 dp (UI-SPEC § Spacing revision 1)" - "Typography scale has display/title/body/label roles with locked sizes/weights (UI-SPEC § Typography)" - "Color hex values are exactly those locked in UI-SPEC § Color" artifacts: - path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeColors.kt" provides: "Semantic color data class + Light/Dark instances" contains: "data class RecipeColors" - path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTypography.kt" provides: "Typography token data class + default instance" contains: "data class RecipeTypography" - path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeSpacing.kt" provides: "Spacing tokens" contains: "data class RecipeSpacing" - path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeShapes.kt" provides: "Shape tokens (pill / circle radii)" - path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeGlass.kt" provides: "GlassSurface default token bundle" - path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt" provides: "RecipeTheme composable + RecipeTheme object accessors" contains: "object RecipeTheme" key_links: - from: "ui/theme/RecipeTheme.kt" to: "ui/theme/RecipeColors.kt + ui/theme/RecipeTypography.kt + ui/theme/RecipeSpacing.kt + ui/theme/RecipeShapes.kt + ui/theme/RecipeGlass.kt" via: "CompositionLocalProvider provides for each token bundle" pattern: "CompositionLocalProvider" --- Establish the full Recipe design-token scaffold per CONTEXT D-14 / D-15 and UI-SPEC § Color / Typography / Spacing / Glass. Produce five token data classes with locked values plus a single `RecipeTheme` composable that wraps `MaterialTheme(...)` (so legacy auth screens keep working — RESEARCH § Open Question 3) AND provides `LocalRecipeColors`, `LocalRecipeTypography`, `LocalRecipeSpacing`, `LocalRecipeShapes`, `LocalRecipeGlass` to descendants. New code reads `RecipeTheme.colors.*` etc; legacy auth code keeps reading `MaterialTheme.*`. Purpose: Every later plan in this phase (and every later phase) reads from these tokens. Get the API and values right now. Output: Six files in `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/`. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-CONTEXT.md @.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-RESEARCH.md @.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-UI-SPEC.md @.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-PATTERNS.md @composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt Current `RecipeTheme.kt` (analog — to be rewritten while preserving the MaterialTheme wrapper): ```kotlin private val LightColors = lightColorScheme(primary = Color(0xFF3B6939)) private val DarkColors = darkColorScheme(primary = Color(0xFFA2D597)) @Composable fun RecipeTheme(content: @Composable () -> Unit) { val colors = if (isSystemInDarkTheme()) DarkColors else LightColors MaterialTheme(colorScheme = colors, content = content) } ``` Legacy consumers (must keep working — DO NOT break): - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginScreen.kt — reads `MaterialTheme.colorScheme.surface`, `MaterialTheme.typography.displaySmall`, etc. - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginPlaceholderScreen.kt — reads `MaterialTheme.colorScheme.surface`, `MaterialTheme.typography.headlineSmall`. - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/SplashScreen.kt — likely reads MaterialTheme. UI-SPEC § Color (verbatim hex values): - background light=#F7F5F1, dark=#0F1113 - surface light=#FFFFFF, dark=#1A1D21 - surfaceGlass light=#FFFFFF @ 60% alpha, dark=#1A1D21 @ 55% alpha - content light=#0F1113, dark=#F1EFEA - contentMuted light=#6B6E73, dark=#9AA0A6 - accent light=#D97757, dark=#E48A6E - separator light=#E5E1DA, dark=#2A2D31 - borderCard light=#E5E1DA @ 60% alpha, dark=#FFFFFF @ 8% alpha - destructive light=#C0392B, dark=#E57368 UI-SPEC § Typography: - display: 28sp, FontWeight.SemiBold (W600), lineHeight 34sp, letterSpacing -0.2sp - title: 20sp, FontWeight.SemiBold, lineHeight 24sp, letterSpacing 0sp - body: 16sp, FontWeight.Normal (W400), lineHeight 24sp, letterSpacing 0sp - label: 13sp, FontWeight.SemiBold, lineHeight 16sp, letterSpacing 0.1sp UI-SPEC § Spacing (rev 1): - xs=4dp, sm=8dp, lg=16dp, xl=24dp, 2xl=32dp, 3xl=48dp UI-SPEC § Glass (defaults consumed by GlassSurface): - Dock pill corner radius: 28dp (height 56dp), collapsed 22dp (height 44dp) - Search pill / floating button: 22dp (height 44dp) - Border: 1dp borderCard - Shadow (light): y=8dp, blur=24dp, alpha=12%; (dark): no shadow - Blur radius (Liquid+Haze): 24dp initial Task 1: Create token data classes (Colors, Typography, Spacing, Shapes, Glass) composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeColors.kt, composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTypography.kt, composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeSpacing.kt, composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeShapes.kt, composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeGlass.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt (current — note the file already imports `androidx.compose.material3.*` for the MaterialTheme wrapper; that import stays in RecipeTheme.kt only, NOT in the new token files) - .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-UI-SPEC.md § Color (lines 75-115) - .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-UI-SPEC.md § Typography (lines 56-73) - .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-UI-SPEC.md § Spacing (lines 33-54) - .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-UI-SPEC.md § Glass / Layout (lines 230-270) - .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-PATTERNS.md § new files — Theme tokens (lines 31-39) - RecipeColors data class has 9 Color fields and the file declares two top-level vals `LightRecipeColors` / `DarkRecipeColors` matching UI-SPEC hex. - RecipeTypography data class has 4 TextStyle fields (display/title/body/label) with locked sizes/weights/lineHeights. - RecipeSpacing data class has 6 Dp fields named `xs sm lg xl xxl xxxl` (Kotlin identifiers must start with letter; `xxl` represents `2xl`, `xxxl` represents `3xl`). - RecipeShapes has pill/circle Dp constants used by chrome. - RecipeGlass has tint color (sourced from RecipeColors at composition time), corner radius defaults, border stroke, shadow params. - All five files compile against `androidx.compose.ui.graphics.Color`, `androidx.compose.ui.unit.{Dp,dp,sp,TextUnit}`, `androidx.compose.ui.text.TextStyle`, `androidx.compose.ui.text.font.FontWeight`. NONE import `androidx.compose.material3.*`. Create five files. Use `dev.ulfrx.recipe.ui.theme` package. NO Material 3 imports in any of these five (only RecipeTheme.kt, in the next task, retains the MaterialTheme wrapper). File 1 — `RecipeColors.kt`: ```kotlin package dev.ulfrx.recipe.ui.theme import androidx.compose.ui.graphics.Color /** * Semantic color tokens (UI-SPEC § Color, CONTEXT D-14, D-15). * Values are locked; do not introduce raw hex in screen code. */ public data class RecipeColors( val background: Color, val surface: Color, val surfaceGlass: Color, val content: Color, val contentMuted: Color, val accent: Color, val separator: Color, val borderCard: Color, val destructive: Color, ) public val LightRecipeColors: RecipeColors = RecipeColors( background = Color(0xFFF7F5F1), surface = Color(0xFFFFFFFF), surfaceGlass = Color(0xFFFFFFFF).copy(alpha = 0.60f), content = Color(0xFF0F1113), contentMuted = Color(0xFF6B6E73), accent = Color(0xFFD97757), separator = Color(0xFFE5E1DA), borderCard = Color(0xFFE5E1DA).copy(alpha = 0.60f), destructive = Color(0xFFC0392B), ) public val DarkRecipeColors: RecipeColors = RecipeColors( background = Color(0xFF0F1113), surface = Color(0xFF1A1D21), surfaceGlass = Color(0xFF1A1D21).copy(alpha = 0.55f), content = Color(0xFFF1EFEA), contentMuted = Color(0xFF9AA0A6), accent = Color(0xFFE48A6E), separator = Color(0xFF2A2D31), borderCard = Color(0xFFFFFFFF).copy(alpha = 0.08f), destructive = Color(0xFFE57368), ) ``` File 2 — `RecipeTypography.kt`: ```kotlin package dev.ulfrx.recipe.ui.theme import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp /** * Typography tokens (UI-SPEC § Typography). System default font family * (SF Pro on iOS, Roboto on Android) for v1. */ public data class RecipeTypography( val display: TextStyle, val title: TextStyle, val body: TextStyle, val label: TextStyle, ) public val DefaultRecipeTypography: RecipeTypography = RecipeTypography( display = TextStyle( fontFamily = FontFamily.Default, fontSize = 28.sp, fontWeight = FontWeight.SemiBold, lineHeight = 34.sp, letterSpacing = (-0.2).sp, ), title = TextStyle( fontFamily = FontFamily.Default, fontSize = 20.sp, fontWeight = FontWeight.SemiBold, lineHeight = 24.sp, letterSpacing = 0.sp, ), body = TextStyle( fontFamily = FontFamily.Default, fontSize = 16.sp, fontWeight = FontWeight.Normal, lineHeight = 24.sp, letterSpacing = 0.sp, ), label = TextStyle( fontFamily = FontFamily.Default, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, lineHeight = 16.sp, letterSpacing = 0.1.sp, ), ) ``` File 3 — `RecipeSpacing.kt`: ```kotlin package dev.ulfrx.recipe.ui.theme import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp /** * Spacing scale (UI-SPEC § Spacing rev 1: 4 / 8 / 16 / 24 / 32 / 48). * `xxl` and `xxxl` map to UI-SPEC's `2xl` / `3xl` because Kotlin identifiers * cannot start with a digit. Tokens are referenced by these property names * in screen code; UI-SPEC token names (`2xl`/`3xl`) are the documented contract. */ public data class RecipeSpacing( val xs: Dp, val sm: Dp, val lg: Dp, val xl: Dp, val xxl: Dp, val xxxl: Dp, ) public val DefaultRecipeSpacing: RecipeSpacing = RecipeSpacing( xs = 4.dp, sm = 8.dp, lg = 16.dp, xl = 24.dp, xxl = 32.dp, xxxl = 48.dp, ) ``` File 4 — `RecipeShapes.kt`: ```kotlin package dev.ulfrx.recipe.ui.theme import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp /** * Shape tokens (UI-SPEC § Glass — corner radii for chrome elements). */ public data class RecipeShapes( val dockExpanded: Dp, val dockCollapsed: Dp, val searchPill: Dp, val floatingButton: Dp, ) public val DefaultRecipeShapes: RecipeShapes = RecipeShapes( dockExpanded = 28.dp, dockCollapsed = 22.dp, searchPill = 22.dp, floatingButton = 22.dp, ) ``` File 5 — `RecipeGlass.kt`: ```kotlin package dev.ulfrx.recipe.ui.theme import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp /** * Glass surface defaults (UI-SPEC § Glass / Layout). * Consumed by GlassSurface (plan 02.1-03) and the dock / search pill / * floating button (plan 02.1-05). */ public data class RecipeGlass( val borderWidth: Dp, val shadowOffsetY: Dp, val shadowBlur: Dp, val shadowAlphaLight: Float, val shadowAlphaDark: Float, val blurRadius: Dp, ) public val DefaultRecipeGlass: RecipeGlass = RecipeGlass( borderWidth = 1.dp, shadowOffsetY = 8.dp, shadowBlur = 24.dp, shadowAlphaLight = 0.12f, shadowAlphaDark = 0.0f, blurRadius = 24.dp, ) ``` ./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q - All 5 files exist under `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/` - `grep -c 'data class RecipeColors' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeColors.kt` returns 1 - `grep -c '0xFFF7F5F1' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeColors.kt` returns 1 (exact light background hex) - `grep -c '0xFF0F1113' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeColors.kt` returns at least 2 (dark background + light content) - `grep -c '0xFFD97757' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeColors.kt` returns 1 (light accent) - `grep -c '28.sp' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTypography.kt` returns 1 (display fontSize) - `grep -c '13.sp' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTypography.kt` returns 1 (label fontSize) - `grep -c 'xxl = 32.dp' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeSpacing.kt` returns 1 - `grep -c 'xxxl = 48.dp' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeSpacing.kt` returns 1 - No file imports material3: `grep -rn 'androidx.compose.material3' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeColors.kt composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTypography.kt composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeSpacing.kt composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeShapes.kt composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeGlass.kt` returns no matches - `./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q` exits 0 Five token files compile cleanly under iOS source set; values match UI-SPEC verbatim; no Material 3 imports leaked into the new token layer. Task 2: Rewrite RecipeTheme.kt — CompositionLocals + system-following light/dark + MaterialTheme wrapper preserved composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt (current shape — `LightColors`/`DarkColors` Material 3 schemes + `MaterialTheme(...)` wrapper) - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginScreen.kt (consumer — `MaterialTheme.colorScheme.surface`, `MaterialTheme.typography.displaySmall`. Both must keep resolving.) - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginPlaceholderScreen.kt (consumer — `MaterialTheme.typography.headlineSmall`) - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt (caller of `RecipeTheme { ... }` at the root) - .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-RESEARCH.md § Open Question 3 (lines 686-690 — locks the dual-theme decision) - .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-PATTERNS.md § RecipeTheme.kt (rewritten) lines 126-148 - `RecipeTheme(content)` composable selects light/dark via `isSystemInDarkTheme()` (D-15). - Wraps content in `MaterialTheme(colorScheme = ..., content = { CompositionLocalProvider(...) { content() } })` — auth screens read MaterialTheme, new screens read RecipeTheme, both compose simultaneously. - Five `CompositionLocal` sentinels declared: `LocalRecipeColors`, `LocalRecipeTypography`, `LocalRecipeSpacing`, `LocalRecipeShapes`, `LocalRecipeGlass`. All use `staticCompositionLocalOf` (read-only invariants). Defaults throw with a helpful message when accessed outside `RecipeTheme { ... }`. - `object RecipeTheme` exposes 5 properties (`colors`, `typography`, `spacing`, `shapes`, `glass`) as `@Composable @ReadOnlyComposable get()` accessors mirroring `MaterialTheme` idiom. Replace `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` with: ```kotlin package dev.ulfrx.recipe.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.staticCompositionLocalOf /** * Recipe theme entry point (CONTEXT D-14, D-15). * * Wraps a Material 3 [MaterialTheme] so the legacy auth screens * (LoginScreen / PostLoginPlaceholderScreen / SplashScreen) continue to * resolve `MaterialTheme.colorScheme.*` / `MaterialTheme.typography.*` * (RESEARCH § Open Question 3). New code reads `RecipeTheme.colors.*`, * `RecipeTheme.typography.*`, etc. */ private val LegacyMaterialLightColors = lightColorScheme(primary = LightRecipeColors.accent) private val LegacyMaterialDarkColors = darkColorScheme(primary = DarkRecipeColors.accent) public val LocalRecipeColors: androidx.compose.runtime.ProvidableCompositionLocal = staticCompositionLocalOf { error("RecipeColors accessed outside RecipeTheme { }") } public val LocalRecipeTypography: androidx.compose.runtime.ProvidableCompositionLocal = staticCompositionLocalOf { error("RecipeTypography accessed outside RecipeTheme { }") } public val LocalRecipeSpacing: androidx.compose.runtime.ProvidableCompositionLocal = staticCompositionLocalOf { error("RecipeSpacing accessed outside RecipeTheme { }") } public val LocalRecipeShapes: androidx.compose.runtime.ProvidableCompositionLocal = staticCompositionLocalOf { error("RecipeShapes accessed outside RecipeTheme { }") } public val LocalRecipeGlass: androidx.compose.runtime.ProvidableCompositionLocal = staticCompositionLocalOf { error("RecipeGlass accessed outside RecipeTheme { }") } @Composable public fun RecipeTheme(content: @Composable () -> Unit) { val dark = isSystemInDarkTheme() val recipeColors = if (dark) DarkRecipeColors else LightRecipeColors val materialColors = if (dark) LegacyMaterialDarkColors else LegacyMaterialLightColors MaterialTheme(colorScheme = materialColors) { CompositionLocalProvider( LocalRecipeColors provides recipeColors, LocalRecipeTypography provides DefaultRecipeTypography, LocalRecipeSpacing provides DefaultRecipeSpacing, LocalRecipeShapes provides DefaultRecipeShapes, LocalRecipeGlass provides DefaultRecipeGlass, content = content, ) } } public object RecipeTheme { public val colors: RecipeColors @Composable @ReadOnlyComposable get() = LocalRecipeColors.current public val typography: RecipeTypography @Composable @ReadOnlyComposable get() = LocalRecipeTypography.current public val spacing: RecipeSpacing @Composable @ReadOnlyComposable get() = LocalRecipeSpacing.current public val shapes: RecipeShapes @Composable @ReadOnlyComposable get() = LocalRecipeShapes.current public val glass: RecipeGlass @Composable @ReadOnlyComposable get() = LocalRecipeGlass.current } ``` Notes: - The `RecipeTheme` composable function and the `object RecipeTheme` coexist in Kotlin (function vs declaration in same package). - `MaterialTheme(colorScheme = materialColors)` keeps the auth-screen path working using a thin wrapper of Recipe's accent — the auth screens never relied on a specific Material primary; they only used `surface` (which `lightColorScheme(primary = ...)` provides via Material defaults) and typography defaults. - DO NOT remove the existing import `androidx.compose.foundation.isSystemInDarkTheme` style; replicate the file structure shown above verbatim. ./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q - `grep -c 'staticCompositionLocalOf' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` returns 5 - `grep -c 'CompositionLocalProvider' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` returns 1 - `grep -c 'MaterialTheme(colorScheme' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` returns 1 - `grep -c 'public object RecipeTheme' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` returns 1 - `grep -c '@ReadOnlyComposable' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` returns 5 - `grep -c 'isSystemInDarkTheme' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` returns at least 1 - Legacy auth screens still compile (regression check): `./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q` exits 0 (will fail if MaterialTheme wrapper accidentally removed) - `./gradlew :composeApp:check -q` does not introduce new test failures RecipeTheme.kt exposes five CompositionLocals + a `RecipeTheme` object with `@Composable @ReadOnlyComposable` accessors, all under a preserved `MaterialTheme(...)` wrapper so legacy auth screens keep resolving Material symbols. Whole composeApp still compiles for iosSimulatorArm64. - `./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q` exits 0 - `./gradlew :composeApp:commonTest -q` exits 0 (no regression in existing Phase 2 tests) - All 6 theme files exist; no Material 3 imports leak into the 5 token files - Legacy auth screens unchanged on disk (verified by `git diff --name-only composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/` shows nothing in this plan's diff) 1. Token data classes exist with values exactly matching UI-SPEC. 2. `RecipeTheme { ... }` provides all five CompositionLocals AND wraps `MaterialTheme(...)` (Phase 2 auth screens unaffected). 3. New code can read `RecipeTheme.colors.background`, `RecipeTheme.typography.title`, `RecipeTheme.spacing.lg`, etc., from any `@Composable` descendant. 4. composeApp builds cleanly for iOS simulator and Phase 2 test suite stays green. Create `.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-02-SUMMARY.md` per template. Note any deviations from the locked color/typography/spacing values (there should be none) and the exact identifier mapping for `2xl`/`3xl` → `xxl`/`xxxl`.