docs: mark track-card plain-shell refactor completed

This commit is contained in:
daniel-c-harvey
2026-06-05 16:27:51 -04:00
parent 3c17260f32
commit 7c401d75b5
2 changed files with 471 additions and 0 deletions
@@ -0,0 +1,446 @@
# 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`](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:
```css
.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:
```css
.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:
1. **Specificity / source-order.** `.mud-paper { background-color: ... }` is a single class
selector (specificity `0,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 to `deepdrft-styles.css` is 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:
`!important` is being used to paper over an *ordering/specificity* uncertainty, not a genuine
need.
2. **CSS isolation cannot help here.** This is the deeper structural reason. Blazor scoped CSS
(`*.razor.css`) works by stamping a `b-<hash>` attribute onto elements *that Blazor renders*
and rewriting the scoped selector to `.foo[b-<hash>]` (specificity `0,2,0` — would beat
`.mud-paper`). But `MudCard`/`MudPaper` render their **own** inner HTML; Blazor never stamps
the scope attribute onto MudBlazor's internal elements. So a `TrackCard.razor.css` rule
targeting `.mud-paper` would compile to `.mud-paper[b-<hash>]` and match **nothing**. (This is
also why no `TrackCard.razor.css` exists 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: hidden` box. A *transparent*
stacking context so the album-art div, fallback glass, and content scrim layer correctly.
No surface paint, no Material elevation (already `Elevation="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-paper` background means no `!important`
anywhere. 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.css`
*would* 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.razor` is built entirely from
plain `<div>`s with a `.razor.css` and 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-paper`
rulesets 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 of `Elevation="0"`.
- **Ripple:** `MudCard`/`MudPaper` do **not** ripple (ripple is a `MudButton`/`MudFab`/`MudIconButton`
behavior). The card is not clickable as a whole — the only interactive element is the `MudFab`,
which stays and keeps its ripple. Nothing lost.
- **ARIA / semantics:** `MudCard` renders 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
explicit `role`/`aria-label` to 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` / `-light` wrappers, 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:
```razor
<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-`MudCard` edge cases.
### Cons
- **Scatters styling concern into Razor.** The whole point of section 8 is centralized,
theme-aware card styling. An inline `Style` on 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.css`
still can't reach it. The seam stays closed.
- **Inline `transparent` is itself a quiet "magic override."** It's `!important` by another name —
a higher-specificity hammer applied at the element. Marginally cleaner than `!important` in 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:
```css
.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-surface` is 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 nests
`MudChip` and `MudFab` — both read other palette vars (`--mud-palette-primary` etc.), but a
future addition (a `MudMenu`, a `MudTooltip` surface, a `MudPaper` accent) 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-paper` continuing to read exactly
`--mud-palette-surface` for its background. If a MudBlazor version computes the surface
differently (e.g. a derived var, or `color-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 `background` declaration.
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 `!important`s, 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* `!important` from
section 8. Documenting it is not eliminating it.
- **`!important` is 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 `!important` tax.
- **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.
```razor
<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 12: `<MudCard Class="deepdrft-track-card-container" Elevation="0">`
`<div class="deepdrft-track-card-container">`. (Drop `Elevation="0"` — meaningless on a div; the
flat look is the default.)
- Line 1113: `<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 216223** — container:
```css
.deepdrft-track-card-container {
width: 250px;
height: 250px;
min-width: 250px;
position: relative;
overflow: hidden;
background: transparent; /* was: transparent !important */
}
```
**Line 262269** — fallback base:
```css
.deepdrft-track-card-fallback {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--deepdrft-navy-mid, #162437); /* was: ... !important */
}
```
**Line 272276** — fallback dark:
```css
.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 279282** — fallback light:
```css
.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-mid` fallback 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` / `-fallback` expecting a
`.mud-paper` ancestor (grep — `TracksGallery.razor` wraps 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-card` class, and that no `background ... !important` remains 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.
```