/* RSCanvas - the Canvas Suite design language, ported from launchcanvas/
   public/style.css (whose header says it in one line: dark panel chrome
   driven by --se-* custom properties, system font stack, flat components
   with 4-6px radii). themes.js recolors everything by setting the fifteen
   variables on :root; Classic below is what you get without it.

   The suite's rules travel with the chrome. The two that shape this file:
   no literal colours in component CSS (a hex outside :root is a component
   that will not theme), and state gets a SHAPE as well as a colour - pill,
   dot, bordered badge - so it survives a colour-blind operator and a
   photograph of a wall display. */

* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
    --se-panel: #262a33;
    --se-panel-2: #2d323d;
    --se-input: #1b1e25;
    --se-border: #3a4150;
    --se-txt: #e6e9ef;
    --se-txt-dim: #9aa3b2;
    --se-accent: #4c8bf5;
    --se-active: #0066cc;
    /* status colors - tuned to sit on dark panels; themes may override */
    --se-up: #2e9b57;
    --se-down: #d64545;
    --se-warn: #d9a92f;
    --se-unknown: #8a8f98;
    --se-series-out: #2e9b57;
    --se-logo-a: #4c8bf5;
    --se-logo-b: #2e9b57;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background: var(--se-panel-2);
    color: var(--se-txt);
    min-height: 100vh;
    font-size: 14px;
}

a { color: var(--se-accent); text-decoration: none; }
a:hover { text-decoration: underline; }

.hidden { display: none !important; }
.muted { color: var(--se-txt-dim); }
.small { font-size: 12px; }
.error-text { color: var(--se-down); font-size: 13px; min-height: 1.2em; }

/* ===== Top bar ===== */
#topbar {
    display: flex;
    align-items: center;
    gap: 14px;
    height: 42px;
    padding: 0 12px;
    background: var(--se-panel);
    border-bottom: 1px solid var(--se-border);
    position: sticky;
    top: 0;
    z-index: 100;
}
.brand { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 15px; }
.brand .logo { display: inline-flex; align-items: center; }
.brand-name { letter-spacing: 0.2px; }
.topbar-spacer { flex: 1; }
#topbar label { color: var(--se-txt-dim); font-size: 12px; }

#theme-select, #width-select {
    background: var(--se-input);
    color: var(--se-txt);
    border: 1px solid var(--se-border);
    border-radius: 4px;
    font-size: 12px;
    font-family: inherit;
    padding: 3px 6px;
}

/* ===== Layout ===== */
#main {
    max-width: 1100px;
    margin: 0 auto;
    padding: 18px 16px 60px;
}

.panel {
    background: var(--se-panel);
    border: 1px solid var(--se-border);
    border-radius: 6px;
    padding: 14px;
    margin-bottom: 16px;
    overflow-x: auto; /* rule 7: a 48-port table scrolls here, not the page */
}
/* A wide table scrolls in its own box (2026-09-30): the panel's own scroll
   above stays as the backstop, but it carried the device's name, banners and
   charts sideways with the interface table in a narrow window. */
.table-scroll { overflow-x: auto; }
.panel h2 { font-size: 15px; font-weight: 600; margin-bottom: 10px; }
.panel h2 .muted { font-weight: normal; font-size: 12px; }
.panel h3 {
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.6px;
    color: var(--se-txt-dim);
    margin: 16px 0 6px;
}

/* ===== Buttons & inputs (verbatim suite chrome) ===== */
button, .btn {
    font-family: inherit;
    font-size: 13px;
    padding: 5px 12px;
    border-radius: 4px;
    border: 1px solid var(--se-border);
    background: var(--se-panel-2);
    color: var(--se-txt);
    cursor: pointer;
}
button:hover, .btn:hover { border-color: var(--se-accent); }
button:disabled { opacity: 0.5; cursor: default; border-color: var(--se-border); }
.btn-primary {
    background: var(--se-accent);
    border-color: var(--se-accent);
    color: #fff;
    font-weight: 500;
}
.btn-primary:hover { filter: brightness(1.1); }

input[type="text"], input[type="password"], input[type="number"], select {
    font-family: inherit;
    font-size: 13px;
    padding: 5px 8px;
    background: var(--se-input);
    border: 1px solid var(--se-border);
    border-radius: 4px;
    color: var(--se-txt);
}
input:focus, select:focus { outline: none; border-color: var(--se-accent); }
label { font-size: 13px; }

