Files
deepdrft/product-notes/track-card-css-architecture.md
T
2026-06-05 16:27:51 -04:00

22 KiB
Raw Blame History

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:

  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:

<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:

.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 !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 !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 (MudCarddiv, MudPaperdiv, MudCardContentdiv); 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 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:

.deepdrft-track-card-container {
    width: 250px;
    height: 250px;
    min-width: 250px;
    position: relative;
    overflow: hidden;
    background: transparent;        /* was: transparent !important */
}

Line 262269 — 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 272276 — 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 279282 — 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-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.