/* SnazzyVPS control plane.

   Light, dense, and deliberately small — the shell must paint before any data
   arrives, so there is no framework here. Every colour, radius and shadow comes
   from tokens.css, which the documentation site loads too; nothing below
   declares a hex of its own.

   The component vocabulary is Stack's (ThemeForest 22959011), rebuilt rather
   than vendored. Stack ships ~17.5MB of core across Bootstrap 4, jQuery,
   material-design-kit and dom-factory to draw a card with a shadow on it. What
   follows is the same design language in the panel's own CSS, so the shell
   still costs one small file and panel.js keeps its vanilla contract.

   Class names are unchanged wherever views depend on them — forty views carry
   inline renderers, and PanelViewTests reads the markup they emit. */

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.5 var(--font-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

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

h1, h2, h3 { font-family: var(--font-head); color: var(--ink-heading); }
h1 { font-size: 24px; font-weight: 700; margin: 0 0 4px; letter-spacing: -.2px; }
h2 { font-size: 16px; font-weight: 600; margin: 0 0 12px; }

.muted { color: var(--muted); }
.small { font-size: 13px; }
.spacer { margin-left: auto; }

/* --------------------------------------------------------------- chrome */

.topbar {
  display: flex; align-items: center; gap: 16px;
  height: var(--topbar-h); padding: 0 20px;
  background: var(--dark); color: #fff;
}
.brand { display: flex; align-items: center; gap: 11px; color: #fff; font-weight: 600; }
.brand:hover { text-decoration: none; }
/* Width and height are on the element as well as here, so the topbar does not
   reflow between first paint and the SVG arriving. */
.brand-logo { height: 28px; width: auto; display: block; }
.brand-mark { width: 26px; height: 26px; display: block; }
.brand-divider { width: 1px; height: 22px; background: rgba(255,255,255,.25); }
.brand-name { font-family: var(--font-head); font-size: 15px; font-weight: 700; letter-spacing: .02em; }

.account-switch select, .account-name {
  background: rgba(255,255,255,.1); color: #fff;
  border: 1px solid rgba(255,255,255,.2); border-radius: var(--radius);
  padding: 6px 10px; font-size: 14px;
}
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 14px; font-size: 13px; }
.user-email { color: rgba(255,255,255,.66); }
.logout { color: #fff; }

/* The hamburger. Hidden until the sidebar starts overlapping — see the media
   query at the foot of this file. */
.nav-toggle {
  display: none; background: transparent; color: #fff; border: 0;
  padding: 6px; margin-left: -6px; cursor: pointer; line-height: 0;
}
.nav-toggle:hover { background: rgba(255,255,255,.12); filter: none; }

.layout { display: flex; min-height: calc(100vh - var(--topbar-h)); }

.sidebar {
  width: var(--sidebar-w); flex: none; background: var(--surface);
  border-right: 1px solid var(--line); padding: 16px 0;
}
.sidebar-account { padding: 0 18px 14px; border-bottom: 1px solid var(--line); margin-bottom: 12px; }
.sidebar-account-name { font-weight: 600; }
.sidebar-account-role {
  font-size: 12px; color: var(--muted-soft);
  text-transform: uppercase; letter-spacing: .04em;
}
.nav-heading {
  padding: 14px 18px 6px; font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em; color: var(--muted-soft);
}
.nav-list { list-style: none; margin: 0; padding: 0; }
.nav-list a {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 18px; color: var(--ink); font-size: 14px;
}
.nav-list a:hover { background: var(--bg); text-decoration: none; }
.nav-list a.active {
  background: rgba(var(--primary-rgb),.08); color: var(--primary); font-weight: 600;
  box-shadow: inset 3px 0 0 var(--primary);
}
.nav-list .material-icons { font-size: 20px; color: var(--muted-soft); flex: none; }
.nav-list a:hover .material-icons { color: var(--muted); }
.nav-list a.active .material-icons { color: var(--primary); }

.content { flex: 1; padding: 24px 28px; min-width: 0; }

/* The scrim behind the sidebar when it is a drawer. Inert until the body
   carries .nav-open, so it costs nothing on a desktop. */
.nav-scrim { display: none; }

/* ------------------------------------------------------------ page heading */

.page-head { display: flex; align-items: center; gap: 12px; margin-bottom: 18px; flex-wrap: wrap; }
/* Wraps the breadcrumb, title and subtitle so they stack as one block while the
   page action stays on the right. Index pages have no breadcrumb and keep the
   flat h1 + .spacer shape, which still works. */
.page-head-main { display: flex; flex-direction: column; min-width: 0; }
.page-head-main .subtitle { color: var(--muted); font-size: 13px; }

/* Ancestors only — no trailing entry for the page you are already on. Stack
   repeats the title in the crumb above it; saying "web-01" twice in 40px of
   each other is noise, and the crumb's job here is the way back. */
.breadcrumb {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  list-style: none; margin: 0 0 3px; padding: 0;
  font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
}
.breadcrumb li { display: flex; align-items: center; gap: 6px; color: var(--muted-soft); }
.breadcrumb li + li::before { content: "/"; color: var(--line-strong); }
.breadcrumb a { color: var(--muted); }
.breadcrumb a:hover { color: var(--primary); }

/* ---------------------------------------------------------------- cards
   Stack's card carries a shadow and no border; the old panel did the opposite.
   .pane is the same box under a second name — index pages say .card, detail
   tabs say .pane. Both are kept so no view has to change, but they are now one
   look rather than two that nearly match. */

.card, .pane {
  background: var(--surface); border: none; border-radius: var(--radius);
  box-shadow: var(--sh-card); padding: 18px 20px; margin-bottom: 18px;
}
.pane { padding: 20px 22px; }
.card-empty { text-align: center; color: var(--muted); padding: 40px 20px; }
.card > h2, .pane > h2 { color: var(--ink-heading); }
.pane > h2 { display: flex; align-items: baseline; gap: 10px; }
.pane > h2 .muted { font-weight: 400; font-size: 13px; font-family: var(--font-body); }
.pane-note { color: var(--muted); font-size: 13px; line-height: 1.6; margin: 14px 0 0; }
.pane-note code {
  background: var(--track); color: var(--primary-ink);
  border-radius: var(--radius); padding: 1px 5px; font-size: 12.5px;
  font-family: var(--font-mono);
}

/* ----------------------------------------------------------------- tiles
   Stack's .text-amount: big, lightly tracked, and the label above it rather
   than beside. The badge slot is what a metric's severity renders into. */

.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; margin-bottom: 18px; }
.tile {
  position: relative; overflow: hidden;
  background: var(--surface); border: none; border-radius: var(--radius);
  box-shadow: var(--sh-card); padding: 16px 18px;
}
.tile-label {
  font-size: 11px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .05em; color: var(--muted-soft);
}
.tile-value {
  font-family: var(--font-head); font-size: 30px; font-weight: 600;
  letter-spacing: 0; color: var(--ink); margin-top: 4px; line-height: 1.2;
}
.tile-head { display: flex; align-items: center; gap: 8px; }
.tile-head .badge { margin-left: auto; }
/* A watermark, not a decoration: it sits behind the number and must never
   compete with it for the eye. */
.tile-icon {
  position: absolute; right: 10px; bottom: 6px;
  font-size: 44px; color: var(--track); pointer-events: none;
}

/* --------------------------------------------------------------- badges
   From Stack's _badge.scss: 10% tint behind, the colour darkened 5% in front.
   The derived hexes live in tokens.css because darken() works in HSL. */

.badge {
  display: inline-flex; align-items: center; gap: 4px; vertical-align: middle;
  font-size: 11px; font-weight: 600; line-height: 1;
  padding: .35em .55em; border-radius: var(--radius);
  text-transform: uppercase; letter-spacing: .04em; white-space: nowrap;
}
.badge-pill { border-radius: var(--radius-pill); padding: .35em .7em; }
.badge .material-icons { font-size: 13px; }

.badge-soft-ok      { color: var(--ok-ink);      background: rgba(var(--ok-rgb),.1); }
.badge-soft-warn    { color: var(--warn-ink);    background: rgba(var(--warn-rgb),.14); }
.badge-soft-crit    { color: var(--crit-ink);    background: rgba(var(--crit-rgb),.1); }
.badge-soft-info    { color: var(--info-ink);    background: rgba(var(--info-rgb),.1); }
.badge-soft-primary { color: var(--primary-ink); background: rgba(var(--primary-rgb),.1); }
/* The absence of a reading is not a good reading. A metric nobody has reported
   gets the neutral badge, never the green one. */
.badge-soft-none    { color: var(--muted);       background: var(--track); }

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

/* Wide tables scroll inside their card rather than pushing the page sideways.
   Several website tabs carry eight columns and overflowed the layout below a
   laptop; the page body must never scroll horizontally. */
.table-responsive { overflow-x: auto; -webkit-overflow-scrolling: touch; }

table.list { width: 100%; border-collapse: collapse; }
table.list th {
  text-align: left; font-size: 13.2px; font-weight: 600;
  color: rgba(55,77,103,.54); padding: 10px; border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
table.list td { padding: 12px 10px; border-bottom: 1px solid var(--line); font-size: 14px; vertical-align: middle; }
table.list tr:last-child td { border-bottom: 0; }
table.list tbody tr:hover td { background: var(--bg); }
table.list tbody tr.selected > td { background: rgba(var(--primary-rgb),.05); }
/* Figures line up on the decimal point or they are not worth tabulating. */
table.list td.num, table.list th.num { text-align: right; font-variant-numeric: tabular-nums; }

.chip { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; }
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted-soft); display: inline-block; }
.dot.ok { background: var(--ok); } .dot.warn { background: var(--warn); } .dot.bad { background: var(--crit); }
.site-favicon { vertical-align: -3px; border-radius: 3px; margin: 0 2px; }

