diff --git a/COMPLETED.md b/COMPLETED.md
index 3e8d430..7d689a1 100644
--- a/COMPLETED.md
+++ b/COMPLETED.md
@@ -381,6 +381,55 @@ Eliminated `!important` declarations from track card CSS by replacing MudBlazor
- Plain-div shell re-enables CSS isolation as an option (a `TrackCard.razor.css` would now work against the shell divs). Section 8's public-only scoping remains convenient; isolation is optional for future polish.
- Removes the structural mismatch of using a Material surface component (`MudCard`/`MudPaper`) solely as a layout shell. TrackCard now mirrors the construction of `NowPlayingCard` (plain divs + themed CSS).
+---
+
+## Track Detail Page (/track/{entryKey})
+
+**Status:** Landed on 2026-06-06 (branch `track-detail-page`, merged to dev).
+
+A focused, editorial single-track view in `DeepDrftPublic.Client`. The track gallery answers "what is in the library"; this page answers "tell me about *this* track" — full metadata, cover art, and a single prominent play affordance, styled to feel like a record-sleeve back-cover rather than a form. Link-only for now (reached from a gallery card / Now Playing), not a top-level nav entry.
+
+### Implemented solution
+
+**Components (`DeepDrftPublic.Client/Pages/`):**
+- `TrackDetail.razor` + `TrackDetail.razor.cs` — routed at `@page "/track/{EntryKey}"` with `@rendermode InteractiveWebAssembly`. Three render states (loading skeleton, loaded layout, 404 not-found) driven by `TrackDetailViewModel` flags. Cascades `IStreamingPlayerService` for play-affordance wiring. Subscribes to `PlayerService.StateChanged` to keep the play button label in sync with live transport state.
+
+**ViewModel (`DeepDrftPublic.Client/ViewModels/`):**
+- `TrackDetailViewModel` — scoped, registered in `Startup.ConfigureDomainServices`. Depends on `ITrackDataService` (render-mode-agnostic seam, existing). Properties: `Track` (loaded DTO), `IsLoading`, `NotFound`. Single `Load(entryKey)` command idempotent per route, fully resetting all three flags on each call to prevent stale track bleed on navigation.
+
+**DI registration (`DeepDrftPublic.Client/Startup.cs`):**
+- `TrackDetailViewModel` registered scoped.
+
+**UI layout:**
+1. Subtle back-link `← All tracks` to `/tracks`, muted low-emphasis text affordance.
+2. Large square cover art block — placeholder themed `MudPaper` with `Album` glyph when cover unavailable (default state pending 2.1 image-vault wiring); will display `
` once 2.1 lands.
+3. Title (TrackName, display-serif h3) / artist (h6, primary accent) masthead.
+4. Prominent **Play** button under masthead with state-reactive label ("Play" / "Pause" / "Resume" keyed to current track and playback state via `PlayerService` subscription).
+5. `MudDivider` separator.
+6. Optional-field metadata block (Album, Genre, ReleaseDate) — definition-row layout, rendered only if non-null; all three omit silently if unavailable.
+7. Skeleton loading state matching the loaded layout silhouette.
+8. 404 messaging on not-found.
+
+**CSS classes (`DeepDrftPublic/wwwroot/styles/deepdrft-styles.css` §14):**
+- `deepdrft-track-detail-container` — centered single column, max-width, auto-margins, vertical padding.
+- `deepdrft-track-detail-cover` — square aspect-ratio frame, rounded, subtle shadow/border (light/dark theme-aware), `overflow: hidden` for clean image crop.
+- `deepdrft-track-detail-masthead` — title/artist spacing, display-serif via existing `deepdrft-` font classes.
+- `deepdrft-track-detail-meta` — metadata block rhythm, small-caps muted labels.
+- `deepdrft-track-detail-back` — back-link affordance, muted color, hover treatment.
+
+**Inbound links wired (`DeepDrftShared.Client/Components/TrackCard.razor`):**
+- Cover block and title/artist are now `display:contents` anchors to `href="/track/{track.EntryKey}"`, making the entire card clickable to the detail page.
+- Play button on the card untouched (still functions independently for gallery playback).
+
+**Architecture notes:**
+- Render mode `InteractiveWebAssembly` (server prerender → WASM hydrate) mirrors `TracksView` consistency.
+- `TrackDetailViewModel` is scoped (per-instance), not singleton — navigating between `/track/A` and `/track/B` reuses the same scoped instance, so `Load` must fully reset state to prevent cross-navigation bleed.
+- Play button implements the same `PlayerService.StateChanged` subscription pattern as `TracksView` — mandatory for label coherence when the dock bar drives state.
+- Cover-art default (placeholder) is intentional and designed to be the resting state, not degraded; the page ships immediately without waiting for 2.1 image-vault wiring. Once 2.1 lands and `api/image/{entryKey}` exists on `DeepDrftContent`, the `
` binding swaps in without further component changes (the placeholder's `onerror` fallback ensures graceful degradation if a vault entry is missing).
+- Page is link-only navigation (not in the header `MenuPages`); reachability depends on inbound links from `TrackCard` and Now Playing surfaces, which were wired simultaneously.
+
+---
+
**Status:** Desktop AudioPlayerBar redesign landed on 2026-06-04.
### Desktop AudioPlayerBar — migrate to MudBlazor theme system
diff --git a/DeepDrftPublic.Client/CLAUDE.md b/DeepDrftPublic.Client/CLAUDE.md
index 5413ff8..96c8f9b 100644
--- a/DeepDrftPublic.Client/CLAUDE.md
+++ b/DeepDrftPublic.Client/CLAUDE.md
@@ -10,7 +10,7 @@ All interactive UI for the site. Blazor WebAssembly. Pages, controls, the stream
## Actual structure
-- `Pages/`: Routable components. `Home.razor` (hero/about), `TracksView.razor` (track gallery with pagination/sorting). **No demo pages** (`Counter.razor`, `Weather.razor` do not exist).
+- `Pages/`: Routable components. `Home.razor` (hero/about), `TracksView.razor` (track gallery with pagination/sorting), `TrackDetail.razor` (single-track detail view with cover, metadata, play affordance). **No demo pages** (`Counter.razor`, `Weather.razor` do not exist).
- `Layout/`: `MainLayout.razor` (root layout, wraps in `AudioPlayerProvider`, hosts theme switcher), `DeepDrftMenu.razor` (branded menu bar), `NavMenu.razor` (nav list), `Pages.cs` (centralised nav index — `MenuPages` for header, `AllPages` for exhaustive list).
- `Controls/`: Reusable components.
- `TrackCard.razor`: Individual track display (image, name, artist, album, genre, release date). Play/pause icon controlled via `IsPaused` parameter.
@@ -34,6 +34,7 @@ All interactive UI for the site. Blazor WebAssembly. Pages, controls, the stream
- `TrackMediaClient`: Content API. Uses named `IHttpClientFactory` client `"DeepDrft.Content"`. Methods like `GetAudioStreamAsync(trackId, offset)` → `Stream`.
- `ViewModels/`: Component state.
- `TracksViewModel`: Scoped. Holds current page, page size, sort column, descending flag. `SetPage(pageNumber)` calls `TrackClient.GetPageAsync` and updates. Registered in `Startup.ConfigureDomainServices`.
+ - `TrackDetailViewModel`: Scoped. Holds loaded track, loading flag, not-found flag. `Load(entryKey)` fetches via `ITrackDataService` and resets all flags per call (prevents cross-navigation bleed). Registered in `Startup.ConfigureDomainServices`.
- `Common/`: Shared utilities.
- `DarkModeSettings.cs`: `[PersistentState]`-annotated class (single source of truth for dark mode in the client). Registered scoped.
- `DDIcons.cs`: Hand-rolled SVG icons (gas-lamp lit/unlit for dark mode toggle).
diff --git a/PLAN.md b/PLAN.md
index a1a8779..41c08b3 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -204,116 +204,6 @@ A small set of items that are real but don't fit a phase yet. Surface them when
---
-## Track Detail Page (/track/{entryKey})
-
-A focused, editorial single-track view in `DeepDrftPublic.Client`. The track gallery answers "what is in the library"; this page answers "tell me about *this* track" — full metadata, cover art, and a single prominent play affordance, styled to feel like a record-sleeve back-cover rather than a form. Link-only for now (reached from a gallery card / Now Playing), not a top-level nav entry.
-
-**Status:** `[spec — not yet built]`. Spec is implementation-ready *except* for the cover-art dependency called out in §4 below, which forks on **2.1**.
-
-### Two facts that correct the originating brief
-
-The dispatch brief carried two assumptions that the current tree does not bear out. Both are load-bearing for whoever implements this:
-
-1. **There is no cover-art URL pattern yet.** The brief said to "confirm and spec the correct URL" for `api/track/{entryKey}/image` as if it existed. It does not. Per **2.1**, the image vault is unwired: `TrackEntity.ImagePath` is still a free-form URL string and no image read endpoint exists on `DeepDrftContent`. The page must therefore treat cover art as a *dependency on 2.1*, not a given. See §4 for the resolution and the graceful-degradation default that lets the page ship before 2.1 lands.
-
-2. **The page consumes `ITrackDataService`, not `TrackClient` directly.** `ITrackDataService.GetTrack(string trackId)` already exists and already delegates to `TrackClient.GetTrack(entryKey)` (`TrackClientDataService.cs:29`). It is the render-mode-agnostic seam — WASM hits it over HTTP, the SSR prerender pass can swap a direct implementation. The ViewModel must depend on `ITrackDataService`, mirroring `TracksViewModel`, so the prerender bridge in §6 actually fetches in-process during prerender rather than round-tripping. Calling `TrackClient` directly would couple the page to the WASM transport and defeat the persisted-state bridge.
-
-### 1. Route and render mode
-
-- **Route:** `@page "/track/{EntryKey}"`. `EntryKey` is a `[Parameter]` string; Blazor route matching is case-insensitive by convention. The segment is the FileDatabase entry key, matching `TrackClient.GetTrack`'s `api/track/meta/by-key/{entryKey}` lookup. The component must `Uri.UnescapeDataString` is **not** needed — Blazor decodes route segments before binding.
-- **Render mode:** `@rendermode InteractiveWebAssembly`, consistent with `TracksView` and the other public pages: server prerenders, WASM hydrates. This is what makes the §6 `PersistentComponentState` bridge necessary.
-
-### 2. ViewModel shape — `TrackDetailViewModel`
-
-Scoped, registered in `Startup.ConfigureDomainServices` alongside `TracksViewModel`. Depends on `ITrackDataService` (the existing seam), not `TrackClient`.
-
-Properties:
-- `TrackDto? Track` — the loaded track, `null` until the fetch resolves. Single source of truth for the view.
-- `bool IsLoading` — `true` from construction until the first load completes (pass or fail). Drives the skeleton.
-- `bool NotFound` — `true` when the load resolved but the result was a failure (404 or deserialize miss). Drives the error state. Kept distinct from `IsLoading` so the view has three clean states: loading / loaded / not-found.
-
-One async command:
-- `Task Load(string entryKey)` — calls `TrackData.GetTrack(entryKey)`. On `Success`, sets `Track` and clears `NotFound`; on failure, sets `NotFound = true` and leaves `Track` null. Sets `IsLoading = false` in a `finally`. Idempotent enough to be safe if called once per `OnInitializedAsync` (it is — see §6).
-
-Note the VM is keyed transiently: unlike `TracksViewModel` (which holds paging state across the session), this VM is per-track. Because it is scoped, navigating between two `/track/...` routes reuses the same instance — `Load` must fully overwrite all three properties each call so a stale `Track` never bleeds across a navigation. Reset `IsLoading = true` and `NotFound = false` at the *top* of `Load`.
-
-### 3. Component layout
-
-Editorial, not a property grid. Target feel: the back of a record sleeve. Single centered column on a generous max-width container (~720–820px), plenty of vertical rhythm, type doing most of the work.
-
-Top to bottom:
-
-1. **Back link.** A subtle, small `← All tracks` at the top-left of the content column, routing to `/tracks`. `MudLink` with low-emphasis styling (`Typo.body2`, muted color from the active palette). Not a button — a quiet text affordance.
-
-2. **Cover art block.** A large square (responsive, ~320–400px, centered). When art resolves (§4), an `
` inside a `deepdrft-track-detail-cover` frame. When it does not (null `ImagePath`, or 2.1 not yet wired), a themed placeholder — a `MudPaper` with a centered low-emphasis `Icons.Material.Filled.Album` glyph, *not* a broken image and *not* the word "no image". The placeholder is the default state today, so it must look intentional.
-
-3. **Title / artist masthead.** `TrackName` as the hero — `MudText Typo="Typo.h3"` (or `h2`), in the display serif the theme already loads (Bodoni Moda / Cormorant via the `deepdrft-` type classes). `Artist` directly beneath, `Typo.h6`, lighter weight, `Color="Color.Primary"` or a muted secondary. These two are never null, so they always render.
-
-4. **Play affordance.** Immediately under the masthead — see §5. Prominent; this is the page's primary action.
-
-5. **`MudDivider`** — a hairline rule separating the masthead from the metadata block, reinforcing the back-cover feel.
-
-6. **Metadata block.** The optional fields (`Album`, `Genre`, `ReleaseDate`), each rendered **only if non-null**. No "Unknown Album" placeholders — a null field omits its entire row. Two viable treatments; implementer picks one and stays consistent:
- - **Definition rows:** small-caps muted label (`Album` / `Genre` / `Released`) left, value right, one `MudText` row each. Editorial, calm.
- - **Chips:** `Genre` as a `MudChip` (it reads as a tag); `Album` and `ReleaseDate` as definition rows. Mixed, slightly livelier.
- Recommended: definition rows for all three for coherence, with `Genre` optionally promoted to a chip if the page feels too text-flat in review. `ReleaseDate` (a `DateOnly?`) formats as year-or-full-date — `ToString("MMMM yyyy")` reads editorially; avoid raw ISO.
-
-If *all three* optional fields are null, the metadata block and its preceding divider both collapse — the page is then just back-link / cover / masthead / play, which is still a complete, composed view. Guard the divider on "at least one optional field present."
-
-### 4. Cover-art URL resolution
-
-This is the spec's one real fork, because the endpoint does not exist yet (see correction #1).
-
-- **Default (ship-now) behavior:** treat `ImagePath` as not-displayable and render the §3.2 themed placeholder unconditionally. This lets the page ship before **2.1**. The placeholder is designed to be the resting state, not a degraded one.
-- **When 2.1 lands** (image vault wired, `GET api/image/{entryKey}` on `DeepDrftContent`, `ImagePath` semantics changed to "image vault entry key"): resolve cover art by composing the content-API base with the new endpoint — `api/image/{track.EntryKey}` against the `"DeepDrft.Content"` client's base address — and bind it to the `
`. This mirrors how audio is keyed: structured metadata over `"DeepDrft.API"`, binary over `"DeepDrft.Content"`. The `
` should still carry an `onerror` fallback to the placeholder so a missing vault entry degrades cleanly.
-- **Do not** invent an `api/track/{entryKey}/image` endpoint for this page alone. Cover art is a library-wide capability owned by 2.1; the detail page is a *consumer*, not the place to define the seam. If cover art is wanted before 2.1's full scope, that is a scope conversation for 2.1, not a side-door endpoint here.
-
-Captured as a dependency edge: **Track Detail cover art → 2.1 (cover-art / image vault wired through).** The page is shippable without it; the cover block simply stays in placeholder state until 2.1 closes.
-
-### 5. Play affordance
-
-The page resolves a `TrackDto` (carrying both `Id` and `EntryKey`); playback is keyed by the SQL `Id` via the existing stream path, so no ID-space ambiguity arises here (unlike the FramePlayer note above — this page always has the full DTO).
-
-- A single prominent **Play** button under the masthead. `MudButton Variant="Variant.Filled"` with a leading play icon, or a large `MudIconButton` if the visual language wants a circular transport control. Label "Play" (or "Play track").
-- Wires to the cascaded player: `[CascadingParameter] IStreamingPlayerService PlayerService` (cascaded from `AudioPlayerProvider` in `MainLayout`, available to all pages).
-- Click handler mirrors `TracksView.PlayTrack`: if `PlayerService.CurrentTrack?.Id == Track.Id && PlayerService.IsPaused`, call `TogglePlayPause()` (resume); otherwise `SelectTrackStreaming(Track)`. (The brief named `SelectTrackStreaming` — that is correct and current; `TracksView` happens to call the base `SelectTrack`, but the streaming entry point is the right one for an explicit Play action.)
-- **State-reactive label:** the button should reflect live transport state for *this* track — show "Pause" when this track is the current track and playing, "Play"/"Resume" otherwise. That requires the same multicast subscription `TracksView` uses: subscribe to `PlayerService.StateChanged` in `OnParametersSet` (reference-guarded, idempotent), `InvokeAsync(StateHasChanged)` on fire, unsubscribe on `Dispose`. The cascade is `IsFixed`, so without this subscription the button label goes stale when the dock bar drives state. This is mandatory, not polish — copy the `_subscribedService` guard pattern verbatim from `TracksView.razor.cs`.
-- Optional, defer: a secondary "Now playing" indicator or the `WaveformProfile` mini-render. Out of scope for v1 — note it as a later enhancement, don't build it.
-
-### 6. Loading and error states
-
-Three states, driven by the §2 VM flags.
-
-- **Loading (`IsLoading`):** a skeleton matching the loaded layout's silhouette so the transition doesn't reflow — a square `MudSkeleton` (Rectangle) for the cover, a wide `MudSkeleton` (Text) for the title, a narrower one for the artist, and two or three short ones for the metadata rows. Reuse the `MudSkeleton` idiom already in `TracksView.razor` for visual consistency.
-- **Loaded (`Track != null`):** the §3 layout.
-- **Not found (`NotFound`):** 404-style messaging — a centered `MudText` ("Track not found") with a quieter line ("This track may have been moved or removed.") and a `MudButton`/`MudLink` back to `/tracks`. Theme-coherent, calm, not an error-red alert. This is the failure surface for both a genuine 404 and a deserialize miss; the VM collapses both into `NotFound`, so the page need not distinguish them.
-
-### 7. CSS — new `deepdrft-` classes
-
-All in `DeepDrftPublic/wwwroot/styles/deepdrft-styles.css` (shared server/client). Described semantically; implementer writes the rules.
-
-- `deepdrft-track-detail-container` — the centered single column: max-width, horizontal auto-margins, top/bottom padding for vertical rhythm.
-- `deepdrft-track-detail-cover` — the square cover frame: aspect-ratio 1/1, rounded corners consistent with the gallery cards, subtle shadow/border keyed to the active palette, `overflow: hidden` so the `
` crops cleanly. Applies to both the `
` and the placeholder `MudPaper` so they occupy identical space (no layout jump between placeholder and real art once 2.1 lands).
-- `deepdrft-track-detail-masthead` — title/artist spacing and the display-serif type treatment (lean on existing `deepdrft-` font classes rather than redefining font stacks).
-- `deepdrft-track-detail-meta` — the metadata block: the small-caps muted label treatment and row rhythm for the definition rows.
-- `deepdrft-track-detail-back` — the quiet back-link affordance (muted color, hover treatment).
-
-Theme coherence comes for free via MudBlazor palette tokens (`Color.Primary`, `Color.Secondary`, surface/text from the active `PaletteLight`/`PaletteDark`); the `deepdrft-` classes handle layout, rhythm, and the serif/small-caps editorial touches that MudBlazor doesn't express. Verify both palettes ("Charleston in the Day" / "Lowcountry Summer Nights") in review — the cover shadow and back-link muted color are the two spots most likely to read wrong in one mode.
-
-### 8. `Pages.cs` registration
-
-**Link-only — do not add to `MenuPages`.** A per-track detail route is not a destination you navigate to from the header; it's reached contextually (a gallery card, the Now Playing surface). Adding it to nav makes no sense without a specific `{entryKey}`.
-
-Open question for a follow-up, not this page: should gallery cards / Now Playing link *into* `/track/{entryKey}`? That is the inbound-link wiring that makes this page reachable, and it lives in those components, not here. This spec delivers the destination; the links that point at it are a small separate change (likely: make the `TrackCard` cover/title a `MudLink` to `/track/@track.EntryKey`). Flag it so the page isn't shipped orphaned and unreachable.
-
-### Dependencies and sequencing
-
-- **Shippable now** in placeholder-cover form — no blocking dependency.
-- **Cover art** depends on **2.1**; until then the cover block is the themed placeholder. (§4)
-- **Reachability** depends on a small inbound-link change in `TrackCard` / Now Playing (§8) — trivial, but required or the page is orphaned. Recommend bundling that link change with this page so v1 is actually reachable.
-
----
-
## Working with this file
- **Add items by extending an existing phase first**; only create a new phase when the addition genuinely doesn't fit any of 1–5. Phase numbers are organisational, not sequencing.