:root {
  /* Surfaces — layered dark, each step a little lighter */
  --bg:         #0a0d12;
  --sidebar:    #10151d;
  --surface:    #151b25;
  --surface-2:  #1b2230;
  --surface-3:  #222b3b;
  --border:     #232c3b;
  --border-soft:#1b2331;

  /* Aliases. Several templates were written against --line and --panel,
     which were never defined. An undefined var invalidates the WHOLE
     declaration at computed-value time, so `border:1px solid var(--line)`
     rendered as NO border — silently, with no console error. That is why
     the chips in the UFW table and the Fail2Ban ban/ignore chips looked
     borderless. Defining the aliases fixes all of them at once; prefer
     --border / --surface in new markup. */
  --line:       #232c3b;
  --panel:      #151b25;
  /* inbox.html highlights unread messages with background:var(--raised);
     undefined, that highlight simply never rendered. */
  --raised:     #1b2230;

  --text:       #e9edf3;
  --text-2:     #b6c0cf;
  --dim:        #7d8899;
  --faint:      #5a6577;

  /* Teal primary, orange secondary */
  --accent:     #2dd4bf;
  --accent-2:   #14b8a6;
  --accent-dim: #2dd4bf1a;
  --accent-glow:#2dd4bf33;
  --orange:     #fb923c;
  --orange-dim: #fb923c1a;

  --ok:      #34d399;
  --ok-dim:  #34d3991a;
  --warn:    #fbbf24;
  /* Brand gold. Same value as --warn today, deliberately its own token:
     the wordmark is not a warning, and changing the warning colour must
     not repaint the logo. Kept in step with the bolt in logo.svg and
     favicon.svg, which cannot read a CSS variable because they are
     loaded as <img>. Change one, change all three. */
  --gold:    #fbbf24;
  --warn-dim:#fbbf241a;
  --danger:  #f87171;
  --danger-dim:#f871711a;
  --info:    #60a5fa;
  --info-dim:#60a5fa1a;
  --purple:  #a78bfa;
  --purple-dim:#a78bfa1a;

  --mono: "SF Mono","JetBrains Mono",ui-monospace,Menlo,Consolas,monospace;
  --sans: "InterLocal","Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;

  --side-w: 286px;
  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 16px;
  /* The bar across the top, and the words ON an accent-coloured
     button. Both were literals and so never followed the theme: the bar
     stayed near-black on a white page, and the button had near-black
     words on a deep teal (operator, 2026-08-29). */
  --topbar-bg:  rgba(10,13,18,.85);
  --on-accent:  #04211d;
  --shadow: 0 4px 24px -6px rgba(0,0,0,.6);
}

/* The pale theme.
 *
 * The same tokens with different values, so every rule in this file
 * follows without being touched - and a rule added tomorrow gets it for
 * free, because it was written against the tokens like everything else.
 *
 * Not an inversion. Dark surfaces step LIGHTER as they come forward;
 * pale ones step DARKER, and the shadow becomes a faint edge rather than
 * a black glow. Inverting instead of choosing gives grey on grey.
 */
:root[data-theme="light"] {
  --bg:         #f5f7fa;
  --sidebar:    #ffffff;
  --surface:    #ffffff;
  --surface-2:  #f2f5f9;
  --surface-3:  #e8edf4;
  --border:     #d8e0ea;
  --border-soft:#e6ecf3;
  --line:       #d8e0ea;
  --panel:      #ffffff;
  --raised:     #f2f5f9;

  --text:       #16202e;
  --text-2:     #46536a;
  --dim:        #6b7789;
  --faint:      #8b96a8;

  /* The SAME hue, deeper.

     #2dd4bf is chosen for a near-black ground. On white it is a pale
     mint: too little contrast to read, and far too little for white text
     on a button. A palette is not one colour that works everywhere.

     The tints are lifted for the opposite reason - a 10% wash that reads
     on near-black disappears on white. */
  --accent:     #0d9488;
  --accent-2:   #0f766e;
  --accent-dim: #0d948826;
  --accent-glow:#0d948840;
  --gold:       #b45309;
  --orange:     #c2410c;
  --ok:         #15803d;
  --warn:       #b45309;
  --danger:     #b91c1c;
  --info:       #1d4ed8;
  --purple:     #6d28d9;
  --orange-dim: #c2410c1f;
  --ok-dim:     #15803d1f;
  --warn-dim:   #b453091f;
  --danger-dim: #b91c1c1f;
  --info-dim:   #1d4ed81f;
  --purple-dim: #6d28d91f;

  --topbar-bg:  rgba(255,255,255,.85);
  /* White, because the light theme's accent is deep enough to carry it -
     and every ready-made pair was chosen to be. */
  --on-accent:  #ffffff;
  --shadow: 0 2px 10px -4px rgba(22,32,46,.18);
}

* { box-sizing: border-box; }

/* The `hidden` attribute must actually hide.
 *
 * Browsers give [hidden] `display:none` from their OWN stylesheet, which
 * ANY author rule beats — including a plain class like `.fm-overlay
 * { display:grid }`. So an element written as `<div class="fm-overlay"
 * hidden>` renders immediately, and every state inside it shows at once:
 * the file manager's upload modal appeared on page load with the
 * progress bar, "Overwrite/Skip" and "Done" all visible together.
 *
 * !important is right here and nowhere else: this restores the meaning
 * of a standard HTML attribute, which no layout rule should be able to
 * override by accident. Put display rules on the element and let
 * `hidden` win. */
[hidden] { display: none !important; }
html,body {
  margin:0; padding:0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  /* 100% is the reader's own setting - whatever they chose in their
     browser or their operating system. Any fixed value here silently
     overrides it, which is what this panel used to do: a 14px base,
     with most text smaller still. Every size below is a rem, so they
     all follow from this one line. */
  font-size: 100%;
  /* Regular by default. This panel had FOURTEEN weights and exactly one
     use of 400, so nothing stood out - everything was emphasised at
     once. Worse, 650/680/720/730 only exist in a variable font; with an
     ordinary installed font the browser rounds them, so those "subtle"
     steps were rendering as full bold. Three weights now: 400 reads,
     500 leans, 600 is a heading. Hierarchy comes from size and colour. */
  font-weight: 400;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: none; }
::selection { background: var(--accent-glow); }

