Files
FabledSteward/steward/static/steward.css
T
bvandeusenandClaude Opus 5.5 4dcf958022
CI / lint (push) Successful in 4s
CI / integration (push) Successful in 55s
CI / unit (push) Successful in 1m18s
CI / publish (push) Successful in 1m28s
feat(dashboard): widgets fit their content; wide screens get more grid columns (#5249)
Each widget is as tall as its content, never taller than its saved height
(where it scrolls). The dashboard is capped at 2400px instead of 1400px.
Past 12 columns' worth of width it adds columns (one per 110px, up to 24)
and places widgets again in reading order at their saved widths, so more sit
side by side instead of stretching. Edit mode shows and saves the
12-column layout, so a layout doesn't depend on the screen it was edited on.

The share view uses the same layout code (static/dash-layout.js).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:10:42 -04:00

389 lines
23 KiB
CSS

/* steward/static/steward.css — Steward's base stylesheet.
*
* Values come from theme.css, the stylesheet design system 6 ("Steward",
* inheriting FabledSword) generates. The short names below are Steward's
* aliases onto those tokens, kept so templates written against them still
* render; new work writes the --fs-* token. Contrast is a CI guard
* (tests/test_contrast.py): text 4.5:1 and control outlines 3:1 on page,
* card and hover, and status text on its own tint over each of them.
*/
:root {
--bg: var(--fs-surface-page);
--bg-card: var(--fs-surface-raised);
--bg-elevated: var(--fs-surface-hover);
--border: color-mix(in srgb, var(--fs-border-color) 70%, var(--fs-surface-raised));
--border-mid: var(--fs-border-color);
/* The outline that marks a control (input, select, ghost button): 3:1 on
page, card and hover (WCAG 1.4.11), where --border-mid is ~1.4-1.9:1. */
--border-control: color-mix(in srgb, var(--fs-text-secondary) 45%, var(--fs-border-color));
--text: var(--fs-text-primary);
--text-muted: var(--fs-text-secondary);
--text-dim: var(--fs-text-tertiary);
--gold: var(--fs-accent);
--gold-hover: color-mix(in srgb, var(--fs-accent) 85%, var(--fs-text-primary));
--gold-dim: var(--fs-accent-soft);
--accent: var(--fs-accent);
--accent-hover: var(--gold-hover);
/* Status: the -fg colour is the text and indicator; the -dim tint sits behind it.
Warning, error and their mix are lifted toward the primary text: the house
-fg falls to ~4.0:1 on its own tint over a hovered row (#5096). */
--green: var(--fs-success-fg);
--green-dim: color-mix(in srgb, var(--fs-success) 20%, transparent);
--yellow: color-mix(in srgb, var(--fs-warning-fg) 85%, var(--fs-text-primary));
--yellow-dim: color-mix(in srgb, var(--fs-warning) 20%, transparent);
/* Between warning and error: the worse-than-warning band (slow, degraded). */
--orange: color-mix(in srgb, color-mix(in srgb, var(--fs-warning-fg) 50%, var(--fs-error-fg)) 82%, var(--fs-text-primary));
--orange-dim: color-mix(in srgb, var(--fs-warning) 20%, transparent);
--red: color-mix(in srgb, var(--fs-error-fg) 85%, var(--fs-text-primary));
--red-dim: color-mix(in srgb, var(--fs-error) 20%, transparent);
/* Info (acknowledged): lighter than the house --fs-info-fg (50%), which falls
to 4.18:1 on its tint over a hovered row; 40% holds 5.09:1 there. */
--blue: color-mix(in srgb, var(--fs-info) 40%, var(--fs-text-primary));
--blue-dim: color-mix(in srgb, var(--fs-info) 20%, transparent);
--font-serif: var(--fs-font-display);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--fs-font-body); background: var(--bg); color: var(--text); line-height: var(--fs-leading-body); }
a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-hover); }
code { font-family: var(--fs-font-mono); font-size: 0.875em; background: var(--bg-elevated); padding: 0.1em 0.35em; border-radius: 3px; }
#candle-glow {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
background:
radial-gradient(ellipse 80% 60% at 50% 40%, color-mix(in srgb, var(--fs-accent) 4.5%, transparent) 0%, transparent 70%),
radial-gradient(ellipse 50% 40% at 20% 80%, color-mix(in srgb, var(--fs-accent) 2.5%, transparent) 0%, transparent 65%);
}
/* ── App shell: persistent left sidebar + content column ───────────────────── */
.app-shell { display: flex; min-height: 100vh; position: relative; z-index: 1; }
.sidebar {
width: 220px; flex-shrink: 0; background: var(--bg-card);
border-right: 1px solid var(--border-mid);
display: flex; flex-direction: column;
position: sticky; top: 0; height: 100vh; z-index: 20;
}
.sidebar .brand {
font-family: var(--font-serif); font-weight: 500; font-size: 1.15rem;
letter-spacing: 0.01em; color: var(--gold); text-decoration: none;
display: flex; align-items: center; gap: 0.5rem;
padding: 0.85rem 1.25rem; border-bottom: 1px solid var(--border);
}
.sidebar .brand img { flex-shrink: 0; }
/* Only the nav list scrolls when long — the brand and user/logout stay pinned. */
.side-nav { flex: 1; min-height: 0; overflow-y: auto; padding: 0.75rem 0; }
.nav-group { margin-bottom: 0.9rem; }
.nav-group-label {
font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.09em;
color: var(--text-dim); font-weight: 500; padding: 0 1.25rem; margin-bottom: 0.3rem;
}
.side-nav a {
display: flex; align-items: center; gap: 0.5rem;
padding: 0.4rem 1.25rem; font-size: 0.9rem; color: var(--text-muted);
border-left: 2px solid transparent; text-decoration: none;
transition: color .12s, background .12s, border-color .12s;
}
.side-nav a:hover { color: var(--text); background: var(--bg-elevated); }
.side-nav a.active {
color: var(--gold); border-left-color: var(--gold);
background: color-mix(in srgb, var(--gold) 8%, transparent);
}
/* A small count that needs attention: alerts beside the Alerts link, UniFi alarms. */
.count-badge {
margin-left: auto; padding: 0 0.45em; border-radius: var(--fs-radius-sm);
background: var(--red-dim); color: var(--red); font-size: 0.75rem; font-weight: 500;
font-variant-numeric: tabular-nums;
}
.side-user { border-top: 1px solid var(--border); padding: 0.7rem 1.25rem; }
.side-user form { margin: 0; }
.side-user button {
color: var(--text-muted); font-size: 0.84rem; text-decoration: none;
display: flex; align-items: center; gap: 0.4rem;
background: none; border: 0; padding: 0; font-family: inherit; cursor: pointer;
}
.side-user button:hover { color: var(--text); }
/* Keyboard focus: a clear inset ring on every interactive sidebar element. */
.sidebar .brand:focus-visible,
.side-nav a:focus-visible,
.side-user button:focus-visible {
outline: 2px solid var(--accent); outline-offset: -2px; color: var(--text);
}
.app-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.topbar { display: none; } /* slim mobile bar; shown only under 900px */
.nav-scrim { display: none; }
/* Layout */
main { padding: 1.5rem 2rem; max-width: 1400px; margin: 0 auto; width: 100%; min-width: 0; box-sizing: border-box; position: relative; z-index: 1; }
/* The dashboard keeps a cap too, a wider one: past 12 columns' worth it adds
grid columns rather than stretching widgets (static/dash-layout.js). */
main.main-wide { max-width: 2400px; }
/* Responsive: sidebar slides off-canvas under 900px, toggled by ☰ */
@media (max-width: 900px) {
.sidebar {
position: fixed; left: 0; top: 0; bottom: 0; height: 100%;
transform: translateX(-100%); transition: transform .2s ease;
box-shadow: var(--fs-shadow-3);
}
body.nav-open .sidebar { transform: translateX(0); }
.topbar {
display: flex; align-items: center; gap: 0.75rem; height: 48px; padding: 0 1rem;
background: var(--bg-card); border-bottom: 1px solid var(--border-mid);
position: sticky; top: 0; z-index: 15;
}
.sidebar-toggle {
background: none; border: none; color: var(--text-muted); font-size: 1.3rem;
cursor: pointer; line-height: 1; padding: 0.25rem; border-radius: 4px;
}
.sidebar-toggle:hover { color: var(--text); }
.sidebar-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.topbar .brand-sm {
font-family: var(--font-serif); color: var(--gold); font-weight: 500;
font-size: 1.15rem; text-decoration: none;
}
body.nav-open .nav-scrim {
display: block; position: fixed; inset: 0; background: var(--fs-overlay); z-index: 18;
}
}
@media (prefers-reduced-motion: reduce) {
.sidebar { transition: none; }
.side-nav a { transition: none; }
}
/* Alerts */
.alert { padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: 0.9rem; }
.alert-error { background: var(--red-dim); border: 1px solid color-mix(in srgb, var(--fs-error) 45%, transparent); color: var(--red); }
.alert-success { background: var(--green-dim); border: 1px solid color-mix(in srgb, var(--fs-success) 60%, transparent); color: var(--green); }
/* Cards */
.card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; margin-bottom: 1rem; }
.card-flush { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; overflow-x: auto; overflow-y: hidden; margin-bottom: 1rem; }
/* A table wider than the screen scrolls sideways inside its card instead of
widening the page at phone width. Any card holding a table does this, so no
page has to remember a wrapper; .table-wrap is for a table outside a card.
(Only cards with a table: a plain card can hold a popover that must not clip.) */
.card:has(table.table) { overflow-x: auto; }
.table-wrap { overflow-x: auto; }
/* Typography */
.page-title { font-family: var(--font-serif); font-size: 1.6rem; font-weight: 500; color: var(--text); line-height: var(--fs-leading-heading); margin-bottom: 1.5rem; }
/* The tiny micro-label: body face (display face only at 18px and up), caps, tracked. */
.section-title { font-family: var(--fs-font-body); font-size: var(--fs-size-tiny); font-weight: 500; color: var(--text-muted); text-transform: uppercase; letter-spacing: var(--fs-tracking-tiny); }
h1, h2 { font-family: var(--font-serif); font-weight: 500; }
h3, h4 { font-family: var(--fs-font-body); font-weight: 500; }
strong, b, th { font-weight: 500; }
/* Tables */
.table { width: 100%; border-collapse: collapse; }
.table th { text-align: left; padding: 0.6rem 1rem; color: var(--text-muted); font-weight: 500; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; border-bottom: 1px solid var(--border-mid); }
.table td { padding: 0.7rem 1rem; border-bottom: 1px solid var(--border); font-size: 0.9rem; }
.table tbody tr:last-child td { border-bottom: none; }
.table tbody tr:hover td { background: var(--bg-elevated); }
.table .td-actions { text-align: right; white-space: nowrap; }
/* Forms */
.form-group { margin-bottom: 1rem; }
label { display: block; margin-bottom: 0.3rem; font-size: 0.85rem; color: var(--text-muted); font-family: var(--fs-font-body); }
input[type=text], input[type=email], input[type=password], input[type=number], input[type=url], select, textarea {
width: 100%; padding: 0.5rem 0.75rem; background: var(--bg); border: 1px solid var(--border-control);
border-radius: 5px; color: var(--text); font-size: 0.9rem; font-family: inherit;
transition: border-color .15s;
}
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent); }
/* Keyboard focus is always visible: the design system's ring on anything
that takes focus. */
a:focus-visible, button:focus-visible, summary:focus-visible, [tabindex]:focus-visible,
input:focus-visible, select:focus-visible, textarea:focus-visible { outline: none; box-shadow: var(--fs-focus-ring); }
select { cursor: pointer; }
textarea { resize: vertical; }
/* Form fields: .field wraps a .field-label, the .input and an optional .field-hint. */
.field { display: flex; flex-direction: column; gap: 0.2rem; margin-bottom: 0.9rem; }
.field-label { display: block; margin-bottom: 0; font-size: 0.75rem; color: var(--text-muted); }
.field-hint { font-size: 0.75rem; color: var(--text-dim); }
.field-error { font-size: 0.85rem; color: var(--red); margin-bottom: 0.75rem; }
.input { width: 100%; padding: 0.4rem 0.65rem; background: var(--bg); border: 1px solid var(--border-control); border-radius: var(--fs-radius-sm); color: var(--text); font-size: 0.88rem; font-family: inherit; }
.input:focus { outline: none; border-color: var(--accent); }
.input:disabled { opacity: var(--fs-disabled-opacity); }
/* Buttons */
.btn { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.45rem 1rem; background: var(--accent); color: var(--fs-surface-page); border: none; border-radius: 5px; cursor: pointer; font-size: 0.875rem; font-family: inherit; font-weight: 500; transition: background .15s; text-decoration: none; }
.btn:hover { background: var(--accent-hover); color: var(--fs-surface-page); }
.btn-sm { padding: 0.3rem 0.65rem; font-size: 0.8rem; }
.btn-icon { padding: 0.3rem 0.5rem; line-height: 1; }
.btn-ghost { background: transparent; color: var(--text-muted); border: 1px solid var(--border-control); }
.btn-ghost:hover { background: var(--bg-elevated); color: var(--text); }
.btn-danger { background: var(--fs-action-destructive); color: var(--fs-text-on-action); }
.btn-danger:hover { background: var(--fs-action-destructive-hover); color: var(--fs-text-on-action); }
.btn-warn { background: var(--yellow-dim); color: var(--yellow); border: 1px solid color-mix(in srgb, var(--fs-warning) 60%, transparent); }
.btn-warn:hover { background: color-mix(in srgb, var(--fs-warning) 32%, transparent); }
/* Badges / Status */
.badge { display: inline-block; padding: 0.2em 0.55em; border-radius: var(--fs-radius-sm); font-size: 0.75rem; font-weight: 500; letter-spacing: 0.02em; }
.badge-green { background: var(--green-dim); color: var(--green); }
.badge-red { background: var(--red-dim); color: var(--red); }
.badge-yellow { background: var(--yellow-dim); color: var(--yellow); }
.badge-orange { background: var(--orange-dim); color: var(--orange); }
.badge-gold { background: var(--gold-dim); color: var(--fs-accent-fg); }
.badge-dim { background: var(--bg-elevated); color: var(--text-muted); }
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.dot-up { background: var(--green); box-shadow: 0 0 4px var(--green); }
.dot-down { background: var(--red); }
.dot-warn { background: var(--yellow); }
.dot-dim { background: var(--text-dim); }
/* Tones the status_badge / status_dot macros draw (formatting.STATES). */
.badge-ok { background: var(--green-dim); color: var(--green); }
.badge-warn { background: var(--yellow-dim); color: var(--yellow); }
.badge-crit { background: var(--red-dim); color: var(--red); }
.badge-info { background: var(--blue-dim); color: var(--blue); }
.dot-ok { background: var(--green); }
.dot-crit { background: var(--red); }
.dot-info { background: var(--blue); }
.dot-label { margin-left: 0.35rem; font-size: 0.85rem; }
/* Text in a tone: status_class() results ('ok' / 'warn' / 'crit' / 'unknown')
and the STATES tones. */
.tone-ok { color: var(--green); }
.tone-warn { color: var(--yellow); }
.tone-crit { color: var(--red); }
.tone-info { color: var(--blue); }
.tone-dim, .tone-unknown { color: var(--text-dim); }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
/* Empty state */
.empty { color: var(--text-muted); font-size: 0.9rem; padding: 2rem; text-align: center; }
.empty-state { display: flex; flex-direction: column; align-items: center; gap: 0.75rem; color: var(--text-muted); font-size: 0.9rem; padding: 2rem 1rem; text-align: center; }
/* Page header: title on the left, its actions on the right (page_header macro). */
.page-header { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; gap: 0.75rem 1rem; margin-bottom: 1.5rem; }
.page-header .page-title { margin-bottom: 0; }
.page-subtitle { margin-top: 0.3rem; color: var(--text-muted); font-size: 0.9rem; }
.page-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
/* Stat tile (stat_tile macro): a label over one headline number. */
.stat-tile { display: flex; flex-direction: column; gap: 0.25rem; padding: 0.9rem 1rem; background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--fs-radius-md); color: var(--text); min-width: 8rem; }
a.stat-tile:hover { border-color: var(--border-mid); background: var(--bg-elevated); color: var(--text); }
.stat-tile-label { font-size: var(--fs-size-tiny); font-weight: 500; color: var(--text-muted); text-transform: uppercase; letter-spacing: var(--fs-tracking-tiny); }
.stat-tile-value { font-size: 1.6rem; font-weight: 500; line-height: 1.1; font-variant-numeric: tabular-nums; }
.stat-tile-hint { font-size: 0.78rem; color: var(--text-dim); }
.stat-ok .stat-tile-value { color: var(--green); }
.stat-warn .stat-tile-value { color: var(--yellow); }
.stat-crit .stat-tile-value { color: var(--red); }
.stat-info .stat-tile-value { color: var(--blue); }
.stat-dim .stat-tile-value { color: var(--text-dim); }
.stat-strip { display: grid; grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); gap: 0.75rem; margin-bottom: 1rem; }
/* "Needs attention": what is wrong, first on a page. */
.problem-card { border-color: color-mix(in srgb, var(--fs-error) 45%, var(--border)); }
.problem-list { list-style: none; display: flex; flex-direction: column; gap: 0.45rem; }
.problem-list li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.25rem 0.6rem; overflow-wrap: anywhere; }
/* A label/value list: <dl class="kv"><dt>…</dt><dd>…</dd></dl>. */
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 0.35rem 1.25rem; font-size: 0.88rem; }
.kv dt { color: var(--text-muted); }
/* An Ansible run just started from the run form (ansible/run_started.html). */
.run-started-head { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; font-size: 0.9rem; }
.run-started-link { margin-left: auto; font-size: 0.82rem; }
.run-output {
margin: 0 0 0.5rem; padding: 0.75rem; background: var(--bg); border-radius: var(--fs-radius-sm);
font-family: var(--fs-font-mono); font-size: 0.78rem; color: var(--text-muted);
max-height: 400px; overflow-y: auto; white-space: pre-wrap;
}
/* Monospaced data: ids, limits, tags, paths. */
.mono { font-family: var(--fs-font-mono); font-size: 0.85em; }
/* The host page's health header (_host_vitals.html). */
.host-health-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.35rem 0.75rem; margin-bottom: 0.75rem; }
.host-health-summary { font-size: 0.92rem; }
/* A filled bar for a percentage; the fill takes its grade from status_class. */
.meter { height: 8px; background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
.meter-fill { height: 100%; }
.meter-ok { background: var(--green); }
.meter-warn { background: var(--yellow); }
.meter-crit { background: var(--red); }
.meter-unknown { background: var(--text-dim); }
.problem-detail { color: var(--text-muted); font-size: 0.82rem; }
/* Breadcrumbs */
/* Breadcrumb renders as a kicker line tucked directly above the page header
(.page-title), so nested and top-level views share one consistent header. */
.breadcrumb { display:flex; align-items:center; flex-wrap:wrap; gap:0.35rem; font-size:0.78rem; color:var(--text-dim); margin:0 0 0.45rem; }
.breadcrumb a { color:var(--text-muted); }
.breadcrumb a:hover { color:var(--text); }
.breadcrumb-sep { color:var(--text-dim); }
.breadcrumb-cur { color:var(--text-muted); }
/* Stat cards (dashboard) */
.stat-val { font-size: 2rem; font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; }
.stat-label { font-size: 0.8rem; color: var(--text-muted); margin-left: 0.3rem; }
/* Ping pills */
.ping-card { padding: 0.75rem 1.25rem; }
.ping-row { display:flex; align-items:center; gap:1rem; padding:0.45rem 0; border-bottom:1px solid var(--border); }
.ping-row:last-child { border-bottom:none; }
.ping-row-paused { opacity:0.7; }
.row-actions { display:flex; gap:0.35rem; flex-shrink:0; }
.ping-meta { min-width:120px; max-width:180px; flex-shrink:1; overflow:hidden; }
.ping-name { font-size:0.875rem; font-weight:500; display:block; overflow-wrap:anywhere; }
a.ping-name:hover { text-decoration:underline; }
/* Dashboard widget grid — Gridstack-rendered. Static (positioned but inert) in
view mode; the dashboard itself becomes the edit surface when Edit is toggled,
so drag-resize happens over live widget data. The bottom drawer is a fixed
overlay, so the dashboard width is identical entering/leaving edit mode. */
.grid-stack-item-content {
background:var(--bg-card); border:1px solid var(--border-mid); border-radius:8px;
padding:0.8rem 1rem; display:flex; flex-direction:column; overflow:hidden;
}
.grid-stack-item-content .dash-cell-body { flex:1; min-height:0; overflow:auto; }
/* The widget body is a query container, so fragments can restyle to their OWN
panel width (not the viewport) — widgets adapt as you resize them. */
.dash-cell-body { container-type:inline-size; }
/* Host-list widgets flow into columns as the panel widens: use the width, drop
the vertical deadspace; stay single-column (and untruncated) when narrow. */
.host-blocks { display:grid; grid-template-columns:1fr; gap:0 1.5rem; }
@container (min-width:520px) { .host-blocks { grid-template-columns:1fr 1fr; } }
@container (min-width:900px) { .host-blocks { grid-template-columns:1fr 1fr 1fr; } }
.panel-chrome { display:none; }
.panel-drag { cursor:grab; color:var(--text-dim); user-select:none; }
.panel-drag:active { cursor:grabbing; }
body.dash-editing .panel-chrome { display:inline-flex; align-items:center; }
body.dash-editing .grid-stack-item-content { outline:1px dashed var(--border-mid); outline-offset:-1px; }
body.dash-editing main { padding-bottom:46vh; }
.widget-drawer {
position:fixed; left:0; right:0; bottom:0; transform:translateY(100%);
transition:transform .25s ease; max-height:44vh; overflow-y:auto;
background:var(--bg-card); border-top:1px solid var(--border-mid);
box-shadow:var(--fs-shadow-3); z-index:60; padding:1rem 1.5rem 1.5rem;
}
body.dash-editing .widget-drawer { transform:translateY(0); }
.ping-addr { display:block; color:var(--text-muted); font-size:0.75rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.ping-pills { display:flex; gap:2px; flex:1; min-width:0; overflow:hidden; align-items:center; }
.pill { display:inline-block; width:7px; height:22px; border-radius:2px; cursor:default; transition:opacity .1s; }
.pill:hover { opacity:.75; }
.pill-empty { background:var(--bg-elevated); }
.pill-up { background:var(--green); }
.pill-down { background:var(--red); }
.ping-cur { min-width:72px; text-align:right; font-size:0.85rem; font-variant-numeric:tabular-nums; }
.ping-cur-good { color:var(--green); }
.ping-cur-warn { color:var(--yellow); }
.ping-cur-bad { color:var(--orange); }
.ping-cur-down { color:var(--red); font-weight:500; }
.ping-cur-nd { color:var(--text-dim); }
/* Widget headers */
.widget-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:0.75rem; }
.widget-title { font-family:var(--fs-font-body); font-size:var(--fs-size-tiny); font-weight:500; color:var(--text-muted); text-transform:uppercase; letter-spacing:var(--fs-tracking-tiny); }
.widget-link { font-size:0.8rem; color:var(--text-muted); }
/* A widget cell's own state (#5208): loading, couldn't load, and how fresh
the shown data is. Set by static/dash-cells.js. */
.widget-asof { font-size:var(--fs-size-tiny); color:var(--text-dim); white-space:nowrap; }
.widget-asof-failed { color:var(--yellow); }
.cell-state { font-size:0.85rem; color:var(--text-muted); padding:0.25rem 0; }
.cell-state-failed { display:flex; flex-direction:column; align-items:flex-start; gap:0.5rem; color:var(--yellow); }
.widget-link:hover { color:var(--text); }
/* Time range selector */
.range-btn { background:none; border:1px solid var(--border-control); border-radius:4px; color:var(--text-muted); padding:0.18rem 0.5rem; cursor:pointer; font-size:0.75rem; font-family:inherit; transition:background .1s,color .1s,border-color .1s; }
.range-btn:hover { background:var(--bg-elevated); color:var(--text); border-color:var(--accent); }
.range-btn.active { background:var(--accent); border-color:var(--accent); color:var(--fs-surface-page); }