View mode splits each saved row into seven, so a widget no longer carries up to 69px of empty space below its content (Overview, SNMP). Edit mode and the saved layout keep 70px rows. The Busiest Containers host column aligns left like its header, its note fits one line, and the Hosts column reads Disk. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
419 lines
26 KiB
CSS
419 lines
26 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); }
|
||
/* Widget content, in the order of the widget-shape ruling (UI & Design):
|
||
a verdict line, the exceptions, then the healthy rest in a compact table
|
||
whose column labels appear once. Names cut off with … and show whole on
|
||
hover; the table never scrolls sideways. */
|
||
.w-verdict { font-size:0.85rem; margin-bottom:0.5rem; }
|
||
.w-table { width:100%; border-collapse:collapse; table-layout:fixed; font-size:0.82rem; font-variant-numeric:tabular-nums; }
|
||
.w-table th { text-align:right; font-weight:500; font-size:var(--fs-size-tiny); color:var(--text-dim); text-transform:uppercase; letter-spacing:var(--fs-tracking-tiny); padding:0 0 0.25rem 0.5rem; white-space:nowrap; }
|
||
.w-table td { text-align:right; padding:0.2rem 0 0.2rem 0.5rem; border-top:1px solid var(--border); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||
.w-table th:first-child, .w-table td:first-child { text-align:left; padding-left:0; }
|
||
.w-table .w-text { text-align:left; } /* a text column after the name: reads from the left, like its header */
|
||
.w-table td.w-name { overflow:hidden; text-overflow:ellipsis; }
|
||
.w-table td.w-name a { color:inherit; text-decoration:none; }
|
||
.w-table td.w-name a:hover { text-decoration:underline; }
|
||
.w-table .dot { margin-right:0.4rem; vertical-align:middle; }
|
||
.w-sub { display:block; font-size:0.72rem; color:var(--text-muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||
.w-more { font-size:0.75rem; color:var(--text-dim); padding-top:0.3rem; }
|
||
.w-section { font-size:var(--fs-size-tiny); color:var(--text-dim); text-transform:uppercase; letter-spacing:var(--fs-tracking-tiny); margin:0.5rem 0 0.2rem; }
|
||
/* The healthy rest: one line per item, as many columns as the widget's width
|
||
allows (it is a size container, so this follows the widget, not the window). */
|
||
.w-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(15rem, 1fr)); gap:0 1.25rem; }
|
||
.w-item { display:flex; align-items:center; gap:0.4rem; min-width:0; font-size:0.82rem; padding:0.12rem 0; }
|
||
.w-item .w-label { flex:1; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||
.w-item .w-label a { color:inherit; text-decoration:none; }
|
||
.w-item .w-meta { flex-shrink:0; font-size:0.72rem; color:var(--text-muted); font-variant-numeric:tabular-nums; }
|
||
/* A number with its share drawn behind it (--pct, 0–100), in the number's own
|
||
tone: the bar never stands without the figure it measures. */
|
||
.w-meter { background:linear-gradient(90deg, color-mix(in srgb, currentColor 20%, transparent) var(--pct, 0%), transparent var(--pct, 0%)); border-radius:3px; }
|
||
.w-note { font-size:0.7rem; color:var(--text-dim); margin-top:0.3rem; }
|
||
@container (max-width: 26rem) { .w-wide-only { display:none; } }
|
||
|
||
|
||
/* 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); }
|