Move remaining static inline styles into dashboard.css, bump Now Playing text size

Three inline style="..." attributes were pure styling with no per-request
data behind them (a hero-block spacing tweak, a trailing note's margin, a
gap between two stacked rows) — moved each into dashboard.css as a scoped
structural selector so future adjustments happen in one place instead of
being split between the template and the stylesheet.

Also bumped .stream-title/.stream-user/.stream-icon in the Now Playing
card one step up the existing --fs-* scale (13px->15px, 12px->13px,
15px->18px) rather than introducing a one-off size.

Documented the "styles belong in CSS, not inline" rule in CLAUDE.md,
with the actual exception: per-request computed values (a meter's width,
a threshold colour) and JS-toggled initial visibility still can't live
in a static stylesheet and are fine to stay inline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Barely Removable 2026-08-27 06:27:43 -07:00
parent 1cc5a247cf
commit 326ab6cee5
3 changed files with 26 additions and 6 deletions

View file

@ -42,6 +42,17 @@ the user's Unraid home server. Server-side project directory is still named
`SCRIPT_NAME` subpath. Server-rendered config (timezone, script root) `SCRIPT_NAME` subpath. Server-rendered config (timezone, script root)
reaches JS through `<body data-*>`, never string-interpolated into a reaches JS through `<body data-*>`, never string-interpolated into a
script block. script block.
- **No `style="..."` attributes for anything static.** A one-off tweak
(button height, a margin, a font size) belongs in the matching
`static/css/*.css` file, scoped with a structural selector (an
adjacent-sibling or `:last-child` rule tied to the surrounding markup)
or a small modifier class — not inline on the element. The only
legitimate inline `style=` left in `dashboard.html` are per-request
computed values Jinja has to interpolate (a meter's `width: {{ pct
}}%`, a colour chosen by threshold) and JS-toggled initial visibility
(`display:none` on something `dashboard.js` reveals) — those can't
live in a static stylesheet. Everything else should read as a class
name, not a value.
- **`static/css/base.css`** — the single source of truth for the design - **`static/css/base.css`** — the single source of truth for the design
tokens, shared by all three templates: surfaces, a 6-step `--fs-*` type tokens, shared by all three templates: surfaces, a 6-step `--fs-*` type
scale, semantic colours, and `--tap: 44px` for minimum touch targets. scale, semantic colours, and `--tap: 44px` for minimum touch targets.

View file

@ -131,6 +131,9 @@ main {
} }
.row.cols-3 { grid-template-columns: repeat(3, 1fr); } .row.cols-3 { grid-template-columns: repeat(3, 1fr); }
.row.cols-2 { grid-template-columns: repeat(2, 1fr); } .row.cols-2 { grid-template-columns: repeat(2, 1fr); }
/* A section with two stacked rows (Watch insights) needs the same gap
between them as the grid uses between columns. */
.row + .row { margin-top: var(--gap); }
/* ── Card ───────────────────────────────────────────── */ /* ── Card ───────────────────────────────────────────── */
.card { .card {
@ -193,13 +196,13 @@ main {
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
} }
.stream:first-child { border-top: none; padding-top: 0; } .stream:first-child { border-top: none; padding-top: 0; }
.stream-icon { font-size: var(--fs-base); flex-shrink: 0; } .stream-icon { font-size: var(--fs-lg); flex-shrink: 0; }
.stream-info { min-width: 0; flex: 1; } .stream-info { min-width: 0; flex: 1; }
.stream-title { .stream-title {
font-size: var(--fs-sm); font-weight: 600; font-size: var(--fs-base); font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
} }
.stream-user { font-size: var(--fs-xs); color: var(--text-3); } .stream-user { font-size: var(--fs-sm); color: var(--text-3); }
.live-dot { .live-dot {
width: 7px; height: 7px; border-radius: 50%; width: 7px; height: 7px; border-radius: 50%;
background: var(--ok); flex-shrink: 0; background: var(--ok); flex-shrink: 0;
@ -242,6 +245,10 @@ dl.stats {
margin: 0; margin: 0;
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
} }
/* Only the Downloads card stacks stats below the hero figure/sub block —
give that pairing breathing room without affecting stats blocks that
open a card (Sonarr/Radarr put dl.stats right after the search bar). */
.card-body > [aria-live="polite"] + dl.stats { margin-top: 0.9rem; }
dl.stats > div { dl.stats > div {
background: var(--surface-2); background: var(--surface-2);
border: 1px solid var(--border); border: 1px solid var(--border);
@ -595,6 +602,8 @@ dd.is-accent { color: var(--accent); }
} }
.empty { color: var(--text-3); font-size: var(--fs-sm); } .empty { color: var(--text-3); font-size: var(--fs-sm); }
/* Storage card's usage note trails the meter with no room to spare below it. */
.meter-legend + .empty { margin-bottom: 0; }
/* ── Errors ─────────────────────────────────────────── */ /* ── Errors ─────────────────────────────────────────── */
.error { .error {

View file

@ -130,7 +130,7 @@
<span id="sab-timeleft-row" style="display:none;"> · ETA <span id="sab-timeleft"></span></span> <span id="sab-timeleft-row" style="display:none;"> · ETA <span id="sab-timeleft"></span></span>
</div> </div>
</div> </div>
<dl class="stats" style="margin-top:0.9rem;"> <dl class="stats">
<div><dt>Avg speed (7d)</dt><dd>{{ sabnzbd.avg_speed }}</dd></div> <div><dt>Avg speed (7d)</dt><dd>{{ sabnzbd.avg_speed }}</dd></div>
<div><dt>Grabbed (7d)</dt><dd>{{ sabnzbd.completed_7d }}</dd></div> <div><dt>Grabbed (7d)</dt><dd>{{ sabnzbd.completed_7d }}</dd></div>
<div><dt>Volume (7d)</dt><dd>{{ sabnzbd.week_size }}</dd></div> <div><dt>Volume (7d)</dt><dd>{{ sabnzbd.week_size }}</dd></div>
@ -168,7 +168,7 @@
{% if unraid.disk_count %}<span>{{ unraid.disk_count }} disk{{ 's' if unraid.disk_count != 1 }}</span>{% endif %} {% if unraid.disk_count %}<span>{{ unraid.disk_count }} disk{{ 's' if unraid.disk_count != 1 }}</span>{% endif %}
</div> </div>
{% if unraid.source == "local" %} {% if unraid.source == "local" %}
<p class="empty" style="margin-bottom:0;">Container disk usage ({{ unraid.mount }}). Set <code>UNRAID_API_KEY</code> for full array stats.</p> <p class="empty">Container disk usage ({{ unraid.mount }}). Set <code>UNRAID_API_KEY</code> for full array stats.</p>
{% endif %} {% endif %}
{% if unraid.disks or unraid.caches %} {% if unraid.disks or unraid.caches %}
@ -520,7 +520,7 @@
</div> </div>
{% if tautulli.top_tv_list or tautulli.top_movie_list %} {% if tautulli.top_tv_list or tautulli.top_movie_list %}
<div class="row cols-2" style="margin-top: var(--gap);"> <div class="row cols-2">
<div class="card"> <div class="card">
<div class="card-header"><h3>Top TV</h3></div> <div class="card-header"><h3>Top TV</h3></div>
<div class="card-body"> <div class="card-body">