# Mix Detail — Hero + MetaContent overlay rework (mirror the Sessions hero) — Design Spec Status: **shipped on dev** (2026-06-16). Author: product-designer. Date: 2026-06-16. ## 0. Goal Rework the **Mix detail** Hero + MetaContent so the **cover art becomes a background image** with **all metadata laid out on top of it**, inside a **max-medium square cover-art region** — mirroring the already-shipped **Session detail** hero-overlay composition. This consolidates the masthead + cover + meta into one overlaid block, frees vertical room for the lava-lamp visualizer behind the content, and cleans up the aesthetic so the Mix page reads as a member of the same design family as Sessions. The Mix visualizer + eight-knob controls layout landed in the Phase 10 reframe (`TopRowCenter` slot, in-flow controls container between the back link and the lava-lamp) **must be preserved unchanged** — this rework touches only what sits *below* that top row. --- ## 1. What is actually shipped today (confirmed from live source, not spec) Read of the live tree on 2026-06-16: **Session detail** (`Pages/SessionDetail.razor` + `.razor.css`) — the target aesthetic, **already shipped**: - **Does NOT use `ReleaseDetailScaffold`.** It composes its own hero overlay directly inside a `MudContainer MaxWidth="Large"`, and wires `PlayTrack` in its own `@code` block (the scaffold's play-toggle logic is duplicated here deliberately — see `SessionDetail.razor` lines 131–145). - Structure: a `.session-hero` positioning context (`position: relative; aspect-ratio: 16/10; max-height: 70vh; min-height: 420px; overflow: hidden; border-radius: 8px`) holding: - `.session-hero-img` — a `MudPaper` whose `background-image` is the hero/cover image (`background-size: cover`), or a `.session-hero-placeholder` when none. - `.session-hero-shim` — a plain `
` darkening gradient (stronger top + bottom) for overlay legibility. - `.session-hero-top` — absolutely-positioned overlay row: genre chip + release-date + `SharePopover`. - `.session-hero-bottom` — absolutely-positioned overlay row: optional cover thumbnail + title/artist + `PlayStateIcon`. - The back link (`.deepdrft-track-detail-back`) sits **above** the hero, in normal flow. - CSS uses `::deep` on every class that lands on a MudBlazor child component's native output (`.session-hero-img` on `MudPaper`, `.session-overlay-chip.mud-chip` on `MudChip`, `.session-hero-bottom-row` on `MudStack`, the play/share icon-color overrides on `MudIconButton`). **Mix detail** (`Pages/MixDetail.razor` + `.razor.css`) — the page to rework, **uses the scaffold**: - `` paints a fixed full-viewport backdrop (`z-index: 0`); a `.mix-detail-foreground` wrapper (`z-index: 1`) lifts the content above it; inside a `MudContainer MaxWidth="Large"` (class `mix-detail-container`) sits a ``. - The scaffold's **`TopRowCenter`** slot holds ``; **`TopRightAction`** holds the lava-lamp `MudIconButton` (the Phase 10 reframe layout — **do not touch**). - The scaffold's **`Hero`** slot today is a `.mix-detail-cover` (square, `max-width: 220px`, centered) holding a `MudPaper` cover-art / placeholder. The **`MetaContent`** slot holds genre chip + release date. The masthead (title + artist) is rendered by the scaffold's **default header region** above the hero. The `BodyContent` slot holds the share row. **The scaffold** (`Controls/ReleaseDetailScaffold.razor` + `.razor.cs`) — the shared chrome: - Vertical order: top row (back | `TopRowCenter` | `TopRightAction`) → `TopContent` → header (masthead + play, or a custom `Header`) → `Hero` → divider + `MetaContent` (gated by `ShowMeta`) → `BodyContent` → default track-keyed share row (gated by `ShowShareRow`). - Owns the back link and the play-toggle wiring. Cut and Track also consume it; Session does **not**. **Render boundary (both detail pages are identical here — confirmed):** both `SessionDetail` and `MixDetail` derive from `ReleaseDetailBase`, run under `InteractiveAuto`, and bridge the prerendered release+track across the prerender→WASM seam via `PersistentComponentState` (keyed `session-detail` / `mix-detail`). The release DTO (title, artist, genre, release date, `ImagePath`) is **available at first render in both passes** — there is no render-mode divergence to design around. The image is served from `api/image/{EscapeDataString(key)}` in both pages. **Conclusion: the data read at render time is the same shape and same timing for both; the overlay rework is purely presentational.** --- ## 2. The DRY/SOLID question — where is the source of truth? This is the load-bearing decision. The brief asks: extract a shared hero-overlay shell that both Sessions and Mixes consume, or per-page duplication? ### The honest finding: the two pages are structurally divergent *by design today* Sessions **deliberately does not use the scaffold** — it forks the whole chrome to get the overlay composition. Mixes **does use the scaffold**, and the scaffold's `Hero`/`MetaContent`/masthead are *separate stacked regions*, which is the opposite of "everything overlaid on the cover." So the two pages do not share a hero today, and the scaffold's stacked-region model is not the overlay model. Three meaningfully different directions, in shape: **Direction A — Per-page copy.** Inline the `.session-hero` overlay structure into `MixDetail.razor`, copy the `.session-*` CSS into `MixDetail.razor.css` (renamed `.mix-hero-*`), tune the square/medium sizing. No shared component. - *Pro:* fastest; zero risk to Sessions; Mix can diverge freely (it has a visualizer behind it that Sessions does not). - *Con:* **two copies of the overlay cascade to keep in sync** — exactly the drift the `track-view-css- consolidation` note spent a pass undoing. The shim gradient, the overlay-label typography, the `::deep` icon-color overrides, the responsive wrap rules all get duplicated. A future overlay tweak has to land twice. Violates the "one source, multiple views" instinct (Daniel's standing preference). **Direction B — Extract a shared `ReleaseHeroOverlay` component, both pages consume it.** Pull the `.session-hero` overlay (image/placeholder + shim + top overlay + bottom overlay) into a new `Controls/ReleaseHeroOverlay.razor` that takes the release data (+ optional play affordance + optional share slot + optional cover-thumb) and renders the overlaid hero. Sessions swaps its inline hero for the component; Mix renders the component inside its `.mix-detail-foreground`, **bypassing the scaffold's `Hero`/`MetaContent`/masthead regions** (Mix stops using the scaffold for the hero, same as Sessions already does). - *Pro:* **one source of truth for the overlay** — the headline DRY win. Honors "same data shape, different rendering": both pages feed the same release DTO into one overlay VM/parameter set, the divergence (visualizer backdrop, square-vs-wide aspect) rides parameters/CSS, not a second copy. SOLID: the overlay is a single-responsibility presentational component; the pages compose it. - *Con:* touches the **already-shipped Sessions page** (regression surface on a working view) and means Mix no longer routes its hero through the scaffold — Mix keeps the scaffold only for the back/controls top row, or drops the scaffold entirely (see §4 sub-decision). More upfront work; a real refactor. **Direction C — Teach the scaffold an overlay mode.** Add an overlay-hero capability to `ReleaseDetailScaffold` so it can render its `Hero`/masthead/`MetaContent` as one overlaid block when a flag/slot says so; migrate Sessions onto the scaffold in overlay mode and switch Mix to overlay mode. - *Pro:* one component owns *all* detail chrome including the overlay; Sessions finally joins the scaffold. - *Con:* **largest blast radius and worst SOLID.** It bloats the scaffold with a second layout personality (stacked-regions *and* overlaid-hero) gated by a flag — exactly the "variance rides a flag" anti-pattern the scaffold's own convention forbids (Phase 9 §5.3: "layout variance rides a slot, never a boolean"). It also drags the working Sessions page through a chrome migration for no user-visible gain. Over-engineered for a two-consumer overlay. ### Recommendation: **Direction B — extract `ReleaseHeroOverlay`, both pages consume it.** Rationale, with the trade-off stated plainly: - **The source of truth should be one overlay component, not the scaffold and not a copy.** Sessions already proved the overlay wants to live *outside* the scaffold's stacked-region model — forcing it back into the scaffold (Direction C) fights that and bloats shared chrome. Copying it (Direction A) reintroduces the exact cascade-duplication this codebase already paid down once. - **It satisfies "one source, multiple views" directly:** one overlay fed the same release DTO, rendering differences (Mix's visualizer backdrop, the square medium cover vs. Sessions' wide hero) expressed as component parameters + a CSS class, never as a forked structure. - **The cost is real and must be owned:** Direction B edits the shipped Sessions page. That is a regression surface on a working view. Mitigation: extract by *moving* Sessions' exact current markup + CSS into the component first (a behavior-preserving lift — Sessions should look pixel-identical after), then point Mix at it. The Sessions migration is the risky step; treat it as its own wave with a before/after visual check (§7 acceptance). **If Daniel wants to minimize risk to Sessions and ship Mix fast, Direction A is the acceptable fallback** — but it takes on the duplication debt knowingly, and a later consolidation pass (like the track-card one) becomes likely. Recommended only if the Sessions page is considered too load-bearing to touch right now. **Flagging this as the one open decision for Daniel (§8, Q1).** --- ## 3. The shared component — `ReleaseHeroOverlay` (Direction B shape) A new plain-shell presentational component: `DeepDrftPublic.Client/Controls/ReleaseHeroOverlay.razor` (+ `.razor.css` carrying the overlay cascade, moved from `SessionDetail.razor.css`). **Responsibility (single):** given a release's display data, render the background-image hero with the metadata overlaid (top row: genre/date + share slot; bottom row: optional cover thumb + title/artist + optional play affordance). It owns no data fetch, no player wiring beyond invoking a passed-in callback, no JS interop. **Parameters (the "same data shape" contract):** | Parameter | Type | Purpose | |-----------|------|---------| | `HeroImageKey` | `string?` | The background image entry key (Sessions: hero-then-cover precedence; Mix: cover). Null → placeholder. | | `PlaceholderIcon` | `string` | Material icon for the no-image placeholder (Sessions: `Piano`; Mix: `Album`). | | `CoverThumbKey` | `string?` | Optional small cover thumbnail shown in the bottom row (Sessions shows it only when it differs from the hero image; Mix likely null — see §4). | | `Title` / `Artist` | `string` / `string?` | Overlaid title + artist. | | `Genre` | `string?` | Genre chip (top overlay) when present. | | `ReleaseDate` | `DateOnly?` | Release date (top overlay) when present. | | `ShareContent` | `RenderFragment?` | The share affordance (each page passes its `SharePopover` with the right release params). | | `PlayContent` | `RenderFragment?` | The play affordance (each page passes its `PlayStateIcon` wired to its own toggle). | | `Class` | `string?` | Extra class for per-page aspect/sizing variance (`mix-hero` vs default wide). | Play/share ride as **slots**, not as wired-in player logic, so the component stays presentational and each page keeps owning its own play-toggle (Sessions already does; the scaffold does for Mix today — this preserves that ownership without the component reaching for the cascaded player). **House constraint — plain-div shell.** The overlay shell (`.release-hero`, `.release-hero-shim`, `.release-hero-top`, `.release-hero-bottom`) is **plain `
`s with project CSS classes**, exactly as Sessions does today. The **background-image surface stays a `MudPaper`** *only because Sessions already uses one there* — but note it carries no card affordances; if staff-engineer prefers, it can become a plain `
` with the same `background-image` style, which is *more* aligned with the plain-shell rule. **Recommendation: make it a plain `
`** and drop the `MudPaper` — there is no MudPaper behavior being used (Elevation=0, Square=true), so the Mud wrapper is dead weight. This is a small improvement over the shipped Sessions code; flag it to Daniel as an incidental cleanup (§8, Q2). Chips/icons remain Mud components (they carry real behavior) and keep their `::deep` overrides. --- ## 4. Mix-specific composition + the square medium cover Daniel's ask: "metadata laid out on top of a **max-medium square cover-art region**." Two design notes specific to Mix that differ from Sessions: ### 4a. Aspect ratio — square medium, not Sessions' wide hero Sessions uses `aspect-ratio: 16/10; max-height: 70vh; min-height: 420px` (a wide, tall hero). Daniel wants Mix to use a **square** region at a **max-medium** size. The current Mix cover is `max-width: 220px` (small). "Max-medium square" reads as: a centered `aspect-ratio: 1/1` block, capped at a medium width — recommend **`max-width: 420–480px`** (medium: bigger than the 360px track cover, smaller than the Large container) — so the overlaid metadata has room to sit on the cover without crowding, while the lava-lamp visualizer keeps the surrounding canvas. This rides the `Class` parameter (`mix-hero`) + a Mix CSS rule, not a forked component. - *Why square, not wide:* the Mix cover art is square album art; a 16/10 crop would letterbox or distort it, and the point is to free room for the visualizer, which a smaller square does better than a full-bleed wide hero. - *Open sub-question (§8, Q3):* on a square overlay, the **top overlay row (genre/date/share) + bottom overlay row (title/artist/play) over a 480px square** may feel cramped versus Sessions' tall hero. Acceptable mitigation: keep the same two-overlay structure but let the shim carry more darkening, or drop the cover thumbnail (4b). Daniel tunes on screen per his standing preference. ### 4b. No cover thumbnail in the bottom row (recommended) Sessions shows a small cover thumb in the bottom overlay *only when the hero image differs from the cover* (it has a dedicated hero image distinct from the cover). **Mix has no separate hero image — the cover art *is* the background.** So a cover thumbnail would duplicate the background. **Recommend Mix passes `CoverThumbKey = null`** — the bottom overlay is just title/artist + play. This falls out of the shared component's existing `showCover` logic for free. ### 4c. Mix keeps the scaffold for the top row only — or drops it With the hero now a self-composed overlay (not the scaffold's `Hero`/`MetaContent`/masthead regions), Mix has a sub-decision: - **Option (i) — keep `ReleaseDetailScaffold` for the back/controls top row only.** Mix keeps supplying `TopRowCenter` (controls) + `TopRightAction` (lava-lamp) to the scaffold, but stops supplying `Hero` / `MetaContent` / the default masthead, and instead renders `` in `BodyContent` (or a new slot). **Problem:** the scaffold's default header region renders the masthead (title+artist) *and* a second `PlayStateIcon` — which would now duplicate the overlay's title/artist/play. Suppressing the scaffold's masthead requires the `Header` slot to render *nothing*, which is awkward (the slot exists to *replace* the masthead, and an empty `Header` is a smell). - **Option (ii) — Mix drops the scaffold, composes directly like Sessions does.** Mix renders the back link + the controls top row + the lava-lamp + `` itself inside `.mix-detail-foreground`, mirroring how Sessions composes directly. **But** this loses the scaffold's ownership of the back link and the three-zone top-row structure that the Phase 10 reframe specifically built into the scaffold (`TopRowCenter`), and would duplicate that row. **Recommendation: Option (i), with the scaffold's masthead suppressed by passing an empty-but-present `Header` fragment** — *or*, cleaner, **the overlay renders inside the scaffold's existing `Hero` slot and Mix passes a `Header` fragment that renders nothing** so the scaffold contributes only the top row + hero. The least-smelly realization: **keep the scaffold (it owns the Phase 10 top row — the constraint we must preserve), put `` in the `Hero` slot, leave `MetaContent` null (metadata now lives in the overlay), and pass a no-op `Header` fragment to suppress the duplicate masthead/play.** This preserves the visualizer/controls layout exactly (§5) while moving the hero+meta into the overlay. > **Flag for staff-engineer:** an empty `Header` fragment to suppress the masthead is slightly awkward > but is the lowest-risk way to keep the Phase 10 top row intact. If it reads badly in code, the > alternative is a small scaffold change (a `ShowHeader` gate mirroring `ShowMeta`/`ShowShareRow`) — that > is a *minimal, slot-consistent* scaffold edit (a gate, not a layout flag), acceptable under the Phase 9 > §5.3 convention because it suppresses an optional region rather than switching layouts. **Recommend the > `ShowHeader` gate over the empty-fragment hack** if staff-engineer touches the scaffold anyway. Noted > as §8 Q4. --- ## 5. Preserving the Phase 10 visualizer + controls layout (hard constraint) The Phase 10 reframe put the controls in the scaffold's **`TopRowCenter`** slot and the lava-lamp in **`TopRightAction`**, with the in-flow grow/collapse behavior and the `flex-wrap` responsive drop. This rework **must not disturb any of that.** Concretely: - **Keep the scaffold and its three-zone top row.** Mix continues supplying `TopRowCenter` (``) and `TopRightAction` (the lava-lamp `MudIconButton`) unchanged. The `_controlsExpanded` flag, `ToggleSettings`, and the filled/outline lamp glyph are untouched. - **The overlay goes *below* the top row**, in the `Hero` slot — it occupies the space the old `.mix-detail-cover` + masthead + `MetaContent` occupied. Net effect: the region below the controls row gets *shorter* (one overlaid block instead of masthead + 220px cover + meta divider + meta row), which **frees more canvas for the lava-lamp visualizer** — exactly Daniel's stated goal. - **`MixWaveformVisualizer` is unchanged.** It is a fixed full-viewport backdrop at `z-index: 0`; the overlay rides inside `.mix-detail-foreground` (`z-index: 1`) like the current cover does. The footer clip / lava-rest-line work (Phase 10 §2c) is independent of this and unaffected. - **`MetaContent` becomes null** (metadata moves into the overlay), so the scaffold's divider + `.deepdrft-track-detail-meta` row no longer renders for Mix. The share row moves into the overlay's `ShareContent` slot (matching Sessions, which overlays share top-right). **Acceptance tie-in:** after the rework, expanding the controls still grows the in-flow container between back and lamp, still wraps on narrow widths, and the lava-lamp still toggles it — no change to that interaction (§7). --- ## 6. CSS approach - **The overlay cascade moves into `ReleaseHeroOverlay.razor.css`** (Direction B), renamed from `.session-*` to neutral `.release-hero-*`. Sessions' `SessionDetail.razor.css` keeps only what is page-specific (the `.session-detail-page` padding, the per-page aspect overrides if any); Mix's `MixDetail.razor.css` keeps only the `.mix-detail-foreground` z-index lift + the `mix-hero` square/ medium sizing override. - **`::deep` is required** wherever a class lands on a MudBlazor child component's native output — this is unchanged from Sessions today and must carry into the shared component's scoped CSS: - the background-image surface **if it stays `MudPaper`** (`::deep .release-hero-img`) — *avoidable by making it a plain `
`, §3, which removes the `::deep` need for that element entirely*; - the genre chip on `MudChip` (`::deep .release-overlay-chip.mud-chip`); - the bottom-row `MudStack` wrap (`::deep .release-hero-bottom-row`); - the play/share icon-color overrides on `MudIconButton`/`MudProgressCircular` (`::deep .release-hero-play .mud-icon-button`, etc.). - **The square/medium aspect for Mix** is a single override scoped to the `mix-hero` class (`aspect-ratio: 1/1; max-width: ~480px; margin-inline: auto`) — it overrides the component's default wide aspect. This is the "divergence rides CSS, not structure" realization of the one-source rule. - **No `MudCard`/`MudPaper` shell for the overlay container** (house rule). The `.release-hero` positioning context, the shim, and both overlay rows are plain `
`s — as Sessions already does. The only Mud wrappers that remain are the chip, the icon buttons, and (optionally) the image surface — each carrying real component behavior, none acting as a layout shell. **If staff-engineer reaches for a Mud wrapper as a layout shell, stop and flag it** (per the standing constraint). --- ## 7. Acceptance criteria (observable) 1. **Cover-as-background.** On a Mix detail page with a cover image, the cover renders as a background-image hero (`background-size: cover`), not as a bordered thumbnail; metadata sits overlaid on top of it. 2. **Square medium region.** The hero is a centered square (`aspect-ratio: 1/1`) capped at a medium width (~480px), visibly larger than the old 220px cover, with the surrounding canvas left to the visualizer. 3. **All metadata overlaid.** Title, artist, genre (when present), release date (when present), and the share affordance all render *over* the cover image — no separate masthead block, no separate meta divider/row below the cover. 4. **Placeholder path.** A Mix with no cover image shows the placeholder treatment (Album icon over the soft-secondary gradient) with metadata still legibly overlaid. 5. **Legibility.** The darkening shim keeps overlaid text readable over light and dark cover art, in both light and dark theme. 6. **Visualizer/controls preserved.** The lava-lamp toggle still opens the in-flow eight-knob controls container between the back link and the lamp; it still grows in place, still wraps on narrow widths, and the lava-lamp glyph still swaps filled/outline. No regression to the Phase 10 layout. 7. **More canvas for the visualizer.** The content block below the controls row is shorter than before (one overlaid hero vs. masthead + cover + meta), leaving more visible visualizer area. 8. **Sessions unchanged (Direction B).** After the shared-component extraction, the Session detail page renders pixel-identically to before (before/after visual check on a session with a dedicated hero image, a session with cover-only, and a session with no image). 9. **One source of truth (Direction B).** The overlay markup + cascade exists in exactly one component; grepping for `.session-hero` / `.release-hero` finds no duplicate structure across the two pages. 10. **No Mud layout shell.** The overlay container, shim, and overlay rows are plain `
`s; no `MudCard`/`MudPaper` acts as a layout wrapper (image surface excepted only if kept as `MudPaper`). --- ## 8. Open questions for Daniel 1. **DRY decision (the load-bearing one).** Recommended **Direction B** — extract a shared `ReleaseHeroOverlay` both pages consume (one source of truth; edits the shipped Sessions page). Fallback **Direction A** — per-page copy in Mix (fast, zero Sessions risk, takes on duplication debt). **Which do you want?** (B is the "one source, multiple views" call; A is the play-it-safe-on-Sessions call.) 2. **Drop the `MudPaper` on the image surface** for a plain `
` while we're in here? It carries no Mud behavior (Elevation=0/Square=true), so it is dead weight and a plain div is more on-spec with the plain-shell rule. Incidental cleanup — yes/no? 3. **Square hero size.** Recommended ~**480px max-width** square. Crowded vs. roomy is a feel call you'll tune on screen — is ~480px the right starting point, or do you want it bigger (closer to the Sessions hero scale) / smaller (more visualizer room)? 4. **Suppressing the duplicate masthead.** Mix's hero now carries title/artist/play, so the scaffold's default masthead must be suppressed. Recommended: add a small **`ShowHeader` gate** to the scaffold (slot-consistent with `ShowMeta`/`ShowShareRow`), vs. the hackier empty-`Header`-fragment. OK to add the `ShowHeader` gate? 5. **Square overlay crowding (deferrable).** If the genre/date/share top row + title/artist/play bottom row feel cramped on a 480px square, are you fine tuning shim/spacing on screen, or do you want a different overlay arrangement for the square (e.g. a single bottom-stacked overlay) speced now? --- ## 9. File-change inventory (for staff-engineer, Direction B) **New:** - `DeepDrftPublic.Client/Controls/ReleaseHeroOverlay.razor` (+ `.razor.css`) — the shared overlay, lifted from Sessions' current hero markup + CSS, parameterized per §3. **Edited:** - `DeepDrftPublic.Client/Pages/SessionDetail.razor` — replace the inline `.session-hero` block with ``, passing its hero/cover precedence, share, and play slots. Behavior- preserving (must render identically). - `DeepDrftPublic.Client/Pages/SessionDetail.razor.css` — remove the overlay cascade now living in the component; keep only page-specific rules. - `DeepDrftPublic.Client/Pages/MixDetail.razor` — replace the `.mix-detail-cover` `Hero` slot with ``; drop `MetaContent`; move the share row into the overlay's `ShareContent` slot; suppress the scaffold masthead (per Q4). **`TopRowCenter` / `TopRightAction` / the visualizer / `_controlsExpanded` wiring untouched.** - `DeepDrftPublic.Client/Pages/MixDetail.razor.css` — keep `.mix-detail-foreground`; add the `mix-hero` square/medium sizing override; remove `.mix-detail-cover`. - `DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor[.cs]` — **only if Q4 = ShowHeader gate:** add a `bool ShowHeader = true` gate around the default header region. - `DeepDrftPublic/wwwroot/styles/deepdrft-styles.css` — no change required; `.deepdrft-track-detail- cover*` stays (Track detail still uses it). The `.mix-detail-container .deepdrft-track-detail- container` width override stays (the scaffold still hosts the top row). **For Direction A (fallback):** no new component; the overlay markup is inlined into `MixDetail.razor` and the `.session-*` cascade is copied + renamed into `MixDetail.razor.css`. Sessions untouched.