# Phase 17 — Player-Bar Queue View

Status: **design spec — all 11 open questions resolved (Daniel, 2026-06-19).** Author: product-designer. Date: 2026-06-19.
**Plan only — no code has been written by this doc.**

**Resolved (Daniel, 2026-06-19):** OQ1 → **Option A, conditional** (collapse/expand toggle *if* dynamic
iframe resize is achievable in the embed snippet; **else fall back to Option B**, omit the button —
feasibility call made during 17.3). OQ2 → **yes, both modes** (clicking a queued row jumps playback to
that track in docked overlay *and* read-only embed; reuses `PlayRelease(Items, index)`). OQ3 + OQ11 →
**the currently-playing track cannot be removed at all** (no "remove current" action; remove affordance
suppressed on the current row; queue empties only organically when the current track ends with nothing
queued after). OQ4 → **`MudDropContainer` for now** (pivot to a touch-viable mechanism later if mobile
issues surface; C6 softened — touch-viability is a known risk, not a pre-ship blocker). OQ5 → **yes,
Clear in the overlay header** — but Clear must **not** stop or remove the currently-playing track (it
keeps playing and stays in the queue; only the other queued tracks are cleared). OQ6 → **fixed sensible
height with internal scroll past N rows** (not grow-to-cap; affects `EmbedSnippetBuilder.ForRelease`).
OQ7 → **Material icons for now** (`QueueMusic` / `PlaylistAdd`; bespoke `DDIcons` glyph not pursued in
Phase 17). OQ8 → **pure append, confirmed** (add ≠ play; first add into a dormant queue leaves a coherent
`CurrentIndex` via a small additive engine affordance, landing with wave 17.1). OQ9 → **exclude
`StreamNowButton`** (no fixed track until one resolves). OQ10 → **deferred, confirmed** (cards get no
Add-to-Queue in Phase 17; the deferred card work is captured in `TODO.md`).

This phase makes the play-queue **visible and manipulable**. Phase 11 (wave 11.F) built the queue
*engine* (`IQueueService` / `QueueService`) and wired auto-advance, skip-prev/next, and release
embeds — but the queue itself has **no UI**. A listener can start an album and skip through it, but
cannot *see* what is next, reorder it, drop a track, or hand-build a queue. This phase surfaces it.

Cross-references (read before implementing):
- `PLAN.md §17` — the concise phase entry.
- `DeepDrftPublic.Client/Services/IQueueService.cs` / `QueueService.cs` — the engine this phase
  drives. Its docstrings **already anticipate** reorder + removal as additive members (see §1).
- `DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor[.cs]` — the bar that gains the
  Queue button + (Fixed) the embedded panel.
- `DeepDrftPublic.Client/Controls/AudioPlayerBar/PlayerControls.razor` / `PlayerTransportZone.razor`
  — where transport buttons live; the Queue button sits relative to these.
- `DeepDrftPublic.Client/Controls/AudioPlayerProvider.razor[.cs]` — cascades the queue (`IsFixed`).
  **Player + queue state stays here, at layout level** (project memory; do not move into pages).
- `DeepDrftPublic.Client/Pages/FramePlayer.razor` + `Layout/EmbedLayout.razor` — the Fixed/embed
  surface; the iframe-host sizing constraint lives here (§4).
- `DeepDrftPublic.Client/Controls/SharePopover.razor` / `WaveformVisualizerControlPopover.razor` —
  the established overlay/dismissal idioms to borrow for the non-Fixed overlay (§3).
- `DeepDrftPublic.Client/Pages/CutDetail.razor`, `Controls/ReleaseHeroOverlay.razor`,
  `Controls/ReleaseGallery.razor`, `Controls/StreamNowButton.razor` — the play-affordance sites the
  "Add to Queue" button (§5) attaches beside.

---

## 0. State it inherits (verified 2026-06-19)

The queue engine is **mature and complete for orchestration**:

