/* ============================================================
   De Haas Site Manager - app.css
   Operator UI design system. One dark theme, dense, single accent.

   THESIS
   This panel used to dress every part of itself as a terminal:
   letterspaced mono capitals carried the sidebar groups, the field
   labels, the table heads, the stat keys, the tool group labels, the
   tabs, the breadcrumbs and the whole table body. Eight component
   families in one costume, and the costume is the thing that dated it.

   So there are two voices here now, and the split is the design:

     MACHINE VOICE (mono)  things a person copies, compares or types.
                           Hosts, paths, slugs, uids, byte counts,
                           grants, tokens, versions, log actions.
     HUMAN VOICE (sans)    everything a person reads. Labels, titles,
                           help text, table heads, tab names, nav,
                           breadcrumbs, display names, prose.

   A value in the wrong voice is a bug in this file, not a preference.

   The rest follows from that: a four plane ground so a card sits on a
   page instead of dissolving into it, a type scale with a real top end
   so a page title outranks a stat, and states built from tint plus
   border rather than a colour swap.

   Nothing here is fetched from a third party. The two faces are served
   from this container. No framework, no build step. One stylesheet.
   ============================================================ */

/* === Faces ===============================================
   Geist and Geist Mono, served from this container and from nowhere else.
   They were the panel's faces before this rebuild and they stay its faces;
   what changed is where they come from. They used to arrive from
   fonts.googleapis.com, which put a third party in the render path of a
   login-gated operator console, blocked first paint on two extra origins,
   and leaked a request per page load from a panel whose whole point is that
   it is private.

   One variable file per family rather than the eight static weights the old
   link requested: 141KB for both, against 8 files and 2 DNS lookups, and any
   weight between 100 and 900 is available without another request.

   font-display: swap, so text is readable on the first paint in the fallback
   and swaps when the file lands. The fallbacks below each face are the system
   stacks, which is also what renders if the files are ever missing.

   Geist is SIL Open Font License 1.1, from vercel/geist-font v1.7.2. The
   licence ships beside the files at assets/fonts/OFL.txt, which is the copy
   the OFL requires to travel with them.
   ============================================================ */
@font-face {
  font-family: "Geist";
  src: url("fonts/Geist-Variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Geist Mono";
  src: url("fonts/GeistMono-Variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* === Tokens ============================================== */
:root {
  /* Ground. Four planes, and the gaps between them are deliberately
     wide enough to see: the previous ramp put the sidebar three RGB
     units from the page and the card six from that, so every surface
     in the panel read as one surface. Sidebar is the darkest plane
     because it is furthest back, content sits on it, cards sit on
     content, and a raised element inside a card is the top plane. */
  --bg-dim:           #08080b;  /* sidebar rail, sunken wells, inputs  */
  --bg:               #101014;  /* page ground                         */
  --surface:          #16161c;  /* card plane                          */
  --surface-2:        #1e1e26;  /* raised inside a card                */
  --surface-3:        #282833;  /* hover on a raised element           */
  --hairline:         #24242e;
  --hairline-strong:  #33333f;

  --text:             #f2f2f5;
  --text-2:           #b8b8c4;
  /* 4.9:1 on --surface, so it carries secondary content legitimately. */
  --text-3:           #85858f;
  /* DECORATION ONLY. Under 3:1 against these surfaces, so it fails
     WCAG AA for text and must never carry content: no labels, no
     values, no placeholders. Separators, chevrons and dividers only,
     which 1.4.3 exempts. Anything a reader has to read is --text-3 or
     lighter. Raising this to pass would collapse it into --text-3 and
     cost the ramp its quietest step, so the token keeps its job and
     gives up the misuse. */
  --text-4:           #5c5c66;
  /* Consumed by the quiet <pre> blocks in ftp/accounts/cron. Aliased
     rather than given its own value: one quiet grey is enough. */
  --muted:            var(--text-3);

  /* Accent. One hue, four jobs, and they are separate tokens because
     they have separate contrast requirements. --accent is a fill that
     carries white text; --accent-2 is the hue used AS text or as an
     icon on a dark ground, which needs to be far lighter to clear
     4.5:1; the tints are surfaces. Collapsing these was why the old
     file used the same value for a button fill and for link text. */
  --accent:           #c026d3;
  --accent-hover:     #d132e4;
  /* The dark end of every white-on-accent surface. #fff on --accent is
     4.7:1 and on --accent-deep 5.9:1, so a label sitting anywhere on one of
     these gradients clears AA; running the gradient the other way, toward
     --accent-hover, puts white on 3.97:1 and fails it. */
  --accent-deep:      #a81fb8;
  --accent-2:         #e879f9;
  --accent-soft:      rgba(192, 38, 211, 0.16);
  --accent-line:      rgba(232, 121, 249, 0.30);

  --ok:               #4ade80;
  --warn:             #fbbf24;
  --bad:              #f43f5e;
  --info:             #38bdf8;
  --ok-soft:          rgba(74, 222, 128, 0.10);
  --ok-line:          rgba(74, 222, 128, 0.28);
  --warn-soft:        rgba(251, 191, 36, 0.10);
  --warn-line:        rgba(251, 191, 36, 0.28);
  --bad-soft:         rgba(244, 63, 94, 0.10);
  --bad-line:         rgba(244, 63, 94, 0.30);
  --info-soft:        rgba(56, 189, 248, 0.10);
  --info-line:        rgba(56, 189, 248, 0.28);

  /* Faces. Geist and Geist Mono, self-hosted (see @font-face above), each
     over a system stack that carries the page until the file lands and
     that stands in for it if the file never does. The Liberation and
     DejaVu entries are there for the Linux desktops and containers where
     system-ui resolves to nothing useful. */
  --sans:  "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI",
           Roboto, "Helvetica Neue", "Liberation Sans", Arial, sans-serif;
  --mono:  "Geist Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo,
           Consolas, "Liberation Mono", "DejaVu Sans Mono", monospace;
  /* --serif kept for back-compat with existing inline styles in views.
     Aliased to sans so the panel reads consistently. */
  --serif: var(--sans);

  /* Elevation. Every raised plane carries the same pair: a shadow
     underneath and a one pixel light line along its top edge. The top
     line is what makes a dark surface read as lit from above rather
     than as a lighter rectangle, and it is the whole difference
     between a flat panel and one with a ground. */
  --e-1: 0 1px 2px rgba(0, 0, 0, 0.40),
         inset 0 1px 0 rgba(255, 255, 255, 0.035);
  --e-2: 0 6px 20px -6px rgba(0, 0, 0, 0.55),
         inset 0 1px 0 rgba(255, 255, 255, 0.045);
  --e-3: 0 28px 64px -16px rgba(0, 0, 0, 0.70),
         inset 0 1px 0 rgba(255, 255, 255, 0.055);

  --r-sm: 8px;   /* controls: buttons, inputs, chips   */
  --r:    10px;  /* icon boxes, tiles                  */
  --r-md: 12px;  /* cards                              */
  --r-lg: 16px;  /* the auth card, modals              */

  /* Spacing. 4-based, deliberately few steps: rhythm comes from the
     contrast between a tight interval and a generous one, and a scale
     with a value every 2px has no contrast left to spend. Everything
     in this file resolves to one of these. If a layout wants 18px, one
     of 16 or 20 is the answer.
     Grouping rule: --s-2/--s-3 inside a group, --s-6 between groups. */
  --s-0:  2px;
  --s-1:  4px;
  --s-2:  8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 20px;
  --s-6: 24px;
  --s-7: 32px;
  --s-8: 48px;
  --s-9: 64px;

  /* Type. Fixed px, never fractional: a 13.5px label lands on a half
     pixel and renders soft, which reads as blur rather than as a size.
     The whole scale moved up one step from the old one and gained a
     top end. The old scale ran 10px to 20px, which meant a page title
     was 20px, a stat value 17px and a card title 15px: three sizes
     inside one 1.3x span, with no size anywhere that could lead a
     page. 24px at the top is what makes a page title read as the
     largest thing on the page rather than as bold body text. */
  --t-2xs: 11px;   /* version lines, eyebrow, table micro labels */
  --t-xs:  12px;   /* table heads, row action labels, captions   */
  --t-sm:  13px;   /* table body, metadata, secondary text       */
  --t-md:  14px;   /* the UI default: buttons, inputs, body      */
  --t-lg:  16px;   /* card titles                                */
  --t-xl:  19px;   /* stat values, modal + empty-state titles    */
  --t-2xl: 24px;   /* page titles                                */
  --t-3xl: 30px;   /* a figure that is the point of its page     */

  /* One height for every control, so an input, a select and a button
     sitting in the same row line up. */
  --control-h: 40px;
  --btn-h: 36px;

  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --dur: 0.14s;
}

/* === Base ================================================ */
* { box-sizing: border-box; }
html, body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: var(--t-md);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
body { min-height: 100dvh; }

::selection { background: var(--accent-soft); color: var(--text); }

button { font-family: inherit; color: inherit; }
input, textarea, select { font-family: inherit; color: inherit; }

a { color: var(--text-2); text-decoration: none; transition: color var(--dur) var(--ease); }
a:hover { color: var(--text); }
/* Prose links keep their underline: inside a sentence, colour alone is
   not enough to find them. Chrome links (nav, tiles, actions) opt out
   by being flex or carrying their own class. */
.lede a, .card-sub a, .field-help a, .field-help-sm a { color: var(--accent-2); text-decoration: underline; text-underline-offset: 2px; }
.lede a:hover, .card-sub a:hover { color: var(--accent-2); filter: brightness(1.15); }

code, .mono { font-family: var(--mono); }
/* Mono renders wider than the sans at the same nominal size, so a
   hostname inline in a sentence sat visibly larger than the words
   around it. One step down puts the two on the same visual weight. */
.mono { font-size: 0.945em; }

:focus-visible {
  /* --accent-2, not --accent: the ring has to be findable against the
     card, the sidebar and a filled accent button alike, and the fill
     hue disappears into the last of those. */
  outline: 2px solid var(--accent-2);
  outline-offset: 2px;
  border-radius: 4px;
}

/* Visually hidden, still announced. The clip-rect technique rather
   than display:none or visibility:hidden, both of which remove the
   element from the accessibility tree, which would defeat the point. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Off-screen until focused. */
.skip-link {
  position: fixed;
  top: var(--s-3);
  left: var(--s-3);
  z-index: 60;
  transform: translateY(-200%);
  background: var(--surface-2);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--r-sm);
  padding: var(--s-2) var(--s-4);
  font-size: var(--t-md);
  color: var(--text);
  box-shadow: var(--e-2);
  transition: transform var(--dur) var(--ease);
}
.skip-link:focus { transform: none; }

/* === Shell: centered (setup, login, forgot, reset) ======= */
.shell-bleed {
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: var(--s-8) var(--s-5) var(--s-9);
  /* One very soft accent wash behind the auth card, so the signed out
     pages are recognisably the same product as the panel without
     putting a decorative gradient in front of a login form. */
  background:
    radial-gradient(900px 480px at 50% -10%, rgba(192, 38, 211, 0.10), transparent 70%),
    var(--bg);
}

/* === Cards =============================================== */
.card {
  /* .card is overflow:hidden for its border radius, so anything wider
     than it is simply cut off: no scrollbar, no way to reach it.
     Filesystem paths, schema names and email addresses have no break
     opportunities, so at 390px they were being truncated mid-string.
     Break them instead. Set on .card rather than .card-body because
     several views put content directly in the card. Tables are exempt:
     they get a real scroller on .card-body-flush, and breaking a path
     inside a table cell hurts more than scrolling to it. */
  overflow-wrap: anywhere;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--r-md);
  box-shadow: var(--e-1);
  overflow: hidden;
  position: relative;
}
.card-bleed {
  width: 100%;
  max-width: 440px;
  padding: var(--s-8) var(--s-7);
  border-radius: var(--r-lg);
  box-shadow: var(--e-3);
}

/* === Brand mark ========================================== */
.brand-mark {
  width: 44px;
  height: 44px;
  border-radius: var(--r);
  /* The mark is the one place the accent is allowed to be the whole
     surface. Everywhere else it is a signal on a neutral. */
  background: linear-gradient(160deg, var(--accent), var(--accent-deep));
  border: 1px solid var(--accent-line);
  box-shadow: 0 4px 14px -4px rgba(192, 38, 211, 0.55),
              inset 0 1px 0 rgba(255, 255, 255, 0.22);
  display: grid;
  place-items: center;
  margin: 0 auto;
  color: #fff;
}
.brand-mark svg { width: 22px; height: 22px; }

/* === Eyebrow + headline ================================== */
/* The eyebrow stays in the machine voice on purpose: on the auth
   pages it names the system, which is the one label here that is a
   machine fact rather than something to read. */
.eyebrow {
  font-family: var(--mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-3);
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
}
.eyebrow .dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ok);
  box-shadow: 0 0 0 3px var(--ok-soft);
}

