22 KiB
Track Card CSS Architecture — eliminate !important from section 8
Status: completed. Author: product-designer. Date: 2026-06-05. Implementer: daniel-c-harvey. Landed: 2026-06-05.
Predecessor: track-card-theming.md (completed 2026-06-05) landed the
navy-glass + moss-green treatment and, in doing so, introduced the two !important rules this
spec exists to remove. That note is the what it should look like; this note is the how the
CSS should be structured so it looks like that without !important. Nothing here changes the
visual design — only the mechanism.
§1. Root cause analysis — why MudCard/MudPaper fight our CSS
1.1 The two !important rules in question
DeepDrftPublic/wwwroot/styles/deepdrft-styles.css section 8:
.deepdrft-track-card-container { ... background: transparent !important; } /* line 222 */
.deepdrft-track-card-fallback { ... background: var(--deepdrft-navy-mid, #162437) !important; } /* line 268 */
.deepdrft-theme-dark .deepdrft-track-card-fallback { ... !important; } /* line 273 */
.deepdrft-theme-light .deepdrft-track-card-fallback { ... !important; } /* line 280 */
Four !important declarations, all on background, all on elements rendered by MudBlazor
(MudCard → container, MudPaper → fallback).
1.2 What MudBlazor paints and with what selector
MudBlazor ships a single global <style> block (injected by MudThemeProvider, served as
_content/MudBlazor/MudBlazor.min.css). The relevant rules are roughly:
.mud-paper {
background-color: var(--mud-palette-surface); /* the conflict */
color: var(--mud-palette-text-primary);
...
}
.mud-card { /* composes .mud-paper */ }
MudCard and MudPaper both render a root element carrying the .mud-paper class, and
--mud-palette-surface resolves to DeepDrftPalettes.Surface — #162437 (navy-mid) in dark,
#FAFAF8 (off-white) in light. So every track card root gets a painted surface color we do
not want, because we layer our own album-art div / fallback glass / scrim underneath the content.
1.3 Why our scoped CSS can't reach it and !important was the escape hatch
Two compounding facts:
-
Specificity / source-order.
.mud-paper { background-color: ... }is a single class selector (specificity0,1,0). Our.deepdrft-track-card-container { background: ... }is also a single class selector (0,1,0). When specificity ties, source order wins — and MudBlazor's stylesheet load order relative todeepdrft-styles.cssis not something section 8 controls reliably (framework CSS, theme-injected CSS, and our sheet are separate links). To guarantee we win regardless of order, the author reached for!important. That is the smell:!importantis being used to paper over an ordering/specificity uncertainty, not a genuine need. -
CSS isolation cannot help here. This is the deeper structural reason. Blazor scoped CSS (
*.razor.css) works by stamping ab-<hash>attribute onto elements that Blazor renders and rewriting the scoped selector to.foo[b-<hash>](specificity0,2,0— would beat.mud-paper). ButMudCard/MudPaperrender their own inner HTML; Blazor never stamps the scope attribute onto MudBlazor's internal elements. So aTrackCard.razor.cssrule targeting.mud-paperwould compile to.mud-paper[b-<hash>]and match nothing. (This is also why noTrackCard.razor.cssexists today — it would be inert against the very elements that need overriding.) The team correctly fell back to global rules in section 8 — but global single-class rules don't beat MudBlazor without either higher specificity or!important.
The fundamental mismatch: we are using MudBlazor surface components (MudCard, MudPaper)
as layout shells while wanting to own their backgrounds entirely. MudBlazor's whole contract is
"I paint the surface from the palette." We are fighting the component's purpose. Every option
below is a different way to stop fighting it.
1.4 What we actually need from these two elements
Stripped to first principles, the container and fallback need:
- Container: a
position: relative, fixed-size,overflow: hiddenbox. A transparent stacking context so the album-art div, fallback glass, and content scrim layer correctly. No surface paint, no Material elevation (alreadyElevation="0"), no ripple. - Fallback: an absolutely-positioned full-bleed panel painted with our navy-glass (dark) or
navy-tint (light) treatment, with a border +
backdrop-filter.
Neither needs anything MudBlazor's surface model provides. That observation drives the recommendation.
§2. Option A — Replace container + fallback with plain <div>
Drop MudCard → <div class="deepdrft-track-card-container"> and the fallback MudPaper →
<div class="deepdrft-track-card-fallback">. Keep MudCardContent, MudText, MudChip,
MudFab (none of these own a competing background once the card root is a plain div — and the
text/chip/fab already work today).
Note: MudCardContent technically expects a MudCard ancestor, but it only emits a
<div class="mud-card-content"> with padding — no background, no JS, no required parent context.
It renders fine inside a plain div. If we want zero MudBlazor coupling in the shell, it can also
become a plain <div class="deepdrft-track-card-content"> (the class already carries all the
real styling — padding, flex, z-index, scrim). Recommend replacing it too, for consistency.
Pros
- Removes the conflict at the source. No
.mud-paperbackground means no!importantanywhere. Section 8 backgrounds become plain single-class rules that win by default. - Re-enables CSS isolation as an option. With plain divs, a future
TrackCard.razor.csswould work (Blazor stamps the scope attribute on divs it renders). We are not required to use it — section 8's public-only scoping is still convenient per the predecessor note — but the door reopens. This satisfies the "design the seam" instinct: we are not just fixing today, we are removing a structural block. - Precedent already exists in this codebase.
NowPlayingCard.razoris built entirely from plain<div>s with a.razor.cssand achieves the exact aesthetic these cards are imitating. This option makes TrackCard structurally consistent with the component it was told to match. The two should be siblings in construction, not one Material and one hand-rolled. - Lighter DOM + less CSS the browser must override. Removes
.mud-card/.mud-paperrulesets from applying to these nodes entirely.
Cons — what MudBlazor functionality is lost
- Elevation shadow: already
Elevation="0"on both elements (current Razor). Nothing lost — the design explicitly moved to flat-glass (predecessor §3d). A plain div is the honest expression ofElevation="0". - Ripple:
MudCard/MudPaperdo not ripple (ripple is aMudButton/MudFab/MudIconButtonbehavior). The card is not clickable as a whole — the only interactive element is theMudFab, which stays and keeps its ripple. Nothing lost. - ARIA / semantics:
MudCardrenders a plain<div>with no implicit role — it adds no ARIA semantics a plain div lacks. The card conveys meaning through its text content and the labeled FAB, not through a container role. Nothing lost. (If anything, a future enhancement could add an explicitrole/aria-labelto the div — easier on a plain element than fighting MudBlazor's.) - Theme reactivity of the surface color: we want to lose this — the whole problem is that
MudBlazor repaints the surface from the palette. Our section-8 rules are already theme-aware via
.deepdrft-theme-dark/-lightwrappers, so theme switching still works. - Migration cost: ~6 lines of Razor changed. Trivial.
Assessment: acceptable in full. There is no MudBlazor capability the track-card shell actually uses. This is the option that resolves the root cause rather than the symptom.
§3. Option B — Inline Style="background: transparent" on MudCard/MudPaper
Keep the MudBlazor components; pass an inline style to clear their background:
<MudCard Class="deepdrft-track-card-container" Elevation="0" Style="background: transparent;">
<MudPaper Class="deepdrft-track-card-fallback" Elevation="0" Style="background: transparent;">
Inline styles have specificity above any selector (and above !important from a stylesheet, for
non-!important inline declarations the inline wins over normal stylesheet rules; MudBlazor's
.mud-paper background is not !important, so inline transparent beats it cleanly).
But note: the fallback's real navy-glass paint then has to come from somewhere. If we set
the MudPaper transparent inline and paint via .deepdrft-track-card-fallback in CSS, we are
back to a CSS class trying to beat .mud-paper — except now .mud-paper is already transparent
(inline), so our class wins without !important. That works. The container is simpler: it just
wants transparent, which the inline gives directly.
Pros
- No
!important. Inline transparent neutralizes.mud-paper, then our classes paint normally. - Smaller change than Option A (no element swap), keeps MudBlazor components if there's a reason to (there isn't a strong one here).
- No risk of
MudCardContent-without-MudCardedge cases.
Cons
- Scatters styling concern into Razor. The whole point of section 8 is centralized,
theme-aware card styling. An inline
Styleon the component is a second place card appearance is decided — exactly the maintenance split the team should avoid. Next person editing the card background looks in section 8, doesn't find the override, and is confused. - Doesn't address the root mismatch. We're still using a surface component as a layout shell and then telling it "don't be a surface." It reads as an apology for the wrong component choice.
- CSS isolation stays blocked — MudBlazor still renders the inner HTML, so a
.razor.cssstill can't reach it. The seam stays closed. - Inline
transparentis itself a quiet "magic override." It's!importantby another name — a higher-specificity hammer applied at the element. Marginally cleaner than!importantin a sheet, but the same category of move: beating MudBlazor by force rather than not inviting the fight.
Assessment: cleaner than !important strictly speaking, but trades one smell (sheet
!important) for a milder one (scattered inline overrides) and leaves the structural mismatch and
the blocked isolation seam intact. A lateral move, not a fix.
§4. Option C — Override --mud-palette-surface locally on the container
Scope a CSS custom-property override to the card so MudBlazor's own
background-color: var(--mud-palette-surface) resolves to transparent inside the card subtree:
.deepdrft-track-card-container {
--mud-palette-surface: transparent;
}
Because .mud-paper reads var(--mud-palette-surface) and custom properties inherit, both the
container's own paint and the nested MudPaper fallback would inherit the transparent value — no
!important needed (we're changing the input to MudBlazor's rule, not overriding the rule).
Pros
- No
!important, no inline styles, no element swap. One declaration. - Works with MudBlazor's mechanism instead of against it — arguably the most "correct" CSS-vars approach. We feed the palette variable the value we want for this subtree.
- Centralized in section 8.
Cons
- Blast radius is the whole subtree.
--mud-palette-surfaceis read by many MudBlazor components, not just paper. Any MudBlazor component nested in the card that relies on--mud-palette-surface(now or in future) silently renders transparent. Today the card nestsMudChipandMudFab— both read other palette vars (--mud-palette-primaryetc.), but a future addition (aMudMenu, aMudTooltipsurface, aMudPaperaccent) would inherit the transparent surface and break subtly. This is a spooky-action-at-a-distance override: it works today but plants a trap. - Fragile against MudBlazor internals. It depends on
.mud-papercontinuing to read exactly--mud-palette-surfacefor its background. If a MudBlazor version computes the surface differently (e.g. a derived var, orcolor-mix), the override misses. Coupling our fix to a framework implementation detail is a maintenance liability — the kind of thing that breaks on a minor version bump with no compile error. - Still doesn't reopen CSS isolation. Same as B — MudBlazor renders the HTML.
- Obscure. The next maintainer sees a card rendering transparent and has no obvious thread to
pull — the cause is a custom-property override three rules up, not a
backgrounddeclaration. Low discoverability.
Assessment: the most elegant-looking, the most dangerous. It's a clever override that depends on framework internals and leaks into every descendant. Reject for the trap it sets, despite the aesthetics.
§5. Option D — Keep !important with a clear comment
Leave the four !importants, add a comment explaining the MudBlazor specificity conflict, accept
it as documented technical debt.
Pros
- Zero change, zero risk. The cards render correctly today.
- A good comment turns a mystery smell into a known, explained trade-off — which is genuinely
better than an unexplained
!important.
Cons
- Doesn't meet the stated goal. The task is explicitly to eliminate
!importantfrom section 8. Documenting it is not eliminating it. !importantis contagious. Once the card backgrounds are!important, any future rule that needs to legitimately override them (a "now playing" highlight state on a card, a selected state, a hover treatment) must also be!important, escalating the war. The predecessor note already floats a genre-chip treatment and skeleton tints — feature growth on this card is expected, and each new state inherits the!importanttax.- Leaves the structural mismatch and blocked isolation seam in place — same as B and C.
Assessment: honest, but it's the do-nothing option dressed as a decision. Acceptable only if all other options were blocked, which they are not. The comment is worth keeping as a fallback if Option A is somehow rejected.
§6. Recommendation — Option A (plain <div> shell)
Replace MudCard and the fallback MudPaper with plain <div>s. Replace MudCardContent with
a plain <div> too (it carries no behavior). Keep MudText, MudChip, MudFab. This removes
all four !important declarations, preserves the glass-card aesthetic exactly (the section-8 paint
rules are unchanged except for dropping !important), is the most maintainable, and aligns
TrackCard structurally with NowPlayingCard — the very component it was designed to imitate.
Why A over the others, in one line each:
- Over B (inline transparent): A fixes the root mismatch and reopens the isolation seam; B scatters overrides and leaves both problems.
- Over C (palette-var override): A has no blast radius and no coupling to MudBlazor internals; C plants a transparent-surface trap for every future nested component.
- Over D (keep
!important): A meets the stated goal; D doesn't.
Trade-off being accepted with A: TrackCard's shell stops being a MudBlazor component. That is a
feature here (the card never used MudBlazor's surface behavior), but it does mean the shell no
longer auto-reacts to a future palette change to Surface — which is exactly what we want, since
our theme-aware section-8 rules own the card's appearance. No real cost.
6.1 Exact Razor changes — DeepDrftShared.Client/Components/TrackCard.razor
Replace the current file body with the following. Only the three shell elements change
(MudCard→div, MudPaper→div, MudCardContent→div); all class hooks, MudText,
MudChip, and MudFab are preserved verbatim from the current file.
<div class="deepdrft-track-card-container">
@if (!string.IsNullOrEmpty(TrackModel?.ImagePath))
{
<div class="deepdrft-track-card-bg" style="background-image: url('@TrackModel.ImagePath');">
</div>
}
else
{
<div class="deepdrft-track-card-fallback"></div>
}
<div class="deepdrft-track-card-content">
<div class="deepdrft-track-info-top">
<MudText Typo="Typo.subtitle1"
Class="deepdrft-track-title text-truncate mb-1">
@TrackModel?.TrackName
</MudText>
<MudText Typo="Typo.caption"
Class="deepdrft-track-artist text-truncate mb-2">
@TrackModel?.Artist
</MudText>
</div>
<div class="deepdrft-track-info-middle">
@if (!string.IsNullOrEmpty(TrackModel?.Album))
{
<MudText Typo="Typo.caption"
Class="deepdrft-track-meta text-truncate">
@TrackModel.Album
</MudText>
}
@if (!string.IsNullOrEmpty(TrackModel?.Genre))
{
<MudChip T="string"
Size="Size.Small"
Variant="Variant.Filled"
Color="Color.Primary"
Class="deepdrft-genre-chip">
@TrackModel.Genre
</MudChip>
}
</div>
<div class="deepdrft-track-info-bottom">
@if (TrackModel?.ReleaseDate.HasValue == true)
{
<MudText Typo="Typo.caption"
Class="deepdrft-track-meta">
@TrackModel.ReleaseDate.Value.Year
</MudText>
}
else
{
<div></div>
}
<MudFab Color="Color.Primary"
Size="Size.Medium"
StartIcon="@PlayPauseIcon"
OnClick="@PlayClick"/>
</div>
</div>
</div>
Changes, precisely:
- Line 1–2:
<MudCard Class="deepdrft-track-card-container" Elevation="0">→<div class="deepdrft-track-card-container">. (DropElevation="0"— meaningless on a div; the flat look is the default.) - Line 11–13:
<MudPaper Class="deepdrft-track-card-fallback" Elevation="0"></MudPaper>→<div class="deepdrft-track-card-fallback"></div>. - Line 16:
<MudCardContent Class="deepdrft-track-card-content">→<div class="deepdrft-track-card-content">. - Line 72 (
</MudCard>) →</div>; the</MudCardContent>close (line 70) →</div>.
No change to TrackCard.razor.cs — it references no MudBlazor card types (only Icons,
TrackDto, EventCallback). The using MudBlazor; stays (still used by Icons.Material).
6.2 Exact CSS changes — DeepDrftPublic/wwwroot/styles/deepdrft-styles.css section 8
Remove !important from all four declarations. No other change — the colors, layout, and
theme-aware scoping all stay exactly as they are.
Line 216–223 — container:
.deepdrft-track-card-container {
width: 250px;
height: 250px;
min-width: 250px;
position: relative;
overflow: hidden;
background: transparent; /* was: transparent !important */
}
Line 262–269 — fallback base:
.deepdrft-track-card-fallback {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--deepdrft-navy-mid, #162437); /* was: ... !important */
}
Line 272–276 — fallback dark:
.deepdrft-theme-dark .deepdrft-track-card-fallback {
background: color-mix(in srgb, var(--deepdrft-navy) 55%, transparent); /* was: ... !important */
border: 1px solid rgba(250, 250, 248, 0.12);
backdrop-filter: blur(8px);
}
Line 279–282 — fallback light:
.deepdrft-theme-light .deepdrft-track-card-fallback {
background: color-mix(in srgb, var(--deepdrft-navy) 8%, var(--deepdrft-white)); /* was: ... !important */
border: 1px solid var(--deepdrft-border);
}
After this change, the four backgrounds are plain single-class (and class-under-theme-wrapper)
rules painting plain <div>s. Nothing competes; nothing needs !important. The
.deepdrft-theme-dark .deepdrft-track-card-fallback selector is 0,2,0 and the base is 0,1,0,
so the theme override correctly beats the base — the normal cascade, working as intended.
6.3 Verification checklist for the implementer
- Dark public site: card with album art shows art + scrim + green/off-white text; card without
art shows navy-glass fallback. No grey/teal surface flash on hydration (the base
--deepdrft-navy-midfallback rule still covers the no-wrapper-class window). - Light public site: fallback shows the navy-tint-on-white panel; text legible.
- Toggle dark/light: card repaints correctly (theme-wrapper-scoped rules still drive it).
- Confirm no other component references
.deepdrft-track-card-container/-fallbackexpecting a.mud-paperancestor (grep —TracksGallery.razorwraps cards but only positions them via.deepdrft-track-gallery-item-center). - Confirm the rendered card root is now a plain
<div>(devtools) with no.mud-paper/.mud-cardclass, and that nobackground ... !importantremains anywhere in section 8.
6.4 What this unblocks (not in scope, noted for the seam)
With a plain-div shell, the deferred polish items from the predecessor note become cleaner to
implement and could, if desired, migrate to a real TrackCard.razor.css (now functional):
genre-chip distinct treatment (predecessor §3a), skeleton tints (§3c), card hover/selected/now-
playing states. None are required by this spec; flagged so a future pass knows the seam is open.
§7. Fallback position
If Option A is rejected for a reason not surfaced here (e.g. a policy that shell components must
remain MudBlazor for consistency with other cards), the next-best is Option D with comments —
not B or C. B scatters the override and C couples to framework internals; a documented
!important is more honest and more maintainable than either clever hack. But A has no real cost
identified, so this fallback should not be needed.