# Phase 12 — Waveform Visualizer Generalization + NowPlayingHero Rewire (Design Spec)

Status: **design-complete, implementation-ready.** Daniel resolved the three §8 open questions on
2026-06-17 — committing **Direction B** (high-res compute for all media), correcting the datum model to
**per-track, not per-release**, and resolving §8b to **full parity on the lava controls**: the visualizer
rides every Release Detail host (Mix, Cut, Session) **and the controls ride all three** — not Mix-only. The
three-hosting-mode *layout* framing (visualizer-is-the-page on Mix / ambient environment on Cut/Session /
contained on NowPlaying) is retained; the change is that controls are present on all three detail-page hosts,
not suppressed on Cut/Session.

**Controls-hosting revision (Daniel, 2026-06-17, supersedes the inline knob-bar model).** *"We have enough
[controls] now that I want to design a panel to be hosted in a popover for the visualizer controls. The
lava-lamp toggle should be wired to this popover, so anywhere we can put one Icon we can put the control
surface."* The eight knobs no longer ride an inline *bar* per page. They move into a **single
popover-hosted control panel** triggered by the **lava-lamp icon**: click the icon → the panel pops over.
This is **more DRY than the per-page inline bar** and it **dissolves §8b-followup** — with a popover, every
host (Mix, Cut, Session, *and* the NowPlaying home card) places the *same* lava-lamp icon and gets the
*identical* panel; full parity is achieved *through the popover*, not through a bar re-laid-out on each
page. §8b-followup is now **answered**: the NowPlaying card gets the icon → popover like everywhere else.
The panel adopts the **NowPlaying Hero look** (dark-navy ground, green-accent knobs, light icons, muted-navy
filler — §3g). The §3/§6 hosting sections and the wave decomposition (§7, controls work consolidates into a
distinct 12.E concern) are revised to this model. One new open question the popover creates — its
positioning/anchor per host — is surfaced at §8e. Author: product-designer. Date: 2026-06-17. **No code has
been written by this doc.**

This phase has **two deliverables that share one engine**:

1. **Generalize** the landed Mix waveform visualizer (the WebGL2 lava renderer + its eight-knob controls)
   from a Mix-only treatment into a **track-cardinal visualizer** that every Release Detail page can host
   — Cuts, Sessions, and Mixes alike — rendering the waveform of **whatever track is currently
   playing/selected** on that page.
2. **Overhaul** the home-page `NowPlayingHero` (`NowPlayingCard`) so its "Now Playing" animation is the
   **real waveform visualizer** driven by live playback, replacing the 20 hardcoded CSS-animated bars
   (the "stochastic bullshit").

The explicit ask is **DRY / SOLID**: one reusable visualizer engine serving Mix detail, all Release
Detail pages, and the NowPlayingHero — **not three forks.** This spec's central finding makes that
cheap: the engine is *already* track-cardinal below the surface. The work is extraction, a per-track
high-res compute generalization (Direction B), and three hosting modes of the one engine — not a rebuild.