.headline {
  font-family: var(--sans);
  font-weight: 600;
  font-size: var(--t-2xl);
  line-height: 1.2;
  letter-spacing: -0.018em;
  margin: 0;
  text-wrap: balance;
  color: var(--text);
}
.headline em { font-style: normal; font-weight: 600; color: var(--accent-2); }

.lede {
  color: var(--text-2);
  font-size: var(--t-md);
  line-height: 1.6;
  margin: 0;
  max-width: 70ch;
}

/* === Form ================================================ */
.field { display: flex; flex-direction: column; gap: var(--s-2); }
/* Was 11px mono capitals at 0.14em. A field label is the single most
   read piece of text in a form, so it gets the reading face at a
   reading size. The weight is what separates it from its help line. */
.field-label {
  font-family: var(--sans);
  font-size: var(--t-sm);
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-2);
}
/* Help text is prose, so it gets a measure like every other run of
   prose in here. Without one, a two-sentence hint under a full-width
   textarea ran about 180 characters across a single line. */
.field-help {
  font-size: var(--t-sm);
  color: var(--text-3);
  margin: 0;
  max-width: 72ch;
  line-height: 1.55;
}
.field-help-sm {
  font-size: var(--t-xs);
  color: var(--text-3);
  margin-top: var(--s-1);
  max-width: 72ch;
  line-height: 1.55;
}

.input {
  width: 100%;
  /* Recessed, not raised: an input is a hole you type into. The inset
     shadow is what says so on a dark ground, where a darker fill alone
     reads as a disabled block. */
  background: var(--bg-dim);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--r-sm);
  padding: var(--s-2) var(--s-3);
  min-height: var(--control-h);
  font-size: var(--t-md);
  line-height: 20px;
  color: var(--text);
  outline: none;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.35);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.input:hover:not(:disabled):not([readonly]) { border-color: #3d3d4a; }
.input:focus {
  border-color: var(--accent);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.35), 0 0 0 3px var(--accent-soft);
}
.input::placeholder { color: var(--text-3); }
textarea.input { min-height: 0; line-height: 1.6; }

.input-with-button { display: flex; gap: var(--s-2); }

/* Field-level error. Keyed off [aria-invalid] rather than a class so
   the visual state and the state a screen reader announces cannot
   drift apart: there is one attribute and both read it. Colour alone
   never carries the message, which is why .field-error text is
   mandatory alongside it and is wired to the input with
   aria-describedby. */
.input[aria-invalid="true"] {
  border-color: var(--bad);
}
.input[aria-invalid="true"]:focus {
  border-color: var(--bad);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.35), 0 0 0 3px var(--bad-soft);
}
.field-error {
  margin-top: var(--s-1);
  font-size: var(--t-xs);
  color: #ff8fa3;
}

/* === Buttons ============================================ */
.btn {
  font-family: var(--sans);
  font-size: var(--t-md);
  font-weight: 500;
  line-height: 1;
  min-height: var(--btn-h);
  padding: 0 var(--s-4);
  border-radius: var(--r-sm);
  border: 1px solid transparent;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
              color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
  white-space: nowrap;
}
.btn:active { transform: translateY(1px); }

/* Disabled. Sits above .is-pending because a pending control is busy,
   not unavailable, and the two must not look the same. */
.btn:disabled,
.btn[disabled],
.btn[aria-disabled="true"],
.input:disabled,
.input[disabled],
select.input:disabled,
button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  box-shadow: none;
}
/* Kill the hover response: a control that cannot be pressed should not
   answer. Each variant restores its OWN resting value, because a
   blanket `background: inherit` resolved to the card background and a
   disabled primary button visibly changed on hover, which is the
   opposite of the intent. These sit at 0-3-0 and so beat the 0-2-0
   variant hovers regardless of source order. */
.btn:disabled:hover,
.btn[disabled]:hover { filter: none; }
.btn-primary:disabled:hover,
.btn-primary[disabled]:hover   { background: var(--accent);   border-color: var(--accent); }
.btn-secondary:disabled:hover,
.btn-secondary[disabled]:hover { background: var(--surface-2); color: var(--text); }
.btn-ghost:disabled:hover,
.btn-ghost[disabled]:hover     { background: transparent; border-color: transparent; color: var(--text-2); }
.btn-danger:disabled:hover,
.btn-danger[disabled]:hover    { filter: none; }

/* In-flight submit: dimmed, non-interactive, with a spinner in place
   of the usual leading icon. Applied by app.js on form submit. */
.btn.is-pending {
  opacity: 0.72;
  cursor: progress;
  pointer-events: none;
}
.btn.is-pending::before {
  content: "";
  width: 13px;
  height: 13px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: btn-spin 0.6s linear infinite;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .btn.is-pending::before { animation-duration: 2.4s; }
}

.btn-primary {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4),
              inset 0 1px 0 rgba(255, 255, 255, 0.18);
}
.btn-primary:hover {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
}
.btn-primary > .icon { opacity: 0.9; }

.btn-secondary {
  background: var(--surface-2);
  border-color: var(--hairline-strong);
  color: var(--text);
  box-shadow: var(--e-1);
}
.btn-secondary:hover {
  background: var(--surface-3);
  border-color: #3d3d4a;
  color: var(--text);
}

.btn-ghost {
  background: transparent;
  border-color: transparent;
  color: var(--text-2);
}
.btn-ghost:hover {
  color: var(--text);
  background: var(--surface-2);
  border-color: var(--hairline);
}

.btn-danger {
  /* Not var(--bad): #fff on #f43f5e is 3.4:1, under AA, on the one
     control that destroys things. This is the same hue darkened until
     the label clears 4.5:1, with --bad kept as the border so the
     button still reads as the same red as every other danger
     affordance. */
  background: #b81238;
  color: #fff;
  border-color: var(--bad);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4),
              inset 0 1px 0 rgba(255, 255, 255, 0.14);
}
.btn-danger:hover { filter: brightness(1.12); }

.btn-danger-outline {
  background: transparent;
  border-color: var(--hairline-strong);
  color: var(--text-3);
}
.btn-danger-outline:hover,
.btn-danger-outline:focus-visible {
  background: var(--bad-soft);
  border-color: var(--bad);
  color: #ff8fa3;
}

.btn-block { width: 100%; }
.btn-icon-sm {
  padding: 0 var(--s-2);
  min-width: var(--btn-h);
  background: var(--surface-2);
  border-color: var(--hairline-strong);
  color: var(--text-2);
}
.btn-icon-sm:hover { background: var(--surface-3); color: var(--text); }

/* === Alerts ============================================= */
/* The message text used to be set in the status hue on a tint of the
   same hue, which is the least readable arrangement available: two
   values of one colour. The hue moved to the left bar and the icon
   dot, and the words went back to --text, where they can be read.
   Colour is still never the only signal, because the words state the
   outcome. */