- `IQueueService` exposes `Items` (`IReadOnlyList<TrackDto>`), `CurrentIndex`, `Current`, `HasNext`,
  `HasPrevious`, `IsArmed`, the `QueueChanged` event, and commands `PlayRelease`, `Arm`, `Start`,
  `Enqueue`, `EnqueueRange`, `Next`, `Previous`, `Clear`. **There is no reorder or remove member
  yet** — but the interface docstring explicitly reserves them as additive ("reordering mutates
  `Items` and re-emits `QueueChanged`"). This phase adds them.
- The queue is cascaded from `AudioPlayerProvider` as an `IsFixed` cascade alongside the player.
- `AudioPlayerBar` already subscribes to `QueueChanged` to re-render skip affordances, and reads
  `HasNext`/`HasPrevious`. Adding a Queue button + panel is the same subscription, more rendering.
- **Fixed** (`AudioPlayerBar.Fixed`) is the embed flag, set true by `FramePlayer` via `EmbedLayout`.
  In Fixed mode the bar is in normal flow (no minimize/spacer/clip); the host iframe is sized by the
  embed snippet (`EmbedSnippetBuilder`). Today a release embed arms a shared queue; a single-track
  embed leaves the queue empty.
- **"Shared queue" today = a multi-track release** loaded via `PlayRelease` (docked) or `Arm`/`Start`
  (embed). The queue is *populated from one release's ordered tracks*. There is no hand-assembled
  multi-source queue yet — `Enqueue`/`EnqueueRange` exist on the interface but **no UI calls them**.

The gap this phase closes: the queue is a black box. You can drive it (skip) but not inspect or edit
it. Item 4 ("Add to Queue") also activates the dormant `Enqueue` path — the first time the queue
becomes a thing a listener *builds*, not just a release they *play*.

---

## 1. Goal

Make the queue a first-class, visible, editable object in the player bar — without moving any player
or queue state out of the layout-level provider, and without changing the streaming/playback seam.

Three user-visible capabilities, gated by a single new **Queue** toggle button in the bar:

1. **See** the queue — an ordered list of what is queued, with the current track marked.
2. **Edit** the queue (where editing is allowed) — drag-reorder and per-track removal.
3. **Grow** the queue — an "Add to Queue" affordance everywhere there is a play button.

The two view modes (overlay vs. embedded panel) differ only in **presentation and edit-permission**,
not in the data they read — both bind the same cascaded `IQueueService.Items` (project memory: *one
source, multiple views*).

---

## 2. Constraints (hard)

- **C1 — Player/queue state stays at layout level.** The Queue view reads the cascaded
  `IQueueService`; it does not own or relocate queue state. No page-level player state. (Project
  memory.)
- **C2 — No new playback semantics.** Reorder/remove mutate `Items` + `CurrentIndex` and re-emit
  `QueueChanged`; they must **not** restart, re-stream, or interrupt the currently-playing track
  (matching the engine's existing "the player stays a single-track device" posture). Reordering the
  *currently-playing* item or items around it must keep playback uninterrupted.
- **C3 — Embed = read-only shared queue.** In Fixed (embed) mode, a shared (release) queue is
  **fixed-order and non-editable**: no drag, no remove. The panel is a *display* of the up-next list.
- **C4 — The engine is extended additively, never reshaped.** New members (`Move`/`RemoveAt`) are
  added to `IQueueService`; existing members and their contracts are untouched. Consumers written
  against today's surface keep working (the interface's stated open/closed posture).
- **C5 — Reorder/remove are interop-free state mutations.** Like `Enqueue`/`Arm`, they touch only the
  in-memory list + index + `QueueChanged`. They run safely during prerender. Only `PlayRelease`/
  `Start`/`Next`/`Previous` touch JS.
- **C6 — Drag-and-drop should be touch-viable (softened — OQ4 resolved).** The public site's primary
  listening surface includes mobile (per `PLAN.md §1.7`). **Resolved (Daniel, 2026-06-19): ship with
  `MudDropContainer` now.** Touch-viability is **no longer a hard pre-ship blocker** — it is a known
  risk with a planned pivot path: if mobile/touch issues surface in practice, pivot to a touch-viable
  mechanism (pointer-interop reorder, matching `RadialKnob`'s `capturePointer`, or an up/down-arrow
  fallback) in a later pass. See OQ4 (resolved).
- **C7 — Embedded panel must not break the iframe host.** Adding the panel in Fixed mode grows the
  player's intrinsic height; the embed snippet's iframe dimensions must account for it (§4, OQ6).

---

## 3. Item 1+2 — Queue button & non-Fixed (overlay) mode

### 3.1 The Queue button

A new icon button in the player bar, shown **only when a queue is loaded** (`Items.Count > 0`).
Mirror the existing skip-affordance gating: with no queue (null) or empty queue, the button is
absent, so the bar is byte-for-byte its pre-queue self for single-track play.

- **Placement (Daniel):** to the **left of the timestamps and below the other player control
  buttons.** Today `PlayerTransportZone` is a vertical `MudStack` of (transport-button row) over
  (`TimestampLabel`). The Queue button sits **between** those two — a second row below the
  play/skip controls and above the timestamp. That literally satisfies "below the control buttons,
  to the left of the timestamps."
- **Glyph:** `Icons.Material.Filled.QueueMusic` (or a bespoke `DDIcons` queue glyph if Daniel wants
  the hand-rolled aesthetic — **OQ7**). `Color.Primary` to match the transport row.
- **Active state:** when the overlay/panel is open, the button reads active (filled/highlighted),
  matching the visualizer popover idiom.

### 3.2 Non-Fixed overlay panel

When the bar is **not Fixed** (the docked public player), clicking Queue toggles a **screen-centered
overlay** — borrow the exact idiom Phase 15 settled on for the visualizer controls: `MudOverlay`
(`DarkBackground`, `Modal`), panel stops click propagation, scrim-click closes, drag-safe (a
`position:fixed` drag capture sits above the scrim so a drag that ends outside the panel does not
dismiss). **Not** a `MudPopover` anchored to the bar — a centered modal is the proven pattern here
and avoids anchor math against a `position:fixed` dock.

- **Shape:** "mostly square" (Daniel) — a panel roughly `min(90vw, 520px)` square, scrollable track
  list inside. Chrome matches `NowPlayingCard` / the visualizer panel (square corners, lighter-navy,
  thin border) for family resemblance.
- **Contents:** ordered list of `Items`. Each row: drag handle, track number/position, track
  name + artist, a now-playing marker on `Current` (index == `CurrentIndex`), a remove (×) button.
  Clicking a row's body jumps playback to that track (`PlayRelease(Items, thatIndex)` — reuses the
  existing "play from index" semantics; **OQ2** confirms this is desired vs. a dedicated jump).