**The keystone model correction (Daniel, 2026-06-17).** The waveform datum is **per-track, not
per-release.** *"Each track in the release must get the metadata. The waveform visualizer metadata should
be tied to the track, right? The release is just the host."* Every track gets its own high-res waveform
datum; the release is merely the *host* surface, and the visualizer renders **the datum of whatever track
is currently playing/selected**. This *simplifies* the design — it aligns with the bridge already keying on
`TrackId`, and it **dissolves** the old "what is a multi-track Cut's waveform?" question (there is no
release-level datum to choose; the visualizer just shows the current track's). Threaded through §4, §5, the
endpoint shape, and the acceptance criteria below.

Cross-references (read before implementing):

- `product-notes/phase-10-mix-visualizer-lava-reframe.md` — the lava renderer this generalizes. The
  CPU-physics wax-blob model, the OKLab three-color gradient, the eight-knob control model, the bridge
  contract, and the read-only contract all **carry forward unchanged**. This spec does not re-derive any
  of them; it changes *where the engine lives*, *what feeds it*, and *who hosts it*.
- `product-notes/mix-visualizer-webgl-renderer.md` — the renderer architecture (pipeline, datum-as-texture,
  bridge, rAF loop). The §2 contract carries forward; §4/§7 are already superseded by the reframe.
- `DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor[.cs/.css]` — the Blazor bridge. **Already
  keyed on `ReleaseEntryKey` + `TrackId`, not on Mix.** Renamed and re-pointed by this phase (§3, §4).
- `DeepDrftPublic.Client/Controls/MixVisualizerControls.razor[.cs/.css]`,
  `DeepDrftPublic.Client/Services/MixVisualizerControlState.cs`,
  `DeepDrftPublic.Client/Controls/MixZoomMapping.cs` — the controls + state + mapping. Renamed; the controls
  component becomes the **panel content** hosted inside a new popover wrapper (§3d-revised, §3g) rather than
  an inline `TopRowCenter` bar. State + mapping otherwise unchanged.
- **New (this revision):** a popover host component — working name `WaveformVisualizerControlPopover` — that
  pairs the lava-lamp trigger icon with the `WaveformVisualizerControls` panel as its overlay content. This
  is the single affordance every host places (§3d-revised). MudBlazor `MudPopover` (already in the dependency
  set) is the natural substrate. Styled to the NowPlaying Hero look (§3g) — no inline hex; tokens from
  `DeepDrftShared.Client/wwwroot/styles/deepdrft-tokens.css`.
- `DeepDrftPublic/Interop/visualizer/MixVisualizer.ts` — the WebGL2 renderer module. Renamed; the only
  *logic* change is how the datum's time-mapping is established when no high-res mix datum exists (§5).
- `DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor` — the shared detail chrome. The visualizer
  becomes a scaffold-level concern via a new optional `Ambient` slot (§3c, §3f).
- `DeepDrftPublic.Client/Pages/MixDetail.razor`, `SessionDetail.razor`, `CutDetail.razor` — the three
  release-detail hosts (three distinct hosting modes — §3f).
- `DeepDrftPublic.Client/Controls/NowPlayingCard.razor[.css]`, `NowPlaying.razor` — the home-page
  now-playing card carrying the stochastic bars (§6).
- `DeepDrftAPI/Controllers/TrackController.cs`, `ReleaseController.cs` — the waveform endpoints; under the
  per-track model the datum resolves for a **track** (§5), with the release as addressing context.
- `DeepDrftContent/Processors/WaveformProfileService.cs`, `MixWaveformResolution.cs`,
  `Constants/VaultConstants.cs` — the compute/store path and the vaults; **Direction B generalizes the
  high-res compute here to every track** (§5).
- `DeepDrftAPI/Services/UnifiedTrackService.cs` — the upload path; Direction B adds a per-track high-res
  compute at upload (§5, §8a-new).
- `DeepDrftManager` CMS generate action (the Mix-only "generate waveform" trigger) — Direction B
  generalizes it off Mix-only to any track (§5).

---

## 1. The central finding — the engine is already release-cardinal

Before any plan: a read of the live code shows the "Mix coupling" is **mostly nominal**, not structural.
The visualizer is named `Mix*` throughout, but its *architecture* is release-generic:

| Layer | Reality on disk today | Mix-coupled? |
|-------|----------------------|--------------|
| `MixWaveformVisualizer` bridge inputs | `ReleaseEntryKey` (string) + `TrackId` (long?) + cascaded player | **No** — already track-cardinal (keys on `TrackId`) |
| Playback coupling | `IsActivePlayer` gates on `TrackId` matching the cascaded player's current track | **No** — works for any track |
| Renderer (`MixVisualizer.ts`) | datum texture + scroll/zoom geometry + wax-blob physics + OKLab gradient | **No** — pure function of a loudness datum + duration |
| Controls + state | seven normalized dials, scoped persistence, `Changed` seam | **No** — renderer-agnostic |
| **Datum fetch** | `IReleaseDataService.GetMixWaveform(entryKey)` → `GET api/release/{entryKey}/mix/waveform` | **Yes** — 404s unless `Medium == Mix`; resolves per *release*, not per *track* |
| **Datum source** | the high-res `mix-waveforms` vault, keyed by `MixMetadata.WaveformEntryKey` | **Yes** — only the Mix's single track gets the high-res datum |
| Names / comments | `Mix*` everywhere | cosmetic |

So the genuinely Mix-specific surface is exactly **two things**: the *fetch endpoint that gates on
`Medium == Mix` and resolves per-release*, and the *high-res datum that only Mix tracks have*. Everything
else is a rename.

**This is the SOLID seam the whole phase turns on.** The renderer and bridge already obey the
right abstraction ("render a loudness datum coupled to a playing **track**"); they were just *named* for
their first consumer. Generalizing is: rename to the abstraction, give the abstraction a **per-track**
datum source that exists for every track, and let three hosts mount it. No new renderer, no fork.

**The crucial data fact (verified):** *every uploaded track already has a waveform profile.*
`UnifiedTrackService.UploadAsync` calls `WaveformProfileService.ComputeAndStoreAsync(...)` at upload time
for **every** track, storing a **512-bucket** profile in the `waveform-profiles` vault keyed by the
track's `EntryKey` (this is the datum the player-bar `WaveformSeeker` already consumes). Mix tracks
*additionally* get a duration-derived **high-res** datum (~333 samples/sec) in the separate
`mix-waveforms` vault, triggered by a CMS action. So **the high-res datum is the only gap** — and under
Daniel's per-track model it is a gap *per track*, not per release: every track should carry its own
high-res datum, computed at upload and (for existing tracks) backfilled. **Direction B (committed)** makes
the high-res compute a per-track compute for **all** media, keyed by track `EntryKey` (§5). The cheaper
road — serving the existing 512-bucket profile to non-Mix and skipping new compute (old "Direction A") —
was **the road not taken** (§5).

---

## 2. Goal and scope boundary

**Goal.** One reusable `WaveformVisualizer` (renamed from `MixWaveformVisualizer`) + its lava renderer +
its controls, mounted in **three hosting modes** (§3f) — *visualizer-is-the-page* on Mix detail, *ambient
environment* on Cut/Session detail, *contained live element* on the home-page NowPlaying card — each fed by
a **per-track high-res datum** that exists for every track (Direction B, §5). The visualizer always renders
**the currently playing/selected track's** datum; the release is the host, not the datum's owner. The lava
controls ride **every host** — Mix, Cut, Session, *and* the NowPlaying card — via a **single
popover-hosted control panel** triggered by the lava-lamp icon (§3d-revised). Every host places one icon and
gets the identical panel; full parity (including the NowPlaying card) is achieved through the popover, so
§8b-followup is closed. The NowPlaying card drives the *same* engine off live playback (§6).

**In scope.**

- **Rename + relocate** the visualizer engine to a track-cardinal identity (`MixWaveformVisualizer` →
  `WaveformVisualizer`, `MixVisualizerControls` → `WaveformVisualizerControls`, `MixVisualizerControlState`
  → `WaveformVisualizerControlState`, `MixVisualizer.ts` → `WaveformVisualizer.ts`, `MixZoomMapping` →
  `WaveformZoomMapping`). Pure renames; no behavior change. (§3a)
- **Generalize the high-res compute to every track (Direction B, §5).** This is now the load-bearing data
  change — and it is **larger than a single endpoint**. It touches: (a) the **content** compute path
  (`WaveformProfileService` / `MixWaveformResolution`) — generalize the duration-derived high-res compute
  off Mix-only to any track; (b) the **upload** path (`UnifiedTrackService.UploadAsync`) — compute the
  high-res per-track datum at upload for every new track; (c) the **CMS** generate action — generalize the
  Mix-only "generate waveform" trigger to any track; (d) a **backfill** of the high-res datum for every
  existing track that lacks one (§8a-new); (e) a **per-track datum fetch** endpoint (§5) the bridge calls.
- **Host the visualizer in three modes** — via a new optional `Ambient` slot on `ReleaseDetailScaffold`
  for the Cut/Session ambient mode (§3c, §3f), Mix keeping its visualizer-is-the-page full-bleed mount, and
  a contained mount on the NowPlaying card. The slot lets scaffold-composing pages mount without
  re-implementing the wrapper.
- **Rewire the NowPlayingHero** to mount the visualizer driven by the live cascaded player, replacing the
  20 hardcoded CSS bars (§6).
- **Build the popover-hosted control panel** (§3d-revised, §3g). The renamed `WaveformVisualizerControls`
  becomes the **panel content**; a new `WaveformVisualizerControlPopover` host pairs the lava-lamp trigger
  icon with that panel as overlay content. One panel, one popover host, placed by an icon anywhere — the
  SOLID seam (§3d-revised). Style the panel to the **NowPlaying Hero look** (§3g) from real theme tokens.
- **Place the lava-lamp icon → popover on every host** — Mix, Cut, Session, *and* the NowPlaying card. Every
  host's controls affordance is now identical: one icon, one popover, one panel. Full parity on all four
  surfaces (§3d-revised) — §8b-followup is dissolved by the popover.

**Out of scope / unchanged.**

- **No renderer rewrite.** The wax-blob physics, the OKLab gradient, the collision model, the seven dials
  — all carry forward from the Phase 10 reframe exactly. This phase moves and renames the engine and
  changes its *input plumbing* and its *compute breadth*, never its art.
- **No bridge redesign.** The single-owner bridge, the idempotent datum guard, the `IsActivePlayer`
  gating, the `isPlaying`-gated rAF loop — all preserved. Extend the fetch, not the contract.
- **No new control model.** The eight knobs and `…ControlState` stay as-is (renamed). No new dials. The
  popover changes *where the controls are hosted* (a popover panel instead of an inline bar), not *what they
  are* — same knobs, same state, same `Changed` seam.
- **No new high-res *algorithm*.** Direction B generalizes the *existing* duration-derived compute to run
  per-track for all media; it does not redesign how the high-res datum is computed (`MixWaveformResolution`
  carries forward — the same ~333 samples/sec duration-derived model).
- **No playback-control change.** Read-only contract holds everywhere, including the NowPlaying card —
  the home card visualizes, it does not become a transport.

---

## 3. Generalizing the visualizer onto every Release Detail page

### 3a. The rename (Wave 12.A — pure, mechanical, no behavior change)

Rename the engine to its abstraction. This is a mechanical sweep with zero behavior change, done first so
every later wave references the generalized names:

| Today (Mix-named) | Generalized |
|-------------------|-------------|
| `MixWaveformVisualizer.razor[.cs/.css]` | `WaveformVisualizer.razor[.cs/.css]` |
| `MixVisualizerControls.razor[.cs/.css]` | `WaveformVisualizerControls.razor[.cs/.css]` |
| `MixVisualizerControlState.cs` (+ DI registration) | `WaveformVisualizerControlState.cs` |
| `MixZoomMapping.cs` | `WaveformZoomMapping.cs` |
| `MixVisualizer.ts` (+ the `./js/visualizer/MixVisualizer.js` import path) | `WaveformVisualizer.ts` |
| `DDIcons.LavaLamp` / `LavaLampFilled` | keep (the lava-lamp glyph is now the **popover trigger** — the single controls affordance, placed on every host — §3d-revised) |
| *(new)* | `WaveformVisualizerControlPopover.razor[.cs/.css]` — popover host pairing the lava-lamp trigger icon with the `WaveformVisualizerControls` panel (§3d-revised, §3g). Not a rename; new in 12.E. |

The `ReleaseEntryKey` / `TrackId` parameters and the fetch keep working unchanged through the rename.
The `mix-waveforms` vault name and `MixMetadata.WaveformEntryKey` stay (they are still where the high-res
*mix* datum lives — generalizing the *fetch* doesn't require renaming the *mix-specific high-res store*;
see §5). **Acceptance:** the Mix detail page looks and behaves identically after the rename; the only diff
is identifiers.

> **DRY note.** The temptation is to skip the rename and "just reuse `MixWaveformVisualizer` on other
> pages." Resist it: a `MixWaveformVisualizer` mounted on a Cut page is a lie that every future reader has
> to decode, and it cements the wrong mental model right when we're trying to break it. The rename is
> cheap and it *is* the SOLID move — name the thing for its abstraction, not its first caller.

### 3b. What stays specialized (the abstraction boundary)

Generalizing does **not** mean flattening every medium to the same look. The clean Liskov boundary:

- **Shared (the engine):** the renderer, the bridge, the controls component, the state, the datum
  contract, the playback coupling, the read-only contract. One copy, consumed by all hosts.
- **Per-host (the composition):** *whether* the visualizer is mounted, *where the lava-lamp icon sits* (the
  controls affordance is now a single popover-triggering icon — §3d-revised), and *what datum* the host
  points it at. These ride host composition (slots + parameters), never a `switch (medium)` inside the
  engine. A medium that wants no visualizer mounts none; a host that wants the controls places the lava-lamp
  icon and the identical popover panel follows.

This is the same "variance rides a slot, never a flag" discipline the scaffold already uses for
`Header`/`Hero`/`TopRightAction` (Phase 9 §5.3) — extended to the ambient mount.

### 3c. The hosting seam — an `Ambient` slot on `ReleaseDetailScaffold` (Wave 12.C)

> **Naming note.** This slot was first drafted as `Backdrop`. Daniel pushed back: on the Mix page the
> visualizer is **not** a backdrop — it is the full-bleed centerpiece that *is* the page. "Backdrop" only
> describes what the visualizer does on Cut/Session (ambient environment *behind* the hero+content). The
> slot is named for *that* mode — the **ambient** mode — because Mix doesn't use the slot at all (it keeps
> its own visualizer-is-the-page mount; see §3f). So: `Ambient`, not `Backdrop`.