.alert {
  position: relative;
  border: 1px solid;
  border-radius: var(--r-sm);
  padding: var(--s-3) var(--s-4);
  font-size: var(--t-md);
  line-height: 1.55;
  color: var(--text);
  display: flex;
  gap: var(--s-3);
}
.alert::before {
  content: "";
  flex: none;
  width: 8px;
  height: 8px;
  margin-top: 7px;
  border-radius: 50%;
  background: currentColor;
}
.alert-error {
  background: var(--bad-soft);
  border-color: var(--bad-line);
}
.alert-error::before { color: var(--bad); }
.alert-success {
  background: var(--ok-soft);
  border-color: var(--ok-line);
}
.alert-success::before { color: var(--ok); }
.alert ul { margin: 0; padding-left: var(--s-4); }

/* === Well (boxed disclosure region) ====================== */
.well {
  padding: var(--s-3) var(--s-4);
  background: var(--bg-dim);
  border: 1px solid var(--hairline);
  border-radius: var(--r-sm);
}

/* === Footer / version line =============================== */
/* Machine voice: this is a build string. */
.footer-line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  font-family: var(--mono);
  font-size: var(--t-2xs);
  color: var(--text-3);
  letter-spacing: 0.04em;
}
.hairline-divider {
  height: 1px;
  background: var(--hairline);
  margin: var(--s-4) 0;
}

/* === Utility ============================================ */
.stack > * + * { margin-top: var(--gap, var(--s-4)); }
.stack-sm { --gap: var(--s-2); }
.stack-md { --gap: var(--s-4); }
.stack-lg { --gap: var(--s-6); }
.stack-xl { --gap: var(--s-7); }
.text-center { text-align: center; }
.section-gap { margin-bottom: var(--s-6); }

.icon {
  width: 1em;
  height: 1em;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
  flex-shrink: 0;
  vertical-align: -0.15em;
}

/* Icon sizes. An .icon is a 1em square, so its size is the font-size
   it sits at and nothing else: one class per step, no width/height per
   call site. The files disagree about their viewBox (24, 25, 32, 48,
   98.385) and this is what makes that invisible: every one of them
   ends up the same box.

   One step per job the layout has for an icon: next to a label in a
   row, a button or a table cell (16), a sidebar row (20), a bare icon
   button in the domain header (22), a quick-action row (24), a tool
   tile (32). Never a transform: scaling the box scales the stroke with
   it, and a row of icons with different stroke weights is the thing
   this set was drawn to avoid. */
.icon-16 { font-size: 16px; }
.icon-20 { font-size: 20px; }
.icon-22 { font-size: 22px; }
.icon-24 { font-size: 24px; }
.icon-32 { font-size: 32px; }

/* =========================================================
   App shell (sidebar + topbar + content)
   ========================================================= */
.shell-app {
  display: grid;
  grid-template-columns: 248px minmax(0, 1fr);
  min-height: 100dvh;
}

/* ----- Provider working inside a customer ----------------
   Moved out of an inline <style> in layout.php, where it could not use
   the elevation or type tokens and shipped a 0.85rem label on a solid
   accent strip.

   It stays loud on purpose. This bar is the answer to the one mistake
   in this panel that cannot be undone, which is making a change to the
   wrong customer's site, so it is the only element here allowed to own
   its whole row in the accent. Quietening it would be taste beating a
   product principle. */
.tenant-banner {
  grid-column: 1 / -1;
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  min-height: 44px;
  padding: var(--s-2) var(--s-5);
  background: linear-gradient(180deg, var(--accent), var(--accent-deep));
  border-bottom: 1px solid rgba(0, 0, 0, 0.35);
  color: #fff;
  font-size: var(--t-sm);
  box-shadow: 0 2px 14px -4px rgba(192, 38, 211, 0.6);
}
.tenant-banner-dot {
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.25);
}
.tenant-banner strong {
  font-family: var(--mono);
  font-weight: 500;
  background: rgba(0, 0, 0, 0.22);
  border-radius: 5px;
  padding: 1px 6px;
}
.tenant-banner form { margin-left: auto; }
.tenant-banner button {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  background: rgba(0, 0, 0, 0.20);
  color: inherit;
  border: 1px solid rgba(255, 255, 255, 0.45);
  border-radius: var(--r-sm);
  padding: 5px var(--s-3);
  cursor: pointer;
  font: inherit;
  font-weight: 500;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.tenant-banner button:hover {
  background: rgba(0, 0, 0, 0.32);
  border-color: rgba(255, 255, 255, 0.75);
}
.tenant-banner :focus-visible { outline-color: #fff; }

/* A full-height sidebar under a 44px banner is 44px too tall for the screen,
   and what falls off the bottom is the account block. The banner is sticky, so
   the offset holds at every scroll position rather than only at the top. */
body:has(.tenant-banner) .sidebar { top: 44px; height: calc(100dvh - 44px); }
body:has(.tenant-banner) .topbar  { top: 44px; }

/* ----- Sidebar ---------------------------------------- */
.sidebar {
  position: sticky;
  top: 0;
  height: 100dvh;
  background: var(--bg-dim);
  border-right: 1px solid var(--hairline);
  display: flex;
  flex-direction: column;
  padding: var(--s-5) var(--s-3) var(--s-3);
  gap: var(--s-1);
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--hairline-strong) transparent;
}
.brand {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: 0 var(--s-2) var(--s-5);
}
.brand-mark-sm {
  width: 34px;
  height: 34px;
  margin: 0;
  border-radius: 9px;
}
.brand-mark-sm svg { width: 18px; height: 18px; }
.brand-text { line-height: 1.2; min-width: 0; }
.brand-name {
  font-family: var(--sans);
  font-size: var(--t-md);
  font-weight: 600;
  letter-spacing: -0.012em;
  color: var(--text);
}
/* Machine voice: a platform name and a build number. */
.brand-sub {
  font-family: var(--mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.06em;
  color: var(--text-3);
  margin-top: 1px;
}

.nav { display: flex; flex-direction: column; }
.nav-group { display: flex; flex-direction: column; gap: var(--s-0); }
/* Was 10px mono capitals at 0.18em tracking. A section heading in a
   navigation column is read, not typed, so it moved to the reading
   face at a readable size and lost the tracking. Weight and colour do
   the separating now. */
.nav-label {
  font-family: var(--sans);
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: 0.01em;
  text-transform: none;
  color: var(--text-3);
  padding: var(--s-5) var(--s-3) var(--s-2);
}
.nav-item {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  /* 40px is what a 20px glyph needs before the row starts squeezing
     it, and the height is set rather than padded to, so a row with a
     trailing lock glyph is exactly as tall as one without. */
  min-height: 40px;
  padding: var(--s-1) var(--s-3);
  border-radius: var(--r-sm);
  font-size: var(--t-md);
  font-weight: 500;
  color: var(--text-2);
  cursor: pointer;
  border: 1px solid transparent;
  background: transparent;
  text-align: left;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
.nav-icon { color: var(--text-3); transition: color var(--dur) var(--ease); }
.nav-item:hover { color: var(--text); background: #14141a; }
.nav-item:hover .nav-icon { color: var(--text-2); }
/* The active row is a filled pill rather than a two pixel bar welded
   to the left edge of a flat strip. The accent lands on the icon and
   on a tint of the row, which is enough to find at a glance and does
   not need the text to change colour as well. */
.nav-item-active {
  color: var(--text);
  font-weight: 600;
  background: var(--accent-soft);
  border-color: var(--accent-line);
  box-shadow: none;
}
.nav-item-active:hover { background: var(--accent-soft); }
.nav-item-active .nav-icon,
.nav-item-active:hover .nav-icon { color: var(--accent-2); }
/* A destination this operator's role cannot open. Quieter than an
   enabled row but still legible: the point is to show the capability
   exists, not to hide it. The lock trails the label so the row still
   scans as its own name first. */
.nav-item-locked {
  color: var(--text-3);
  font-weight: 500;
  cursor: not-allowed;
}
.nav-item-locked:hover { background: transparent; color: var(--text-3); }
.nav-item-locked .nav-icon { color: var(--text-4); }
.nav-item-locked:hover .nav-icon { color: var(--text-4); }
.nav-item-locked .nav-icon:last-child { margin-left: auto; }

.sidebar-footer {
  margin-top: auto;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  /* The account block is the shell's footer identity: who you are
     signed in as, in what role, and the way out. It carries its own
     surface so it reads as a block rather than as two more nav rows
     that happen to sit at the bottom. */
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--r-sm);
  padding: var(--s-2) var(--s-2) var(--s-2) var(--s-3);
  margin-bottom: var(--s-1);
  box-shadow: var(--e-1);
}
.avatar {
  width: 30px;
  height: 30px;
  flex: none;
  border-radius: 8px;
  background: linear-gradient(160deg, var(--accent), var(--accent-deep));
  display: grid;
  place-items: center;
  font-family: var(--sans);
  font-size: var(--t-sm);
  font-weight: 600;
  color: #fff;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
}
.user-meta { line-height: 1.25; min-width: 0; }
.user-name {
  font-size: var(--t-sm);
  font-weight: 500;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Machine voice: a role is an enum the system checks. */
.user-role {
  font-size: var(--t-2xs);
  color: var(--text-3);
  font-family: var(--mono);
  letter-spacing: 0.04em;
}
.icon-btn {
  margin-left: auto;
  width: 30px;
  height: 30px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: var(--r-sm);
  background: transparent;
  border: 1px solid transparent;
  cursor: pointer;
  color: var(--text-3);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
.icon-btn:hover {
  color: var(--text);
  background: var(--surface-2);
  border-color: var(--hairline-strong);
}
.logout-form { margin-left: auto; display: flex; }

/* ----- Main column ------------------------------------- */
.main {
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: var(--s-4);
  min-height: 56px;
  padding: var(--s-2) var(--s-7);
  border-bottom: 1px solid var(--hairline);
  /* Translucent rather than opaque, so content scrolling under the bar
     is visibly behind it rather than vanishing at a hard line. */
  background: rgba(16, 16, 20, 0.82);
  backdrop-filter: saturate(150%) blur(10px);
  -webkit-backdrop-filter: saturate(150%) blur(10px);
}
/* Navigation, so it reads in the reading face. Hosts and paths inside
   it carry .mono at their call sites and stay in the machine voice. */
.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
  font-size: var(--t-sm);
  color: var(--text-3);
  /* A deep doc-root path has no break opportunities and would
     otherwise widen the topbar past the viewport, taking the document
     with it. */
  overflow-wrap: anywhere;
  min-width: 0;
}
.crumbs a:hover { color: var(--text); }
.crumbs .sep { color: var(--text-4); }
.crumbs .here { color: var(--text); font-weight: 500; }

.topbar-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--s-2);
}
.status-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-1) var(--s-3);
  border-radius: 999px;
  font-size: var(--t-xs);
  font-weight: 500;
  border: 1px solid;
  white-space: nowrap;
}
.status-pill .dot,
.status-pill .dot-ok,
.status-pill .dot-warn {
  width: 6px;
  height: 6px;
  flex: none;
  border-radius: 50%;
}
.status-pill .dot-ok   { background: var(--ok);   box-shadow: 0 0 0 3px var(--ok-soft); }
.status-pill .dot-warn { background: var(--warn); box-shadow: 0 0 0 3px var(--warn-soft); }
.status-ok {
  background: var(--ok-soft);
  border-color: var(--ok-line);
  color: var(--ok);
}
.status-warn {
  background: var(--warn-soft);
  border-color: var(--warn-line);
  color: var(--warn);
}