/* ----------------------------------------------------------------- pills
   A state as a solid block of the colour that state means — the loud sibling
   of .badge, whose 10% tint is right beside a big number and too quiet in a
   row of twelve service names.

   The support inbox has asked for `.pill` since it was written and there was
   no such rule, so every ticket status rendered as bare unstyled text.

   No modifier is the NEUTRAL pill, not the healthy one: an unrecognised state
   must never come out green. See Panel.statePill(), which owns the mapping. */

.pill {
  display: inline-flex; align-items: center; vertical-align: middle;
  padding: .38em .62em; border-radius: var(--radius);
  font-size: 11px; font-weight: 700; line-height: 1;
  text-transform: uppercase; letter-spacing: .04em; white-space: nowrap;
  background: var(--muted); color: #fff;
}
.pill.ok   { background: var(--ok); }
.pill.info { background: var(--info); }
.pill.crit, .pill.bad { background: var(--crit); }
/* Amber is the one that cannot take white text: #fff on #ffc107 is about
   1.9:1, which fails at every size — and this is the pill whose entire job is
   to be read. Dark ink on the same amber is over 8:1. */
.pill.warn { background: var(--warn); color: #4a3600; }
.pill.none { background: var(--track); color: var(--muted); }

/* Pills in a line. `align-items: center` so a button sharing the row does not
   drag the pills to its own height. */
.pill-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.pill-row button { margin-left: 6px; }

/* ------------------------------------------------------------ state grid
   A set of "this thing is in this state" pairs — service units today, and the
   same shape wherever else a screen lists named states.

   Reads as a table, but flows into as many columns as the card is wide rather
   than down one. Panel.kv() was the other candidate and is the wrong shape
   here: its 160px label column against a two-word value leaves most of a
   full-width card empty, and six units become six near-empty rows.

   The name is plain text and only the STATE carries colour, so the eye lands
   on the one unit that is not green instead of scanning six identical
   rectangles for the odd one out. */

/* 300px, not 230: with a Restart control in the cell alongside the pill,
   narrower columns truncated the very names being restarted — `php8.4-f…`
   and `systemd-jour…` are not names anyone can act on. Fewer, wider columns
   read better than more, tighter ones. */
.state-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 0 32px;
}
.state-grid-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 9px 0; min-width: 0; font-size: 14px;
  border-bottom: 1px solid var(--line);
}
/* The name can be long (`php8.3-fpm`, a unit with a path in it) and must give
   way to the state rather than pushing it out of the cell. */
