/* =============================================================================
   License Vault — Monitoring module (workbench/license#60)

   The fleet dashboard: status tiles, the ranked Attention list, the node grid,
   and sparklines on node detail.

   Everything here is built from the existing tokens in app.css, so light and
   dark both work without a second palette to keep in sync, and the documented
   contrast ratios on those tokens keep applying.

   CSP: style-src 'self', no 'unsafe-inline'. Nothing may carry a style=
   attribute — it is silently dropped. Two consequences worth knowing before
   editing:

     * Bar widths reuse the generated .meter-fill[data-pct="N"] rules at the
       foot of metrics.css rather than defining their own. That file already
       emits 0–100, so a mini-meter only needs the class and the attribute.
     * Sparklines are inline SVG. An SVG <polyline points="..."> is geometry,
       not style, so it is unaffected by the CSP — which is why the node detail
       chart needs no JavaScript and no canvas.
   ========================================================================== */

/* --- status pills ---------------------------------------------------------
   Extends .pill from app.css. Colour alone never carries the meaning: the pill
   always contains the word ("Up", "Late", "Down"), so this reads correctly to
   anyone who cannot distinguish the hues.

   The colours for .pill.status-up / -late / -down / -never now live with the
   status vocabulary in app.css (workbench/license#136): up is Healthy, late
   Warning, down Critical, never Unknown/off. Defining them here as well would
   let this file, which loads second, quietly win.
   ------------------------------------------------------------------------ */

/* --- stat tiles -----------------------------------------------------------
   Explicit column counts, not auto-fit/auto-fill (workbench/license#77 §4).

   This was `repeat(auto-fit, minmax(150px, 1fr))` with the vCenter group in a
   SECOND .mon-tiles using auto-FILL. The two differ precisely when a grid has
   fewer items than it has room for: auto-fit collapses the empty tracks so the
   items stretch, auto-fill keeps them so the items stay narrow. So the second
   row could only match the first by coincidence, and did not -- measured at
   2560px, the fleet row rendered 203px cells above a vCenter row of 159px,
   under a comment claiming it kept the size of the row above.

   Both groups are one grid now, split by a full-width label, so the cells
   cannot disagree. Counts are fixed per breakpoint because the tile count is
   not fixed: mon_render_tiles() emits between 6 and 13 depending on whether
   anything is flapping, never-reported, disabled or polled from vCenter, and
   an auto- track count that moves with the content is what made the layout
   unpredictable in the first place. The counts are chosen so the cell never
   falls below ~150px, which is the floor the old minmax() gave for free and
   which a fixed count does not. Measured on the dashboard: 159px at 620,
   156 at 861, 160 at 1060, 160 at 1250, 163 at 1460, 271 at 2560. Below 620
   it is two columns at 139-260px, which is a phone.

   Media queries are on the viewport but the cell size depends on the
   CONTAINER, and the two callers differ: the dashboard carries the 232px
   subnav, Home does not. Every breakpoint below is therefore set from the
   DASHBOARD, the narrower of the two. Home is wider at the same viewport and
   simply gets roomier tiles.

   Measured, not derived, and the arithmetic was wrong twice: a first pass put
   121px cells on the dashboard at 900px (SECURITY UPDATES wraps to two
   lines), and a second still dipped to 141px at 1360. If you move these,
   re-measure -- the chrome between viewport and container is not a constant
   you can subtract.

   The 7- and 8-column steps were added when the vCenter tiles moved into the
   census row instead of having a heading of their own. That row now holds up
   to eight -- Up, Late, Down, Never reported, Disabled, and three from vCenter
   -- and at six columns the seventh wrapped onto a line of its own directly
   above the Needs attention break, which looked like a mistake.

   Re-measured on the DASHBOARD, same method and same floor: 7 columns gives
   147px at 1500, 150 at 1520, 153 at 1540, 156 at 1560; 8 gives 160px at 1760,
   165 at 1800, 180 at 1920. The breakpoints are set one step above where each
   crosses ~150px rather than exactly on it, because the crossing point was
   measured with one tile count and the count varies with what is wrong.

   Starting 7 columns at 1460 instead would give one clean row rather than six
   tiles and an orphan between 1460 and 1539, and it was rejected on evidence:
   at the 141px cell that produces, FIVE labels wrap to two lines -- including
   SECURITY UPDATES, which is the exact failure this floor was written after.
   At 153px only VCENTER ACKNOWLEDGED, UNHEALTHY CONTAINERS and RESTARTING
   REPEATEDLY wrap, and all three already wrap at the current 167px, so the
   step to 7 columns adds no wrapping at all. A tidier row is not worth a
   less readable tile. */
.mon-tiles {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-3);
    margin-bottom: var(--space-5);
}
@media (min-width:  620px) { .mon-tiles { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1060px) { .mon-tiles { grid-template-columns: repeat(4, 1fr); } }
@media (min-width: 1250px) { .mon-tiles { grid-template-columns: repeat(5, 1fr); } }
@media (min-width: 1460px) { .mon-tiles { grid-template-columns: repeat(6, 1fr); } }
@media (min-width: 1540px) { .mon-tiles { grid-template-columns: repeat(7, 1fr); } }
@media (min-width: 1760px) { .mon-tiles { grid-template-columns: repeat(8, 1fr); } }


/* Group label inside the tile grid. Spanning every column forces a row break,
   which is what separates the two groups now that they share a grid -- and
   unlike the old second-grid-plus-negative-margin, it says out loud what the
   break means. */
.mon-tiles-break {
    grid-column: 1 / -1;
    margin: var(--space-2) 0 0;
    font-size: var(--text-sm);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .07em;
    color: var(--fg-muted);
}
.mon-tiles-break:first-child { margin-top: 0; }

.mon-tile {
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space-4);
    box-shadow: var(--shadow-1);
    display: flex;
    flex-direction: column;
    gap: .25rem;
    min-width: 0;
}

/* On Home the tiles are links into the fleet view (workbench/license#70). They
   must not look like body links -- no underline, no link colour -- but they do
   have to show focus, because that is the only way a keyboard user can tell
   which one they are on. The visible ring is not optional decoration here. */
a.mon-tile {
    color: inherit;
    text-decoration: none;
}
a.mon-tile:hover {
    border-color: var(--accent);
}
a.mon-tile:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* The tile whose filter is currently on (workbench/license#101). Deliberately
   NOT the chips' solid accent fill: a tile is a big block and filling it would
   make the selected one the loudest thing on a page whose whole job is to make
   the CRITICAL one loudest. A ring plus a tinted ground says "this is the
   view you are looking at" without competing.

   Two signals, not just colour: the ring is drawn with outline (which stacks
   with, rather than replaces, the severity border-color below) and the label
   goes full-strength. Border colour alone would be invisible on the tiles that
   already have a coloured border, which is most of the ones worth selecting. */
.mon-tile.is-active {
    outline: 2px solid var(--accent);
    outline-offset: -1px;
    background: color-mix(in srgb, var(--accent) 8%, var(--bg-panel));
}
.mon-tile.is-active .mon-tile-label { color: var(--fg); }

/* A tile only takes on colour when it is reporting a problem. A dashboard
   where every tile is tinted teaches people to ignore the colour. */
.mon-tile.is-crit { border-color: var(--danger); }
.mon-tile.is-warn { border-color: var(--warn-fg); }
/* Routine catch-up work rather than a fault, so Info -- and the same colour
   as the "N updates" pill-info on the cards, so the tile and the badges agree
   (both moved from accent to --info with the #136 status vocabulary). */
.mon-tile.is-info { border-color: var(--info); }

.mon-tile-value {
    font-size: 2rem;
    font-weight: 600;
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
    color: var(--fg);
}
.mon-tile.is-crit .mon-tile-value { color: var(--danger); }
.mon-tile.is-warn .mon-tile-value { color: var(--warn-fg); }
.mon-tile.is-info .mon-tile-value { color: var(--info); }
.mon-tile.is-zero .mon-tile-value { color: var(--fg-dim); }

.mon-tile-label {
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--fg-muted);
}

.mon-tile-sub { font-size: var(--text-sm); color: var(--fg-muted); }

/* A severity badge carrying the VENDOR's scale rather than CVSS (#77 §8).
   The tiles on that page count CVSS from NVD; a finding NVD has not scored
   falls back to the vendor's word, which is drawn from the same five words and
   is not comparable. The source rides in the badge because a title attribute
   is invisible to anyone skim-reading, which is everyone. */
.pill.is-vendor { gap: .4rem; }
.pill .pill-src {
    font-size: var(--text-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    opacity: .75;
    border-right: 1px solid currentColor;
    padding-right: .4rem;
}

/* Affected hosts on a CVE row. Wraps rather than truncating -- the column is
   there precisely so the names are readable -- but is capped so one finding on
   eight hosts cannot push the description column off the screen, and is set a
   little smaller than the row so a wrapped list still reads as one cell. */
.mon-cve-hosts {
    font-size: var(--text-sm);
    line-height: 1.55;
    max-width: 34ch;
}

.mon-allclear {
    padding: var(--space-4);
    color: var(--fg-muted);
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}


/* --- "about this page" disclosure -----------------------------------------
   Explanatory prose, folded away (workbench/license#77 §10). The writing in
   this module is one of its better features, but it was rendered at full
   weight in the primary reading path on every load -- and it is longest on
   exactly the pages with the least data, where a paragraph, a legend and a
   caveat could wrap a single row.

   Styled as a quiet link rather than as an accordion card: it is an aside on a
   page about something else, and details.acc's panel treatment would give it
   more presence closed than the paragraphs had open, which is the opposite of
   the point.
   ------------------------------------------------------------------------ */
.mon-about { margin: 0 0 var(--space-4); }

.mon-about > summary {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    width: fit-content;
    cursor: pointer;
    font-size: var(--text-md);
    color: var(--accent);
    list-style: none;
}
/* Both spellings: Safari still needs the -webkit- pseudo-element. */
.mon-about > summary::-webkit-details-marker { display: none; }
.mon-about > summary::marker { content: ""; }
.mon-about > summary:hover { text-decoration: underline; }
.mon-about > summary:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: 3px;
}

/* Chevron, rotating on open. Borders rather than a glyph, matching
   details.acc's marker so the two disclosures behave alike. */
.mon-about > summary::before {
    content: "";
    width: 6px;
    height: 6px;
    border-right: 1.6px solid currentColor;
    border-bottom: 1.6px solid currentColor;
    transform: rotate(-45deg);
    transition: transform .15s ease;
    margin-right: .1rem;
}
.mon-about[open] > summary::before { transform: rotate(45deg); }

.mon-about-body {
    margin-top: var(--space-2);
    max-width: 76ch;
    color: var(--fg-muted);
    font-size: var(--text-md);
}
.mon-about-body p { margin: 0 0 var(--space-2); }
.mon-about-body p:last-child { margin-bottom: 0; }

@media (prefers-reduced-motion: reduce) {
    .mon-about > summary::before { transition: none; }
}

/* --- fact grid -------------------------------------------------------------
   The node summary's identity block, and the three ZFS figures (#77 §7).

   This was a two-column <table class="table mon-kv">. Twelve rows of roughly
   thirty characters each were given the card's full ~1500px measure, leaving
   about 1000px of empty column beside every value and pushing the resource
   meters -- the reason most people open the page -- off the first screen. A
   label above its value in two or three columns says exactly the same thing in
   a third of the height.

   Column counts are explicit for the reason set out on .mon-tiles above: this
   page carries the subnav, so the container is narrower than the viewport the
   media query sees, and the counts are set from the container that results.
   ------------------------------------------------------------------------ */
.mon-facts {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-3) var(--space-5);
    margin: 0;
}
@media (min-width:  760px) { .mon-facts { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1250px) { .mon-facts { grid-template-columns: repeat(3, 1fr); } }

/* A fact carrying a sentence rather than a value takes the full width instead
   of wrapping to four lines in a third of one. mon_kv() decides by length. */
.mon-fact.is-wide { grid-column: 1 / -1; }

.mon-fact dt {
    font-size: var(--text-sm);
    color: var(--fg-muted);
    margin-bottom: .1rem;
}
.mon-fact dd {
    margin: 0;
    font-size: var(--text-md);
    overflow-wrap: anywhere;
}

/* --- node grid ------------------------------------------------------------ */
.mon-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: var(--space-3);
    /* Not stretched to the row's tallest card (#135 §14): a down node has one
       line of data, and stretching it made a 275px empty red box. */
    align-items: start;
}

.mon-node {
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space-4);
    box-shadow: var(--shadow-1);
    display: flex;
    flex-direction: column;
    gap: .6rem;
    min-width: 0;
}
.mon-node.is-down { border-color: var(--danger); }
.mon-node.is-late { border-color: var(--warn-fg); }

/* A node that has been switched off deliberately is dimmed rather than hidden:
   it should not raise an alarm, but it also should not silently vanish from
   the inventory. */
.mon-node.is-disabled { opacity: .55; }

.mon-node-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2);
    flex-wrap: wrap;
}
.mon-node-name {
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}
.mon-node-meta { font-size: var(--text-sm); color: var(--fg-muted); }
.mon-node-badges { display: flex; gap: .35rem; flex-wrap: wrap; }

.mon-mini { display: flex; flex-direction: column; gap: .3rem; }
.mon-mini-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2);
    font-size: var(--text-sm);
}
.mon-mini-label { color: var(--fg-muted); }
.mon-mini-val   { font-family: var(--font-mono); color: var(--fg-muted); }