/* ----- Content ----------------------------------------- */
.content {
  padding: var(--s-7) var(--s-7) var(--s-9);
  max-width: 1480px;
  width: 100%;
  margin: 0 auto;
}

/* ----- Page hero (compact admin header) ----------------
   No kicker above the title: the breadcrumb in the topbar already says
   where you are, and a third restatement of it was pure noise. Title,
   one line of lede, stats on the right. */
.page-hero {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-7);
  padding-bottom: var(--s-5);
  margin-bottom: var(--s-6);
  border-bottom: 1px solid var(--hairline);
}
.hero-title {
  font-family: var(--sans);
  font-weight: 600;
  font-size: var(--t-2xl);
  line-height: 1.2;
  letter-spacing: -0.02em;
  margin: 0 0 var(--s-2);
  color: var(--text);
}
/* The old rule set this in italic, which the system stacks render as a
   slanted UI face and which read as an accident. It is an emphasis on
   one word of a page title, so it takes the accent instead. */
.hero-title em { font-style: normal; font-weight: 600; color: var(--accent-2); }
.page-hero .lede { max-width: 68ch; }

/* Hero stats are the page's figures, so they get a surface. Naked
   columns of numbers against the page ground read as a caption to
   whatever sits above them. */
.hero-stats {
  display: flex;
  gap: var(--s-3);
  flex-shrink: 0;
  flex-wrap: wrap;
}
.hero-stats .hero-stat {
  flex: none;
  min-width: 116px;
  padding: var(--s-3) var(--s-4);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--r-sm);
  box-shadow: var(--e-1);
  text-align: left;
}
.hero-stat { flex: none; white-space: nowrap; }
/* Was 10px mono capitals at 0.16em. A stat key is a word you read. */
.hero-stat .k {
  font-family: var(--sans);
  font-size: var(--t-xs);
  font-weight: 500;
  letter-spacing: 0;
  color: var(--text-3);
  text-transform: none;
  margin-bottom: var(--s-1);
}
/* The value stays in the machine voice: these are counts and byte
   figures, and tabular digits are what makes two of them comparable. */
.hero-stat .v {
  font-family: var(--mono);
  font-size: var(--t-xl);
  font-weight: 500;
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.hero-stat .d {
  font-size: var(--t-xs);
  color: var(--text-3);
  margin-top: var(--s-1);
}
.hero-stat .d a { color: var(--accent-2); }

/* ----- Card head / body --------------------------------- */
.card-head {
  padding: var(--s-4) var(--s-5);
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-4);
  border-bottom: 1px solid var(--hairline);
}
.card-title {
  font-family: var(--sans);
  font-size: var(--t-lg);
  font-weight: 600;
  letter-spacing: -0.012em;
  line-height: 1.35;
  margin: 0;
  color: var(--text);
}
.card-sub {
  font-size: var(--t-sm);
  color: var(--text-3);
  margin-top: var(--s-1);
  line-height: 1.55;
  /* Same reason .page-hero .lede is capped: on a wide screen an
     uncapped sub ran the full card width, which at this size is well
     past the measure the eye can track back from at the end of a
     line. */
  max-width: 76ch;
}
.card-body { padding: var(--s-5); }

/* === Tables ==============================================
   The panel is mostly tables, so they get a real component. Views used
   to hand-style every cell, which is why no two tables agreed on
   padding, size or muted-text colour. Reach for `.table` plus the cell
   modifiers below instead of a `style=` attribute.

     <table class="table">
       <thead><tr><th>name</th><th class="num">size</th></tr></thead>
       <tbody>
         <tr>
           <td class="key">wp_main <span class="tag tag-accent">wordpress</span></td>
           <td class="num mut">4.2 MB</td>
         </tr>
       </tbody>
     </table>

   Cell modifiers: .key (machine identifier, mono) · .name (a human
   name, sans) · .num (right, mono, tabular figures) · .mut (secondary)
   · .dim (tertiary) · .actions (row actions)

   The body used to be mono at 12px across every column, which put
   email addresses, roles, relative timestamps and display names in the
   typewriter. The body is the reading face now, and the three
   modifiers that mark a machine value bring the mono back.
   ========================================================= */
.table { width: 100%; border-collapse: collapse; }

/* Sans capitals at 12px with a hair of tracking, not mono capitals at
   11px with a lot of it. A column head has to be scannable at a glance
   and unmistakably not data. */
.table thead th {
  background: var(--surface-2);
  font-family: var(--sans);
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: 0.045em;
  text-transform: uppercase;
  color: var(--text-3);
  text-align: left;
  padding: var(--s-3) var(--s-4);
  white-space: nowrap;
  border-bottom: 1px solid var(--hairline);
}

.table tbody td {
  padding: var(--s-3) var(--s-4);
  font-family: var(--sans);
  font-size: var(--t-sm);
  color: var(--text-2);
  border-bottom: 1px solid var(--hairline);
  vertical-align: middle;
}
/* No trailing rule: the card border already closes the block. */
.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr { transition: background 0.1s var(--ease); }
.table tbody tr:hover { background: var(--surface-2); }

/* Cell modifiers. Listed twice on purpose: the bare class is a general
   text utility usable anywhere, and the `.table tbody td` form
   re-states it at higher specificity so it still wins inside a table
   cell. */
/* .key is the row's identifier: a slug, a schema name, a username, a
   host, a path. All machine values, so mono. */
.key,             .table tbody td.key {
  font-family: var(--mono);
  font-size: var(--t-sm);
  font-weight: 500;
  color: var(--text);
}
/* A human name, not an identifier. "Advanced Custom Fields" is prose
   and belongs in the text face. Setting a whole table in mono because
   the subject is technical is a costume. */