.state-grid-name {
  color: var(--ink); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The state, and whatever control the caller put beside it. `flex-shrink: 0`
   so the pill keeps its size and the NAME is what gives way in a narrow cell —
   a truncated unit name is recoverable from its title attribute, a truncated
   state is just wrong. */
.state-grid-state {
  display: flex; align-items: center; gap: 10px; flex-shrink: 0;
}
.state-grid-state .btn-sm { padding: 3px 9px; font-size: 12px; }

.meter { height: 6px; background: var(--track); border-radius: var(--radius-pill); overflow: hidden; min-width: 70px; }
.meter-lg { height: 14px; } .meter-lg i { transition: width .4s ease; }
.meter i { display: block; height: 100%; background: var(--ok); border-radius: inherit; }
.meter.warn i { background: var(--warn); } .meter.crit i { background: var(--crit); }

/* ----------------------------------------------------------- pagination
   Stack's .pagination-light: square hit area, pill radius, transparent until
   hovered. The chevrons are Material Icons ligatures. */

.pagination-bar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line);
  font-size: 13px; color: var(--muted);
}
.pagination-count { margin-right: auto; }
.pagination-count b { color: var(--ink); font-weight: 600; }

/* Rows per page. `select` is width:100% globally — for a form field that is
   right, and in this one-line bar it would push the page buttons onto a row of
   their own, so the width is taken back here. */