Today the Mix page mounts the visualizer *outside* the scaffold (a sibling `<MixWaveformVisualizer>` then
a `.mix-detail-foreground` wrapper, with the scaffold inside `MudContainer`). Session mounts nothing.
Cut mounts nothing. To let Cut/Session host the visualizer as ambient environment DRY-ly:

**Add an optional `RenderFragment? Ambient` slot to `ReleaseDetailScaffold`**, rendered as the
full-bleed `position: fixed; inset: 0` layer *behind* the scaffold's content (the scaffold's existing
container becomes the foreground stacking context — promote the `mix-detail-foreground` stacking pattern
into the scaffold so it is the default, not a Mix bespoke). A host that supplies no `Ambient` gets today's
plain background (Liskov: absent slot = no ambient layer, no regression).

- **Cut** (composes the scaffold) supplies `<WaveformVisualizer ReleaseEntryKey=… TrackId=… />` to
  `Ambient` and places the **lava-lamp icon → popover panel** (full parity, §3d-revised) — a living
  waveform field behind the album hero + track list, rendering the *currently selected/playing track's*
  datum, tunable in place via the popover. **§8b is resolved: Cut and Session get the ambient layer AND the
  controls (full parity, not Mix-only).** The ambient *layout* (visualizer behind hero+content) is
  unchanged; the difference from the earlier draft is that the controls are present, and (this revision)
  they live in a popover behind the lava-lamp icon rather than an inline bar.
- **Session** does not compose the scaffold (§3e) — it mounts the ambient visualizer directly behind its
  own hero overlay, the same engine, its own thin full-bleed wrapper.
- **Mix does *not* use this slot.** Mix is the *visualizer-is-the-page* mode (§3f) and keeps its existing
  full-bleed mount as the Phase 10 reframe landed it. **The one controls change for Mix in this revision:**
  its `TopRowCenter` inline knob-bar is replaced by the lava-lamp icon → popover panel (§3d-revised), the
  same affordance every other host now uses. The `TopRightAction` lava-lamp glyph Mix already mounts becomes
  the popover trigger directly — a small, contained change to Mix, not a redesign. The `Ambient` slot is for
  the *ambient* mode only — folding Mix into it would force the slot to carry both "the page" and "behind
  the page," which is the conflation §3f exists to avoid.

**Why the scaffold, not each page.** The full-bleed wrapper, the foreground stacking context, and the
footer-clip plumbing (the dynamic-footer overflow clip from the reframe §2c) are all *chrome*, and the
scaffold is where chrome lives. Putting the ambient layer on the scaffold means the clip logic, the
stacking context, and the mount point are written once. `SessionDetail` is the lone holdout that doesn't
compose the scaffold today — see §3e.

### 3d. Where do the lava controls live? (the controls boundary — popover-hosted, full parity, resolved)

**Revised model (Daniel, 2026-06-17): a single popover-hosted control panel, triggered by the lava-lamp
icon.** *"We have enough [controls] now that I want to design a panel to be hosted in a popover for the
visualizer controls. The lava-lamp toggle should be wired to this popover, so anywhere we can put one Icon
we can put the control surface."* This supersedes the inline knob-*bar* model (where each detail page laid
out its own eight-knob bar). The controls are now **one panel behind one icon**, placed identically on
every host.

The mechanics:

- **The lava-lamp icon is the single affordance.** Click the icon → the control panel pops over. There is
  no separate inline bar and no separate toggle; the icon *is* the toggle and the popover *is* the panel.
  Closed state is just the icon (compact, unobtrusive). Open state floats the panel over the surface.
- **One panel, one popover host, placed by an icon anywhere.** The renamed `WaveformVisualizerControls`
  becomes the **panel content** (the eight RadialKnobs, unchanged). A new `WaveformVisualizerControlPopover`
  host pairs the lava-lamp trigger icon with that panel as its overlay content (MudBlazor `MudPopover` the
  natural substrate). Every host renders exactly this one component; the panel is byte-for-byte identical
  everywhere. **This is the SOLID seam, named precisely:** *one panel component, one popover host, placed by
  an icon anywhere.*
- **Full parity across all four surfaces, the popover way.** Mix, Cut, Session, **and** the NowPlaying card
  each place the lava-lamp icon and get the identical popover panel. There is no per-host control divergence
  left to decide — the variance that *used* to live in "which page shows the bar" is dissolved because every
  host shows the same icon. The only per-host question that remains is *where the icon sits and where the
  popover anchors* (§8e), which is positioning, not presence.