- **Reorder:** drag-and-drop reordering of rows → `IQueueService.Move(from, to)` (new member, §6).
- **Remove:** per-row × → `IQueueService.RemoveAt(index)` (new member, §6).
- **Header:** title ("Queue" / "Up Next"), optional "Clear" action (calls existing `Clear()`;
  **OQ5** — does Clear belong in the UI, and does it stop playback?).
- **Empty state:** the button is hidden when empty, so the overlay is never opened empty by the
  button. But removal *can* empty a non-empty queue mid-session — define the terminal state (**OQ3**).

### 3.3 Borrowed precedent

The overlay shell, dismissal, and drag-safety are a **direct lift** of
`WaveformVisualizerControlPopover` (Phase 15 §4). The "mostly square panel, family chrome" is the
`NowPlayingCard` look. Reuse, don't reinvent.

---

## 4. Item 3 — Fixed (embedded) mode

When the bar **is** Fixed (the `/FramePlayer` iframe embed), the queue list is **always shown below
the existing player-bar controls** — not a toggle overlay. It is part of the embed's standing layout.

- **Permissions (C3):** the embedded shared (release) queue is **fixed-order, non-editable** — no
  drag handles, no remove buttons. It is a read-only up-next display: ordered track list, current
  track marked, future tracks listed. Tapping a future row *may* jump to it (**OQ2** — even read-only
  queues could allow jump-to-track; reorder/remove are the forbidden ops, jump is arguably fine).