.table tbody td.name {
  font-family: var(--sans);
  font-size: var(--t-md);
  font-weight: 500;
  color: var(--text);
}
.table tbody td.name .desc {
  font-weight: 400;
  font-size: var(--t-sm);
  color: var(--text-3);
  margin-top: var(--s-1);
  max-width: 420px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mut,             .table tbody td.mut { color: var(--text-3); }
.dim,             .table tbody td.dim { color: var(--text-3); }
.ok,              .table tbody td.ok  { color: var(--ok); }
.bad,             .table tbody td.bad { color: #ff8fa3; }
/* .ok and .bad existed and .warn did not, so five views reached for an inline
   `color: var(--warn)` for the middle state. */
.warn,            .table tbody td.warn { color: var(--warn); }
.num,             .table tbody td.num {
  text-align: right;
  font-family: var(--mono);
  /* Digits share one advance width, so columns of numbers line up. */
  font-variant-numeric: tabular-nums;
}
/* .num right-aligns the cells; the head above them has to move too, or every
   numeric column in the panel reads with its label hanging off to the left of
   the figures it names. `.table thead th` sets text-align on its own, so this
   has to restate it at the same specificity or higher. */
.table thead th.num { text-align: right; }
.table .actions { text-align: right; font-size: var(--t-xs); white-space: nowrap; }
@media (max-width: 900px) {
  /* nowrap keeps rows one line high on desktop; on a phone it is the
     thing that pushes the row past the card edge. */
  .table .actions { white-space: normal; }
  /* A head whose title and button will not fit side by side stacks
     instead of letting the button slide out of the card. */
  .card-head { flex-wrap: wrap; }
}

/* Inline label attached to a cell value (wordpress / system / user). */
.tag {
  display: inline-block;
  margin-left: var(--s-2);
  padding: 1px var(--s-2);
  border-radius: 999px;
  font-family: var(--sans);
  font-size: var(--t-2xs);
  font-weight: 500;
  line-height: 1.6;
  background: var(--surface-3);
  border: 1px solid var(--hairline-strong);
  color: var(--text-2);
  vertical-align: middle;
  white-space: nowrap;
}
.tag-accent { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent-2); }
.tag-info   { background: var(--info-soft);   border-color: var(--info-line);   color: var(--info); }
.tag-warn   { background: var(--warn-soft);   border-color: var(--warn-line);   color: var(--warn); }

/* .btn-inline, .btn-inline-danger, .btn-inline-warn and .actions-sep
   were here. They were 11px mono text links standing in for buttons,
   and the measured result was 149 controls in this panel under the
   24px minimum target size. .btn-action replaced every one of them;
   the rules went with them rather than being left for the next view to
   reach for. */
.link-accent { color: var(--accent-2); }
.link-accent:hover { color: var(--accent-2); filter: brightness(1.15); }
/* Keeps a one-button POST form inline with sibling action links. */
.inline-form { display: inline; }

/* Table that fills its card edge to edge.
   Every table in the panel sits in one of these. `.card` is
   `overflow: hidden`, so without a scroller here a table wider than
   its card was simply cut off with no way to reach the rest: at 390px
   the accounts table rendered 856px wide and lost its last three
   columns AND every row action. Scrolling the table rather than the
   page keeps the body from overflowing sideways.
   `-webkit-overflow-scrolling` keeps the momentum scroll on iOS
   Safari. */
.card-body-flush {
  padding: 0;
  overflow-wrap: normal;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
/* Body holding a .meta list: the rows supply their own vertical
   rhythm. */
.card-body-gutter { padding: 0 var(--s-5); }

/* Quiet block of literal text: a shell snippet to copy, a captured log
   tail, a set of connection details. Four views had each rolled this
   by hand with slightly different sizes and greys, which read as four
   different kinds of thing rather than one. Scrolls rather than wraps,
   because a wrapped command line is a command line you cannot copy
   correctly. */
.code-block {
  margin: 0;
  padding: var(--s-4);
  background: var(--bg-dim);
  border: 1px solid var(--hairline);
  border-radius: var(--r-sm);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.35);
  font-family: var(--mono);
  font-size: var(--t-sm);
  line-height: 1.7;
  color: var(--text-2);
  overflow-x: auto;
}

/* The absolute path of the thing shown below it: which log file this tail
   came from. A caption in the machine voice, flush to the top of the block it
   labels. */
.path-bar {
  background: var(--bg-dim);
  border-top: 1px solid var(--hairline);
  padding: var(--s-2) var(--s-4);
  font-size: var(--t-xs);
  color: var(--text-3);
  overflow-wrap: anywhere;
}

/* A log tail. Same block as .code-block, but a log line is read rather
   than copied whole, so it wraps instead of scrolling sideways and caps
   its height so the page keeps its own scroll. */
.code-block-log {
  white-space: pre-wrap;
  word-break: break-all;
  max-height: 70vh;
  overflow: auto;
  border-radius: 0;
  border-left: 0;
  border-right: 0;
  border-bottom: 0;
  box-shadow: none;
}

/* ----- A file or directory name in a row ----------------
   The file manager drew this inline in three places with three slightly
   different greys. The glyph tells a directory from a file at a glance, so it
   carries the accent for one and the quietest grey for the other: the one
   distinction in that table that decides whether a click navigates or
   selects. */
.file-name {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-width: 0;
  color: var(--text);
}
.file-name > .icon { color: var(--accent-2); }
.file-name-plain { color: var(--text-2); }
.file-name-plain > .icon { color: var(--text-4); }
a.file-name:hover { color: var(--text); }
a.file-name:hover > .icon { color: var(--accent); }

/* Drop target for an upload, over the file manager card. Pointer events
   are off so the drag events keep firing on the card underneath; this is
   only the affordance. `display` is toggled inline by the page's own
   script, which is why the resting value stays on the element. */
.drop-overlay {
  position: absolute;
  inset: 0;
  z-index: 10;
  pointer-events: none;
  align-items: center;
  justify-content: center;
  background: var(--accent-soft);
  border: 2px dashed var(--accent-2);
  border-radius: inherit;
}
.drop-overlay-badge {
  background: var(--surface-2);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--r-sm);
  padding: var(--s-3) var(--s-5);
  font-size: var(--t-md);
  font-weight: 600;
  color: var(--text);
  box-shadow: var(--e-2);
}

/* Small bordered surface: a toolbar segment, a path crumb, a count
   badge. */
.subtle-box {
  padding: var(--s-2) var(--s-3);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  border-radius: var(--r-sm);
}

/* A version being replaced by another, on the upload confirmation page. Two
   lines, one leaving and one arriving, in the same shape a diff uses, because
   that is what the operator is being asked to approve. */
.diff-row {
  font-family: var(--mono);
  font-size: var(--t-sm);
  padding: var(--s-1) var(--s-2);
  border-radius: 5px;
  border: 1px solid;
}
.diff-row + .diff-row { margin-top: var(--s-1); }
.diff-row-out { background: var(--bad-soft); border-color: var(--bad-line); color: #ff8fa3; }
.diff-row-in  { background: var(--ok-soft);  border-color: var(--ok-line);  color: var(--ok); }

/* The list of guarantees under a destructive confirmation. Each line is a
   promise about what the operation will not destroy, so it reads as prose with
   a measure rather than as a bulleted feature list. */
.safeguards {
  list-style: none;
  margin: 0;
  padding: 0;
  color: var(--text-2);
  font-size: var(--t-sm);
  line-height: 1.55;
  max-width: 76ch;
}
.safeguards > li + li { margin-top: var(--s-2); }

/* Boxed inline code token (a granted db name, a grant pattern). */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-family: var(--mono);
  font-size: var(--t-xs);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  border-radius: 6px;
  padding: 2px var(--s-2);
  margin-right: var(--s-1);
  white-space: nowrap;
}
.chip-accent {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--accent-2);
}

/* === Modal (native <dialog>) =============================
   Replaces window.prompt/alert/confirm. showModal() supplies the focus
   trap, Esc handling and inert background, so this is purely visual.
   ========================================================= */
.modal {
  padding: 0;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--r-lg);
  background: var(--surface);
  color: var(--text);
  width: min(460px, calc(100vw - 32px));
  box-shadow: var(--e-3);
}
.modal::backdrop {
  background: rgba(4, 4, 6, 0.66);
  backdrop-filter: blur(3px);
}
.modal-form { padding: var(--s-6); display: grid; gap: var(--s-4); }
.modal-title {
  margin: 0;
  font-size: var(--t-xl);
  font-weight: 600;
  letter-spacing: -0.015em;
}
.modal-body {
  margin: 0;
  font-size: var(--t-md);
  line-height: 1.6;
  color: var(--text-2);
  /* Confirmation copy sometimes carries a second line of consequence. */
  white-space: pre-line;
}
.modal-fields { display: grid; gap: var(--s-3); }

/* Checkbox list inside a modal (e.g. which databases a user may
   access). */
.check-group {
  display: grid;
  gap: var(--s-0);
  max-height: 260px;
  overflow-y: auto;
  border: 1px solid var(--hairline);
  border-radius: var(--r-sm);
  background: var(--bg-dim);
  padding: var(--s-2) var(--s-3);
}
.check-group .field-label { margin-bottom: var(--s-1); }
/* .check-label carries a top margin for its standalone use under a
   form; inside a list the rows sit flush. */
.check-row { margin-top: 0; padding: var(--s-1) var(--s-0); font-family: var(--mono); font-size: var(--t-sm); }
.check-row:hover { color: var(--text); }
.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--s-2);
  margin-top: var(--s-0);
}

/* File upload queue. Each file gets its own real progress and result. */
.upload-modal { width: min(640px, calc(100vw - 32px)); }
.upload-modal-inner { padding: var(--s-6); display: grid; gap: var(--s-4); }
.upload-modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s-4); }
.upload-summary { margin: var(--s-1) 0 0; color: var(--text-3); font-size: var(--t-sm); }
.upload-total-percent { color: var(--text); font-family: var(--mono); font-size: var(--t-md); font-variant-numeric: tabular-nums; }
.upload-total-track,
.upload-file-track { height: 6px; overflow: hidden; border-radius: 999px; background: var(--bg-dim); box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.4); }
.upload-total-track span,
.upload-file-track span { display: block; width: 100%; height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--accent), var(--accent-2)); transform: scaleX(0); transform-origin: left; transition: transform 0.15s ease-out; }
.upload-file-list { display: grid; gap: var(--s-2); max-height: min(50vh, 420px); overflow-y: auto; }
.upload-file { padding: var(--s-3); border: 1px solid var(--hairline); border-radius: var(--r-sm); background: var(--surface-2); }
.upload-file-top { display: flex; justify-content: space-between; gap: var(--s-3); }
.upload-file-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); font-size: var(--t-sm); }
.upload-file-state { flex: none; color: var(--text-3); font-size: var(--t-xs); }
.upload-file-meta { margin: var(--s-1) 0 var(--s-2); color: var(--text-3); font-size: var(--t-xs); font-family: var(--mono); }
.upload-file.is-complete .upload-file-state { color: var(--ok); }
.upload-file.is-complete .upload-file-track span { background: var(--ok); }
.upload-file.is-failed .upload-file-state { max-width: 50%; overflow: hidden; text-overflow: ellipsis; color: #ff8fa3; }
.upload-file.is-failed .upload-file-track span { background: var(--bad); }
.upload-file.is-cancelled { opacity: 0.65; }

/* Entry animation, skipped for prefers-reduced-motion (see below). */
@media (prefers-reduced-motion: no-preference) {
  .modal[open] { animation: modal-in 0.16s var(--ease); }
  @keyframes modal-in {
    from { opacity: 0; transform: translateY(-8px) scale(0.98); }
    to   { opacity: 1; transform: none; }
  }
}

@media (max-width: 900px) {
  .modal-actions { flex-direction: column-reverse; }
  .modal-actions .btn { width: 100%; justify-content: center; }
}

/* === Tab bar ============================================= */
.tabs {
  display: flex;
  gap: var(--s-1);
  margin-bottom: var(--s-6);
  border-bottom: 1px solid var(--hairline);
}
/* Was 11px mono capitals at 0.12em. A tab is a word you read to decide
   where to go. */
.tab {
  padding: var(--s-3) var(--s-4);
  margin-bottom: -1px;
  font-family: var(--sans);
  font-size: var(--t-md);
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-3);
  border-bottom: 2px solid transparent;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.tab:hover { color: var(--text-2); }
.tab-active { color: var(--text); font-weight: 600; border-bottom-color: var(--accent-2); }

/* === Inline form layouts ================================= */
/* Field row that ends in a submit button. Follows .grid-2's naming:
   the suffix counts the flexible columns before the trailing auto
   button. */
.form-grid { display: grid; gap: var(--s-3); align-items: end; }
.form-grid-1 { grid-template-columns: 1fr auto; }
.form-grid-2 { grid-template-columns: 1fr 1fr auto; }
.form-grid-3 { grid-template-columns: 1fr 1fr 1fr auto; }
.form-grid-4 { grid-template-columns: 1fr 1fr 1fr auto auto; }
/* A button ending a .form-grid row lines up with the inputs beside it,
   which are --control-h tall and not --btn-h. */
.form-grid > .btn, .form-grid .form-grid-btn { min-height: var(--control-h); }

/* Selects render shorter than inputs at the same padding; pin them so
   a select sitting beside an input in a .form-grid lines up. */
select.input { height: var(--control-h); }
.input-narrow { max-width: 260px; }
/* For a control whose content has its own fixed width: a PEM key is 64
   to 70 base64 characters per line, and stretching the box to a 1400px
   card turns the pasted key into a ragged sliver down the left edge of
   a lot of nothing. */
.input-wide { max-width: 76ch; }

.check-label {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-4);
  font-size: var(--t-md);
  color: var(--text-2);
  cursor: pointer;
}
/* The platform checkbox and radio, tinted to the accent. One line, and
   it is the difference between every toggle in the panel being the
   browser's blue and being this product's magenta. */