/* Compact bar. Reuses .meter-fill's generated width rules from metrics.css --
   see the header note. */
.mon-mini-bar {
    height: 5px;
    border-radius: 999px;
    background: var(--track);
    overflow: hidden;
}

/* --- filters -------------------------------------------------------------- */
.mon-filters {
    display: flex;
    gap: var(--space-2);
    flex-wrap: wrap;
    align-items: center;
    margin-bottom: var(--space-4);
}

/* .mon-filters was built for the chip rows, and the search forms reuse it for
   the same flex + gap. But app.css gives every text input width:100%, so the
   field claimed the whole row and wrapped the submit onto a line of its own --
   which is what the Pending packages and CVE search boxes have been doing.
   Size it from content instead, with room to grow and a readable ceiling: a
   package name or a CVE id needs nowhere near 1200px. */
.mon-filters input[type="text"],
.mon-filters input[type="search"] {
    width: auto;
    flex: 1 1 18rem;
    max-width: 32rem;
    min-width: 0;
}

.mon-filter {
    display: inline-block;
    padding: .25rem .7rem;
    border-radius: 999px;
    border: 1px solid var(--border);
    background: var(--bg-panel);
    color: var(--fg-muted);
    text-decoration: none;
    font-size: var(--text-sm);
}
.mon-filter:hover { background: var(--bg-hover); }
.mon-filter.is-active {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-fg);
}

/* --- sparkline ------------------------------------------------------------
   Inline SVG, no JS. preserveAspectRatio="none" lets one viewBox stretch to
   whatever width the card gives it, so the polyline needs no per-render
   measurement.
   ------------------------------------------------------------------------ */
.mon-spark-wrap {
    display: flex;
    flex-direction: column;
    gap: .3rem;
    min-width: 0;
}
.mon-spark {
    width: 100%;
    height: 44px;
    display: block;
    background: var(--bg-inset);
    border-radius: var(--radius);
    overflow: hidden;
}
.mon-spark polyline {
    fill: none;
    stroke: var(--accent);
    stroke-width: 1.5;
    vector-effect: non-scaling-stroke;   /* keeps the line 1.5px after stretch */
    stroke-linejoin: round;
    stroke-linecap: round;
}
.mon-spark polygon { fill: var(--accent-soft); stroke: none; }

.mon-spark-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2);
    flex-wrap: wrap;
}

.mon-sparks {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: var(--space-4);
}

/* --- event log ------------------------------------------------------------ */
.mon-events { width: 100%; border-collapse: collapse; }
.mon-events td { padding: .4rem .6rem; border-top: 1px solid var(--border-soft); vertical-align: top; }
/* "failed" was one letter wide on a phone, the output beside it taking the row. */
.mon-action-recent td:nth-child(2) { white-space: nowrap; }
.mon-events .ev-when { color: var(--fg-muted); white-space: nowrap; font-size: var(--text-sm); }
.mon-events .ev-crit { color: var(--danger); font-weight: 600; }
.mon-events .ev-warn { color: var(--warn-fg); font-weight: 600; }
.mon-events .ev-info { color: var(--fg-muted); }
/* Fleet (Ansible) change events (#151): told apart from state transitions. */
.mon-events .ev-fleet { color: var(--info); font-weight: 600; }

/* --- small screens --------------------------------------------------------
   .mon-tiles used to re-declare its columns here, as a third auto-* rule that
   won on phones by sitting later in the file. The explicit ladder at the top
   covers this range now (2 columns, 3 from 620px), so re-declaring it here
   only reintroduced the auto- behaviour the ladder exists to replace. */
@media (max-width: 560px) {
    .mon-tile-value { font-size: 1.5rem; }
}

/* --- OS marks -------------------------------------------------------------
   Inline SVG at currentColor, so a mark takes the colour of whatever it sits
   in -- muted in the card meta line, accent when its filter chip is active --
   without a second rule per context.
   ------------------------------------------------------------------------ */
.mon-os-icon {
    display: inline-flex;
    align-items: center;
    margin-right: .35rem;
    vertical-align: -.15em;
}
.mon-os-icon svg { width: 1.05em; height: 1.05em; }

/* The same mark at title size, on the node detail page. Sized in em so it
   tracks the h1 rather than needing its own breakpoints, and nudged down a
   little because a square mark set on the text baseline reads as sitting high
   next to a cap-height name. */
.mon-os-mark { margin-right: .5rem; vertical-align: -.22em; }
.mon-os-mark svg { width: 1.7em; height: 1.7em; }

/* Brand colour for the OS marks (workbench/license#77).

   The marks themselves stay fill="currentColor" and the colour is applied
   here, from a per-theme token. That is what makes a light and a dark value
   per brand possible: several of these are unreadable on the opposite ground
   -- Ubiquiti's #0559c9 all but disappears on --bg-panel in dark, Windows'
   #00a4ef is 2.78:1 on white -- and baking a hex into the SVG would leave one
   theme or the other with a mark nobody can see.

   It is also what keeps the escape hatch: any context that wants the old
   inherit-the-text-colour behaviour takes it back with `color: inherit`, which
   is exactly what the active filter chip does below. */
.mon-os-icon.is-freebsd { color: var(--os-freebsd); }
.mon-os-icon.is-ubuntu  { color: var(--os-ubuntu); }
.mon-os-icon.is-debian  { color: var(--os-debian); }
.mon-os-icon.is-rhel    { color: var(--os-rhel); }
.mon-os-icon.is-windows { color: var(--os-windows); }
/* Green only. The amber is a second token, read straight from the mark's
   presentation attribute -- see the ESXi note in src/modules/monitoring.php. */
.mon-os-icon.is-vmware  { color: var(--os-vmware); }
.mon-os-icon.is-unifi   { color: var(--os-unifi); }
.mon-os-icon.is-cisco   { color: var(--os-cisco); }
.mon-os-icon.is-synology { color: var(--os-synology); }
.mon-os-icon.is-netscaler { color: var(--os-netscaler); }
.mon-os-icon.is-other   { color: var(--os-other); }

/* Layout only. The brand colour comes from .mon-os-icon.is-*, which the chips
   also carry -- see mon_os_icon_span()'s call site. Duplicating the colour
   rules here would be a second place to forget when a family is added. */
.mon-filter-icon {
    display: inline-flex;
    align-items: center;
    margin-right: .35rem;
}
.mon-filter-icon svg { width: 1.05em; height: 1.05em; }
.mon-filter { display: inline-flex; align-items: center; }

/* An active chip is a solid --accent block. A brand colour on top of it is
   both unreadable and wrong -- the chip is saying "this filter is on", not
   "this is Ubuntu orange" -- so the mark goes back to inheriting, which on
   that chip is --accent-fg.

   ESXi's mark is two-coloured and its second colour does not ride on
   currentColor, so `color: inherit` alone would leave the amber behind on the
   accent block. Re-pointing the token at currentColor for this scope brings
   it with the rest. */
.mon-filter.is-active .mon-filter-icon {
    color: inherit;
    --os-vmware-accent: currentColor;
}

/* Filter rows stack: status, then OS family, then group. Each is its own row
   so a fleet with several groups does not push the OS chips onto a second line
   of a single mixed row, where it stops being obvious which chip does what. */
.mon-filters + .mon-filters { margin-top: .4rem; }

/* --- Scrolling tables ------------------------------------------------------ */

/* A wide table will not fit a phone, and the page body must never scroll
   sideways -- so the table gets its own scroll container and everything else on
   the page stays put. Began with the fleet table (#71); shared folders use it
   too, and the #136 fleet table has its own (.fl-scroll). */
.table-scroll {
    overflow-x: auto;
    /* Keeps the focus ring on a sorted header from being clipped by the
       scroll container's edge. */
    padding: 2px;
    margin: 0 -2px;
}



/* ---- vCenter alarms (workbench/license#75) ------------------------------- */

/* An acknowledged alarm is dimmed, never hidden: something that may never be
   cleared should stop shouting without disappearing from the list. Opacity
   rather than a grey colour so the status pill dims with everything else and
   cannot still read as urgent. */
.mon-alarm-ack td {
    opacity: .55;
}
.mon-alarm-ack:hover td {
    opacity: 1;
}

/* The optional note beside the Acknowledge button. Narrow on purpose -- it is
   a margin note ("expanding the LUN Monday"), not a description. */
.ack-note {
    width: 13rem;
    max-width: 40vw;
    margin-right: .35rem;
}

/* Node groups on the fleet dashboard (workbench/license#60).
   The accordion itself is app.css's details.acc -- this only adjusts the body
   padding, because a grid of cards already carries its own gutters and the
   accordion's default padding on top of them reads as a gap nobody asked for. */
.mon-group-body { padding: var(--space-3) var(--space-4) var(--space-4); }
.mon-group + .mon-group { margin-top: var(--space-3); }
/* The count sits against the name; the down-pill needs air so it does not read
   as part of the same number. */
.mon-group > summary .pill { margin-left: var(--space-2); }


/* Live action status (workbench/license#69).
   Colour carries the state, but never alone -- the wording says it too, which
   is the same rule the status pills follow. */
.mon-action-status { display: flex; align-items: center; gap: var(--space-2); margin: 0; }
.mon-action-verb   { font-weight: 600; }
.mon-action-status.status-failed  .mon-action-label { color: var(--danger); font-weight: 600; }
.mon-action-status.status-expired .mon-action-label { color: var(--warn-fg); }
.mon-action-status.status-done    .mon-action-label { color: var(--ok); }

/* The spinner exists in the markup for every state and is revealed only while
   something is moving, so updating it is one attribute rather than building
   elements -- and so PHP and app.js render an identical page for a given
   state. */
.mon-action-spin { display: none; }
.mon-action-status[data-live="1"] .mon-action-spin {
    display: inline-block;
    /* Bigger and thicker than the first attempt. At .85em with a 2px border it
       was a faint ring that read as a bullet point -- the movement was there and
       nobody could see it. The whole job of this element is to look like
       something is happening. */
    width: 1.05em; height: 1.05em;
    border: 3px solid var(--border);
    border-top-color: var(--accent);
    border-right-color: var(--accent);
    border-radius: 50%;
    /* Two of four sides coloured, so the sweep is obvious rather than a single
       tick chasing its tail. */
    animation: mon-spin .7s linear infinite;
    flex: none;
}
@keyframes mon-spin { to { transform: rotate(360deg); } }

/* SLOWER, NOT DIFFERENT. Two earlier attempts got this wrong in opposite
   directions: first `animation: none`, which left a static ring beside the
   words "applying updates…" and read as a hung page; then an opacity pulse,
   which is alive but is not what a spinner looks like -- someone watching it
   reported it as "not spinning, just pulsating", which is exactly right.

   The preference targets large or vestibular motion -- parallax, sweeping
   transitions, autoplaying video -- not a 16-pixel progress spinner. And every
   OTHER spinner in this application (patch-step-spin, se-spin) rotates
   regardless, so opting this one out made it the odd one on the same screen.

   So it keeps rotating and simply takes longer to do it. Still unmistakably a
   spinner, still visibly less motion than the default. */
@media (prefers-reduced-motion: reduce) {
    .mon-action-status[data-live="1"] .mon-action-spin {
        animation-duration: 2.2s;
    }
}

.mon-action-buttons { display: flex; flex-wrap: wrap; gap: var(--space-2); }
form.mon-action-buttons { margin: 0; align-items: center; }
.mon-action-then { display: inline-flex; align-items: center; gap: 8px; margin: 0; color: var(--muted); font-size: 13px; }

/* The compact in-flight badge on the grid (workbench/license#69).
   Smaller than the node page's version because here it competes with forty
   other cards -- spinner, state, and nothing else unless the node has said a
   phase. */
.mon-action-status.is-inline {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    font-size: var(--text-sm);
    color: var(--fg-muted);
    /* Truncates rather than pushing a card wide. A phase naming six KBs must
       not be what decides the grid's column width. */
    max-width: 100%;
    min-width: 0;
}
.mon-action-status.is-inline .mon-action-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* On a card the badge sits under the name, indented to the same gutter as the
   meters below it. */
.mon-node > .mon-action-status.is-inline {
    display: flex;
    margin: 0 0 var(--space-2);
}
.mon-action-status.is-inline.status-failed  { color: var(--danger); }
.mon-action-status.is-inline.status-expired { color: var(--warn-fg); }


/* An apply that succeeded without finishing the job (workbench/license#69).
   Inline inside the recent-actions table rather than a .flash block: it belongs
   to one row, and a full-width banner per row would drown the table. Colour
   only -- the row already carries the spacing. */
.mon-leftover {
    color: var(--danger);
    font-weight: 600;
}

/* Update history: the arrow between versions (workbench/license#76). Dimmer
   than the new version and lighter than the old one, so the row reads
   old → NEW rather than three things of equal weight. */
.hist-arrow { color: var(--fg-muted); }

/* Update history legend (workbench/license#76). One line per meaning rather
   than a wrapped paragraph: the pill has to sit next to the words it explains,
   and a floated pill mid-sentence breaks the line where the eye needs it. */
.hist-legend {
    display: flex;
    flex-direction: column;
    gap: .35rem;
    margin: .75rem 0 .5rem;
    font-size: var(--text-md);
    color: var(--fg-muted);
}
.hist-legend > span { display: flex; align-items: baseline; gap: .5rem; }
/* Fixed pill width so the descriptions line up in a column. Three labels of
   different lengths otherwise start the text at three different points, which
   is exactly the scanning cost this legend exists to remove. */