/* Scrollbars */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--surface-3); border-radius: 10px; border: 3px solid transparent; background-clip: content-box; }
::-webkit-scrollbar-thumb:hover { background: #2f3a4d; background-clip: content-box; }

/* ══ Shell ══════════════════════════════════════════════════════════ */
.shell { display: flex; min-height: 100vh; }

/* ── Sidebar ── */
.sidebar {
  width: var(--side-w);
  flex-shrink: 0;
  background: var(--sidebar);
  border-right: 1px solid var(--border-soft);
  display: flex; flex-direction: column;
  position: sticky; top: 0; height: 100vh;
  overflow: hidden;
}
.side-head { padding: 18px 18px 14px; }
.brand { display: flex; align-items: center; gap: 11px; }
.brand-mark {
  width: 38px; height: 38px; border-radius: 11px; flex-shrink: 0;
  display: grid; place-items: center;
  background: linear-gradient(145deg, var(--accent) 0%, var(--accent-2) 100%);
  box-shadow: 0 4px 14px -5px var(--accent-glow);
  transition: transform .16s;
}
.brand:hover .brand-mark { transform: scale(1.05); }
/* 26px inside the 38px tile: the shield then fills about two thirds of
   it, which is the smallest size at which the bolt still reads as a
   bolt rather than a smudge. logo.svg carries no background of its
   own -- the tile below is the background. */
.brand-mark img { width: 26px; height: 26px; }
.brand-text { display: flex; flex-direction: column; line-height: 1.15; min-width: 0; }
.brand-name { font-size:1.25rem; font-weight:400; letter-spacing: -.025em; color: var(--text); }
/* "Panel" carries the same teal-to-sky gradient as the public roadmap,
   so the product and the page that sells it finally look related.
   -webkit- prefix included: background-clip:text is still prefixed in
   Safari, and without it the text renders transparent on nothing --
   an invisible brand name. */
/* The O picks up the gold of the bolt, so the mark and the name refer
   to each other instead of just sitting next to each other. */
.brand-name .gold,
.login-name .gold { color: var(--gold); }
.brand-name .accent,
.login-name .accent {
  background: linear-gradient(90deg, var(--accent), #38bdf8);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* Holds the server hostname. Constrained and ellipsised: a long FQDN
   would otherwise push the sidebar wider or wrap under the logo. The
   full value is in the element's title attribute. */
.brand-sub {
  font-size:0.8125rem; color: var(--faint); margin-top: 1px;
  display: block; max-width: 150px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.side-search { padding: 0 16px 12px; }
.search-box {
  display: flex; align-items: center; gap: 9px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 9px 11px;
  transition: border-color .15s;
}
.search-box:focus-within { border-color: var(--accent); }
.search-box svg { color: var(--faint); flex-shrink: 0; }
.search-box input {
  border: none; background: none; outline: none;
  color: var(--text); font-size:0.875rem; width: 100%;
  font-family: var(--sans);
}
.search-box input::placeholder { color: var(--faint); }
.kbd {
  font-size:0.75rem; font-family: var(--mono);
  color: var(--faint); background: var(--surface-2);
  border: 1px solid var(--border); border-radius: 5px;
  padding: 2px 5px; white-space: nowrap;
}

/* Admin / User role tabs */
.role-tabs {
  display: flex; margin: 0 16px 6px;
  border-bottom: 1px solid var(--border-soft);
}
.role-tab {
  flex: 1; text-align: center;
  padding: 9px 0 10px;
  font-size:0.875rem; font-weight:400;
  color: var(--faint);
  display: flex; align-items: center; justify-content: center; gap: 6px;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color .15s, border-color .15s;
}
.role-tab:hover { color: var(--text-2); }
.role-tab.active { color: var(--accent); border-bottom-color: var(--accent); }

.side-scroll { flex: 1; overflow-y: auto; padding: 6px 12px 16px; }

.nav-group { margin-bottom: 4px; }
.nav-group-head {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px;
  border-radius: var(--r-sm);
  font-size:0.875rem; font-weight:400;
  color: var(--text-2);
  cursor: default;
  user-select: none;
}
.nav-group-head .chev { margin-left: auto; color: var(--faint); transition: transform .2s; }
.nav-group.collapsed .chev { transform: rotate(-90deg); }
.nav-group.collapsed .nav-items { display: none; }
.nav-items { padding: 2px 0 6px; }

.nav-item {
  display: flex; align-items: center; gap: 11px;
  padding: 8px 12px 8px 14px;
  border-radius: var(--r-sm);
  color: var(--dim);
  font-size:0.875rem; font-weight:400;
  position: relative;
  transition: color .13s, background .13s;
}
.nav-item:hover { background: var(--surface); color: var(--text); }
.nav-item.active { background: var(--accent-dim); color: var(--accent); font-weight:400; }
.nav-item.active::before {
  content:""; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  width: 3px; height: 17px; border-radius: 0 3px 3px 0; background: var(--accent);
}
.nav-item .ic { width: 17px; height: 17px; flex-shrink: 0; display: grid; place-items: center; }

.side-foot {
  border-top: 1px solid var(--border-soft);
  padding: 12px 16px;
  display: flex; align-items: center; gap: 10px;
}
.avatar {
  width: 32px; height: 32px; border-radius: 9px; flex-shrink: 0;
  display: grid; place-items: center;
  background: var(--accent-dim); color: var(--accent);
  font-weight:400; font-size:0.875rem;
}
.avatar.is-root { background: var(--orange-dim); color: var(--orange); }
.who-name { font-size:0.875rem; font-weight:400; line-height: 1.2; }
.who-role { font-size:0.8125rem; color: var(--faint); text-transform: capitalize; }
.icon-btn {
  width: 30px; height: 30px; border-radius: 8px;
  display: grid; place-items: center;
  color: var(--faint); background: none; border: none; cursor: pointer; padding: 0;
}
.icon-btn:hover { background: var(--surface-2); color: var(--danger); }
/* Anchor variants of .icon-btn (profile, docs) sit in the same footer row:
   inherit the button box, but hover to the accent rather than danger —
   only signing out is destructive. */
a.icon-btn { text-decoration: none; flex-shrink: 0; }
a.icon-btn:hover { color: var(--accent); }
.side-foot { flex-wrap: nowrap; }
.side-foot .who-name, .side-foot .who-role {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ── Main ── */
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; }

.topbar {
  height: 58px; flex-shrink: 0;
  border-bottom: 1px solid var(--border-soft);
  background: var(--topbar-bg);
  backdrop-filter: blur(12px);
  display: flex; align-items: center; gap: 14px;
  padding: 0 24px;
  position: sticky; top: 0; z-index: 20;
}
.topbar .search-box { flex: 1; max-width: 620px; background: var(--surface); }

/* ── Anything jumped to must clear the sticky bar ──
   The topbar above is 58px and sticky, so a browser scrolling an
   element to the top of the window puts it UNDERNEATH. What the
   reader then sees is a card with no heading - they were sent to a
   panel about one address and the address is the part hidden.

   Set once, for every element with an id, rather than per anchor:
   the first attempt put 24px on two elements in one template, which
   was both too small and only true in that file. 74px is the bar
   plus a little air. Costs nothing when a page is not jumped to. */
[id] { scroll-margin-top: 74px; }

/* ══ Server facts strip (topbar, administrators only) ══
   Replaces the duplicate search box. Each fact is a label above its
   value so the strip reads as data rather than a sentence, and the whole
   row collapses away on narrow screens where the username and page
   controls matter more than the kernel version. */
.sysbar {
  display: flex; align-items: center; gap: 18px;
  min-width: 0; overflow: hidden;
}
.sysbit { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
.sysk {
  font-size:0.75rem; letter-spacing: .7px; text-transform: uppercase;
  color: var(--faint); font-weight:400;
}
.sysv {
  font-size:0.875rem; color: var(--text-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* CPU model strings are long and vary wildly between machines, so this
   one gets room to breathe and truncates rather than pushing the
   username off the bar. Full value is in the title attribute. */
.sysbit.sys-wide { max-width: 240px; }
/* The kernel chip when the running kernel is older than the installed
   one. A link, not a label: a warning somebody cannot act on from
   where they are reading it is a warning they scroll past. */
.sysbit-warn { text-decoration: none; cursor: pointer; }
.sysbit-warn .sysv { color: var(--warn); }
.sysbit-warn .sysk { color: var(--warn); opacity: .85; }
.sysbit-warn:hover .sysv { text-decoration: underline; }
/* A newer kernel is available but not installed. Deliberately quieter
   than sysbit-warn: the panel installs these by itself each morning,
   so on most servers this is a few hours old and not a fault. It is
   worth seeing, not worth alarming about. */
.sysbit-note { text-decoration: none; cursor: pointer; }
.sysbit-note .sysv { color: var(--info); }
.sysbit-note .sysk { color: var(--info); opacity: .85; }
.sysbit-note:hover .sysv { text-decoration: underline; }
@media (max-width: 1200px) { .sysbit.sys-wide, .sysbit:nth-child(3) { display: none; } }
@media (max-width: 900px)  { .sysbar { display: none; } }
.top-right { margin-left: auto; display: flex; align-items: center; gap: 10px; }
.chip {
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 999px; padding: 6px 13px;
  font-size:0.875rem; color: var(--text-2); font-weight:400;
}
.chip svg { color: var(--accent); }
.chip-link { text-decoration: none; padding: 6px 10px; position: relative; }
/* The status light: smaller TEXT, same box as everything beside it. */
.chip-mini { font-size:0.75rem; gap: 5px; }
.chip-mini.ok { color: var(--ok, #34d399); border-color: rgba(52,211,153,.35); }

/* Every chip in the topbar is one height.

   They were sized by their padding plus whatever their text happened
   to be, so the status light (12px text) came out shorter than the
   bell (14px) and the row read as ragged. An explicit height with
   horizontal-only padding takes the font out of the equation: the
   boxes match whatever is written in them, and the icon-only ones are
   square. */
.top-right .chip {
  height: 34px; box-sizing: border-box;
  padding-top: 0; padding-bottom: 0;
  justify-content: center;
}
.top-right .chip-link { min-width: 34px; }
.chip-link:hover { border-color: var(--accent); }
/* The count sits ON the bell rather than beside it: the chip is narrow,
   and a number that changes width would shift the account name next to
   it every time an alert arrives. */
.chip-count {
  position: absolute; top: -5px; right: -5px;
  min-width: 16px; height: 16px; padding: 0 4px;
  border-radius: 999px; background: var(--danger); color: #fff;
  font-size:0.75rem; font-weight:400; line-height: 16px; text-align: center;
}
.chip.ok    { border-color: #34d39944; color: var(--ok); background: var(--ok-dim); }
.chip.ok svg { color: var(--ok); }
.chip.bad   { border-color: #f8717144; color: var(--danger); background: var(--danger-dim); }
.chip.bad svg { color: var(--danger); }

/* No width cap. A 1500px cap left a dead strip down the right of every
   wide screen, and this is a control panel: the content is tables and
   cards, which USE the width - a wider Domains table is a Manage button
   that stops being cut, not a longer line of prose to read.

   The usual argument for a cap is line length, and that is answered
   where it actually applies: .page-sub is capped at 78ch, so the
   explanatory paragraphs stay readable however wide the window is. */
.content { padding: 26px 28px 60px; width: 100%; }

/* ══ Page header ══ */
.page-head { margin-bottom: 24px; }
.page-title {
  font-size:1.375rem; font-weight:400; letter-spacing: -.025em;
  margin: 0; display: flex; align-items: center; gap: 11px;
}
.page-sub { color: var(--dim); margin: 5px 0 18px; font-size:0.875rem; line-height: 1.55; max-width: 78ch; }

/* ══ Section band (ACCOUNT MANAGER etc.) ══ */
.band {
  display: flex; align-items: center; gap: 12px;
  margin: 30px 0 14px;
}
.band:first-of-type { margin-top: 4px; }
.band-label {
  font-size:0.8125rem; font-weight:400; letter-spacing: .1em;
  text-transform: uppercase; color: var(--dim);
  white-space: nowrap;
}
.band-line { flex: 1; height: 1px; background: linear-gradient(90deg, var(--border) 0%, transparent 100%); }

/* ══ Tile grid — the DirectAdmin-style launcher ══ */
.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(178px, 1fr));
  gap: 12px;
}
.tile {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  padding: 20px 14px 16px;
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  text-align: center;
  color: var(--text-2);
  transition: transform .16s cubic-bezier(.2,.8,.3,1), border-color .16s, background .16s, box-shadow .16s;
  position: relative; overflow: hidden;
}
.tile::after {
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(120% 80% at 50% 0%, var(--tile-glow, transparent) 0%, transparent 62%);
  opacity: 0; transition: opacity .18s; pointer-events: none;
}
.tile:hover {
  transform: translateY(-3px);
  border-color: var(--tile-c, var(--accent));
  background: var(--surface-2);
  box-shadow: var(--shadow);
  color: var(--text);
}
.tile:hover::after { opacity: 1; }
.tile-ic {
  width: 50px; height: 50px; border-radius: 14px;
  display: grid; place-items: center;
  background: var(--tile-bg, var(--accent-dim));
  color: var(--tile-c, var(--accent));
  transition: transform .16s;
}
.tile:hover .tile-ic { transform: scale(1.07); }
.tile-label { font-size:0.875rem; font-weight:400; line-height: 1.35; }
.tile-note { font-size:0.75rem; color: var(--faint); margin-top: -6px; }

/* Tile colour variants */
.t-teal   { --tile-c: var(--accent); --tile-bg: var(--accent-dim); --tile-glow: var(--accent-dim); }
.t-orange { --tile-c: var(--orange); --tile-bg: var(--orange-dim); --tile-glow: var(--orange-dim); }
.t-blue   { --tile-c: var(--info);   --tile-bg: var(--info-dim);   --tile-glow: var(--info-dim); }
.t-purple { --tile-c: var(--purple); --tile-bg: var(--purple-dim); --tile-glow: var(--purple-dim); }
.t-green  { --tile-c: var(--ok);     --tile-bg: var(--ok-dim);     --tile-glow: var(--ok-dim); }
.t-red    { --tile-c: var(--danger); --tile-bg: var(--danger-dim); --tile-glow: var(--danger-dim); }
.t-amber  { --tile-c: var(--warn);   --tile-bg: var(--warn-dim);   --tile-glow: var(--warn-dim); }

/* ══ Stat cards ══ */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px,1fr)); gap: 12px; margin-bottom: 8px; }
.stat {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  padding: 16px 18px;
  display: flex; align-items: center; gap: 14px;
}
.stat-ic {
  width: 40px; height: 40px; border-radius: 11px; flex-shrink: 0;
  display: grid; place-items: center;
  background: var(--tile-bg, var(--accent-dim)); color: var(--tile-c, var(--accent));
}
.stat .n { font-size:1.25rem; font-weight:400; letter-spacing: -.02em; line-height: 1.1; }
.stat .l { color: var(--dim); font-size:0.8125rem; margin-top: 2px; font-weight:400; }

/* A bigger variant for the handful of stats that are a real COUNT
   worth reading at a glance (Security Center's live blocked/banned
   figures), rather than a short status word. tabular-nums keeps the
   digits from shifting width as the count changes. */
.stat-lg .stat-ic { width: 48px; height: 48px; }
.stat-lg .n { font-size: 2rem; font-variant-numeric: tabular-nums; }
.stat-lg .l { font-size: 0.8125rem; }

/* ══ Cards ══ */
.card {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  margin-bottom: 18px;
  overflow: hidden;
}
.card-header {
  padding: 14px 18px;
  border-bottom: 1px solid var(--border-soft);
  font-weight:400; font-size:0.875rem;
  display: flex; align-items: center; gap: 9px;
  background: linear-gradient(180deg, rgba(255,255,255,.016), transparent);
}
.card-header .hint { margin-left: auto; font-weight:400; font-size:0.875rem; color: var(--faint); }
.card-body { padding: 18px; }

/* The paging line under a list. It had no styling at all, so it sat
   flush against the left edge while everything above it was inset by the
   card's padding - the sort of thing that reads as broken without being
   nameable. Matched to .card-body so it lines up with the content it
   describes. */
.pagenav {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 12px 18px; font-size:0.875rem; color: var(--dim);
  border-top: 1px solid var(--border);
}
.pagenav a { text-decoration: none; }

/* "Working on <account>", under the picker on every page that has one.

   Loud on purpose. It exists to stop somebody acting on the wrong
   account, so it has to be visible at a glance rather than blend into
   the page - the accent bar down the left is what the eye catches when
   scanning past. */
.workingon {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 0 0 14px; padding: 9px 14px;
  background: var(--surface-2); border: 1px solid var(--border);
  border-left: 3px solid var(--gold); border-radius: var(--r-sm);
  font-size:0.875rem; color: var(--text);
  /* It STAYS. Moving it higher up the page would help until the first
     scroll and then be exactly as easy to miss (operator, 2026-08-29).

     58px is the top bar, which is sticky and that tall. z-index 15 sits
     under the bar (20) and over the page. Opaque on purpose: the page
     scrolls underneath it, and --surface-2 is a solid colour in both
     themes. */
  position: sticky; top: 58px; z-index: 15;
}
/* The NAME in gold, not the whole bar.
   
   Operator's request, and the right half to colour: the account or
   domain is the one word that has to be read correctly, and colouring
   only it makes it the thing the eye lands on rather than another
   coloured panel to skim past.
   
   --gold and not --warn, though they are the same value today. Amber
   means "warning" everywhere else here: the retrying state, the warn
   buttons, the reset control. A bar that means "this is what you are
   working on" is not a warning, and when the warning colour is next
   adjusted this must not move with it. That is exactly why the two
   tokens were split. */
.workingon b { font-weight:400; color: var(--gold); }

/* A Working-on bar that belongs to one control rather than to the page.
   The backup one reflects a dropdown a few lines above it; sticking that to
   the top would carry it over content it says nothing about. */
.workingon-inline { position: static; }

/* What a setting is RIGHT NOW, beside the control that changes it.
   It was plain dim text, which reads as a hint about the control rather
   than as the live state of the site - the one thing on that card the
   reader came to check. A bordered pill with the value in normal text
   colour reads as a fact; the word "currently" stays dim because it is
   the label, not the answer. */
.state-now {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 11px;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 999px;
  font-size:0.875rem; color: var(--text); white-space: nowrap;
}
.state-now .k { color: var(--dim); font-size:0.8125rem;
                text-transform: uppercase; letter-spacing: .04em; }
.state-now .v { font-weight:400; }
.state-now .dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--ok); flex-shrink: 0;
}
.workingon-label {
  font-size:0.8125rem; letter-spacing: .04em; text-transform: uppercase;
  color: var(--dim);
}

/* Action bar for a long multi-card form. Sticks to the bottom of the
   viewport so the save control is reachable from any card, instead of
   being buried at the end of whichever card happens to be last. */
.form-actions {
  position: sticky; bottom: 0; z-index: 5;
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 12px 16px; margin: 16px 0 0;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  box-shadow: 0 -6px 18px rgba(0,0,0,.28);
}
/* The save bar sticks to the bottom of the viewport, so whatever follows
   it on the page appears to float below a shadow with a band of empty
   space between. On Service Configuration the Config files card sits
   there, and the gap read as a layout fault rather than as a section
   break. Give the next section the page's normal card rhythm instead of
   the sticky bar's incidental margin. */
.form-actions + * { margin-top: 18px; }
.form-actions-note { margin-left: auto; font-size:0.875rem; color: var(--faint); }
@media (max-width: 900px) {
  .fm-split { flex-direction: column; }
  .fm-tree {
    flex: 1 1 auto; max-width: none; position: static;
    max-height: 240px;
  }
}
@media (max-width: 620px) {
  .form-actions-note { margin-left: 0; flex-basis: 100%; }
}

/* ══ Flash ══ */
.flash {
  border-radius: var(--r-md);
  padding: 12px 16px; margin-bottom: 16px;
  font-size:0.875rem; line-height: 1.55; white-space: pre-wrap;
  /* pre-wrap is for the flashes carrying command output - a failing
     `nginx -t` is multi-line and collapsing it loses the message.
     A banner whose text is prose written across several template lines
     must opt out with .prose, or it renders with the source's line
     breaks and one indent's worth of leading spaces on every line. */
  border: 1px solid; display: flex; gap: 10px; align-items: flex-start;
}
.flash svg { flex-shrink: 0; margin-top: 1px; }
.flash.prose { white-space: normal; }
.flash.ok    { background: var(--ok-dim);     border-color:#34d39938; color:#8ee9c4; }
.flash.error { background: var(--danger-dim); border-color:#f8717138; color:#fca5a5; }
.flash.warn  { background: var(--warn-dim);   border-color:#fbbf2438; color:#fcd77f; }

/* ══ Tables ══ */
table { width: 100%; border-collapse: collapse; font-size:0.875rem; }
th {
  text-align: left; padding: 11px 18px;
  color: var(--faint); font-weight:400;
  font-size:0.75rem; text-transform: uppercase; letter-spacing: .075em;
  border-bottom: 1px solid var(--border-soft);
  background: rgba(255,255,255,.012);
}
td { padding: 13px 18px; border-bottom: 1px solid var(--border-soft); color: var(--text-2); }
tbody tr:last-child td, table tr:last-child td { border-bottom: none; }
tbody tr:hover td { background: rgba(255,255,255,.014); }
code,.mono { font-family: var(--mono); font-size:0.875rem; color: var(--text-2); }

/* ══ Forms ══ */
label { display:block; font-size:0.875rem; color:var(--dim); margin-bottom:6px; font-weight:400; }
input[type=text],input[type=password],input[type=email],input[type=number],select,textarea {
  width: 100%;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 9px 11px;
  color: var(--text); font-size:0.875rem; font-family: var(--sans);
  transition: border-color .14s, box-shadow .14s;
}
input:focus,select:focus,textarea:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-dim);
}
select {
  appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%237d8899' stroke-width='3'><polyline points='6 9 12 15 18 9'/></svg>");
  background-repeat: no-repeat; background-position: right 11px center;
  padding-right: 30px;
  /* The themed dropdown (.jsop-combo-select-btn) sets this explicitly.
     A select this project's own enhanceSelect() skips - anything with
     fewer than 2 options, e.g. "PHP version" on a server with only one
     PHP installed - stays a plain, native select with no such rule of
     its own, and reads as centered rather than left-aligned. Made
     explicit here so every native select matches the enhanced one,
     not just the ones with enough options to be wrapped. */
  text-align: left;
}
.field { margin-bottom: 14px; }

/* ══ Account/domain picker (search box that opens a real menu) ══

   Every account/domain switcher in the panel is an <input list=...>
   next to a <datalist> - chosen over a plain <select> because listing
   every account on a 2000-account server in one dropdown is not
   usable. The browser's own popup for that pairing cannot be
   restyled at all - reported live 2026-09-09, it looks like address-
   bar history, not a themed menu.

   base.html's script replaces that native popup with this one,
   reading the same <datalist> markup that was already there - no
   template carries anything new. */
/* A CEILING, not just a floor. Several of these switchers
   (ftp.html, sitestats.html, sshkeys.html, both fields on
   standby.html) never gave their own field a max-width at all -
   fine as a plain input sitting in an ordinary flex row, but this
   wrap's own dropdown copies the input's rendered width exactly, so
   a field that happened to stretch (a grid column collapsing on a
   narrower screen, same shape as the databases.html report) handed
   the menu the same stretch. Rather than hunt down and patch every
   template that is missing its own constraint - and every future one
   that forgets it too - this caps the wrap itself, so no picker
   anywhere can ever render wider than a normal field regardless of
   what, if anything, its own template set. A field that already sets
   its own narrower max-width (domains.html's 280px, trash.html's
   190px, ...) is unaffected - this is only ever a ceiling. */
/* 320px was tuned against account/domain NAME lists (short strings),
   and cut off every select whose OPTIONS are a sentence rather than a
   name - "Its own - a new empty folder for this domain",
   "This whole server - the default everyone sees" and half a dozen
   others across the panel all lost their second half to the ellipsis.
   Raised to fit the longest of those with room to spare. A page whose
   own field is narrower than this (most account/domain pickers sit in
   a grid column well under 420px) is unaffected either way - this is
   a ceiling, not a forced width, and the one page that specifically
   needed to be narrower (databases/list.html's account switcher) has
   its own tighter max-width on the surrounding field, which still
   wins. */
.jsop-combo-wrap { display: block; position: relative; max-width: 450px; }
/* input.jsop-combo-input, not the bare class: the shorthand
   `background:` on the input[type=text] rule above resets
   background-image too, and that rule's specificity (an attribute
   selector) beats a lone class - it silently ate this arrow until
   the selector was raised to match it. */
input.jsop-combo-input {
  appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%237d8899' stroke-width='3'><polyline points='6 9 12 15 18 9'/></svg>");
  background-repeat: no-repeat; background-position: right 11px center;
  padding-right: 30px;
}
.jsop-combo-menu {
  /* left/top/width are set inline, per instance, from the input's own
     getBoundingClientRect() every time it opens - "fixed" so .card's
     own "overflow: hidden" (it rounds off the header/body corners)
     cannot clip this the way "absolute" inside it did.

     z-index 200, not 90: .modal-backdrop (accounts/list.html's own
     "Manage" dialog, and every other page sharing that same class) is
     100 - a menu below that opened correctly and rendered completely
     invisible behind the dialog it was opened from, reported live
     2026-09-09 as the dropdown simply "not selectable" from a Manage
     dialog. 200 matches .term-menu, the one other floating menu in
     this codebase that already has to sit above a modal - still well
     under the confirmation dialog's own 9999, which is correct: a
     real confirm/alert should still be able to interrupt an open
     dropdown, not the other way round. */
  position: fixed; z-index: 200;
  max-height: 280px; overflow-y: auto;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--r-md); box-shadow: var(--shadow);
  padding: 5px;
}
.jsop-combo-opt {
  padding: 7px 10px; border-radius: var(--r-sm); font-size: 0.875rem;
  color: var(--text); cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.jsop-combo-opt:hover, .jsop-combo-opt.active { background: var(--surface-3); }
.jsop-combo-opt.current { color: var(--accent); }
.jsop-combo-opt.disabled { color: var(--faint); cursor: default; pointer-events: none; }
.jsop-combo-empty { padding: 8px 10px; font-size: 0.8125rem; color: var(--faint); }

/* The same menu, opened from a plain <select> instead of a typed
   field - every other <select> in the panel still popped up the
   browser's own native list, which looked like a different product
   sitting next to the picker above. This is a BUTTON standing in for
   the (still real, still what the form submits) select underneath
   it, styled like the text version above it minus the cursor - there
   is nothing to type, only something to choose. */
.jsop-combo-select-btn {
  /* The real bug behind the "PHP version shows centered" report - and,
     since this class is the ONE button in the whole panel every
     enhanced <select> in it uses, every one of them, not just that
     page: the earlier fix (text-align:left on the base `select {}`
     rule) targeted the wrong element entirely - a NATIVE select,
     which was never what was on screen here. This IS the real
     element, and `text-align` was never going to move it, because
     `button,.btn` above (the base rule every <button> in this panel
     inherits) makes every button a flex container with
     `justify-content: center` - and once a button is a flex
     container, a text node inside it becomes an anonymous flex item
     whose position is decided by `justify-content`, not `text-align`.
     `text-align: left` here was true and had no effect, the same way
     setting a font colour on an element with `visibility:hidden` is
     true and invisible. `.term-menu-item` a few hundred lines down
     already gets this right - it sets its own `justify-content`
     rather than trusting `text-align` alone - and is the pattern this
     now matches. */
  justify-content: flex-start;
  width: 100%; text-align: left; cursor: pointer;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: 9px 11px;
  color: var(--text); font-size: 0.875rem; font-family: var(--sans);
  transition: border-color .14s, box-shadow .14s;
  appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%237d8899' stroke-width='3'><polyline points='6 9 12 15 18 9'/></svg>");
  background-repeat: no-repeat; background-position: right 11px center;
  padding-right: 30px;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.jsop-combo-select-btn:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-dim);
}

.grid-2 { display:grid; grid-template-columns:1fr 1fr; gap:14px; }
.grid-3 { display:grid; grid-template-columns:repeat(3,1fr); gap:14px; }
@media (max-width: 780px) { .grid-2,.grid-3 { grid-template-columns:1fr; } }

button,.btn {
  display:inline-flex; align-items:center; justify-content:center; gap:7px;
  background: linear-gradient(180deg, var(--accent) 0%, var(--accent-2) 100%);
  color: var(--on-accent); border:none; border-radius: var(--r-sm);
  padding:9px 17px; font-size:0.875rem; font-weight:400; cursor:pointer;
  font-family:var(--sans); transition: filter .14s, transform .1s;
}
a.btn { text-decoration: none; }
/* The Generate/Show pair added beside a password box.
   Alignment only - the gap comes from whichever row it sits in. */
.genpw { display:inline-flex; gap:6px; align-items:center; vertical-align:middle; }
/* The other arrangement: a full-width field, buttons wrapping underneath,
   where they need clearing from the box above them. */
.field .genpw { margin-top:8px; }
/* ...but not when the field and its buttons are one row. The margin
   above clears buttons that WRAPPED under a full-width box; in a row it
   pushes Generate and Show 8px below the button beside them, which is
   the only reason they sat out of line with Change password. */
.inline-actions { display:flex; gap:6px; align-items:center; flex-wrap:wrap; }
.field .inline-actions .genpw { margin-top:0; }
/* btn-sm is used by nine templates and has never existed. Defined
   here as the size it reads as, so those buttons stop rendering at
   full size while their markup says small. */
.btn-sm { padding:7px 13px; font-size:0.875rem; border-radius:7px; }
/* A button label never wraps. In a flex row the form around a button
   gets squeezed to its minimum width and "Update all" breaks into two
   lines, which reads as a broken control rather than a tight layout.
   The text is the minimum width. */
button, .btn { white-space: nowrap; }
button:hover,.btn:hover { filter: brightness(1.09); }
button:active { transform: translateY(1px); }
.btn-danger { background: linear-gradient(180deg,#f87171,#ef4444); color:#2b0707; }
.btn-warn   { background: linear-gradient(180deg,#fbbf24,#f59e0b); color:#2b1a02; }
/* A toggle button whose own label already changes ("Lock"/"Unlock",
   "Allow"/"Disallow") still read as the same neutral button either
   way - a customer had to read the words carefully to know which
   state they were in. This is the ON state: applied by the same
   script that already swaps the label, alongside it, never instead
   of it - the colour is a second signal, not a replacement for the
   words. Left off (plain .btn-ghost) is the OFF/default state. */
.btn-on { background: linear-gradient(180deg,#34d399,#10b981); color:#052e1c; }
.btn-ghost {
  background: var(--surface-2); color: var(--text-2);
  border: 1px solid var(--border); font-weight:400;
}
.btn-ghost:hover { background: var(--surface-3); color: var(--text); filter:none; }
.btn-xs { padding:5px 11px; font-size:0.8125rem; border-radius:6px; }

/* ══ Firewall (UFW) rules table ══
   The Source cell carries the fail2ban ban comment, which embeds the
   entire matched request line — country, reason, method, path, status,
   user agent. Unclamped that is hundreds of characters and it stretched
   every row into a wall of text. Two lines is enough to read the IP and
   why it was banned; the full string stays in the title attribute (hover)
   and in the row's data-text, so search still matches on all of it. */
.fw-src {
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
  overflow:hidden; font-size:0.8125rem; line-height:1.55;
  overflow-wrap:anywhere; word-break:break-word;
  border:1px solid var(--line); border-radius:6px;
  padding:4px 9px; background:var(--surface-2); color:var(--text-2);
  /* Fallback for engines without -webkit-line-clamp: cap the height so a
     long comment still cannot stretch the row. */
  max-height:3.4em;
}
.fw-del {
  padding:5px 12px; font-size:0.8125rem; font-weight:400;
  border-radius:8px; white-space:nowrap;
}

/* ══ Badges ══ */
.badge {
  display:inline-block; padding:3px 9px; border-radius:999px;
  font-size:0.75rem; font-weight:400; letter-spacing:.04em; text-transform:uppercase;
}
.badge-root { background: var(--orange-dim); color: var(--orange); }
.badge-user { background: rgba(255,255,255,.06); color: var(--dim); }
.badge-warn { background: var(--warn-dim); color: var(--warn); }
.badge-ok   { background: var(--ok-dim); color: var(--ok); }
.badge-bad  { background: var(--danger-dim); color: var(--danger); }

.empty { color: var(--faint); padding: 44px 30px; text-align:center; font-size:0.875rem; }
.empty svg { display:block; margin:0 auto 12px; opacity:.4; }

/* ══ Editor / config ══ */
.editor {
  width:100%; min-height:460px;
  background:#070a0e; border:1px solid var(--border); border-radius:var(--r-md);
  padding:14px 16px; color:var(--text);
  font-family:var(--mono); font-size:0.875rem; line-height:1.65;
  resize:vertical; tab-size:4;
}
/* Editor toolbar: find, wrap, and where the cursor is. Sits above the
   text area and stays put while the text scrolls. */
.ed-tools {
  display:flex; align-items:center; gap:8px; flex-wrap:wrap;
  margin-bottom:8px;
}
.ed-tools input[type=text] {
  width:230px; padding:6px 10px; font-size:0.875rem;
  background:var(--surface-2); border:1px solid var(--border);
  border-radius:var(--r-sm); color:var(--text); font-family:var(--mono);
}
.ed-tools .ed-stat {
  margin-left:auto; font-size:0.8125rem; color:var(--dim);
  font-family:var(--mono);
}
/* Wrapping is a per-file decision the reader makes, so it is a class on
   the box rather than a rewrite of the element's own attribute. */
.editor.wrap { white-space:pre-wrap; overflow-wrap:anywhere; }

.file-list { display:flex; flex-direction:column; gap:2px; }
.file-item {
  display:flex; justify-content:space-between; align-items:center; gap:10px;
  padding:9px 12px; border-radius:var(--r-sm);
  color:var(--dim); font-size:0.875rem; font-family:var(--mono);
}
.file-item:hover { background:var(--surface-2); color:var(--text); }
.file-item.active { background:var(--accent-dim); color:var(--accent); }
.file-item .sz { font-size:0.75rem; opacity:.7; font-family:var(--sans); }
/* The file list on its own, before anything is selected. Capped so a
   short list does not stretch to the full width of a wide screen. */
.nosplit { max-width: 420px; margin-top: 18px; }
.split + .nosplit, .nosplit + .split { margin-top: 18px; }
.split { display:grid; grid-template-columns:262px 1fr; gap:18px; align-items:start; margin-top:18px; }
@media (max-width:980px){ .split { grid-template-columns:1fr; } }
.toolbar { display:flex; gap:9px; align-items:center; flex-wrap:wrap; margin-bottom:12px; }
.toolbar .path { font-family:var(--mono); font-size:0.8125rem; color:var(--dim); margin-right:auto; }

/* ══ Login ══ */
.login-wrap {
  min-height:100vh; display:grid; place-items:center; padding:20px;
  background:
    radial-gradient(900px 500px at 15% -10%, rgba(45,212,191,.10), transparent 60%),
    radial-gradient(700px 460px at 88% 108%, rgba(251,146,60,.09), transparent 60%),
    var(--bg);
}
.login-card {
  width:100%; max-width:376px;
  background:var(--surface);
  border:1px solid var(--border);
  border-radius:20px; padding:34px 32px 30px;
  box-shadow:0 24px 70px -22px rgba(0,0,0,.85);
}
.login-brand { display:flex; flex-direction:column; align-items:center; gap:13px; margin-bottom:26px; }
.login-mark {
  width:56px; height:56px; border-radius:16px; display:grid; place-items:center;
  background:linear-gradient(145deg,var(--accent),var(--accent-2));
  box-shadow:0 8px 26px -8px var(--accent-glow);
}
/* Same two-thirds proportion as the sidebar, on the 56px tile. */
.login-mark img { width:38px; height:38px; }
.login-name { font-size:1.25rem; font-weight:400; letter-spacing:-.02em; }
.login-tag { font-size:0.875rem; color:var(--faint); margin-top:-9px; }

/* ══ Service Configuration tabs ══
   A horizontal strip that scrolls rather than wraps: on a phone the tabs
   stay on one line and swipe, which keeps the page structure legible
   instead of turning the nav into four rows of buttons. */
.cfg-tabs {
  display: flex; gap: 2px; margin-bottom: 18px;
  border-bottom: 1px solid var(--border-soft);
  overflow-x: auto; scrollbar-width: thin;
}
.cfg-tab {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 9px 14px; font-size:0.875rem; font-weight:400;
  color: var(--dim); text-decoration: none; white-space: nowrap;
  border-bottom: 2px solid transparent;
}
.cfg-tab:hover { color: var(--text-2); }
.cfg-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
.cfg-tab-n {
  font-size:0.75rem; padding: 1px 6px; border-radius: 999px;
  background: var(--surface-3); color: var(--faint);
}
.cfg-tab.active .cfg-tab-n { background: var(--accent-dim, var(--surface-3)); color: var(--accent); }
/* Marks the layer that cannot break anything, so the difference between
   the two halves of a tab is visible without reading the prose.
   margin-left:auto, NOT float: .card-header is a flex container and a
   float on a flex item is ignored outright. It only looked aligned
   because it happened to be the last child. */
.cfg-safe {
  margin-left: auto; font-size:0.75rem; font-weight:400;
  color: var(--ok); text-transform: none; letter-spacing: 0;
  text-align: right;
}
/* On a narrow screen the note wraps under the title rather than
   squeezing it to nothing. */
@media (max-width: 620px) {
  .card-header { flex-wrap: wrap; }
  .cfg-safe { margin-left: 0; flex-basis: 100%; text-align: left; }
}

/* ══ File manager: bulk bar + transfer modal ══ */
.bulkbar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 10px 16px;
  background: var(--surface); border-bottom: 1px solid var(--border-soft);
  font-size:0.875rem;
}
/* The arrow on a link that leaves the panel. Small and dim: it is a
   hint about where the link goes, not part of the label. */
.ext { font-size:0.75rem; opacity: .55; vertical-align: 1px; }

/* Facts about an application, under its description in the catalogue.
   Small and quiet: they answer a question the reader may not have
   thought to ask, and must not compete with the name. */
.app-php, .app-auto {
  display: block; margin-top: 3px;
  font-size:0.8125rem; line-height: 1.4;
}
.app-php  { color: var(--warn); }
.app-auto { color: var(--ok, #4ade80); }

/* What each toolkit button does. Shown, not hidden behind a tooltip:
   a tooltip is invisible on a phone and to anyone who does not think
   to hover, which is most people reading a button for the first time. */
.wp-what {
  width: 100%; margin: 8px 0 0; padding: 8px 0 0;
  border-top: 1px solid var(--border-soft);
  font-size:0.8125rem; line-height: 1.55;
}
.wp-what dt { color: var(--text); font-weight:400; margin-top: 5px; }
.wp-what dt:first-child { margin-top: 0; }
.wp-what dd { margin: 0 0 0 2px; color: var(--dim); }

/* File manager: folder tree beside the listing. */
.fm-split { display: flex; gap: 14px; align-items: flex-start; }
.fm-tree {
  flex: 0 0 240px; max-width: 240px;
  position: sticky; top: 14px;
  max-height: calc(100vh - 40px); display: flex; flex-direction: column;
}
/* The listing must be allowed to shrink. Without min-width:0 a long
   file name makes a flex item refuse to go below its content width,
   and the table pushes the page sideways instead of scrolling. */
.fm-list { flex: 1 1 auto; min-width: 0; }
.fm-tree-body { overflow: auto; padding: 6px 0 10px; }
.fm-tw {
  display: block; padding: 5px 10px 5px 8px;
  font-size:0.875rem; color: var(--text); text-decoration: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  border-left: 2px solid transparent;
}
.fm-tw:hover { background: var(--surface-3); }
.fm-tw.on {
  background: var(--surface-3); border-left-color: var(--warn);
  font-weight:400;
}
.fm-tw-i { display: inline-block; width: 14px; color: var(--dim); }
.fm-tw.open > .fm-tw-i { color: var(--warn); }
.fm-tw-none { padding: 8px 12px; font-size:0.875rem; color: var(--dim); }
.fm-dest { padding: 5px 7px; font-size:0.875rem; min-width: 240px; }

.fm-overlay {
  position: fixed; inset: 0; z-index: 80;
  background: rgba(5,7,10,.6); backdrop-filter: blur(3px);
  display: grid; place-items: center;
}
.fm-modal {
  width: min(440px, calc(100vw - 40px));
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 14px; padding: 20px 22px;
  box-shadow: 0 18px 60px -18px rgba(0,0,0,.8);
}
.fm-title { font-size:1rem; font-weight:400; margin-bottom: 6px; }
.fm-body { font-size:0.875rem; color: var(--dim); word-break: break-word; min-height: 18px; }
.fm-progress {
  height: 8px; border-radius: 999px; background: var(--surface-3);
  overflow: hidden; margin-top: 14px;
}
.fm-bar {
  height: 100%; width: 0; border-radius: 999px;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  transition: width .15s;
}
.fm-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }

/* Guided settings are a three-column table: label, current value,
   control. Below this width the control is wider than its column and
   the row becomes unreadable, so the table stacks instead. */
@media (max-width: 900px) {
  .cfg-rows, .cfg-rows tr, .cfg-rows td, .cfg-rows th { display: block; width: 100%; }
  .cfg-rows thead { display: none; }
  .cfg-rows tr { padding: 12px 16px; border-bottom: 1px solid var(--border-soft); }
  .cfg-rows td { padding: 2px 0; }
  .cfg-rows td:last-child { text-align: left !important; margin-top: 8px; }
}

/* ══ Small screens ══
   The sidebar used to be display:none below 1080px with no button to
   bring it back — on a phone the panel had NO navigation at all. It is
   now an off-canvas drawer: hidden by default, slid in by the hamburger
   in the topbar, dismissed by the backdrop, the × or Escape. */
.nav-burger { display: none; }
@media (max-width: 1080px) {
  .nav-burger {
    display: inline-flex; align-items: center; justify-content: center;
    width: 38px; height: 38px; flex-shrink: 0;
    /* The shared `button` rule sets padding:9px 17px. With
       box-sizing:border-box that leaves 38 - 34 - 2 = 2px of content
       width, and the icon rendered as a dot. A fixed-size icon button
       has to state its own padding. */
    padding: 0;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: 10px; color: var(--text-2); cursor: pointer;
  }
  /* SVGs shrink as flex items; the codebase already does this for every
     other icon inside a flex container. */
  .nav-burger svg { flex: 0 0 auto; width: 18px; height: 18px; }
  .nav-burger:hover { color: var(--text); }
  .sidebar {
    position: fixed; left: 0; top: 0; z-index: 60;
    height: 100vh; height: 100dvh;
    transform: translateX(-104%);
    transition: transform .22s ease;
    box-shadow: 12px 0 40px -18px rgba(0,0,0,.7);
  }
  body.nav-open .sidebar { transform: translateX(0); }
  .nav-backdrop {
    display: none; position: fixed; inset: 0; z-index: 55;
    background: rgba(5,7,10,.55); backdrop-filter: blur(2px);
  }
  body.nav-open .nav-backdrop { display: block; }
  /* The drawer covers content, so the page must not also scroll. */
  body.nav-open { overflow: hidden; }
  .topbar { padding: 0 14px; }
}
/* Wide tables scroll inside a WRAPPER, at every width, automatically.

   Three earlier attempts put overflow on the table itself behind a
   breakpoint - 720px, then 1280px - and each was still clipped one size
   up, because the width at which a table overflows depends on the table,
   not on the screen. Unconditional is the answer.

   But it must not touch the TABLE: display:block stops a table being a
   table, so its columns size to their content and a wide screen gets a
   half-empty card with the text bunched to the left. The wrapper carries
   the overflow and the table stays exactly as it was, filling its card
   when there is room and scrolling inside the wrapper when there is not.

   overflow-x:auto shows nothing at all until the content is genuinely
   wider, so this costs nothing on a big screen and needs no breakpoint
   to switch it on when the browser is resized. */
.tablewrap { overflow-x: auto; scrollbar-width: thin; }
@media (max-width: 720px) {
  .content { padding:18px 16px 50px; }
  .tiles { grid-template-columns:repeat(auto-fill,minmax(146px,1fr)); }
  /* (wide-table scrolling moved to its own query below - 720px was
     too narrow: the Domains table was clipped at laptop widths.) */
  .top-right .chip { max-width: 130px; overflow: hidden; }
  .page-title { font-size:1.25rem; }
}

/* The gauge rules now live with the dashboard block further down. The
   old .gauge sizing was left here after the Server row was removed and
   would have won on specificity, shrinking the new rings and rotating
   their labels sideways. */

/* Bar meters, for values where a ring is overkill */
.meter { height: 6px; border-radius: 999px; background: var(--surface-3); overflow: hidden; margin-top: 7px; }
.meter > span { display: block; height: 100%; border-radius: 999px; background: var(--m-c, var(--accent)); transition: width .5s ease; }

/* Software / spec rows */
.spec-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px,1fr)); gap: 0 26px; }
.spec {
  display: flex; align-items: baseline; gap: 12px;
  padding: 9px 0;
  border-bottom: 1px solid var(--border-soft);
  font-size:0.875rem;
}
.spec:last-child { border-bottom: none; }
.spec-k { color: var(--dim); font-size:0.875rem; min-width: 108px; flex-shrink: 0; }
.spec-v { color: var(--text); font-weight:400; word-break: break-word; }
.spec-v .mono { color: var(--text); }
.ver-pill {
  display: inline-block; padding: 1px 7px; margin-right: 4px;
  border-radius: 5px; font-size:0.75rem; font-weight:400;
  background: var(--accent-dim); color: var(--accent);
  font-family: var(--mono);
}

/* ══ Services page ════════════════════════════════════════════════════ */
/* margin-bottom, for the same reason .gauge-row needed margin-top: a
   grid has no margin of its own, so the card that follows it sat flush
   against the last row of service cards and the two read as one block. */
.svc-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 12px;
  margin-bottom: 22px;
}
.svc {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  padding: 15px 16px;
  display: flex; flex-direction: column; gap: 12px;
  transition: border-color .15s;
}
.svc:hover { border-color: var(--border); }
.svc.is-down { border-color: #f8717133; background: linear-gradient(180deg, var(--danger-dim), var(--surface) 60%); }
.svc-top { display: flex; align-items: flex-start; gap: 11px; }
.svc-dot {
  width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; margin-top: 5px;
  background: var(--danger);
  box-shadow: 0 0 0 3px var(--danger-dim);
}
.svc.is-up .svc-dot { background: var(--ok); box-shadow: 0 0 0 3px var(--ok-dim); }
.svc-name { font-size:0.875rem; font-weight:400; line-height: 1.25; }
.svc-unit { font-family: var(--mono); font-size:0.8125rem; color: var(--faint); margin-top: 2px; }
.svc-state { margin-left: auto; text-align: right; flex-shrink: 0; }
.svc-actions { display: flex; gap: 6px; flex-wrap: wrap; }
.svc-actions form { display: inline-flex; }
.svc-actions button { flex: 0 0 auto; }

.ws-picker { display: flex; gap: 10px; flex-wrap: wrap; }
.ws-opt {
  flex: 1 1 190px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 14px;
  cursor: pointer;
  display: flex; align-items: center; gap: 11px;
  transition: border-color .15s, background .15s;
  position: relative;
}
.ws-opt:hover { border-color: var(--accent); background: var(--surface-2); }
.ws-opt:has(input:checked), .ws-opt.is-active {
  border-color: var(--accent); background: var(--accent-dim);
}
.ws-opt:has(input:checked) .ws-ic, .ws-opt.is-active .ws-ic {
  background: var(--accent-dim); color: var(--accent);
}
.ws-opt input:focus-visible ~ * { outline: 2px solid var(--accent); outline-offset: 3px; }
.ws-opt input { position: absolute; opacity: 0; width: 0; height: 0; }
.ws-opt { user-select: none; }
.ws-opt.is-active { border-color: var(--accent); background: var(--accent-dim); }
.ws-ic {
  width: 36px; height: 36px; border-radius: 10px; flex-shrink: 0;
  display: grid; place-items: center;
  background: var(--surface-3); color: var(--dim);
}
.ws-opt.is-active .ws-ic { background: var(--accent-dim); color: var(--accent); }
.ws-name { font-size:0.875rem; font-weight:400; }
.ws-meta { font-size:0.8125rem; color: var(--faint); margin-top: 1px; }

/* ══ Impersonation banner ═════════════════════════════════════════════ */
.imp-bar {
  position: sticky; top: 0; z-index: 60;
  background: linear-gradient(90deg, var(--orange) 0%, #f97316 100%);
  color: #2b1402;
  display: flex; align-items: center; gap: 12px;
  padding: 9px 20px;
  font-size:0.875rem; font-weight:400;
}
.imp-bar svg { flex-shrink: 0; }
.imp-bar .who { font-weight:400; }
.imp-bar form { margin-left: auto; }
.imp-bar button {
  background: rgba(0,0,0,.28); color: #fff;
  border: 1px solid rgba(0,0,0,.2);
  padding: 5px 13px; font-size:0.875rem; font-weight:400; border-radius: 6px;
}
.imp-bar button:hover { background: rgba(0,0,0,.42); filter: none; }

/* ══ Impersonation banner ═════════════════════════════════════════════ */
.imp-bar {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; gap: 12px;
  padding: 9px 24px;
  background: linear-gradient(90deg, var(--orange) 0%, #f97316 100%);
  color: #2a1405;
  font-size:0.875rem; font-weight:400;
  box-shadow: 0 2px 12px -3px rgba(251,146,60,.5);
}
.imp-bar svg { flex-shrink: 0; }
.imp-bar form { margin-left: auto; }
.imp-bar button {
  background: rgba(0,0,0,.22); color: #fff;
  border: 1px solid rgba(0,0,0,.25);
  padding: 5px 13px; font-size:0.875rem; border-radius: 7px; font-weight:400;
}
.imp-bar button:hover { background: rgba(0,0,0,.34); filter: none; }

/* Role tabs are buttons so they can switch view mode without navigating. */
button.role-tab {
  background: none; border: none; border-bottom: 2px solid transparent;
  border-radius: 0; cursor: pointer; font-family: var(--sans);
}
button.role-tab:hover { filter: none; background: none; color: var(--text-2); }
button.role-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
button.role-tab:active { transform: none; }

/* Tuning category markers — colour carries the service group, so the
   sections are distinguishable at a glance while scrolling. */
.cat-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--cat-c, var(--accent));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--cat-c, var(--accent)) 18%, transparent);
  flex-shrink: 0;
}
.cat-server   { --cat-c: var(--accent); }
.cat-code     { --cat-c: var(--purple); }
.cat-database { --cat-c: var(--info); }
.cat-mail     { --cat-c: var(--orange); }
.cat-shield   { --cat-c: var(--danger); }
.cat-zap      { --cat-c: var(--warn); }
.cat-globe    { --cat-c: var(--ok); }
.cat-folder   { --cat-c: var(--info); }
.cat-cpu      { --cat-c: var(--gold); }
.cat-sliders  { --cat-c: var(--dim); }

/* ══ Sub-navigation tabs (Security Center, etc.) ══════════════════════ */
.subtabs {
  display: flex; gap: 4px; flex-wrap: wrap;
  border-bottom: 1px solid var(--border-soft);
  margin-bottom: 20px;
}
.subtab {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 10px 15px;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  color: var(--dim);
  font-size:0.875rem; font-weight:400;
  border-radius: 8px 8px 0 0;
  transition: color .14s, background .14s, border-color .14s;
}
.subtab:hover { color: var(--text); background: var(--surface); }
/* A tab that switches a pane in place is a button, not a link - it goes
   nowhere and must not be dragged to a new window or opened in a tab.
   The class was written for anchors, so the browser's button defaults
   have to be cleared or the row renders as five grey boxes. */
button.subtab {
  background: none; border: none; border-bottom: 2px solid transparent;
  font: inherit; font-size:0.875rem; font-weight:400;
  cursor: pointer;
}
button.subtab.active { background: var(--accent-dim); }
.subtab.active { color: var(--accent); border-bottom-color: var(--accent); background: var(--accent-dim); }
.subtab .dot {
  width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
  background: var(--st-c, var(--faint));
  box-shadow: 0 0 0 2.5px color-mix(in srgb, var(--st-c, var(--faint)) 20%, transparent);
}
.subtab .cnt {
  font-size:0.75rem; font-weight:400; color: var(--faint);
  background: var(--surface-2); border-radius: 999px; padding: 1px 7px;
}
.st-ok   { --st-c: var(--ok); }
.st-warn { --st-c: var(--warn); }
.st-bad  { --st-c: var(--danger); }
.st-off  { --st-c: var(--faint); }

/* ══ Application catalogue ════════════════════════════════════════════ */
.app-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(215px, 1fr));
  gap: 9px;
}
.app-opt {
  border: 1px solid var(--border); border-radius: var(--r-md);
  padding: 12px 14px; cursor: pointer;
  display: flex; flex-direction: column; gap: 3px;
  transition: border-color .14s, background .14s;
}
.app-opt:hover { border-color: var(--accent); background: var(--surface-2); }
.app-opt input { position: absolute; opacity: 0; width: 0; height: 0; }
.app-opt:has(input:checked) { border-color: var(--accent); background: var(--accent-dim); }
.app-name { font-size:0.875rem; font-weight:400; color: var(--text); }
.app-desc { font-size:0.8125rem; color: var(--dim); line-height: 1.45; }

/* ══ WordPress toolkit ════════════════════════════════════════════════ */
.wp-sites { display: flex; flex-direction: column; gap: 12px; padding: 14px 18px 18px; }
.wp-site {
  border: 1px solid var(--border-soft); border-radius: var(--r-lg);
  background: var(--surface-2); overflow: hidden;
}
.wp-site-head {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: 16px; padding: 15px 17px; flex-wrap: wrap;
  border-bottom: 1px solid var(--border-soft);
}
.wp-site-name { font-size:1rem; font-weight:400; }
.wp-site-meta { font-size:0.875rem; color: var(--dim); margin-top: 3px; }
.wp-site-path { font-size:0.8125rem; color: var(--faint); margin-top: 3px; }
/* One group of buttons - Updates, Staging, Maintenance - with its own
   explanation underneath and a LINE between it and the next.

   The line was var(--border-soft) at #1b2331, which against the card
   is very nearly invisible: the three groups ran together and read as
   one long list. var(--border) is the colour used for a real edge.

   More room top and bottom for the same reason. A separator with no
   space around it separates nothing. */
.wp-actions {
  display: flex; align-items: flex-start; gap: 6px; flex-wrap: wrap;
  padding: 14px 17px 16px;
  border-bottom: 1px solid var(--border);
}
/* The last group has the card edge below it already. */
.wp-actions:last-of-type { border-bottom: 0; }
.wp-group-label {
  font-size:0.75rem; font-weight:400; letter-spacing: .06em;
  text-transform: uppercase; color: var(--faint);
  min-width: 84px; flex-shrink: 0;
}
/* A firmer line between groups than between a group and its own help. */

/* The label is a heading for the group, so it sits at the top rather
   than floating in the middle of a wrapped row of buttons. */
.wp-group-label { padding-top: 5px; }

.wp-plugins { padding: 12px 17px; border-bottom: 1px solid var(--border-soft); }
/* The plugin list: a thing you act on, not a report.

   Each row carries its own buttons, so one plugin can be updated
   without touching the rest - which is the whole reason for it. */
.plugin-table { width:100%; font-size:12.5px; border-collapse:collapse; }
.plugin-table th {
  text-align:left; font-weight:400; color:var(--faint);
  font-size:0.75rem; text-transform:uppercase; letter-spacing:.05em;
  padding:0 8px 6px 0; border-bottom:1px solid var(--border);
}
.plugin-table td { padding:7px 8px 7px 0; border-bottom:1px solid var(--border-soft); }
.plugin-table tr:last-child td { border-bottom:0; }
/* The buttons sit right and stay on one line, so the names line up. */
.plugin-acts { text-align:right; white-space:nowrap; }
.plugin-acts form { margin-left:4px; }

/* Installing one by name, under the table. */
/* Installing a plugin by name.

   Its own panel rather than a row of controls: it is a different kind
   of thing from the table above it - that lists what is there, this
   adds something new - and running them together read as one long
   list of half-related controls.

   The input carries type=text in the markup. Without it the rule
   above matches nothing and the browser draws its own white box in
   the middle of a dark panel. */
.plugin-add {
  display:flex; align-items:center; gap:10px; flex-wrap:wrap;
  margin-top:14px; padding:12px;
  background:var(--bg); border:1px solid var(--border);
  border-radius:var(--r-sm);
  font-size:0.8125rem;
}
.plugin-add::before {
  content:"Add a plugin";
  flex-basis:100%;
  color:var(--faint); font-size:0.75rem;
  text-transform:uppercase; letter-spacing:.05em;
}
/* Sized here, not in a style attribute on the element. */
.plugin-add input[type=text] { width:15rem; padding:7px 10px; }
.plugin-add label {
  display:flex; align-items:center; gap:6px; color:var(--dim);
  white-space:nowrap;
}
.plugin-add label input { margin:0; }
/* The hint drops to its own line rather than squeezing the controls. */
.plugin-add .muted { flex-basis:100%; font-size:0.75rem; }

.wp-plugins .plugin-box {
  background: var(--surface); border-radius: 10px; padding: 12px; font-size:0.875rem;
}
.wp-danger { padding: 12px 17px; background: var(--danger-dim); }

/* ══ Terminal ═════════════════════════════════════════════════════════ */
#term {
  background: #05070a;
  color: #d6dde7;
  font-family: var(--mono);
  font-size:0.875rem;
  line-height: 1.5;
  padding: 14px 16px;
  min-height: 460px;
  max-height: 70vh;
  overflow-y: auto;
  white-space: pre-wrap;
  word-break: break-word;
  outline: none;
  cursor: text;
  border-radius: 0 0 var(--r-lg) var(--r-lg);
}
#term:focus { box-shadow: inset 0 0 0 1px var(--accent); }

/* The block cursor.

   The page drew no cursor at all, so correcting a typo meant guessing
   where the next character would land. The emulator has always known -
   it has to, or a redraw goes to the wrong place - and now the page
   renders it.

   Filled while the terminal has the keyboard, hollow when it does not.
   That difference is the point: a solid cursor on a panel that is not
   listening tells you your typing is going somewhere it is not. */
#term .term-cursor {
  background: var(--accent);
  color: #05070a;
  border-radius: 1px;
}
#term:focus .term-cursor {
  animation: term-blink 1.1s steps(1, end) infinite;
}
#term:not(:focus) .term-cursor {
  background: transparent;
  color: inherit;
  box-shadow: inset 0 0 0 1px var(--accent);
}
@keyframes term-blink {
  0%, 55%   { opacity: 1; }
  55.01%, 100% { opacity: 0.28; }
}
/* A cursor that blinks for somebody who asked for less motion is the
   one animation on the page they cannot look away from. */