.pagination-size {
  display: inline-flex; align-items: center; gap: 8px;
  margin: 0; font-size: 13px; color: var(--muted); white-space: nowrap;
}
.pagination-size select {
  width: auto; padding: 5px 8px; font-size: 13px;
  border-radius: var(--radius-pill); color: var(--ink);
}

.pagination { display: flex; align-items: center; gap: .05rem; list-style: none; margin: 0; padding: 0; }
.pagination .page-item + .page-item { margin-left: .05rem; }
.page-link {
  display: flex; align-items: center; justify-content: center;
  min-width: 32px; height: 32px; padding: 0 8px;
  border: 0; background: transparent; color: var(--muted);
  font: 600 13px/1.5 var(--font-body); border-radius: var(--radius-pill);
  cursor: pointer;
}
.page-link:hover { background: rgba(var(--primary-rgb),.1); color: var(--primary); text-decoration: none; filter: none; }
.page-item.active .page-link { background: var(--primary); color: #fff; }
.page-item.active .page-link:hover { background: var(--primary); color: #fff; }
.page-item.disabled .page-link { color: var(--line-strong); pointer-events: none; }
.page-link .material-icons { font-size: 18px; }

/* -------------------------------------------------------------- panels */

.panel-loading, .panel-empty, .panel-error { padding: 24px; text-align: center; font-size: 14px; color: var(--muted); }
.panel-error { color: var(--crit); }

/* --------------------------------------------------------------- forms */

label { display: block; font-size: 13px; font-weight: 600; margin: 12px 0 4px; }
.text-label {
  font-size: 11px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .05em; color: var(--muted-soft); margin-bottom: 6px;
}
.form-group { margin-bottom: 14px; }
.form-group > label { margin-top: 0; }
.form-help { font-size: 12.5px; color: var(--muted); margin: 4px 0 0; }
.form-error { font-size: 12.5px; color: var(--crit-ink); margin: 4px 0 0; font-weight: 600; }

input[type=text], input[type=email], input[type=password], input[type=url],
input[type=number], input[type=search], select, textarea {
  width: 100%; padding: 9px 11px;
  border: 1px solid var(--line-strong); border-radius: var(--radius);
  font-size: 14px; font-family: inherit; background: #fff; color: var(--ink);
}
input::placeholder, textarea::placeholder { color: var(--muted-soft); }
input:focus, select:focus, textarea:focus {
  outline: 0; border-color: var(--primary); box-shadow: var(--sh-focus);
}
.is-invalid, .is-invalid:focus { border-color: var(--crit); }
.is-invalid:focus { box-shadow: 0 0 0 .2rem rgba(var(--crit-rgb),.25); }
input:disabled, select:disabled, textarea:disabled { background: var(--bg); color: var(--muted); cursor: not-allowed; }

/* -------------------------------------------------------------- buttons */

button, .btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  background: var(--primary); color: #fff; border: 1px solid transparent;
  border-radius: var(--radius); padding: 9px 16px;
  font-size: 14px; font-weight: 600; font-family: inherit; line-height: 1.4;
  cursor: pointer; text-align: center;
}
button:hover, .btn:hover { filter: brightness(1.07); text-decoration: none; }
button:focus-visible, .btn:focus-visible { outline: 0; box-shadow: var(--sh-focus); }
button:disabled, .btn:disabled, .btn.disabled {
  opacity: .55; cursor: not-allowed; filter: none; pointer-events: none;
}
.btn-secondary { background: var(--track); color: var(--ink); }
.btn-outline { background: transparent; color: var(--primary); border-color: var(--line-strong); }
.btn-outline:hover { background: rgba(var(--primary-rgb),.08); border-color: var(--primary); filter: none; }
.btn-danger { background: var(--crit); }
.btn-sm { padding: 5px 10px; font-size: 12.5px; }
.btn-lg { padding: 12px 22px; font-size: 15px; }
.btn-block { display: flex; width: 100%; }
.btn-icon { padding: 6px; min-width: 32px; }
button .material-icons, .btn .material-icons { font-size: 18px; }

/* A destructive icon button in a table row.
 *
 * Quiet until reached for. A row of solid red buttons makes the eye go to the
 * one irreversible control on the screen before the data it is about, and
 * turns "delete" into the most prominent thing on a list whose usual purpose
 * is reading. It colours on hover and on focus, where intent already exists. */
.btn-icon-danger {
  background: transparent; color: var(--muted);
  border: 1px solid transparent; line-height: 0;
}
.btn-icon-danger:hover,
.btn-icon-danger:focus-visible {
  background: rgba(var(--crit-rgb),.1); color: var(--crit-ink);
  border-color: var(--crit-edge); filter: none;
}
.btn-icon-danger:disabled { opacity: .45; cursor: default; }

/* Right-aligned, and only as wide as the control — otherwise the actions
   column steals space from the domain, which is what people read. */
td.row-actions { text-align: right; width: 1%; white-space: nowrap; }

.btn-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 16px; align-items: center; }