- **Container resize (C7):** the Fixed bar gains vertical height for the panel. Two coupled changes:
  1. The `EmbedLayout` / `FramePlayer` Fixed bar renders the panel inline below the controls.
  2. `EmbedSnippetBuilder.ForRelease(...)` must mint an iframe tall enough to show the panel.
     `ForTrack(...)` stays at today's compact height (single-track embeds have no queue, so no
     panel). **This makes the two embed snippets diverge in height** — track-embed compact,
     release-embed taller. That is correct: only release embeds have a queue to show. (**OQ6** —
     fixed taller height for all release embeds, vs. a height that scales with track count up to a
     cap. Recommend: a fixed sensible height with the panel internally scrollable past N rows.)

### 4.1 The Queue button in embed mode (Daniel asked to clarify)

**Recommendation:** in Fixed mode the panel is always shown, so the Queue button is **not needed for
toggling** — but rather than hide it, **repurpose it as a collapse/expand control** for the panel so a
listener can reclaim the iframe space. Two viable readings, pick one:

- **Option A (resolved — preferred) — collapse toggle.** Queue button is present in Fixed mode and
  collapses/expands the always-default-open panel. Default = expanded. Gives the embedder's viewer
  control. Consistent button meaning across modes ("toggle the queue view").
- **Option B (fallback) — omit the button in Fixed mode entirely.** Panel is always open, no toggle.
  Simpler, but the button's meaning then differs by mode (present+toggles-overlay when docked, absent
  when embedded), and a viewer can't reclaim space.

**OQ1 — RESOLVED (Daniel, 2026-06-19): Option A, *conditional on feasibility*.** Use Option A **if**
the iframe can be dynamically resized appropriately for collapse/expand — e.g. an embed-snippet
`postMessage` → host resize handshake so the host iframe shrinks when the panel collapses and grows
when it expands. **Otherwise fall back to Option B** (omit the button, panel always open at fixed
height). **A is preferred; B is the fallback; the deciding factor is whether dynamic iframe resize is
achievable in the embed snippet.** This A-vs-B determination is **made during 17.3 implementation** —
record it there. Note the interaction with OQ6 (embed panel height): a collapse/expand that does *not*
resize the iframe (the earlier "iframe stays sized for expanded; collapsed just shows less" reading) is
**not** Option A under this resolution — Option A requires the iframe to actually resize. A non-resizing
collapse is cosmetic and does not let the viewer reclaim space, so it collapses into Option B's value.

---

## 5. Item 4 — "Add to Queue" affordance

An **"Add to Queue" icon button with a tooltip** beside **every play button for a release or track.**

- **Glyph + tooltip:** `Icons.Material.Filled.QueueMusic` / `PlaylistAdd` with `MudTooltip`
  "Add to queue" (track) / "Add release to queue" (release). (**OQ7** — Material vs. bespoke glyph.)
- **Behavior:**
  - **Track context:** `IQueueService.Enqueue(track)` — appends without disturbing current playback
    (existing member, no UI calls it today; this lights it up).
  - **Release context:** `IQueueService.EnqueueRange(orderedTracks)` — appends the whole release's
    ordered track list (existing member).
