Merge p10-w1-mix-hero-overlay into dev (Mix detail: shared ReleaseHeroOverlay, cover-as-overlaid-600px-square hero, ShowHeader scaffold gate)

This commit is contained in:
daniel-c-harvey
2026-06-16 20:54:51 -04:00
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
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). *@
@if (Header is not null)
its own Header fragment. Layout variance rides this slot, never a boolean flag (Phase 9 §5.3).
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
}
else
{
<MudStack Row AlignItems="AlignItems.Start" Justify="Justify.SpaceBetween" Style="margin: 2rem 0 1.5rem;">
<div class="deepdrft-track-detail-masthead">
<MudText Typo="Typo.h3">@Title</MudText>
<MudText Typo="Typo.h6" Color="Color.Primary">@Artist</MudText>
</div>
@if (Header is not null)
{
@Header
}
else
{
<MudStack Row AlignItems="AlignItems.Start" Justify="Justify.SpaceBetween" Style="margin: 2rem 0 1.5rem;">
<div class="deepdrft-track-detail-masthead">
<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. *@
@if (Track is not null)
{
<MudStack Row AlignItems="AlignItems.Center" Spacing="1">
<PlayStateIcon Track="@Track" Size="Size.Large" Color="Color.Secondary" OnToggle="@PlayTrack" />
</MudStack>
}
</MudStack>
@* Play only makes sense once a playable track is resolved. *@
@if (Track is not null)
{
<MudStack Row AlignItems="AlignItems.Center" Spacing="1">
<PlayStateIcon Track="@Track" Size="Size.Large" Color="Color.Secondary" OnToggle="@PlayTrack" />
</MudStack>
}
</MudStack>
}
}
@Hero
@@ -60,6 +60,14 @@ public partial class ReleaseDetailScaffold : ComponentBase
/// <summary>Optional medium-specific metadata block, rendered under a divider when present.</summary>
[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>
/// 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
@@ -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}"
@using DeepDrftPublic.Client.Controls
@using DeepDrftPublic.Client.Services
@inherits ReleaseDetailBase
<PageTitle>@(ViewModel.Release?.Title ?? "Mix") - DeepDrft</PageTitle>
@@ -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
<div class="mix-detail-foreground">
<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"
Artist="@release.Artist"
Track="@ViewModel.Track"
BackHref="/mixes"
BackLabel="All mixes"
ShowMeta="@(hasGenre || hasDate)"
ShowHeader="false"
ShowMeta="false"
ShowShareRow="false">
<TopContent>
@* The seven-knob band lives in its own full-width area below the back/lamp top row.
@@ -73,43 +77,28 @@ else
</MudTooltip>
</TopRightAction>
<Hero>
<div class="mix-detail-cover">
@if (!string.IsNullOrEmpty(release.ImagePath))
{
<MudPaper Elevation="2" Class="deepdrft-track-detail-cover-art"
Style="@($"background-image: url('api/image/{Uri.EscapeDataString(release.ImagePath)}');")" />
}
else
{
<MudPaper Elevation="2" Class="deepdrft-track-detail-cover-placeholder deepdrft-gradient-soft-secondary">
<MudIcon Icon="@Icons.Material.Filled.Album" Color="Color.Primary" />
</MudPaper>
}
</div>
@* 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. *@
<ReleaseHeroOverlay Class="mix-hero"
HeroImageKey="@release.ImagePath"
PlaceholderIcon="@Icons.Material.Filled.Album"
Title="@release.Title"
Artist="@release.Artist"
Genre="@release.Genre"
ReleaseDate="@release.ReleaseDate">
<ShareContent>
@* Release-mode share: copies the canonical /mixes/{entryKey} URL, not a single track (§3b). *@
<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>
<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>
</MudContainer>
</div>
@@ -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.
@@ -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 <div> (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;
}
+20 -69
View File
@@ -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;
<MudContainer MaxWidth="MaxWidth.Large" Class="session-detail-page">
@@ -52,70 +46,27 @@ else
&larr; All sessions
</MudLink>
@* 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="session-hero">
@if (!string.IsNullOrEmpty(heroImage))
{
<MudPaper Elevation="0" Square="true" Class="session-hero-img"
Style="@($"background-image: url('api/image/{Uri.EscapeDataString(heroImage)}');")" />
}
else
{
<MudPaper Elevation="0" Square="true" Class="session-hero-placeholder deepdrft-gradient-soft-secondary">
<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>
@* 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. *@
<ReleaseHeroOverlay HeroImageKey="@heroImage"
CoverThumbKey="@release.ImagePath"
PlaceholderIcon="@Icons.Material.Filled.Piano"
Title="@release.Title"
Artist="@release.Artist"
Genre="@release.Genre"
ReleaseDate="@release.ReleaseDate">
<ShareContent>
@* 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" />
</div>
</div>
@* Bottom overlay: cover thumbnail, title/artist, and the play affordance in one row. *@
<div class="session-hero-bottom">
<MudStack Row AlignItems="AlignItems.Center" Spacing="4" Class="session-hero-bottom-row">
@if (showCover)
{
<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>
<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>
</MudContainer>
}
@@ -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;
}
}