.hist-legend .pill { min-width: 4.75rem; justify-content: center; flex: none; }

/* Manage nodes: the rule between "change what exists" and "create something
   new" (workbench/license#62). The page does two unrelated jobs and used to run
   them together as one column of cards. */
.mon-zone-rule {
    border: 0;
    border-top: 1px solid var(--border);
    margin: 2rem 0 0;
}
/* Zone headings. Deliberately NOT .section-title: that class sets its own
   font-size, so an h2 and an h3 carrying it render identically and the
   hierarchy this page needs would exist only in the markup. These are the two
   things the page does; .section-title stays for the subsections under them. */
.mon-zone-title {
    font-size: 1.25rem;
    font-weight: 650;
    letter-spacing: 0;
    text-transform: none;
    color: var(--fg);
    margin: 1.75rem 0 .25rem;
}

/* flash('success', ...) is what every handler in this app actually calls, but
   the palette only ever defined .flash-ok -- so success messages rendered as an
   uncoloured box. Aliased rather than renamed: 54 call sites in this module
   alone say 'success', and changing the vocabulary to match the stylesheet is a
   bigger and riskier edit than teaching the stylesheet the word in use. */
.flash-success { background: var(--ok-soft); border-color: transparent; border-left-color: var(--ok); }

/* Node page tabs (workbench/license#77 §15).
 *
 * Links, not a widget -- see mon_render_tabs(). The strip scrolls sideways
 * rather than wrapping: a vCenter host can carry nine tabs, and a strip that
 * reflows to two rows moves the panel below it every time the node changes,
 * which is worse than a scroll on the handful of screens narrow enough to need
 * one. The page body must never scroll horizontally, so the overflow is on the
 * strip itself.
 */
.tabs {
    margin: var(--space-4) 0 var(--space-4);
    border-bottom: 1px solid var(--border);
    overflow-x: auto;
    overscroll-behavior-x: contain;
}
.tab-list {
    display: flex;
    gap: 20px;
    list-style: none;
    margin: 0;
    padding: 0;
    min-width: max-content;
}
/* #136: an underline strip, as the mockups draw it -- no box, no hover
 * fill. The current tab is --fg at 600 on an accent rule; the rest are
 * --muted at 500. No -1px margin to sit the rule on the strip's border: the
 * strip is an overflow-x scroller, which would clip that pixel into a
 * one-pixel vertical scroll. */
.tab {
    display: flex;
    align-items: center;
    gap: 6px;
    height: 36px;
    padding: 0;
    color: var(--fg-muted);
    font-weight: 500;
    text-decoration: none;
    white-space: nowrap;
    border-bottom: 2px solid transparent;
}
.tab:hover {
    color: var(--fg);
    border-bottom-color: var(--border);
    text-decoration: none;
}
.tab.is-current {
    color: var(--fg);
    border-bottom-color: var(--accent);
    font-weight: 600;
}
/* The count is secondary to the label but must stay legible: --fg-muted, not
 * --fg-dim, which is a border/decoration colour and fails 4.5:1 as text. */
.tab-count {
    color: var(--fg-muted);
    font-weight: 400;
    font-variant-numeric: tabular-nums;
}

/* The panel owns the space under the strip. The first heading inside it sits
 * right under the border, so it loses the top margin a section-title carries
 * when it follows other content. */
.tab-panel > .section-title:first-child,
.tab-panel > .card:first-child,
.tab-panel > .acc:first-child { margin-top: 0; }

/* ===========================================================================
   Node view and node drawer (workbench/license#136 Phase 5, #135 §7-§9)
   The node page and the drawer on the Nodes page render the same markup
   (mon_render_node_view); everything below the "drawer" heading is only the
   frame the drawer puts round it. On the #136 tokens, so right in both themes
   without a dark block of its own.
   ======================================================================== */

/* The view sizes its own layouts from ITS width, not the viewport's: the same
   Summary sits in a ~1500px page column and in a 760px drawer. */
.mon-node-view { container-type: inline-size; }
/* A panel's table wider than its column (Containers, Pending updates, in a
   760px drawer or on a phone) scrolls inside its card, never the page. */
.mon-node-view .tab-panel .card { overflow-x: auto; }

/* --- reason banner (#135 §7) -----------------------------------------------
   Why a node we are not hearing from is quiet: the poller's last error, the
   failure run and the last success -- the first thing on the page. */
.mon-reason {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0 0 var(--space-3);
    padding: 10px 12px;
    border-radius: var(--radius-lg);
    font-weight: 500;
    overflow-wrap: anywhere;
}
.mon-reason svg { width: 16px; height: 16px; flex: none; }
.mon-reason.is-crit { background: var(--crit-soft); color: var(--crit); }
.mon-reason.is-warn { background: var(--warn-soft); color: var(--warn); }
.mon-reason.is-off  { background: var(--off-soft);  color: var(--off); }

/* --- attention strip (#135 §8) ---------------------------------------------
   One .chip per problem, each a link to the tab that explains it. */
.mon-attn {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 0 0 var(--space-2);
    padding: 0;
    list-style: none;
}
.mon-attn .chip { height: 30px; padding: 0 10px; text-decoration: none; }
.mon-attn .chip svg { width: 14px; height: 14px; }
/* a.chip:hover blanks a chip to the hover grey; keep the level's wash, as the
   fleet's signal counters do, and ring it. */
.mon-attn a.chip:hover { box-shadow: inset 0 0 0 1px currentColor; text-decoration: none; }
.mon-attn a.chip.is-crit:hover { background: var(--crit-soft); color: var(--crit); }
.mon-attn a.chip.is-warn:hover { background: var(--warn-soft); color: var(--warn); }
.mon-attn a.chip.is-off:hover  { background: var(--off-soft);  color: var(--off); }

/* --- Summary: resource gauges ----------------------------------------------
   The number large, the bar under it, the detail beneath. The bar is metrics.
   css's .meter-bar/.meter-fill, so its thresholds and colours are the app's. */
.mon-gauges {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: var(--space-3);
    margin: 0 0 var(--space-4);
}
.mon-gauge {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
    padding: 12px 14px;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
}
.mon-gauge-label {
    color: var(--muted);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: .05em;
    text-transform: uppercase;
}
.mon-gauge-val {
    font-family: var(--font-mono);
    font-size: 22px;
    font-weight: 600;
    line-height: 1.2;
    color: var(--text);
}
.mon-gauge-val.is-warn { color: var(--warn); }
.mon-gauge-val.is-crit { color: var(--crit); }
.mon-gauge-sub { color: var(--muted); font-size: var(--text-sm); overflow-wrap: anywhere; }

/* --- Summary: facts beside recent events ----------------------------------- */
.mon-sum-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-3);
    align-items: start;
    margin: 0 0 var(--space-4);
}
.mon-sum-grid > .card { margin: 0; }
.mon-sum-grid > .card > .section-title:first-child,
.mon-sum-grid .card-head .section-title { margin: 0 0 var(--space-3); }
.mon-sum-grid .card-head { align-items: baseline; }
@container (min-width: 700px) {
    .mon-sum-grid { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); }
    .mon-sum-grid.is-single { grid-template-columns: minmax(0, 1fr); }
}

/* Facts as a key/value list rather than the tiled .mon-facts grid: it has
   half the width it had. Specific enough to beat .mon-facts' column ladder. */
.mon-facts.is-list { grid-template-columns: minmax(0, 1fr); gap: 7px; }
.mon-facts.is-list .mon-fact {
    display: grid;
    grid-template-columns: 9rem minmax(0, 1fr);
    column-gap: var(--space-3);
}
.mon-facts.is-list .mon-fact dt { margin: 0; font-size: var(--text-md); }

.mon-evlist {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin: 0;
    padding: 0;
    list-style: none;
}
.mon-evlist li { display: flex; gap: 10px; align-items: flex-start; }
.mon-evlist .dot { margin-top: 6px; }
.mon-evlist-body { display: flex; flex-direction: column; gap: 1px; min-width: 0; overflow-wrap: anywhere; }
.mon-evlist-when { color: var(--muted); font-size: var(--text-sm); }

/* --- the drawer ------------------------------------------------------------
   node-drawer.js builds .nd-scrim and aside.nd on <body>, outside the page's
   live-refresh regions. 760px from the right; the whole screen on a phone. */
.nd-scrim {
    position: fixed;
    inset: 0;
    z-index: 69;
    background: rgba(14, 16, 20, .32);
    opacity: 0;
    transition: opacity .16s ease;
}
.nd-scrim.is-open { opacity: 1; }
.nd {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    z-index: 70;
    display: flex;
    flex-direction: column;
    width: min(760px, 100vw);
    background: var(--bg);
    border-left: 1px solid var(--line);
    box-shadow: var(--shadow-3);
    transform: translateX(24px);
    opacity: 0;
    transition: transform .18s ease, opacity .18s ease;
}
.nd.is-open { transform: none; opacity: 1; }
/* display: flex outranks the UA's [hidden] rule; without this a closed
   drawer stays in place, invisible, over the right of the list. */
.nd[hidden] { display: none; }
.nd:focus { outline: none; }
.nd-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 18px 24px 24px;
}
/* A fetch in flight dims what is there rather than blanking it: ↑/↓ through
   a list should not flash an empty panel between every node. */
.nd.is-loading .nd-body { opacity: .55; transition: opacity .1s ease .12s; }
.nd-foot {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: 10px 24px;
    background: var(--panel);
    border-top: 1px solid var(--line);
    color: var(--muted);
    font-size: var(--text-sm);
}
.nd-keys { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.nd-keys kbd {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    padding: 1px 5px;
    border: 1px solid var(--line);
    border-radius: 4px;
    background: var(--raised);
    color: var(--text);
}
.nd-keys kbd + kbd { margin-left: 8px; }
.nd-full { margin-left: auto; white-space: nowrap; }
.nd-msg { color: var(--muted); margin: var(--space-4) 0; }

/* The head: page_head()'s parts at drawer size, with an h2. */
.nd-head { align-items: center; margin-bottom: var(--space-3); }
.nd-head .page-head-main { align-items: center; }
.nd-title {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    margin: 0;
    font-size: 18px;
    font-weight: 600;
    line-height: 1.3;
    overflow-wrap: anywhere;
}
.nd-title .mon-os-mark { margin-right: 2px; }
.nd .tabs { margin-top: var(--space-3); }

/* The row whose node is open, so the list says where you are after ↑/↓. */
.fl-row.is-drawer-current,
.mon-node.is-drawer-current { background: var(--sel-row); }

@media (max-width: 600px) {
    .nd-body { padding: 14px 16px 20px; }
    .nd-foot { padding: 10px 16px; }
    /* No arrow keys on a phone. */
    .nd-keys { display: none; }
}
@media (prefers-reduced-motion: reduce) {
    .nd, .nd-scrim, .nd.is-loading .nd-body { transition: none; }
}

/* Captured report bodies (workbench/license#77 §17). Scrolls in its own box:
 * a 500-line JSON payload must not make the page scroll horizontally, and must
 * not push everything below it off the screen either. */
.mon-capture {
    max-height: 28rem;
    overflow: auto;
    white-space: pre;
    background: var(--bg-sunken, var(--bg-hover));
    border: 1px solid var(--border-soft);
    border-radius: var(--radius);
    padding: var(--space-3);
    font-size: var(--text-sm);
}

/* Live action output (workbench/license#100).
 *
 * A fixed-height scroller rather than a growing block: an upgrade producing two
 * hundred lines must not push the rest of the page out of reach, and a bounded
 * box is what makes "scroll back to the error" a thing you can do.
 *
 * pre-wrap, not pre. Package managers emit long lines -- full URLs, dependency
 * chains -- and a horizontal scrollbar on a live log is how you end up reading
 * the left third of the interesting line.
 */
.mon-console-log {
    max-height: 26rem;
    overflow-y: auto;
    overflow-x: hidden;
    white-space: pre-wrap;
    word-break: break-word;
    font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
    font-size: var(--text-sm);
    line-height: 1.45;
    margin: 0.5rem 0 0;
    padding: 0.75rem;
    background: var(--surface-2, rgba(0, 0, 0, 0.25));
    border: 1px solid var(--border, rgba(255, 255, 255, 0.08));
    border-radius: 6px;
}

/* Which run is on screen (workbench/license#100). Only rendered when there is
   more than one to choose between — a single button offering the thing you are
   already looking at is furniture.

   Shaped like the dashboard's filter chips because it does the same job, and
   sized down: this selects a view within one panel rather than filtering the
   fleet, and matching them exactly would give it more weight than the
   transcript it sits above. */
.mon-console-picker {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin: 0 0 0.5rem;
}
.mon-console-pick {
    font: inherit;
    font-size: var(--text-xs);
    padding: 0.15rem 0.6rem;
    border-radius: 999px;
    border: 1px solid var(--border);
    background: transparent;
    color: var(--fg-muted);
    cursor: pointer;
}
.mon-console-pick:hover { background: var(--bg-hover); }
.mon-console-pick.is-active {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-fg);
}
.mon-console-pick:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* The state line carries a live dot only while something is running, so a
   finished transcript does not keep implying it is still moving. */
.mon-console-state { margin: 0; }

