diff --git a/.planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md b/.planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md
new file mode 100644
index 0000000..68adf3e
--- /dev/null
+++ b/.planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md
@@ -0,0 +1,303 @@
+# Phase 1: Stabilize Video Pipeline — Context
+
+**Gathered:** 2026-05-15
+**Status:** Ready for planning
+
+
+## Phase Boundary
+
+Stabilize the video-capture pipeline so the extension continuously records the
+operator's session into a ring buffer and produces a **playable** WebM on
+demand. The phase ends when:
+
+1. The video buffer is alive end-to-end with **one** authoritative source of
+ truth for the recorder code (no parallel offscreen implementations).
+2. The MediaRecorder lifecycle is correct (no shadowed bindings, predictable
+ start/stop).
+3. The buffer holds at most 30 s of footage at any moment AND concatenating
+ the retained chunks produces a file that `ffprobe -v error` accepts as
+ playable.
+4. State (the buffer itself) survives the MV3 service-worker idle unload
+ cycle without losing video.
+
+Scope anchor from ROADMAP.md — Phase 2 (DOM/event privacy), Phase 3 (export
+UX), Phase 4 (smoke verification), and Phase 5 (P1/P2 hardening) are
+explicitly out of scope here.
+
+
+
+
+## Implementation Decisions
+
+### Capture API — AMENDS DEC-003
+
+This phase REPLACES the SPEC-locked `chrome.tabCapture` choice with
+`getDisplayMedia()` capture. Done eyes-open: the operator gains broader
+capture coverage at the cost of the SPEC §1 "silent operation" property.
+The doc cascade is enumerated in the **Doc Amendments (precede code)**
+subsection below.
+
+- **D-01:** Capture mechanism is `navigator.mediaDevices.getDisplayMedia()`
+ invoked **inside the offscreen document**. No more
+ `chrome.tabCapture.getMediaStreamId`, no more SW-side gesture juggling.
+- **D-02:** Offscreen document is created with
+ `chrome.offscreen.Reason.DISPLAY_MEDIA` (replaces `USER_MEDIA`).
+- **D-03:** One-time source picker on session start; the operator picks
+ "screen" or "window" once. If they later click the Chrome "Stop sharing"
+ banner or the captured source disappears, the offscreen surfaces an error
+ to the SW and the popup re-prompts on next interaction. (Exact error-UX
+ copy is deferred to Phase 3 — see Deferred Ideas.)
+- **D-04:** Operator UX is **NOT** silent. Chrome's permanent "Sharing your
+ screen" indicator is shown while recording. We accept this as the cost
+ of the API choice.
+- **D-05:** `manifest.json` permissions follow the new API: `desktopCapture`
+ replaces `tabCapture`; `activeTab` becomes unnecessary for the video
+ pipeline but stays for `chrome.tabs.captureVisibleTab` (screenshot path,
+ Phase 3 concern — kept).
+
+### Offscreen source-of-truth location
+
+- **D-06:** Recorder code lives at **`src/offscreen/recorder.ts`** as a real
+ TypeScript module with strict type-check, source maps, and IDE support.
+- **D-07:** `offscreen/index.html` is rewritten to load the bundled module
+ via crxjs. The runtime path remains `offscreen/index.html` (referenced
+ from SW via `chrome.runtime.getURL('offscreen/index.html')`).
+- **D-08:** **DELETE** `offscreen/index.ts` (orphaned dead code) and the
+ entire `copy-offscreen` plugin block in `vite.config.ts:11-184`. crxjs
+ picks up the new TS entry through the HTML reference.
+
+### Ring-buffer mechanism
+
+- **D-09:** **Single continuous MediaRecorder** for the whole session.
+ `mediaRecorder.start(2000)` so chunks land on cluster boundaries per the
+ spec timeslice (DEC-003, SPEC §4.1). No restart strategy at this point.
+- **D-10:** Retain the **first emitted chunk** (the chunk produced by the
+ first `dataavailable` event after `start()`) **indefinitely** — it carries
+ the EBML header plus the initial cluster. CON-webm-header-retention.
+- **D-11:** Drop later chunks once they are older than 30 s, by chunk
+ arrival timestamp. Keep header + every chunk newer than `now - 30000 ms`.
+- **D-12:** Acceptance gate for Phase 1: `ffprobe -v error -f matroska -i
+ ` must return exit 0 with no decoder warnings on a
+ fresh-export sample. Plan-checker enforces this as a phase success
+ criterion.
+- **D-13:** **Fallback if D-12 fails:** revise the plan mid-phase to use
+ *restart-segments* (stop + restart the MediaRecorder every 10 s, keep
+ the 3 most-recent self-contained segments, concat on save). Documented
+ as a known fallback so the planner can pre-stage the alternative
+ structure in PLAN.md.
+
+### Tab-switch behavior
+
+- **D-14:** **Not applicable** under the new capture API. `getDisplayMedia()`
+ captures a screen or window, not a tab — there is nothing to re-attach
+ on `chrome.tabs.onActivated`. Phase 1 explicitly **removes** any
+ tab-switch handling from `src/background/index.ts`.
+- **D-15:** Operator switching tabs no longer interrupts the recording —
+ the buffer keeps filling regardless of active tab.
+
+### State survival across SW unload
+
+- **D-16:** Video buffer **ownership moves to the offscreen document**. The
+ offscreen survives SW unloads because it holds the
+ `DISPLAY_MEDIA`-reason capture; chunks accumulate there.
+- **D-17:** A long-lived `chrome.runtime.connect` port from offscreen → SW
+ serves as the keepalive (this is the only mechanism that actually
+ resets the SW idle timer — `chrome.alarms` callbacks do not, contrary
+ to DEC-010).
+- **D-18:** **DELETE** the `chrome.alarms` keepalive
+ (`src/background/index.ts:171-178`). DEC-010 and CON-service-worker-keepalive
+ are amended in the doc cascade below.
+- **D-19:** On export, SW requests the buffer from offscreen over the port
+ (or one-shot `chrome.runtime.sendMessage`). SW does **NOT** cache
+ chunks. CON-buffer-storage is honored — buffer is plain JS variable in
+ offscreen memory, no `chrome.storage.session`, no IndexedDB. The
+ existing IndexedDB code path in `vite.config.ts:43-104` is **DELETED**
+ along with the inline plugin.
+
+### Doc Amendments (precede code)
+
+These document edits **MUST** ship before any code-touching task in this
+phase, so downstream phases see a consistent baseline:
+
+- **D-A1:** Amend `.planning/intel/decisions.md` DEC-003 to record the
+ `getDisplayMedia` replacement, with rationale and the explicit silent-
+ operation trade-off. Amend DEC-010 to record port keepalive replacing
+ alarms keepalive.
+- **D-A2:** Amend `.planning/intel/constraints.md` to **RETIRE**
+ CON-tab-capture-binding and CON-service-worker-keepalive. Add new
+ CON-display-capture-binding (one-time picker, "Sharing" indicator).
+- **D-A3:** Amend `.planning/PROJECT.md` Key Decisions table (DEC-003,
+ DEC-010) and Constraints section accordingly.
+- **D-A4:** Amend `.planning/REQUIREMENTS.md` REQ-video-ring-buffer to
+ remove "active-tab" wording and update API binding.
+- **D-A5:** Amend `.planning/ROADMAP.md` Phase 1 description and Success
+ Criterion #2 (drop the "tab re-attach" clause).
+- **D-A6:** Amend `manifest.json`: swap `tabCapture` → `desktopCapture`
+ in `permissions`. Keep `activeTab` for the screenshot path.
+
+### Claude's Discretion
+
+- Exact protocol choice for offscreen↔SW messaging (port for keepalive +
+ sendMessage for one-shot vs port-only).
+- Codec strictness: enforce `video/webm; codecs=vp9` via
+ `MediaRecorder.isTypeSupported`; fail loud if unsupported (no fallback
+ chain — current code's vp9→vp8→h264→default fallback is removed).
+- Internal naming for the new buffer-owning module (offscreen-recorder vs
+ display-recorder etc.).
+- Code-style choices around TS strictness within `src/offscreen/`
+ (already on `"strict": true` per tsconfig).
+
+
+
+
+## Specific Ideas
+
+- "Fail loud" on missing `getDisplayMedia` support — log a clear error to
+ the SW console with the failing user-agent string. No silent fallback.
+- The ffprobe verification gate is a real CLI invocation in the plan's
+ acceptance, not a hand-wave — Phase 1 success requires producing a
+ sample WebM and running ffprobe against it.
+- The "stop sharing" recovery path: when the offscreen detects
+ `MediaStreamTrack.onended`, it should clear the buffer, send a
+ RECORDING_STOPPED message to SW, and stop attempting to record. The
+ popup will re-prompt next time the user clicks save (Phase 3
+ territory).
+- The `OFFSCREEN_READY` Message type is already declared in
+ `src/shared/types.ts:18` but never used. Phase 1 wires up an actual
+ handshake so SW doesn't fire `START_RECORDING` before the offscreen
+ listener is registered (this fixes audit item P1 #12 in passing).
+
+
+
+
+## Canonical References
+
+**Downstream agents MUST read these before planning or implementing.**
+
+### Project baseline (current state, will be amended by D-A1..D-A6)
+
+- `.planning/PROJECT.md` — overall project context. Note that DEC-003,
+ DEC-010, and the Constraints section will be amended by this phase.
+- `.planning/REQUIREMENTS.md` §"Video" — REQ-video-ring-buffer wording
+ changes in this phase.
+- `.planning/ROADMAP.md` §"Phase 1" — phase definition and success
+ criteria (Success Criterion #2 changes).
+- `.planning/intel/SYNTHESIS.md` — entry point for ingested intel.
+- `.planning/intel/decisions.md` §DEC-003, §DEC-009, §DEC-010.
+- `.planning/intel/constraints.md` §CON-video-window, §CON-video-codec,
+ §CON-webm-header-retention, §CON-tab-capture-binding (RETIRED),
+ §CON-service-worker-keepalive (RETIRED), §CON-buffer-storage.
+
+### Authoritative SPEC
+
+- `Тз расширение фаза1.md` §2 (stack), §4.1 (video buffer parameters),
+ §7 (manifest permissions), §8 row 3 (tab-capture binding — being
+ AMENDED), §10 acceptance criteria #2, #3, #7.
+
+### Audit
+
+- `/home/parf/.claude/plans/dear-claude-there-is-snazzy-fox.md` — full
+ P0 analysis. Key spots: P0-1 (offscreen duality + mediaRecorder
+ shadow, `vite.config.ts:113`/`vite.config.ts:208`), P0-2 (WebM ring
+ buffer + 200 ms timeslice issue), P0-3 (always-on capture), P1 #8
+ (alarms keepalive ineffective), P1 #12 (OFFSCREEN_READY handshake
+ missing).
+
+### Chrome APIs (read before researching)
+
+- MDN: `MediaDevices.getDisplayMedia()` — capture semantics, picker UX,
+ MediaStreamTrack.onended.
+- Chrome docs: `chrome.offscreen` API, especially `Reason.DISPLAY_MEDIA`
+ (Chrome 116+).
+- Chrome docs: Service worker lifecycle in MV3 — confirm `chrome.alarms`
+ vs `chrome.runtime.connect` port behavior with respect to idle timer.
+- WebM/Matroska EBML format — cluster boundary semantics; needed only if
+ the D-13 fallback (restart-segments) activates.
+
+
+
+
+## Existing Code Insights
+
+### Reusable assets
+
+- `src/background/index.ts:16-73` — `videoBuffer: VideoChunk[]` array +
+ `cleanupVideoBuffer()`. Structural pattern (header retention + age
+ trim) is **kept**; the implementation is **moved** to
+ `src/offscreen/recorder.ts`.
+- `src/shared/types.ts` — `Message`, `VideoChunk`, `VideoBufferResponse`,
+ `OFFSCREEN_READY`, `RECORDING_ERROR`. Reuse as-is; wire up
+ `OFFSCREEN_READY` and `RECORDING_ERROR` in Phase 1.
+- `src/shared/logger.ts` — `Logger` (SW context) and `ContentLogger`
+ (content-script context). Add an `OffscreenLogger` or reuse `Logger`
+ with prefix `[Offscreen:...]`.
+
+### Established patterns
+
+- Strict TS with `noUnusedLocals`/`noUnusedParameters` (`tsconfig.json`).
+ New code must comply — no `as any`, no `@ts-ignore` (audit item P1
+ #13 informs this).
+- Russian comments inline are acceptable per project provenance
+ (see `Тз расширение фаза1.md`, the user-facing UI strings in REQ-popup-ui).
+
+### Files to DELETE in this phase
+
+- `offscreen/index.ts` — dead code (audit P2 #18).
+- `offscreen/index.html` — REPLACED by a crxjs-managed equivalent
+ pointing at the new `src/offscreen/recorder.ts`. (Path stays the same
+ in dist; source moves.)
+- `vite.config.ts:11-184` — the entire `copy-offscreen` inline plugin
+ block including the embedded JS string + IndexedDB plumbing. Replaces
+ with a small note + the standard crx() invocation.
+- `src/background/index.ts:171-178` — `setupKeepalive()` and its
+ `chrome.alarms` registration.
+- `src/background/index.ts:445-475` — `loadChunkFromIndexedDB()` and
+ `openIndexedDB()` (the SW-side IDB plumbing).
+- `src/background/index.ts:432-442` — the `VIDEO_CHUNK` message handler
+ that expected Blob over `chrome.runtime.sendMessage` (the
+ fundamentally broken path from audit P0 #2 — never reachable today
+ but documented to delete).
+
+### Files to CREATE
+
+- `src/offscreen/recorder.ts` — new module owning getDisplayMedia +
+ MediaRecorder + ring buffer + keepalive port.
+- `src/offscreen/index.html` (or amended `offscreen/index.html`) —
+ minimal HTML that loads the new module via crxjs.
+
+### Files to MODIFY heavily
+
+- `src/background/index.ts` — owns offscreen lifecycle, port handling,
+ export-time buffer fetch. Most of the file shrinks.
+- `manifest.json` — permissions swap, `offscreen` already present.
+- `vite.config.ts` — collapse to a clean crxjs invocation, no inline
+ plugin.
+
+
+
+
+## Deferred Ideas
+
+- **Error UX for "user stopped sharing" mid-session.** The popup needs a
+ state for this — Phase 3 territory (REQ-popup-ui state machine
+ extension).
+- **Audio capture.** `getDisplayMedia()` makes audio capture trivial
+ (`audio: true`), but SPEC §9 explicitly excludes audio from Phase 1
+ (Phase 2 work — CAP-01). Capture this as an easier-now-than-before
+ follow-up.
+- **Per-tab silent capture mode** as an opt-in via `config.json`. Could
+ re-introduce tabCapture for installations that prioritize silent
+ operation over broad coverage. Future phase if there's demand.
+- **Cluster-aware EBML trim (ts-ebml).** Not needed for Phase 1 if
+ continuous + age-trim verifies via ffprobe. Keep on the shelf as a
+ third fallback under D-13.
+- **`chrome.storage.session` cold-start recovery.** Buffer pointer
+ rehydration after offscreen crash. Phase 5 (Harden + clean up)
+ territory.
+
+
+
+---
+
+*Phase: 01-stabilize-video-pipeline*
+*Context gathered: 2026-05-15*
diff --git a/.planning/phases/01-stabilize-video-pipeline/01-DISCUSSION-LOG.md b/.planning/phases/01-stabilize-video-pipeline/01-DISCUSSION-LOG.md
new file mode 100644
index 0000000..e6717f0
--- /dev/null
+++ b/.planning/phases/01-stabilize-video-pipeline/01-DISCUSSION-LOG.md
@@ -0,0 +1,82 @@
+# Phase 1: Stabilize Video Pipeline — 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-05-15
+**Phase:** 01-stabilize-video-pipeline
+**Areas discussed:** Offscreen source-of-truth location, Ring-buffer mechanism, Tab-switch behavior, State survival across SW unload
+
+---
+
+## Offscreen source-of-truth location
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| `src/offscreen/recorder.ts` as crxjs offscreen entry | Real TS module bundled by crxjs; HTML references the bundle; type-check + source maps; deletes both `offscreen/index.ts` and the inline `copy-offscreen` plugin | ✓ |
+| `src/offscreen/recorder.ts` via `web_accessible_resources` | Same source file, declared as WAR; reachable from `chrome.runtime.getURL()`; more boilerplate | |
+| Keep vite.config.ts inline string | Status quo — hard NO per audit; no type-check, no source maps, easy to lose | |
+
+**User's choice:** crxjs offscreen entry (Recommended)
+**Notes:** Aligns with the audit's primary structural recommendation. The inline plugin and dead `offscreen/index.ts` are both deleted in the same phase.
+
+---
+
+## Ring-buffer mechanism
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Single continuous MediaRecorder + age-based trim (`start(2000)`) | Spec-aligned timeslice; retain header forever, drop later chunks by age; verify with ffprobe; pivot to restart-segments if verification fails | ✓ |
+| Restart-segments every ~10 s | Each segment self-contained valid WebM; bulletproof playability; ~200–300 ms gap per restart (~2–3% loss); higher RAM (3 segments in flight) | (fallback) |
+| Cluster-aware EBML trim via ts-ebml | Theoretically optimal; ~80 KB dep + more test surface | (third fallback, in deferred) |
+| Chunk-arrival-time trim, 200 ms timeslice (status quo) | Current broken approach | |
+
+**User's choice:** "It seems that 2 is the most stable? But if you think 1 is better it is up to you."
+After follow-up: locked **Option 1** with **Option 2 as documented fallback**.
+**Notes:** Decision deferred to Claude after weighing the trade-offs; rationale captured in CONTEXT.md D-09..D-13. Plan-checker enforces ffprobe verification as the Phase 1 success gate; if it fails, the plan revision pivots to restart-segments without re-asking the user.
+
+---
+
+## Tab-switch behavior
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Hard restart (stop old recorder, start fresh on new tab) | SPEC §8-implied; spec-aligned | (rendered N/A by capture-API change) |
+| Cross-tab carry-over | Preserve prior tab's buffer until new fills; ~2× RAM during overlap | |
+| Per-tab buffers (one per recently-visited tab) | High RAM; risks CON-ram-ceiling | |
+| **(emergent option from user: capture whole browser / whole screen)** | Switch capture API to `getDisplayMedia()` / `chrome.desktopCapture`; tab-switch handling becomes N/A | ✓ |
+
+**User's choice:** "May we just record the whole browser? or the whole screen?" → confirmed in the follow-up question as **switch to chrome.desktopCapture / getDisplayMedia**.
+**Notes:** This is a project-level amendment to DEC-003 (capture API), not just a Phase 1 implementation detail. Trade-off was made explicit to the user before confirming: Chrome shows a permanent "Sharing your screen" banner (loses SPEC §1 "silent for operator" property), picker dialog required on session start. User accepted. Phase 1's plan therefore includes the doc-amendment cascade (D-A1..D-A6) BEFORE any code work, and removes all `chrome.tabs.onActivated`/`onUpdated` reattachment logic.
+
+---
+
+## State survival across SW unload
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Move buffer ownership to offscreen + long-lived port keepalive | Offscreen survives SW; port resets idle timer (only mechanism that does); buffer in plain offscreen memory; honors CON-buffer-storage | ✓ |
+| Same as above + chrome.storage.session belt-and-suspenders | Cold-start recovery on offscreen crash; borderline CON-buffer-storage compliance | (deferred to Phase 5) |
+| Keep SW-owned buffer, add proper keepalive port | Smallest diff; SW can still die under memory pressure | |
+| Accept buffer loss on SW unload | Don't fight lifecycle; violates "continuous" intent | |
+
+**User's choice:** "one seems no problem." — locked **Option 1**.
+**Notes:** `chrome.alarms` keepalive deleted (DEC-010 / CON-service-worker-keepalive amended). The `chrome.storage.session` belt-and-suspenders variant moved to Phase 5 deferred ideas. The SW-side IndexedDB code path in `vite.config.ts:43-104` and `src/background/index.ts:445-475` is deleted alongside.
+
+---
+
+## Claude's Discretion
+
+- Internal protocol choice between `chrome.runtime.connect` port and `chrome.runtime.sendMessage` for offscreen↔SW messaging (decision: port for keepalive, sendMessage for one-shot requests — captured in CONTEXT.md D-19).
+- Codec strictness — enforce vp9 only via `MediaRecorder.isTypeSupported`, fail loud if unavailable. No fallback chain (current code's vp9→vp8→h264→default is removed).
+- Internal module naming for the offscreen recorder.
+- Exact error-UX copy for "user stopped sharing" — deferred to Phase 3 (popup state machine territory).
+- Code-style choices within `src/offscreen/` (already constrained by `tsconfig` strict mode).
+
+## Deferred Ideas
+
+- **Audio capture.** `getDisplayMedia({ audio: true })` makes this trivial — but SPEC §9 excludes audio from Phase 1. Phase 2 work.
+- **Per-tab silent capture mode** (re-introducing `tabCapture` as an opt-in via `config.json`) for installations that prioritize silent operation over broad coverage.
+- **Cluster-aware EBML trim (ts-ebml)** — third-line fallback under D-13 if even restart-segments don't produce playable output. Currently parked.
+- **`chrome.storage.session` cold-start buffer-pointer recovery** — Phase 5 (Harden + clean up).
+- **Error UX for "user stops sharing"** — popup state extension, Phase 3.