- **Why this is more DRY than the inline bar.** The earlier model asked each detail page to host the
  controls bar in its own layout (Mix in `TopRowCenter`, Cut/Session in the ambient composition, the
  NowPlaying card suppressed because a bar didn't fit). That is three-to-four *different* control
  compositions over one shared state. The popover collapses them to **one** composition — `<icon → popover →
  panel>` — reused verbatim. The awkwardness that justified suppressing controls on the small NowPlaying card
  evaporates: a popover doesn't compete with the card's compact layout the way an inline bar would, because
  it's closed by default and floats when opened. **That is what dissolves §8b-followup** (below).

**§8b-followup is dissolved, not deferred.** The old open sub-question — "do the full-parity controls extend
onto the small NowPlaying card?" — existed *because* an inline eight-knob bar was awkward on a compact card.
With the popover, the card places the same lava-lamp icon as every other host and the panel floats on
demand; there is no compact-layout conflict to weigh. **Resolution: the NowPlaying card gets the icon →
popover like everywhere else — full parity on all four surfaces.** (See §8b-followup, now marked resolved.)

> **State-scoping note (staff-engineer's call at 12.E).** With one shared panel surfaced on every host,
> `WaveformVisualizerControlState` should be **one shared tuning** — a knob turned in the popover on a Cut
> page is the same lava everywhere. This is even more natural under the popover than under the inline-bar
> model: there is literally one panel, so "one set of dials" is the only coherent reading. It matches the
> single-engine framing and is what the persisted `WaveformVisualizerControlState` already is. Flag only;
> not a product blocker.

### 3e. The `SessionDetail` scaffold question

`SessionDetail` deliberately does **not** compose `ReleaseDetailScaffold` (it wraps its own
`MudContainer` + `ReleaseHeroOverlay`). If Session is to host the ambient layer via the scaffold's new
slot, either (a) `SessionDetail` adopts the scaffold (a larger refactor, out of scope here — Session's
divergence was a deliberate Phase 11 call), or (b) Session mounts `<WaveformVisualizer>` directly with
its own full-bleed wrapper (small, local, mirrors what Mix does inline today). **Recommend (b)** if
Session gets the ambient layer at all — don't reopen the Session-vs-scaffold decision for this. Cut *does*
compose the scaffold, so Cut gets the ambient layer for free via the slot. This asymmetry is fine: the
slot serves scaffold-composing pages; the one non-composing page mounts the shared engine directly. The
*engine* is still single-source either way — only the *mount* differs, which is exactly the per-host
variance §3b sanctions. **Under full parity (§3d-revised), Session also places the lava-lamp icon → popover
panel** — the same single affordance every host uses. Because the controls are now one popover behind one
icon (not an inline bar threaded into a page's top row), Session's lack of scaffold composition is a
non-issue for controls: it just places the icon wherever its own chrome puts it. Session is full-parity even
though it doesn't compose the scaffold.

### 3f. Three hosting modes of the one engine (the elaboration §8b asked for)

Daniel's pushback on "backdrop": *"backdrop?? MIXES doesn't really have a backdrop?"* — and he is right.
The one engine is hosted in **three distinct modes**, and "backdrop" only fits one of them. Spelling them
out surface-by-surface, because the slot design and the naming follow from this distinction:

| Mode | Surfaces | What the visualizer *is* on screen | Controls | Mount mechanism |
|------|----------|-----------------------------------|----------|-----------------|
| **A — Visualizer-is-the-page** | **Mix detail** | The full-bleed centerpiece. The lava field **is** the page; the mix details sit *over* it as a thin overlay. You came here to watch the lava. | Lava-lamp icon → popover panel (§3d-revised) | Mix's own full-bleed mount (unchanged from Phase 10 reframe) — **not** the `Ambient` slot |
| **B — Ambient environment** | **Cut detail, Session detail** | Living texture *behind and around* the hero + content. The album cover / track list / session hero is the subject; the waveform is environment that makes the page feel alive. You came here for the release; the lava is atmosphere. | Lava-lamp icon → popover panel (full parity, §3d-revised) | `ReleaseDetailScaffold.Ambient` slot (Cut) / direct full-bleed mount (Session, §3e) |
| **C — Contained live element** | **NowPlaying card** (home) | A small, bounded live panel *inside* the card. Not full-bleed, not "behind" anything — a contained element that shows the real signal of whatever is playing. | Lava-lamp icon → popover panel (full parity — the popover dissolves the old suppression, §3d-revised) | Contained mount, `Fill`-mode canvas sized to the card, not the viewport (§6c) |

**Why three modes and not "backdrop everywhere":** the three differ in *what the user's eye treats as the
subject*. On Mix the visualizer **is** the subject (mode A). On Cut/Session the *release* is the subject
and the visualizer is environment (mode B — this is the only mode that is genuinely a "backdrop"). On the
home card the visualizer is a *bounded live readout* of current playback (mode C). Same engine, same datum
contract, same renderer — three compositions. This is the SOLID payoff stated precisely: the variance is
entirely in **hosting composition** (full-bleed vs. slot vs. contained; viewport-sized vs. container-sized),
never in the engine. **The controls are no longer a per-mode discriminator at all** — under the
popover-hosted model (§3d-revised) all four surfaces place the *same* lava-lamp icon → popover panel. The
only per-mode controls nuance left is *where the icon anchors* (§8e), which is positioning, not presence.

**What this resolves about the slot.** Because Mix (mode A) keeps its own mount and the NowPlaying card
(mode C) is a contained mount, the `ReleaseDetailScaffold` slot serves **mode B only** — and that is why it
is named `Ambient`, not `Backdrop` and not `Visualizer`. A `Visualizer` slot would imply Mix routes through
it too; an `Ambient` slot says exactly what it carries: the behind-the-content environment layer for
scaffold-composing detail pages. Mode A and mode C mount the engine without the slot. The **controls popover
is orthogonal to the slot** — it's an icon any host places, independent of how the *visualizer layer* is
mounted (slot vs. full-bleed vs. contained).

**§8b status: RESOLVED — modes A + B + C all live, with FULL PARITY on the controls across all four hosts
via the popover (Daniel, 2026-06-17).** All three hosting *modes* ship (Mix is the page, Cut/Session are
ambient, the home card is contained) **and** the lava-lamp controls ride every host — Mix, Cut, Session, and
the NowPlaying card — via the single popover-hosted panel (§3d-revised). The earlier "mode 1 (Mix +
NowPlaying only, Cut/Session plain)" fallback is closed; the earlier "controls Mix-only" default is
overridden; and the "is the NowPlaying card controls-suppressed?" sub-question is **dissolved by the
popover** (§8b-followup, now resolved). Each host is a one-visualizer-mount-plus-one-lava-lamp-icon
composition. The remaining open item is the popover's anchor/positioning per host (§8e) — a layout detail,
not a presence decision.

### 3g. Panel styling — the NowPlaying Hero look (theme tokens, no hardcoded hex)

Daniel's styling direction for the popover panel: **"same look and feel as the NowPlaying Hero."** Concretely:

- **Dark-navy primary background** — the same navy ground the NowPlaying Hero card sits on.
- **Green-accent knobs** with **light-colored icons**.
- **Muted-navy filler / circular border fill** around the knobs.

These map directly onto the existing token layer
(`DeepDrftShared.Client/wwwroot/styles/deepdrft-tokens.css` — the single source of truth; the Phase 10
reframe's "one source, no hardcoded hexes" discipline holds). The mapping staff-engineer should pull from:

| Panel element | Daniel's intent | Token (source of truth) | Value (for reference only — *use the token, not the hex*) |
|---------------|-----------------|--------------------------|-----------------------------------------------------------|
| Panel background | dark-navy primary | `--deepdrft-navy` (ground) / `--deepdrft-navy-mid` (elevated panel) | `#112338` / `#17283f` |
| Knob accent / active arc | green accent | `--deepdrft-green-accent` | `#3D7A68` |
| Knob icons / labels | light-colored | `--deepdrft-white` | `#FAFAF8` |
| Knob filler / circular border fill | muted-navy | `--deepdrft-navy-mid` (fill) / `--deepdrft-muted` (border-muted) | `#17283f` / `#8A9BB0` |
| Panel border / divider | subtle | `--deepdrft-border` (or the dark-theme `rgba(250,250,248,0.10)` divider) | — |

The NowPlaying Hero card itself (`NowPlayingCard.razor.css`) renders on a translucent off-white wash
(`rgba(250,250,248,0.06)` over the page's navy ground, `backdrop-filter: blur(8px)`) with the green accent
on its label/dot (`--deepdrft-green-accent`) and off-white title (`--deepdrft-white`). The panel should read
as a sibling of that card — the same navy-ground/green-accent/off-white vocabulary, the same restrained
translucency-over-navy feel — so a popover opened anywhere looks like it belongs to the same family as the
home card. **Staff-engineer pulls the tokens above; no inline hexes.** If a token is missing for a needed
shade (e.g. a knob's inactive track fill), prefer a `color-mix()` over the nearest token to a new hardcoded
value, and flag the gap rather than minting an untokened hex.

> **Note on the MudBlazor palette.** The MudBlazor theme (`DeepDrftShared.Client/Common/DeepDrftPalettes.cs`,
> applied via `<MudThemeProvider Theme="DeepDrftPalettes.Default">`) carries the *same* navy/green/off-white
> vocabulary in its `PaletteDark` (`Primary = #3D7A68` green-accent, `Background = #0D1B2A` navy,
> `Surface = #162437` navy-mid, `TextPrimary = #FAFAF8` off-white). For MudBlazor-component-level theming
> inside the popover, prefer `Color="Color.Primary"`/`Color.Surface` against that palette over CSS overrides
> where the component supports it; reach for the CSS `--deepdrft-*` tokens for the bespoke knob/panel chrome
> the RadialKnob owns. Either way the colors trace back to one source — the tokens file and the palette are
> the same vocabulary — so there is no third place to hardcode.

---

## 4. The bridge, generalized (Wave 12.B)

The bridge (`WaveformVisualizer.razor.cs`, ex-`MixWaveformVisualizer`) needs **one** real change beyond
the rename: its datum fetch must resolve the high-res datum for the **current track**, not for a Mix
release.

Today: `await ReleaseData.GetMixWaveform(ReleaseEntryKey)` → resolves per-release, 404 for non-Mix →
blank. This is the per-*release* fetch the model correction supersedes.

**Generalized (per-track model).** The visualizer renders *the currently playing/selected track's*
waveform, and the bridge already knows that track — it is the `TrackId` it gates `IsActivePlayer` on. So
the fetch should be keyed to the **track**, with the release as addressing context. Two shapes for the
fetch are viable; the spec recommends the first and leaves the exact route to staff-engineer (§5):

- **Track-keyed (recommended):** `await ReleaseData.GetTrackWaveform(trackEntryKey)` → returns the
  per-track high-res datum from the generalized waveform vault, keyed by the track's `EntryKey`. This is the
  cleanest expression of "the datum is the track's." The bridge needs the current track's `EntryKey`; it
  currently carries `TrackId` (the SQL PK) — see §5 on reconciling `TrackId` → `EntryKey`.
- **Release-addressed, track-resolved:** `GET api/release/{releaseEntryKey}/track/{trackId}/waveform` — the
  release locates the track, the track owns the datum. Heavier route, but keeps the release as the
  addressing root the bridge already holds (`ReleaseEntryKey`). Use this if resolving `TrackId` → `EntryKey`
  client-side is awkward.

The bridge stays otherwise identical:

- **Re-fetch key changes from release to track.** The fetch-once guard must re-arm when the **track**
  changes, not (only) when the release changes — because on a multi-track Cut the release is fixed while the
  track scrolls. (Today's guard keys on `_loadedReleaseKey == ReleaseEntryKey`; under the per-track model it
  keys on the current track identity.) This is the one bridge subtlety the model correction introduces — and
  it is *exactly* the behavior the NowPlaying card already needs (§6c), so the two converge.
- Still derives duration from the cascaded player (`PlayerDurationSeconds`) — the duration source is the
  *player*, which already reflects the current track, so the time↔sample mapping is correct per-track for
  free.
- Still gates playback coupling on `TrackId` via `IsActivePlayer`.
- Still pushes the seven control dials, the datum, playback, and theme through the unchanged handle.

The `PlaybackPosition` composability fallback (the no-player-cascade path) stays as the documented escape
hatch.

**No open data *question* remains here** — Direction B (committed, §5) means every track has a high-res
datum, so the fetch always resolves to high-res. The open *items* the model created are operational, not
design: the backfill (§8a-new) and the upload-time compute cost (§8b-new).

---

## 5. The datum source — Direction B, per-track (committed)

The visualizer renders a **loudness datum + a duration**. Daniel's model correction makes the datum
**per-track**: every track carries its own high-res waveform datum; the release is just the host. And
Daniel chose **Direction B**: compute that high-res datum for **all** media, not the 512-bucket fallback.
Together these settle §5 — no remaining product call here; the work is a generalization + a backfill.

**Datums in the system today:**

| Datum | Vault | Resolution | Who has it today | Keyed by |
|-------|-------|-----------|------------------|----------|
| Per-track profile | `waveform-profiles` | **512 buckets** (fixed) | **every track** (computed at upload) | track `EntryKey` |
| High-res datum | `mix-waveforms` | **~333 samples/sec** (duration-derived) | **Mix tracks only** (CMS-triggered) | `MixMetadata.WaveformEntryKey` (= the mix track's `EntryKey`) |

The 512-bucket profile already exists per-track and stays (the player-bar `WaveformSeeker` consumes it —
untouched). The high-res datum is the one the lava visualizer wants, and today only Mix tracks have it.
**Direction B closes that gap per-track for every medium.**

### 5a. The generalization (Direction B), end to end

Direction B is **not** just a new endpoint — under the per-track model it touches five surfaces:

1. **Content compute path.** Generalize the duration-derived high-res compute (`WaveformProfileService` +
   `MixWaveformResolution`) off its Mix-only framing so it computes a per-track high-res datum for **any**
   track from that track's audio. The *algorithm* is unchanged (the same ~333 samples/sec duration-derived
   model `MixWaveformResolution` already defines); only its *applicability* widens from "the mix track" to
   "any track." Store keyed by the track's `EntryKey`.
2. **Store / vault.** The high-res datum becomes per-track-cardinal. Either rename/repurpose the
   `mix-waveforms` vault to a medium-neutral `track-waveforms` (cleaner; consistent with the rename
   discipline) or keep the vault and drop the Mix-only key assumption — staff-engineer's call, but the
   *name* should stop saying "mix" once it holds every track's datum. `MixMetadata.WaveformEntryKey` is no
   longer the only pointer; the datum keys directly off the track's `EntryKey` (the same key the
   `waveform-profiles` profile uses).
3. **Upload path.** `UnifiedTrackService.UploadAsync` already computes the 512-bucket profile for every
   track at upload; **add the per-track high-res compute alongside it** so every *new* track gets its
   high-res datum at upload time, no CMS step required. (Cost flagged — §8b-new.)
4. **CMS generate action.** Generalize the Mix-only "generate waveform" trigger to **any track** — so a
   CMS operator can (re)generate a track's high-res datum on demand (the backfill mechanism, and the repair
   path if a compute fails at upload). This is the action that drives the backfill (§8a-new).
5. **Backfill.** Every *existing* track that predates Direction B has a 512-bucket profile but no high-res
   datum. Backfill computes the per-track high-res datum for all of them. **This is a real operational
   step, Daniel-gated** — see §8a-new (does it run as a one-shot migration/script, or as a CMS batch
   action over the generalized generate action?).

### 5b. The per-track datum fetch endpoint

Under the per-track model the fetch resolves a datum for a **track**, addressed with the release as
context. **Recommended shape:** `GET api/track/{trackEntryKey}/waveform` (track-cardinal, on
`TrackController`) returning the per-track high-res datum, 404 → graceful blank. This is the natural home
once the datum is the track's, and it sits beside the existing per-track surfaces.

- **Reconciling with the bridge's identifiers.** The bridge today carries `ReleaseEntryKey` + `TrackId`
  (the SQL PK), not the track's `EntryKey`. The fetch needs the track's `EntryKey`. Two ways to bridge:
  (i) the player/track view-model the bridge already cascades exposes the current track's `EntryKey`
  (preferred — the player knows the track it loaded, and `EntryKey` is how audio is keyed), so the bridge
  reads `CurrentTrack.EntryKey` directly; or (ii) keep the release as the addressing root and resolve
  server-side — `GET api/release/{releaseEntryKey}/track/{trackId}/waveform` — if the client can't cleanly
  reach the track's `EntryKey`. **Recommend (i)**; it makes the route track-cardinal and matches the datum's
  ownership. Final identifier plumbing is staff-engineer's, but the *route should be track-cardinal*.
- **`/mix/waveform` fate.** The Mix-gated `GET api/release/{entryKey}/mix/waveform` can be retired (folded
  into the track-cardinal fetch — the Mix's single track resolves through the same path) or kept as a thin
  delegate during transition. Staff-engineer's call; the spec's intent is **one** track-cardinal fetch.
- `IReleaseDataService` (or a track-cardinal `ITrackDataService` if cleaner) gains `GetTrackWaveform(
  trackEntryKey)`; the bridge calls it.

**The road not taken (one-line, per Daniel).** A cheaper v1 — serve the existing 512-bucket per-track
profile to non-Mix surfaces and skip new compute entirely ("Direction A") — was considered and **declined**
in favor of uniform high-res. Recorded so the trade isn't relitigated: A was zero-compute but coarse for the
lava; Daniel chose quality + uniformity over the cheaper road.

---

## 6. Overhauling the NowPlayingHero (Wave 12.D)

### 6a. What's there now

`NowPlayingCard.razor` shows the now-playing title/sub plus, when `Player.IsLoaded`, a `waveform-bars`
div of **20 hardcoded `<div class="waveform-bar">`** elements, each with fixed `--h-lo/--h-hi/--dur` CSS
custom properties driving a CSS keyframe bounce. It is **purely synthetic** — no audio data, no coupling
to the actual signal; it bounces on a fixed loop whenever *anything* is loaded. This is the "stochastic
bullshit."

### 6b. The rewire — mount the real engine, driven by live playback

Replace the `waveform-bars` block with a mounted **`<WaveformVisualizer>`** scoped to the card, driven by
the **live cascaded player**:

- The NowPlaying card already cascades `IStreamingPlayerService` (it reads `Player.CurrentTrack`).
- Mount `<WaveformVisualizer ReleaseEntryKey="@Player.CurrentTrack.Release?.EntryKey"
  TrackId="@Player.CurrentTrack.Id" />` — the same bridge, the same playback coupling. Because the card is
  *inside* the cascade, `IsActivePlayer` is true for whatever is actually playing, and the visualizer
  scrolls/animates to the *real* signal. When nothing is playing, it sits at its at-rest slice (the
  bridge already handles the no-active-player state → static), which replaces the `waveform-placeholder`.
- The datum comes from §5's per-track fetch keyed on the **current track** (`CurrentTrack.EntryKey`) — so
  the home card shows the real high-res waveform of whatever track the listener started, Mix or not. This
  is the cleanest illustration of the per-track model: the card has no "release" of its own to speak of; it
  follows the *track*, and the datum is the track's.

**This is the payoff of the generalization:** the NowPlaying card is *just another host* (mode C, §3f) of
the same engine, pointed at "whatever track is playing right now." No NowPlaying-specific renderer, no fork
— the DRY win the brief demands. And because the datum is per-track and every track now has a high-res
datum (Direction B), the home card renders at full fidelity for *any* track, not just Mixes.

### 6c. Constraints specific to the NowPlaying context

- **Live, not static.** Unlike a single-track detail page, the NowPlaying card's **track** changes as
  playback advances. Under the per-track model (§4) the fetch-once guard re-arms on **track** change (not
  release change), so track-change → new per-track datum is handled — and this is the *same* guard change
  the per-track model already requires for a multi-track Cut (where the release is fixed but the track
  scrolls). The two converge on one behavior: **re-fetch when the current track's identity changes.**
  Verify the guard keys on the current track, not the release.
- **Small surface, popover controls (full parity via the icon).** The card is a small hero panel, not a
  full-bleed page — but under the popover-hosted model (§3d-revised) that is no longer a reason to suppress
  controls. The card places the **same lava-lamp icon → popover panel** as every other host; the panel
  floats over the page on demand and doesn't compete with the card's compact title/sub layout the way an
  inline eight-knob bar would have. **This is what dissolved §8b-followup** — the popover removed the
  awkwardness that justified suppression. The card's lava-lamp icon sits in/near the card chrome (its anchor
  is one of the §8e positioning calls — a small card is the tightest anchor case). The visualizer canvas is
  still sized to the card, not `position: fixed` (§6c container-sizing below). **Flagged for staff-engineer
  (§8d):** the renderer's footer-clip + full-viewport assumptions (`position: fixed; inset: 0`,
  clip-to-footer) are
  written for a full-page backdrop; mounting it in a *contained card* needs the canvas to size to its
  container instead of the viewport. This is a real renderer-hosting wrinkle — the engine assumes
  full-window today. Either (i) parameterize the visualizer's sizing (full-viewport vs.
  fill-container), or (ii) the NowPlaying card is the forcing function to make the canvas
  container-relative. **Recommend (i)** — a `Fill` mode parameter — because it also future-proofs any
  contained mount (an embed, a CMS preview). This is the one genuine engineering subtlety in the
  NowPlaying rewire; everything else is composition.
- **Performance.** A WebGL2 lava render on the *home page* (the highest-traffic, first-paint surface) is a
  heavier ask than on a detail page the user navigated to deliberately. Keep the existing `MAX_DPR = 2`
  cap and the `isPlaying`-gated rAF loop (it burns no frames when nothing plays — so an idle home page
  pays nothing). If the lava is too heavy for the home card specifically, the card's mount
  can run a *cheaper* preset (fewer blobs) via a density default — but do not fork the renderer;
  use the existing density dial (now adjustable from the card's own popover too). **Flagged, not committed
  (§8d).**

### 6d. What the card keeps

The title/sub text block (`np-title`/`np-sub`) and the "Now Playing" label stay — only the synthetic
`waveform-bars` / `waveform-placeholder` block is replaced by the mounted visualizer. The pulsing
`circle-deco` rings in `NowPlaying.razor` are unrelated decor; leave them.

---

## 7. Wave decomposition + dependency shape

Sequenced so the mechanical rename de-risks everything, the per-track high-res generalization (now the
heaviest wave) unblocks the new hosts, and the NowPlaying rewire (the trickiest, per §6c) comes last on a
proven engine. **Direction B re-costs 12.B materially** — it is no longer "a new endpoint" but a
content + upload + CMS + backfill + fetch slice; the spec splits it into 12.B1 (compute generalization +
backfill) and 12.B2 (the bridge's per-track fetch) so the heavy data work and the thin client rewire can be
sequenced and tested apart.

**The controls work now consolidates into its own wave, 12.E** (the popover-panel revision). Under the
inline-bar model the controls were threaded into each detail-page mount, so 12.C carried "ambient layer +
controls bar" together. The popover model **pulls the controls out of the per-host mounts into one shared
component** built once (12.E) and *placed* by each host (an icon, §3d-revised). This is cleaner: 12.C and
12.D each reduce to "mount the visualizer layer + place the lava-lamp icon," and the panel/popover/styling
all live in one wave. 12.E depends only on the rename (12.A) — it touches the controls component + a new
popover host + the panel styling, none of which need the per-track fetch — so it can run in parallel with
the data work (12.B1/12.B2).

- **12.A — Rename to the abstraction (mechanical, no behavior change).** `Mix*` → `Waveform*` across the
  five files + the TS module + the import path + the DI registration (§3a). **Load-bearing prerequisite**
  — every later wave references the generalized names. Acceptance: Mix detail behaves identically;
  diff is identifiers only.
- **12.B1 — Generalize the high-res compute to every track + backfill (the data change, Direction B).**
  Generalize the duration-derived compute off Mix-only (`WaveformProfileService` / `MixWaveformResolution`),
  store per-track keyed by `EntryKey` in the (renamed) `track-waveforms` vault, add the per-track high-res
  compute to `UnifiedTrackService.UploadAsync`, generalize the CMS generate action to any track, and run the
  **backfill** for existing tracks (§5a, §8a-new — backfill is Daniel-gated). **Independent of 12.A** (it is
  server/content-side, no renamed client identifiers). Acceptance: every track — Mix, Session, and each Cut
  track — has a high-res datum; new uploads get one automatically; the generate action works for any track.
- **12.B2 — Per-track datum fetch + bridge rewire.** New track-cardinal `GET
  api/track/{trackEntryKey}/waveform` (§5b); `GetTrackWaveform`; bridge resolves the *current track's*
  `EntryKey` and re-fetches on track-change (§4). **Depends on 12.A (renamed bridge) + 12.B1 (a datum to
  fetch).** Acceptance: the Mix detail page renders the same high-res lava via the new track-cardinal fetch;
  a non-Mix track now returns a high-res datum.
- **12.E — Popover-hosted control panel (the controls revision).** Turn the renamed
  `WaveformVisualizerControls` into the **panel content** and build the `WaveformVisualizerControlPopover`
  host pairing the lava-lamp trigger icon with that panel as overlay content (`MudPopover`, §3d-revised).
  Style the panel to the **NowPlaying Hero look** from theme tokens (§3g — no hardcoded hex). Make the
  state-scoping call (one shared `WaveformVisualizerControlState`, §3d-revised note). **Depends on 12.A**
  (renamed controls component) only — no per-track datum needed, so it runs **parallel to 12.B**. Acceptance:
  the lava-lamp icon opens a popover panel with all eight knobs, styled to the Hero look; turning a knob
  drives the visualizer (where one is mounted) via the unchanged `Changed` seam; the panel is one component
  reused everywhere. **This wave is the unit every host then places (§3d-revised).**
- **12.C — `Ambient` slot on the scaffold + mount on detail pages (mode B, §3f).** Promote the
  full-bleed/foreground/footer-clip pattern into `ReleaseDetailScaffold` as an optional `Ambient` slot
  (§3c); Cut mounts the ambient layer **and places the lava-lamp icon → popover** (full parity, §3d-revised);
  Session mounts directly **also full-parity** (§3e). Mix is **unchanged** as a visualizer layer (mode A
  keeps its own full-bleed mount); its only controls change is swapping the inline `TopRowCenter` bar for the
  lava-lamp icon → popover (folded into 12.E's affordance). **Depends on 12.B2** (a datum to render) **and
  12.E** (the popover to place). **§8b is resolved (full parity, no longer gated)** — Cut and Session ship
  with both the ambient layer and the popover controls. Acceptance: Mix unchanged as a layer; Cut and Session
  each show an ambient living layer **with a working lava-lamp icon → popover panel**, rendering the current
  track's datum, no regression to the hero/content.
- **12.D — NowPlayingHero rewire (mode C, §3f).** Replace the synthetic bars with a contained
  `<WaveformVisualizer>` driven by the live player, pointed at the current track (§6); add the
  `Fill`/container-sizing mode (§6c); **place the lava-lamp icon → popover on the card** (full parity, §6c —
  the popover dissolves the old suppression). **Depends on 12.A + 12.B2 + 12.E** (renamed engine + a
  per-track datum + the popover to place). **Independent of 12.C** (different host; doesn't need the scaffold
  slot). Acceptance: the home card shows the *real* high-res waveform of the playing track, sits at-rest when
  nothing plays, and carries the lava-lamp icon → popover like every other host; no synthetic bars remain.

**Dependency shape:** `12.A → 12.B2 → (12.C ‖ 12.D)`, with **12.B1 a parallel server-side track** that
12.B2 depends on (`12.B1 → 12.B2`) but that can start cold day one (it needs no renamed client identifiers),
and **12.E (the popover controls) a third parallel track** depending only on 12.A — both 12.C and 12.D now
also depend on 12.E to place the icon. So: `12.A → {12.B1 → 12.B2, 12.E}`, then `(12.B2 ∧ 12.E) → (12.C ‖
12.D)`. 12.A is the cheap mechanical unblock; **12.B1 is the load-bearing heavy** (compute + backfill); 12.B2
is the thin client rewire that consumes it; **12.E is the controls consolidation** (one panel + popover +
styling, built once); 12.C (detail-page ambient hosts) and 12.D (NowPlaying host) are independent siblings
that each mount a visualizer layer and place the popover. The cold-start items are **12.A** (touches
everything, risks nothing), **12.B1** (the data work — start it early; the backfill gate is the only thing
blocking it), and **12.E** (the controls panel — independent of all the data work). With §8b resolved to
full parity *and* the popover dissolving §8b-followup, 12.C and 12.D carry no controls-suppression branch —
every host places the same icon.

---

## 8. Open product decisions

**Resolved by Daniel (2026-06-17), kept visible per file convention:**

- **§8a (was: non-Mix datum resolution) → Direction B.** High-res compute for **all** media, not the
  512-bucket fallback. The cheaper "Direction A" (serve the existing 512-bucket profile to non-Mix) is the
  road not taken (§5). Re-cost lands in 12.B1 (compute + backfill).
- **§8c (was: multi-track Cut's waveform) → dissolved by the per-track model.** The datum is per-track; the
  visualizer renders the *current* track's datum, so there is no release-level "which track represents the
  album" choice to make. The old "first track by `TrackNumber`" recommendation is moot.
- **§8b (Cut/Session hosting + controls) → RESOLVED: all three modes live, FULL PARITY on controls, now
  POPOVER-HOSTED.** Daniel chose **option (3) — full parity on the lava controls** (over option (2) Mix-only
  controls): the visualizer rides every Release Detail host (Mix, Cut, Session) **and** the controls ride all
  three. The three-hosting-mode *layout* framing (§3f — visualizer-is-the-page on Mix, ambient on
  Cut/Session, contained on NowPlaying) is retained; "backdrop" stays retired as the wrong word for Mix.
  **Controls-hosting revision (2026-06-17):** the controls are no longer an inline knob *bar* per page — they
  are a **single popover-hosted panel** triggered by the lava-lamp icon, placed identically on every host
  (§3d-revised). The earlier mode-1 fallback (Mix + NowPlaying only) and the "controls Mix-only" default are
  both closed. The sub-question that spun out (§8b-followup) is now **resolved by the popover** (below).

- **§8b-followup (do full-parity controls extend onto the NowPlaying card?) → RESOLVED by the popover.**
  This sub-question existed *because* an inline eight-knob bar was awkward on a small, compact, high-traffic
  card — so the earlier draft kept the card controls-suppressed by default and flagged the question for
  Daniel. **The popover-hosting revision dissolves it:** the card places the same lava-lamp icon as every
  other host, and the panel floats on demand rather than occupying the card's layout. There is no
  compact-layout conflict left to weigh. **Resolution: the NowPlaying card gets the icon → popover like
  everywhere else — full parity on all four surfaces** (Mix, Cut, Session, NowPlaying card). The only
  remaining controls nuance is *where the icon anchors per host* — a positioning detail, now §8e.

**Newly open (created by the popover revision and by Direction B + the per-track model):**

**§8e — Popover anchor/positioning per host (created by the popover revision).**
The popover dissolves the *presence* question but creates a *placement* one: where does the lava-lamp
trigger icon sit, and where does the panel anchor, on each host? The hosts differ enough that one anchor
rule may not fit all:
- **Mix (mode A, full-bleed)** — the icon can take Mix's existing `TopRightAction` slot (it already mounts
  the lava-lamp glyph there); the panel anchors off that corner over the full-bleed field. Cleanest case.
- **Cut/Session (mode B, ambient)** — the icon rides the page chrome (Cut via the scaffold, Session via its
  own composition); the panel floats over hero+content. Needs to clear the hero overlay and not collide with
  the share/play affordances already in the top row.
- **NowPlaying card (mode C, small contained)** — the tightest case. A small card has little room for an
  icon, and a popover panel sized to the Hero look may be larger than the card itself — so the panel likely
  anchors to the icon but **overflows the card bounds** (which is fine — popovers float above), yet its open
  position must not cover the title/sub it's meant to accompany. Worth a deliberate anchor choice (e.g.
  open upward/leftward away from the card body).
**Recommend:** a single `WaveformVisualizerControlPopover` with a per-host *anchor origin* parameter
(MudPopover already exposes `AnchorOrigin`/`TransformOrigin`), defaulted sensibly and overridden per host —
not a forked popover. This keeps one component while letting each host place it well. **Staff-engineer-owned
layout call; flagged for Daniel only because the card case (mode C) may look cramped and is worth a glance
in review.** Does not block the phase; the default anchor is shippable and tunable.

**§8a-new — How does the high-res backfill run, and is it gated?**
Direction B means every *existing* track needs its per-track high-res datum computed once. Two shapes:
(1) a **one-shot migration/script** run against the content store (like the Daniel-gated EF migrations in
Phase 11 — authored, applied on Daniel's go-ahead); or (2) a **CMS batch action** over the generalized
generate action (an operator triggers "generate all," visible progress, re-runnable for failures). **Recommend
(2)** — it reuses the generalized generate action 12.B1 builds anyway, needs no separate one-shot tooling,
and the CMS already has the generate-status surface (Phase 8 §8.2's in-grid status column + generate
actions) to hang it on. **This is a Daniel-gated operational step either way** — like the Phase 11 release
migrations, the backfill is authored in 12.B1 but *run* on Daniel's word. Open: which shape, and when it
runs relative to shipping the per-track fetch (the fetch 404s gracefully for not-yet-backfilled tracks, so
the fetch can ship before the backfill completes — the visualizer just blanks until a track is backfilled).

**§8b-new — Per-track high-res compute at upload: perf + storage cost (flag for Daniel).**
Direction B adds a duration-derived high-res compute (~333 samples/sec) to **every** track upload, on top
of the existing 512-bucket profile. Two costs to surface:
- **Upload latency.** The high-res compute walks the full audio at upload time. For a long Mix this is
  already paid (Mixes compute it today); for short Cut tracks it is small. But it is now on the *upload hot
  path* for every track, where before only the cheap 512-bucket profile was. If upload latency matters,
  the compute could be **deferred** (upload returns; high-res computes async/queued, the generate action
  the backfill uses doubling as the async worker) rather than inline. **Recommend: inline for now**
  (simplest; the 512-bucket compute is already inline and the high-res isn't dramatically heavier for
  typical tracks), flag deferral as the escape hatch if upload feels slow.
- **Storage.** A per-track high-res datum (~333 samples/sec × duration) is materially larger than the
  fixed 512-bucket profile, and now *every* track stores one (a multi-track Cut stores N of them). For an
  album of short tracks this is modest; for a library of long Sessions it adds up. **Not a blocker** — disk
  is cheap and the datum is loudness samples, not audio — but flagged so the storage growth from "every
  track gets a high-res datum" is a known consequence, not a surprise. **No action recommended;** surface
  only.

**§8d — NowPlaying container-sizing + home-page performance (engineering subtleties, staff-engineer-owned
but flag for Daniel).** The renderer assumes full-viewport (`position: fixed; inset: 0`, clip-to-footer);
the NowPlaying card (mode C, §3f) needs it container-relative (§6c) — recommend a `Fill` mode parameter.
And a WebGL2 lava render on the home page's first paint is heavier than on a detail page — the
`isPlaying`-gated rAF means an idle home page pays nothing, but a cheaper blob-density preset for the card
is a fallback if needed. Neither blocks; both are tuning/hosting calls surfaced so Daniel isn't surprised
by a lava lamp on the landing page.

---

## 9. Acceptance criteria (observable)

1. **Rename clean.** The engine is named for its abstraction (`WaveformVisualizer*`); the Mix detail page
   is visually and behaviorally identical to before the rename.
2. **Per-track high-res datum everywhere (Direction B).** Every track — Mix, Session, and each track of a
   Cut — has a high-res duration-derived datum keyed by its `EntryKey`; new uploads get one automatically;
   the generalized CMS generate action produces one for any track; the backfill has populated all
   pre-existing tracks.
3. **Track-cardinal fetch.** `GET api/track/{trackEntryKey}/waveform` (or the agreed track-cardinal route)
   returns the current track's high-res datum, 404 → graceful blank. The bridge resolves the *current
   track* and re-fetches on track change.
4. **Mix unchanged as a layer (mode A); controls via popover.** The Mix detail page still renders the
   high-res lava visualizer-is-the-page at parity with the Phase 10 reframe — now via the track-cardinal
   fetch. Its controls are reached through the **lava-lamp icon → popover panel** (replacing the inline
   `TopRowCenter` bar), the same affordance every other host uses.
5. **Ambient + popover controls on Cut/Session (mode B, full parity).** A Cut and a Session detail page show
   an ambient living waveform layer rendering the *currently selected/playing track's* datum, **with a
   working lava-lamp icon → popover control panel** (full parity — controls present and tunable in place via
   the popover), with no regression to the hero/content.
6. **NowPlaying is real (mode C), with popover controls (full parity).** The home NowPlaying card shows the
   *actual* high-res waveform of the playing track (scrolls/animates to the real signal, changes with track
   changes), sits at-rest when nothing plays, and **carries the lava-lamp icon → popover panel like every
   other host** (§8b-followup resolved by the popover). No hardcoded synthetic bars remain.
7. **One popover-hosted control panel, placed everywhere.** There is exactly one
   `WaveformVisualizerControls` panel and one `WaveformVisualizerControlPopover` host; Mix, Cut, Session, and
   the NowPlaying card all place the *same* lava-lamp icon → popover and get the *identical* panel — verified
   by no per-host control fork. The panel is styled to the NowPlaying Hero look (§3g) from theme tokens, no
   hardcoded hex.
8. **One engine, three modes.** Mix (mode A), the Cut/Session ambient layer (mode B), and the NowPlaying
   card (mode C) all consume the *same* `WaveformVisualizer` component + renderer + state — verified by
   there being exactly one of each, no per-host fork; the differences are hosting composition only
   (full-bleed vs. `Ambient` slot vs. contained; viewport- vs. container-sized). Controls are uniform across
   all four hosts (the single popover panel, criterion 7).
9. **Read-only everywhere.** No host (including the NowPlaying card) exposes a seek/scrub/transport via
   the visualizer; the read-only contract holds on every mount. The popover panel exposes only the eight
   tuning dials — no transport.
10. **Bridge intact, re-keyed to track.** Single-owner handle, idempotent datum guard, `IsActivePlayer`
   gating, and the `isPlaying`-gated rAF loop are unchanged across all mounts; the datum guard now re-arms
   on **track** change (the multi-track Cut and the NowPlaying card both rely on this — they converge).