@media (prefers-reduced-motion: reduce) {
  #term:focus .term-cursor { animation: none; }
}

/* An application's own output, on the Node apps page.

   Scrolls inside itself rather than stretching the page: two hundred
   lines of a crash trace would otherwise push every control off the
   bottom of the screen. Long lines wrap, because a stack trace with a
   sideways scrollbar is one nobody reads to the end. */
.logbox {
  margin: 0;
  max-height: 420px;
  overflow: auto;
  padding: 12px 14px;
  background: #05070a;
  color: #d6dde7;
  border-radius: var(--r-md);
  font-family: var(--mono);
  font-size: 0.8125rem;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
}

/* The terminal's own right-click menu.

   The browser offers Paste only on an editable field, so on the
   terminal the native menu had no Paste at all. Ours does. */
.term-menu {
  position: fixed;
  z-index: 200;
  min-width: 190px;
  padding: 5px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow);
  font-family: var(--sans);
  font-size: 0.875rem;
}
.term-menu-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  width: 100%;
  padding: 7px 10px;
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.term-menu-item:hover:not(:disabled),
.term-menu-item:focus-visible {
  background: var(--surface-3);
  outline: none;
}
/* Greyed rather than hidden: a menu that changes shape between
   clicks makes people hunt for the item that moved. */