/* --------------------------------------------------------------- alerts
   Stack's .alert-soft-*: a 5% wash, a mixed border, the colour itself for
   text. The class names stay .flash-* — header.php interpolates the kind
   straight into them and fn_flash_take() is single-consume. */

.flash {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 11px 14px; border: 1px solid transparent; border-radius: var(--radius);
  margin-bottom: 16px; font-size: 14px;
}
.flash::before { font-family: "Material Icons"; font-size: 18px; line-height: 1.2; flex: none; }
.flash-ok    { background: rgba(var(--ok-rgb),.05);   border-color: var(--ok-edge);   color: var(--ok-ink); }
.flash-error { background: rgba(var(--crit-rgb),.05); border-color: var(--crit-edge); color: var(--crit-ink); }
/* Neither green nor red: a skipped test proved nothing, which is its own
   outcome and must not be able to read as either. */
.flash-warn  { background: rgba(var(--warn-rgb),.08); border-color: var(--warn-edge); color: #8a6100; }
.flash-info  { background: rgba(var(--info-rgb),.05); border-color: var(--info-edge); color: var(--info-ink); }
.flash-ok::before    { content: "check_circle"; }
.flash-error::before { content: "error"; }
.flash-warn::before  { content: "warning"; }
.flash-info::before  { content: "info"; }

/* A "Last polled" figure that is too old to trust. Amber rather than red: the
   server is very likely fine — it is the panel's picture of it that is stale,
   and colouring that as a failure would train operators to ignore real ones. */
.stale-poll { color: var(--warn-ink); font-weight: 600; cursor: help; }

pre.command {
  background: var(--ink); color: #e9edf2; padding: 12px 14px; border-radius: var(--radius);
  overflow-x: auto; font-size: 13px; font-family: var(--font-mono);
  white-space: pre-wrap; word-break: break-all;
}

/* ----------------------------------------------------------------- auth */

body.centred { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.auth-card {
  background: var(--surface); border: none; border-radius: var(--radius-lg);
  box-shadow: var(--sh-card); padding: 28px; width: 100%; max-width: 380px;
}
.auth-brand { margin-bottom: 22px; }
.auth-brand img { display: block; width: 192px; height: auto; }
/* Both auth pages also add the favicon and icons.css; see the note in each. */
.auth-card button { width: 100%; margin-top: 18px; }

/* ------------------------------------------------------------------ tabs */

.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--line); margin-bottom: 18px; flex-wrap: wrap; }
.tabs a { padding: 10px 14px; font-size: 14px; font-weight: 600; color: var(--muted); border-bottom: 2px solid transparent; }
.tabs a:hover { color: var(--ink); text-decoration: none; }
.tabs a.active { color: var(--primary); border-bottom-color: var(--primary); }