.mon-console.is-live .mon-console-state::before {
    content: '';
    display: inline-block;
    width: 0.5rem;
    height: 0.5rem;
    margin-right: 0.45rem;
    border-radius: 50%;
    background: var(--accent, #4ea1ff);
    animation: mon-console-pulse 1.4s ease-in-out infinite;
}

@keyframes mon-console-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.25; }
}

/* Respect a reader who has asked for less movement -- the dot's job is to say
   "live", and a static dot still says it. */
@media (prefers-reduced-motion: reduce) {
    .mon-console.is-live .mon-console-state::before { animation: none; }
}

/* ===========================================================================
   The fleet table (workbench/license#136 Phase 3)
   The monitoring dashboard as the "Fleet" board draws it: one health band, one
   toolbar, one grouped table. Everything here is on the #136 tokens (--panel,
   --line, the five status levels), so it is right in both themes without a
   dark-mode block of its own.
   ======================================================================== */

/* Page-head buttons carry an icon now; the registry's SVGs have no size. */
.fleet .btn > svg { width: 15px; height: 15px; flex: none; }

/* --- icon buttons ----------------------------------------------------------
   Row actions and the toolbar's view toggle. 30px square, 15px glyph; the
   bordered variant is the 32px one that sits beside .btn in a toolbar. */
.mon-iconbtn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 30px;
    height: 30px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--radius);
    background: transparent;
    color: var(--muted);
    cursor: pointer;
    text-decoration: none;
}
.mon-iconbtn svg { width: 15px; height: 15px; }
.mon-iconbtn:hover { background: var(--hover); color: var(--text); text-decoration: none; }
.mon-iconbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.mon-iconbtn.is-bordered {
    width: 32px;
    height: 32px;
    border-color: var(--line);
    background: var(--panel);
}
.mon-iconbtn.is-bordered svg { width: 16px; height: 16px; }
/* Unavailable, not absent: dimmed, still focusable, and the title says why.
   The help cursor, like a disabled .btn, because hovering IS how you ask. */
.mon-iconbtn[aria-disabled="true"] { opacity: .4; cursor: help; }
.mon-iconbtn[aria-disabled="true"]:hover { background: transparent; color: var(--muted); }

/* --- health band ---------------------------------------------------------- */
.fl-health {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-4) var(--space-5);
    padding: 14px 16px;
    margin-bottom: var(--space-4);
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
}
.fl-census {
    flex: 0 1 420px;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
/* One quantity split five ways, as proportions. SVG, so the widths are
   attributes rather than the inline styles the CSP refuses. */
.fl-health-bar { display: block; width: 100%; height: 10px; border-radius: 5px; overflow: hidden; }
.fl-seg.is-ok       { fill: var(--ok); }
.fl-seg.is-warn     { fill: var(--warn); }
.fl-seg.is-crit     { fill: var(--crit); }
.fl-seg.is-off      { fill: var(--off); }
.fl-seg.is-disabled { fill: var(--track); }

.fl-legend { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 4px 12px; }
.fl-legend-item {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 1px 4px;
    margin: 0 -4px;
    border-radius: var(--radius);
    color: var(--text);
    text-decoration: none;
    white-space: nowrap;
}
.fl-legend-item:hover { background: var(--hover); text-decoration: none; }
.fl-legend-item.is-active { background: var(--accent-soft); }
.fl-legend-n { font-weight: 600; font-variant-numeric: tabular-nums; }
.fl-legend-label { color: var(--muted); }
.fl-legend-item.is-zero .fl-legend-n { color: var(--muted); font-weight: 500; }
/* Disabled's dot is the bar's track colour, ringed so it is still a dot. */
.fl-legend .dot.is-disabled::before { background: var(--track); box-shadow: inset 0 0 0 1px var(--faint); }

.fl-signals {
    flex: 1 1 420px;
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    padding-left: var(--space-5);
    border-left: 1px solid var(--line);
}
.chip.fl-signal {
    height: 30px;
    padding: 0 10px;
    gap: 6px;
    font-size: var(--text-md);
    font-weight: 400;
    text-decoration: none;
}
.chip.fl-signal svg { width: 14px; height: 14px; }
.fl-signal strong { font-weight: 600; }
/* a.chip:hover blanks a chip to the hover grey, which here would take the
   level colour off the one counter being pointed at. Keep the wash; ring it. */
a.chip.fl-signal:hover,
.chip.fl-signal.is-active { box-shadow: inset 0 0 0 1px currentColor; }
a.chip.fl-signal.is-crit:hover { background: var(--crit-soft); color: var(--crit); }
a.chip.fl-signal.is-warn:hover { background: var(--warn-soft); color: var(--warn); }
a.chip.fl-signal.is-info:hover { background: var(--info-soft); color: var(--info); }
a.chip.fl-signal.is-off:hover  { background: var(--off-soft);  color: var(--off); }

/* --- toolbar --------------------------------------------------------------- */
.fl-toolbar {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    margin: 0 0 var(--space-3);
}
/* Both are <label>s, which app.css makes stacked blocks with a top margin. */
.fl-search,
.fl-dd {
    display: inline-flex;
    align-items: center;
    height: 32px;
    margin: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--panel);
    color: var(--muted);
    font-weight: 400;
}
/* Shrinks before anything wraps: the toolbar is one row (#135 §13), and the
   box is the one control whose content is typed rather than chosen. */
.fl-search { gap: 8px; flex: 0 1 280px; min-width: 180px; padding: 0 10px; }
.fl-hostlink:focus-visible { outline-offset: -2px; }
.fl-search-icon { display: flex; }
.fl-search-icon svg { width: 14px; height: 14px; }
.fl-search input {
    flex: 1;
    width: auto;
    min-width: 0;
    min-height: 0;
    height: 30px;
    padding: 0;
    border: 0;
    background: transparent;
}
.fl-search:focus-within,
.fl-dd:focus-within { outline: 2px solid var(--accent); outline-offset: -1px; }
.fl-search input:focus,
.fl-dd select:focus { outline: none; }

.fl-dd { position: relative; gap: 6px; padding-left: 10px; }
.fl-dd-label,
.fl-fold-label { white-space: nowrap; }
.fl-dd select {
    -webkit-appearance: none;
    appearance: none;
    width: auto;
    /* Sized to the CHOSEN option where the browser can (field-sizing), not to
       the longest one: "Nodes with unhealthy containers (1)" would otherwise
       make every Condition box that wide and wrap the toolbar at 1440. */
    field-sizing: content;
    max-width: 13em;
    min-height: 0;
    height: 30px;
    padding: 0 28px 0 0;
    border: 0;
    background: transparent;
    color: var(--text);
    font-weight: 500;
    cursor: pointer;
}
.fl-dd-chev { position: absolute; right: 9px; top: 50%; transform: translateY(-50%); display: flex; pointer-events: none; }
.fl-dd-chev svg { width: 12px; height: 12px; }
/* A dropdown doing something says so from across the room. */
.fl-dd.is-set { border-color: var(--accent); }
.fl-dd.is-set select { color: var(--accent); }

/* app.js hides Apply once it can submit on change. .btn is inline-flex, which
   outranks the UA's [hidden] rule, so say it here. */
.fl-apply[hidden] { display: none; }
.fl-view { display: inline-flex; align-items: center; gap: 8px; margin-left: auto; }
.fl-by-label { color: var(--muted); font-size: var(--text-sm); }
.fl-segs {
    display: inline-flex;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--panel);
    overflow: hidden;
}
.fl-segbtn {
    display: inline-flex;
    align-items: center;
    height: 30px;
    padding: 0 10px;
    color: var(--muted);
    text-decoration: none;
}
.fl-segbtn + .fl-segbtn { border-left: 1px solid var(--line); }
.fl-segbtn:hover { background: var(--hover); color: var(--text); text-decoration: none; }
.fl-segbtn.is-active { background: var(--accent-soft); color: var(--accent); font-weight: 500; }

/* --- the table --------------------------------------------------------------
   CSS grid rows with table roles. The template is ONE custom property so the
   head, the group headers and the rows cannot disagree, and so Phase 4 changes
   one line: the first 36px column is already reserved (empty) for the
   selection checkbox.

   Sized to fit the content column at 1440 without scrolling; below that the
   table scrolls inside .fl-scroll and the page itself never does. */
.fl-scroll {
    /* The containing block for the cells' .sr-only text. Those are absolutely
       positioned, and without a positioned ancestor they escape this box's
       overflow and widen the phone layout viewport to wherever the column is. */
    position: relative;
    overflow-x: auto;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
}
.fl-table {
    /* Every column grows a little on a wide screen, the two text ones most:
       at 2560 a fixed 72px metric beside a 600px name column reads as two
       tables pushed together. */
    --fl-cols: 36px minmax(220px, 2fr) minmax(130px, 1.4fr) repeat(3, minmax(72px, .5fr))
               minmax(104px, .6fr) minmax(120px, .6fr) 76px 80px 138px;
    min-width: 1120px;
    font-size: var(--text-md);
}
.fl-row {
    display: grid;
    grid-template-columns: var(--fl-cols);
    align-items: center;
    min-height: 40px;
    border-bottom: 1px solid var(--line-soft);
}
.fl-row > * { min-width: 0; padding-right: 8px; }
/* The reserved first column: empty until Phase 4 puts the selection checkbox
   in it, already padded the way the mockup seats one. */
.fl-sel { padding-left: 14px; }
.fl-table > .fl-group:last-child > .fl-row:last-child { border-bottom: 0; }
.fl-group > .fl-row:not(.fl-grouphead):not(.fl-folded):hover { background: var(--hover); }

.fl-head {
    min-height: 34px;
    background: var(--raised);
    border-bottom-color: var(--line);
    color: var(--muted);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: .05em;
    text-transform: uppercase;
}
.fl-sort { display: inline-flex; align-items: center; gap: 3px; color: inherit; text-decoration: none; white-space: nowrap; }
.fl-sort:hover, .fl-sort.is-active { color: var(--accent); text-decoration: none; }
.fl-sort-arrow { display: inline-block; min-width: .8em; }
.fl-sort-note { margin-left: 4px; color: var(--muted); font-weight: 500; letter-spacing: 0; text-transform: none; }
a.fl-sort-note { color: var(--accent); }
.fl-th.is-actions { text-align: right; padding-right: 14px; }

.fl-grouphead {
    min-height: 34px;
    background: var(--sunk);
    border-bottom-color: var(--line);
}
.fl-groupcell {
    grid-column: 2 / -1;
    display: flex;
    align-items: center;
    gap: 10px;
    padding-right: 14px;
}
.fl-group-name { display: inline-flex; align-items: center; font-weight: 600; }
.fl-group-count { color: var(--muted); }
.fl-group-more { margin-left: auto; white-space: nowrap; }

/* Node: dot, the name's first label whole, the domain dimmed and the part
   that gives way. The link stays overflow:visible; only the domain clips. */
.fl-host { display: flex; align-items: center; gap: 8px; }
/* A single-label name longer than the column (rare: most names are FQDNs and
   give way at the domain) is cut at the cell edge rather than drawn over the
   OS column. */
.fl-host { overflow: hidden; }
.fl-host .dot,
.fl-host .pill { flex: none; }
.fl-hostlink {
    display: flex;
    /* Grows to fill the cell, and never below the first label: the domain has
       no width of its own, so the link's minimum is the hostname exactly and
       only the domain ever gives way. The pill ends up at the cell's right. */
    flex: 1 1 auto;
    color: var(--text);
    font-weight: 500;
    text-decoration: none;
    white-space: nowrap;
}
.fl-hostname { flex: none; }
.fl-domain { flex: 1 1 auto; width: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--muted); font-weight: 400; }
.fl-hostlink:hover { text-decoration: none; }
.fl-hostlink:hover .fl-hostname { color: var(--accent); text-decoration: underline; }