.term-menu-item:disabled {
  color: var(--faint);
  cursor: default;
}
.term-menu-key {
  color: var(--faint);
  font-family: var(--mono);
  font-size: 0.75rem;
}

/* ══ Modal dialog ═════════════════════════════════════════════════════ */
/* The previous dialog grew past the viewport with no way to reach the
   bottom. Here the panel is height-capped and only the BODY scrolls, so
   the title and the save button stay visible however much is added. */
.modal-backdrop {
  position: fixed; inset: 0; z-index: 100;
  background: rgba(2, 5, 9, .72);
  backdrop-filter: blur(3px);
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
}
.modal-panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  width: 100%; max-width: 620px;
  max-height: calc(100vh - 48px);
  display: flex; flex-direction: column;
  box-shadow: 0 24px 60px rgba(0, 0, 0, .55);
  overflow: hidden;
}
.modal-head {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: 16px; padding: 18px 22px 14px;
  border-bottom: 1px solid var(--border-soft); flex-shrink: 0;
}
.modal-title { font-size:1rem; font-weight:400; }
.modal-sub { font-size:0.8125rem; color: var(--dim); margin-top: 3px; }
.modal-x {
  background: none; border: none; color: var(--faint);
  font-size:1.375rem; line-height: 1; cursor: pointer; padding: 0 4px;
}
.modal-x:hover { color: var(--text); }