/* ------------------------------------------------------- toasts & modals
   Replacements for alert() and confirm(). A native dialog blocks the
   renderer until something dismisses it, which makes a working page look
   hung — and in an automated context nothing ever does. */

#toast-host { position: fixed; right: 20px; bottom: 20px; display: flex;
              flex-direction: column; gap: 8px; z-index: 50; }
.toast { background: var(--dark); color: #fff; padding: 11px 16px; border-radius: var(--radius);
         font-size: 14px; max-width: 380px; box-shadow: var(--sh-toast);
         opacity: 1; transition: opacity .5s ease; }
.toast-error { background: var(--crit-ink); }
.toast.leaving { opacity: 0; }

.modal-back { position: fixed; inset: 0; background: rgba(17,43,74,.45);
              display: flex; align-items: center; justify-content: center; z-index: 60;
              padding: 20px; }
.modal { background: #fff; border-radius: var(--radius-lg); padding: 24px;
         max-width: 420px; width: 100%; box-shadow: 0 15px 45px rgba(17,43,74,.25); }
.modal-message { margin: 0 0 18px; font-size: 15px; }
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; }

/* ------------------------------------------------------- site & server tabs
   The building blocks the thirteen website tabs and the server page are made
   of. Everything here is expressed in the tokens, so a change to --primary or
   --line reaches these too. */

/* Label/value rows — Panel.kv(). Two columns on anything but a phone. */
.kv { display: grid; grid-template-columns: 160px 1fr; gap: 0; }
.kv-label { color: var(--muted); font-size: 14px; padding: 11px 0;
            border-bottom: 1px solid var(--line); }
