s, so no ::deep is needed here. */
+.release-hero-img {
+ position: absolute;
+ inset: 0;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.release-hero-placeholder {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: var(--mud-palette-surface);
+}
+
+::deep .release-hero-placeholder .mud-icon-root {
+ font-size: 120px;
+}
+
+/* Darkening gradient shim: stronger at the bottom (under the title/play row) and lighter toward
+ the middle, with a top darken so the genre/share overlay stays legible too. */
+.release-hero-shim {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ background:
+ linear-gradient(to bottom,
+ rgba(0, 0, 0, 0.55) 0%,
+ rgba(0, 0, 0, 0.15) 28%,
+ rgba(0, 0, 0, 0.15) 55%,
+ rgba(0, 0, 0, 0.75) 100%);
+}
+
+/* --- Top overlay: genre / date / share --- */
+.release-hero-top {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 2;
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 1rem;
+ padding: 1.25rem 1.5rem;
+}
+
+.release-overlay-date {
+ display: flex;
+ flex-direction: column;
+ gap: 0.1rem;
+}
+
+.release-overlay-label {
+ font-family: var(--deepdrft-font-mono);
+ font-size: 0.6rem;
+ letter-spacing: 0.25em;
+ text-transform: uppercase;
+ color: var(--deepdrft-green-accent);
+}
+
+.release-overlay-value {
+ font-family: var(--deepdrft-font-body);
+ font-size: 0.8rem;
+ color: var(--deepdrft-white);
+}
+
+/* Genre chip themed to the glassmorphic NowPlaying surface. The class lands on MudChip's native
+ .mud-chip output, so ::deep is required to reach it. */
+::deep .release-overlay-chip.mud-chip {
+ background: rgba(250, 250, 248, 0.06);
+ border: 1px solid rgba(250, 250, 248, 0.12);
+ backdrop-filter: blur(8px);
+ color: var(--deepdrft-white);
+ font-family: var(--deepdrft-font-mono);
+ letter-spacing: 0.12em;
+}
+
+/* --- Bottom overlay: cover thumb / title / play --- */
+.release-hero-bottom {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 2;
+ padding: 1.5rem;
+}
+
+.release-cover-thumb {
+ flex: 0 0 auto;
+ width: 96px;
+ height: 96px;
+ overflow: hidden;
+ border: 1px solid rgba(250, 250, 248, 0.12);
+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
+}
+
+/* The cover-thumb art surface is a plain
, so no ::deep is needed. */
+.release-cover-thumb .deepdrft-track-detail-cover-art {
+ width: 100%;
+ height: 100%;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.release-hero-titles {
+ flex: 1 1 auto;
+ min-width: 0;
+}
+
+.release-overlay-title {
+ font-family: var(--deepdrft-font-display);
+ font-size: clamp(1.75rem, 4vw, 2.75rem);
+ font-weight: 400;
+ line-height: 1.1;
+ color: var(--deepdrft-white);
+ text-shadow: 0 2px 12px rgba(0, 0, 0, 0.6);
+}
+
+.release-overlay-artist {
+ font-family: var(--deepdrft-font-body);
+ font-size: 0.85rem;
+ letter-spacing: 0.08em;
+ color: rgba(250, 250, 248, 0.7);
+ margin-top: 0.35rem;
+}
+
+.release-hero-play {
+ flex: 0 0 auto;
+}
+
+/* The play affordance and share button sit over a dark image — force their icon glyphs to the
+ light theme color regardless of MudBlazor's Secondary palette. Both PlayStateIcon and
+ SharePopover render MudIconButton / MudProgressCircular internals, so ::deep is required. */
+::deep .release-hero-play .mud-icon-button,
+::deep .release-hero-play .mud-progress-circular,
+::deep .release-hero-share .mud-icon-button {
+ color: var(--deepdrft-white);
+}
+
+@media (max-width: 599.98px) {
+ .release-hero {
+ aspect-ratio: 3 / 4;
+ min-height: 380px;
+ }
+
+ /* release-hero-bottom-row rides on MudStack's native output div, so ::deep is required. */
+ ::deep .release-hero-bottom-row {
+ flex-wrap: wrap;
+ }
+
+ .release-cover-thumb {
+ width: 72px;
+ height: 72px;
+ }
+}
diff --git a/DeepDrftPublic.Client/Pages/MixDetail.razor b/DeepDrftPublic.Client/Pages/MixDetail.razor
index 386b5fd..5041869 100644
--- a/DeepDrftPublic.Client/Pages/MixDetail.razor
+++ b/DeepDrftPublic.Client/Pages/MixDetail.razor
@@ -1,5 +1,6 @@
@page "/mixes/{EntryKey}"
@using DeepDrftPublic.Client.Controls
+@using DeepDrftPublic.Client.Services
@inherits ReleaseDetailBase
@(ViewModel.Release?.Title ?? "Mix") - DeepDrft
@@ -31,8 +32,6 @@ else if (ViewModel.NotFound || ViewModel.Release is null)
else
{
var release = ViewModel.Release;
- var hasGenre = release.Genre is not null;
- var hasDate = release.ReleaseDate is not null;
@* Full-page waveform sits behind the scaffold content. The scaffold's container is positioned
above it via the mix-detail-foreground stacking context. TrackId lets the visualizer couple to
@@ -41,12 +40,17 @@ else
+ @* Mix keeps the scaffold solely for the Phase 10 top row (back link | controls | lava-lamp).
+ Title/artist/genre/date/share/play all move into the overlaid hero, so the scaffold's
+ default header and meta regions are suppressed (ShowHeader/ShowMeta=false) and the share
+ row stays off (ShowShareRow=false). *@
@* The seven-knob band lives in its own full-width area below the back/lamp top row.
@@ -73,43 +77,28 @@ else
-
- @if (!string.IsNullOrEmpty(release.ImagePath))
- {
-
- }
- else
- {
-
-
-
- }
-
+ @* Cover-as-background hero with all metadata overlaid, square `mix-hero` sizing. The
+ cover art IS the background, so no separate cover thumbnail (CoverThumbKey defaults
+ to null). Share and play ride in as slots, matching Sessions. *@
+
+
+ @* Release-mode share: copies the canonical /mixes/{entryKey} URL, not a single track (§3b). *@
+
+
+
+ @if (ViewModel.Track is not null)
+ {
+
+ }
+
+
-
- @if (hasGenre)
- {
-
-
- @release.Genre
-
-
- }
- @if (hasDate)
- {
-
- Released
- @release.ReleaseDate!.Value.ToString("MMMM yyyy")
-
- }
-
-
- @* Release-mode share: copies the canonical /mixes/{entryKey} URL, not a single track (§3b). *@
-
-
-
-
@@ -118,6 +107,27 @@ else
@code {
protected override string PersistKey => "mix-detail";
+ [CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
+
+ // The hero now carries the play affordance (the scaffold's header is suppressed), so the
+ // play-toggle is wired here directly — mirroring SessionDetail. Toggle if this track is already
+ // active, otherwise start a fresh stream.
+ private async Task PlayTrack()
+ {
+ var track = ViewModel.Track;
+ if (track is null || PlayerService is null) return;
+
+ var isThisTrack = PlayerService.CurrentTrack?.Id == track.Id;
+ if (isThisTrack && (PlayerService.IsPlaying || PlayerService.IsPaused))
+ {
+ await PlayerService.TogglePlayPause();
+ }
+ else
+ {
+ await PlayerService.SelectTrackStreaming(track);
+ }
+ }
+
// Lava-lamp knob-band visibility. Pure presentation over MixVisualizerControlState — gates whether
// the seven-knob MixVisualizerControls is rendered into the TopContent band; toggling it touches no
// control value or bridge push. The lava-lamp button's filled/outline glyph is driven off this flag.
diff --git a/DeepDrftPublic.Client/Pages/MixDetail.razor.css b/DeepDrftPublic.Client/Pages/MixDetail.razor.css
index 0f0b971..1a3c271 100644
--- a/DeepDrftPublic.Client/Pages/MixDetail.razor.css
+++ b/DeepDrftPublic.Client/Pages/MixDetail.razor.css
@@ -4,12 +4,14 @@
z-index: 1;
}
-/* Medium square cover — deliberately smaller than the 360px cut cover so the
- waveform backdrop keeps room. The placeholder/art MudPaper fills this frame. */
-.mix-detail-cover {
+/* Mix's per-page hero variance: a centered medium square, overriding ReleaseHeroOverlay's default
+ wide (16/10) aspect. The cover art is square album art, and a smaller square frees the surrounding
+ canvas for the lava-lamp visualizer. The mix-hero class lands on the overlay component's root
+ .release-hero
(a child Razor component's native output), so ::deep is required to reach it. */
+::deep .release-hero.mix-hero {
aspect-ratio: 1 / 1;
- max-width: 220px;
- margin: 0 auto 2rem;
- overflow: hidden;
- box-shadow: 0 8px 28px color-mix(in srgb, var(--mud-palette-text-secondary) 18%, transparent);
+ max-width: 600px;
+ min-height: 0;
+ max-height: none;
+ margin-inline: auto;
}
diff --git a/DeepDrftPublic.Client/Pages/SessionDetail.razor b/DeepDrftPublic.Client/Pages/SessionDetail.razor
index 5862b9d..f48a90c 100644
--- a/DeepDrftPublic.Client/Pages/SessionDetail.razor
+++ b/DeepDrftPublic.Client/Pages/SessionDetail.razor
@@ -39,12 +39,6 @@ else
var heroKey = release.SessionMetadata?.HeroImageEntryKey;
// Hero image precedence: the session's dedicated hero, then the release cover, then a placeholder.
var heroImage = !string.IsNullOrEmpty(heroKey) ? heroKey : release.ImagePath;
- // Show the cover thumbnail only when it differs from what the hero displays. When there is no
- // dedicated hero, the hero already falls back to release.ImagePath — rendering the cover too
- // would duplicate the same image.
- var showCover = !string.IsNullOrEmpty(release.ImagePath) && release.ImagePath != heroImage;
- var hasGenre = release.Genre is not null;
- var hasDate = release.ReleaseDate is not null;
@@ -52,70 +46,27 @@ else
← All sessions
- @* The hero is the positioning context for every overlay row; the gradient shim and the
- top/bottom overlays are absolutely positioned children of this wrapper. *@
-
- @if (!string.IsNullOrEmpty(heroImage))
- {
-
- }
- else
- {
-
-
-
- }
-
- @* Darkening shim so overlaid text/controls stay legible over any image. *@
-
-
- @* Top overlay: secondary details (genre, release date) and the share affordance. *@
-
-
- @if (hasGenre)
- {
-
- @release.Genre
-
- }
- @if (hasDate)
- {
-
- Released
- @release.ReleaseDate!.Value.ToString("MMMM yyyy")
-
- }
-
+ @* The overlay shows the cover thumbnail only when it differs from the resolved hero image —
+ when there is no dedicated hero, heroImage already falls back to release.ImagePath, so the
+ thumb would duplicate the background. That logic lives in ReleaseHeroOverlay. *@
+
+
@* Release-mode share: copies the canonical /sessions/{entryKey} URL, not a single track (§3b). *@
-
-
-
-
-
- @* Bottom overlay: cover thumbnail, title/artist, and the play affordance in one row. *@
-
-
- @if (showCover)
- {
-
-
-
- }
-
-
@release.Title
-
@release.Artist
-
- @if (ViewModel.Track is not null)
- {
-
- }
-
-
-
+
+
+
+ @if (ViewModel.Track is not null)
+ {
+
+ }
+
+
}
diff --git a/DeepDrftPublic.Client/Pages/SessionDetail.razor.css b/DeepDrftPublic.Client/Pages/SessionDetail.razor.css
index 9bbe881..53b2c50 100644
--- a/DeepDrftPublic.Client/Pages/SessionDetail.razor.css
+++ b/DeepDrftPublic.Client/Pages/SessionDetail.razor.css
@@ -1,174 +1,8 @@
/* Session detail is hero-dominant: a large background image with the detail components overlaid
- directly on top, themed to match the NowPlaying glassmorphic family. The page widens to the
+ directly on top (the overlay composition lives in ReleaseHeroOverlay). The page widens to the
Large container (set in markup) rather than the shared 760px detail column. */
::deep .session-detail-page {
padding-top: 2rem;
padding-bottom: 4rem;
}
-
-/* Positioning context for every overlay. A tall, dominant frame rather than the 16:9 strip. */
-.session-hero {
- position: relative;
- width: 100%;
- aspect-ratio: 16 / 10;
- max-height: 70vh;
- min-height: 420px;
- margin-top: 1rem;
- overflow: hidden;
- border-radius: 8px;
- box-shadow: 0 12px 40px color-mix(in srgb, var(--mud-palette-text-secondary) 22%, transparent);
-}
-
-/* session-hero-img / session-hero-placeholder ride on MudPaper (child Razor component);
- the class lands on MudPaper's native .mud-paper output, so ::deep pierces the component boundary. */
-::deep .session-hero-img {
- position: absolute;
- inset: 0;
- background-size: cover;
- background-position: center;
- background-repeat: no-repeat;
-}
-
-::deep .session-hero-placeholder {
- position: absolute;
- inset: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: var(--mud-palette-surface);
-}
-
-::deep .session-hero-placeholder .mud-icon-root {
- font-size: 120px;
-}
-
-/* Darkening gradient shim: stronger at the bottom (under the title/play row) and lighter toward
- the middle, with a top darken so the genre/share overlay stays legible too. */
-.session-hero-shim {
- position: absolute;
- inset: 0;
- pointer-events: none;
- background:
- linear-gradient(to bottom,
- rgba(0, 0, 0, 0.55) 0%,
- rgba(0, 0, 0, 0.15) 28%,
- rgba(0, 0, 0, 0.15) 55%,
- rgba(0, 0, 0, 0.75) 100%);
-}
-
-/* --- Top overlay: genre / date / share --- */
-.session-hero-top {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- z-index: 2;
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 1rem;
- padding: 1.25rem 1.5rem;
-}
-
-.session-overlay-date {
- display: flex;
- flex-direction: column;
- gap: 0.1rem;
-}
-
-.session-overlay-label {
- font-family: var(--deepdrft-font-mono);
- font-size: 0.6rem;
- letter-spacing: 0.25em;
- text-transform: uppercase;
- color: var(--deepdrft-green-accent);
-}
-
-.session-overlay-value {
- font-family: var(--deepdrft-font-body);
- font-size: 0.8rem;
- color: var(--deepdrft-white);
-}
-
-/* Genre chip themed to the glassmorphic NowPlaying surface. The class lands on MudChip's native
- .mud-chip output, so ::deep is required to reach it. */
-::deep .session-overlay-chip.mud-chip {
- background: rgba(250, 250, 248, 0.06);
- border: 1px solid rgba(250, 250, 248, 0.12);
- backdrop-filter: blur(8px);
- color: var(--deepdrft-white);
- font-family: var(--deepdrft-font-mono);
- letter-spacing: 0.12em;
-}
-
-/* --- Bottom overlay: cover thumb / title / play --- */
-.session-hero-bottom {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- z-index: 2;
- padding: 1.5rem;
-}
-
-.session-cover-thumb {
- flex: 0 0 auto;
- width: 96px;
- height: 96px;
- overflow: hidden;
- border: 1px solid rgba(250, 250, 248, 0.12);
- box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
-}
-
-.session-hero-titles {
- flex: 1 1 auto;
- min-width: 0;
-}
-
-.session-overlay-title {
- font-family: var(--deepdrft-font-display);
- font-size: clamp(1.75rem, 4vw, 2.75rem);
- font-weight: 400;
- line-height: 1.1;
- color: var(--deepdrft-white);
- text-shadow: 0 2px 12px rgba(0, 0, 0, 0.6);
-}
-
-.session-overlay-artist {
- font-family: var(--deepdrft-font-body);
- font-size: 0.85rem;
- letter-spacing: 0.08em;
- color: rgba(250, 250, 248, 0.7);
- margin-top: 0.35rem;
-}
-
-.session-hero-play {
- flex: 0 0 auto;
-}
-
-/* The play affordance and share button sit over a dark image — force their icon glyphs to the
- light theme color regardless of MudBlazor's Secondary palette. Both PlayStateIcon and
- SharePopover render MudIconButton / MudProgressCircular internals, so ::deep is required. */
-::deep .session-hero-play .mud-icon-button,
-::deep .session-hero-play .mud-progress-circular,
-::deep .session-hero-share .mud-icon-button {
- color: var(--deepdrft-white);
-}
-
-@media (max-width: 599.98px) {
- .session-hero {
- aspect-ratio: 3 / 4;
- min-height: 380px;
- }
-
- /* session-hero-bottom-row rides on MudStack's native output div, so ::deep is required. */
- ::deep .session-hero-bottom-row {
- flex-wrap: wrap;
- }
-
- .session-cover-thumb {
- width: 72px;
- height: 72px;
- }
-}