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

<objective>
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/`.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.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

<interfaces>
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
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Create token data classes (Colors, Typography, Spacing, Shapes, Glass)</name>
  <files>
    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
  </files>
  <read_first>
    - 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)
  </read_first>
  <behavior>
    - 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.*`.
  </behavior>
  <action>
    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,
    )
    ```
  </action>
  <verify>
    <automated>./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q</automated>
  </verify>
  <acceptance_criteria>
    - 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
  </acceptance_criteria>
  <done>Five token files compile cleanly under iOS source set; values match UI-SPEC verbatim; no Material 3 imports leaked into the new token layer.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Rewrite RecipeTheme.kt — CompositionLocals + system-following light/dark + MaterialTheme wrapper preserved</name>
  <files>composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt</files>
  <read_first>
    - 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
  </read_first>
  <behavior>
    - `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.
  </behavior>
  <action>
    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<RecipeColors> =
        staticCompositionLocalOf { error("RecipeColors accessed outside RecipeTheme { }") }

    public val LocalRecipeTypography: androidx.compose.runtime.ProvidableCompositionLocal<RecipeTypography> =
        staticCompositionLocalOf { error("RecipeTypography accessed outside RecipeTheme { }") }

    public val LocalRecipeSpacing: androidx.compose.runtime.ProvidableCompositionLocal<RecipeSpacing> =
        staticCompositionLocalOf { error("RecipeSpacing accessed outside RecipeTheme { }") }

    public val LocalRecipeShapes: androidx.compose.runtime.ProvidableCompositionLocal<RecipeShapes> =
        staticCompositionLocalOf { error("RecipeShapes accessed outside RecipeTheme { }") }

    public val LocalRecipeGlass: androidx.compose.runtime.ProvidableCompositionLocal<RecipeGlass> =
        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.
  </action>
  <verify>
    <automated>./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q</automated>
  </verify>
  <acceptance_criteria>
    - `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
  </acceptance_criteria>
  <done>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.</done>
</task>

</tasks>

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

<success_criteria>
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.
</success_criteria>

<output>
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`.
</output>