- **First-add semantics — RESOLVED (Daniel, 2026-06-19): pure append (option (a)).** Add-to-Queue is
  explicitly *not* play; keep the verbs distinct. If the queue is **empty/dormant** (`CurrentIndex ==
  -1`), the first add appends and **leaves a coherent `CurrentIndex`** (the small additive engine
  affordance) so the next play/skip behaves correctly — but **does not auto-play**. The listener still
  presses play to start. (Options (b) "first add also stages/sets current without auto-playing" and (c)
  "first add behaves like `PlayRelease`/play" are declined — they blur add vs. play.) **This engine
  tweak belongs with wave 17.1** (it is part of the `Enqueue`-into-empty index handling, alongside the
  `Move`/`RemoveAt` additions). See **OQ8 (resolved)**.

### 5.1 Where the button attaches (inventory)

Every site that currently has a play affordance:

| Site | Current play affordance | Add-to-Queue target |
|---|---|---|
| `CutDetail` header | "Play" `MudButton` (whole album) | release → `EnqueueRange` |
| `CutDetail` track rows | per-row `PlayStateIcon` | track → `Enqueue` |
| `SessionDetail` / `MixDetail` | `PlayContent` slot in `ReleaseHeroOverlay` (single track) | track → `Enqueue` |
| `StreamNowButton` | random-track stream trigger | **edge case — OQ9** (random has no fixed track until resolved) |
| `ReleaseGallery` cards | **none today** (cards are pure links) | **scope question — OQ10** |

