> Idea: An iOS focus timer where each completed session grows a plant in a little zen garden; streaks unlock rare plants.

Build a native iOS app called **Focus Garden** — a Pomodoro-style focus timer where every completed focus session plants and grows a living plant in a serene, hand-illustrated zen garden. The garden is the entire reward loop: finish a session, a seed germinates and grows over real days; abandon a session and nothing is planted; keep a daily streak and you unlock rare, harder-to-grow species. This is a single-player, offline-first, calm-productivity app. Build it end to end as a shippable App Store target. Make every reasonable assumption below; do not leave placeholders or "TODO" stubs.

## Goal
Ship a polished, offline-first iOS focus-timer app whose core loop is: the user starts a focus session of a chosen length, stays focused (app foregrounded, optional Live Activity running), and on completion earns a plant that is sown into a persistent zen garden and grows visually over subsequent real-world days as the user returns. The emotional target is *calm momentum* — soft, generous, never punishing, but with a quiet collection/streak hook that makes a daily focus habit feel rewarding. Concretely, the finished app must let a user:

- Run a configurable focus timer (default 25 min) with optional break timer (default 5 min), pause/resume, and an honest "abandon" path that grows nothing.
- Earn exactly one plant per completed focus session, chosen from a species the user has unlocked, sown into the next open garden plot.
- Watch each plant advance through growth stages (seed → sprout → seedling → mature → flowering) across multiple real days, with stage progression gated by elapsed real time AND by the user continuing to complete sessions.
- Maintain a **daily streak** (at least one completed focus session per calendar day) that unlocks progressively rarer plant species and garden themes.
- Browse a **Garden** of all grown plants, tap any plant to see its species, the date/session that planted it, and total focus minutes it represents.
- Browse a **Collection/Almanac** of all species (locked + unlocked) with unlock conditions.
- See lightweight **Stats** (today's focus minutes, current streak, longest streak, lifetime sessions, lifetime minutes, plants grown).
- Get local notifications when a session ends and an optional daily reminder.
- Run entirely offline with all data persisted locally; no account, no network dependency.

Success is measured by: the loop is genuinely satisfying, the timer is *accurate and trustworthy* (survives backgrounding, lock, and app kill), the garden state is never corrupted or lost, and the whole thing feels like a premium, HIG-native iOS app rather than a web port.

## Tech stack
- **Language:** Swift 5.9+ (use Swift 6 language mode if it compiles cleanly; otherwise 5.9 with strict concurrency warnings addressed).
- **UI:** SwiftUI only. No UIKit except where unavoidable (e.g. a `UIViewRepresentable` is *not* needed here — render the garden in SwiftUI Canvas / shapes / SF Symbols + custom vector paths). Target **iOS 17+**.
- **Architecture:** MVVM. Views are declarative and dumb; `@Observable` view models own state and business logic; a small set of services (timer engine, growth engine, unlock engine, notification scheduler) are injected.
- **Persistence:** **SwiftData** (`@Model`) as the single source of truth for plants, garden, species unlocks, sessions, and streak/aggregate stats. Use a `ModelContainer` configured in the App entry and injected via `.modelContainer`. Lightweight key/value prefs (selected timer durations, sound on/off, theme) go in `@AppStorage` / `UserDefaults`.
- **Concurrency:** Swift Concurrency (`async`/`await`, `Task`, `@MainActor` view models). The timer must be driven by **wall-clock timestamps**, not by a ticking counter, so it stays correct across backgrounding.
- **System frameworks:** `UserNotifications` (session-complete + daily reminder), `ActivityKit` (Dynamic Island / Lock Screen Live Activity for the running timer), `WidgetKit` (Home Screen widget showing current streak + last plant + a "Start focusing" deep link), `StoreKit 2` is **out of scope** (app is free, no IAP), `AVFoundation` only for short completion chimes, `CoreHaptics`/`UIFeedbackGenerator` for haptics, `os` (`Logger`) for logging.
- **Animation:** SwiftUI native animations, `PhaseAnimator`/`KeyframeAnimator` for plant growth and the timer ring, `Canvas` + `TimelineView` for the breathing garden ambiance. No third-party animation libs.
- **Dependencies:** Zero third-party Swift packages. Everything must be buildable from a clean checkout with only Apple frameworks. This keeps the one-shot reproducible.
- **Project:** A standard Xcode project (`FocusGarden.xcodeproj`) with the app target, a Widget Extension target, and a unit-test target. Use a shared App Group container so the widget and Live Activity can read the latest garden/streak snapshot.

## App architecture & screens
Single-window app. Root is a `TabView` with three tabs: **Focus**, **Garden**, **Almanac**, plus a modal **Settings** sheet reachable from a toolbar gear. Use a top-level `AppRouter`/`@Observable` coordinator only if needed for the start-from-widget deep link; otherwise tab selection state lives on the root view.

**App entry (`FocusGardenApp.swift`)**
- Configures `ModelContainer` for all `@Model` types, seeds the default species + empty garden on first launch (idempotent seeding guarded by a "didSeed" flag), injects shared services as environment objects/values, requests notification authorization lazily (on first session start, not at launch), and handles the `focusgarden://start` deep link from the widget.

**Tab 1 — Focus (the timer, home screen)**
- The hero screen. Centered circular **progress ring** counting down, large monospaced time label in the middle, the currently-selected duration, and the species that *will* be planted on completion (a small preview sprite + name).
- Primary button cycles state: **Start → Pause/Resume → (running) → Give up**. A separate "Skip to break" affordance appears only when a focus session completes.
- A horizontal duration selector (chips: 15 / 25 / 45 / 60 min, plus a "Custom" stepper sheet from 5–120 min) — disabled/locked visually while a session is running.
- Live ambient garden strip along the bottom showing the 3 most recent plants gently swaying.
- States: **idle** (ring full, "Start focusing"), **running** (ring depleting, time ticking, Live Activity active), **paused** (ring frozen, dimmed, "Resume"/"Give up"), **completed** (celebratory: ring fills gold, the new plant animates being sown, haptic + chime, confetti-free tasteful particle puff), **break-running**, **break-completed**.
- Empty/first-run: a soft coachmark "Finish your first session to plant something."

**Tab 2 — Garden**
- A scrollable, illustrated plot grid (isometric-ish but kept simple/flat-friendly: a grid of soft soil tiles). Each occupied tile renders its plant at its current growth stage with a gentle idle animation (sway, occasional leaf shimmer). Empty tiles show tilled soil.
- The garden auto-expands: it starts as a 3×3 plot; reaching plant-count thresholds (9, 18, 30, 45…) unlocks a new plot "bed" the user can horizontally page between, each with its own subtle theme (Meadow, Riverstone, Moss Temple, Desert Bloom, Night Garden — themes themselves are streak-unlocked).
- Tapping a plant opens a **Plant Detail** sheet: large rendering, species name + rarity, growth stage with a progress bar to next stage, "planted on {date}", "from a {duration}-min session", total minutes, and an optional one-line auto-generated "memory" tag the user can edit (e.g. a note about that session).
- Long-press a plant for a context menu: **Rename note**, **Move plant** (drag to an empty tile — reorder), **Compost** (remove; requires a confirm; explicitly tell the user composting frees the tile but does NOT refund the streak/stats).
- States: **empty garden** (illustrated empty bed + "Your garden is waiting"), **populated**, **scrolled to a locked future bed** (shows the unlock requirement).

**Tab 3 — Almanac (species collection)**
- A grid of all species cards. Unlocked species show full art + name + rarity + how many you've grown. Locked species show a silhouette + the unlock condition ("Reach a 7-day streak", "Grow 10 plants", "Complete a 60-min session").
- Sort/filter: by rarity, by unlocked/locked, by times grown.
- Tap a species → **Species Detail**: lore blurb, rarity, growth-stage artwork carousel, base grow-time, unlock condition, and your personal stats for it.
- Header row of **Stats**: today's minutes, current streak (with a flame), longest streak, lifetime sessions, lifetime minutes, total plants.

**Settings (modal sheet)**
- Default focus duration, default break duration, auto-start break (toggle), auto-start next focus after break (toggle).
- Sound: completion chime on/off + a picker of 3 chimes (Bell, Bowl, Chime) with preview.
- Haptics on/off.
- Daily reminder: on/off + time picker.
- Keep screen awake during sessions (toggle, default off).
- Garden theme picker (only unlocked themes selectable).
- Appearance: System / Light / Dark.
- Data: "Reset garden" (double-confirm, destructive) and "Export garden as image" (renders the current garden bed to a shareable PNG via `ImageRenderer`).
- About: version, a short credits/lore line.

**Navigation rules:** Tabs persist scroll position. Sheets use `.presentationDetents` where a partial sheet reads better (Plant Detail = `.medium`/`.large`; Custom duration = `.height`). Honor Dynamic Type and VoiceOver throughout.

## Data layer
Use SwiftData `@Model` classes. Define them precisely:

**`Species`** (catalog; seeded, effectively read-only at runtime)
- `id: UUID` (stable; seed with fixed UUIDs so unlocks survive reinstall logic if needed)
- `key: String` (unique slug, e.g. `"fern"`, `"lotus"`, `"ghost_orchid"`)
- `displayName: String`
- `rarity: Rarity` (enum: `common`, `uncommon`, `rare`, `legendary` — stored as String raw value)
- `loreBlurb: String`
- `baseGrowDays: Double` (real days from sown → flowering at the slowest gate; commons ~2, legendaries ~7)
- `unlockRule: UnlockRule` (Codable value stored as transformable/JSON: see below)
- `paletteKey: String` (drives the vector art tint set)
- `artStageCount: Int` (always 5)
- Relationship: `plants: [Plant]` inverse of `Plant.species`.

**`Plant`** (an instance growing in the garden)
- `id: UUID`
- `species: Species` (to-one relationship, non-optional)
- `plantedAt: Date`
- `sessionDurationMinutes: Int`
- `originSessionID: UUID` (links to the `FocusSession` that produced it)
- `plotIndex: Int` (which bed)
- `tileIndex: Int` (slot within the bed; (plotIndex, tileIndex) unique)
- `noteText: String?` (user-editable memory)
- `growthStageCached: Int` (0…4, recomputed by the growth engine; cached for cheap rendering, but the *truth* is derived from `plantedAt`, species `baseGrowDays`, and the count of qualifying sessions since planting)
- `isComposted: Bool` (soft-delete; composted plants free the tile but remain in lifetime stats)
- Validation: `sessionDurationMinutes` in 5…120; `tileIndex`/`plotIndex` non-negative; on insert, must occupy a free tile (engine assigns, never the view).

**`FocusSession`** (an audit log of every started session, completed or not)
- `id: UUID`
- `startedAt: Date`
- `plannedDurationMinutes: Int`
- `endedAt: Date?`
- `outcome: SessionOutcome` (enum: `completed`, `abandoned`, `interrupted` — stored as String). `interrupted` = app/system killed it; `abandoned` = user pressed Give up.
- `actualFocusedSeconds: Int` (accumulated foregrounded focus time, for honesty in stats)
- `producedPlantID: UUID?` (set only when `completed`)
- Validation: `endedAt >= startedAt` when present; `completed` requires `producedPlantID`.

**`StreakState`** (singleton row; enforce single instance in the engine)
- `id: UUID`
- `currentStreak: Int`
- `longestStreak: Int`
- `lastQualifyingDay: Date?` (start-of-day in the user's current calendar/timezone for the last day a session completed)
- Derived rule: a day "qualifies" if ≥1 `FocusSession.completed` exists for that calendar day. Streak increments when a qualifying day is the calendar day immediately after `lastQualifyingDay`; resets to 1 if there's a gap of ≥2 days; unchanged if same day repeats. Recompute defensively on app foreground (a missed day while the app was closed must drop the streak).

**`Unlock`** (which species/themes the user has unlocked)
- `id: UUID`
- `targetKey: String` (species key or theme key)
- `targetType: UnlockTargetType` (`species` | `theme`)
- `unlockedAt: Date`
- Unique on (`targetKey`, `targetType`).

**`AppStats`** (singleton aggregate, denormalized for cheap reads)
- `lifetimeSessions: Int`, `lifetimeFocusedMinutes: Int`, `plantsGrown: Int`, `lastUpdated: Date`. Recomputed transactionally whenever a session completes.

**Value types (not `@Model`)**
- `enum Rarity: String, Codable, CaseIterable` with `weight`/`color` helpers.
- `enum SessionOutcome: String, Codable`.
- `enum UnlockTargetType: String, Codable`.
- `struct UnlockRule: Codable` with a `kind` discriminator: `.streakReaches(days: Int)`, `.plantsGrown(count: Int)`, `.singleSessionMinutes(min: Int)`, `.alwaysUnlocked`. The unlock engine evaluates these after every completed session.

**Seeding:** On first launch, seed ~14 species across rarities with fixed UUIDs and `UnlockRule`s, e.g. *Clover/Fern/Daisy* (alwaysUnlocked, common), *Lavender* (plantsGrown 5), *Sunflower* (singleSessionMinutes 45), *Lotus* (streakReaches 7, rare), *Maple Bonsai* (plantsGrown 25, rare), *Ghost Orchid* (streakReaches 30, legendary), *Moonflower* (Night theme, legendary), etc. Also seed the 3×3 starting garden as empty tiles and the 5 themes (only "Meadow" unlocked). Seeding is idempotent.

**App Group / widget snapshot:** After any state change, write a tiny `Codable` `GardenSnapshot` (current streak, last plant species key + stage, today's minutes, total plants) to a JSON file in the shared App Group container so the WidgetKit timeline and Live Activity can render without touching SwiftData.

## Behavior & features
**Timer engine (`FocusTimerEngine`, `@Observable`, `@MainActor`):**
- Driven by absolute timestamps. On start, store `startDate` and `plannedDuration`; the displayed remaining time is `plannedDuration - (now - startDate - pausedAccumulated)`. A `Timer`/`TimelineView` only drives the *UI refresh*, never the source of truth.
- **Pause:** record `pauseStartedAt`; on resume, add the elapsed to `pausedAccumulated`. Cap total pause time at, say, 15 min before auto-prompting "Still there?".
- **Backgrounding:** when the app backgrounds mid-session, schedule a local notification for the exact completion time, keep the Live Activity running, and on return recompute remaining from wall clock. If the completion moment passed while backgrounded, transition straight to the **completed** state and plant retroactively (using the real completion timestamp). Do NOT rely on background execution to "tick".
- **App kill mid-session:** persist the in-flight session (`startedAt`, planned duration) to a small restorable record. On next launch, if a session was in flight and its computed end time has passed AND the user had not abandoned it, offer a one-tap "Claim your plant from the session you finished while away?" *only if* it genuinely elapsed; otherwise mark it `interrupted` and grow nothing. Be conservative and honest — never grant a plant for a session that didn't actually elapse.
- **Completion:** mark `FocusSession.completed`, run the growth/plant placement, run the unlock engine, update streak + stats, fire haptic + optional chime + the sow animation, refresh the widget snapshot, and end the Live Activity with a success final state.
- **Give up:** confirm once ("Give up this session? Nothing will be planted."), mark `abandoned`, end Live Activity, grow nothing, do not break the streak (streak only depends on *completed* sessions per day, so giving up is neutral).

**Plant placement & growth engine (`GrowthEngine`):**
- On completion, choose the species to plant: the user pre-selects from unlocked species on the Focus screen (default = a rotating "today's seed" that nudges variety, but user can override). Weight legendaries to be rarer even when unlocked (small random chance to "find a rare seed" on any completed session adds delight — e.g. a 3% chance to upgrade the sown species to a random unlocked rarer one; surface this with a special "A rare seed sprouted!" toast).
- Assign the plant to the first free tile in the current bed; if the bed is full, expand to / create the next bed.
- **Growth stages (0…4):** stage advances are gated by BOTH real elapsed days since `plantedAt` (proportional to species `baseGrowDays`) AND continued engagement (the plant only advances on days the user completes ≥1 session — neglect pauses growth, it never regresses). Concretely: compute `progress = min(realDaysElapsedFactor, engagementFactor)` and map to a stage. Recompute `growthStageCached` for all plants on every foreground and after each completion.
- Flowering (stage 4) is the visual payoff; flowering plants get a subtle particle/glow idle animation.

**Streak engine (`StreakEngine`):** Recompute on foreground and on completion per the `StreakState` rules above. Expose `currentStreak`, `longestStreak`, and "days until next streak unlock" for the UI.

**Unlock engine (`UnlockEngine`):** After every completed session (and on foreground for time-based safety), evaluate every locked `Species`/theme `UnlockRule` against current stats/streak; insert `Unlock` rows for any newly satisfied rule; surface a celebratory unlock sheet ("New species unlocked: Lotus 🌸") at most once per unlock, queued so multiple unlocks present sequentially.

**Notifications (`NotificationScheduler`):** Request authorization on first session start. Schedule a completion notification at session end (title "Session complete 🌱", body "Your {species} is ready to plant."). Optional daily reminder at the user's chosen time ("Your garden misses you — grow something today."). Cancel/replace pending notifications appropriately when sessions are paused/abandoned. Handle the case where authorization is denied (degrade gracefully — in-app completion still works; show a one-time non-nagging banner explaining notifications are off).

**Live Activity (`ActivityKit`):** Start on session start; show remaining time (use ActivityKit's native countdown), the species being grown, and a small ring. Lock Screen + Dynamic Island compact/expanded/minimal presentations. End it on completion/abandon. Deep-link tapping it back into the Focus tab.

**Widget (`WidgetKit`):** A small + medium Home Screen widget reading the App Group snapshot: current streak with flame, the most recent plant's stage art, today's focus minutes, and a "Start focusing" deep link (`focusgarden://start`). Timeline refreshes on snapshot writes and a periodic policy.

**Haptics & sound:** Light haptic on start, success haptic + selected chime on completion, soft tick haptic when the duration chip changes. All respect the Settings toggles and the system silent switch where applicable.

**Stats:** All derived from `AppStats` + `StreakState` + a live query; "today's minutes" sums completed sessions for the current calendar day.

## UX & visual design (HIG-aligned)
**Overall feel:** calm, tactile, "morning garden" warmth. It should feel like a premium native app — generous spacing, soft depth, restrained motion, never gamified-loud. Fully HIG-aligned: standard `TabView`, navigation, sheets, Dynamic Type, Reduce Motion, Increase Contrast, and full VoiceOver support.

**Color palette (semantic, light & dark):**
- Light: background `#F6F4EC` (warm paper), surface `#FFFFFF`, soil `#6B4F3A`, primary accent (sprout green) `#5C8A4A`, secondary (bloom) `#E89B6C`, gold (completion) `#E7B84B`, text primary `#2E2A24`, text secondary `#7A7466`, streak flame `#E0663A`.
- Dark ("Night Garden"): background `#15171A`, surface `#1E2125`, soil `#3A2E24`, accent green `#7FB069`, bloom `#F0A878`, gold `#F0C75E`, text primary `#ECE8DF`, text secondary `#9A9488`.
- Rarity colors: common `#8AA87A`, uncommon `#5C8A4A`, rare `#5C7FB0`, legendary `#B06AC9`. Use as accent rings/labels, never full backgrounds.
- Define all of these as semantic Color assets / a `Theme` struct, never hardcoded inline.

**Typography:** SF Pro (system). Scale: large title 34 for screen titles, title2 22 for section heads, body 17, callout 16, footnote 13. The timer numerals use `.monospacedDigit()` at ~64pt rounded design weight so the countdown doesn't jitter. Plant/species names in `.rounded` design for friendliness. Respect Dynamic Type with `.dynamicTypeSize(...)` clamping only on the giant timer.

**Spacing scale:** 4-pt base grid → tokens 4/8/12/16/24/32/48. Cards use 16 corner radius, 12 inner padding, soft shadow (y 6, blur 18, ~8% opacity in light; subtle border in dark instead of shadow).

**Motion:**
- Timer ring: smooth `.linear` depletion via `TimelineView(.animation)`; on completion it fills to gold with a spring.
- Plant sow: a `KeyframeAnimator` — seed drops, soil puffs (a few particle dots), sprout scales up with a gentle overshoot spring (`.spring(response: 0.5, dampingFraction: 0.7)`).
- Garden idle: `TimelineView` + `Canvas` sine-wave sway, each plant phase-offset by its `tileIndex` so the garden breathes asynchronously. Flowering plants add a slow glow.
- Tab/sheet transitions: system defaults. **Honor Reduce Motion** — when on, replace sway/particles/overshoot with simple cross-fades and static stages.

**Plant rendering:** Render plants procedurally with SwiftUI `Path`/`Shape` + `Canvas` so there are no image assets to ship and every species/stage is data-driven from `paletteKey` + `artStageCount`. Each species is a parameterized vector recipe (stem curve, leaf count/shape, bloom shape/color) varying by `growthStage`. Keep them charming and simple (think flat illustration), not photoreal. Empty tiles render tilled-soil texture via Canvas strokes.

**Iconography:** SF Symbols throughout — `timer`, `leaf.fill`, `flame.fill` (streak), `book.closed` (almanac), `gearshape`, `play.fill`/`pause.fill`, `trash` (compost), `square.and.arrow.up` (export). Tint to the accent palette.

**Accessibility:** Every plant tile has a VoiceOver label ("Lotus, flowering, planted June 3rd, from a 25-minute session"). The timer announces remaining time on demand and posts an accessibility announcement on completion. Color is never the only signal (rarity shows a label + symbol, not just hue). All tap targets ≥ 44×44 pt. Support full keyboard/Voice Control by using standard controls.

## Entitlements & permissions
- **Notifications:** `UserNotifications` — request `.alert, .sound, .badge` authorization lazily on first session start. No special Info.plist key required, but provide a clear in-app rationale screen before the system prompt (soft-ask pattern).
- **Live Activities:** add `NSSupportsLiveActivities = YES` to Info.plist. Implement the `ActivityAttributes`.
- **App Group:** add an App Group entitlement (e.g. `group.com.focusgarden.shared`) shared by the app, widget, and Live Activity for the `GardenSnapshot` file and any shared defaults.
- **Widget Extension:** its own target with the App Group entitlement; declare the supported families (`.systemSmall`, `.systemMedium`).
- **Custom URL scheme:** register `focusgarden://` in Info.plist (`CFBundleURLTypes`) for the widget/Live Activity "Start focusing" deep link.
- **Background:** no background modes entitlement needed — the timer is wall-clock based and uses notifications, not background execution. Do **not** request background-fetch/processing.
- **No** camera, location, contacts, microphone, network, tracking, or HealthKit. The app must function fully in airplane mode. No `NSUserTrackingUsageDescription` (no tracking). Keep the entitlements surface minimal and explain each in code comments.

## Edge cases & error handling
- **Backgrounded through completion:** plant is granted using the real completion timestamp; UI shows the sow animation on return. Never grant if the session was abandoned or didn't elapse.
- **Device clock change / timezone travel:** recompute streak and growth defensively against the *current* calendar; guard against a backwards clock shift granting double days or breaking growth (clamp negative elapsed to 0; if `lastQualifyingDay` is in the future due to a clock change, don't reset the streak — re-anchor safely).
- **Midnight rollover during a session:** the completed session counts for the calendar day it *completed* in; ensure streak logic uses completion time, not start time.
- **Garden full / new bed creation:** auto-create the next bed and page to it; never lose a plant for lack of space.
- **Compost confirmation:** destructive, requires confirm; explicitly state it frees the tile but keeps lifetime stats and does not refund the streak.
- **Reset garden / reset all:** double-confirm, wipe SwiftData rows (keep species catalog + re-seed empties), clear the snapshot, cancel notifications, reset stats — all in one transaction.
- **Notifications denied:** core loop still works; show a single dismissible banner, never re-prompt aggressively, and offer a Settings deep link.
- **Live Activity unsupported (older device / disabled):** detect and silently skip; the timer still works.
- **Rapid start/stop/pause spamming:** debounce state transitions; the engine is the single authority so the UI can't desync.
- **SwiftData write failure / migration:** wrap saves in do/catch, log via `Logger`, surface a non-blocking "Couldn't save — your last plant is safe and will retry" and retry on next event; never crash the user out of their garden. Provide a lightweight migration plan/`VersionedSchema` so future model changes don't wipe data.
- **Concurrency:** all model mutations on the `@MainActor` context; no data races. Use `@Observable` correctly to avoid update storms during the 1 Hz UI refresh.
- **Custom duration bounds:** clamp 5–120 min; reject out-of-range; default to 25 on bad input.
- **Empty states:** Focus (first-run coachmark), Garden (illustrated empty bed), Almanac (everything but commons locked initially with clear conditions), Stats (zeros render cleanly, streak shows 0 with an unlit flame).
- **Export image failure:** if `ImageRenderer` returns nil, show a gentle error toast; don't crash.
- **Performance:** garden with 45+ animated plants must stay smooth — cap concurrently animated tiles to those on-screen, throttle off-screen idle animation, and keep `Canvas` redraws cheap.

## Definition of done
- [ ] App builds cleanly from a fresh checkout with **zero third-party dependencies**, no warnings introduced by strict concurrency, on iOS 17+.
- [ ] Three-tab app (Focus / Garden / Almanac) + Settings sheet, all implemented and navigable, with every loading/empty/error/success state present.
- [ ] Timer is wall-clock-accurate and survives pause/resume, backgrounding, lock, and app kill without granting unearned plants or losing earned ones.
- [ ] Completing a focus session reliably plants exactly one plant, places it in a free tile (auto-expanding beds), runs unlock + streak + stats updates in a single consistent transaction, and plays the sow animation + haptic + (optional) chime.
- [ ] Plants advance through all 5 growth stages over real days, gated by both elapsed time and continued engagement, recomputed on foreground; growth never regresses.
- [ ] Daily streak increments/holds/resets correctly across midnight, app-closed days, timezone changes, and clock changes; longest streak tracked.
- [ ] ~14 seeded species across 4 rarities with working `UnlockRule`s (streak / plants-grown / single-session-minutes / always); locked species show silhouettes + conditions; unlock celebration sheet queues correctly.
- [ ] Garden detail (tap), rename note, move/reorder, and compost (with confirm) all work; export-garden-as-PNG works.
- [ ] Stats screen shows accurate today's minutes, current/longest streak, lifetime sessions, lifetime minutes, plants grown.
- [ ] Local notifications (completion + optional daily reminder) work with a soft-ask rationale and graceful denial handling.
- [ ] Live Activity (Lock Screen + Dynamic Island, all three presentations) shows the running timer and ends correctly; degrades silently where unsupported.
- [ ] Home Screen widget (small + medium) reads the App Group snapshot, shows streak/last-plant/today's-minutes, and deep-links into a new session.
- [ ] All persistence via SwiftData with a versioned schema/migration plan; no data loss on relaunch; defensive save error handling, never crashes.
- [ ] Full HIG compliance: Dynamic Type, Dark Mode (Night Garden palette), Reduce Motion, Increase Contrast, VoiceOver labels on every plant and control, 44pt tap targets, SF Symbols.
- [ ] Semantic theme system (no hardcoded colors), defined typography/spacing tokens, and the specified motion behaviors implemented.
- [ ] Settings (durations, auto-start, sound + chime picker, haptics, daily reminder, keep-awake, theme, appearance, reset, export, about) all functional and persisted.
- [ ] Works fully offline / airplane mode; minimal entitlements (App Group, Live Activities, notifications, custom URL scheme) and nothing else.
- [ ] Unit tests cover the timer math (incl. backgrounded completion), streak transitions (all branches), growth-stage computation, and unlock-rule evaluation; tests pass.
- [ ] Code is organized MVVM (Views / ViewModels / Models / Services / Theme), readable, commented where non-obvious, and the project runs to a satisfying first-session-plants-a-seed demo out of the box.