input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); }

/* Checkbox whose label is a title plus an explanation. .check-label
   centres a one-line label against its box; a two-part label has to
   align to the top or the box ends up floating against the middle of
   the sentence. The detail line is a .field-help-sm, so it inherits
   the same size and measure as every other explanation in the panel
   instead of the 11.5px one view invented for it. */
.check-block {
  display: flex;
  gap: var(--s-3);
  align-items: flex-start;
  cursor: pointer;
}
.check-block-title {
  font-size: var(--t-md);
  font-weight: 500;
  color: var(--text);
}

.is-hidden { display: none; }

body.is-navigating { cursor: progress; }

/* ----- Loading skeleton -------------------------------- */
/* Placeholder blocks the dynamic navigator drops into <main> while a
   page is in flight. Only the blocks live here: the surrounding
   .topbar/.content/.card wrappers are the real layout classes, so the
   skeleton already has the geometry of the page replacing it. */
.sk {
  display: block;
  border-radius: var(--r-sm);
  background: linear-gradient(
    90deg,
    var(--surface-2) 25%,
    var(--surface-3) 37%,
    var(--surface-2) 63%
  );
  background-size: 400% 100%;
  animation: sk-shimmer 1.4s ease infinite;
}
.sk + .sk { margin-top: var(--s-3); }
.sk-head    { height: 30px; width: 300px; max-width: 100%; }
.sk-text    { height: 13px; width: 160px; max-width: 100%; }
.sk-text-sm { width: 110px; }
.sk-text-lg { width: 460px; }
.sk-rows .sk         { height: 15px; width: 100%; }
.sk-rows .sk:nth-child(2n) { width: 72%; }
.sk-rows .sk:nth-child(3n) { width: 86%; }

@keyframes sk-shimmer { to { background-position: -400% 0; } }
/* Matches .btn.is-pending: slowed, not removed. The page is still busy
   and that has to stay visible. */
@media (prefers-reduced-motion: reduce) {
  .sk { animation-duration: 4s; }
}
/* Detail region revealed by a .check-label checkbox. */
.check-detail { margin-top: var(--s-3); }

@media (max-width: 900px) {
  .form-grid-1, .form-grid-2, .form-grid-3, .form-grid-4 { grid-template-columns: 1fr; }
}

/* Grid of fields where each field carries help text under its input.
   The gap is the between-groups step (--s-6), not .grid's --s-4: the
   field's own parts sit --s-2 apart, and a 16px gap between fields is
   too close to that to read as a boundary, so the help line ends up
   looking like a caption for the label below it. Two columns down to
   the same breakpoint everything else uses. */
.field-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-6);
  align-items: start;
}
@media (max-width: 900px) {
  .field-grid { grid-template-columns: 1fr; }
}

/* === Meta rows (key/value detail lists) ================== */
.meta { display: grid; }
.meta-row {
  display: grid;
  grid-template-columns: 160px minmax(0, 1fr);
  gap: var(--s-4);
  align-items: baseline;
  padding: var(--s-3) 0;
  border-bottom: 1px solid var(--hairline);
}
.meta-row:last-child { border-bottom: 0; }
/* Was 11px mono capitals at 0.16em. The key is the question, the value
   is the answer: the question is read, so it is sans, and the answer is
   almost always a machine value, so it stays mono. */
.meta-key {
  font-family: var(--sans);
  font-size: var(--t-sm);
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-3);
}
.meta-val { font-family: var(--mono); font-size: var(--t-sm); color: var(--text); }
.meta-val-info   { color: var(--info); }
.meta-val-accent { color: var(--accent-2); }
.meta-val-empty  { color: var(--text-3); font-family: var(--sans); }

@media (max-width: 900px) {
  .meta-row { grid-template-columns: 1fr; gap: var(--s-1); }
}

/* Flush-mount tables align with .card-head's 20px gutter. */
.card-body > table { width: 100%; border-collapse: collapse; }
.card-body > table th:first-child,
.card-body > table td:first-child { padding-left: var(--s-5); }
.card-body > table th:last-child,
.card-body > table td:last-child  { padding-right: var(--s-5); }
.card-body > div > table:not(.no-card-align) th:first-child,
.card-body > div > table:not(.no-card-align) td:first-child { padding-left: var(--s-5); }
.card-body > div > table:not(.no-card-align) th:last-child,
.card-body > div > table:not(.no-card-align) td:last-child  { padding-right: var(--s-5); }

/* align-items:start, so a card with four rows in it does not get stretched to
   the height of the card beside it and end in a third of a screen of nothing.
   Cards in this panel carry lists of wildly different lengths. */
.grid { display: grid; gap: var(--s-4); align-items: start; }
.grid-2 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
/* Same breakpoint the .form-grid-* rules use. Without this the two
   columns simply got narrower on a phone until the content inside them
   wrapped character by character. */
@media (max-width: 900px) {
  .grid-2 { grid-template-columns: minmax(0, 1fr); }
}

/* ----- Quick-action list ------------------------------ */
.qa-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}
.qa-item {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  border-radius: var(--r);
  color: var(--text-2);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
              color var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.qa-item:hover {
  background: var(--surface-3);
  border-color: var(--accent-line);
  color: var(--text);
}
.qa-item:hover .tile-icon {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent);
}
/* Layout only. The box it sits in is .tile-icon, which every call site
   pairs it with: a quick action and a tool tile are the same control at
   two widths, so they get the same square rather than two that nearly
   match. */
.qa-icon {
  display: grid;
  place-items: center;
}
.qa-text { display: flex; flex-direction: column; gap: 1px; flex: 1; min-width: 0; }
.qa-title { font-size: var(--t-md); font-weight: 600; color: var(--text); letter-spacing: -0.006em; }
.qa-sub { font-size: var(--t-sm); color: var(--text-3); }
.qa-sub code, .qa-sub .mono {
  font-family: var(--mono);
  font-size: var(--t-xs);
  background: var(--bg-dim);
  border: 1px solid var(--hairline);
  border-radius: 5px;
  padding: 1px var(--s-1);
}
.qa-arrow { color: var(--text-4); transition: color var(--dur) var(--ease), transform var(--dur) var(--ease); }
.qa-item:hover .qa-arrow { color: var(--accent-2); transform: translateX(2px); }
@media (prefers-reduced-motion: reduce) {
  .qa-item:hover .qa-arrow { transform: none; }
}

/* ----- Activity list ---------------------------------- */
.activity-list { list-style: none; margin: 0; padding: 0; }
.activity-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-2) 0;
  border-bottom: 1px solid var(--hairline);
  font-size: var(--t-sm);
}
.activity-row:last-child { border-bottom: none; }
/* An audit action is an identifier the system wrote, so it keeps the
   machine voice and the boxed treatment that says so. */
.activity-row code.mono {
  font-family: var(--mono);
  font-size: var(--t-xs);
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  border-radius: 5px;
  padding: 2px var(--s-2);
  white-space: nowrap;
}
.activity-row .muted { color: var(--text-3); font-size: var(--t-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.activity-row .right { text-align: right; }

/* ----- Site cards (sites.php) --------------------------
   The preview used to be the card: a 16:10 screenshot taking about 70%
   of the area, with the six actions squeezed into a 280px rail beside
   it and a large void underneath. On a page whose entire job is
   operating a site, the decoration was leading and the work was in the
   margin.

   So the preview is now identification, thumbnail size, in the header
   next to the domain it identifies, and the actions are the body. Card
   height drops from about 660px to about 200px, which is the difference
   between one install per screen and three. */
.site-head {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  min-width: 0;
}
.site-thumb {
  border: 1px solid var(--hairline);
  border-radius: var(--r-sm);
  overflow: hidden;
  background: var(--bg-dim);
  width: 116px;
  aspect-ratio: 16/10;
  flex: none;
  display: block;
  text-decoration: none;
  box-shadow: var(--e-1);
  transition: border-color var(--dur) var(--ease);
}
.site-thumb:hover { border-color: var(--hairline-strong); }
.site-thumb img { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; }

/* Actions read left to right in the order the job usually runs: look
   inside, then act on it. Backup is the one that changes something, so
   it is the one that carries the accent. */
.site-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  align-items: center;
}
.site-actions .spacer { margin-left: auto; }

/* Metadata as a spec row rather than a middot sentence: each fact gets
   its own labelled column so the eye can jump to the one it wants. */
.site-specs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-6);
  margin-top: var(--s-3);
}
.site-spec { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.site-spec .k {
  font-family: var(--sans);
  font-size: var(--t-xs);
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-3);
}
.site-spec .v {
  font-family: var(--mono);
  font-size: var(--t-sm);
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
}

