- @if (!string.IsNullOrEmpty(heroImage))
+
+
+
+ ← 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")
+
+ }
+
+ @if (ViewModel.Track is not null)
{
-
- }
- else
- {
-
-
-
+
+
+
}
- @if (showCover)
- {
-
-
-
- }
-
-
- @if (hasGenre)
- {
-
-
- @release.Genre
-
-
- }
- @if (hasDate)
- {
-
- Released
- @release.ReleaseDate!.Value.ToString("MMMM yyyy")
-
- }
-
-
+
+ @* 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)
+ {
+
+ }
+
+
+
+
+
}
@code {
protected override string PersistKey => "session-detail";
+
+ [CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
+
+ // Mirrors the play-toggle wiring the shared scaffold owns. Session detail composes the player
+ // affordance directly (it diverges from ReleaseDetailScaffold for the overlay layout), so the
+ // toggle logic lives here: 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);
+ }
+ }
}
diff --git a/DeepDrftPublic.Client/Pages/SessionDetail.razor.css b/DeepDrftPublic.Client/Pages/SessionDetail.razor.css
index 59aa8b4..43551df 100644
--- a/DeepDrftPublic.Client/Pages/SessionDetail.razor.css
+++ b/DeepDrftPublic.Client/Pages/SessionDetail.razor.css
@@ -1,24 +1,174 @@
-/* Hero-dominant: a wide 16:9 image rather than the square cover used on track detail. */
-.session-detail-hero {
- margin: 0 auto 2rem;
- overflow: hidden;
+/* 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
+ Large container (set in markup) rather than the shared 760px detail column. */
+
+.session-detail-page {
+ padding-top: 2rem;
+ padding-bottom: 4rem;
}
-/* session-detail-hero-img rides on MudPaper (child Razor component); ::deep pierces its output. */
-::deep .session-detail-hero-img {
+/* Positioning context for every overlay. A tall, dominant frame rather than the 16:9 strip. */
+.session-hero {
+ position: relative;
width: 100%;
- aspect-ratio: 16 / 9;
+ 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;
}
-/* Secondary medium square cover sitting below the hero — same 220px frame as mix detail,
- smaller than the cut cover so the hero stays dominant. The cover-art MudPaper fills it. */
-.session-detail-cover {
- 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);
+::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;
+ }
}