diff --git a/.agent/phases/todo/23_containerfile_build/01_fix_containerfile_build.md b/.agent/phases/complete/23_containerfile_build/01_fix_containerfile_build.md
similarity index 100%
rename from .agent/phases/todo/23_containerfile_build/01_fix_containerfile_build.md
rename to .agent/phases/complete/23_containerfile_build/01_fix_containerfile_build.md
diff --git a/.agent/phases/todo/22_background_animation/00_phase.md b/.agent/phases/todo/22_background_animation/00_phase.md
deleted file mode 100644
index a9007b9..0000000
--- a/.agent/phases/todo/22_background_animation/00_phase.md
+++ /dev/null
@@ -1,114 +0,0 @@
-# Phase 22 — Animated Background: Make It Actually Animate
-
-**Source:** `TODO.md` L5 — *"Fix background animation not working, just
-blinking"*
-**Story:** `.agent/user_stories/background-animation.md` (created by task 02)
-**Context:** `frontend/assets/styles.css` — the phase-08 animated
-background block (~lines 60–100): `body::before` (44px drifting grid,
-1px lines at ~35% `--line` alpha, radial mask
-`radial-gradient(120% 90% at 50% 0%, black 25%, transparent 78%)`,
-`animation: bg-grid-drift 60s linear infinite`) and `body::after` (two
-soft radial glows, `animation: bg-glow-breathe 14s ease-in-out infinite
-alternate`, opacity 0.65↔1 + scale 1↔1.05). Both layers are
-`position: fixed; inset: 0; z-index: -1; pointer-events: none`. `html`
-owns the `var(--bg)` canvas and `body` is `background: transparent`
-(~lines 42–54) — if any later rule occludes that, the layers vanish.
-The **phase-08 design comments are the spec** for what "working" means.
-
-## Objective
-Owner report 2026-08-24: the background "just blinks" — i.e. the motion
-the phase-08 design promised (a slow, seamless grid drift + a gentle
-glow breathe) is not perceived; at most a flicker/blink is visible.
-Diagnose which layer(s) actually fail in a real Chromium viewport, fix
-the CSS, and leave a background that visibly and smoothly animates as
-designed — no blink, no static frame, no jank.
-
-## Owner-confirmed (2026-08-24, roadmap A3)
-1. **Intended effect = the phase-08 design comments:** seamless 60s grid
- drift (one cell per loop) + 14s ease glow breathing. The fix serves
- that design, not a redesign.
-2. **Pure CSS, zero JS** (phase-08 anchor) — no animation JS, no new
- assets, no `filter: blur` (perf note in the block).
-
-## Design / diagnostic plan
-The fix is found, not guessed — work through this checklist in a real
-Chromium window (dev server, full page, ~15s of observation):
-1. **Per-layer visibility:** toggle each pseudo-element (DevTools
- generated-content / a temp outline) and screenshot — is the grid
- visible at all? Is only the glow (the "blink" the user perceives)
- alive?
-2. **Grid layer:** sample `background-position` on `body::before` at two
- timestamps — is it actually moving? Is the radial mask fading the
- visible region so small that the 44px/60s drift is imperceptible?
- (If the drift is real but too faint: raise the grid line alpha and/or
- the mask's visible radius — smallest change that reads as "smooth
- drift".)
-3. **Glow layer:** is the 14s breathe reading as a *blink*? (If the
- opacity swing 0.65↔1 is perceived as pulsing: lengthen the period
- and/or narrow the opacity delta so it reads as breathing.)
-4. **Occlusion check:** confirm nothing later in `styles.css` (or in
- `html`/`body` rules) paints an opaque background over the
- `z-index: -1` layers — the phase-08 comment at ~line 42 is the
- contract.
-5. **Apply the fix in `styles.css`** — document the found root cause in
- the phase report (screenshot before/after in
- `.agent/screenshots/22_background_animation/`).
-
-## Dependencies
-- `08_story_dark_tech_theme` (complete) — owns the layers, the palette,
- and the "pure CSS, zero JS" anchor this phase must respect.
-- `07_story_responsive_polish` (complete) — no new overflow at 360px
- (both layers are `fixed; inset: 0` — keep it that way).
-
-## Tasks
-1. `01_fix_background_animation.md` — diagnosis + the CSS fix +
- source-level unit pins.
-2. `02_e2e_story_suite_commit.md` — `tests/e2e/test_background_animation.py`
- (the story gate, isolated), regression suites, story file, final
- validation, the single atomic commit, phase move to `complete/`.
-
-## Locked decisions
-- **Phase-08 anchor honored** — pure CSS, zero JS, no `filter: blur`,
- WCAG AA palette untouched (background layers carry no text).
- **A11 untouched** — no CDN, no new assets. **A16 honored** — one new
- story E2E suite + adapted regressions. No anchor changed.
-
-## Testing & Quality
-- **Unit (source-level, new `tests/unit/test_background_animation.py`,
- repo source-pin pattern):** `styles.css` still defines
- `@keyframes bg-grid-drift` and `@keyframes bg-glow-breathe`;
- `body::before` animates `bg-grid-drift` with `linear infinite`;
- `body::after` animates `bg-glow-breathe`; both layers remain
- `position: fixed; z-index: -1; pointer-events: none`; `html` keeps
- `background: var(--bg)` and `body` keeps `background: transparent`
- (the no-occlusion contract). Pin the **final** values the fix lands
- on (durations/opacities may move per the design plan).
-- **Integration:** none (no `app/` changes).
-- **Coverage:** frontend-only; the >90% `app/` gate is unaffected.
-- **E2E:** `tests/e2e/test_background_animation.py` (task 02), green
- **in isolation** (prereq `podman compose up -d db`).
-- **Lint/types:** `uv run ruff check . && uv run pyright` clean.
-
-## Completion Criteria
-- [ ] In a real Chromium viewport, the background visibly and smoothly
- animates (grid drift + glow breathe) — screenshot before/after in
- the phase report; owner's "just blinking" perception gone.
-- [ ] Root cause documented in `.agent/reports/22_background_animation/`.
-- [ ] `uv run pytest` green; `uv run pytest --cov=app
- --cov-report=term-missing` ≥ today's number.
-- [ ] `uv run pytest tests/e2e/test_background_animation.py -v --no-cov`
- green in isolation; regressions green in isolation:
- `test_dark_tech_theme.py`, `test_responsive_polish.py`.
-- [ ] `uv run ruff check . && uv run pyright` clean.
-- [ ] UI Structure Check (AGENTS.md rule 5): layers stay behind content
- (`z-index: -1`, `pointer-events: none`), no text/contrast impact,
- no 360px overflow.
-- [ ] `.agent/user_stories/background-animation.md` exists.
-- [ ] One `--no-gpg-sign` commit (below);
- `.agent/phases/todo/22_background_animation/` moved to
- `.agent/phases/complete/`.
-
-## Commit
-```bash
-git add -A .agent/ frontend/ tests/ && git commit --no-gpg-sign -m "fix(ui): animated background actually animates — grid drift and glow breathe per the phase-08 design"
-```
diff --git a/.agent/phases/todo/22_background_animation/02_e2e_story_suite_commit.md b/.agent/phases/todo/22_background_animation/02_e2e_story_suite_commit.md
deleted file mode 100644
index 8124519..0000000
--- a/.agent/phases/todo/22_background_animation/02_e2e_story_suite_commit.md
+++ /dev/null
@@ -1,68 +0,0 @@
-# Task 02 — E2E story suite, story file, validation, commit
-
-**Phase:** `22_background_animation` · **Source:** `TODO.md` L5
-
-## Objective
-The story gate: `tests/e2e/test_background_animation.py` proves both
-background layers are actually running animations (not just declared),
-plus regressions, story file, final validation, and the single atomic
-commit.
-
-## Work
-1. `tests/e2e/test_background_animation.py` (new). The layers are CSS
- pseudo-elements, so assert via computed style + the Web Animations
- API (Chromium reports pseudo-element CSS animations through
- `element.getAnimations()`):
- 1. `test_grid_layer_animation_running` —
- `getComputedStyle(document.body, "::before").animationName` is the
- grid-drift keyframe (final name from task 01), timing function
- `linear`, iteration count `infinite`; and a matching entry in
- `document.body.getAnimations()` with `playState === "running"`.
- 2. `test_glow_layer_animation_running` — same for `"::after"` with
- the glow-breathe keyframe; `playState === "running"`.
- 3. `test_animations_advance` — sample `animation.currentTime` (or
- the `getAnimations()` entry's `currentTime`) for both layers,
- wait ~500ms (`page.wait_for_timeout`), assert both advanced —
- the animations are truly running, not paused.
- 4. `test_background_layers_contracts` — both pseudo-elements:
- `position: fixed`, `z-index: -1`, `pointer-events: none`;
- `document.documentElement` computed `background-color` is the
- palette bg (the canvas stays on `html`); `document.body` computed
- `background-color` is `rgba(0, 0, 0, 0)` (no occlusion).
- 5. `test_no_horizontal_overflow_with_layers` (regression, 360px) —
- viewport 360px: `document.documentElement.scrollWidth <=
- clientWidth` (the phase-07 pin, replicated locally).
-2. `.agent/user_stories/background-animation.md` (new) — story file per
- the repo format: goal, the bug report verbatim from `TODO.md` L5, the
- owner-confirmed A3 decisions + the found root cause (from task 01's
- report), E2E mapping table.
-3. Run the suite **in isolation** (prereq `podman compose up -d db`):
- `uv run pytest tests/e2e/test_background_animation.py -v --no-cov`.
-4. Regressions, in isolation, one command each:
- - `uv run pytest tests/e2e/test_dark_tech_theme.py -v --no-cov`
- - `uv run pytest tests/e2e/test_responsive_polish.py -v --no-cov`
-5. Final validation: `uv run pytest` green; `uv run pytest --cov=app
- --cov-report=term-missing` ≥ today's number (>90% gate);
- `uv run ruff check . && uv run pyright` clean.
-6. **UI Structure Check** (AGENTS.md rule 5): layers stay behind
- content, no text/contrast impact, no overflow at 360px.
-7. Finish the phase report (`.agent/reports/22_background_animation/` —
- E2E results + the task-01 screenshots).
-8. Commit (one atomic commit) and move the phase:
- ```bash
- git add -A .agent/ frontend/ tests/
- git commit --no-gpg-sign -m "fix(ui): animated background actually animates — grid drift and glow breathe per the phase-08 design"
- mv .agent/phases/todo/22_background_animation .agent/phases/complete/
- ```
-
-## Testing & Quality
-- Story suite green **in isolation**; both regression suites green in
- isolation; full unit+integration suite green; `app/` coverage at or
- above today's number (>90%); ruff + pyright clean.
-
-## Completion Criteria
-- [ ] `test_background_animation.py` 5/5 in isolation.
-- [ ] Regressions (dark tech theme, responsive polish) green in
- isolation.
-- [ ] Story file + phase report (with screenshots) exist.
-- [ ] One `--no-gpg-sign` commit; phase directory in `complete/`.
diff --git a/.agent/reports/23_containerfile_build/phase_report.md b/.agent/reports/23_containerfile_build/phase_report.md
new file mode 100644
index 0000000..4df17cb
--- /dev/null
+++ b/.agent/reports/23_containerfile_build/phase_report.md
@@ -0,0 +1,120 @@
+# Phase 23 report — Containerfile: build the whole app image again
+
+**Date:** 2026-08-24 · **Source:** `TODO.md` L6 ("Fix Containerfile build
+not working") · **Story:** `.agent/user_stories/containerfile-build.md`
+
+## Root cause (verified, not guessed — logs in this directory)
+
+1. **Build failure:** phase 19's absolute `import … from
+ "/assets/header.js"` is resolved by esbuild as the *filesystem* path
+ `/assets/header.js` → `✘ [ERROR] Could not resolve "/assets/header.js"`,
+ reproduced with the exact pinned esbuild **0.25.5**
+ (`repro_esbuild_0.25.5.log`, `repro_podman_build_before_fix.log`).
+2. **Under-shipped image:** stage 1 bundled only `app.js` +
+ `sources.js` and copied only `index.html` + `sources.html` —
+ `document.html` / `login.html` / `document.js` / `login.js` /
+ `markdown.js` were all missing from the image.
+3. **Double-evaluation trap:** the four direct `
-
-
+