/* An empty state is read before anything else on the page it takes
   over, so it gets a measure and a real title size rather than centred
   text at body weight. */
.empty-state {
  text-align: center;
  padding: var(--s-8) var(--s-6);
}
.empty-state-title {
  font-family: var(--sans);
  font-size: var(--t-xl);
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--text);
  margin-bottom: var(--s-2);
}
.empty-state .lede { max-width: 48ch; margin-inline: auto; }
td.empty-state { text-align: center; }

.manual-db-grid {
  display: none;
  grid-template-columns: 1fr 1fr 1fr;
  gap: var(--s-3);
  align-items: end;
  margin-bottom: var(--s-4);
}
.manual-db-grid.is-open { display: grid; }
.manual-db-grid > .full {
  grid-column: span 3;
  display: flex;
  gap: var(--s-2);
  align-items: center;
  font-size: var(--t-sm);
  color: var(--text-2);
  cursor: pointer;
}

.form-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--s-2);
  /* --s-2 put the submit as close to the last field as that field's own
     help line sits to its input, so the button read as part of the
     field rather than as the end of the form. */
  margin-top: var(--s-6);
}

/* ====================================================== */
/* Responsive                                              */
/* ====================================================== */
@media (max-width: 1100px) {
  .content { padding: var(--s-6) var(--s-5) var(--s-8); }
  .topbar { padding: var(--s-2) var(--s-5); }
  .shell-app { grid-template-columns: 228px minmax(0, 1fr); }
}

@media (max-width: 900px) {
  /* The sidebar used to stack its full height above the page: brand,
     six group labels, ten items and the account footer, about 470px of
     chrome before the first word of content. Narrow is not a designed
     mobile experience here, but it does have to start with the page, so
     the nav becomes one scrollable bar and everything below it is the
     work. */
  .shell-app { grid-template-columns: minmax(0, 1fr); }
  .sidebar {
    position: sticky;
    top: 0;
    z-index: 30;
    height: auto;
    flex-direction: row;
    align-items: center;
    gap: var(--s-2);
    border-right: none;
    border-bottom: 1px solid var(--hairline);
    padding: var(--s-2) var(--s-4);
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
  }
  .sidebar::-webkit-scrollbar { display: none; }
  .brand { padding: 0; gap: var(--s-2); flex: none; }
  .brand-text { display: none; }
  .nav { flex-direction: row; gap: var(--s-1); }
  .nav-group { flex-direction: row; gap: var(--s-1); }
  .nav-label { display: none; }
  .nav-item { white-space: nowrap; }
  .sidebar-footer {
    margin-top: 0;
    margin-left: auto;
    margin-bottom: 0;
    padding: var(--s-1);
    flex: none;
    background: transparent;
    border-color: transparent;
    box-shadow: none;
  }
  .user-meta { display: none; }

  .content { padding: var(--s-4) var(--s-4) var(--s-8); }
  .topbar { padding: var(--s-2) var(--s-4); min-height: 48px; }
  .tenant-banner { position: static; }
  body:has(.tenant-banner) .sidebar { top: 0; height: auto; }
  body:has(.tenant-banner) .topbar  { top: 0; }
  .page-hero {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--s-4);
  }
  .hero-title { font-size: var(--t-xl); }
  .hero-stats { width: 100%; flex-wrap: wrap; }
  .hero-stats .hero-stat { flex: 1 1 116px; }
  .grid-2 { grid-template-columns: minmax(0, 1fr); }
  .manual-db-grid { grid-template-columns: 1fr; }
  .manual-db-grid > .full { grid-column: auto; }
  .card-bleed { padding: var(--s-6) var(--s-5); }
  .tenant-banner { padding: var(--s-2) var(--s-4); flex-wrap: wrap; }

  /* Card head stacks so the domain is not crushed against "Open site",
     and the action row runs left to right instead of pinning backup to
     the far edge. */
  .card-head { flex-direction: column; align-items: flex-start; gap: var(--s-3); }
  .card-head > div { width: 100%; }
  .site-actions .spacer { display: none; }
}

/* ====================================================== */
/* Motion preference                                       */
/* ====================================================== */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }

  /* Two exceptions, and they must sit AFTER the blanket rule above:
     with both sides !important, source order decides, and these lose
     everywhere else in the file. Both communicate "still working".
     Frozen, they say "broken" instead, which is worse than the motion
     they were meant to spare. Slowed, not removed. */
  .sk {
    animation-duration: 4s !important;
    animation-iteration-count: infinite !important;
  }
  .btn.is-pending::before {
    animation-duration: 2.4s !important;
    animation-iteration-count: infinite !important;
  }
}

/* ====================================================== */
/* Customer forms                                          */
/* ====================================================== */
/* The provider's customer pages laid their fields out with .form-grid,
   whose align-items:end lines up the BOTTOM of each cell. With a help
   line under every input and no two of them the same length, that
   pushed each input to a different height and dropped the submit
   button into the next column. These pages use .field-grid instead
   (align-items:start, so help text grows downward and the row above it
   never moves), which needed a row of three for a credential row and a
   mail relay, and a gap for a card that ends in two actions rather
   than one. */
.field-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
/* Two stacked grids are how a form mixes rows of two with rows of
   three; the step between them is the same --s-6 the grid puts between
   its own rows. */
.field-grid + .field-grid { margin-top: var(--s-6); }
/* A help line that explains a whole .form-grid row rather than one
   field in it. Inside a field it would be one more thing making the
   row's cells different heights, which is the bug above. */
.form-grid-help { grid-column: 1 / -1; }
/* An action row whose last control is the one that cannot be undone. The
   customer lifecycle card holds re-mint, suspend and delete in one row, and
   right-aligning all three put "Delete customer" the same 8px from "Suspend"
   as "Suspend" is from "Re-mint credentials". The reversible pair stays
   together on the left and the destructive one is pushed to the far edge, so
   reaching it is a deliberate move across the card. */
.form-actions-split { justify-content: flex-start; flex-wrap: wrap; }
.form-actions-split > :last-child { margin-left: auto; }
@media (max-width: 900px) {
  .form-actions-split > :last-child { margin-left: 0; }
}
@media (max-width: 900px) {
  .field-grid-3 { grid-template-columns: minmax(0, 1fr); }
}

/* ============================================================
   Plesk-style layout (icons, domain cards, tool tiles)

   One section, appended rather than threaded through the file, so the
   previous design system stays readable and this can be lifted out
   whole. Everything below reuses the tokens at the top: no new
   colours, no new spacing steps, no dependencies.
   ============================================================ */

/* ----- File-backed icons --------------------------------
   public/assets/icons/*.svg come from several sources and paint
   themselves: each shape carries its own fill or stroke. .icon paints
   for the inline glyph set instead (stroke only, no fill), which would
   erase every filled path here, so this undoes that half and keeps the
   1em sizing. */
.icon-file {
  fill: none;
  stroke: none;
}
/* A shape that carries neither is one of the few solid icons whose
   colour was only on the root, so it fills; every other shape keeps
   the fill or stroke it declares, because an element's own
   presentation attribute beats a value it inherits. */
.icon-file :not([fill]):not([stroke]) { fill: currentColor; }

/* ----- Home: two columns ---------------------------------
   Cards on the left, the usage column on the right, the way Plesk
   splits its home screen. The right column stacks under the cards at
   1024px, which is where 320px of it stops being worth the width it
   takes from the cards. */
.pl-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 320px;
  gap: var(--s-6);
  align-items: start;
}
.pl-main { min-width: 0; }
.pl-side {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  position: sticky;
  top: calc(56px + var(--s-5));
}
.pl-side .section-gap { margin-bottom: 0; }
/* The usage column is 320px wide, and a 160px label track inside it left the
   figure squeezed against the card's right edge. In a panel this narrow the
   pair reads better as a labelled row: name on the left, value flush right,
   where a column of figures lines up down the card. */
.pl-side .meta-row {
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--s-3);
  align-items: baseline;
}
.pl-side .meta-val { text-align: right; }
.pl-links { list-style: none; margin: 0; padding: 0; }
.pl-links li + li { border-top: 1px solid var(--hairline); }
.pl-links a {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-5);
  font-size: var(--t-md);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.pl-links a .icon { color: var(--text-3); }
.pl-links a:hover { background: var(--surface-2); color: var(--text); }
.pl-links a:hover .icon { color: var(--accent-2); }

/* ----- Domain card --------------------------------------
   <details> and not a button with a script: the card's open state is
   the platform's own job, and the page a session starts on should not
   need JavaScript to open its first host. */