.kv-value { font-size: 14px; padding: 11px 0; border-bottom: 1px solid var(--line);
            display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.kv > :nth-last-child(-n+2) { border-bottom: 0; }
.kv-value code { background: rgba(var(--primary-rgb),.08); color: var(--primary-ink);
                 border-radius: var(--radius); padding: 2px 7px; font-size: 13px;
                 font-family: var(--font-mono); }

/* Choice rows — Panel.pills(). 24h / 7d, 120 / 500 / 2000, the log types. */
.pills { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 14px; }
.pills button { background: transparent; color: var(--muted); font-weight: 600;
                padding: 5px 12px; font-size: 13px; border-radius: var(--radius-pill); }
.pills button:hover { background: var(--track); color: var(--ink); filter: none; }
.pills button.active { background: rgba(var(--primary-rgb),.1); color: var(--primary); }

/* The hardening posture: one row per check, its state on the right. */
.posture { display: flex; flex-direction: column; }
.posture-row { display: flex; align-items: flex-start; gap: 12px; padding: 12px 0;
               border-bottom: 1px solid var(--line); }
.posture-row:last-child { border-bottom: 0; }
.posture-row .dot { margin-top: 6px; flex: 0 0 auto; }
.posture-name { font-weight: 600; font-size: 14px; }
.posture-why { color: var(--muted); font-size: 13px; margin-top: 2px; }
.posture-state { margin-left: auto; font-size: 13px; color: var(--ok-ink); }
.posture-state.off { color: var(--warn-ink); }

/* A chart box. Height is fixed because Chart.js needs one to lay out into. */
.spark { position: relative; height: 64px; margin-top: 10px; }
.spark-tall { height: 150px; }

/* Inline control rows — a field and its button, several to a line. */
.control-row { display: flex; gap: 8px; align-items: flex-start; flex-wrap: wrap;
               margin-top: 12px; }
.control-row input, .control-row select { width: auto; min-width: 200px; flex: 0 1 auto; }
.control-row .select2-container { width: auto !important; min-width: 200px; }
.control-row button { flex: 0 0 auto; }
.control-row.wide input { flex: 1 1 260px; }

/* A grid of small labelled fields — the PHP limits, the New site form. */
.field-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
              gap: 0 14px; align-items: end; }
.field-grid label { margin-top: 0; }

/* Rank rows — top clients, top paths. The bar IS the background. */
.ranks { display: flex; flex-direction: column; gap: 6px; }
.rank { display: flex; align-items: center; gap: 12px; font-size: 13px; }
.rank-bar { flex: 1 1 auto;
            background: linear-gradient(to right, rgba(var(--primary-rgb),.12) var(--w), transparent var(--w));
            border-radius: var(--radius); padding: 5px 8px; overflow: hidden;
            white-space: nowrap; text-overflow: ellipsis; }
.rank-n { color: var(--muted); flex: 0 0 auto; font-variant-numeric: tabular-nums; }

/* Two equal columns that become one on a narrow screen — the top-clients and
   top-paths pair, and the server page's tiles beside their chart. */
.two-up { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
          gap: 8px 28px; margin-top: 20px; }
.two-up h2 { margin-bottom: 10px; }

/* A filter bar above a list. Narrower than the list card it sits on, because
   it is a control and not content. */
.card-filter { display: flex; align-items: center; gap: 12px; padding: 14px 18px; }
.card-filter label { margin: 0; white-space: nowrap; }
.card-filter select { width: auto; min-width: 260px; }
.card-filter .select2-container { width: auto !important; min-width: 260px; }

/* The CLI handover modal, and the typed confirmation for irreversible ones. */
.modal-wide { max-width: 640px; }

/* A handed-over command that costs something. Panel.commandModal()'s `danger`
   was in its contract and read nowhere, so `vps site:remove` — which deletes
   the files and the database — looked exactly like `nginx -t`. */
.modal-danger {
  margin: 0 0 14px; padding: 10px 12px; border-radius: var(--radius);
  font-size: 13.5px; font-weight: 600;
}
.modal-destructive .modal-danger {
  color: var(--warn-ink); background: rgba(var(--warn-rgb),.16);
}
.modal-irreversible .modal-danger {
  color: var(--crit-ink); background: rgba(var(--crit-rgb),.1);
}
.modal-title { font-family: var(--font-head); font-size: 16px; font-weight: 600; margin: 0 0 10px; }
.modal-command { margin: 14px 0 18px; white-space: pre-wrap; word-break: break-all; }
.modal-input { margin: 10px 0 18px; }