.modal-tabs {
  display: flex; gap: 2px; padding: 0 16px;
  border-bottom: 1px solid var(--border-soft); flex-shrink: 0;
  overflow-x: auto;
}
.modal-tab {
  background: none; border: none; cursor: pointer;
  padding: 10px 14px; font-size:0.875rem; font-weight:400;
  color: var(--dim); border-bottom: 2px solid transparent;
  white-space: nowrap;
}
.modal-tab:hover { color: var(--text-2); }
.modal-tab.active { color: var(--accent); border-bottom-color: var(--accent); }

/* The only scrolling region. */
.modal-body { padding: 18px 22px; overflow-y: auto; flex: 1; min-height: 0; }
.modal-pane { animation: paneIn .12s ease; }
@keyframes paneIn { from { opacity: 0; transform: translateY(3px); } }

.modal-foot {
  display: flex; justify-content: flex-end; gap: 10px;
  padding: 14px 22px; border-top: 1px solid var(--border-soft);
  background: var(--surface-2); flex-shrink: 0;
}
.field-hint { font-size:0.8125rem; color: var(--faint); line-height: 1.55; margin-top: 5px; }
.check-row {
  display: flex; align-items: flex-start; gap: 9px;
  font-size:0.875rem; color: var(--text-2); margin-bottom: 15px;
}
.check-row input[type=checkbox] { width: auto; margin-top: 2px; flex-shrink: 0; }
.check-row .field-hint { display: block; margin-top: 3px; }