.pl-domain > summary {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-4) var(--s-5);
  cursor: pointer;
  list-style: none;
  transition: background var(--dur) var(--ease);
}
.pl-domain > summary::-webkit-details-marker { display: none; }
.pl-domain[open] > summary {
  border-bottom: 1px solid var(--hairline);
  /* An open card's header is the label for the region under it, so it
     sits on the raised plane and the body sits on the card. */
  background: var(--surface-2);
}
.pl-domain > summary:hover { background: var(--surface-2); }
.pl-domain[open] > summary:hover { background: var(--surface-3); }
.pl-domain-mark {
  width: 36px;
  height: 36px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: var(--r);
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  color: var(--accent-2);
  /* One mechanism for every icon in the panel: the box is the
     font-size. */
  font-size: 18px;
}
.pl-domain-id { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
/* The host is the identifier this whole card is about, so it is the
   one place in the panel where the machine voice is also the
   headline. */
.pl-domain-host {
  font-family: var(--mono);
  font-size: var(--t-lg);
  font-weight: 500;
  color: var(--text);
  letter-spacing: -0.01em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pl-domain-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
  font-size: var(--t-sm);
  color: var(--text-3);
}
.pl-dot { color: var(--text-4); }

/* Status chip. The dot carries no meaning on its own: the word next to
   it is the state, so colour is never the only signal. */
.pl-chip-ok,
.pl-chip-bad {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  margin-left: auto;
  flex: none;
  font-family: var(--sans);
  font-weight: 500;
}
.pl-chip-dot { width: 6px; height: 6px; flex: none; border-radius: 50%; background: currentColor; }
.pl-chip-ok  { background: var(--ok-soft);  border-color: var(--ok-line);  color: var(--ok); }
.pl-chip-bad { background: var(--bad-soft); border-color: var(--bad-line); color: #ff8fa3; }

.pl-quick { display: flex; align-items: center; gap: var(--s-2); flex: none; }
/* Icon buttons in the card header: a 22px glyph in a 36px square,
   which is the smallest box that still reads as a button rather than
   as a glyph someone stuck in the header. 8px between them, because at
   4px four of them read as one strip and the hover target of the one
   you want is a guess. They carry their surface at rest rather than
   only on hover: four bare glyphs in a row read as status icons, and
   the operator asked for buttons. Each carries a title, because a bare
   icon with no label has to say what it opens. */
.pl-quick-btn {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border-radius: var(--r-sm);
  color: var(--text-3);
  background: var(--surface);
  border: 1px solid var(--hairline-strong);
  box-shadow: var(--e-1);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
.pl-quick-btn:hover {
  color: var(--accent-2);
  background: var(--surface-3);
  border-color: var(--accent-line);
}
.pl-chevron { color: var(--text-4); flex: none; transition: transform var(--dur) var(--ease); }
.pl-domain[open] > summary .pl-chevron { transform: rotate(180deg); color: var(--text-3); }
@media (prefers-reduced-motion: reduce) {
  .pl-chevron { transition: none; }
}

/* ----- Inside an open card -------------------------------
   Stats on the left, the tabs on the right, like Plesk's split. */
.pl-domain-body {
  display: grid;
  grid-template-columns: 200px minmax(0, 1fr);
  gap: var(--s-6);
  padding: var(--s-5);
}
.pl-stats {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  text-align: left;
}
/* Inside the card the stats are boxed like the page hero's, so a
   figure reads as a figure wherever it appears. */
.pl-stats .hero-stat {
  white-space: normal;
  padding: var(--s-3) var(--s-4);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  border-radius: var(--r-sm);
}
.pl-stats .hero-stat .d { overflow-wrap: anywhere; }
.pl-stats .btn { margin-top: var(--s-1); }

/* Tabs without a script: the two radios sit before the bar and the
   panels, so :checked ~ reaches both. They are .sr-only rather than
   display:none, which would take them out of the tab order and make
   the tabs keyboard-dead. */
.pl-panel { display: none; }
.pl-tab-dash:checked ~ .pl-panel-dash,
.pl-tab-host:checked ~ .pl-panel-host { display: block; }
.pl-tab-dash:checked ~ .pl-tabbar .pl-tabl-dash,
.pl-tab-host:checked ~ .pl-tabbar .pl-tabl-host {
  color: var(--text);
  font-weight: 600;
  border-bottom-color: var(--accent-2);
}
.pl-tabbar label { cursor: pointer; }
.pl-tab:focus-visible ~ .pl-tabbar label { outline: 2px solid var(--accent-2); outline-offset: 2px; }
.pl-tabbar { margin-bottom: var(--s-5); }

.pl-group + .pl-group { margin-top: var(--s-6); }
/* Was 10px mono capitals. A tool group label is the heading for the
   tiles under it. */
.pl-group-label {
  font-family: var(--sans);
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: 0.01em;
  text-transform: none;
  color: var(--text-2);
  margin-bottom: var(--s-3);
  padding-bottom: var(--s-2);
  border-bottom: 1px solid var(--hairline);
}
/* Three across, like Plesk, at any width that holds them. The count is
   not hard-coded: this grid is nested two columns deep (the usage
   column takes 320, the stats column 200), so the tile area is nowhere
   near the viewport width and a viewport media query drops it to two
   120px columns while the card still has room for three. 190px is the
   measured width where the widest label, "SFTP connection info", stops
   wrapping mid-word, and min(100%, ...) keeps the track from
   overflowing a container narrower than one tile. */
.pl-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 190px), 1fr));
  gap: var(--s-3);
}

/* The icon box, for the tool tiles and the quick-action rows both. One
   accent tint for every one of them, never a colour per tool: eleven
   tinted squares in eleven hues is decoration pretending to be
   information, and the label next to the icon is what actually tells
   them apart. */
.tile-icon {
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: var(--r);
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  color: var(--accent-2);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
/* The tiles' icons are called with .icon-32, which left 4px of air in
   a 40px box. The box is the one thing sized here, so the glyph is
   brought back to the step the box was built for rather than the box
   being grown to fit a glyph. */
.pl-tiles .tile-icon .icon,
.qa-list .tile-icon .icon { font-size: 22px; }
.pl-tiles .qa-item { align-items: flex-start; }
.pl-tiles .tile-icon { width: 38px; height: 38px; }
.pl-tiles .qa-title { font-size: var(--t-md); font-weight: 600; }
.pl-tiles .qa-sub { line-height: 1.45; }
.pl-panel-actions { margin: var(--s-5) 0 0; }

@media (max-width: 1200px) {
  .pl-layout { grid-template-columns: minmax(0, 1fr) 280px; }
}
@media (max-width: 1024px) {
  .pl-layout { grid-template-columns: minmax(0, 1fr); }
  .pl-side { position: static; flex-direction: row; flex-wrap: wrap; }
  .pl-side > .card { flex: 1 1 280px; }
}
@media (max-width: 900px) {
  .pl-domain-body { grid-template-columns: minmax(0, 1fr); gap: var(--s-5); padding: var(--s-4); }
  .pl-domain > summary { flex-wrap: wrap; padding: var(--s-4); }
  .pl-chip-ok, .pl-chip-bad { margin-left: 0; }
  .pl-quick { margin-left: auto; }
  .pl-side { flex-direction: column; }
  .pl-stats { flex-direction: row; flex-wrap: wrap; }
  .pl-stats .hero-stat { flex: 1 1 130px; }
  .pl-stats .btn { flex: 1 1 100%; }
}

/* ----- Row actions --------------------------------------
   One component for every "do this to this row" control in the panel:
   edit, enter, change password, grants, enable, restore, delete,
   remove, drop. They used to be bare text links joined by a middot,
   four views spelled the separator differently, and the destructive one
   was the same weight as the harmless one next to it. A 30px chip with
   a 16px glyph is the smallest thing that reads as a button, which is
   what these are: every one of them either submits a form or opens a
   page.

   Two flavours only. Secondary for anything that opens or changes, and
   danger-outline for anything that destroys, restore included: it
   replaces files and database in place with no snapshot of what was
   there. */
.btn-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  height: 30px;
  padding: 0 var(--s-3);
  border-radius: var(--r-sm);
  font-family: var(--sans);
  font-size: var(--t-sm);
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  background: var(--surface-2);
  border: 1px solid var(--hairline-strong);
  color: var(--text-2);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
              color var(--dur) var(--ease);
}
.btn-action:hover { background: var(--surface-3); border-color: #3d3d4a; color: var(--text); }
.btn-action:active { transform: translateY(1px); }
.btn-action > .icon { font-size: 16px; color: var(--text-3); }
.btn-action:hover > .icon { color: var(--text-2); }
/* Transparent rather than filled, so a row with one destructive action
   does not lead with it; the border and the label turn red on the way
   in. */
.btn-action-danger {
  background: transparent;
  border-color: var(--hairline-strong);
  color: var(--text-3);
}
.btn-action-danger:hover,
.btn-action-danger:focus-visible {
  background: var(--bad-soft);
  border-color: var(--bad);
  color: #ff8fa3;
}
.btn-action-danger:hover > .icon { color: #ff8fa3; }

/* The selected one of a set. The logs page drew its source picker as mono
   pills with the state spelled out inline in two branches; a filter that is
   currently applied is a row action in its on state, so it gets one here
   instead. */
.btn-action-on {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--accent-2);
}
.btn-action-on:hover { background: var(--accent-soft); border-color: var(--accent-2); color: var(--accent-2); }
.btn-action-on > .icon, .btn-action-on:hover > .icon { color: var(--accent-2); }

/* The actions cell. 6px between buttons and not one of the spacing
   steps: at 8px two chips read as two separate controls, at 4px as one
   wide one. Wraps rather than overflowing, because the cell it sits in
   is the narrowest column of the table. */
.btn-action-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 6px;
}
/* A form here only ever wraps one button. As a flex item its inline
   display is blockified and it shrinks to the button, so the gap above
   measures between the buttons whether or not a form sits in
   between. */
.btn-action-group > form { margin: 0; }

/* Below 1100 the label is the first thing to go: the tables here carry
   five or six columns before the actions, and three labelled chips is
   what pushes the row off the card. The title attribute every one of
   them carries is the label, so nothing is lost that a hover or a
   focus cannot recover. */
@media (max-width: 1100px) {
  .btn-action-group .btn-action { width: 30px; padding: 0; }
  .btn-action-group .btn-action > span { display: none; }
  /* The exception: a group whose actions have no unambiguous glyph between
     them. The file manager's row carries edit, download, extract, compress,
     chmod, rename and delete, and picking an icon for "rename" that is not
     also the icon for "edit" means inventing one. A wrapped label costs less
     than a wrong glyph, and the group wraps rather than overflowing. */
  .btn-action-group.btn-action-group-labels .btn-action { width: auto; padding: 0 var(--s-3); }
  .btn-action-group.btn-action-group-labels .btn-action > span { display: inline; }
}