/* A command control sitting in a tab's prose. */
.cmd-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 12px; }
.cmd-row .muted { flex: 1 1 240px; font-size: 13px; }
.cmd-cli { background: var(--track); color: var(--ink); font-weight: 600; }
.cmd-cli:hover { background: var(--line-strong); filter: none; }
.cmd-danger { background: var(--crit); }

/* A value the operator supplies before the control will run — a backup
   destination and the credential for it, today. Inline rather than in a modal
   that appears after the click: a label you can read before committing is
   worth more than one you read while deciding. */
.cmd-arg { display: flex; flex-direction: column; gap: 3px; flex: 0 1 220px; }
.cmd-arg > span { font-size: 12px; font-weight: 600; color: var(--muted); }
.cmd-arg > input {
  padding: 7px 9px; font-size: 13px; border-radius: var(--radius);
  border: 1px solid var(--line); background: var(--surface); color: var(--ink);
}
.cmd-arg > input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }

/* -------------------------------------------------------- support thread
   These five classes are emitted by the supportThread renderer in
   view/support/detail.php and were defined nowhere, so the one screen an
   operator answers a customer on was raw unstyled markup. */

.thread { display: flex; flex-direction: column; gap: 14px; }
.thread-msg {
  border: 1px solid var(--line); border-left: 3px solid var(--line-strong);
  border-radius: var(--radius); padding: 12px 14px; background: var(--surface);
}
/* The operator's own replies, tinted so a long thread reads as a conversation
   and not a wall. Whoever spoke last decides where the ticket goes next, so
   telling the two apart at a glance is the point. */
.thread-msg--admin { background: rgba(var(--primary-rgb),.04); border-left-color: var(--primary); }
/* A system note is neither party — it records that something happened, so it
   is set back rather than given a voice. */
.thread-msg--system { background: var(--bg); border-left-color: var(--line-strong); font-size: 13px; }
.thread-msg--system .thread-body { color: var(--muted); }
.thread-meta {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  font-size: 12px; color: var(--muted); margin-bottom: 6px;
}
.thread-meta strong { color: var(--ink); font-size: 13px; }
.thread-body { font-size: 14px; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; }

/* ---------------------------------------------------------------- mobile
   There were no media queries at all: below about 900px the 220px sidebar ate
   the content column and the wider tables pushed the page sideways. The
   sidebar becomes a drawer, toggled by adding .nav-open to <body> — the same
   mechanism the documentation shell already uses, rather than a layout
   engine. */

@media (max-width: 900px) {
  .nav-toggle { display: inline-flex; }
  .topbar { gap: 10px; padding: 0 14px; }
  .user-email { display: none; }

  .sidebar {
    position: fixed; top: var(--topbar-h); bottom: 0; left: 0; z-index: 40;
    overflow-y: auto; transform: translateX(-100%);
    transition: transform .18s ease; box-shadow: var(--sh-pop);
  }
  body.nav-open .sidebar { transform: none; }
  body.nav-open { overflow: hidden; }
  body.nav-open .nav-scrim {
    display: block; position: fixed; inset: var(--topbar-h) 0 0; z-index: 35;
    background: rgba(17,43,74,.45);
  }

  .content { padding: 18px 16px; }
  .tiles { grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; }
  .kv { grid-template-columns: 1fr; }
  .kv-label { padding-bottom: 0; border-bottom: 0; }
  .kv > :nth-last-child(2) { border-bottom: 0; }
  .card-filter { flex-wrap: wrap; }
  .card-filter select, .card-filter .select2-container { min-width: 0; width: 100% !important; }
}

@media (prefers-reduced-motion: reduce) {
  .sidebar, .toast { transition: none; }
}