/* ══ Database engine marks ════════════════════════════════════════════ */
/* Colour-coded so the engine is identifiable at a glance, rather than by
   reading the button at the far end of the row. */
.db-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 27px; height: 27px; border-radius: 8px; flex-shrink: 0;
}
.db-mariadb    { background: rgba(0, 130, 155, .16);  color: #38bec9; }
.db-postgresql { background: rgba(51, 103, 145, .18); color: #6ba3d6; }
.db-mongodb    { background: rgba(77, 179, 61, .15);  color: #66c458; }

/* Service version line */
.svc-ver { font-size:0.8125rem; color: var(--dim); margin-top: 3px;
           display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }

/* ══ Dashboard: system chips, gauges, network ═════════════════════════ */
/* Hardware facts as labelled chips — identifiable at a glance rather
   than read as prose. */
.chip-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
             gap: 9px; }
.chip { display: flex; justify-content: space-between; align-items: center; gap: 12px;
        background: var(--surface-2); border: 1px solid var(--border-soft);
        border-radius: 9px; padding: 10px 13px; min-width: 0; }
.chip-wide { grid-column: span 2; }
@media (max-width: 700px) { .chip-wide { grid-column: span 1; } }
.chip-k { font-size:0.75rem; letter-spacing: .08em; color: var(--faint);
          font-weight:400; white-space: nowrap; }
.chip-v { font-size:0.875rem; color: var(--text); font-weight:400;
          text-align: right; overflow: hidden; text-overflow: ellipsis; }

/* margin-top matters: the gauges used to follow the System info card,
   which supplied the gap. With the action tiles above them instead they
   sat flush against the last row of tiles, reading as one block. */
.gauge-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
             gap: 14px; margin: 26px 0 14px; }
.gauge-body { display: flex; align-items: center; justify-content: space-between; gap: 14px; }
.gauge-side { min-width: 0; }
.gauge-fig { font-size:1.25rem; font-weight:400; letter-spacing: -.02em; }
.gauge-sub { font-size:0.8125rem; color: var(--dim); margin-top: 2px; }

.gauge { width: 108px; height: 108px; flex-shrink: 0; transform: rotate(-90deg); }
.gauge-track { fill: none; stroke: var(--border); stroke-width: 9; }
.gauge-fill { fill: none; stroke: var(--accent); stroke-width: 9; stroke-linecap: round;
              transition: stroke-dasharray .5s ease; }
.gauge-fill.is-warm { stroke: var(--warn); }
.gauge-fill.is-hot  { stroke: var(--danger); }
/* Counter-rotate the text so the ring can start at twelve o'clock. */
.gauge-num { fill: var(--text); font-size:1.375rem; font-weight:400; text-anchor: middle;
             transform: rotate(90deg); transform-origin: 60px 60px; }
.gauge-pct { fill: var(--dim); font-size:0.8125rem; text-anchor: middle;
             transform: rotate(90deg); transform-origin: 60px 60px; }

.net-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; }
.net-k { font-size:0.75rem; letter-spacing: .08em; color: var(--faint);
         font-weight:400; margin-bottom: 5px; }