/* The real OS marks at 16px beside 13px text (Nathan, #136) -- never letters. */
.fl-os { display: flex; align-items: center; color: var(--muted); white-space: nowrap; }
.fl-os .mon-os-icon,
.fl-group-name .mon-os-icon,
.fl-fold-chip .mon-os-icon { flex: none; margin-right: 7px; }
.fl-os .mon-os-icon svg,
.fl-group-name .mon-os-icon svg,
.fl-fold-chip .mon-os-icon svg { width: 16px; height: 16px; display: block; }
.fl-ostext { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

.fl-metric { display: flex; flex-direction: column; gap: 3px; padding-right: 14px; }
.fl-num { font-family: var(--font-mono); font-size: var(--text-sm); }
.fl-num.is-warn { color: var(--warn); }
.fl-num.is-crit { color: var(--crit); }
/* The bar reuses .meter-fill's data-pct width rules from metrics.css. */
.fl-bar { display: block; height: 4px; border-radius: 2px; background: var(--track); overflow: hidden; }
/* 2px ends on every level (#136 Phase 4), the mockup's shape; the full-height
   pill ends metrics.css gives the 8px meters read as blobs at 4px. */
.fl-bar .meter-fill { display: block; border-radius: 2px; }

/* A down device's reason, in place of the metrics, Updates and Signals --
   all five are stale or empty on such a row, and the reason is the one thing
   on it worth reading in full (#135 §7). */
.fl-reason {
    grid-column: span 5;
    padding-right: 14px;
    font-size: var(--text-sm);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.fl-reason.is-crit { color: var(--crit); }
.fl-reason.is-warn { color: var(--warn); }
.fl-reason.is-off  { color: var(--muted); }

.fl-upd { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
.fl-of { color: var(--muted); font-size: var(--text-sm); }
.fl-pend { font-variant-numeric: tabular-nums; }
.fl-current { color: var(--ok); }
.fl-none { color: var(--muted); }

.fl-sigs { display: flex; flex-wrap: wrap; gap: 6px; }
.fl-sig {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 22px;
    height: 22px;
    border-radius: 5px;
}
.fl-sig svg { width: 13px; height: 13px; }
.fl-sig.is-crit { background: var(--crit-soft); color: var(--crit); }
.fl-sig.is-warn { background: var(--warn-soft); color: var(--warn); }
.fl-sig.is-off  { background: var(--off-soft);  color: var(--off); }
.fl-sig.is-info { background: var(--info-soft); color: var(--info); }

.fl-seen { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--muted); white-space: nowrap; }
.fl-uptime { font-family: var(--font-mono); font-size: var(--text-sm); white-space: nowrap; text-align: right; padding-right: 16px; }

.fl-actions { display: flex; justify-content: flex-end; gap: 2px; padding-right: 10px; }
.fl-actform { display: inline-flex; margin: 0; }

/* Switched off on purpose: present, dimmed, never coloured as a problem. */
.fl-row.is-disabled > .fl-host,
.fl-row.is-disabled > .fl-os { opacity: .6; }

/* The healthy groups, folded into one line. */
.fl-folded { min-height: 42px; }
.fl-foldcell {
    grid-column: 2 / -1;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px 8px;
    padding: 6px 14px 6px 0;
    color: var(--muted);
}
.fl-fold-icon { display: flex; }
.fl-fold-icon svg { width: 14px; height: 14px; }
.fl-fold-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    border: 1px solid var(--line);
    border-radius: 12px;
    color: var(--text);
    text-decoration: none;
    white-space: nowrap;
}
.fl-fold-chip:hover { background: var(--hover); text-decoration: none; }
.fl-fold-n { color: var(--muted); }

@media (max-width: 900px) {
    .fl-signals { padding-left: 0; border-left: 0; flex-basis: 100%; }
    .fl-census { flex-basis: 100%; }
}
@media (max-width: 600px) {
    .fl-search { flex-basis: 100%; }
}

/* ===========================================================================
   Bulk selection, the bulk bar and the confirm step (workbench/license#136
   Phase 4). The "12 selected" board: a box per row, per group and for all;
   the bar; the confirm panel. The run page follows in its own section.
   ======================================================================== */

/* The reserved first column's occupant. 15px, the accent when ticked. */
.fl-check {
    width: 15px;
    height: 15px;
    margin: 0;
    vertical-align: middle;
    accent-color: var(--accent);
    cursor: pointer;
}
.fl-sel { display: flex; align-items: center; }
/* A ticked row, as the board draws it: the accent wash, and a bar at the left
   edge so it reads without colour too. */
.fl-group > .fl-row.is-selected,
.fl-group > .fl-row.is-selected:hover { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }

/* The dock: the confirm panel above the bar. Without a script it is a plain
   block under the table and the bar is always there; app.js adds .is-floating,
   which pins it to the foot of the content column and hides it until a box is
   ticked. */
.fl-bulkdock {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
    margin-top: var(--space-3);
}
.fl-bulkdock[hidden],
.fl-bulk-panel[hidden] { display: none; }
.fl-bulkdock.is-floating {
    position: fixed;
    z-index: 40;
    left: var(--sidebar-w, 0px);
    right: 0;
    bottom: 24px;
    align-items: center;
    margin: 0;
    padding: 0 16px;
    pointer-events: none;
}
.fl-bulkdock.is-floating > * { pointer-events: auto; }
/* Opened from the head's "Apply all" with nothing ticked: the panel alone. */
.fl-bulkdock.is-panel-only .fl-bulk { display: none; }
.fl-bulk { margin: 0; max-width: 100%; }
/* Room under the table for the floating bar, so it never sits on the last row. */
.fleet:has(.fl-bulkdock.is-floating:not([hidden])) { padding-bottom: 72px; }

/* The bar: dark on the light theme and light on the dark one -- the inverse
   of the page, so it is unmistakably not part of the table. */
.fl-bulkbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    padding: 6px 6px 6px 14px;
    background: var(--text);
    color: var(--bg);
    border-radius: 10px;
    box-shadow: var(--shadow-3);
}
.fl-bulk-count { font-weight: 600; padding-right: 10px; white-space: nowrap; }
.fl-bulk-sep { width: 1px; height: 20px; margin: 0 4px; background: currentColor; opacity: .25; }
.fl-bulkbtn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 32px;
    padding: 0 10px;
    border: 0;
    border-radius: 6px;
    background: transparent;
    color: inherit;
    font: 500 var(--text-md)/1 var(--font-ui);
    white-space: nowrap;
    cursor: pointer;
}
.fl-bulkbtn svg { width: 15px; height: 15px; flex: none; }
.fl-bulkbtn:hover { background: color-mix(in srgb, currentColor 14%, transparent); }
.fl-bulkbtn:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; }
.fl-bulkbtn.is-clear { padding: 0 8px; }
/* The word "Clear" is for the no-script bar; floating, the x says it. */
.is-floating .fl-bulk-cleartext { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* The confirm step: a panel above the bar, or the body of its own page. */
.fl-confirm {
    display: flex;
    flex-direction: column;
    gap: 14px;
    width: min(600px, 100%);
    box-sizing: border-box;
    padding: 18px 20px;
    background: var(--panel);
    color: var(--text);
    border: 1px solid var(--line);
    border-radius: 10px;
    box-shadow: var(--shadow-3);
}
.fl-confirm-page .fl-confirm { width: auto; max-width: 720px; box-shadow: var(--shadow-1); }
.fl-bulk-panel { width: min(600px, 100%); max-height: calc(100vh - 140px); overflow-y: auto; border-radius: 10px; }
.fl-confirm-head { display: flex; align-items: center; gap: 10px; }
.fl-confirm-head h2 { margin: 0; font-size: 15px; font-weight: 600; }
.fl-confirm-head h2:focus { outline: none; }
.fl-confirm-head .mon-iconbtn { margin-left: auto; }
.fl-confirm-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.fl-confirm-list li { display: flex; align-items: baseline; gap: 10px; }
.fl-confirm-list .chip { flex: none; white-space: nowrap; }
.fl-confirm-names { color: var(--muted); min-width: 0; overflow-wrap: anywhere; }
.fl-confirm-names strong { color: var(--text); font-weight: 500; }
.fl-confirm-warn { margin: 0; color: var(--warn); font-size: var(--text-sm); }
.fl-confirm-form { margin: 0; }
.fl-confirm-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px 16px;
    padding-top: 12px;
    border-top: 1px solid var(--line-soft);
}
.fl-confirm-opt { display: inline-flex; align-items: center; gap: 8px; margin: 0; }
.fl-confirm-opt input[type="checkbox"] { accent-color: var(--accent); }
.fl-confirm-note { color: var(--muted); font-size: var(--text-sm); }
.fl-confirm-grow { flex: 1 1 auto; }
.fl-confirm-actions { display: inline-flex; gap: 10px; margin-left: auto; }
/* The one filled red button (#136 design system): the confirm step of a
   destructive action -- here, rebooting a selection. */
