diff --git a/docs/IMPROVEMENT_LOG.md b/docs/IMPROVEMENT_LOG.md
index 47160e3..19bc6c9 100644
--- a/docs/IMPROVEMENT_LOG.md
+++ b/docs/IMPROVEMENT_LOG.md
@@ -2,6 +2,37 @@
Dated entries from improvement cycles. Newest at top.
+## 2026-05-03 — Cycle 3: clip hardening + VOD filter + cancel-cross-talk fix
+
+Three independent improvements landed this cycle.
+
+### 1. `download-clip` IPC: integrity, cancellation, sanitization (server defensive)
+
+- **File**: `src/main.ts` — `download-clip` IPC handler, new `activeClipProcesses` map.
+- **Problem**: The handler reported `success: true` on streamlink exit code 0 even when the resulting file was empty / a few hundred bytes (Twitch occasionally returns a manifest with no segments). The path passed `clipInfo.broadcaster_name` straight to `path.join` — Twitch returns the broadcaster's *display* name, which can carry unicode, spaces, or punctuation that produced surprising directory layouts on Windows. The spawned streamlink process was tracked nowhere, so `window-all-closed` left it orphaned.
+- **Fix**: `safeBroadcaster` runs through `sanitizeFilenamePart`. `safeTitle` falls back to `clip` when the title sanitises to empty. The output filename now goes through `ensureUniqueFilename(path, clipId)` so retrying a clip with the same title doesn't overwrite the previous download. After streamlink exits, the file is rejected if smaller than 16 KiB or if `validateDownloadedFileIntegrity` fails (no video stream / unreadable). The proc is tracked in a new `activeClipProcesses` map and killed by `window-all-closed`.
+
+### 2. VOD list filter / search (client feature: VM/state + UI + persistence + keyboard)
+
+- **Files**: `src/renderer-streamers.ts`, `src/renderer.ts`, `src/renderer-texts.ts`, `src/index.html`, `src/renderer-locale-de.ts`, `src/renderer-locale-en.ts`.
+- **Problem**: A streamer can have hundreds of VODs (the test fixture alone has 37 cards). There was no way to find a specific VOD by title — only scroll. With a long archive this is genuinely painful.
+- **Fix**: Filter row above the VOD grid (`vodFilterInput`, clear button, match counter). State (`vodFilterQuery`) is persisted to `localStorage` via `loadPersistedVodFilter` / `persistVodFilter`, so the search bar survives an app restart. The render path was split: `renderVODs` now stores `lastLoadedVods` + `lastLoadedStreamer` and delegates to `renderVodGridFromCurrentState`, which applies `filterVodsByQuery` on every input event without re-fetching. Empty-state DOM is built via `setVodGridEmptyState` using `createElement` + `textContent` (no `innerHTML` for locale strings — defense-in-depth even though the strings are trusted). Keyboard: `Ctrl+F` / `Cmd+F` focuses the filter (only when the VODs tab is active and Electron's no-op default is suppressed); `Esc` clears the filter when the input has focus and content; `Esc` still closes modals first if any are open.
+
+### 3. Decouple `currentProcess` from queue downloads (server cleanup + race fix)
+
+- **File**: `src/main.ts` — global rename and assignment removal.
+- **Problem**: A single `currentProcess: ChildProcess | null` was shared by `cutVideo`, `mergeVideos`, `splitMergedFile`, AND `downloadVODPart`. With parallel downloads the global was constantly overwritten between siblings, but the cross-talk that mattered was different: if a queue download was running and the user kicked off a video cut, the cutter ffmpeg ran into the same global. Pressing the queue's *cancel-download* button then iterated `activeDownloads` (correct) AND called `currentProcess.kill()` (incorrect — that was the cutter ffmpeg by then), killing the unrelated cut.
+- **Fix**: `currentProcess` renamed to `currentEditorProcess` and confined to the editor pipeline (cutter / merger / splitter). `downloadVODPart` no longer assigns to it — `activeDownloads` is the sole source of truth for queue children. The fallback `if (currentProcess) currentProcess.kill()` was removed from `remove-from-queue`, `pause-download`, and `cancel-download`. `window-all-closed` still kills it (so a cutter ffmpeg gets cleaned up on app exit) and now also kills `activeClipProcesses` introduced by Pick 1.
+
+### Regression
+
+- `npm run build` — clean (TypeScript strict, 0 errors).
+- `npm run test:e2e:update-logic` — passed.
+- `npm run test:e2e` — passed (`issues: []`).
+- `npm run test:e2e:guide` — passed (`failures: []`).
+- `npm run test:merge-split` — passed.
+- `npm run test:e2e:full` — passed (`failures: []`, `runtimeIssues: []`; flows: language switch, queue add, duplicate prevention, runtime metrics, clip queue, pause/resume, retry, reorder, media cut/merge, update check).
+
## 2026-05-03 — Cycle 2: release pipeline + defensive parsing
Three independent improvements landed this cycle.
diff --git a/src/index.html b/src/index.html
index 56668cb..e502863 100644
--- a/src/index.html
+++ b/src/index.html
@@ -239,6 +239,11 @@