diff --git a/.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-RESEARCH.md b/.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-RESEARCH.md
new file mode 100644
index 0000000..ad7e8b6
--- /dev/null
+++ b/.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-RESEARCH.md
@@ -0,0 +1,732 @@
+# Phase 18: Auto Timezone Detection and Ability to Change Timezone - Research
+
+**Researched:** 2026-06-15
+**Domain:** Server-side timezone configuration, Admin settings, IANA validation
+**Confidence:** HIGH (all findings grounded in actual codebase inspection)
+
+---
+
+
+## User Constraints (from CONTEXT.md)
+
+### Locked Decisions
+
+- **D-01:** Single **household-wide** timezone, stored in `app_config` (key `household_timezone`, IANA string value). No per-member `users.timezone` column.
+- **D-02:** Auto-detect the browser IANA timezone (`Intl.DateTimeFormat().resolvedOptions().timeZone`) and use it to **seed** the stored value during the Phase 12 setup wizard / first run.
+- **D-03:** After seeding, timezone changes **only** via the settings UI. No auto-overwrite on later login/detection differences. (Optional drift notice allowed, not required.)
+- **D-04:** Surface the timezone in the existing **role-gated `/admin` Settings** (Phase 10 `requireAdmin` boundary) and seed it from the **Phase 12 setup wizard**.
+- **D-05:** The stored timezone becomes the **source of truth for the server-side all-day "9 AM local" reminder computation** (`reminderScheduler.ts`, `outboxWorker.ts`), replacing the bare `process.env.TZ ?? Intl…` lookup at those sites.
+- **D-06:** **Fallback chain when `household_timezone` is unset**: fall back to `process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone`.
+- **D-07:** Display rendering and timed-event write serialization stay **browser-local** and unchanged. Must NOT touch `eventDateTime.ts` or `hydrateEvents.ts`.
+
+### Claude's Discretion
+
+- Timezone picker UX: a searchable IANA dropdown. Validate the value is a real IANA zone before storing.
+- Exact `app_config` key name and the read/cache strategy for the stored value in the scheduler/outbox (e.g. read-per-run vs cached).
+- Whether to show a non-blocking "detected zone differs" notice on login (allowed per D-03, not required).
+
+### Deferred Ideas (OUT OF SCOPE)
+
+- Per-member timezones (would add `users.timezone` + per-row scheduler logic).
+- Driving display/timed reminders off the stored tz (deliberately excluded, D-07).
+
+
+---
+
+## Summary
+
+Phase 18 is a tightly scoped wiring change: one new `app_config` key (`household_timezone`) becomes the source of truth for the server-side all-day reminder computation, replacing two bare `process.env.TZ ?? Intl…` lookups in `reminderScheduler.ts` (line 247) and `outboxWorker.ts` (lines 501 and 607). A single shared accessor helper reads this key from the DB with the D-06 fallback, ensuring both scheduler sites stay synchronized. No new npm packages are required. No schema migration is needed — `app_config` already exists and accepts arbitrary keys as additive rows.
+
+The admin surface (Phase 10 `adminRouter`, `requireAdmin`, `AdminPage.tsx`) already provides the exact pattern to extend: add GET + PUT endpoints to `/api/admin/config/timezone` following the same route file, client API function, TanStack Query + mutation pattern already present in `AdminPage.tsx`. The IANA validation uses a Zod `.refine()` with a `try/catch` on `Intl.DateTimeFormat` — no external library needed.
+
+Phase 12 (Initial Setup Wizard) has NOT been executed yet. Its status is `draft` (only a UI spec exists, no route code). The `household_timezone` seeding must therefore be planned as a Phase 18 deliverable that is additive/optional — a standalone seed endpoint (`POST /api/admin/config/timezone/seed`) or an inline seed in an early Phase 18 task — so Phase 18 is not blocked. When Phase 12 eventually executes, it calls the same write endpoint.
+
+**Primary recommendation:** Extract a `getHouseholdTimezone(db): Promise` helper in `apps/api/src/lib/householdTimezone.ts`, add GET/PUT endpoints on `adminRouter`, extend `AdminPage.tsx` with a new Timezone section, and wire both scheduler sites through the helper. No new dependencies. Read-per-run (not cached) for correctness.
+
+---
+
+## Architectural Responsibility Map
+
+| Capability | Primary Tier | Secondary Tier | Rationale |
+|------------|-------------|----------------|-----------|
+| Store household timezone | Database / Storage | API | `app_config` table; PK-keyed key/value row |
+| Read timezone for scheduling | API / Backend | — | `reminderScheduler.ts` + `outboxWorker.ts` run server-side; DB read at each tick |
+| Validate IANA timezone string | API / Backend | Browser / Client | Server validates before write (real boundary); browser validates before submit (UX) |
+| Admin read/write timezone | API / Backend | Frontend Server | `requireAdmin` is server enforced; client `isAdmin` is UX only (Phase 10 D-03) |
+| Browser timezone detection | Browser / Client | — | `Intl.DateTimeFormat().resolvedOptions().timeZone` is client-side only |
+| Display rendering, timed-event serialization | Browser / Client | — | Unchanged by D-07; remains browser-local |
+
+---
+
+## Standard Stack
+
+### Core (all already installed — no new packages)
+
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| drizzle-orm | 0.45.2 | DB read/write for `app_config` | Already in stack; `eq()` + `.select()` / `.insert().onDuplicateKeyUpdate()` for upsert |
+| zod | 3.25.x | IANA string validation | Already in stack; `.refine()` with `Intl.DateTimeFormat` try/catch |
+| @hono/zod-validator | 0.8.0 | Route body validation | Already wired in `adminRouter` |
+| hono | 4.12.23 | Route handlers | Already in stack; extend `adminRouter` |
+| @tanstack/react-query | 5.101.0 | PWA data fetch + mutation | Already in `AdminPage.tsx` |
+| Native `Intl` API | Node 22 built-in | IANA validation + browser detection | No package needed |
+
+**No new npm installs required for this phase.**
+
+### Supporting
+
+| Library | Version | Purpose | When to Use |
+|---------|---------|---------|-------------|
+| `mysql2` (via drizzle) | 3.22.4 | Underlying driver | Used implicitly by drizzle; no direct use needed |
+
+### Alternatives Considered
+
+| Instead of | Could Use | Tradeoff |
+|------------|-----------|----------|
+| `try/catch Intl.DateTimeFormat` | `Intl.supportedValuesOf('timeZone')` membership check | `supportedValuesOf` excludes 'UTC', 'GMT', 'Etc/UTC' (verified in Node 22) — those ARE valid. The try/catch approach accepts all valid zones including UTC variants [VERIFIED: Node 22 runtime test] |
+| Read-per-run DB read | In-memory TTL cache | Cache adds invalidation complexity; read-per-run means changes propagate within 60s (one scheduler tick) with no restart; PK lookup is negligible cost |
+| Extend existing `adminRouter` | New router/file | The existing pattern (`adminRouter.get/put`, `requireAdmin` first, `zValidator`) is correct and established — extending is the right choice |
+
+---
+
+## Package Legitimacy Audit
+
+This phase installs **no new packages**. All capabilities use packages already in the monorepo.
+
+**Packages removed due to [SLOP] verdict:** none
+**Packages flagged as suspicious [SUS]:** none (no new installs)
+
+> Note: `hono` was flagged `SUS` by the registry scanner due to a recent publish date, but it is a locked stack choice from CLAUDE.md and already installed. This flag does not apply to existing dependencies.
+
+---
+
+## Architecture Patterns
+
+### System Architecture Diagram
+
+```
+Browser (PWA) API Server MariaDB
+─────────────────────────────────────────────────────────────────────────────
+[AdminPage /admin] [adminRouter] [app_config]
+ useQuery('admin','timezone') → GET /api/admin/config/timezone → SELECT key='household_timezone'
+ useMutation(PUT) → PUT /api/admin/config/timezone → INSERT ... ON DUPLICATE KEY UPDATE
+ requireAdmin (DB check)
+ Intl.DateTimeFormat() zod IANA validate
+ .resolvedOptions().timeZone → store value
+ (browser detection for seed)
+
+[reminderScheduler.ts] [getHouseholdTimezone(db)] [app_config]
+ runReminderCheck() every 60s → SELECT key='household_timezone' → value | null
+ fallback: process.env.TZ ?? Intl…
+ pass tz → computeAlertInstantUtc()
+
+[outboxWorker.ts] [getHouseholdTimezone(db)] [app_config]
+ processOutboxRow() → SELECT key='household_timezone' → value | null
+ (allDay branch: lines 501, 607) fallback chain (same key, same fallback)
+ pass tz → computeAlertInstantUtc()
+```
+
+### Recommended Project Structure
+
+```
+apps/api/src/
+├── lib/
+│ └── householdTimezone.ts # NEW: getHouseholdTimezone(db) helper + IANA validator
+├── routes/
+│ └── admin.ts # EXTEND: add GET + PUT /config/timezone endpoints
+├── broker/
+│ ├── reminderScheduler.ts # MODIFY: line 247 — replace bare process.env.TZ lookup
+│ └── outboxWorker.ts # MODIFY: lines 501, 607 — replace bare process.env.TZ lookups
+apps/pwa/src/
+├── api/
+│ └── client.ts # EXTEND: add fetchAdminTimezone() + setAdminTimezone()
+└── routes/
+ └── AdminPage.tsx # EXTEND: add Timezone section after Shared Calendar section
+```
+
+### Pattern 1: Stored TZ Accessor Helper (getHouseholdTimezone)
+
+**What:** A single exported async function that reads `household_timezone` from `app_config` and applies the D-06 fallback chain.
+
+**When to use:** Called at the start of each all-day processing block in `reminderScheduler.ts` and `outboxWorker.ts`. Not called for timed events (those don't use local time).
+
+**Example:**
+```typescript
+// apps/api/src/lib/householdTimezone.ts
+import { eq } from 'drizzle-orm';
+import type { MySql2Database } from 'drizzle-orm/mysql2';
+import type * as schema from '../db/schema.js';
+import { appConfig } from '../db/schema.js';
+
+/**
+ * Read the household timezone from app_config.
+ * D-06 fallback: process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone
+ * (same fallback the bare lookups used before Phase 18).
+ *
+ * Read-per-call so timezone changes propagate within one scheduler tick
+ * without requiring a worker restart.
+ */
+export async function getHouseholdTimezone(
+ db: MySql2Database,
+): Promise {
+ const [row] = await db
+ .select({ value: appConfig.value })
+ .from(appConfig)
+ .where(eq(appConfig.key, 'household_timezone'))
+ .limit(1);
+
+ return (
+ row?.value ??
+ process.env.TZ ??
+ Intl.DateTimeFormat().resolvedOptions().timeZone
+ );
+}
+
+/**
+ * Validate that a string is an IANA timezone accepted by the JS engine.
+ * try/catch on Intl.DateTimeFormat covers 'UTC', 'GMT', 'Etc/UTC', and all
+ * 418 named IANA zones. Intl.supportedValuesOf('timeZone') is NOT used because
+ * it excludes 'UTC' and 'Etc/*' variants in Node 22 and Chrome.
+ */
+export function isValidIanaTimezone(tz: string): boolean {
+ try {
+ Intl.DateTimeFormat(undefined, { timeZone: tz });
+ return true;
+ } catch {
+ return false;
+ }
+}
+```
+
+**Usage in reminderScheduler.ts (line 247 replacement):**
+```typescript
+// BEFORE (line 247):
+const serverTz = process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+// AFTER:
+const serverTz = await getHouseholdTimezone(db);
+```
+
+**Usage in outboxWorker.ts (lines 501 and 607 replacement):**
+```typescript
+// BEFORE (line 501):
+const tz = process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+// AFTER:
+const tz = await getHouseholdTimezone(db);
+```
+
+### Pattern 2: Admin Endpoint (GET + PUT /api/admin/config/timezone)
+
+**What:** Two new routes on `adminRouter`, following the exact pattern of existing admin routes. GET reads the current value (with fallback), PUT validates + upserts.
+
+**When to use:** Admin settings UI reads/writes.
+
+**Example:**
+```typescript
+// In apps/api/src/routes/admin.ts (extend existing file)
+
+const timezoneSchema = z.object({
+ timezone: z.string().refine(isValidIanaTimezone, { message: 'Invalid IANA timezone' }),
+});
+
+// GET /api/admin/config/timezone
+adminRouter.get('/config/timezone', async (c) => {
+ const [row] = await db
+ .select({ value: appConfig.value })
+ .from(appConfig)
+ .where(eq(appConfig.key, 'household_timezone'))
+ .limit(1);
+
+ const timezone =
+ row?.value ??
+ process.env.TZ ??
+ Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+ return c.json({ timezone, isExplicitlySet: row?.value != null });
+});
+
+// PUT /api/admin/config/timezone
+adminRouter.put(
+ '/config/timezone',
+ zValidator('json', timezoneSchema),
+ async (c) => {
+ const { timezone } = c.req.valid('json');
+
+ await db
+ .insert(appConfig)
+ .values({ key: 'household_timezone', value: timezone })
+ .onDuplicateKeyUpdate({ set: { value: timezone } });
+
+ return c.json({ ok: true });
+ },
+);
+```
+
+### Pattern 3: PWA TanStack Query + Mutation (AdminPage extension)
+
+**What:** A new Timezone section in `AdminPage.tsx`, mirroring the existing Shared Calendar section pattern exactly.
+
+**Example:**
+```typescript
+// In apps/pwa/src/api/client.ts (extend existing file)
+export interface AdminTimezoneResponse {
+ timezone: string;
+ isExplicitlySet: boolean;
+}
+
+export async function fetchAdminTimezone(): Promise {
+ const res = await fetch('/api/admin/config/timezone', {
+ credentials: 'include',
+ redirect: 'manual',
+ });
+ handleAuthResponse(res, 'GET /api/admin/config/timezone');
+ return res.json() as Promise;
+}
+
+export async function setAdminTimezone(timezone: string): Promise {
+ const res = await fetch('/api/admin/config/timezone', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ redirect: 'manual',
+ body: JSON.stringify({ timezone }),
+ });
+ handleAuthResponse(res, 'PUT /api/admin/config/timezone');
+}
+```
+
+```typescript
+// In AdminPage.tsx (new section, same query/mutation pattern as calendarsQuery)
+const timezoneQuery = useQuery({
+ queryKey: ['admin', 'timezone'],
+ queryFn: fetchAdminTimezone,
+ retry: false,
+ staleTime: 60 * 1000,
+});
+
+const timezoneMutation = useMutation({
+ mutationFn: (tz: string) => setAdminTimezone(tz),
+ onSuccess: () => {
+ void queryClient.invalidateQueries({ queryKey: ['admin', 'timezone'] });
+ },
+});
+```
+
+**IANA Picker UX — no new library:** A `