.form-grid {
    display: grid;
    grid-template-columns: 180px 1fr;
    gap: 8px 12px;
    align-items: center;
    max-width: 600px;
}
.form-grid label { color: var(--se-txt-dim); }
.form-actions { margin-top: 12px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

/* ===== Login panel ===== */
#login .panel { max-width: 320px; margin: 18vh auto 0; }
#login h1 { font-size: 18px; font-weight: 600; margin-bottom: 14px; }
#login input { display: block; width: 100%; margin-bottom: 10px; }
#login button { width: 100%; }

/* ===== Tables ===== */
table.list { width: 100%; border-collapse: collapse; font-size: 13px; }
table.list th {
    text-align: left;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.6px;
    color: var(--se-txt-dim);
    font-weight: 600;
    padding: 6px 8px;
    border-bottom: 1px solid var(--se-border);
    white-space: nowrap;
}
table.list td {
    padding: 7px 8px;
    border-bottom: 1px solid var(--se-border);
    vertical-align: middle;
}
table.list tbody tr:last-child td { border-bottom: none; }
table.list td.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
table.list th.num { text-align: right; }
table.list td.mid, table.list th.mid { text-align: center; }

/* ===== State: a shape as well as a colour (rule 5) ===== */
.sev {
    display: inline-block;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.6px;
    padding: 1px 8px;
    border-radius: 9px;
    border: 1px solid;
}
.sev.crit { color: var(--se-down); border-color: var(--se-down); }
.sev.warn { color: var(--se-warn); border-color: var(--se-warn); }
.sev.pending, .sev.clearing { color: var(--se-txt-dim); border-color: var(--se-border); }
/* No .sev.active: active is the null value of state and renders as a blank
   cell (see renderAlerts) - only the transitional states earn a shape. */
.sev.cleared { color: var(--se-up); border-color: var(--se-up); }

/* Charts - classes only, no inline colour, so a chart re-themes with
   everything else. Two series is the maximum any RSCanvas chart draws:
   in/out for an interface, used/total for a filesystem, one line for
   everything else. */
.chart-wrap {
    border: 1px solid var(--se-border);
    border-radius: 6px;
    padding: 8px 10px;
    margin: 10px 0;
    background: var(--se-panel);
}
.chart-head {
    display: flex;
    align-items: baseline;
    gap: 10px;
    margin-bottom: 4px;
}
.chart-head .title { font-weight: 600; }
.chart-svg { width: 100%; height: 220px; display: block; }
.chart-grid-line { stroke: var(--se-border); stroke-width: 1; }
.chart-axis-label { fill: var(--se-txt-dim); font-size: 10px; }
.chart-cursor { stroke: var(--se-txt-dim); stroke-width: 1; stroke-dasharray: 3 3; }
/* The timeline on a chart (IDEAS-2026-10-15 item 4). A marker is a thin
   stroke with a SHAPE as well as a colour (suite rule 5): dotted for what
   the device did (a restart, ping lost or back), dashed for what an alert
   did. Maintenance is a faint band behind the lines it explains. */
.chart-marker { stroke-width: 1.4; }
.chart-marker-restart { stroke: var(--se-warn); stroke-dasharray: 1 3; }
.chart-marker-down { stroke: var(--se-down); stroke-dasharray: 1 3; }
.chart-marker-up { stroke: var(--se-up); stroke-dasharray: 1 3; }
.chart-marker-raise { stroke: var(--se-down); stroke-dasharray: 5 3; }
.chart-marker-clear { stroke: var(--se-up); stroke-dasharray: 5 3; }
.chart-band-maint { fill: var(--se-txt-dim); opacity: .12; }
/* a/b are the TRAFFIC pair - accent and the out-series colour, the same two
   the product uses for in/out everywhere. c/d are the TROUBLE pair, and
   they borrow the severity palette the tables already speak: errors take
   --se-down because a corrupted frame is a physical fault, discards take
   --se-warn because a drop is usually congestion or policy. Green and blue
   on a chart that only appears when something is wrong reads as reassuring,
   which is the opposite of its job (operator, 2026-08-15). The table paints
   a non-zero error cell with cell-warn already; this is the same
   vocabulary, not a second one. */
.chart-line-a { fill: none; stroke: var(--se-accent); stroke-width: 1.6; }
.chart-line-b { fill: none; stroke: var(--se-series-out); stroke-width: 1.6; }
.chart-line-c { fill: none; stroke: var(--se-down); stroke-width: 1.6; }
.chart-line-d { fill: none; stroke: var(--se-warn); stroke-width: 1.6; }
.chart-area-a { fill: var(--se-accent); opacity: 0.12; stroke: none; }
.chart-area-b { fill: var(--se-series-out); opacity: 0.12; stroke: none; }
.chart-area-c { fill: var(--se-down); opacity: 0.14; stroke: none; }
.chart-area-d { fill: var(--se-warn); opacity: 0.14; stroke: none; }
/* A reading alone between two gaps (charts.js): a path of one point draws nothing. */
.chart-dot-a { fill: var(--se-accent); }
.chart-dot-b { fill: var(--se-series-out); }
.chart-dot-c { fill: var(--se-down); }
.chart-dot-d { fill: var(--se-warn); }
.chart-swatch-a { background: var(--se-accent); }
.chart-swatch-b { background: var(--se-series-out); }
.chart-swatch-c { background: var(--se-down); }
.chart-swatch-d { background: var(--se-warn); }
.chart-legend {
    display: flex;
    gap: 14px;
    font-size: 11px;
    color: var(--se-txt-dim);
    margin-top: 2px;
}
.chart-legend .swatch {
    display: inline-block;
    width: 10px;
    height: 3px;
    border-radius: 2px;
    margin-right: 5px;
    vertical-align: middle;
}
.chart-tip {
    position: absolute;
    background: var(--se-panel);
    border: 1px solid var(--se-border);
    border-radius: 4px;
    padding: 4px 8px;
    font-size: 11px;
    color: var(--se-txt);
    pointer-events: none;
    white-space: nowrap;
}
.card.clickable:hover, tr.clickable:hover { background: var(--se-hover, rgba(127,127,127,0.12)); }

/* Sensor cards (the sensors slice) - the parent's device-page layout:
   a responsive grid of small cards, each a name, a large current reading,
   an optional context line, and a thin bar. The bar is the accent colour
   except when the sensor itself says alarm, which borrows the down colour
   the badges already use. */
.cards {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 10px;
    margin: 10px 0;
}
.card {
    border: 1px solid var(--se-border);
    border-radius: 6px;
    padding: 8px 12px;
    background: var(--se-panel);
}
/* The head is a row so only the NAME truncates: the track button and the
   threshold gear keep their width however long the sensor's name is. */
.card .card-head {
    display: flex;
    align-items: center;
    gap: 2px;
    min-width: 0;
}
.card .card-name {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 11px;
    color: var(--se-txt-dim);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.card .card-head .btn-track,
.card .card-head .card-gear { flex: none; }
.card .card-value {
    font-size: 20px;
    font-weight: 600;
    margin: 2px 0;
}
.card .card-sub {
    font-size: 11px;
    color: var(--se-txt-dim);
}
.card .bar {
    height: 4px;
    border-radius: 2px;
    background: var(--se-border);
    margin-top: 6px;
    overflow: hidden;
}
.card .bar-fill {
    height: 100%;
    background: var(--se-accent);
}
.card.alarm .card-value { color: var(--se-down); }
.card.alarm .bar-fill { background: var(--se-down); }
/* Service checks (slice 58): the same card, with an amber state for a
   check that answers with the wrong content or too slowly - serving, not
   well - and a dim one for a check whose last run is too old to stand. */
.card.svc-warn .card-value { color: var(--se-warn); }
.card.svc-stale .card-value { color: var(--se-txt-dim); }
.svc-form {
    border: 1px solid var(--se-border);
    border-radius: 6px;
    padding: 10px 12px;
    margin: 10px 0;
    background: var(--se-panel);
}
.svc-form .form-grid { max-width: 820px; }
.svc-form #svc-url { width: 100%; }
.svc-result-ok { color: var(--se-up); }
.svc-result-bad { color: var(--se-down); }

.badge {
    display: inline-block;
    font-size: 11px;
    padding: 1px 7px;
    border-radius: 9px;
    border: 1px solid var(--se-border);
    color: var(--se-txt-dim);
}
.badge.ok { color: var(--se-up); border-color: var(--se-up); }
.badge.fail { color: var(--se-down); border-color: var(--se-down); }
.badge.owed { color: var(--se-warn); border-color: var(--se-warn); }
/* Slice 22: "in maintenance" is deliberate-and-quiet, not failing - dashed
   and dim where owed is solid and warning-colored. */
.badge.maint { color: var(--se-txt-dim); border-color: var(--se-txt-dim); border-style: dashed; }
tr.maint-row td { opacity: .82; }
/* Slice 25: a standing policy reads like maintenance - deliberate-and-quiet -
   but dotted where a window is dashed, because "who declared this" and "when
   does it end" are different next questions. */
.badge.policy { color: var(--se-txt-dim); border-color: var(--se-txt-dim); border-style: dotted; }
/* Slice 62: acknowledged - someone is on it. Dim like the quiet badges, and
   SOLID, because nothing about the alert is withheld: it is still open. */
.badge.ack { color: var(--se-txt-dim); border-color: var(--se-txt-dim); }
/* Slice 64: a device's notes keep the lines and spacing they were written
   with - an owner on one line, an asset tag on the next. */
.notes-text { white-space: pre-wrap; overflow-wrap: anywhere; }
.notes-edit { min-height: 5em; resize: vertical; }
.badge.warn { color: var(--se-warn); border-color: var(--se-warn); }
/* Aged-out facts: a ghost interface, an unrated speed. Warn-coloured because
   the row deserves an eye, DASHED because nothing is alarming right now -
   solid warn is owed (a debt), dim dashed is maint (a decision). This class
   was referenced by the unrated pill before it was ever defined here, so
   that pill silently wore the plain badge look; now both facts share one
   deliberate rendering. */
.badge.stale { color: var(--se-warn); border-color: var(--se-warn); border-style: dashed; }

/* Slice 30: breathing room above the mint field. Even with rows present,
   the form sat flush against the last table row - close enough that the
   operator read the headers as belonging to the input. */
.token-mint { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--se-border); }

/* Status dot, sibling-standard 9px */
.dot {
    display: inline-block;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    margin-right: 6px;
    vertical-align: baseline;
    background: var(--se-unknown);
}
.dot.ok { background: var(--se-up); }
.dot.bad { background: var(--se-down); }
.dot.warn { background: var(--se-warn); }
/* Slice 25: off is dim-and-deliberate, never red and never blank - a hollow
   dot, so a powered-down transient device reads as a state, not a fault. */
.dot.off { background: transparent; border: 1.5px solid var(--se-txt-dim); }

.cell-warn { color: var(--se-warn); font-weight: 600; }
.cell-crit { color: var(--se-down); font-weight: 600; }
/* The Dashboard's up counts (2026-09-28, operator): green, but not bold -
   a column of healthy numbers should read calm beside the red. */
.cell-ok { color: var(--se-up); }

.allquiet { color: var(--se-txt-dim); padding: 10px 0; font-size: 13px; }

/* ===== Search ===== */
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.grow { flex: 1; min-width: 220px; }
#search-form { margin-bottom: 4px; }

/* Guidance, not an error: a zero-result search whose value is a substring of
   something real. Bordered like a badge and coloured with --se-warn, which is
   the suite's "look at this" without claiming anything broke. */
.hints {
    border: 1px solid var(--se-warn);
    border-left-width: 3px;
    border-radius: 4px;
    padding: 8px 10px;
    margin: 8px 0;
    font-size: 13px;
    color: var(--se-txt);
}
.hints div + div { margin-top: 4px; }

/* A refusal names what to change, so it gets the same shape in --se-down. */
.refusal {
    border: 1px solid var(--se-down);
    border-left-width: 3px;
    border-radius: 4px;
    padding: 8px 10px;
    margin: 8px 0;
    font-size: 13px;
    color: var(--se-txt);
}

td.msg { font-family: ui-monospace, Consolas, monospace; font-size: 12px; }
td.ts { white-space: nowrap; color: var(--se-txt-dim); font-variant-numeric: tabular-nums; }

/* U1: rows and headers you can act on. The pointer is the affordance; the
   hover tint is what makes a 100-row table trackable across a wide screen. */
tr.clickable, th.clickable { cursor: pointer; }
table.list tbody tr.clickable:hover { background: var(--se-hover, rgba(127,127,127,0.12)); }
th.clickable:hover { text-decoration: underline; }

/* The section nav. Tab-shaped rather than button-shaped: these select a view,
   they do not perform an action, and giving them the same affordance as
   "Delete" would be a small lie told constantly. The active one is marked by
   an accent underline and full-strength text, so the inactive ones recede
   without becoming unreadable - a nav whose current item is the only legible
   one makes people hunt for where they can go next. */
#nav {
    gap: 2px;
    margin-bottom: 14px;
    border-bottom: 1px solid var(--se-border);
    padding-bottom: 0;
}
.navbtn {
    background: none;
    border: none;
    border-bottom: 2px solid transparent;
    border-radius: 0;
    padding: 7px 14px;
    color: var(--se-txt-dim);
    font-size: 13px;
}
.navbtn:hover { color: var(--se-txt); border-color: var(--se-border); }
.navbtn.active {
    color: var(--se-txt);
    border-bottom-color: var(--se-accent);
    font-weight: 500;
}

