```yaml
id: UJ-003
title: Theme preference persists across reload
actors: [dashboard-operator]
surface: shell
goal: Operator selects light theme and fresh page load preserves it.
trigger: Activate `Light` in Theme control.
fixtures: [browser-localStorage]
success_state:
  visible: HTML has `data-theme="light"`.
  durable: localStorage `overdeck-theme` is `light`.
  persistence: Reload HTML has `data-theme="light"`.
source_specs: [apps/web/src/components/ThemeToggle.tsx, apps/web/src/layouts/Base.astro, apps/web/tests/shell.spec.ts]
readiness: ready
canonical: true
blockers: []
```

## Path

| Hop | Evidence |
|---|---|
| UI trigger | `apps/web/src/components/ThemeToggle.tsx:38` |
| Request construction | N/A — browser-local preference has no HTTP request. |
| Endpoint auth and parse | N/A — browser-local preference has no endpoint. |
| Authoritative write | `apps/web/src/layouts/Base.astro:33` |
| Response | N/A — browser-local write has no HTTP response. |
| Terminal render | `apps/web/src/layouts/Base.astro:25` |

## Happy path

### H1
- Setup: No `overdeck-theme` localStorage value.
- Action: Activate `Light` in Theme control.
- Request: N/A — browser-local preference has no HTTP request.
- Response: N/A — transport produced no HTTP response.
- Visible: HTML has `data-theme="light"`.
- Durable: localStorage key `overdeck-theme` equals `light`.
- Fresh read: Full reload renders HTML with `data-theme="light"`.
- Forbidden: No collector request occurs.
- Evidence:
  - Setup: `apps/web/src/layouts/Base.astro:29`
  - Action: `apps/web/tests/shell.spec.ts:48`
  - Request: `apps/web/src/components/ThemeToggle.tsx:20`
  - Response: `apps/web/src/layouts/Base.astro:33`
  - Visible: `apps/web/tests/shell.spec.ts:49`
  - Durable: `apps/web/src/layouts/Base.astro:34`
  - Fresh read: `apps/web/tests/shell.spec.ts:51`
  - Forbidden: `apps/web/src/components/ThemeToggle.tsx:20`

## Alternate and failure paths

### A1
- Setup: localStorage key `overdeck-theme` equals `invalid`.
- Action: Load any shell page.
- Request: N/A — browser-local preference has no HTTP request.
- Response: N/A — transport produced no HTTP response.
- Visible: HTML has `data-theme="dark"`.
- Durable: Invalid stored value remains unchanged.
- Fresh read: Full reload renders HTML with `data-theme="dark"`.
- Forbidden: Invalid value is not applied as HTML theme.
- Evidence:
  - Setup: `apps/web/src/layouts/Base.astro:29`
  - Action: `apps/web/src/layouts/Base.astro:38`
  - Request: `apps/web/src/layouts/Base.astro:29`
  - Response: `apps/web/src/layouts/Base.astro:30`
  - Visible: `apps/web/src/layouts/Base.astro:25`
  - Durable: `apps/web/src/layouts/Base.astro:29`
  - Fresh read: `apps/web/src/layouts/Base.astro:38`
  - Forbidden: `apps/web/src/layouts/Base.astro:30`

## Permissions and boundaries

N/A — browser-local preference has no user identity boundary.

## Source specs

- `apps/web/src/components/ThemeToggle.tsx`
- `apps/web/src/layouts/Base.astro`
- `apps/web/tests/shell.spec.ts`
