` above transport controls when Theater is on and a release is playing.
- **Three detail pages** (`CutDetail.razor`, `MixDetail.razor`, `SessionDetail.razor`): page-level `@if (!VisualizerControlState.TheaterMode)` gates content regions on each page individually (not in `ReleaseDetailScaffold`, so Session — which does not use the scaffold — is covered identically). Each page's top action cluster hosts `` in a `.dd-detail-top-actions` flex wrapper.
- **`deepdrft-styles.css`**: new `.dd-detail-top-actions` layout-only class (`display:flex; align-items:center; gap:0.25rem`) — no colour; shared by all three pages.
- **`DeepDrftTests/WaveformVisualizerControlStateTests.cs`** (new): unit tests for the `CoerceTheaterMode()` auto-exit invariant.
- **Design memo:** `product-notes/phase-20-theater-mode.md`.
### Phase 20 — Wave 2 — Theater Mode refinements (landed 2026-06-21)
**Landed:** 2026-06-21 on dev.
- **What:** Three refinements to the base Phase 20 feature. (1) **Full-screen detail body:** each detail page's foreground container gained `.dd-detail-fill` (`min-height: calc(100vh - var(--deepdrft-nav-height, 88px))`), so the visualizer reads as full-screen and the footer is pushed below the fold regardless of Theater Mode. (2) **Eased collapse (no pop):** the hard `@if` content-hide on the three detail pages was replaced by a `.dd-theater-collapsible` / `.dd-theater-collapsible-inner` wrapper pair that receives `.dd-theater-collapsed` when `IsContentHidden` is true — animates `grid-template-rows: 1fr → 0fr`, `opacity`, and `visibility` (deferred via `transition-behavior: allow-discrete`) so Theater ON/OFF eases rather than pops; `prefers-reduced-motion` collapses instantly. The same wrapper pattern drives the player-bar `NowShowingPanel`, which is now kept mounted whenever a release is playing and collapsed (not `@if`-removed) when Theater is OFF — enabling the ease-in when Theater turns ON (resolves OQ2 design intent for a mounted-but-dormant panel). (3) **Playing-release scoping:** Theater Mode now only applies to the currently-playing release. `ReleaseDetailBase` and `CutDetailBase` each gained a cascaded `IStreamingPlayerService PlayerService`, a reference-guarded `StateChanged` subscription (disposed in `Dispose`), and three predicates: `IsThisReleasePlaying` (`CurrentTrack?.Release?.EntryKey == EntryKey`), `IsContentHidden` (`TheaterMode && IsThisReleasePlaying`), `ShowTheaterToggle` (`(LavaEnabled || WaveformEnabled) && IsThisReleasePlaying`). `TheaterModeToggle.razor` gained an `Available` parameter (default `true`) folded into its render gate; all three pages pass `Available="ShowTheaterToggle"`. A detail page whose release is not playing shows no toggle and ignores the global `TheaterMode` flag.
---
## Phase 18 — Theme / Dark-Mode Remediation (landed 2026-06-19)
**Landed:** 2026-06-19 on dev (Wave 1 + Wave 2 + Wave 3).
- **What:** A DRY token pass resolving six theming symptoms (five in dark mode, one in light) that all traced to three root causes: neutral page surfaces bound to constant brand tokens, the play chip bound to a constant light-grey, and no theme-aware popover-surface token. Resolved as one coherent pass via a shared token layer rather than per-component patches.
- **Why:** Symptom consolidation and root-cause analysis showed all six symptoms shared the same underlying structure — component CSS bypassing the theme-aware alias layer and binding constant source tokens directly. A single additive token pass in `deepdrft-tokens.css` plus targeted re-pointing of consumers fixes all six without scattering dark-mode rules.
- **Shape:**
- **Token foundation (`deepdrft-tokens.css`):** Three new theme-aware token families added to `DeepDrftShared.Client/wwwroot/styles/deepdrft-tokens.css`, each defined in both `:root` (light) and `.deepdrft-theme-dark` (dark):
- `--deepdrft-page-surface` / `--deepdrft-page-text` / `--deepdrft-page-text-muted` — neutral page surface family. Light: `--deepdrft-white` / `--deepdrft-navy` / `--deepdrft-muted`. Dark: `var(--mud-palette-background)` (#0D1B2A, the true page ground) / `--deepdrft-white` / `color-mix(muted 70%, white)` — neutral sections dissolve into the site background as one continuous dark field rather than reading as raised panels.
- `--deepdrft-play-chip` / `--deepdrft-play-glyph` / `--deepdrft-play-chip-soft` — play-chip family. Light: soft-grey chip (matching prior `--deepdrft-soft`). Dark: `--deepdrft-green-accent` chip + `--deepdrft-navy` glyph (navy-on-green for solid chips); `--deepdrft-play-chip-soft` is `color-mix(green-accent 30%, transparent)` (the player-bar translucent override).
- `--deepdrft-popover-surface` — popover surface. Light: `color-mix(navy 8%, white)` soft desaturated-navy wash. Dark: `#162437` (pixel-identical to `DeepDrftPalettes.Dark.Surface` — dark popovers unchanged, only light is retoned).
- **Neutral-surface inversion (T2):** `Home.razor.css`, `About.razor.css`, `DeepDrftFooter.razor.css` re-pointed from constant `--deepdrft-white`/`--deepdrft-navy` to `--deepdrft-page-surface`/`--deepdrft-page-text`. Decorative navy/green sections (`.section-dark`, `.split-left`, `.cta-banner`, hero overlays) untouched — classification encoded in which token each section binds.
- **Play-chip theming (T3):** `PlayStateIcon.razor.css` `.icon-container` re-pointed to `--deepdrft-play-chip`; glyph to `--deepdrft-play-glyph`. Player-bar context overrides chip to `--deepdrft-play-chip-soft` (translucent green wash). Light-mode parity and connect-option hover also corrected.
- **Popover surface (T4):** `deepdrft-styles.css` binds `--deepdrft-popover-surface` to the MudBlazor default popover surface. Bespoke dark-glass panels (`--deepdrft-panel-ground`) untouched.
- **Wave 2 refinements (on top of T1–T4):** App bar background moved to navy (`#112338`) from near-black (`#0D1B2A`). Neutral page surfaces re-pointed to `var(--mud-palette-background)` (`#0D1B2A`) as the true dark ground — sections dissolve into the body background rather than reading as navy-mid raised panels (resolves Wave 1's open question in favour of ground). Dark-mode hero legibility (superseded in Wave 3 — see below). Play-glyph settled on navy-on-green (solid chips) and green-on-green (player bar, via `--deepdrft-play-chip-soft`).
- **Wave 3 — hero dark-mode legibility fix:** `DeepDrftHero.razor.css` hero text re-worked to bind theme-aware tokens directly in the base rules rather than via `:global(.deepdrft-theme-dark)` overrides (matching the About page's proven pattern). `.hero-title` and `.hero-desc` now bind `--deepdrft-page-text` directly; `.hero-subtitle` (previously bound to the constant `--deepdrft-muted`) now binds `--deepdrft-page-text-muted`, making it theme-aware for the first time. Only `.hero-title em` retains an explicit dark override (`:global(.deepdrft-theme-dark) .hero-title em` → `--deepdrft-green-accent`, lifting the low-contrast `--deepdrft-green` on the dark ground). Global hero-button dark treatment added to `deepdrft-styles.css`: `.deepdrft-theme-dark .btn-primary` → `--deepdrft-green-accent` fill + `--deepdrft-navy` text (hover: `--deepdrft-green-interactive`); `.deepdrft-theme-dark .btn-ghost` → `--deepdrft-page-text` color + `--deepdrft-border-light` border.
- **Open questions resolved:** Dark neutral surface = ground (continuous field, `--mud-palette-background`) — not elevated navy-mid. Popover target: `color-mix(navy 8%, white)` in light; dark binds `#162437` (MudBlazor dark Surface) unchanged.
- **Design memo:** `product-notes/theme-dark-mode-remediation.md`.
### Phase 18 — Wave 4 — Popover-surface retune + portaled-popover body-class bridge (landed 2026-06-20)
**Landed:** 2026-06-20 on dev.
- **What:** Follow-on retune of `--deepdrft-popover-surface` values and a root-cause fix for portaled MudBlazor popovers that were never reaching the dark token.
- **Why:** Wave 1–3 shipped `--deepdrft-popover-surface` light at `color-mix(navy 8%, white)` (too saturated — read as a grey slab) and dark at flat `#162437`. More importantly, MudBlazor popovers portal to ``, outside the `.deepdrft-theme-dark` wrapper ``, so the dark token never applied to them at all. Both needed fixing as a pair.
- **Shape:**
- **Token retune (`deepdrft-tokens.css`):** Light value changed from 8% → 4% navy mix (near-page-background, clearly light). Dark value changed from `#162437` to `color-mix(in srgb, var(--deepdrft-navy-mid) 80%, var(--deepdrft-green-accent) 20%)` — a bluer navy with a slight green accent. Dark value hoisted into a new source token `--deepdrft-popover-surface-dark` (defined once in `:root`), referenced by both the `.deepdrft-theme-dark` wrapper block and a new `body.deepdrft-theme-dark` block so portaled content is reached from either selector.
- **Portaled-popover body-class bridge (`MainLayout.razor` + new TS module):** `MainLayout.razor` now stamps/removes `deepdrft-theme-dark` on `` after each render via a new `DeepDrftShared.Client/Interop/theme/theme.ts` module exporting `setBodyThemeClass(isDark: boolean)`. Lazy-imported as `_content/DeepDrftShared.Client/js/theme/theme.js`. Call is gated to fire only on first render or when `_isDarkMode` changes (`_lastAppliedDarkMode` comparison) — no redundant JS calls on unrelated re-renders. `IJSObjectReference _themeModule` is disposed in `DisposeAsync` to clean up the module reference when the circuit tears down.
### Phase 18 — Wave 5 — Glass-panel theme-aware token family (landed 2026-06-20)
**Landed:** 2026-06-20 on dev.
- **What:** The three `MudOverlay`-based glass panels — the queue panel (`.deepdrft-queue-modal`), the waveform visualizer control deck, and the privacy modal — now render as a light translucent glass with legible dark text in light theme, while remaining the existing dark-glass charcoal in dark theme. Dark mode is visually unchanged; a latent white-on-light bug in the inline embed queue row was incidentally fixed by the token flip.
- **Why:** Prior to this wave, all three panels were bound to the constant `--deepdrft-panel-ground` token, exempting them from the theme-aware alias layer established in Waves 1–3. In light theme this produced white text on a near-white glass surface — unreadable. The panels needed their own theme-aware family (separate from `--deepdrft-popover-surface`, which targets MudBlazor default popovers) and the same `body.deepdrft-theme-dark` portal-scope treatment introduced for popovers in Wave 4.
- **Shape:**
- **New token family (`deepdrft-tokens.css`):** `--deepdrft-panel-surface` / `--deepdrft-panel-text` / `--deepdrft-panel-text-muted` / `--deepdrft-panel-border` / `--deepdrft-panel-row-hover` — each defined in `:root` (light values: translucent glass with dark text), `.deepdrft-theme-dark` (dark-glass charcoal with light text, sourced from the existing `--deepdrft-panel-ground` constant), and `body.deepdrft-theme-dark` (same dark values re-declared so the tokens resolve correctly when the panels portal to `` via `MudOverlay`).
- **Consumer re-pointing:** The three panels and their descendants (queue rows, visualizer deck, privacy modal) previously bound `--deepdrft-panel-ground` directly; they are now re-pointed to the appropriate `--deepdrft-panel-surface`/`-text`/`-text-muted`/`-border`/`-row-hover` aliases.
- **Exemption lifted:** This deliberately removes the previously-documented exemption of these panels from the theme-aware layer. `--deepdrft-panel-ground` is now consumed only as the dark-theme value of `--deepdrft-panel-surface`, not directly by any component CSS.
---
## Phase 17 — Player-Bar Queue View: Wave 17.3 — Fixed embed panel + iframe resize (landed 2026-06-19)
**Landed:** 2026-06-19 on dev.
- **What:** The Fixed (embed) mode queue panel and the OQ1 Option-A iframe resize handshake. Release embeds now render an always-shown, read-only queue panel below the player-bar controls; the Queue button collapses/expands that panel and posts the iframe's new height to the host page so the outer `