/* threshold control on a sensor card (SLICE-THRESHOLDS-PLAN) */
.card-gear { float: right; background: none; border: none; cursor: pointer; opacity: .45; font-size: .9em; padding: 0 .2em; }
.card-gear:hover { opacity: 1; }
/* Something is set for this sensor or its device under the cog (2026-10-16):
   the card's settings fold away, so the cog says there is something there. */
.card-gear.set { opacity: .9; color: var(--se-warn); }
/* In an interface row the gear sits between untrack and unmute, not floated
   to the far edge of the cell where it reads as belonging to nothing. */
#entities .card-gear { float: none; }
.th-form { margin-top: .5em; gap: .3em; flex-wrap: wrap; font-size: .85em; }
.th-form input[type=number] { width: 4.5em; }
#group-alerts-table input[type=number] { width: 4.5em; }

/* Untracked entities: present but dimmed - "we saw this and chose not to
 * watch it" must stay visible, or the untrack control has no way back. */
tr.untracked td { opacity: 0.45; }
tr.untracked td:last-child { opacity: 1; }
.card.untracked { opacity: 0.55; }
.btn-track {
    font-size: 11px; padding: 1px 6px; margin-left: 6px;
    background: transparent; border: 1px solid var(--border, #555);
    border-radius: 3px; color: inherit; cursor: pointer;
}
.btn-track:hover { border-color: var(--accent, #888); }

.entities input.bad, #entities input.bad { border-color: #c0504d; }

/* Wide layout: the centred 1100px page is the identity default; wide is
   the opt-in for many roster columns on a big screen. One rule, applied
   as a body class so every view inherits it, same as the parent. */
body.wide #main { max-width: none; padding-left: 28px; padding-right: 28px; }
/* Long free text (location, application, credential) clips with an
   ellipsis and carries the full value in its title, so one verbose
   sysLocation cannot push nine other columns off the screen. */
table.list td.clip { max-width: 190px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ONE LINE A DEVICE (operator, 2026-10-22): a long name, or a reach pill
   saying "down 7h ago", made production's rows two and three lines tall.
   No cell wraps; the name is cut only past 32 characters (whole on hover),
   and a row too wide for the window scrolls in its frame. */
#devices td { white-space: nowrap; }
#devices td.devname { max-width: 32ch; overflow: hidden; text-overflow: ellipsis; }
#cols-list { display: grid; grid-template-columns: auto 1fr auto auto; gap: 2px 8px; align-items: center; margin-bottom: 8px; }
#cols-list button { font-size: 11px; padding: 0 6px; }
#cols-list .col-off { opacity: 0.55; }

/* A hint is guidance beside a control, never a verdict: dimmer than a fact,
 * present only when the machine noticed something a human must decide. */
#alert-facts .hint { color: var(--se-txt-dim); font-size: 12px; max-width: 62ch; }

/* ===== Device page name line (2026-09-25) =====
 * Name, declaration badges, counts, grouping and Modify on one wrapping line.
 * The spacer pushes Modify to the right edge without the 220px floor .grow
 * carries for form fields. */
.panel h2.device-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px 10px; }
.panel h2.device-head .head-spacer { flex: 1; }
.panel h2.device-head button { font-size: 12px; font-weight: normal; padding: 2px 10px; }
.device-grouping { font-weight: normal; display: inline-flex; gap: 12px; flex-wrap: wrap; }
/* The polled address, first after the name (2026-10-16): plain weight, full
   strength - it is the one fact here people copy. */
.device-address { font-weight: normal; }
.device-identity { display: flex; flex-wrap: wrap; gap: 4px 18px; margin: -4px 0 8px; overflow-wrap: anywhere; }
.device-identity .warn-text { color: var(--se-warn); }
/* The interface table's "hide untracked", in its header row (2026-10-16). */
#hide-untracked-ifs-label { font-weight: normal; cursor: pointer; }
/* The folded edit block, set off from the readings below it when open. */
.device-edit {
    border: 1px dashed var(--se-border);
    border-radius: 6px;
    padding: 8px 10px;
    margin: 8px 0 12px;
}
.device-edit .row + .row { margin-top: 6px; }

/* A role with no bulk action gets no selection column (applyCanGates). */
table.list.no-select tr > :first-child { display: none; }

/* ===== System tab: folding panels (2026-09-25) =====
 * The header stays, with its summary (the health verdict, retention's next
 * run), so a folded page still reads as a status board. */
.panel.folded > :not(h2) { display: none; }
.panel.folded h2 { margin-bottom: 0; }
.panel h2.fold-head { cursor: pointer; user-select: none; }
.fold-btn {
    background: transparent;
    border: none;
    padding: 0 6px 0 0;
    font-size: 12px;
    color: var(--se-txt-dim);
}
.fold-btn::before { content: '\25BE'; }
.panel.folded .fold-btn::before { content: '\25B8'; }

/* Your name in the top bar is the way to My account: a button that reads
 * as the plain text it replaced until it is pointed at. */
.whoami {
    background: transparent;
    border: none;
    padding: 0;
    color: var(--se-txt-dim);
}
.whoami:hover { color: var(--se-txt); text-decoration: underline; }

/* ===== Dashboard (2026-09-25) =====
 * Six top-10 widgets that flow two-up on a wide screen and stack on a
 * narrow one; compact rows, because ten of them have to fit a glance. */
.dash-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
    gap: 12px 16px;
    margin-top: 8px;
}
.widget { min-width: 0; overflow-x: auto; }
.widget h3 { margin-top: 4px; }
table.list.compact td, table.list.compact th { padding-top: 3px; padding-bottom: 3px; }
table.list tr.total-row td { font-weight: 600; border-top: 1px solid var(--se-border); }

/* The report's interface picker: a scrolling column of checkboxes, and the
 * chosen interfaces as removable chips. */
.report-pick {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: 2px 12px;
    max-height: 220px;
    overflow-y: auto;
    border: 1px solid var(--se-border);
    border-radius: 4px;
    padding: 6px 8px;
    margin: 6px 0;
}
.report-chosen { display: inline-flex; flex-wrap: wrap; gap: 6px; }
.chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: 12px;
    padding: 1px 4px 1px 8px;
    border: 1px solid var(--se-border);
    border-radius: 10px;
    background: var(--se-panel-2);
}
.chip-x { font-size: 11px; padding: 0 5px; border: none; background: transparent; color: var(--se-txt-dim); }
.chip-x:hover { color: var(--se-txt); }
/* The report's rows are read across; a device name broken over two lines
 * breaks the row. The table scrolls sideways in its panel instead. */
#report-table td { white-space: nowrap; }
/* The services and availability reports too (2026-10-23): their date,
   device and check cells wrapped a fortnight's table to two lines a row. */
#svc-report-table td, #avail-table td { white-space: nowrap; }

/* Service cards, made concise (2026-10-07, operator: "really dense"). Their
   own wider grid - a service says more than a sensor's one reading - every
   line cut at the card's edge rather than wrapping into a wall, the actions
   behind one button, and the whole story in the card's tooltip and in the
   line under its chart (#chart-detail), which wraps freely. */
.cards.svc-cards { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); }
.card .card-clip { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.card .card-sub + .card-sub { margin-top: 1px; }
.card .card-actions { display: flex; gap: 4px; flex-wrap: wrap; margin: 4px 0 2px; }
.card .card-more { letter-spacing: 1px; }
.chart-detail { white-space: pre-line; margin: 2px 0 8px; line-height: 1.5; }

/* Service health (2026-10-07): the counts are four short rows, so they keep
   to a widget's width rather than spreading five numbers across the panel. */
.dash-svc-kinds { max-width: 520px; }
/* The sites and web/TCP tables scroll sideways in their frames on a narrow
   screen, so a cell keeps to one line rather than stacking a name. */
#dash-sites td, #dash-svcs td { white-space: nowrap; }