- **`ReleaseGallery` cards have no play button today** — they navigate to detail. Adding an
  Add-to-Queue button there is *new* affordance, out of the literal brief ("anywhere there is a play
  button"). **OQ10 — RESOLVED (Daniel, 2026-06-19): deferred, confirmed.** Cards get **no**
  Add-to-Queue affordance in Phase 17. Scope item 4 to the detail-page play sites (rows 1–3 above)
  where a play button genuinely exists today. The deferred work — giving cards a play + Add-to-Queue
  affordance (a card-redesign question) — is captured as a `TODO.md` entry.
- **`StreamNowButton`** is a "surprise me" trigger, not a fixed track — Add-to-Queue has no concrete
  item until a track resolves. Recommend: **exclude it** from item 4. **OQ9.**

So the **grounded scope** of item 4: the `CutDetail` header + track rows, and the Session/Mix hero
play slot. That is "every play button that targets a known release or track."

---

## 6. Engine additions (the only `IQueueService` changes)

Two new members, both interop-free state mutations that re-emit `QueueChanged`:

- **`void Move(int fromIndex, int toIndex)`** — reorders `Items`; adjusts `CurrentIndex` so the
  *same track* stays current across the move (find current track's new position). No re-stream
  (C2). No-op on out-of-range or equal indices.
- **`void RemoveAt(int index)`** — removes `Items[index]`.
  - Removing a track **after** current: just remove; `CurrentIndex` unchanged.
  - Removing a track **before** current: remove + decrement `CurrentIndex` (current track stays the
    same item, new lower index). Playback uninterrupted.
  - Removing the **current** track: **define** — recommend stop is *not* triggered (C2 says mutations
    don't touch playback), so the playing track keeps playing to its natural end even though it's no
    longer in the list, and `CurrentIndex` points at what is now at that slot (the next track) so the
    next auto-advance / skip is coherent. Edge: removing current when it's the last item → queue
    becomes shorter; auto-advance simply has nothing to advance to (existing behavior). **OQ3/OQ11.**
  - Removing the **last remaining** track → empty queue (`CurrentIndex = -1`, dormant). The Queue
    button disappears; if the overlay is open it closes (or shows empty state then closes). **OQ3.**

Both are **additive** (C4). Unit-testable against the existing `QueueServiceTests` fake-player
harness with no container (the engine's stated design intent).

---

## 7. Use cases

- **UC1 — Inspect up-next (docked).** Listener playing an album opens the Queue overlay, sees the
  ordered remaining tracks with the current one marked, closes it. No edit.
- **UC2 — Reorder (docked).** Listener drags track 5 above track 2; playback continues uninterrupted;
  the new order drives the next auto-advance.
- **UC3 — Remove (docked).** Listener removes a track they don't want; if it was upcoming, it's gone
  from the up-next; if it was current, current keeps playing to natural end (per §6).
- **UC4 — Build a queue (docked).** Listener on a Cut detail page clicks "Add to Queue" on three
  individual tracks across two albums, then opens the queue and plays from the top.
- **UC5 — Embed up-next (Fixed).** A blog embeds a release; the iframe shows the player *and* the
  fixed-order track list below it; viewer sees what's coming, cannot reorder/remove; (Option A)
  collapses the panel to reclaim space.
- **UC6 — Single-track embed unchanged.** A single-track embed shows no Queue button and no panel —
  the compact iframe is byte-for-byte today's.
- **UC7 — Jump to track.** Listener clicks a queued row → playback jumps to that track and continues
  from there (if OQ2 = yes).

---

## 8. Acceptance criteria

- **AC1** — When `Items.Count == 0`, the Queue button is absent and the docked bar renders exactly as
  it does today (no layout shift, no empty panel).
- **AC2** — When `Items.Count > 0` and not Fixed, the Queue button appears below the transport
  controls and to the left of the timestamp; clicking it opens a centered, mostly-square overlay
  listing the queue with the current track marked.
- **AC3 (docked)** — Drag-reordering a row changes the queue order and is reflected in subsequent
  skip/auto-advance; the currently-playing track is **not** re-streamed or interrupted by any reorder.
- **AC4 (docked)** — Removing an upcoming track removes it from the list and from auto-advance;
  removing a track before the current one keeps the same track playing; removing the current track
  does not stop playback (per §6 decision).
- **AC5 (Fixed)** — In a release embed, the queue panel renders below the controls by default, shows
  the ordered tracks with the current marked, and exposes **no** drag handles or remove buttons.
- **AC6 (Fixed)** — The release embed iframe (via `EmbedSnippetBuilder.ForRelease`) is tall enough to
  show the panel without clipping; the single-track embed (`ForTrack`) height is unchanged.
- **AC7** — An "Add to Queue" button with tooltip appears beside the Cut header Play, each Cut track
  row's play, and the Session/Mix hero play; clicking it appends the track/release to the queue
  without disturbing the currently-playing track.
- **AC8** — Add-to-Queue on a dormant (empty) queue leaves the queue populated with a coherent
  `CurrentIndex` such that the next play/skip behaves correctly (per OQ8 resolution).
- **AC9** — `Move` and `RemoveAt` are additive to `IQueueService`; all existing `QueueServiceTests`
  pass unchanged; no existing member's behavior changes.
- **AC10** — All queue-view rendering and the new engine mutations run during prerender without JS
  interop (no prerender exceptions); only playback transitions touch JS.

---

## 9. Test cases

Engine (`QueueServiceTests`, fake-player harness — no container):

- **T1** `Move(2, 0)` on a 4-item queue reorders correctly; `Items` reflects new order;
  `QueueChanged` fired once.
- **T2** `Move` keeps the *current track* current: with `CurrentIndex` at track X, moving other items
  around it leaves `Current` == X; moving X itself updates `CurrentIndex` to X's new slot.
- **T3** `Move` does not call the player's stream method (assert fake player saw no `SelectTrackStreaming`).
- **T4** `RemoveAt` after current: `CurrentIndex` unchanged, item gone.
- **T5** `RemoveAt` before current: `CurrentIndex` decremented, same track current.
- **T6** `RemoveAt` of current: player not stopped (per §6); `CurrentIndex` resolves to the new
  occupant or `-1` if it was last.
- **T7** `RemoveAt` of last remaining item → empty + dormant (`CurrentIndex == -1`, `Items` empty,
  `QueueChanged` fired).
- **T8** `Move`/`RemoveAt` out-of-range / no-op cases do not throw and do not fire `QueueChanged`.
- **T9** `Enqueue` into a dormant queue then play behaves per OQ8 resolution.
- **T10** Reordering the currently-playing item to a new slot leaves auto-advance pointing at the
  correct next track when the current ends.

Component (if/when component test coverage is in scope — today none exists for the client, per
`DeepDrftPublic.Client/CLAUDE.md`):

- **T11** Queue button absent when queue empty (render assertion).
- **T12** Overlay opens/closes via button + scrim; a simulated drag that ends on the scrim does not
  dismiss.
- **T13** Fixed-mode panel renders inline with no drag/remove controls.
- **T14** `EmbedSnippetBuilder.ForRelease` height differs from `ForTrack` (unit test on the builder).

---

## 10. Open questions for Daniel

**All eleven resolved (Daniel, 2026-06-19): OQ1, OQ2, OQ3, OQ4, OQ5, OQ6, OQ7, OQ8, OQ9, OQ10, OQ11.**

- **OQ1 — Queue button in embed (Fixed) mode. RESOLVED → Option A, conditional.** Use Option A
  (collapse/expand toggle) **if** the iframe can be dynamically resized for collapse/expand (embed-snippet
  `postMessage` → host resize handshake); **else fall back to Option B** (omit the button). A preferred,
  B fallback; deciding factor = whether dynamic iframe resize is achievable in the embed snippet;
  determination made during **17.3** (see §4.1).
- **OQ2 — Click-to-jump. RESOLVED → yes, both modes.** Clicking a queued row jumps playback to that
  track in **both** the docked overlay and the read-only embed, reusing `PlayRelease(Items, index)`.
  Jump is not a forbidden edit — only reorder/remove are — so the read-only embed allows it too.
- **OQ3 — Terminal/empty states. RESOLVED (jointly with OQ11) → the currently-playing track cannot be
  removed at all.** There is no "remove current" user action: the remove (×) affordance is **suppressed
  on the current row** in the overlay. The queue therefore empties only **organically** — when the
  current track ends and nothing is queued after it. There is no mid-session "removed the current track"
  state to design for, because the user cannot reach it. (The road not taken — "show an empty state then
  auto-close on user-driven empty" — is moot: user removal can only ever target non-current rows, so the
  user cannot remove their way to an empty queue while a track is playing.) The engine's `RemoveAt`-of-
  current behavior built in 17.1 remains as **defensive engine behavior but is UI-unreachable**.
- **OQ4 — Drag-and-drop mechanism & touch. RESOLVED → `MudDropContainer` for now.** Ship with
  MudBlazor's `MudDropContainer`. If mobile/touch issues surface in practice, pivot to a touch-viable
  mechanism later (pointer-based interop reorder matching `RadialKnob`'s `capturePointer`, or an
  up/down-arrow fallback). C6 softened: touch-viability is a known risk with a planned pivot path, **not**
  a hard pre-ship blocker (see §2 C6).
- **OQ5 — "Clear queue" in the UI. RESOLVED → yes, in the overlay header.** Include a Clear action in
  the overlay header. Clear empties the up-next but **must NOT stop the currently-playing track and must
  NOT remove it** — the current track keeps playing and stays in the queue; only the *other* queued
  tracks are cleared. (Note: this is a tighter contract than the engine's bare `Clear()`, which empties
  the whole list — the UI's Clear preserves the current track. This belongs in the 17.2 overlay wiring.)
- **OQ6 — Embed panel height. RESOLVED → fixed sensible height with internal scroll past N rows.** Not
  grow-to-cap. Especially important for the embed. Affects `EmbedSnippetBuilder.ForRelease`. (Road not
  taken: a height that grows with track count up to a cap — declined in favor of a fixed height with the
  panel internally scrollable.)
- **OQ7 — Glyph. RESOLVED → Material icons for now.** Use `Icons.Material.Filled.QueueMusic` /
  `PlaylistAdd`. A bespoke hand-rolled `DDIcons` queue glyph (gas-lamp/lava-lamp family aesthetic) is
  **not pursued in Phase 17** — left open for a later aesthetic pass.
- **OQ8 — Add-to-Queue into a dormant (empty) queue. RESOLVED → pure append.** Add ≠ play; first add
  into a dormant queue appends and leaves a coherent `CurrentIndex` (small additive engine affordance)
  so the next play/skip behaves correctly, but does **not** auto-play. The engine tweak lands with wave
  **17.1**. (first-add-stages and first-add-plays both declined — see §5.)
- **OQ9 — `StreamNowButton`. RESOLVED → exclude.** `StreamNowButton` is excluded from the Add-to-Queue
  affordance — it is a "surprise me" trigger with no fixed track until one resolves.
- **OQ10 — `ReleaseGallery` cards. RESOLVED → deferred, confirmed.** Browse-grid cards get **no**
  Add-to-Queue affordance in Phase 17. Item 4 is scoped to the detail-page play sites (Cut header, Cut
  track rows, Session/Mix hero). The deferred card work (play + Add-to-Queue, a card-redesign question)
  is captured in `TODO.md`.
- **OQ11 — Removing the current track. RESOLVED (jointly with OQ3) → the current track cannot be removed
  from the UI at all.** Rather than choosing between "keep playing to natural end" and "skip to next," the
  user-facing answer is that there is **no remove-current action** — the × is suppressed on the current
  row (see OQ3). Both candidate behaviors are therefore UI-unreachable; the engine's `RemoveAt`-of-current
  path remains only as defensive engine behavior.

---

## 11. Wave decomposition (implementation sequencing)

Three waves; **17.1 is the load-bearing prerequisite** (engine + the shared queue-list view that both
modes render). 17.2 and 17.3 then hang off it and are largely parallel.

- **17.1 — Engine additions + shared queue-list view.** Add `Move`/`RemoveAt` to `IQueueService`/
  `QueueService` (+ `QueueServiceTests`). **Plus the OQ8 affordance:** `Enqueue` into a dormant/empty
  queue leaves a coherent `CurrentIndex` (pure append, no auto-play) so the next play/skip is correct.
  Build a single presentational `QueueList` component that renders `Items` with the current marked and
  takes an `Editable` flag (drag/remove on when true; reorder via `MudDropContainer` per OQ4). This is
  the *one source* both modes consume. **Cold-start. Gates 17.2 + 17.3.** No bar changes yet.
- **17.2 — Non-Fixed overlay.** Queue button in the bar (gated on `Items.Count > 0`, placed per
  §3.1), the centered overlay borrowing the visualizer-popover idiom, hosting `QueueList Editable`.
  Reorder/remove/jump wired. **Depends on 17.1.**
- **17.3 — Fixed embed panel + snippet resize + Add-to-Queue.** Inline `QueueList` (non-editable)
  below the Fixed bar; **the OQ1 feasibility determination is made here** — implement Option A
  (collapse/expand button + iframe resize handshake) **if** dynamic iframe resize proves achievable in
  the embed snippet, else fall back to Option B (omit the button); `EmbedSnippetBuilder.ForRelease`
  height bump; the Add-to-Queue buttons at the detail-page play sites (§5.1 grounded scope — **cards
  excluded per OQ10**). **Depends on 17.1.**
  (Add-to-Queue could split into its own wave 17.4 if Daniel wants it shipped independently — it only
  needs the existing `Enqueue`/`EnqueueRange`, not 17.1's new members.)

**Dependency shape:** `17.1 → {17.2, 17.3}`. The cold-start item is 17.1. Most open questions
(OQ1–OQ11) must be resolved before 17.2/17.3 land, but 17.1 can begin immediately — the engine
contract for `Move`/`RemoveAt` is settled (§6) and independent of the UI decisions.
