([], 100)],
+ hasNextPage: true,
+ fetchNextPage
+ })
+ );
+ render(TracksOverflow);
+ await fireEvent.click(screen.getByRole('button', { name: /load more/i }));
+ expect(fetchNextPage).toHaveBeenCalledTimes(1);
+ });
+
+ test('empty q shows a prompt', () => {
+ state.pageUrl = new URL('http://localhost/search/tracks');
+ render(TracksOverflow);
+ expect(screen.getByText(/no query/i)).toBeInTheDocument();
+ });
+});
+```
+
+- [ ] **Step 10: Run, confirm fail**
+
+Run: `cd web && npm test -- 'src/routes/search/tracks/tracks.test.ts'`
+Expected: FAIL.
+
+- [ ] **Step 11: Implement `web/src/routes/search/tracks/+page.svelte`**
+
+```svelte
+
+
+
+
+
+ {#if !q}
+
No query — return to search to start typing.
+ {:else if query?.isError}
+
+ {:else if showSkeleton.value && tracks.length === 0}
+
+ {:else if tracks.length === 0}
+
No track matches.
+ {:else}
+
+ {#each tracks as t, i (t.id)}
+
+ {/each}
+
+ {#if query?.hasNextPage}
+
query.fetchNextPage()}
+ >
+ {query.isFetchingNextPage ? 'Loading…' : 'Load more'}
+
+ {/if}
+ {/if}
+
+```
+
+- [ ] **Step 12: Run, confirm pass**
+
+Run: `cd web && npm test -- 'src/routes/search/tracks/tracks.test.ts'`
+Expected: PASS — 4 tests.
+
+- [ ] **Step 13: Type-check**
+
+Run: `cd web && npm run check`
+Expected: `0 ERRORS 0 WARNINGS`.
+
+- [ ] **Step 14: Commit**
+
+```bash
+git add web/src/routes/search/artists web/src/routes/search/albums web/src/routes/search/tracks
+git commit -m "feat(web): add three search overflow pages
+
+/search/artists, /search/albums, /search/tracks each read ?q= and run
+their own createSearchInfiniteQuery (limit=50). Load more pulls the
+next offset; back link returns to /search?q=. Empty q shows a 'no
+query' prompt and fires no request. Tracks overflow uses the onPlay
+prop on TrackRow to call playRadio(track.id)."
+```
+
+---
+
+## Task 12: Final verification + branch finish
+
+**Files:** none (verification only).
+
+- [ ] **Step 1: Full Go suite**
+
+Run: `go test -short -race ./...`
+Expected: PASS.
+
+- [ ] **Step 2: Go linter**
+
+Run: `golangci-lint run ./...`
+Expected: clean.
+
+- [ ] **Step 3: Web check + full tests + build**
+
+Run: `cd web && npm run check && npm test && npm run build`
+Expected: check 0/0; all vitest files pass; build emits `web/build/`.
+
+Run: `git checkout -- web/build/index.html`
+Expected: committed placeholder restored.
+
+- [ ] **Step 4: Docker build smoke**
+
+Run: `docker build -t minstrel:search-smoke .`
+Expected: image builds.
+
+Run: `docker run --rm --entrypoint /bin/sh minstrel:search-smoke -c 'test -x /usr/local/bin/minstrel && echo ok'`
+Expected: `ok`.
+
+- [ ] **Step 5: Local end-to-end manual check**
+
+Bring up the stack:
+
+```bash
+docker compose up --build -d
+```
+
+In a browser at `localhost:4533`:
+
+1. Sign in. Header now has a search box between brand and the user-menu.
+2. From the home page, type `mil` (or any string with multiple library matches). After ~250ms the URL becomes `/search?q=mil` and three sections appear (or fewer if some facets match nothing).
+3. Empty matches: type `zzzqqq` — page shows "No matches for 'zzzqqq'".
+4. Backspace through the input until empty: URL drops `?q=`; main `/search` page shows "Start typing".
+5. Click the `+` on a track result → bottom-bar player updates queue length but does NOT auto-play (or, if a track was already playing, current playback continues).
+6. Click a track result (the row, not the `+`) → radio plays (one-track queue, since it's the M6 stub).
+7. Click the `+` on an album card → all of that album's tracks are appended; current playback continues.
+8. Click "See all 47 artists →" → overflow page loads; "Load more" pulls the next page; back link returns to `/search?q=…`.
+9. Click an artist on `/search?q=…` → navigates to `/artists/:id`. The header search box still shows "mil".
+10. Browser back → returns to `/search?q=mil` with cached results.
+11. Press Escape with the input focused → input clears and URL clears.
+
+Tear down:
+
+```bash
+docker compose down
+```
+
+- [ ] **Step 6: Finish the branch**
+
+Follow `superpowers:finishing-a-development-branch`. Expected path: Option 2 — push `dev`, open a PR to `main`, wait for Forgejo CI, merge once green.
+
+---
+
+## Self-Review Notes
+
+**Spec coverage:**
+- Goal / non-goals → Task structure honors the non-goals (no autocomplete, no Cmd-K, no recent searches).
+- Header search bar → Task 7 (component) + Task 9 (Shell wiring).
+- Live debounced URL sync → Task 7 with debounce/replaceState/Escape behavior covered by tests.
+- `/search` page with three sections + see-all links → Task 10.
+- Three overflow pages → Task 11.
+- `/api/radio` stub → Task 1 (Go handler + 5 tests covering 200/404/missing/blank/bad-uuid).
+- `enqueueTrack` / `enqueueTracks` / `playRadio` → Task 3.
+- TrackRow `` refactor + `onPlay` + `+ queue` → Task 5.
+- AlbumCard `+ queue` → Task 6.
+- States (empty/loading/error/no-results) → covered in Tasks 10 and 11.
+- Component tests → each task has its own. Manual end-to-end → Task 12 Step 5.
+- Risks → addressed in spec; the TrackRow refactor risk is mitigated by re-running the album-page tests in Task 5 Step 5.
+
+**Type consistency:**
+- `SearchResponse`, `RadioResponse`, `Page`, `ArtistRef`, `AlbumRef`, `TrackRef`, `AlbumDetail` — defined once in `types.ts`; consumed identically in queries, store, and pages.
+- `playRadio(seedTrackId: string)` — same signature in spec, store, and call sites.
+- `enqueueTrack(t: TrackRef)`, `enqueueTracks(ts: TrackRef[])` — consistent.
+- `qk.search`, `qk.searchArtists`, `qk.searchAlbums`, `qk.searchTracks` — consistent across queries.ts and tests.
+- `onPlay?: (tracks: TrackRef[], index: number) => void` — same shape in `TrackRow.svelte`, search page, and tracks overflow page.
+- `SEARCH_SUMMARY_LIMIT = 10` and `SEARCH_FACET_PAGE_SIZE = 50` — used only inside `queries.ts`; not surfaced to UI tests.
+
+**Filename hazards:**
+- No `+`-prefixed test files under route dirs. Search route tests use plain names: `search.test.ts`, `artists.test.ts`, `albums.test.ts`, `tracks.test.ts`.
+- `SearchInput.test.ts` is plain `.test.ts` because the test body itself uses no rune syntax (only the imported component does); `.svelte.test.ts` is reserved for tests like `useDelayed` where the test body declares `$state`/`$effect.root` directly.
+
+**Placeholder scan:** no TBD/TODO/later markers. Every code block is complete; every command has expected output.
+
+**TrackRow refactor risk:** acknowledged; Task 5 re-runs the album page tests as Step 5 to catch any regression before commit.