diff --git a/.gsd/KNOWLEDGE.md b/.gsd/KNOWLEDGE.md
index f875c07..c0da99e 100644
--- a/.gsd/KNOWLEDGE.md
+++ b/.gsd/KNOWLEDGE.md
@@ -1,5 +1,11 @@
# KNOWLEDGE
+## Healthcheck in slim Python Docker images: use os.kill(1,0) not pgrep
+
+**Context:** The `python:3.x-slim` Docker image doesn't include `procps`, so `pgrep -f watcher.py` fails in healthcheck commands. This applies to any slim-based image where a healthcheck needs to verify the main process is alive.
+
+**Fix:** Use `python -c "import os; os.kill(1, 0)"` as the healthcheck command. PID 1 is always the container's entrypoint process. `os.kill(pid, 0)` checks if the process exists without sending a signal — it raises `ProcessLookupError` if the process is dead, causing the healthcheck to fail. No external tools needed.
+
## SQLAlchemy column names that shadow ORM functions
**Context:** If a model has a column named `relationship` (or any other ORM function name like `query`, `metadata`), Python resolves the name to the column's `MappedColumn` descriptor within the class body. This causes `relationship(...)` calls below it to fail with "MappedColumn object is not callable".
diff --git a/.gsd/PROJECT.md b/.gsd/PROJECT.md
index eed90cf..b9dcfde 100644
--- a/.gsd/PROJECT.md
+++ b/.gsd/PROJECT.md
@@ -13,7 +13,8 @@ Five milestones complete plus a sixth refinement milestone. The system is deploy
- **Article versioning** — Pre-overwrite snapshots with pipeline metadata (prompt SHA-256 hashes, model config) for benchmarking extraction quality across prompt iterations.
- **Review queue UI** — Admin interface for approving/editing/rejecting extracted key moments.
- **Search-first web UI** — Dark theme with cyan accents. Semantic search (Qdrant), topic/creator browse pages, technique detail pages with signal chain blocks and video source metadata.
-- **Docker Compose deployment** — API, worker, web, PostgreSQL, Redis, Qdrant, Ollama on ub01 with bind mounts at `/vmPool/r/services/chrysopedia_*`.
+- **Docker Compose deployment** — API, worker, web, watcher, PostgreSQL, Redis, Qdrant, Ollama on ub01 with bind mounts at `/vmPool/r/services/chrysopedia_*`.
+- **Transcript folder watcher** — Standalone service monitors `/vmPool/r/services/chrysopedia_watch/` for new transcript JSON files, validates structure, POSTs to ingest API. Processed files move to `processed/`, failures to `failed/` with `.error` sidecar. Uses watchdog PollingObserver for ZFS reliability.
- **Prompt template system** — Editable per-stage prompt files with SHA-256 tracking for reproducibility.
- **Pipeline admin dashboard** — Admin page at `/admin/pipeline` with video list, status badges, retrigger/revoke controls, expandable event log with token usage, collapsible JSON response viewer, and live worker status indicator. Pipeline events instrumented via `PipelineEvent` model (stage, event_type, token counts, JSONB payload).
- **Technique page 2-column layout** — Prose content (summary + study guide) on left, sidebar (key moments, signal chains, plugins, related techniques) on right at desktop widths. Sticky sidebar. Collapses to single column at ≤768px.
diff --git a/.gsd/milestones/M007/M007-ROADMAP.md b/.gsd/milestones/M007/M007-ROADMAP.md
index fd4e585..fb56c0a 100644
--- a/.gsd/milestones/M007/M007-ROADMAP.md
+++ b/.gsd/milestones/M007/M007-ROADMAP.md
@@ -8,7 +8,7 @@ Make the pipeline fully transparent — every LLM call's full prompt and respons
|----|-------|------|---------|------|------------|
| S01 | Pipeline Debug Mode — Full LLM I/O Capture and Token Accounting | medium | — | ✅ | Toggle debug mode on for a video, trigger pipeline, see full prompt + response stored for every LLM call with per-stage token breakdown |
| S02 | Debug Payload Viewer — Inline View, Copy, and Export in Admin UI | low | S01 | ✅ | Admin clicks an LLM call event, sees full prompt and response in a readable viewer, copies to clipboard, or downloads as JSON |
-| S03 | Transcript Folder Watcher — Auto-Ingest Service | medium | — | ⬜ | Drop a transcript JSON into the watch folder on ub01 → file is detected, validated, POSTed to /ingest, pipeline triggers automatically |
+| S03 | Transcript Folder Watcher — Auto-Ingest Service | medium | — | ✅ | Drop a transcript JSON into the watch folder on ub01 → file is detected, validated, POSTed to /ingest, pipeline triggers automatically |
| S04 | Admin UX Audit — Prune, Streamline, and Polish | low | S02 | ⬜ | Admin pipeline page is cleaner and more efficient for daily content management. Dead UI elements removed. Workflow improvements visible. |
| S05 | Key Moment Card Text Overflow Fix | low | — | ⬜ | Key moment cards with long filenames and timestamp ranges display cleanly — text truncated with ellipsis or wrapped within card bounds, no horizontal bleed |
| S06 | Mobile Viewport Overflow Fix — Technique Pages and Global Content | low | S05 | ⬜ | Technique page on Galaxy S25 Ultra renders cleanly — body text wraps, tags don't overflow, metadata stays within viewport, no horizontal scroll |
diff --git a/.gsd/milestones/M007/slices/S03/S03-SUMMARY.md b/.gsd/milestones/M007/slices/S03/S03-SUMMARY.md
new file mode 100644
index 0000000..c81b2a5
--- /dev/null
+++ b/.gsd/milestones/M007/slices/S03/S03-SUMMARY.md
@@ -0,0 +1,93 @@
+---
+id: S03
+parent: M007
+milestone: M007
+provides:
+ - chrysopedia-watcher Docker service running on ub01
+ - Auto-ingest via file drop to /vmPool/r/services/chrysopedia_watch/
+ - backend/watcher.py standalone script
+requires:
+ []
+affects:
+ []
+key_files:
+ - backend/watcher.py
+ - backend/requirements.txt
+ - docker-compose.yml
+key_decisions:
+ - Used httpx sync client in watcher (runs in synchronous watchdog callback threads)
+ - PollingObserver over inotify Observer for ZFS/NFS reliability
+ - os.kill(1,0) healthcheck instead of pgrep for slim Python images
+ - SCP'd files to ub01 directly instead of git push to avoid outward-facing GitHub action
+ - Ignored files in processed/ and failed/ subdirs to prevent re-processing loops
+patterns_established:
+ - Standalone watcher service pattern: watchdog PollingObserver + file stability check + validate + POST + disposition (processed/failed with error sidecar)
+ - Reusing Dockerfile.api with command override for lightweight companion services
+observability_surfaces:
+ - docker logs chrysopedia-watcher — shows all watcher activity (pickup, stability wait, validation, POST result, file moves)
+ - .error sidecar files in failed/ directory contain HTTP status, response body, or exception traceback
+drill_down_paths:
+ - .gsd/milestones/M007/slices/S03/tasks/T01-SUMMARY.md
+ - .gsd/milestones/M007/slices/S03/tasks/T02-SUMMARY.md
+duration: ""
+verification_result: passed
+completed_at: 2026-03-30T19:26:20.063Z
+blocker_discovered: false
+---
+
+# S03: Transcript Folder Watcher — Auto-Ingest Service
+
+**Built and deployed a watchdog-based folder watcher service that auto-ingests transcript JSON files dropped into a monitored directory on ub01, replacing manual curl/upload for pipeline input.**
+
+## What Happened
+
+This slice delivered a new Docker service (`chrysopedia-watcher`) that monitors `/vmPool/r/services/chrysopedia_watch/` on ub01 for new transcript JSON files and automatically POSTs them to the ingest API.
+
+**T01** built `backend/watcher.py` — a standalone Python script using watchdog's `PollingObserver` (chosen over inotify for ZFS/NFS reliability). The watcher handles file stability detection (waits for size to stabilize over 2 seconds to handle partial SCP/rsync writes), validates JSON structure against required keys (`source_file`, `creator_folder`, `duration_seconds`, `segments`), POSTs valid files as multipart upload via httpx to `/api/v1/ingest`, and moves files to `processed/` or `failed/` subdirectories. Failed files get an `.error` sidecar with the failure details (HTTP status, response body, or exception traceback). Files inside `processed/` and `failed/` subdirectories are ignored to prevent re-processing loops.
+
+**T02** wired the watcher into the Docker Compose stack as `chrysopedia-watcher`, reusing the existing `Dockerfile.api` image with a command override. The healthcheck was changed from `pgrep` (unavailable in slim Python image) to `python -c "import os; os.kill(1, 0)"`. Deployed to ub01 and verified end-to-end: valid transcript JSON auto-ingested (HTTP 200, file moved to `processed/`), invalid JSON moved to `failed/` with `.error` sidecar.
+
+## Verification
+
+All slice-level verification checks pass:
+1. `python -m py_compile backend/watcher.py` — exits 0
+2. `grep -q 'watchdog' backend/requirements.txt` — exits 0
+3. `grep -q 'PollingObserver' backend/watcher.py` — exits 0
+4. `grep -q 'processed' backend/watcher.py && grep -q 'failed' backend/watcher.py` — exits 0
+5. `ssh ub01 "docker ps --filter name=chrysopedia-watcher --format '{{.Status}}'"` — shows "Up N minutes (healthy)"
+6. End-to-end valid JSON ingestion verified on ub01 (HTTP 200, file in processed/)
+7. End-to-end invalid JSON handling verified on ub01 (file in failed/ with .error sidecar)
+
+## Requirements Advanced
+
+- R002 — Transcript ingestion now automated via folder watcher — no manual curl/upload needed
+
+## Requirements Validated
+
+None.
+
+## New Requirements Surfaced
+
+None.
+
+## Requirements Invalidated or Re-scoped
+
+None.
+
+## Deviations
+
+Healthcheck changed from `pgrep -f watcher.py` to `python -c "import os; os.kill(1, 0)"` because procps is not available in the slim Python Docker image.
+
+## Known Limitations
+
+None.
+
+## Follow-ups
+
+None.
+
+## Files Created/Modified
+
+- `backend/watcher.py` — New standalone folder watcher script using watchdog PollingObserver
+- `backend/requirements.txt` — Added watchdog>=4.0,<5.0 dependency
+- `docker-compose.yml` — Added chrysopedia-watcher service definition
diff --git a/.gsd/milestones/M007/slices/S03/S03-UAT.md b/.gsd/milestones/M007/slices/S03/S03-UAT.md
new file mode 100644
index 0000000..410c0db
--- /dev/null
+++ b/.gsd/milestones/M007/slices/S03/S03-UAT.md
@@ -0,0 +1,97 @@
+# S03: Transcript Folder Watcher — Auto-Ingest Service — UAT
+
+**Milestone:** M007
+**Written:** 2026-03-30T19:26:20.063Z
+
+## UAT: Transcript Folder Watcher — Auto-Ingest Service
+
+### Preconditions
+- Chrysopedia stack running on ub01 (`docker ps --filter name=chrysopedia` shows all services healthy)
+- Watch folder exists: `/vmPool/r/services/chrysopedia_watch/`
+- Watcher container running: `docker ps --filter name=chrysopedia-watcher` shows healthy
+
+---
+
+### Test 1: Valid Transcript Auto-Ingest (Happy Path)
+
+1. SSH to ub01
+2. Create a valid transcript JSON file:
+ ```bash
+ cat > /vmPool/r/services/chrysopedia_watch/uat_valid.json << 'EOF'
+ {
+ "source_file": "uat_test_video.mp4",
+ "creator_folder": "UAT Creator",
+ "duration_seconds": 120.5,
+ "segments": [{"start": 0.0, "end": 5.0, "text": "Hello world"}]
+ }
+ EOF
+ ```
+3. Wait 10 seconds for watcher to detect and process
+4. **Expected:** `docker logs chrysopedia-watcher --tail 20` shows:
+ - File pickup log line mentioning `uat_valid.json`
+ - Stability check pass
+ - POST to ingest API with HTTP 200
+ - File moved to processed/
+5. **Expected:** `ls /vmPool/r/services/chrysopedia_watch/processed/uat_valid.json` — file exists
+6. **Expected:** `ls /vmPool/r/services/chrysopedia_watch/uat_valid.json` — file no longer in root
+
+### Test 2: Invalid JSON File Handling
+
+1. Create an invalid JSON file (missing required keys):
+ ```bash
+ echo '{"foo": "bar"}' > /vmPool/r/services/chrysopedia_watch/uat_invalid.json
+ ```
+2. Wait 10 seconds
+3. **Expected:** `ls /vmPool/r/services/chrysopedia_watch/failed/uat_invalid.json` — file exists
+4. **Expected:** `cat /vmPool/r/services/chrysopedia_watch/failed/uat_invalid.json.error` — contains validation error mentioning missing keys
+5. **Expected:** File no longer in root watch folder
+
+### Test 3: Malformed JSON (Parse Error)
+
+1. Create a file with broken JSON:
+ ```bash
+ echo 'not json at all' > /vmPool/r/services/chrysopedia_watch/uat_broken.json
+ ```
+2. Wait 10 seconds
+3. **Expected:** File moved to `failed/uat_broken.json` with `.error` sidecar containing JSON parse error
+
+### Test 4: Non-JSON File Ignored
+
+1. Create a non-JSON file:
+ ```bash
+ echo 'hello' > /vmPool/r/services/chrysopedia_watch/readme.txt
+ ```
+2. Wait 10 seconds
+3. **Expected:** File remains in root watch folder — not moved to processed/ or failed/
+4. **Expected:** No log entry in watcher for this file
+
+### Test 5: File Stability (Simulated Partial Write)
+
+1. Start writing a large file slowly:
+ ```bash
+ (echo '{"source_file":"test.mp4",' ; sleep 3 ; echo '"creator_folder":"Test","duration_seconds":60,"segments":[]}') > /vmPool/r/services/chrysopedia_watch/uat_slow.json
+ ```
+2. **Expected:** Watcher waits for file stability before processing (logs show stability check)
+3. **Expected:** File ultimately processed (valid JSON) and moved to processed/
+
+### Test 6: Watcher Survives API Downtime
+
+1. Stop the API: `docker stop chrysopedia-api`
+2. Drop a valid JSON file into watch folder
+3. **Expected:** File moves to `failed/` with `.error` sidecar containing connection error
+4. Restart the API: `docker start chrysopedia-api`
+5. **Expected:** Watcher continues running (check `docker ps --filter name=chrysopedia-watcher`)
+
+### Cleanup
+```bash
+rm -f /vmPool/r/services/chrysopedia_watch/processed/uat_*.json
+rm -f /vmPool/r/services/chrysopedia_watch/failed/uat_*.json
+rm -f /vmPool/r/services/chrysopedia_watch/failed/uat_*.json.error
+rm -f /vmPool/r/services/chrysopedia_watch/readme.txt
+```
+
+### Operational Readiness (Q8)
+- **Health signal:** `docker ps --filter name=chrysopedia-watcher` shows healthy; `docker logs chrysopedia-watcher --tail 5` shows recent activity or idle polling
+- **Failure signal:** Container exits or becomes unhealthy; `.error` sidecar files accumulate in `failed/` directory
+- **Recovery procedure:** `docker compose restart chrysopedia-watcher` — no state to lose, watcher re-scans on startup
+- **Monitoring gaps:** No alerting on failed/ file accumulation; no metrics endpoint. Admin must manually check `failed/` directory or watcher logs.
diff --git a/.gsd/milestones/M007/slices/S03/tasks/T02-VERIFY.json b/.gsd/milestones/M007/slices/S03/tasks/T02-VERIFY.json
new file mode 100644
index 0000000..342e1b0
--- /dev/null
+++ b/.gsd/milestones/M007/slices/S03/tasks/T02-VERIFY.json
@@ -0,0 +1,9 @@
+{
+ "schemaVersion": 1,
+ "taskId": "T02",
+ "unitId": "M007/S03/T02",
+ "timestamp": 1774898679946,
+ "passed": true,
+ "discoverySource": "none",
+ "checks": []
+}
diff --git a/.gsd/milestones/M007/slices/S04/S04-PLAN.md b/.gsd/milestones/M007/slices/S04/S04-PLAN.md
index 7533e08..66a2da5 100644
--- a/.gsd/milestones/M007/slices/S04/S04-PLAN.md
+++ b/.gsd/milestones/M007/slices/S04/S04-PLAN.md
@@ -1,6 +1,29 @@
# S04: Admin UX Audit — Prune, Streamline, and Polish
-**Goal:** Audit all admin pages (Pipeline, Reports, Review). Remove unhelpful elements. Improve pipeline page layout for the primary workflow: check status, investigate issues, retrigger. Add clear visual hierarchy for the content management workflow.
+**Goal:** Admin pipeline page is cleaner and more efficient for daily content management. Debug mode toggle, status filter, workflow streamlining, and dead UI removal all deployed.
**Demo:** After this: Admin pipeline page is cleaner and more efficient for daily content management. Dead UI elements removed. Workflow improvements visible.
## Tasks
+- [x] **T01: Added DebugModeToggle component and StatusFilter pill bar to AdminPipeline page with API client functions for debug mode GET/PUT endpoints** — Add two new interactive features to AdminPipeline.tsx: (1) a debug mode toggle in the page header that reads/writes via GET/PUT /admin/pipeline/debug-mode, and (2) a status filter pill bar that filters the video list client-side by processing_status.
+
+Steps:
+1. Add `fetchDebugMode()` and `setDebugMode(enabled: boolean)` functions to `public-client.ts`. The backend endpoints are `GET /admin/pipeline/debug-mode` → `{enabled: boolean}` and `PUT /admin/pipeline/debug-mode` with body `{enabled: boolean}`.
+2. In AdminPipeline.tsx, add state for `debugMode` (boolean) and `debugLoading` (boolean). Fetch debug mode on mount. Add a toggle switch in the header-right area next to WorkerStatus — use the same visual pattern as the ModeToggle component on ReviewQueue (a labeled toggle with on/off state). Clicking the toggle calls `setDebugMode(!debugMode)` then updates local state.
+3. Add a status filter pill bar below the page header. Extract unique statuses from the `videos` array. Add `activeFilter` state (string | null, default null = show all). Render filter pills for each status plus an 'All' pill. Filter the `videos` array before rendering when `activeFilter` is set. Use the existing `filter-tab` / `filter-tab--active` CSS classes from ReviewQueue.
+4. Add CSS for the debug mode toggle (`.debug-toggle`, `.debug-toggle__switch`, `.debug-toggle__label`) in App.css. Style it to match the dark theme using existing `var(--color-*)` custom properties.
+5. Verify: Docker build succeeds, toggle visible and functional, filter pills render and filter correctly.
+ - Estimate: 45m
+ - Files: frontend/src/api/public-client.ts, frontend/src/pages/AdminPipeline.tsx, frontend/src/App.css
+ - Verify: ssh ub01 "cd /vmPool/r/repos/xpltdco/chrysopedia && docker compose build chrysopedia-web" succeeds with exit 0
+- [ ] **T02: Prune dead UI, rename view toggle, add debug indicator on trigger, add review queue cross-link** — Clean up the pipeline page: remove low-value UI elements, rename confusing labels, add debug mode context to trigger button, and add cross-navigation to review queue.
+
+Steps:
+1. In EventLog component, rename 'Head'/'Tail' buttons to 'Oldest first'/'Newest first'. The buttons are in the `.pipeline-events__view-toggle` div. Just change the button text.
+2. In EventLog component, remove the duplicate '↻ Refresh' button from `.pipeline-events__header`. The page-level refresh in the main header is sufficient.
+3. In the expanded video detail section (the `pipeline-video__detail` div), remove the `ID: {video.id.slice(0, 8)}…` span. Keep Created and Updated dates.
+4. In the video header actions area, add a '(debug)' indicator next to the Trigger button when debug mode is active. Pass `debugMode` state from the parent AdminPipeline component down to or alongside the trigger button. When debugMode is true, the trigger button text changes to '▶ Trigger (debug)' to indicate the run will capture full LLM I/O.
+5. Add a cross-link button/icon on each pipeline video card that navigates to the review queue filtered by that video. Use `` or React Router Link with the video's creator name as context. A small '→ Moments' link in the video meta area works. Since the review queue doesn't currently support URL-based video filtering, just link to `/admin/review` with the video filename as a visual cue.
+6. Verify: Docker build succeeds, view toggle says 'Oldest first'/'Newest first', no event-level refresh button, no event ID in detail, debug indicator on trigger when debug mode is on, cross-link visible.
+ - Estimate: 30m
+ - Files: frontend/src/pages/AdminPipeline.tsx, frontend/src/App.css
+ - Verify: ssh ub01 "cd /vmPool/r/repos/xpltdco/chrysopedia && docker compose build chrysopedia-web" succeeds with exit 0
diff --git a/.gsd/milestones/M007/slices/S04/S04-RESEARCH.md b/.gsd/milestones/M007/slices/S04/S04-RESEARCH.md
new file mode 100644
index 0000000..41d4bc6
--- /dev/null
+++ b/.gsd/milestones/M007/slices/S04/S04-RESEARCH.md
@@ -0,0 +1,101 @@
+# S04 Research: Admin UX Audit — Prune, Streamline, and Polish
+
+## Summary
+
+This is a **light research** slice — straightforward UX cleanup using established patterns in an already well-structured codebase. The admin pages (Pipeline, Reports, Review) are functional but have accumulated some gaps and could benefit from streamlining for the single-admin daily workflow.
+
+## Recommendation
+
+Three tasks: (1) Add debug mode toggle + video status filter to Pipeline page, (2) Cross-link admin pages and add workflow shortcuts, (3) CSS/UX polish pass. All tasks are independent and could run in parallel.
+
+## Implementation Landscape
+
+### Admin Pages Inventory (canonical on ub01)
+
+| Page | File | Lines | Role |
+|------|------|-------|------|
+| Pipeline Management | `frontend/src/pages/AdminPipeline.tsx` | 535 | Video list, status, trigger/revoke, event log, worker status, debug payload viewer |
+| Content Reports | `frontend/src/pages/AdminReports.tsx` | 246 | User-submitted issue reports, triage workflow |
+| Review Queue | `frontend/src/pages/ReviewQueue.tsx` | 189 | Key moment review with stats, filters, pagination |
+| Moment Detail | `frontend/src/pages/MomentDetail.tsx` | 454 | Single moment review with approve/reject/edit/split/merge |
+
+### Supporting Components
+
+| Component | File | Lines | Used By |
+|-----------|------|-------|---------|
+| AdminDropdown | `components/AdminDropdown.tsx` | 73 | App.tsx header nav |
+| ModeToggle | `components/ModeToggle.tsx` | 59 | ReviewQueue.tsx |
+| AppFooter | `components/AppFooter.tsx` | 47 | App.tsx |
+| StatusBadge | `components/StatusBadge.tsx` | 19 | ReviewQueue, MomentDetail |
+| ReportIssueModal | `components/ReportIssueModal.tsx` | 135 | TechniquePage.tsx only |
+
+### API Clients
+
+- `api/client.ts` — Review queue endpoints only (BASE = `/api/v1/review`)
+- `api/public-client.ts` — Everything else: search, techniques, creators, topics, pipeline admin, reports
+
+Both have independent `request