.btn.btn-danger.is-confirm,
.btn.btn-danger.is-confirm:hover { background: var(--danger); border-color: var(--danger); color: #fff; }

@media (max-width: 860px) {
    /* Above the phone's bottom bar (console.css), not under it. */
    .fl-bulkdock.is-floating {
        left: 0;
        bottom: calc(var(--bottomnav-h, 60px) + env(safe-area-inset-bottom, 0px) + 12px);
        padding: 0 12px;
    }
    .fl-bulk-panel { max-height: calc(100vh - var(--bottomnav-h, 60px) - 120px); }
}
@media (max-width: 600px) {
    /* Icons only on a phone, the words kept for screen readers. */
    .fl-bulkbtn > span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
    .fl-bulkbtn { padding: 0 9px; }
    .fl-bulkbar { flex-wrap: nowrap; }
}

/* ===========================================================================
   Runs (workbench/license#136 Phase 4, board "Bulk run")
   One run: progress, the nodes and their state, and the live output of the
   node you pick. The output pane is dark in both themes, as a terminal is.
   ======================================================================== */
.run-page .page-head h1 { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.run-page .page-head h1 .pill { font-size: var(--text-xs); }
.run-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.run-controls .btn > svg { width: 15px; height: 15px; }

.run-progress {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 16px;
    margin-bottom: 16px;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
}
.run-bar { display: block; width: 100%; height: 10px; border-radius: 5px; overflow: hidden; background: var(--track); }
.run-seg.is-ok     { fill: var(--ok); }
.run-seg.is-crit   { fill: var(--crit); }
.run-seg.is-info   { fill: var(--accent); }
.run-seg.is-queued { fill: color-mix(in srgb, var(--faint) 45%, transparent); }
.run-seg.is-off    { fill: var(--off); }
.run-legend { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px 18px; }
.run-legend li { display: inline-flex; align-items: center; gap: 6px; }
.run-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.run-dot.is-ok     { background: var(--ok); }
.run-dot.is-crit   { background: var(--crit); }
.run-dot.is-info   { background: var(--accent); }
.run-dot.is-queued { background: color-mix(in srgb, var(--faint) 45%, transparent); }
.run-dot.is-off    { background: var(--off); }

.run-grid {
    display: grid;
    grid-template-columns: minmax(0, 540px) minmax(0, 1fr);
    gap: 16px;
    align-items: start;
}
.run-side { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
.run-nodes {
    list-style: none;
    margin: 0;
    padding: 0;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    overflow: hidden;
}
.run-nodes li + li { border-top: 1px solid var(--line-soft); }
.run-node {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 14px;
    color: var(--text);
    text-decoration: none;
}
.run-node:hover { background: var(--hover); text-decoration: none; }
.run-node:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.run-node.is-selected { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }
.run-state {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 24px;
    height: 24px;
    border-radius: 50%;
}
.run-state svg { width: 14px; height: 14px; }
.run-node.is-done .run-state      { background: var(--ok-soft);   color: var(--ok); }
.run-node.is-failed .run-state    { background: var(--crit-soft); color: var(--crit); }
.run-node.is-running .run-state   { background: var(--accent-soft); color: var(--accent); }
.run-node.is-queued .run-state,
.run-node.is-cancelled .run-state { background: var(--off-soft);  color: var(--off); }
.run-spin {
    width: 12px;
    height: 12px;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    animation: mon-spin .8s linear infinite;
}
@media (prefers-reduced-motion: reduce) { .run-spin { animation-duration: 2.2s; } }
.run-node-body { display: flex; flex-direction: column; gap: 3px; flex: 1 1 auto; min-width: 0; }
.run-node-name { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.run-node-name strong { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.run-node-name .muted { font-size: var(--text-sm); white-space: nowrap; }
.run-detail { font-size: var(--text-sm); color: var(--muted); overflow-wrap: anywhere; }
.run-node.is-failed .run-detail { color: var(--crit); }
.run-page .mon-action-status.is-inline.run-detail { display: flex; }
.run-nodebar { margin-top: 2px; }
.run-took { flex: none; font-family: var(--font-mono); font-size: var(--text-sm); color: var(--muted); }
.run-skipped { margin: 0; }
.run-skipped ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }

/* The live output of the selected node. Its own dark palette in both themes:
   it is a terminal, and a transcript on a white card reads as a document. */
.run-output {
    display: flex;
    flex-direction: column;
    min-width: 0;
    background: #0e1b26;
    color: #d7e3ea;
    border: 1px solid #0e1b26;
    border-radius: var(--radius-lg);
    overflow: hidden;
}
.run-output-head {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 14px;
    border-bottom: 1px solid #1f3242;
}
.run-output-head strong { color: #fff; font-weight: 600; overflow-wrap: anywhere; }
.run-output-state { color: #8fa6b5; white-space: nowrap; }
.run-output .mon-iconbtn { color: #8fa6b5; }
.run-output .mon-iconbtn:hover { background: rgba(255, 255, 255, .08); color: #fff; }
.run-live-dot { width: 7px; height: 7px; border-radius: 50%; background: #8fa6b5; flex: none; }
.run-live-dot.is-live { background: #56c58f; }
.run-console { padding: 0 14px 14px; }
.run-console .mon-console-state { margin: 10px 0 0; color: #8fa6b5; font-size: var(--text-sm); }
.run-console .mon-console-log {
    max-height: 60vh;
    margin: 8px 0 0;
    padding: 0;
    background: transparent;
    border: 0;
    color: inherit;
    font-size: 12px;
    line-height: 1.6;
}
.run-console.is-live .mon-console-state::before { background: #56c58f; }

@media (max-width: 1100px) {
    .run-grid { grid-template-columns: minmax(0, 1fr); }
}

/* ===========================================================================
   Phones (#136 mobile)
   The Nodes module below 700px, as MApp.dc.html draws it: node cards instead
   of the grid table, the signal counters as big tap targets, the toolbar as a
   search box and a Filter button that opens a bottom sheet, a select mode with
   a bottom action bar, the node page as a screen with a list of sections and a
   sticky action bar, and the run page without a sideways scroll.

   ONE breakpoint, max-width: 699px, the one the app-wide phone work uses.
   Nothing here changes a pixel above it: the markup this section styles is the
   desktop markup (the table's rows become cards by CSS alone), plus a few
   phone-only pieces that the rule just below keeps undrawn on a desktop.

   The pieces that need a script (the sheet, select mode, the ⋯ in the bulk
   bar) are rendered hidden and switched on by nodes-mobile.js. Without it a
   phone gets the same GET form stacked, the cards with their tick circles
   showing, and the bulk bar as a block under the list: every feature works,
   it is just less of an app.
   ======================================================================== */

/* Never drawn above the phone breakpoint. */
.fl-selbtn,
.fl-filterbtn,
.fl-chips,
.fl-sheet,
.fl-sheet-scrim,
.fl-selbar,
.fl-bulk-more,
.fl-os-seen,
.fl-mlabel,
.fl-sig-tag,
.tab-ico,
.nd-sechead,
.nd-actbar { display: none; }

@media (max-width: 699px) {
    /* --- health: the census, then the counters two-up --------------------- */
    .fl-health { gap: 12px; padding: 14px; border-radius: 14px; }
    .fl-census { flex-basis: 100%; gap: 10px; }
    .fl-legend { gap: 6px 14px; }
    .fl-legend-item { min-height: 28px; }
    .fl-signals {
        flex-basis: 100%;
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 8px;
        padding-left: 0;
        border-left: 0;
    }
    /* Number over label, icon at the left: a 56px target per counter. */
    .chip.fl-signal {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr);
        align-content: center;
        align-items: center;
        column-gap: 10px;
        height: auto;
        min-height: 56px;
        padding: 8px 12px;
        border-radius: 12px;
        white-space: normal;
    }
    .chip.fl-signal svg { grid-row: 1 / 3; width: 18px; height: 18px; }
    .chip.fl-signal > strong { font-size: 17px; line-height: 1.2; }
    .chip.fl-signal > span { font-size: 12px; line-height: 1.25; }

    /* --- the toolbar ---------------------------------------------------------
       Without a script: every control full width, stacked, 44px, 16px text so
       iOS does not zoom into it. With one (.is-sheeted): the search box and
       the Filter button, and the rest is in the sheet. */
    .fl-toolbar { gap: 8px; margin-bottom: 12px; }
    .fl-search,
    .fl-dd { height: 44px; border-radius: 12px; }
    .fl-search { flex: 1 1 100%; min-width: 0; padding: 0 12px; }
    .fl-search-icon svg { width: 18px; height: 18px; }
    .fl-search input { height: 42px; font-size: 16px; }
    .fl-dd { flex: 1 1 100%; padding-left: 12px; }
    .fl-dd select { flex: 1 1 auto; max-width: none; height: 42px; font-size: 16px; }
    .fl-toolbar > .btn { flex: 1 1 auto; justify-content: center; height: 44px; }
    .fl-view { flex-basis: 100%; margin-left: 0; }
    .fl-segbtn { height: 42px; padding: 0 14px; }
    .fl-view .mon-iconbtn.is-bordered { width: 44px; height: 44px; }

    .fl-toolbar.is-sheeted .fl-search { flex: 1 1 0; }
    .fl-toolbar.is-sheeted > .fl-dd,
    .fl-toolbar.is-sheeted > .btn,
    .fl-toolbar.is-sheeted > .fl-view { display: none; }

    .fl-filterbtn:not([hidden]) {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        flex: none;
        height: 44px;
        padding: 0 14px;
        border: 1px solid var(--line);
        border-radius: 12px;
        background: var(--panel);
        color: var(--text);
        font: 500 15px/1 var(--font-ui);
        cursor: pointer;
    }
    .fl-filterbtn svg { width: 18px; height: 18px; }
    .fl-filterbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
    .fl-filterbtn-n {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: 20px;
        height: 20px;
        padding: 0 5px;
        box-sizing: border-box;
        border-radius: 10px;
        background: var(--accent);
        color: var(--accent-text);
        font-size: 12px;
        font-weight: 600;
    }

    /* The active filters, each a link to the view without it. */
    .fl-chips {
        display: flex;
        flex-wrap: wrap;
        flex-basis: 100%;
        gap: 8px;
        margin: 0;
        padding: 0;
        list-style: none;
    }
    .fl-chipx {
        display: inline-flex;
        align-items: center;
        gap: 4px;
        min-height: 36px;
        padding: 0 8px 0 12px;
        border-radius: 18px;
        background: var(--accent-soft);
        color: var(--accent);
        font-size: 14px;
        font-weight: 500;
        text-decoration: none;
    }
    .fl-chipx:hover { text-decoration: none; box-shadow: inset 0 0 0 1px currentColor; }
    .fl-chipx svg { width: 16px; height: 16px; flex: none; }

    /* --- the filter sheet ------------------------------------------------- */
    .fl-sheet-scrim:not([hidden]) {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 80;
        background: rgba(15, 17, 21, .35);
    }
    .fl-sheet:not([hidden]) {
        display: flex;
        flex-direction: column;
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 81;
        max-height: 88vh;
        max-height: 88dvh;
        background: var(--panel);
        color: var(--text);
        border-radius: 18px 18px 0 0;
        box-shadow: 0 -8px 30px rgba(0, 0, 0, .2);
    }
    .fl-sheet:focus { outline: none; }
    html.fl-sheet-open,
    html.fl-sheet-open body { overflow: hidden; }
    .fl-sheet-grip { align-self: center; width: 40px; height: 5px; margin: 8px 0 2px; border-radius: 3px; background: var(--line); }
    .fl-sheet-head { display: flex; align-items: center; padding: 4px 8px 8px 16px; }
    .fl-sheet-head h2 { flex: 1 1 auto; margin: 0; font-size: 18px; font-weight: 600; }
    .fl-sheet-clear,
    .fl-selbar-btn {
        height: 44px;
        padding: 0 10px;
        border: 0;
        background: transparent;
        color: var(--accent);
        font: 600 16px/1 var(--font-ui);
        cursor: pointer;
    }
    .fl-sheet-body {
        flex: 1 1 auto;
        min-height: 0;
        padding: 0 16px 12px;
        overflow-y: auto;
        overscroll-behavior: contain;
    }
    .fl-sheet-fields {
        display: flex;
        flex-direction: column;
        gap: 18px;
        min-width: 0;
        margin: 0;
        padding: 0;
        border: 0;
    }
    .fl-sheet-sec { display: flex; flex-direction: column; gap: 4px; margin: 0; font-weight: 400; }
    .fl-sheet-label {
        margin-bottom: 4px;
        color: var(--muted);
        font-size: 12px;
        font-weight: 600;
        letter-spacing: .06em;
        text-transform: uppercase;
    }
    .fl-pills { display: flex; flex-wrap: wrap; gap: 8px; }
    /* Real radios, drawn as the board's pills and rows: the input stays in
       the form (and in the tab order), just not painted. */
    .fl-pillopt,
    .fl-radrow { position: relative; margin: 0; cursor: pointer; }
    .fl-pillopt input,
    .fl-radrow input {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: 0;
        opacity: 0;
        pointer-events: none;
    }
    .fl-pillopt > span {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        height: 40px;
        padding: 0 14px;
        border: 1px solid var(--line);
        border-radius: 20px;
        color: var(--text);
        font-size: 15px;
    }
    .fl-pillopt input:checked + span { background: var(--accent); border-color: var(--accent); color: var(--accent-text); font-weight: 600; }
    .fl-pillopt input:focus-visible + span,
    .fl-radrow input:focus-visible ~ .fl-radio { outline: 2px solid var(--accent); outline-offset: 2px; }
    .fl-opt-n { opacity: .7; font-variant-numeric: tabular-nums; }
    .fl-radrow {
        display: flex;
        align-items: center;
        gap: 12px;
        min-height: 50px;
        padding: 0 4px;
        border-bottom: 1px solid var(--line-soft);
        color: var(--text);
        font-size: 16px;
        font-weight: 400;
    }
    .fl-radrow-ico { display: inline-flex; justify-content: center; flex: none; width: 20px; color: var(--muted); }
    .fl-radrow-ico svg,
    .fl-radrow-ico .mon-os-icon svg { display: block; width: 20px; height: 20px; }
    .fl-radrow-ico.is-crit { color: var(--crit); }
    .fl-radrow-ico.is-warn { color: var(--warn); }
    .fl-radrow-ico.is-info { color: var(--accent); }
    .fl-radrow-text { flex: 1 1 auto; min-width: 0; }
    .fl-radrow .fl-opt-n { color: var(--muted); opacity: 1; }
    .fl-radio { flex: none; width: 22px; height: 22px; box-sizing: border-box; border: 2px solid var(--faint); border-radius: 50%; }
    .fl-radrow input:checked ~ .fl-radio { border: 7px solid var(--accent); }
    .fl-sheet-select select { height: 50px; font-size: 16px; border-radius: 12px; }
    .fl-sheet-foot { padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px)); border-top: 1px solid var(--line); }
    .fl-sheet-go { width: 100%; height: 50px; justify-content: center; border-radius: 12px; font-size: 16px; }

    /* --- the table, as cards -----------------------------------------------
       The same rows. Each becomes a grid of its own: name (and the pending
       pill) over OS · seen, over CPU / Mem / Disk, over the signal tags. The
       first 17px column is the gutter the status dot sits in, so every line
       under the name starts where the name does. */
    .fl-scroll { overflow: visible; background: transparent; border: 0; border-radius: 0; }
    .fl-table { display: flex; flex-direction: column; gap: 16px; min-width: 0; font-size: 15px; }
    .fl-head { display: none; }
    .fl-group { display: flex; flex-direction: column; gap: 8px; }

    .fl-grouphead { display: flex; min-height: 0; padding: 4px 2px 0; background: transparent; border: 0; }
    .fl-grouphead > .fl-sel { display: none; }
    .fl-groupcell { flex-wrap: wrap; gap: 4px 8px; padding: 0; }
    .fl-group-name { font-size: 16px; }
    .fl-group-count { font-size: 14px; }
    .fl-group-more { display: inline-flex; align-items: center; min-height: 36px; }

    .fl-group > .fl-row:not(.fl-grouphead):not(.fl-folded) {
        position: relative;
        grid-template-columns: 17px repeat(3, minmax(0, 1fr));
        align-items: start;
        gap: 6px 10px;
        min-height: 0;
        padding: 14px 36px 14px 14px;
        background: var(--panel);
        border: 1px solid var(--line);
        border-radius: 14px;
    }
    .fl-group > .fl-row:not(.fl-grouphead):not(.fl-folded):hover { background: var(--panel); }
    .fl-group > .fl-row:not(.fl-grouphead):not(.fl-folded) > * { padding-right: 0; }
    /* The chevron: the card opens the node. */
    .fl-group > .fl-row:not(.fl-grouphead):not(.fl-folded)::after {
        content: "";
        position: absolute;
        top: 50%;
        right: 16px;
        width: 8px;
        height: 8px;
        margin-top: -4px;
        border-right: 2px solid var(--faint);
        border-bottom: 2px solid var(--faint);
        transform: rotate(-45deg);
    }

    /* The whole card is the name's link: the link's own box stretched over
       the card, so a tap anywhere opens the node with no script involved. */
    .fl-host { grid-row: 1; grid-column: 1 / -1; align-items: flex-start; overflow: visible; }
    .fl-row:has(> .fl-upd > :is(.pill, .fl-pend, .mon-action-status)) > .fl-host { grid-column: 1 / 4; }
    .fl-host .dot { margin-top: 6px; }
    .fl-host .dot::before { width: 9px; height: 9px; }
    .fl-hostlink {
        display: block;
        flex: 1 1 auto;
        min-width: 0;
        font-size: 16px;
        font-weight: 600;
        line-height: 1.25;
        white-space: normal;
        overflow-wrap: anywhere;
    }
    .fl-hostlink::after { content: ""; position: absolute; inset: 0; border-radius: 14px; }
    .fl-hostlink:focus-visible { outline: none; }
    .fl-hostlink:focus-visible::after { outline: 2px solid var(--accent); outline-offset: -2px; }
    .fl-hostlink:hover .fl-hostname { color: inherit; text-decoration: none; }
    .fl-domain { display: inline; width: auto; overflow: visible; color: var(--muted); font-weight: 400; }
    /* A break opportunity before the domain, so a name that does not fit
       wraps as "web-prod-syd-03 / .infinite.net.au" rather than at a hyphen
       inside the part that tells nodes apart. */
    .fl-domain::before { content: "\200B"; }
    .fl-host .pill { margin-top: 1px; }

    .fl-upd { grid-row: 1; grid-column: 4; justify-self: end; min-width: 0; max-width: 100%; }
    /* "5 pending" as the board's muted pill; "▲ 8 of 13" keeps its own. */
    .fl-upd:has(> .fl-pend) {
        gap: 3px;
        padding: 2px 9px;
        border-radius: 11px;
        background: var(--off-soft);
        color: var(--off);
        font-size: 12px;
        font-weight: 600;
    }
    .fl-upd:has(> .fl-pend) .fl-of { color: inherit; font-size: 12px; }
    .fl-row:not(:has(> .fl-upd > :is(.pill, .fl-pend, .mon-action-status))) > .fl-upd { display: none; }
    /* Something running says what, in full, on a line of its own at the
       foot of the card: in the pill's corner it was "queued · …". */
    .fl-row:has(> .fl-upd > .mon-action-status) > .fl-host { grid-column: 1 / -1; }
    .fl-row:has(> .fl-upd > .mon-action-status) > .fl-upd { grid-row: 5; grid-column: 2 / -1; justify-self: start; }
    .fl-upd .mon-action-status { max-width: 100%; }
    .fl-upd .mon-action-label { overflow: hidden; text-overflow: ellipsis; }

    .fl-os { grid-row: 2; grid-column: 2 / -1; min-width: 0; font-size: 13px; }
    .fl-os .mon-os-icon { margin-right: 6px; }
    .fl-os .mon-os-icon svg { width: 15px; height: 15px; }
    .fl-ostext { flex: 0 1 auto; }
    .fl-os-seen { display: inline; flex: none; white-space: pre; }

    .fl-os + .fl-metric { grid-column: 2; }
    .fl-os + .fl-metric + .fl-metric { grid-column: 3; }
    .fl-os + .fl-metric + .fl-metric + .fl-metric { grid-column: 4; }
    .fl-metric {
        grid-row: 3;
        display: grid;
        grid-template-columns: auto minmax(0, 1fr);
        align-items: baseline;
        column-gap: 4px;
        row-gap: 3px;
    }
    .fl-mlabel { display: inline; color: var(--muted); font-size: 12px; }
    .fl-metric .fl-num { font-family: var(--font-ui); font-size: 12px; font-weight: 600; }
    .fl-metric .fl-none { font-size: 12px; }
    .fl-metric .fl-bar { grid-column: 1 / -1; }
    .fl-reason {
        grid-row: 3;
        grid-column: 2 / -1;
        font-size: 13px;
        white-space: normal;
        overflow: visible;
        overflow-wrap: anywhere;
    }
    .fl-sigs { grid-row: 4; grid-column: 2 / -1; gap: 6px; }
    .fl-sigs:empty { display: none; }
    .fl-sig { width: auto; height: 26px; gap: 5px; padding: 0 9px; border-radius: 13px; font-size: 12px; font-weight: 600; }
    .fl-sig-tag { display: inline; }
    .fl-seen,
    .fl-uptime,
    .fl-actions { display: none; }

    /* The healthy groups' line, as a sentence of links. */
    .fl-folded { display: block; min-height: 0; background: transparent; border: 0; }
    .fl-folded > .fl-sel { display: none; }
    .fl-foldcell { padding: 0 2px; font-size: 13px; }
    .fl-fold-chip { min-height: 36px; padding: 0 12px; border-radius: 18px; font-size: 14px; }

    /* --- selection ---------------------------------------------------------
       A tick circle at the card's left: always without a script (the box is
       then the only way to select), and only in select mode with one. The
       box itself is the 44px target; the circle is painted inside it. */
    .fleet.is-mjs:not(.is-selecting) .fl-group > .fl-row > .fl-sel { display: none; }
    .fl-group > .fl-row:not(.fl-grouphead):not(.fl-folded) > .fl-sel {
        position: absolute;
        top: 50%;
        left: 4px;
        z-index: 2;
        padding: 0;
        transform: translateY(-50%);
    }
    .fleet:not(.is-mjs) .fl-group > .fl-row:not(.fl-grouphead):not(.fl-folded),
    .fleet.is-selecting .fl-group > .fl-row:not(.fl-grouphead):not(.fl-folded) { padding-left: 52px; }
    .fleet.is-selecting .fl-group > .fl-row:not(.fl-grouphead):not(.fl-folded) { padding-right: 14px; cursor: pointer; }
    .fleet.is-selecting .fl-group > .fl-row:not(.fl-grouphead):not(.fl-folded)::after { content: none; }
    .fl-row .fl-check {
        -webkit-appearance: none;
        appearance: none;
        width: 44px;
        height: 44px;
        margin: 0;
        border: 0;
        border-radius: 50%;
        background: radial-gradient(circle closest-side, transparent 9.5px, var(--faint) 10px, var(--faint) 11.5px, transparent 12px);
    }
    .fl-row .fl-check:checked {
        background:
            url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / 16px 16px no-repeat,
            radial-gradient(circle closest-side, var(--accent) 11.5px, transparent 12px);
    }
    .fl-row .fl-check:focus-visible { outline: 2px solid var(--accent); outline-offset: -8px; }
    .fl-group > .fl-row.is-selected:not(.fl-grouphead),
    .fl-group > .fl-row.is-selected:not(.fl-grouphead):hover { background: var(--sel-row); box-shadow: inset 0 0 0 2px var(--accent); }

    .fleet .fl-selbtn:not([hidden]) { display: inline-flex; min-height: 44px; font-size: 16px; font-weight: 600; }

    /* "N selected · Cancel · All", over the top of the screen while selecting. */
    .fl-selbar:not([hidden]) {
        display: flex;
        align-items: center;
        gap: 4px;
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        z-index: 58;
        height: calc(56px + env(safe-area-inset-top, 0px));
        padding: env(safe-area-inset-top, 0px) 8px 0;
        box-sizing: border-box;
        background: var(--panel);
        border-bottom: 1px solid var(--line);
    }
    .fl-selbar-count { flex: 1 1 auto; text-align: center; font-size: 16px; font-weight: 600; }

    /* --- the bulk bar ------------------------------------------------------
       Without a script: a block under the list, every verb a labelled 44px
       button. In select mode: the bottom action bar -- Security updates wide,
       Reboot and ⋯ square, the rest behind ⋯ -- over the phone's own bottom
       bar and clear of the home indicator. */
    .fl-bulkbtn > span { position: static; width: auto; height: auto; overflow: visible; clip: auto; white-space: nowrap; }
    .fl-bulkdock:not(.is-floating) { align-items: stretch; }
    .fl-bulkdock:not(.is-floating) .fl-bulkbar {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 8px;
        padding: 12px;
        border-radius: 14px;
    }
    .fl-bulkdock:not(.is-floating) .fl-bulk-count { grid-column: 1 / -1; padding: 0; white-space: normal; }
    .fl-bulkdock:not(.is-floating) .fl-bulk-sep { display: none; }
    .fl-bulkdock:not(.is-floating) .fl-bulkbtn { justify-content: center; height: 44px; border: 1px solid color-mix(in srgb, currentColor 30%, transparent); }

    /* Over the select bar too, so the confirm sheet it holds covers it. */
    .fl-bulkdock.is-floating { z-index: 59; }
    .fl-selecting .fl-bulkdock.is-floating { left: 0; right: 0; bottom: 0; padding: 0; align-items: stretch; }
    .fl-selecting .fl-bulk { width: 100%; }
    .fl-selecting .fl-bulkbar {
        flex-wrap: wrap;
        gap: 8px;
        padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px));
        background: var(--panel);
        color: var(--text);
        border-top: 1px solid var(--line);
        border-radius: 0;
        box-shadow: 0 -4px 16px rgba(0, 0, 0, .08);
    }
    .fl-selecting .fl-bulk-count,
    .fl-selecting .fl-bulk-sep,
    .fl-selecting .fl-bulkbtn.is-clear,
    .fl-selecting .fl-bulkbtn[value="all_updates"],
    .fl-selecting .fl-bulkbtn[value="check_updates"] { display: none; }
    .fl-selecting .fl-bulkbtn {
        justify-content: center;
        width: 48px;
        height: 48px;
        padding: 0;
        border: 1px solid var(--line);
        border-radius: 12px;
        background: var(--panel);
    }
    .fl-selecting .fl-bulkbtn svg { width: 22px; height: 22px; }
    .fl-selecting .fl-bulkbtn > span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
    .fl-selecting .fl-bulkbtn[value="security_updates"] {
        flex: 1 1 0;
        gap: 8px;
        width: auto;
        background: var(--accent);
        border-color: var(--accent);
        color: var(--accent-text);
        font-size: 15px;
        font-weight: 600;
    }
    .fl-selecting .fl-bulkbtn[value="security_updates"] svg { width: 18px; height: 18px; }
    .fl-selecting .fl-bulkbtn[value="security_updates"] > span { position: static; width: auto; height: auto; overflow: visible; clip: auto; }
    .fl-selecting .fl-bulkbar.is-empty .fl-bulkbtn[value="security_updates"] { background: var(--faint); border-color: var(--faint); }
    .fl-selecting .fl-bulk-more:not([hidden]) { display: inline-flex; }
    /* ⋯ opens the other verbs as rows above the buttons. */
    .fl-selecting .fl-bulkbar.is-more .fl-bulkbtn:is([value="all_updates"], [value="check_updates"]) {
        display: flex;
        order: -1;
        flex: 1 1 100%;
        justify-content: flex-start;
        gap: 12px;
        width: auto;
        padding: 0 14px;
        font-size: 16px;
    }
    .fl-selecting .fl-bulkbar.is-more .fl-bulkbtn:is([value="all_updates"], [value="check_updates"]) > span { position: static; width: auto; height: auto; overflow: visible; clip: auto; }
    .fl-selecting .fl-bulkbar.is-more .fl-bulk-more { background: var(--accent-soft); color: var(--accent); }
    .fl-selecting .fleet { padding-bottom: 88px; }

    /* --- the confirm step: a full-height sheet ----------------------------- */
    .fl-bulk-panel:not([hidden]) {
        position: fixed;
        inset: 0;
        z-index: 90;
        width: auto;
        max-height: none;
        border-radius: 0;
        background: var(--panel);
        overflow-y: auto;
        overscroll-behavior: contain;
    }
    .fl-bulk-panel .fl-confirm {
        width: 100%;
        min-height: 100%;
        padding: calc(12px + env(safe-area-inset-top, 0px)) 16px 0;
        border: 0;
        border-radius: 0;
        box-shadow: none;
    }
    .fl-bulk-panel .fl-confirm-form {
        position: sticky;
        bottom: 0;
        margin-top: auto;
        background: var(--panel);
    }
    .fl-confirm { gap: 16px; }
    .fl-confirm-head { align-items: flex-start; }
    .fl-confirm-head h2 { font-size: 20px; line-height: 1.25; }
    .fl-confirm-head .mon-iconbtn { width: 44px; height: 44px; margin: -8px -8px 0 0; }
    .fl-confirm-head .mon-iconbtn svg { width: 20px; height: 20px; }
    .fl-confirm-list { gap: 14px; }
    .fl-confirm-list li { flex-direction: column; align-items: flex-start; gap: 6px; }
    .fl-confirm-names { font-size: 15px; line-height: 1.45; }
    .fl-confirm-warn { font-size: 14px; }
    .fl-confirm-foot {
        flex-direction: column;
        align-items: stretch;
        gap: 10px;
        padding: 12px 0 calc(12px + env(safe-area-inset-bottom, 0px));
    }
    .fl-confirm-opt { min-height: 44px; font-size: 15px; }
    .fl-confirm-opt input[type="checkbox"] { width: 22px; height: 22px; }
    .fl-confirm-opt select { height: 44px; font-size: 16px; }
    .fl-confirm-actions { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 10px; margin-left: 0; }
    .fl-confirm-actions .btn { justify-content: center; height: 48px; font-size: 16px; }

    /* --- the node page: a screen, and each tab a screen --------------------
       Summary is the node's screen: the head as a card, what is wrong, the
       gauges two by two, then the tabs as a list of section rows, then the
       rest of Summary. The reorder is CSS (the Summary panel's parts join the
       view's own column), so the markup stays the one the drawer uses. Any
       other tab is a screen of its own with a back link. */
    .mon-node-view.is-page { display: flex; flex-direction: column; padding-bottom: 20px; }
    .mon-node-view.is-page > .page-head {
        flex-direction: column;
        align-items: stretch;
        gap: 10px;
        margin-bottom: 12px;
        padding: 14px;
        background: var(--panel);
        border: 1px solid var(--line);
        border-radius: 14px;
    }
    .mon-node-view.is-page > .page-head .page-head-main { flex-direction: column; align-items: stretch; gap: 6px; }
    .mon-node-view.is-page > .page-head h1 {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr) auto;
        align-items: start;
        gap: 4px 12px;
        margin: 0;
        font-size: 22px;
        line-height: 1.2;
        /* The name is the h1's bare text (an anonymous grid item): wrapped in
           a span of its own it lays out 1/64px differently on a desktop. */
        overflow-wrap: anywhere;
    }
    .mon-node-view.is-page > .page-head h1 .mon-os-mark { margin: 0; }
    .mon-node-view.is-page > .page-head h1 .mon-os-mark svg { display: block; width: 40px; height: 40px; }
    .mon-node-view.is-page > .page-head h1 .pill { margin-top: 4px; }
    .mon-node-view.is-page > .page-head .page-head-meta { font-size: 13px; }
    .mon-node-view.is-page > .page-head .page-head-actions { margin-left: 0; }

    .mon-node-view.is-page .mon-attn { margin-bottom: 12px; }
    .mon-node-view.is-page .mon-attn .chip { height: auto; min-height: 36px; padding: 4px 12px; border-radius: 18px; font-size: 14px; white-space: normal; }
    .mon-node-view.is-page .mon-attn .chip svg { width: 15px; height: 15px; }

    .mon-node-view.is-tab-summary > .tab-panel { display: contents; }
    .mon-node-view.is-tab-summary > .tab-panel > * { order: 3; }
    .mon-node-view.is-tab-summary > .tab-panel > .section-title:first-child,
    .mon-node-view.is-tab-summary > .tab-panel > .mon-gauges { order: 1; }
    .mon-node-view.is-tab-summary > .tabs { order: 2; }
    .mon-node-view.is-page .mon-gauges { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
    .mon-node-view.is-page .mon-gauge { padding: 12px; border-radius: 14px; }
    .mon-node-view.is-page .mon-gauge-val { font-size: 24px; }

    /* The tab strip as the section list. */
    .mon-node-view.is-page > .tabs {
        margin: 0 0 16px;
        overflow: hidden;
        background: var(--panel);
        border: 1px solid var(--line);
        border-radius: 14px;
    }
    .mon-node-view.is-page .tab-list { flex-direction: column; gap: 0; min-width: 0; }
    .mon-node-view.is-page .tab-list > li { border-bottom: 1px solid var(--line-soft); }
    .mon-node-view.is-page .tab-list > li:last-child { border-bottom: 0; }
    .mon-node-view.is-page .tab-list > li.is-current-item { display: none; }
    .mon-node-view.is-page .tab {
        gap: 12px;
        height: auto;
        min-height: 52px;
        padding: 0 14px;
        border-bottom: 0;
        color: var(--text);
        font-size: 16px;
        font-weight: 400;
        white-space: normal;
    }
    .mon-node-view.is-page .tab:hover { background: var(--hover); }
    .mon-node-view.is-page .tab-ico { display: inline-flex; flex: none; color: var(--muted); }
    .mon-node-view.is-page .tab-ico svg { width: 20px; height: 20px; }
    .mon-node-view.is-page .tab-count {
        margin-left: auto;
        padding: 2px 9px;
        border-radius: 11px;
        background: var(--off-soft);
        color: var(--muted);
        font-size: 12px;
        font-weight: 600;
    }
    .mon-node-view.is-page .tab::after {
        content: "";
        flex: none;
        width: 8px;
        height: 8px;
        margin: 0 4px 0 6px;
        border-right: 2px solid var(--faint);
        border-bottom: 2px solid var(--faint);
        transform: rotate(-45deg);
    }
    .mon-node-view.is-page .tab:not(:has(.tab-count))::after { margin-left: auto; }

    /* A section's own screen. */
    .mon-node-view.is-page:not(.is-tab-summary) > .page-head,
    .mon-node-view.is-page:not(.is-tab-summary) > .mon-attn,
    .mon-node-view.is-page:not(.is-tab-summary) > .tabs { display: none; }
    .nd-sechead { display: flex; flex-direction: column; gap: 2px; margin: -6px 0 12px; }
    .nd-back {
        display: inline-flex;
        align-items: center;
        align-self: flex-start;
        gap: 2px;
        min-height: 44px;
        margin-left: -6px;
        color: var(--accent);
        font-size: 16px;
        text-decoration: none;
        overflow-wrap: anywhere;
    }
    .nd-back svg { flex: none; width: 24px; height: 24px; }
    .nd-sectitle { margin: 0; font-size: 24px; font-weight: 600; line-height: 1.2; }
    .nd-sectitle-n { display: block; margin-top: 2px; color: var(--muted); font-size: 13px; font-weight: 400; }

    /* Pending updates: the actions stacked, full width. */
    .mon-node-view.is-page .mon-action-buttons { display: grid; gap: 8px; }
    .mon-node-view.is-page .mon-action-buttons form { display: block; margin: 0; }
    .mon-node-view.is-page .mon-action-buttons .btn { justify-content: center; width: 100%; min-height: 44px; font-size: 15px; }
    .mon-node-view.is-page .mon-action-then { flex-direction: row; align-items: center; margin: 0; font-size: 15px; }
    .mon-node-view.is-page .mon-action-then select { flex: 1; height: 44px; font-size: 16px; }

    /* Pending updates: the package table as MApp's list -- the name and its
       tags on one line (Ignore at the right, for an admin), "installed →
       available" under it. Cells are reordered, not moved: the row's ::after
       is the line break between the two. */
    .mon-node-view .tab-panel .card:has(> .mon-pkgs) { overflow-x: visible; }
    table.mon-pkgs,
    table.mon-pkgs tbody { display: block; }
    table.mon-pkgs thead { display: none; }
    table.mon-pkgs tr {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 4px 8px;
        min-height: 60px;
        padding: 8px 0;
        border-bottom: 1px solid var(--line-soft);
    }
    table.mon-pkgs tr:last-child { border-bottom: 0; }
    table.mon-pkgs tr::after { content: ""; order: 4; flex-basis: 100%; height: 0; }
    table.mon-pkgs td { min-width: 0; padding: 0; border: 0; white-space: normal; }
    table.mon-pkgs td:nth-child(1) { order: 1; font-family: var(--font-mono); font-size: 14px; font-weight: 500; overflow-wrap: anywhere; }
    table.mon-pkgs td:nth-child(4) { order: 2; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 6px; font-size: 12px; }
    table.mon-pkgs td:nth-child(4):empty { display: none; }
    table.mon-pkgs td:nth-child(5) { position: relative; order: 3; margin-left: auto; }
    table.mon-pkgs td:nth-child(2) { order: 5; }
    table.mon-pkgs td:nth-child(3) { order: 6; flex: 1 1 0; }
    table.mon-pkgs td:nth-child(2),
    table.mon-pkgs td:nth-child(3) { color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
    table.mon-pkgs td:nth-child(2)::after { content: " →"; }
    /* Ignore asks why (the field is required). The field stays out of the way
       until it is used -- tapping Ignore with it empty focuses it, and the
       form then takes a line of its own. */
    table.mon-pkgs td:nth-child(5) form { display: flex; align-items: center; gap: 8px; margin: 0; }
    table.mon-pkgs td:nth-child(5) form:not(:focus-within) input[type="text"] {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        border: 0;
        opacity: 0;
    }
    table.mon-pkgs td:nth-child(5):focus-within { order: 7; flex-basis: 100%; margin-left: 0; }
    table.mon-pkgs td:nth-child(5) input[type="text"] { flex: 1 1 auto; min-width: 0; height: 44px; font-size: 16px; }
    table.mon-pkgs td:nth-child(5) .btn { flex: none; min-height: 36px; }

    /* The action bar, pinned over the phone's own bottom bar. */
    .nd-actbar {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 8px;
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 55;
        padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px));
        background: var(--panel);
        border-top: 1px solid var(--line);
        box-shadow: 0 -4px 16px rgba(0, 0, 0, .06);
    }
    .nd-actform { display: flex; margin: 0; }
    .nd-actbar > .nd-actform:first-child,
    .nd-actbar > a.nd-actbar-main { flex: 1 1 0; min-width: 0; }
    .nd-actbar-main {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: 8px;
        width: 100%;
        min-width: 0;
        height: 48px;
        padding: 0 12px;
        border: 0;
        border-radius: 12px;
        background: var(--accent);
        color: var(--accent-text);
        font: 600 15px/1.2 var(--font-ui);
        text-decoration: none;
        cursor: pointer;
    }
    .nd-actbar-main:hover { color: var(--accent-text); text-decoration: none; }
    .nd-actbar-main svg { flex: none; width: 18px; height: 18px; }
    .nd-actbar-main > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .nd-actbar-main:disabled { background: var(--track); color: var(--muted); cursor: not-allowed; }
    .nd-actbar-icon {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        flex: none;
        width: 48px;
        height: 48px;
        padding: 0;
        border: 1px solid var(--line);
        border-radius: 12px;
        background: var(--panel);
        color: var(--text);
        cursor: pointer;
    }
    .nd-actbar-icon svg { width: 22px; height: 22px; }
    .nd-actbar-icon[aria-disabled="true"] { opacity: .4; cursor: not-allowed; }
    .nd-actbar-main:focus-visible,
    .nd-actbar-icon:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
    .nd-actbar-why { flex-basis: 100%; order: -1; margin: 0; color: var(--muted); font-size: 13px; }

    /* --- runs -------------------------------------------------------------- */
    .run-page .page-head h1 { font-size: 20px; line-height: 1.25; }
    .run-controls .btn { min-height: 44px; }
    .run-controls .mon-iconbtn.is-bordered { width: 44px; height: 44px; }
    .run-progress { border-radius: 14px; }
    .run-nodes { display: flex; flex-direction: column; gap: 8px; overflow: visible; background: transparent; border: 0; border-radius: 0; }
    .run-nodes li + li { border-top: 0; }
    .run-node {
        align-items: flex-start;
        padding: 14px;
        background: var(--panel);
        border: 1px solid var(--line);
        border-radius: 14px;
    }
    .run-node.is-selected { box-shadow: inset 0 0 0 2px var(--accent); }
    .run-node-name { flex-wrap: wrap; gap: 2px 8px; }
    .run-node-name strong { overflow: visible; white-space: normal; overflow-wrap: anywhere; }
    .run-output { border-radius: 14px; }
    .run-output .mon-iconbtn { width: 44px; height: 44px; }
    .run-console .mon-console-log { max-height: 55vh; }

    /* The runs list: each run a card -- what and its state, then the rest. */
    .table-scroll:has(> table.run-list) { overflow: visible; }
    table.run-list,
    table.run-list tbody { display: block; overflow: visible; }
    table.run-list thead { display: none; }
    table.run-list tr {
        position: relative;
        display: grid;
        grid-template-columns: auto auto auto minmax(0, 1fr) auto;
        gap: 4px 10px;
        align-items: baseline;
        margin-bottom: 8px;
        padding: 14px;
        background: var(--panel);
        border: 1px solid var(--line);
        border-radius: 14px;
    }
    table.run-list td { min-width: 0; padding: 0; border: 0; }
    table.run-list td:nth-child(2) { grid-row: 1; grid-column: 1 / 5; font-size: 15px; font-weight: 600; overflow-wrap: anywhere; }
    table.run-list td:nth-child(6) { grid-row: 1; grid-column: 5; justify-self: end; }
    table.run-list td:nth-child(1),
    table.run-list td:nth-child(3),
    table.run-list td:nth-child(4),
    table.run-list td:nth-child(5) { grid-row: 2; color: var(--muted); font-size: 13px; }
    table.run-list td:nth-child(5) { grid-column: 4 / -1; overflow-wrap: anywhere; }
    /* The title's link covers the card, so a tap anywhere opens the run. */
    table.run-list td:nth-child(2) a::after { content: ""; position: absolute; inset: 0; border-radius: 14px; }
}

/* Disk acknowledgements: under a Storage meter, and the Disks page's bulk bar. */
.meter-ack { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 6px; }
.meter-ack form { margin: 0; }
.mon-disk-bulk { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: var(--space-3); }
.mon-disk-bulk .muted { font-size: 13px; }
.col-check { width: 32px; }

/* Services tab (#161) */
.mon-svc-sub { display: block; color: var(--fg-muted); font-size: var(--text-sm); }
.mon-svc-err { display: block; color: var(--crit); font-size: var(--text-sm); }
.mon-svc-edit { display: inline-block; }
.mon-svc-edit[open] { display: block; margin: 6px 0; }
.mon-svc-form { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px 12px; }
.mon-svc-form label { margin: 0; }

/* Nodes > Services (#162): the Nodes table, its own columns. */
.fl-table.fl-svc {
    --fl-cols: 36px minmax(200px, 1.5fr) minmax(150px, 1fr) minmax(190px, 1.3fr) minmax(150px, 1fr)
               minmax(200px, 1.4fr) 92px;
    min-width: 1040px;
}
.fl-svcname { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.fl-svc-n { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fl-svcname .fl-of { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fl-svcrun { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 6px; min-width: 0; }
.fl-svcrun .fl-num, .fl-svcother .fl-num { overflow-wrap: anywhere; }
.fl-svcstate { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 6px; }
.fl-svcstate .mon-svc-sub { display: inline; }
.fl-svcother { font-size: var(--text-sm); }
.fl-num.is-info { color: var(--info); }
/* How a running version is known: a quiet tag; "installed" is the weak one. */
.mon-svc-src {
    flex: none;
    padding: 0 6px;
    border: 1px solid var(--line);
    border-radius: 9px;
    color: var(--muted);
    font-size: var(--text-xs);
    line-height: 16px;
}
.mon-svc-src.is-installed { border-style: dashed; color: var(--warn); }
/* Version spread: one line per service, its running versions as chips. */
.fl-spread {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-bottom: var(--space-3, 12px);
    padding: 10px 14px;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
}
.fl-spread-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; }
.fl-spread-name { min-width: 120px; font-weight: 600; color: var(--text); text-decoration: none; }
.fl-spread-name:hover { color: var(--accent); }
.fl-spread .fl-fold-chip .fl-num { font-size: var(--text-sm); }
@media (max-width: 699px) {
    .fl-table.fl-svc { min-width: 0; }
    .fl-svc .fl-group > .fl-row:not(.fl-grouphead):not(.fl-folded) { grid-template-columns: 17px minmax(0, 1fr) auto; }
    .fl-svc .fl-host { grid-row: 1; grid-column: 1 / -1; }
    .fl-svc .fl-svcname { grid-row: 2; grid-column: 2 / -1; }
    .fl-svc .fl-svc-n { font-size: 15px; font-weight: 600; }
    .fl-svc .fl-svcstate { grid-row: 3; grid-column: 2 / -1; }
    .fl-svc .fl-svcrun { grid-row: 4; grid-column: 2 / -1; }
    .fl-svc .fl-svcother { grid-row: 5; grid-column: 2 / -1; }
    .fl-svc .fl-svcother:has(> .fl-none:only-child) { display: none; }
    .fl-spread-name { min-width: 0; flex-basis: 100%; }
}