.net-v { font-size:1rem; font-weight:400; letter-spacing: -.01em; }
.copyable { cursor: pointer; }
.copyable:hover { color: var(--accent); }
.copyable.copied { color: var(--ok); }

/* Busy overlay for long-running actions (see base.html). */
#busy {
    position: fixed; inset: 0; z-index: 9999;
    display: none; align-items: center; justify-content: center;
    background: rgba(6, 10, 15, .72);
    backdrop-filter: blur(2px);
}
#busy.on { display: flex; }
.busy-box {
    background: var(--panel, #131c26);
    border: 1px solid var(--line, #223040);
    border-radius: 14px;
    padding: 26px 42px 30px;
    text-align: center;
    min-width: 320px;
    box-shadow: 0 18px 50px rgba(0, 0, 0, .5);
}
.busy-title { font-size:1rem; font-weight:400; margin-bottom: 7px; }
.busy-sub   { font-size:0.875rem; color: var(--dim, #8b9bab); margin-bottom: 16px; }
.busy-time  { font-size:1.375rem; font-weight:400; color: var(--accent, #2dd4bf); }

/* CLI output, sometimes long: scrolls inside itself rather than growing
   until it covers the page. */
@media (max-width: 620px) {
    }

/* ══ Toast ══════════════════════════════════════════════════════════
   Feedback that does not move the page.

   Every form in this panel posts and redirects. A message rendered in
   the flow therefore pushed the content down and sent the operator to
   the top of the page to read it, then back down to carry on - once per
   action. Fixed to the corner instead, with the scroll position
   restored, so the page is exactly where it was left.

   Defined once and used by every page through base.html, so the place
   to look for a result is the same everywhere. */
.toast {
  position: fixed; right: 18px; bottom: 18px; z-index: 60;
  width: min(520px, calc(100vw - 36px));
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-left-width: 3px;
  border-radius: var(--r-lg);
  box-shadow: 0 12px 38px rgba(0,0,0,.5);
  overflow: hidden;
  animation: toast-in .18s ease-out;
  transition: opacity .18s ease, transform .18s ease;
}
/* A live indicator: something is arriving on its own, without the
   reader pressing anything. The card said "Live feed" and looked
   exactly like every other card, so there was nothing to tell a
   moving list from a still one. */
.live-dot { display: inline-flex; align-items: center; gap: 6px;
            font-size:0.8125rem; font-weight:400; letter-spacing: .04em;
            text-transform: uppercase; color: var(--ok); margin-left: 8px; }
.live-dot::before { content: ""; width: 7px; height: 7px; border-radius: 50%;
                    background: var(--ok); box-shadow: 0 0 0 0 var(--ok);
                    animation: live-pulse 2s ease-out infinite; }
@keyframes live-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(16,185,129,.55); opacity: 1; }
  70%  { box-shadow: 0 0 0 6px rgba(16,185,129,0); opacity: .85; }
  100% { box-shadow: 0 0 0 0 rgba(16,185,129,0); opacity: 1; }
}
/* Someone who has asked for less motion gets a plain dot. */
@media (prefers-reduced-motion: reduce) {
  .live-dot::before { animation: none; }
}

@keyframes toast-in {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* The colour says which kind of message it is before the words do. */
.toast[data-ok="1"] { border-left-color: var(--ok, #34d399); }
.toast[data-ok="0"] { border-left-color: var(--danger, #f87171); }

.toast-head {
  display: flex; align-items: center; gap: 8px;
  padding: 9px 12px;
  border-bottom: 1px solid var(--border-soft);
  background: rgba(255,255,255,.02);
}
.toast-head svg { flex: 0 0 auto; }
.toast[data-ok="1"] .toast-head { color: var(--ok, #34d399); }
.toast[data-ok="0"] .toast-head { color: var(--danger, #f87171); }
.toast-title {
  font-size:0.875rem; font-weight:400;
  letter-spacing: .04em; text-transform: uppercase;
}
.toast-x {
  margin-left: auto; background: none; border: 0; cursor: pointer;
  color: var(--faint); font-size:1rem; line-height: 1;
  padding: 0 2px; transition: color .12s ease;
}
.toast-x:hover { color: var(--text); }

/* Tinted rather than white: easier to read against the dark surface,
   and it keeps the message tied to its heading. */
.toast-body {
  margin: 0; padding: 11px 13px;
  font-size:0.875rem; line-height: 1.6;
  white-space: pre-wrap; word-break: break-word;
  max-height: 42vh; overflow-y: auto;
}
.toast[data-ok="1"] .toast-body { color: #a7ead0; }
.toast[data-ok="0"] .toast-body { color: #fcb4b4; }
/* Command output keeps the monospace it was written in. */
.toast-body.mono { font-size:0.875rem; }

@media (max-width: 620px) {
  .toast { right: 10px; left: 10px; bottom: 10px; width: auto; }
  .toast-body { max-height: 50vh; }
}
/* Movement is the thing that draws the eye; respect a request not to. */
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; transition: none; }
}

/* Written into help text all over the panel, and a browser renders them
   at 700 - heavier than any heading here. */
/* The ONLY weights above regular in the whole panel. Everything else is
   400 - hierarchy comes from size and colour, which is what makes the
   reference the operator compared us against read as light and sharp.
   Every step past this was tried: 700-multiple, then 600, then 500
   scattered through the templates, and each still read as heavy. */
b, strong { font-weight: 500; }
h1, .card-header { font-weight: 500; }

/* (h1 weight is set beside b/strong above.) */

/* Inter, served from this server.

   It was already third in the stack and almost never used, because it
   is not installed on the machines that VIEW the panel - it fell
   through to Segoe UI. A font has to be delivered to the browser, so
   naming it achieves nothing on its own.

   Served by us rather than fetched from a font CDN: a control panel
   must work on a machine with no route to the public internet, and a
   CDN would also tell a third party which of our customers opened
   which page.

   If the file is absent the browser simply uses the next family in
   the stack, so a server that could not install it still renders. */
/* Inter Variable, if the file is there.

   One file carrying the whole 100-900 range, rather than three fixed
   cuts. It renders a weight exactly rather than rounding to the nearest
   file, and it is about 350KB against 1.8MB for the three statics.

   Declared FIRST so it wins when present. Debian only packages the
   static OTFs, so this file has to be put on the server by hand; if it
   is absent the browser falls straight through to the three below and
   nothing changes. That is why this is safe to ship before the file
   exists. */
@font-face {
  font-family: "InterLocal";
  src: url("/static/fonts/InterVariable.woff2") format("woff2-variations"),
       url("/static/fonts/InterVariable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* Large text set at a normal weight looks loose; a touch of negative
   tracking is what makes a heading read as sharp rather than wide. */
h1, h2, h3, .card-header { letter-spacing: -0.01em; }

/* ── Classes the templates use, defined here rather than nowhere ──

   Every one of these was in use and defined in no stylesheet, so the
   element fell back to browser defaults: valid markup, a template
   that compiles, a page that renders, and a silent difference from
   the page next to it. It is the CSS shape of an action missing from
   the allow-list - a name used on one side and never defined on the
   other, with nothing to report it.

   `scratchpad/checkers/ghostclass.py` now lists them. */

/* Text that explains, rather than states.

   Used 385 times across the panel and defined nowhere, so every
   explanatory line rendered at full size and full contrast - the same
   weight as the thing it was explaining, which is why pages read as
   though the body text was louder than the heading. Authors had begun
   compensating in place (`style="font-size:0.8125rem;font-weight:400"`
   in backup.html), which is the tell: a class everybody uses and
   nobody can rely on. */
.muted { color: var(--dim); font-size: 0.8125rem; }
/* A paragraph of it needs room to breathe; a span sitting inside a
   line does not, so only the block form takes spacing. */
p.muted { line-height: 1.55; margin-top: 6px; }

/* A heading INSIDE a card, under the card's own header bar.

   Not the same thing as `.card-header`, which is that bar. And
   deliberately not an <h2>: browsers render one at 1.5rem bold, which
   is what made one page tower over every other while the stylesheet
   said nothing about it either way. */
.card-head {
  font-size: 0.875rem; font-weight: 500;
  color: var(--text); letter-spacing: -0.01em;
}

/* Output from a command or the daemon, shown back to the reader.
   Wraps rather than scrolling sideways: a page that scrolls
   horizontally to read an error message is a page nobody reads the
   error message on. */
.out {
  font-family: var(--mono); font-size: 0.8125rem; line-height: 1.5;
  white-space: pre-wrap; word-break: break-word;
  background: var(--surface-2); border: 1px solid var(--border-soft);
  border-radius: var(--r-md); padding: 11px 13px; margin: 0;
  max-height: 340px; overflow: auto; color: var(--text-2);
}

/* An aside inside a card: something true and worth knowing that is
   not what the reader came for. */
.note {
  font-size: 0.8125rem; line-height: 1.55; color: var(--text-2);
  background: var(--surface-2); border-left: 3px solid var(--border);
  border-radius: var(--r-sm); padding: 10px 13px;
}
.note.warn { border-left-color: var(--warn); background: var(--warn-dim); }
/* A note about something that FAILED, as opposed to something
   switched off. `.err` alone is the status-cell style below - red and
   semi-bold on every word - so it is restated here: the border and the
   ground carry the meaning, and the prose stays readable. .note.err
   outranks .err on specificity, which is why this works. */
.note.err {
  border-left-color: var(--danger); background: var(--danger-dim);
  color: var(--text-2); font-weight: 400;
}

/* The failed half of the ok/err pair used in status cells. */
.err { color: var(--danger); font-weight: 500; }

/* Prose inside a card.

   There was no rule, so a paragraph took the browser default of 1rem
   while the card header above it is 0.875rem and every table in the
   panel is 0.875rem - prose ended up the largest text on the page and
   louder than the heading it sat under. Thirty-seven paragraphs already
   carried `style="font-size:0.875rem"` inline, each author fixing it
   again in place, which is the same tell as `.muted`: everybody works
   around it and nobody states it once.

   Those inline copies still win and still agree, so they can go when
   the files are next touched rather than in one sweep. */
.card-body p, .card-body li { font-size: 0.875rem; line-height: 1.6; }
.card-body ol, .card-body ul { margin: 8px 0 0 18px; }

/* Sub-headings.

   The stylesheet set weight and letter-spacing for h1-h3 and a size for
   NONE of them. h1 is covered by .page-title; h2, h3 and h4 were left at
   the browser default, which is bold and at least 1rem - larger than the
   card header above them (0.875rem, weight 400) and larger than the
   prose beneath. So a sub-heading inside a card was the loudest thing on
   the page, and the only pages where it looked right were the ones whose
   author had set a size by hand.

   Same shape as .muted and .card-body p: everybody worked around it in
   place and nobody stated it once. Inline sizes still win, so the pages
   that already compensate are unchanged.

   The scale is deliberately flat. h1 is 1.375rem and nearly everything
   else in this panel is 0.875rem; a sub-heading earns its emphasis from
   WEIGHT rather than size, which keeps a card readable when it holds
   three of them. */
h2 { font-size: 1rem;      font-weight: 500; margin: 0 0 6px; }
h3 { font-size: 0.9375rem; font-weight: 600; margin: 0 0 6px; }
h4 { font-size: 0.875rem;  font-weight: 600; margin: 0 0 6px; }
.card-body h2, .card-body h3, .card-body h4 { color: var(--text); }

/* The clickable line of a <details>. Three of them in the panel, all
   inside a card, all rendering at the browser default of 1rem while the
   card around them is 0.875rem. Not scoped to .card-body, because a
   summary is a summary wherever it sits and there is nowhere in this
   panel it should be the largest text on screen. */
summary { font-size: 0.875rem; }

/* Lists in the two places prose appears. NOT a bare `li` rule: the
   sidebar is built from list items, and giving every li in the panel a
   size would resize the navigation to match a paragraph. */
.flash li, .prose li { font-size: 0.875rem; line-height: 1.6; }

/* Isolated PHP notice, in the account Limits dialog.

   It answers one question - do the numbers beside it do anything -
   so it is a state panel, not a warning box. Amber while the limits
   are not reaching the websites, green once they are, and the colour
   carries the meaning before the words do, exactly as the toasts do. */
.ownphp {
  border: 1px solid var(--warn);
  border-left-width: 3px;
  background: var(--warn-dim);
  border-radius: 8px;
  padding: 12px 14px;
}
.ownphp.is-on {
  border-color: var(--ok);
  background: var(--ok-dim);
}
.ownphp-head {
  display: flex;
  align-items: center;
  gap: 9px;
}
.ownphp-head b { font-size: 0.9375rem; }
/* The button sits at the far end of the heading rather than after the
   paragraph: the reader looks for the control beside the state, not at
   the end of an explanation they may not read. */
.ownphp-btn { margin-left: auto; }
.ownphp-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--warn);
  flex: none;
}
.ownphp.is-on .ownphp-dot { background: var(--ok); }
.ownphp-body {
  margin: 7px 0 0;
  color: var(--text-2);
  font-size: 0.8125rem;
  line-height: 1.5;
}
/* Only present while the switch differs from what is on the server. */
.ownphp-note {
  margin: 8px 0 0;
  padding-top: 8px;
  border-top: 1px solid var(--border);
  color: var(--accent);
  font-size: 0.8125rem;
}

/* What happened to a message, in the SMTP2GO activity log.

   A column of identical grey words is one somebody has to read every
   row of to find the one that failed. The colour carries the meaning
   before the words do - the same rule the toasts follow. */
.ev {
  display: inline-block;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: .02em;
  white-space: nowrap;
  background: var(--surface-3);
  color: var(--text-2);
}
.ev-ok   { background: var(--ok-dim);     color: var(--ok); }
.ev-bad  { background: var(--danger-dim); color: var(--danger); }
.ev-warn { background: var(--warn-dim);   color: var(--warn); }

/* The date beside it. Short enough to fit, and never wrapped: a
   two-line date makes every row twice as tall for no gain. */
.when { white-space: nowrap; }
