feat(mix-detail): extract shared ReleaseHeroOverlay; Mix cover becomes overlaid 600px square hero (Direction B)

This commit is contained in:
daniel-c-harvey
2026-06-16 20:53:25 -04:00
parent 9cf6bb4cf2
commit a6d25344b4
8 changed files with 399 additions and 301 deletions
@@ -25,27 +25,32 @@
@* The header region. A composer that wants the default masthead+play row supplies nothing; one @* The header region. A composer that wants the default masthead+play row supplies nothing; one
that needs a different arrangement (e.g. the Cut album's left-meta / right-cover split) supplies that needs a different arrangement (e.g. the Cut album's left-meta / right-cover split) supplies
its own Header fragment. Layout variance rides this slot, never a boolean flag (Phase 9 §5.3). *@ its own Header fragment. Layout variance rides this slot, never a boolean flag (Phase 9 §5.3).
@if (Header is not null) ShowHeader (a gate, not a layout flag) suppresses the region entirely for composers that carry
title/artist/play elsewhere — Mix overlays them on its hero. *@
@if (ShowHeader)
{ {
@Header @if (Header is not null)
} {
else @Header
{ }
<MudStack Row AlignItems="AlignItems.Start" Justify="Justify.SpaceBetween" Style="margin: 2rem 0 1.5rem;"> else
<div class="deepdrft-track-detail-masthead"> {
<MudText Typo="Typo.h3">@Title</MudText> <MudStack Row AlignItems="AlignItems.Start" Justify="Justify.SpaceBetween" Style="margin: 2rem 0 1.5rem;">
<MudText Typo="Typo.h6" Color="Color.Primary">@Artist</MudText> <div class="deepdrft-track-detail-masthead">
</div> <MudText Typo="Typo.h3">@Title</MudText>
<MudText Typo="Typo.h6" Color="Color.Primary">@Artist</MudText>
</div>
@* Play only makes sense once a playable track is resolved. *@ @* Play only makes sense once a playable track is resolved. *@
@if (Track is not null) @if (Track is not null)
{ {
<MudStack Row AlignItems="AlignItems.Center" Spacing="1"> <MudStack Row AlignItems="AlignItems.Center" Spacing="1">
<PlayStateIcon Track="@Track" Size="Size.Large" Color="Color.Secondary" OnToggle="@PlayTrack" /> <PlayStateIcon Track="@Track" Size="Size.Large" Color="Color.Secondary" OnToggle="@PlayTrack" />
</MudStack> </MudStack>
} }
</MudStack> </MudStack>
}
} }
@Hero @Hero
@@ -60,6 +60,14 @@ public partial class ReleaseDetailScaffold : ComponentBase
/// <summary>Optional medium-specific metadata block, rendered under a divider when present.</summary> /// <summary>Optional medium-specific metadata block, rendered under a divider when present.</summary>
[Parameter] public RenderFragment? MetaContent { get; set; } [Parameter] public RenderFragment? MetaContent { get; set; }
/// <summary>
/// Gate for the header region (masthead + play, or a custom <see cref="Header"/>). A composer that
/// carries title/artist/play elsewhere — e.g. Mix overlays them on its hero — sets this false to
/// suppress the duplicate. A gate, not a layout flag, so it is slot-consistent with
/// <see cref="ShowMeta"/> / <see cref="ShowShareRow"/> (Phase 9 §5.3). Defaults to shown.
/// </summary>
[Parameter] public bool ShowHeader { get; set; } = true;
/// <summary> /// <summary>
/// Gate for the metadata block. Lets a consumer supply a <see cref="MetaContent"/> fragment but /// Gate for the metadata block. Lets a consumer supply a <see cref="MetaContent"/> fragment but
/// suppress the divider + block when its data is empty (slot fragments cannot be conditionally /// suppress the divider + block when its data is empty (slot fragments cannot be conditionally
@@ -0,0 +1,110 @@
@namespace DeepDrftPublic.Client.Controls
@* Shared background-image hero with all release metadata overlaid: top row (genre/date + share),
bottom row (optional cover thumb + title/artist + play). Single source of truth for the overlay
composition consumed by both Session and Mix detail. Purely presentational — owns no data fetch
and no player wiring; play/share ride in as slots so each page keeps its own toggle. Per-page
aspect/sizing variance rides the Class parameter (e.g. Mix's square `mix-hero`), never a fork. *@
@{
var hasGenre = !string.IsNullOrEmpty(Genre);
var hasDate = ReleaseDate is not null;
// Show the cover thumbnail only when it differs from the hero background — otherwise it would
// duplicate the same image. Mix passes CoverThumbKey=null, so this is false there for free.
var showCover = !string.IsNullOrEmpty(CoverThumbKey) && CoverThumbKey != HeroImageKey;
}
@* 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. *@
<div class="release-hero @Class">
@if (!string.IsNullOrEmpty(HeroImageKey))
{
<div class="release-hero-img"
style="@($"background-image: url('api/image/{Uri.EscapeDataString(HeroImageKey)}');")"></div>
}
else
{
<div class="release-hero-placeholder deepdrft-gradient-soft-secondary">
<MudIcon Icon="@PlaceholderIcon" Color="Color.Primary" />
</div>
}
@* Darkening shim so overlaid text/controls stay legible over any image. *@
<div class="release-hero-shim"></div>
@* Top overlay: secondary details (genre, release date) and the share affordance. *@
<div class="release-hero-top">
<MudStack Row AlignItems="AlignItems.Center" Spacing="3" Class="release-hero-meta">
@if (hasGenre)
{
<MudChip T="string" Variant="Variant.Outlined" Class="release-overlay-chip">
@Genre
</MudChip>
}
@if (hasDate)
{
<div class="release-overlay-date">
<span class="release-overlay-label">Released</span>
<span class="release-overlay-value">@ReleaseDate!.Value.ToString("MMMM yyyy")</span>
</div>
}
</MudStack>
@if (ShareContent is not null)
{
<div class="release-hero-share">
@ShareContent
</div>
}
</div>
@* Bottom overlay: cover thumbnail, title/artist, and the play affordance in one row. *@
<div class="release-hero-bottom">
<MudStack Row AlignItems="AlignItems.Center" Spacing="4" Class="release-hero-bottom-row">
@if (showCover)
{
<div class="release-cover-thumb">
<div class="deepdrft-track-detail-cover-art"
style="@($"background-image: url('api/image/{Uri.EscapeDataString(CoverThumbKey!)}');")"></div>
</div>
}
<div class="release-hero-titles">
<div class="release-overlay-title">@Title</div>
<div class="release-overlay-artist">@Artist</div>
</div>
@if (PlayContent is not null)
{
<div class="release-hero-play">
@PlayContent
</div>
}
</MudStack>
</div>
</div>
@code {
/// <summary>Background image entry key. Null renders the placeholder treatment.</summary>
[Parameter] public string? HeroImageKey { get; set; }
/// <summary>Material icon for the no-image placeholder (Session: Piano; Mix: Album).</summary>
[Parameter] public required string PlaceholderIcon { get; set; }
/// <summary>
/// Optional small cover thumbnail in the bottom row. Shown only when it differs from
/// <see cref="HeroImageKey"/> (otherwise it would duplicate the background). Mix passes null.
/// </summary>
[Parameter] public string? CoverThumbKey { get; set; }
[Parameter] public required string Title { get; set; }
[Parameter] public string? Artist { get; set; }
[Parameter] public string? Genre { get; set; }
[Parameter] public DateOnly? ReleaseDate { get; set; }
/// <summary>Share affordance slot — each page passes its own SharePopover with the right params.</summary>
[Parameter] public RenderFragment? ShareContent { get; set; }
/// <summary>Play affordance slot — each page passes its PlayStateIcon wired to its own toggle.</summary>
[Parameter] public RenderFragment? PlayContent { get; set; }
/// <summary>Extra class for per-page aspect/sizing variance (e.g. Mix's square `mix-hero`).</summary>
[Parameter] public string? Class { get; set; }
}
@@ -0,0 +1,178 @@
/* Background-image hero with the release detail overlaid directly on top, themed to match the
NowPlaying glassmorphic family. The overlay shell (.release-hero, shim, top/bottom rows) is
plain <div>s; per-page aspect/sizing variance rides an extra class (e.g. .mix-hero) layered on
.release-hero. The default aspect here is Sessions' wide hero. */
/* Positioning context for every overlay. A tall, dominant frame rather than the 16:9 strip. */
.release-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);
}
/* The background-image surface and placeholder are plain <div>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 <div>, 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;
}
}
+49 -39
View File
@@ -1,5 +1,6 @@
@page "/mixes/{EntryKey}" @page "/mixes/{EntryKey}"
@using DeepDrftPublic.Client.Controls @using DeepDrftPublic.Client.Controls
@using DeepDrftPublic.Client.Services
@inherits ReleaseDetailBase @inherits ReleaseDetailBase
<PageTitle>@(ViewModel.Release?.Title ?? "Mix") - DeepDrft</PageTitle> <PageTitle>@(ViewModel.Release?.Title ?? "Mix") - DeepDrft</PageTitle>
@@ -31,8 +32,6 @@ else if (ViewModel.NotFound || ViewModel.Release is null)
else else
{ {
var release = ViewModel.Release; 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 @* 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 above it via the mix-detail-foreground stacking context. TrackId lets the visualizer couple to
@@ -41,12 +40,17 @@ else
<div class="mix-detail-foreground"> <div class="mix-detail-foreground">
<MudContainer MaxWidth="MaxWidth.Large" Class="mix-detail-container"> <MudContainer MaxWidth="MaxWidth.Large" Class="mix-detail-container">
@* 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). *@
<ReleaseDetailScaffold Title="@release.Title" <ReleaseDetailScaffold Title="@release.Title"
Artist="@release.Artist" Artist="@release.Artist"
Track="@ViewModel.Track" Track="@ViewModel.Track"
BackHref="/mixes" BackHref="/mixes"
BackLabel="All mixes" BackLabel="All mixes"
ShowMeta="@(hasGenre || hasDate)" ShowHeader="false"
ShowMeta="false"
ShowShareRow="false"> ShowShareRow="false">
<TopContent> <TopContent>
@* The seven-knob band lives in its own full-width area below the back/lamp top row. @* The seven-knob band lives in its own full-width area below the back/lamp top row.
@@ -73,43 +77,28 @@ else
</MudTooltip> </MudTooltip>
</TopRightAction> </TopRightAction>
<Hero> <Hero>
<div class="mix-detail-cover"> @* Cover-as-background hero with all metadata overlaid, square `mix-hero` sizing. The
@if (!string.IsNullOrEmpty(release.ImagePath)) cover art IS the background, so no separate cover thumbnail (CoverThumbKey defaults
{ to null). Share and play ride in as slots, matching Sessions. *@
<MudPaper Elevation="2" Class="deepdrft-track-detail-cover-art" <ReleaseHeroOverlay Class="mix-hero"
Style="@($"background-image: url('api/image/{Uri.EscapeDataString(release.ImagePath)}');")" /> HeroImageKey="@release.ImagePath"
} PlaceholderIcon="@Icons.Material.Filled.Album"
else Title="@release.Title"
{ Artist="@release.Artist"
<MudPaper Elevation="2" Class="deepdrft-track-detail-cover-placeholder deepdrft-gradient-soft-secondary"> Genre="@release.Genre"
<MudIcon Icon="@Icons.Material.Filled.Album" Color="Color.Primary" /> ReleaseDate="@release.ReleaseDate">
</MudPaper> <ShareContent>
} @* Release-mode share: copies the canonical /mixes/{entryKey} URL, not a single track (§3b). *@
</div> <SharePopover ReleaseEntryKey="@release.EntryKey" ReleaseMedium="@release.Medium" />
</ShareContent>
<PlayContent>
@if (ViewModel.Track is not null)
{
<PlayStateIcon Track="@ViewModel.Track" Size="Size.Large" Color="Color.Secondary" OnToggle="@PlayTrack" />
}
</PlayContent>
</ReleaseHeroOverlay>
</Hero> </Hero>
<MetaContent>
@if (hasGenre)
{
<div>
<MudChip T="string" Variant="Variant.Outlined" Color="Color.Tertiary" Class="deepdrft-genre-chip">
@release.Genre
</MudChip>
</div>
}
@if (hasDate)
{
<div>
<MudText Typo="Typo.overline">Released</MudText>
<MudText Typo="Typo.body1">@release.ReleaseDate!.Value.ToString("MMMM yyyy")</MudText>
</div>
}
</MetaContent>
<BodyContent>
@* Release-mode share: copies the canonical /mixes/{entryKey} URL, not a single track (§3b). *@
<div class="deepdrft-share-row">
<SharePopover ReleaseEntryKey="@release.EntryKey" ReleaseMedium="@release.Medium" />
</div>
</BodyContent>
</ReleaseDetailScaffold> </ReleaseDetailScaffold>
</MudContainer> </MudContainer>
</div> </div>
@@ -118,6 +107,27 @@ else
@code { @code {
protected override string PersistKey => "mix-detail"; 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 // 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 // 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. // control value or bridge push. The lava-lamp button's filled/outline glyph is driven off this flag.
@@ -4,12 +4,14 @@
z-index: 1; z-index: 1;
} }
/* Medium square cover — deliberately smaller than the 360px cut cover so the /* Mix's per-page hero variance: a centered medium square, overriding ReleaseHeroOverlay's default
waveform backdrop keeps room. The placeholder/art MudPaper fills this frame. */ wide (16/10) aspect. The cover art is square album art, and a smaller square frees the surrounding
.mix-detail-cover { canvas for the lava-lamp visualizer. The mix-hero class lands on the overlay component's root
.release-hero <div> (a child Razor component's native output), so ::deep is required to reach it. */
::deep .release-hero.mix-hero {
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
max-width: 220px; max-width: 600px;
margin: 0 auto 2rem; min-height: 0;
overflow: hidden; max-height: none;
box-shadow: 0 8px 28px color-mix(in srgb, var(--mud-palette-text-secondary) 18%, transparent); margin-inline: auto;
} }
+20 -69
View File
@@ -39,12 +39,6 @@ else
var heroKey = release.SessionMetadata?.HeroImageEntryKey; var heroKey = release.SessionMetadata?.HeroImageEntryKey;
// Hero image precedence: the session's dedicated hero, then the release cover, then a placeholder. // Hero image precedence: the session's dedicated hero, then the release cover, then a placeholder.
var heroImage = !string.IsNullOrEmpty(heroKey) ? heroKey : release.ImagePath; 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;
<MudContainer MaxWidth="MaxWidth.Large" Class="session-detail-page"> <MudContainer MaxWidth="MaxWidth.Large" Class="session-detail-page">
@@ -52,70 +46,27 @@ else
&larr; All sessions &larr; All sessions
</MudLink> </MudLink>
@* The hero is the positioning context for every overlay row; the gradient shim and the @* The overlay shows the cover thumbnail only when it differs from the resolved hero image —
top/bottom overlays are absolutely positioned children of this wrapper. *@ when there is no dedicated hero, heroImage already falls back to release.ImagePath, so the
<div class="session-hero"> thumb would duplicate the background. That logic lives in ReleaseHeroOverlay. *@
@if (!string.IsNullOrEmpty(heroImage)) <ReleaseHeroOverlay HeroImageKey="@heroImage"
{ CoverThumbKey="@release.ImagePath"
<MudPaper Elevation="0" Square="true" Class="session-hero-img" PlaceholderIcon="@Icons.Material.Filled.Piano"
Style="@($"background-image: url('api/image/{Uri.EscapeDataString(heroImage)}');")" /> Title="@release.Title"
} Artist="@release.Artist"
else Genre="@release.Genre"
{ ReleaseDate="@release.ReleaseDate">
<MudPaper Elevation="0" Square="true" Class="session-hero-placeholder deepdrft-gradient-soft-secondary"> <ShareContent>
<MudIcon Icon="@Icons.Material.Filled.Piano" Color="Color.Primary" />
</MudPaper>
}
@* Darkening shim so overlaid text/controls stay legible over any image. *@
<div class="session-hero-shim"></div>
@* Top overlay: secondary details (genre, release date) and the share affordance. *@
<div class="session-hero-top">
<MudStack Row AlignItems="AlignItems.Center" Spacing="3" Class="session-hero-meta">
@if (hasGenre)
{
<MudChip T="string" Variant="Variant.Outlined" Class="session-overlay-chip">
@release.Genre
</MudChip>
}
@if (hasDate)
{
<div class="session-overlay-date">
<span class="session-overlay-label">Released</span>
<span class="session-overlay-value">@release.ReleaseDate!.Value.ToString("MMMM yyyy")</span>
</div>
}
</MudStack>
@* Release-mode share: copies the canonical /sessions/{entryKey} URL, not a single track (§3b). *@ @* Release-mode share: copies the canonical /sessions/{entryKey} URL, not a single track (§3b). *@
<div class="session-hero-share"> <SharePopover ReleaseEntryKey="@release.EntryKey" ReleaseMedium="@release.Medium" />
<SharePopover ReleaseEntryKey="@release.EntryKey" ReleaseMedium="@release.Medium" /> </ShareContent>
</div> <PlayContent>
</div> @if (ViewModel.Track is not null)
{
@* Bottom overlay: cover thumbnail, title/artist, and the play affordance in one row. *@ <PlayStateIcon Track="@ViewModel.Track" Size="Size.Large" Color="Color.Secondary" OnToggle="@PlayTrack" />
<div class="session-hero-bottom"> }
<MudStack Row AlignItems="AlignItems.Center" Spacing="4" Class="session-hero-bottom-row"> </PlayContent>
@if (showCover) </ReleaseHeroOverlay>
{
<div class="session-cover-thumb">
<MudPaper Elevation="0" Square="true" Class="deepdrft-track-detail-cover-art"
Style="@($"background-image: url('api/image/{Uri.EscapeDataString(release.ImagePath!)}');")" />
</div>
}
<div class="session-hero-titles">
<div class="session-overlay-title">@release.Title</div>
<div class="session-overlay-artist">@release.Artist</div>
</div>
@if (ViewModel.Track is not null)
{
<div class="session-hero-play">
<PlayStateIcon Track="@ViewModel.Track" Size="Size.Large" Color="Color.Secondary" OnToggle="@PlayTrack" />
</div>
}
</MudStack>
</div>
</div>
</MudContainer> </MudContainer>
} }
@@ -1,174 +1,8 @@
/* Session detail is hero-dominant: a large background image with the detail components overlaid /* 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. */ Large container (set in markup) rather than the shared 760px detail column. */
::deep .session-detail-page { ::deep .session-detail-page {
padding-top: 2rem; padding-top: 2rem;
padding-bottom: 4rem; 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;
}
}