diff --git a/.planning/phases/03-event-write-back-pwa-install/03-CONTEXT.md b/.planning/phases/03-event-write-back-pwa-install/03-CONTEXT.md
new file mode 100644
index 0000000..9255c79
--- /dev/null
+++ b/.planning/phases/03-event-write-back-pwa-install/03-CONTEXT.md
@@ -0,0 +1,205 @@
+# Phase 3: Event Write-Back + PWA Install - Context
+
+**Gathered:** 2026-06-05
+**Status:** Ready for planning
+
+
+## Phase Boundary
+
+Members can **create, edit, and delete events** that are written back to the correct
+Fastmail calendar through the existing CalDAV broker boundary (PUT / DELETE via tsdav —
+never a direct Fastmail call from the UI). The app becomes an **installable PWA**
+(web manifest + service worker, HTTPS) with a **guided iOS Add-to-Home-Screen walkthrough**
+and Android install handling. This phase also carries the **Phase 1 Gate 2 live-auth
+verification** (D-14): real Authelia OIDC login over the public Pangolin URL including the
+**iOS standalone-PWA** flow, session persistence, and distinct stable per-member colors.
+
+Requirements: CAL-04 (create timed/all-day), CAL-05 (edit), CAL-06 (delete),
+CAL-07 (create recurring — whole-series only), PWA-01 (installable), PWA-02 (guided install).
+
+**Out of scope (other phases / later):**
+- Single-occurrence and "this-and-following" recurring edits (CAL-09 / CAL-10) — **v1.x**.
+- Shared lists + live SSE sync (Phase 4); Web Push notifications (Phase 5).
+- Wall-display / kiosk theme (v2).
+- SSE as a transport — **must not be relied on in Phase 3** (unverified until Phase 4 gate, D-14).
+
+
+
+## Implementation Decisions
+
+### Target-calendar selection (write target)
+- **D-01:** **Default target = remember last-used per member.** Seed/first-time default is
+ the **creator's own personal calendar** (always exists; no shared calendar may exist yet, D-16).
+- **D-02:** **Calendar picker is shown only when the member has >1 writable calendar.** With a
+ single writable calendar (e.g. personal only, before a shared Family calendar exists) the
+ picker is hidden entirely — zero friction for the non-technical member. It appears once a
+ shared Family calendar is present.
+- **D-03:** **Writable set = the member's own personal calendar + the shared Family calendar**
+ (when shared read-write to them in Fastmail). The **other member's personal calendar is a
+ read-only overlay** — never a write target. Matches the two-equal-partners model.
+- **D-04:** **Edit may move an event to a different calendar.** Implemented as CalDAV
+ **delete-from-old + create-on-new** (not an in-place move). Researcher/planner must handle
+ the UID/etag implications and **partial-failure** (delete succeeded but create failed, or
+ vice-versa) safely.
+
+### Write feedback & sync (the load-bearing architecture decision)
+- **D-05:** **Optimistic-accept + server-side outbox.** On save the UI optimistically reflects
+ the change with a "syncing…" indicator; the API writes a **`pending` row to a MariaDB outbox**
+ and returns immediately. A **backend worker drains the outbox** against Fastmail.
+- **D-06:** **Re-sync on confirm.** When the queued write confirms, the worker triggers a
+ **targeted re-sync of just that one calendar** (not a full poll) so the MariaDB cache becomes
+ authoritative, then clears the pending state. (This is the async evolution of the operator's
+ initial "forced re-sync, then show" — same authoritative-cache guarantee, without a blocking
+ spinner.)
+- **D-07:** **Retry policy — backoff transient, fail-fast hard errors.**
+ - *Transient* (network error, 5xx, timeout) → retry with **exponential backoff over a bounded
+ window** (a few minutes), keeping the "not synced yet" toast visible.
+ - *Hard* (401/403 auth, 400 validation) → **stop immediately** and surface a real
+ "didn't save" error to the user. (Transient failures must be short; anything non-transient
+ is a hard fail that won't self-resolve.)
+- **D-08:** **Edit-conflict handling = detect + warn + reload latest.** Writes send `If-Match`
+ with the cached etag. On **412 Precondition Failed**, the write is **routed out of the retry
+ loop** into a conflict flow: re-sync that event from Fastmail and warn the user
+ ("this event changed elsewhere — review the latest version") before they retry. **No silent
+ last-write-wins overwrite.**
+- **D-09:** **Sync-state is surfaced via polling, not SSE.** The "pending / not-synced" state the
+ toast reads must be exposed over a polled endpoint (or query refetch), because SSE-over-Pangolin
+ is unverified until the Phase 4 entry gate (D-14). Do **not** build Phase 3 sync feedback on SSE.
+
+### Carried forward — locked, NOT re-discussed
+- **D-10:** The **edit/delete surface reuses `EventDetailPopover`** — it was built in Phase 2 with
+ a reserved footer action area specifically for this (Phase 2 D-08). Create can reuse the same
+ surface shell.
+- **D-11:** **Recurring events: create + whole-series edit only** in v1. Single-occurrence
+ (RECURRENCE-ID/EXDATE) and "this-and-following" edits are **v1.x** (CAL-09/CAL-10).
+- **D-12:** **Broker is the only Fastmail I/O boundary.** Routes touch only the MariaDB cache;
+ all PUT/DELETE goes through `src/broker/`. No tsdav import in route handlers.
+- **D-13:** **Dev-auth bypass** stays available for local build/test (project D-14); live
+ Authelia verification is the Gate 2 item folded into this phase.
+
+### Claude's Discretion (researcher / planner decide)
+- **Event form & fields** — exact field set (title, start/end, all-day toggle, location,
+ description) and layout. Build it into / alongside the reused `EventDetailPopover` surface (D-10).
+ Must be slick and low-friction for the non-technical Apple member.
+- **Recurrence creation UX** — how whole-series RRULE is exposed (simple presets daily/weekly/
+ monthly/yearly vs a small custom builder). Keep it minimal for v1; whole-series only (D-11).
+- **iOS install onboarding** — trigger (auto-detect iOS-Safari-non-standalone vs help button vs
+ first-visit banner) and the annotated Add-to-Home-Screen walkthrough content. **Load-bearing:**
+ success criterion 4 requires a non-technical user to follow it independently; no install ⇒ no
+ push in Phase 5.
+- **Android install** — `beforeinstallprompt` handling (custom button vs native prompt).
+- **PWA tooling** — `vite-plugin-pwa` is in the recommended stack (CLAUDE.md) but **not yet
+ installed**; manifest + service worker config is the planner's call. Keep the service worker
+ conservative (don't break the OIDC redirect / standalone-PWA login flow — Gate 2 risk).
+- Outbox worker mechanics (interval vs trigger, idempotency key, max-attempt count, dead-letter
+ surfacing) — implement to satisfy D-05/D-06/D-07.
+
+
+
+
+## Canonical References
+
+**Downstream agents MUST read these before planning or implementing.**
+
+### Project decisions & scope
+- `.planning/PROJECT.md` — core value, constraints, Key Decisions incl. **D-14** (dev-auth
+ bypass; live AUTH + iOS smoke folded into Phase 3), **D-15** (local Newt test rig for Gate 2),
+ **D-16** (shared Family calendar is a collection on the operator's primary account, may not
+ exist yet; `is_shared` flag marks it).
+- `.planning/ROADMAP.md` §"Phase 3: Event Write-Back + PWA Install" — goal + the **6 success
+ criteria** (incl. criterion 6, the carried Gate 2 live-auth/iOS verification).
+- `.planning/REQUIREMENTS.md` — CAL-04/05/06 (create/edit/delete), CAL-07 (recurring create),
+ PWA-01/02 (installable + guided install); v1.x CAL-09/10 (single-occurrence edits — OUT).
+
+### Phase 1/2 foundation this builds on
+- `.planning/phases/02-calendar-display/02-CONTEXT.md` — design-token layer (D-01/02/03),
+ `EventDetailPopover` reuse-as-edit-surface (Phase 2 D-08), server-side expansion, color/owner model.
+- `.planning/phases/01-foundation-broker-spike/CAL-08-DECISION.md` — per-member app-password
+ model (how each member's calendars are reached/written; informs the writable set, D-03).
+- `docs/deployment.md` — **Gate 2 checklist** (live Authelia OIDC over Pangolin, iOS standalone
+ PWA) and the dev-auth bypass context. **Required reading for success criterion 6.**
+
+### Code this phase extends
+- `apps/api/src/broker/client.ts` — tsdav `createDAVClient`; add PUT/DELETE write methods here
+ (broker boundary, D-12).
+- `apps/api/src/broker/sync.ts` — REPORT→ical.js→upsert; the targeted single-calendar re-sync
+ (D-06) builds on this.
+- `apps/api/src/broker/poller.ts` — ctag poller; the outbox worker is a sibling background process.
+- `apps/api/src/routes/events.ts` — current read-only `GET /api/events`; add create/edit/delete
+ endpoints + the pending/sync-state surface (D-09) alongside.
+- `apps/api/src/db/schema.ts` — `calendars` (userId, isShared, url), `calendarEvents`
+ (uid, etag, rawVevent, dtstart split). **New outbox table** lives here (D-05).
+- `apps/pwa/src/components/EventDetailPopover.tsx` — reserved footer action area is the edit/delete
+ entry point (D-10).
+- `apps/pwa/src/api/client.ts` — typed fetch client to extend with write calls + sync-state poll.
+- `apps/pwa/vite.config.ts` — no PWA plugin yet; manifest + service worker added here (PWA-01).
+- `CLAUDE.md` — locked stack incl. `vite-plugin-pwa` 1.3.0, tsdav write-back guidance
+ (PUT new .ics / DELETE by UID), iOS PWA constraints (16.4+, home-screen install required).
+
+
+
+
+## Existing Code Insights
+
+### Reusable Assets
+- `EventDetailPopover` (Phase 2) — read-only detail overlay with a **reserved footer for Phase 3
+ edit/delete** (D-10); responsive bottom-sheet/popover, focus trap, XSS-safe plain-text rendering.
+- `apps/pwa/src/api/client.ts` — typed `fetch` client (`fetchMe`, windowed `fetchEvents`,
+ `credentials: 'include'`); extend with create/edit/delete + sync-state poll.
+- Broker module (`client.ts`/`sync.ts`/`poller.ts`) — owns all Fastmail I/O and ical.js parsing;
+ write methods and the outbox worker attach here.
+- Design-token layer + Zustand UI store + TanStack Query — server state in Query, UI state in Zustand.
+
+### Established Patterns
+- **Broker boundary (T-03-02):** routes read the MariaDB cache only; never import tsdav in a route.
+ Write-back must keep this — endpoint enqueues to the outbox; the broker worker does the CalDAV PUT.
+- **D-13 schema split** for all-day (dtstartDate) vs timed (dtstartUtc) — write-back must produce
+ correct VEVENTs for both and never coerce DATE→DATETIME.
+- **Idempotency:** `calendar_id + uid` composite unique key; etag tracked per event (drives D-08).
+- Hono app exported without auto-start (testable); add write routes + outbox worker in that shape.
+
+### Integration Points
+- **New MariaDB outbox table** (D-05): pending create/edit/delete operations with status, attempt
+ count, target calendar URL, payload/UID, etag for If-Match.
+- **Outbox worker** (sibling to the ctag poller) drains the queue, applies D-07 retry/backoff,
+ triggers the targeted re-sync (D-06), and updates pending status read by the polled sync-state
+ endpoint (D-09).
+- **Write endpoints** on the events router (create/edit/delete) that validate input (zod) and
+ enqueue rather than calling Fastmail inline.
+- **PWA layer**: `vite-plugin-pwa` manifest + service worker (PWA-01) — keep the SW conservative
+ so it does not break the OIDC redirect / iOS standalone login (Gate 2, success criterion 6).
+
+
+
+
+## Specific Ideas
+
+- The non-technical Apple member is the design center: the calendar picker disappears when there's
+ only one choice (D-02), saves never block on a spinner (D-05/D-06), and a failed sync shows a
+ clear, persistent "not synced yet" state rather than silently losing the edit.
+- "Transient failures should be short; anything beyond that is a hard fail that won't auto-resolve"
+ — drives the bounded-backoff-then-fail policy (D-07).
+- iOS install walkthrough must be followable independently with annotated screenshots — it's the
+ prerequisite for her getting Web Push in Phase 5.
+
+
+
+
+## Deferred Ideas
+
+- **Single-occurrence / "this-and-following" recurring edits** (CAL-09/CAL-10) — v1.x; Phase 3 is
+ create + whole-series edit only.
+- **Writing to the other member's personal calendar** — out; other members' personals are
+ read-only overlays in v1 (D-03).
+- **SSE-based live sync-state push** — deferred to after the Phase 4 SSE-over-Pangolin gate; Phase 3
+ surfaces sync state via polling (D-09).
+- **Event form areas not deep-dived** (exact fields, recurrence-builder richness, iOS/Android
+ install UX) — left to researcher/planner discretion within the constraints above; not deferred
+ out of phase, just not operator-locked.
+
+
+
+---
+
+*Phase: 3-Event Write-Back + PWA Install*
+*Context gathered: 2026-06-05*
diff --git a/.planning/phases/03-event-write-back-pwa-install/03-DISCUSSION-LOG.md b/.planning/phases/03-event-write-back-pwa-install/03-DISCUSSION-LOG.md
new file mode 100644
index 0000000..fa7ed37
--- /dev/null
+++ b/.planning/phases/03-event-write-back-pwa-install/03-DISCUSSION-LOG.md
@@ -0,0 +1,111 @@
+# Phase 3: Event Write-Back + PWA Install - Discussion Log
+
+> **Audit trail only.** Do not use as input to planning, research, or execution agents.
+> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
+
+**Date:** 2026-06-05
+**Phase:** 3-Event Write-Back + PWA Install
+**Areas discussed:** Target calendar pick, Write feedback & sync
+
+Areas offered but not selected (left to Claude's discretion): Event form & fields, iOS install onboarding.
+
+---
+
+## Target calendar pick
+
+### Default target calendar for a new event
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Their own personal | New events default to the creator's own personal calendar | |
+| Shared Family | Default to the shared Family calendar; personal is opt-out (needs fallback, may not exist) | |
+| Remember last-used | Default to whatever calendar they last wrote to, persisted per member | ✓ |
+
+**User's choice:** Remember last-used
+**Notes:** Seed/first-time default set to creator's own personal (always exists; shared may not, per D-16).
+
+### Calendar selector visibility
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Only when >1 writable | Hide picker when only one writable calendar exists; show once shared Family exists | ✓ |
+| Always show | Always render the selector with default pre-selected | |
+
+**User's choice:** Only when >1 writable
+
+### Move event between calendars on edit
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Lock calendar on edit | Calendar fixed once created; defer cross-calendar move to v1.x | |
+| Allow move | Edit may change target calendar (CalDAV delete-old + create-new) | ✓ |
+
+**User's choice:** Allow move
+**Notes:** Researcher/planner must handle UID/etag and partial-failure safety.
+
+### Writable calendar set
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Own personal + shared Family | Write to own personal + shared Family; other member's personal is read-only overlay | ✓ |
+| Any visible calendar | Allow writing to any aggregated calendar incl. other member's personal | |
+
+**User's choice:** Own personal + shared Family
+
+---
+
+## Write feedback & sync
+
+### How the member sees their own change after save (poll-based cache)
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Optimistic + forced re-sync | Optimistic UI update + background targeted re-sync of that calendar | (evolved into) |
+| Forced re-sync, then show | Synchronously re-sync that calendar, then refresh; ~0.5–1s spinner | ✓ (initial) |
+| Wait for poll | Let the ctag poller pick it up next cycle; visibly laggy | |
+
+**User's choice:** Forced re-sync, then show — subsequently evolved (via the failure-handling answer) into optimistic-accept + queued write + re-sync on confirm. Same authoritative-cache guarantee, async instead of blocking.
+
+### Behavior when the CalDAV write fails
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Inline error + retry, keep form | Keep form + input, show inline error, manual retry | |
+| Toast + silent rollback | Close form, roll back optimistic change, transient toast | |
+
+**User's choice:** Free-text — "accept the edit, queue it, keep a toast showing it isn't sync'd yet and keep retrying. Transient failures should be short; anything beyond that is a hard fail that won't auto-resolve."
+**Notes:** Reframed the save path into an optimistic-accept + queued-write-with-retry model (outbox).
+
+### Edit-conflict (etag mismatch / 412) handling
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Detect + warn, reload latest | If-Match cached etag; on 412 abort, re-sync, warn before retry | ✓ |
+| Last-write-wins | No If-Match; overwrite whatever is on Fastmail | |
+
+**User's choice:** Detect + warn, reload latest
+
+### Pending-write queue location
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Server-side outbox (MariaDB) | API persists pending row; backend worker drains with backoff; survives app close | ✓ |
+| Client-side queue (PWA) | PWA holds + retries; stops when app closed unless persisted | |
+
+**User's choice:** Server-side outbox (MariaDB)
+
+### Transient vs hard-failure classification
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Backoff transient; fail-fast hard errors | Network/5xx/timeout → bounded backoff; 401/403/400/412 → stop/surface | ✓ |
+| Let me refine the thresholds | Operator specifies retry window/backoff/status codes | |
+
+**User's choice:** Backoff transient; fail-fast hard errors (412 routed to the conflict-reload flow)
+
+---
+
+## Claude's Discretion
+
+- Event form & fields (field set, layout) — build into / alongside the reused `EventDetailPopover`.
+- Recurrence creation UX (presets vs custom builder) — whole-series only for v1.
+- iOS install onboarding (trigger + annotated walkthrough) and Android `beforeinstallprompt`.
+- `vite-plugin-pwa` manifest + service worker config (keep SW conservative re: OIDC/iOS login).
+- Outbox worker mechanics (interval/trigger, idempotency key, max attempts, dead-letter).
+
+## Deferred Ideas
+
+- Single-occurrence / "this-and-following" recurring edits (CAL-09/CAL-10) — v1.x.
+- Writing to the other member's personal calendar — out (read-only overlay in v1).
+- SSE-based live sync-state push — after the Phase 4 SSE-over-Pangolin gate; Phase 3 uses polling.