/* Snazzy Host control plane.

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

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

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

* { box-sizing: border-box; }

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

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

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

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

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

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

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

/* ---------------------------------------------------- notification bell
   The dropdown is a light card floating over the dark topbar; the badge is
   the one solid-crit element in the chrome, because it is the one thing that
   means "something happened while you were not looking". */
.notify { position: relative; }
.notify-toggle {
  position: relative; display: inline-flex; background: transparent; color: #fff;
  border: 0; padding: 6px; cursor: pointer; line-height: 0; border-radius: var(--radius);
}
.notify-toggle:hover { background: rgba(255,255,255,.12); filter: none; }
.notify-badge {
  position: absolute; top: 0; right: 0; min-width: 16px; height: 16px;
  padding: 0 4px; border-radius: var(--radius-pill);
  background: var(--crit); color: #fff;
  font-size: 10px; font-weight: 700; line-height: 16px; text-align: center;
}
/* The same circle on a sidebar row — in the flex flow, pushed to the end. */
.nav-count {
  margin-left: auto; min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: var(--radius-pill); background: var(--crit); color: #fff;
  font-size: 11px; font-weight: 700; line-height: 18px; text-align: center;
}
.notify-pop {
  position: absolute; top: calc(100% + 10px); right: -8px; z-index: 60;
  width: 360px; max-height: 70vh; overflow-y: auto;
  background: var(--surface); color: var(--ink, inherit);
  border: 1px solid var(--line); border-radius: var(--radius-lg);
  box-shadow: var(--sh-pop); font-size: 13px; line-height: 1.4; text-align: left;
}
.notify-pop-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 14px; border-bottom: 1px solid var(--line);
  background: var(--surface-alt); font-weight: 600;
}
.notify-read-all {
  background: transparent; border: 0; padding: 0; cursor: pointer;
  color: var(--primary); font-size: 12px; font-weight: 600;
}
.notify-pop-state { padding: 18px 14px; }
.notify-pop-foot {
  display: block; padding: 9px 14px; border-top: 1px solid var(--line);
  text-align: center; font-weight: 600;
}
.notify-list { display: flex; flex-direction: column; }
.notify-item {
  display: flex; gap: 10px; align-items: flex-start;
  padding: 10px 14px; border-bottom: 1px solid var(--line);
  color: inherit; cursor: pointer;
}
.notify-item:last-child { border-bottom: 0; }
.notify-item:hover { background: var(--surface-alt); text-decoration: none; }
.notify-item.is-unread { background: rgba(var(--primary-rgb),.05); box-shadow: inset 3px 0 0 var(--primary); }
.notify-item.is-unread .notify-title { font-weight: 600; }
.notify-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; margin-top: 5px; background: var(--muted); }
.notify-dot.sev-crit { background: var(--crit); }
.notify-dot.sev-warn { background: var(--warn); }
.notify-dot.sev-ok   { background: var(--ok); }
.notify-dot.sev-info { background: var(--info); }
.notify-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.notify-title { overflow-wrap: anywhere; }
.notify-body  { overflow-wrap: anywhere; }
/* The full page reuses the same row markup inside a card; give it room. */
.card .notify-list .notify-item { padding: 12px 16px; }

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

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

.sidebar {
  width: var(--sidebar-w); flex: none; background: var(--surface);
  border-right: 1px solid var(--line); padding: 16px 0;
}
.sidebar-account { padding: 0 18px 14px; border-bottom: 1px solid var(--line); margin-bottom: 12px; }
.sidebar-account-name { font-weight: 600; }
.sidebar-account-role {
  font-size: var(--text-xs, 12px); color: var(--muted);
  text-transform: uppercase; letter-spacing: .04em;
}
/* The account switcher's sidebar twin and the Log out row exist only when the
   sidebar is a drawer — on a desktop both live in the topbar and these are
   dead weight. Shown again inside the media query at the foot of this file. */
.account-switch-side { display: none; margin-top: 10px; }
.account-switch-side select { width: 100%; }
.nav-mobile-only { display: none; }
.nav-heading {
  padding: 14px 18px 6px; font-size: var(--text-xs, 12px); font-weight: 600;
  text-transform: uppercase; letter-spacing: .05em; color: var(--muted);
}
.nav-list { list-style: none; margin: 0; padding: 0; }
.nav-list a {
  display: flex; align-items: center; gap: 12px;
  padding: var(--nav-pad, 10px 18px); min-height: 42px; color: var(--ink); font-size: var(--text-ui, 14px);
}
.nav-list a:hover { background: var(--bg); text-decoration: none; }
.nav-list a.active {
  background: rgba(var(--primary-rgb),.08); color: var(--primary); font-weight: 600;
  box-shadow: inset 3px 0 0 var(--primary);
}
.nav-list .material-icons { font-size: 20px; color: var(--muted-soft); flex: none; }
.nav-list a:hover .material-icons { color: var(--muted); }
.nav-list a.active .material-icons { color: var(--primary); }

.content { flex: 1; padding: var(--content-pad, 24px 28px 44px); min-width: 0; }

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

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

.page-head { display: flex; align-items: center; gap: 12px; margin-bottom: 18px; flex-wrap: wrap; }
/* A detail tab uses the same head row under an h2, since the page's h1 is the
   thing the tabs are about. h2 carries a bottom margin for the panes it
   normally heads, which in a centred flex row just pushes the title off it. */
.page-head > h2 { margin-bottom: 0; }
/* Wraps the breadcrumb, title and subtitle so they stack as one block while the
   page action stays on the right. Index pages have no breadcrumb and keep the
   flat h1 + .spacer shape, which still works. */
.page-head-main { display: flex; flex-direction: column; min-width: 0; }
.page-head-main .subtitle { color: var(--muted); font-size: 13px; }

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

/* ---------------------------------------------------------------- cards
   An outlined box: hairline edge, no shadow, --radius-card corners (the
   Merchant Center look, operator's call 2026-10-27 — the shadow-only card
   read as fuzzy edges and square corners on the grey ground). .pane is the
   same box under a second name — index pages say .card, detail tabs say
   .pane. Both are kept so no view has to change. Boxes INSIDE a card stay
   on --radius / --radius-lg. */

.card, .pane {
  background: var(--surface); border: 1px solid var(--card-edge); border-radius: var(--radius-card);
  box-shadow: none; padding: 18px 20px; margin-bottom: 18px;
}
.pane { padding: 20px 22px; }
/* Two cards that answer the same question side by side, one under the other
   when there is no room. The children keep their own margin-bottom off so the
   pair lines up with whatever single card follows it. */
.pane-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: 18px; }
.pane-row > .card, .pane-row > .pane { margin-bottom: 18px; }
.card-empty { text-align: center; color: var(--muted); padding: 40px 20px; }
.card > h2, .pane > h2 { color: var(--ink-heading); }
.pane > h2 { display: flex; align-items: baseline; gap: 10px; }
.pane > h2 .muted { font-weight: 400; font-size: 13px; font-family: var(--font-body); }
.pane-note { color: var(--muted); font-size: 13px; line-height: 1.6; margin: 14px 0 0; }
.pane-note code {
  background: var(--track); color: var(--primary-ink);
  border-radius: var(--radius); padding: 1px 5px; font-size: 12.5px;
  font-family: var(--font-mono);
}

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

.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; margin-bottom: 18px; }
.tile {
  position: relative; overflow: hidden;
  background: var(--surface); border: 1px solid var(--card-edge); border-radius: var(--radius-card);
  box-shadow: none; padding: 16px 18px;
}
/* A tile inside a card sits on the page ground, not white on white: a
   standalone grid (the dashboard, a server's overview) is drawn on --bg with
   no card around it, and a grid that is one part of a pane borrows that
   ground so the two read as the same thing. */
.card .tile, .pane .tile { background: var(--bg); }
.tile-label {
  font-size: var(--text-xs, 12px); font-weight: 600; text-transform: uppercase;
  letter-spacing: .05em; color: var(--muted);
}
.tile-value {
  font-family: var(--font-head); font-size: 30px; font-weight: 600;
  letter-spacing: 0; color: var(--ink); margin-top: 4px; line-height: 1.2;
}
.tile-value-sm { font-size: 16px; }
/* The badge wraps under a long label rather than running off the tile's
   right edge (the WordPress-updates tile at a narrow width). */
.tile-head { display: flex; align-items: center; gap: 4px 8px; flex-wrap: wrap; }
.tile-head .badge { margin-left: auto; }
/* A watermark, not a decoration: it sits behind the number and must never
   compete with it for the eye. */
.tile-icon {
  position: absolute; right: 10px; bottom: 6px;
  font-size: 44px; color: var(--track); pointer-events: none;
}

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

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

/* The WordPress-updates cell stacks its per-kind badges one under the other,
   tightly, rather than in a ragged wrapping row. */
.wpu-stack { display: inline-flex; flex-direction: column; align-items: flex-start; gap: 3px; }

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

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

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

table.list { width: 100%; border-collapse: collapse; }
/* Headers are the kicker style — 12px uppercase, muted — on every list,
   the same as the portal's tables. */
table.list th {
  text-align: left; font-size: var(--text-xs, 12px); font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em;
  color: var(--muted); padding: 10px; border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
table.list td { padding: 12px 10px; border-bottom: 1px solid var(--line); font-size: var(--text-ui, 14px); vertical-align: middle; }
table.list tr:last-child td { border-bottom: 0; }
table.list tbody tr:hover td { background: var(--bg); }
table.list tbody tr.selected > td { background: rgba(var(--primary-rgb),.05); }
/* Rows that are not serving traffic, tinted whole (websiteRows() in panel.js
   decides which). Faint on purpose: the tint is a scanning aid, and it sits
   under chips and badges that must stay legible on top of it. Both keep a
   deeper hover so a tinted row still answers the pointer — without these the
   plain hover rule above would be overridden and the row would look dead. */
table.list tbody tr.row-offline        > td { background: rgba(var(--crit-rgb),.06); }
table.list tbody tr.row-suspended      > td { background: rgba(var(--warn-rgb),.10); }
table.list tbody tr.row-offline:hover  > td { background: rgba(var(--crit-rgb),.11); }
table.list tbody tr.row-suspended:hover > td { background: rgba(var(--warn-rgb),.17); }
/* Figures line up on the decimal point or they are not worth tabulating. */
table.list td.num, table.list th.num { text-align: right; font-variant-numeric: tabular-nums; }

/* The dot is a flex sibling, not an inline glyph: inline, a narrow cell
   wrapped the name under it; the dot stays on the first line. */
.item-name { font-weight: 600; font-size: 15px; line-height: 1.3; display: flex; align-items: flex-start; gap: 8px; }
.item-name .dot { width: 10px; height: 10px; margin-top: 5px; flex: none; }
.item-name a { text-decoration: none; }
.item-slug, .item-fixed { font-size: 13px; color: var(--muted); margin-top: 2px; font-weight: 400; }
.item-slug { overflow-wrap: anywhere; }
a.badge:hover { text-decoration: underline; }
/* Pills in an item list read at text size, sentence case — the portal's
   badge — rather than the 11px uppercase tag the panel uses elsewhere. */
table.list.list-items .badge-pill {
  font-size: 13px; text-transform: none; letter-spacing: 0; padding: 3px 10px; gap: 6px;
}

/* The vulnerability table's expanding rows. The whole row is the disclosure:
   the previous markup put a <details> summary in its own full-width row under
   EVERY package, which doubled the table's height and broke the columns into
   ragged bands. inline-block because a transform does not apply to an inline
   box, which is how the chevron silently refused to turn. */
tr.vs-row, tr.u-row, tr.j-row, tr.dm-row { cursor: pointer; }
.ait-row { cursor: pointer; }
.vs-chev, .u-chev, .j-chev, .dm-chev, .ait-chev {
  display: inline-block; font-size: 18px; color: var(--muted);
  transition: transform .15s ease;
}
.vs-chev.open, .u-chev.open, .j-chev.open, .dm-chev.open, .ait-chev.open { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) {
  .vs-chev, .u-chev, .j-chev, .dm-chev, .ait-chev { transition: none; }
}
/* A job's log opens INSIDE the table, and the panel keeps the last 4000
   characters of it — taller than the page it appears in. Ceiling and scroll,
   so opening one row does not push every other row off the screen. */
tr.j-detail pre.command { max-height: 340px; overflow-y: auto; margin: 0 0 12px; }

.chip { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; }
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted-soft); display: inline-block; }
.dot.ok { background: var(--ok); } .dot.warn { background: var(--warn); } .dot.bad { background: var(--crit); }
.site-favicon { vertical-align: -3px; border-radius: 3px; margin: 0 2px; }
/* The websites list's screenshot thumb: a fixed 60px box so rows with and
   without a capture keep one left edge; the img hides on 404, the box stays. */
.site-cell { display: flex; align-items: center; gap: 12px; }
.site-cell-main { min-width: 0; }
.site-thumb {
  flex: none; display: block; width: 107px; height: 60px;
  border: 1px solid var(--line); border-radius: 6px;
  background: var(--bg); overflow: hidden;
}
.site-thumb img { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; }

/* Homepage captures on the website Overview tab: the site facts on the left,
   the mobile and desktop renders on the right, height-matched so the pair
   reads as one exhibit. Width follows each capture's own ratio. */
.ov-site-row { display: flex; gap: 24px; align-items: flex-start; flex-wrap: wrap; }
.ov-site-row .ov-site-info { flex: 1 1 340px; min-width: 0; }
.ov-site-row .site-shots-col { flex: none; max-width: 100%; }
/* max-height, never height: a fixed height plus max-width clamps into a
   squashed picture, and an unwrapped pair pushed the desktop shot clean off
   a phone screen. Wrapping puts it on its own line, ratio intact. */
.site-shots { display: flex; gap: 14px; align-items: flex-start; flex-wrap: wrap; }
.site-shots img {
  max-height: 230px; height: auto; width: auto; max-width: 100%;
  border: 1px solid var(--line); border-radius: 8px; background: var(--bg);
}

/* Websites Gallery view — three cards across, each a link to its site showing
   the desktop capture with the mobile one inset. Falls to two then one column
   as the panel narrows. Pairs with .wg-* in PanelRenderers.websiteGallery. */
.view-toggle { display: inline-flex; gap: 6px; }
.view-toggle .btn[aria-pressed="true"] { background: var(--primary); border-color: var(--primary); color: #fff; }
.wg-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
.wg-card {
  display: block; overflow: hidden; color: inherit; text-decoration: none;
  background: #fff; border: 1px solid var(--line); border-radius: var(--radius-lg);
  transition: box-shadow .15s ease, border-color .15s ease;
}
.wg-card:hover { border-color: var(--primary); box-shadow: var(--sh-pop); text-decoration: none; }
/* The whole card is one link, so the global a:hover underline would cascade
   onto the name, the slug, the meta line and the chips. The border and
   shadow above are the hover cue; no word in the card underlines. */
.wg-card:hover *, .wg-card:hover .wg-name { text-decoration: none; }
/* Fixed frame so a row of cards keeps one baseline whatever each capture's
   own ratio; the desktop shot is cropped to fill it, cover from the top. */
.wg-shots { position: relative; aspect-ratio: 16 / 10; background: var(--bg); overflow: hidden; }
/* The placeholder sits behind the desktop img; when the capture 404s the img
   hides itself (onerror) and this shows through, so the card never blanks. */
.wg-noshot {
  position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; color: var(--muted); font-size: 13px;
}
.wg-desktop {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: top center; display: block;
}
.wg-mobile {
  position: absolute; right: 10px; bottom: 10px; width: 22%; max-width: 76px;
  border: 2px solid #fff; border-radius: 6px; background: #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .25); display: block;
}
.wg-meta { padding: 10px 12px; }
.wg-title { display: flex; align-items: center; gap: 6px; }
.wg-name { font-weight: 600; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (max-width: 1100px) { .wg-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 680px)  { .wg-grid { grid-template-columns: 1fr; } }

/* The platform mark beside a site's type, and the same-size spacer the other
   types carry. Without the spacer one row in three is indented and the column
   reads as ragged — four characters of markup against an uneven column. */
.type-cell { display: inline-flex; align-items: center; gap: 6px; }
.type-mark { flex: 0 0 16px; width: 16px; height: 16px; }
.type-mark-empty { display: inline-block; }

/* The update census for a WordPress site: several badges behaving as one
   control, wrapping rather than widening the column on a narrow screen. */
.wp-updates { display: inline-flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.wp-updates:hover { text-decoration: none; }

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

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

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

.pill {
  display: inline-flex; align-items: center; vertical-align: middle;
  padding: .38em .62em; border-radius: var(--radius);
  font-size: var(--text-xs, 12px); font-weight: 700; line-height: 1;
  text-transform: uppercase; letter-spacing: .03em; white-space: nowrap;
  background: var(--muted); color: #fff;
}
.pill.ok   { background: var(--ok); }
.pill.info { background: var(--info); }
.pill.crit, .pill.bad { background: var(--crit); }
/* Amber is the one that cannot take white text: #fff on #ffc107 is about
   1.9:1, which fails at every size — and this is the pill whose entire job is
   to be read. Dark ink on the same amber is over 8:1. */
.pill.warn { background: var(--warn); color: #4a3600; }
.pill.none { background: var(--track); color: var(--muted); }
/* The DEMO visibility pill: deep orange with white ink (about 5:1) — not the
   amber .warn above, which cannot carry white text. */
.pill.demo { background: #e65100; color: #fff; }
/* Lifecycle chip and visibility pill share a cell; they must not wrap apart. */
.state-cell { white-space: nowrap; }

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

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

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

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

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

.meter { height: 6px; background: var(--track); border-radius: var(--radius-pill); overflow: hidden; min-width: 70px; }
.meter-lg { height: 14px; } .meter-lg i { transition: width .4s ease; }
.meter i { display: block; height: 100%; background: var(--ok); border-radius: inherit; }
.meter.warn i { background: var(--warn); } .meter.crit i { background: var(--crit); }
/* The bar with its number beside it — Panel.meterPct(). The bar keeps its
   min-width so forty rows of meters stay aligned whatever the figures. */
.meter-labelled { display: inline-flex; align-items: center; gap: 8px; }
.meter-labelled .meter { flex: none; }
.meter-label { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }

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

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

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

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

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

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

/* A single figure carrying its own verdict — a deliverability score, where the
   number and whether it is good are the same glance. The -ink variants, so it
   stays readable on the page background rather than only on a filled badge. */
.text-ok   { color: var(--ok-ink); }
.text-warn { color: var(--warn-ink); }
.text-crit { color: var(--crit-ink); }

/* A list of findings rather than a list of navigation. Bulletless and spaced,
   because each item is a sentence with a badge in front of it. */
ul.plain { list-style: none; margin: 0; padding: 0; }
ul.plain > li { padding: 7px 0; border-bottom: 1px solid var(--line); }
ul.plain > li:last-child { border-bottom: 0; }

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

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

input[type=text], input[type=email], input[type=password], input[type=url],
input[type=number], input[type=search], select, textarea {
  width: 100%; padding: var(--control-pad, 9px 11px);
  border: 1px solid var(--control-edge); border-radius: var(--radius);
  font-size: var(--text-ui, 14px); font-family: inherit; background: #fff; color: var(--ink);
  /* The same box as a button (line-height 1.4, --control-h), so a field and
     the button beside it share one top and bottom edge. */
  line-height: 1.4; min-height: var(--control-h, 40px); box-sizing: border-box;
}
textarea { min-height: 0; }
/* Safari and Chrome draw a search field taller than a text field unless
   told not to; one box for both. */
input[type=search] { -webkit-appearance: none; appearance: none; }
/* A small button beside a kv value sits on the value's centre line. */
.kv-value .btn-sm, .kv-value button { margin-left: 10px; vertical-align: middle; }
/* Select2 replaces most of these, but the signup shell has no JS, a view can
   opt out with data-no-select2, and every select is native for the moment
   before enhancement. Native appearance makes Safari ignore the padding
   above; drawing our own chevron is what makes it apply. */
select {
  appearance: none; -webkit-appearance: none;
  padding-right: 32px;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1.5l5 5 5-5' fill='none' stroke='%235b6b7c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 11px center;
}
input::placeholder, textarea::placeholder { color: var(--muted); opacity: 1; }
input:focus, select:focus, textarea:focus { border-color: var(--dark); }
.is-invalid, .is-invalid:focus { border-color: var(--crit); }
.is-invalid:focus-visible { outline-color: var(--crit); }
input:disabled, select:disabled, textarea:disabled { background: var(--bg); color: var(--muted); cursor: not-allowed; }

/* A value edited in its own cell, rather than behind a button and a dialog.
   The rule above makes every input full width, which is right in a form and
   wrong in a table: an unstyled number field took the whole column and turned
   "no limit" into the widest thing on the row. */
.inline-num, .inline-text { padding: 6px 9px; font-size: 13px; }
.inline-num  { width: 92px; }
.inline-text { width: 100%; max-width: 200px; }

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

/* Buttons are brand-free by design (operator's call, 2026-10-27, the same
   day the portal's went): a primary action is a dark navy fill, a secondary
   is white with a grey edge, and colour on a button means a state or a
   warning — green on, red danger, amber armed. The brand colour belongs to
   links, the sidebar and selection (active pill, page number, segmented
   option, selected row), which keep it. A page of magenta buttons shouted
   over the data it was about; do not "fix" this back. */
/* ONE button: navy, white text, the brand magenta on hover and on a
   selected one (a pressed view toggle, an active pill). There is no outlined
   "secondary" any more — every button is the same navy, quiet ones included,
   and the destructive ones go red only when reached for. The box matches an
   input's (--control-h, line-height 1.4) so a button sits level with the
   field beside it. */
button, .btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  background: var(--dark); color: #fff; border: 1px solid transparent;
  border-radius: var(--radius); padding: var(--btn-pad, 9px 16px);
  font-size: var(--text-ui, 14px); font-weight: 600; font-family: inherit; line-height: 1.4;
  min-height: var(--control-h, 40px); box-sizing: border-box;
  cursor: pointer; text-align: center; transition: background .12s ease, border-color .12s ease;
}
button:hover, .btn:hover { background: var(--primary); border-color: var(--primary); color: #fff; filter: none; text-decoration: none; }
button[aria-pressed="true"], .btn[aria-pressed="true"], .btn.active, button.active {
  background: var(--primary); border-color: var(--primary); color: #fff;
}
/* Focus is the shared ring at the foot of this file. */
button:disabled, .btn:disabled, .btn.disabled {
  opacity: .55; cursor: not-allowed; filter: none; pointer-events: none;
}
.btn-secondary { background: var(--dark); color: #fff; border-color: transparent; }
.btn-secondary:hover, .btn-secondary:focus-visible { background: var(--primary); border-color: var(--primary); color: #fff; }
/* Every button is primary already; this is the same navy, kept as a class
   because four views and PanelViewTests name it for an <a> that must read
   as a button. */
.btn-primary { background: var(--dark); color: #fff; }
.btn-primary:hover { background: var(--dark-ink); }
.btn-outline { background: var(--dark); color: #fff; border-color: transparent; }
.btn-outline:hover { background: var(--primary); border-color: var(--primary); filter: none; }
.btn-danger { background: var(--crit); }
.btn-danger:hover { background: var(--crit-ink); }

/* A state button: green means "this is on" and clicking it turns it off;
   the off state is the plain .btn-secondary. Colour is the STATE, never the
   action — a red "Turn off" button read as "this is off" to an eye scanning
   a column of them. Soft at rest for the same reason as .btn-quiet below,
   solid on hover and focus, where intent already exists. */
.btn-on { background: rgba(var(--ok-rgb),.12); color: var(--ok-ink); border-color: var(--ok-edge); }
.btn-on:hover, .btn-on:focus-visible { background: var(--ok); border-color: var(--ok); color: #fff; filter: none; }

/* The text siblings of .btn-icon-quiet and .btn-icon-danger below, on the same
   reasoning: a row of filled pink pills shouts over the data it belongs to,
   and a row of solid red ones sends the eye to the one irreversible control
   before the thing it is about. Nine views had been asking for .btn-quiet and
   one for .btn-danger-quiet with nothing declared, so every admin row action
   rendered as a full primary button — the exact thing the rule below exists
   to prevent. */
.btn-quiet, .btn-danger-quiet {
  background: var(--dark); color: #fff; border: 1px solid transparent;
}
.btn-quiet:hover, .btn-quiet:focus-visible {
  background: var(--primary); color: #fff; border-color: var(--primary); filter: none;
}
.btn-danger-quiet:hover, .btn-danger-quiet:focus-visible {
  background: var(--crit); color: #fff; border-color: var(--crit); filter: none;
}
.btn-quiet:disabled, .btn-danger-quiet:disabled { opacity: .45; cursor: default; }
/* An armed button waiting for its second click — not destructive, but not
   something to do by accident either (reading a customer's mailbox). */
.btn-warn { background: var(--warn); color: #1c1917; }
.btn-warn:hover { background: var(--warn-ink); color: #1c1917; }

/* A page-head button that opens a small action menu. <details>-based so the
   menu opens before (or without) panel.js; the outside-click close bound at
   DOMContentLoaded is polish, not load-bearing. */
.btn-drop { position: relative; display: inline-block; }
.btn-drop > summary { list-style: none; user-select: none; }
.btn-drop > summary::-webkit-details-marker { display: none; }
.btn-drop > summary::after { content: '▾'; font-size: 11px; margin-left: 2px; }
.btn-drop-menu {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 40;
  min-width: 210px; padding: 6px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-lg); box-shadow: var(--sh-pop);
}
.btn-drop-menu a {
  display: block; padding: 8px 12px; border-radius: var(--radius);
  color: var(--ink); font-size: 14px; font-weight: 500; white-space: nowrap;
}
.btn-drop-menu a:hover { background: var(--track); text-decoration: none; }
/* One height whether or not the button carries a glyph: the icon is sized
   to the text and the box is pinned, so an "Update" beside an "On" with a
   tick no longer sits a few pixels lower than its neighbour. */
.btn-sm { padding: var(--btn-sm-pad, 5px 10px); font-size: 12.5px; min-height: 30px; line-height: 1.3; }
/* A row of controls on one line: a select or input and the button that acts
   on it, all one height. Named by admin/invoices and the client's
   Subscriptions tab; it had no rule, so the controls stacked. */
.form-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin: 6px 0 14px; }
.form-row > input, .form-row > select { width: auto; flex: 1 1 160px; }
.form-row > input[class*="w-"] { flex: 0 0 auto; }
.form-row > .select2-container { width: auto !important; flex: 1 1 160px; }
.form-row > label { margin: 0; }
.btn-sm .material-icons { font-size: 16px; }
.btn-lg { padding: 12px 22px; font-size: 15px; }
.btn-block { display: flex; width: 100%; }
/* A square icon button: one glyph, its words in a tooltip (data-tip) and an
   aria-label. 36px square, 30px as a .btn-sm in a table row. */
.btn-icon { padding: 0; width: 36px; min-width: 36px; height: 36px; display: inline-flex;
            align-items: center; justify-content: center; line-height: 0; }
.btn-icon.btn-sm { width: 30px; min-width: 30px; height: 30px; }
.btn-icon .material-icons { font-size: 18px; margin: 0; }
.btn-icon svg { width: 16px; height: 16px; }
button .material-icons, .btn .material-icons { font-size: 18px; }

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

/* Its non-destructive sibling — same quiet-until-reached-for reasoning, but
 * colouring toward the accent instead of red. A bare .btn-icon keeps the
 * filled primary background every button has, which in a table row is a
 * solid pink pill shouting over the data; this is the row-action shape. */
.btn-icon-quiet {
  background: var(--dark); color: #fff;
  border: 1px solid transparent; line-height: 0;
}
.btn-icon-quiet:hover,
.btn-icon-quiet:focus-visible {
  background: var(--primary); color: #fff;
  border-color: var(--primary); filter: none;
}
.btn-icon-quiet:disabled { opacity: .45; cursor: default; }

/* Right-aligned, and only as wide as the control — otherwise the actions
   column steals space from the domain, which is what people read. */
td.row-actions, th.row-actions, table.list th.row-actions { text-align: right; width: 1%; white-space: nowrap; }
/* The buttons in an actions cell share one row and one centre line; the
   browser's inline layout put each on its own baseline. */
td.row-actions { vertical-align: middle; }
td.row-actions > * { vertical-align: middle; }
td.row-actions > button + button, td.row-actions > button + .btn, td.row-actions > .btn + button { margin-left: 6px; }

.btn-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 16px; align-items: center; }
/* Segmented radio group (the SEO Links scope picker): plain radios, no Select2. */
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: var(--radius-pill); overflow: hidden; }
.seg label { display: inline-flex; align-items: center; gap: 6px; padding: .4em .9em; font-size: .9em; cursor: pointer; border-right: 1px solid var(--line); }
.seg label:last-child { border-right: 0; }
.seg label:has(input:checked) { background: rgba(var(--primary-rgb),.1); color: var(--primary-ink); }
.seg input { margin: 0; }

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

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

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

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

/* A block meant to be copied whole and pasted somewhere else — a zone file,
 * a config stanza. Unlike pre.command it does NOT wrap: these are line-per-
 * record and a soft-wrapped DKIM line stops looking like one record. It
 * scrolls inside its own box instead, which is the difference between a wide
 * block and a page whose whole layout is as wide as its widest string. */
pre.code-block {
  background: var(--surface-alt); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 12px 14px; margin: 0;
  font-size: 13px; font-family: var(--font-mono); line-height: 1.7;
  white-space: pre; overflow-x: auto; max-width: 100%;
}

/* ------------------------------------------------------- click-to-copy
 *
 * A value shown head-and-tail with the middle elided (Panel.copyValue). Looks
 * like the text it replaces until it is reached for: it sits inline in a table
 * cell, and something button-shaped there reads as an action on the row rather
 * than as the value itself. The full string is on the element, not on screen. */
.copy-value {
  font: inherit; color: inherit; text-align: left;
  background: none; border: 0; border-radius: var(--radius);
  padding: 1px 5px; margin: -1px -5px;
  max-width: 100%; cursor: pointer; word-break: break-all;
}
.copy-value:hover,
.copy-value:focus-visible {
  background: rgba(var(--primary-rgb),.07); color: var(--primary-ink);
  filter: none;
}
.copy-value::after {
  font-family: "Material Icons"; content: "content_copy";
  font-size: 13px; vertical-align: -2px; margin-left: 6px;
  opacity: 0; transition: opacity .12s ease;
}
.copy-value:hover::after,
.copy-value:focus-visible::after { opacity: .7; }

/* The mail DNS record table.
 *
 * The first column carries a sentence explaining what the record is for. Left
 * to itself the auto layout gave the DKIM value every pixel it asked for and
 * wrapped that sentence one word per line. A floor on the two text columns and
 * an elided value (above) is what keeps the table readable. */
.table-dns td.dns-record { min-width: 190px; }
.table-dns td.dns-name   { min-width: 210px; }
.table-dns td.dns-value  { max-width: 460px; }
.table-dns td.dns-state  { min-width: 160px; }

/* A section folded away because it is bulky and rarely wanted, not because it
   is advanced. The marker stays, so it reads as "there is more here" rather
   than as a control. */
details.fold { margin-top: 16px; }
details.fold > summary {
  cursor: pointer; font-size: 13px; color: var(--muted);
  padding: 4px 0; width: fit-content;
}
details.fold > summary:hover { color: var(--primary); }
details.fold[open] > summary { margin-bottom: 10px; }

/* --------------------------------------------------- staging environments */
/* One card per environment. The staging copy's next step is a callout with
   the panel accent down its left edge, so the eye lands on what to do; the
   rest of the card is quiet rows of small controls. */
.env-card {
  border: 1px solid var(--line); border-radius: var(--radius-lg);
  padding: 14px 16px; margin: 0 0 12px;
}
.env-card-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 15px; }
.env-card-head strong { font-family: var(--font-mono); font-weight: 600; overflow-wrap: anywhere; }
.env-next {
  margin: 12px 0 0; padding: 12px 14px;
  background: rgba(var(--primary-rgb),.05); border-left: 3px solid var(--primary);
  border-radius: 0 var(--radius) var(--radius) 0;
}
.env-next-title { display: flex; align-items: center; gap: 6px; font-weight: 600; font-size: 14px; }
.env-next-title .material-icons { font-size: 18px; color: var(--primary); }
.env-next p { margin: 6px 0 8px; }
table.env-dns { border-collapse: collapse; font-size: 13px; }
table.env-dns th { text-align: left; font-weight: 600; color: var(--muted); padding: 2px 18px 4px 0; }
table.env-dns td { padding: 3px 18px 3px 0; }
.env-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.env-row .select2-container { width: auto !important; min-width: 170px; }
.env-foot { padding-top: 10px; border-top: 1px solid var(--line); }
.env-create > summary { font-size: 14px; color: var(--ink); font-weight: 600; }
.env-create[open] > summary { margin-bottom: 4px; }
#env-push-row label { margin: 0; }

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

body.centred { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.auth-card {
  background: var(--surface); border: 1px solid var(--card-edge); border-radius: var(--radius-card);
  box-shadow: none; padding: 32px; width: 100%; max-width: 400px;
}
/* The lockup sits centred over the form on both auth pages — a card, not a
   bar, so the mark belongs on the centreline of the inputs below it. */
.auth-brand { margin-bottom: 24px; text-align: center; }
.auth-brand img { display: inline-block; width: 192px; height: auto; }
/* Both auth pages also add the favicon and icons.css; see the note in each. */
.auth-card button { width: 100%; margin-top: 18px; }
.auth-card > p:last-child { text-align: center; }

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

/* One row that never wraps. Nineteen website tabs used to fold onto a second
   ragged line under the page head; now the strip scrolls sideways instead.
   The scrollbar is hidden and the AFFORDANCE is the fade at whichever edge
   has more tabs — Panel.enhanceTabStrips() toggles the two classes below from
   the scroll position, and brings the active tab into view on load. A mask,
   not a pseudo-element: a pseudo-element inside a scroll container scrolls
   away with the content, a mask stays on the box. */
.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--line); margin-bottom: 18px;
        flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch;
        scrollbar-width: none; scroll-snap-type: x proximity; position: relative; }
.tabs::-webkit-scrollbar { display: none; }
.tabs a { padding: 10px 14px; font-size: 14px; font-weight: 600; color: var(--muted); border-bottom: 2px solid transparent;
          white-space: nowrap; flex: 0 0 auto; scroll-snap-align: start; }
/* The arrows. Panel.enhanceTabStrips() wraps every .tabs in .tabs-wrap and
   adds one button per side; each is shown only while that side has hidden
   tabs, and a click scrolls the strip by most of its width — the same motion
   as a drag, in one press. The fade above stays as the passive cue. */
.tabs-wrap { position: relative; margin-bottom: 18px; }
.tabs-wrap > .tabs { margin-bottom: 0; }
.tabs-arrow {
  position: absolute; top: 0; bottom: 1px; width: 32px; padding: 0; margin: 0;
  display: none; align-items: center; justify-content: center;
  background: var(--bg); color: var(--muted); border: 0; border-radius: 0;
  cursor: pointer; z-index: 2; filter: none; box-shadow: none;
}
.tabs-arrow .material-icons { font-size: 22px; }
.tabs-arrow:hover { color: var(--primary); background: var(--bg); filter: none; }
.tabs-arrow-left  { left: 0;  box-shadow: 8px 0 12px -4px var(--bg); }
.tabs-arrow-right { right: 0; box-shadow: -8px 0 12px -4px var(--bg); }
.tabs-wrap.tabs-more-left  > .tabs-arrow-left  { display: inline-flex; }
.tabs-wrap.tabs-more-right > .tabs-arrow-right { display: inline-flex; }
/* The fades live on the strip and use the same two flags. */
.tabs.tabs-more-right {
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 56px), transparent);
          mask-image: linear-gradient(90deg, #000 calc(100% - 56px), transparent); }
.tabs.tabs-more-left {
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 56px);
          mask-image: linear-gradient(90deg, transparent, #000 56px); }
.tabs.tabs-more-left.tabs-more-right {
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 56px, #000 calc(100% - 56px), transparent);
          mask-image: linear-gradient(90deg, transparent, #000 56px, #000 calc(100% - 56px), transparent); }
.tabs a:hover { color: var(--ink); text-decoration: none; }
.tabs a.active { color: var(--primary); border-bottom-color: var(--primary); }

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

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

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

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

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

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

/* Several panes in one card — Panel.paneTabs(). The pill row stands in for
   the card's heading, so it carries the heading's weight and a rule under it;
   a section that is not the chosen tab is [hidden] and stays out of flow.
   The pill row is [hidden] itself when PHP gating left a single section. */
.pane-tabs { margin: -4px 0 16px; padding-bottom: 8px; border-bottom: 1px solid var(--line); }
.pane-tabs button { font-size: 14px; }
.pane-tabs button.active { color: #fff; }
.pane-tabs button.active .muted { color: rgba(255,255,255,.75); }
.pane-tabs[hidden] { display: none; }
[data-pane-tab][hidden] { display: none; }

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

/* An on/off switch: a checkbox drawn as a sliding knob. The input stays a
   real checkbox — focus, keyboard and change events all come free — and the
   drawing is the label's two pseudo-elements. Used by the Security tab's
   posture rows; the read-only text form above remains for servers whose
   toolkit cannot flip a single check yet. */
.switch { position: relative; display: inline-block; width: 40px; height: 22px;
          margin-left: auto; flex: 0 0 auto; }
.switch input { position: absolute; opacity: 0; width: 100%; height: 100%;
                margin: 0; cursor: pointer; }
.switch .switch-track { position: absolute; inset: 0; border-radius: 11px;
          background: var(--track, #d7dce2); transition: background .15s ease;
          pointer-events: none; }
.switch .switch-track::after { content: ''; position: absolute; top: 3px; left: 3px;
          width: 16px; height: 16px; border-radius: 50%; background: #fff;
          box-shadow: 0 1px 2px rgba(0,0,0,.25); transition: left .15s ease; }
.switch input:checked + .switch-track { background: var(--ok); }
.switch input:checked + .switch-track::after { left: 21px; }
.switch input:focus-visible + .switch-track { outline: 2px solid var(--blue);
          outline-offset: 2px; }
.switch input:disabled { cursor: not-allowed; }
.switch input:disabled + .switch-track { opacity: .45; }
.switch.busy .switch-track { opacity: .6; }
/* The posture rows push their switch to the far edge; in a control-row it
   sits inline with the select beside it instead. */
.switch.switch-inline { margin-left: 0; align-self: center; }

/* Every bare checkbox renders as the same on/off switch, so plain toggles
   across the app are consistent without extra markup — the real checkbox stays
   underneath (focus, keyboard, change all free). Anything genuinely
   multi-select opts out with .checkbox-plain. */
input[type="checkbox"]:not(.switch input):not(.checkbox-plain) {
  appearance: none; -webkit-appearance: none;
  flex: 0 0 auto; width: 40px; height: 22px; margin: 0 8px 0 0; padding: 0;
  background: var(--track, #d7dce2); border: 0; border-radius: 11px;
  position: relative; cursor: pointer; vertical-align: -6px;
  transition: background .15s ease;
}
input[type="checkbox"]:not(.switch input):not(.checkbox-plain)::after {
  content: ''; position: absolute; top: 3px; left: 3px;
  width: 16px; height: 16px; border-radius: 50%; background: #fff;
  box-shadow: 0 1px 2px rgba(0,0,0,.25); transition: left .15s ease;
}
input[type="checkbox"]:not(.switch input):not(.checkbox-plain):checked { background: var(--ok); }
input[type="checkbox"]:not(.switch input):not(.checkbox-plain):checked::after { left: 21px; }
input[type="checkbox"]:not(.switch input):not(.checkbox-plain):focus-visible {
  outline: 2px solid var(--blue); outline-offset: 2px;
}
input[type="checkbox"]:not(.switch input):not(.checkbox-plain):disabled { opacity: .5; cursor: not-allowed; }

/* CS-Cart config editor: lay a group's controls out in responsive columns so a
   long list of toggles is not one tall stack. */
.csc-fields { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 2px 28px; align-items: center; }
.csc-fields > label { margin: 6px 0; }

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

/* Inline control rows — a field and its button, several to a line. */
.control-row { display: flex; gap: 8px; align-items: flex-start; flex-wrap: wrap;
               margin-top: 12px; }
/* display:flex above outranks the browser's [hidden]{display:none}, so a
   row a renderer hides by attribute (the staging push row until the group
   has two members) stayed on screen with empty selects. */
.control-row[hidden] { display: none; }
.control-row input, .control-row select { width: auto; min-width: 200px; flex: 0 1 auto; }
.control-row .select2-container { width: auto !important; min-width: 200px; }
.control-row button { flex: 0 0 auto; }
.control-row.wide input { flex: 1 1 260px; }

/* A grid of small labelled fields — the PHP limits, the New site form. */
.field-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
              gap: 0 14px; align-items: end; }
.field-grid label { margin-top: 0; }
/* A form of short fields, two to a row: name beside email, company beside
   phone. A textarea, a checkbox or the save row spans both (.field-span).
   One column again on a phone. */
.field-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0 18px; align-items: start; }
.field-grid-2 label { margin-top: 14px; }
.field-grid-2 > .field-span { grid-column: 1 / -1; }
@media (max-width: 640px) { .field-grid-2 { grid-template-columns: 1fr; } }

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

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

/* A filter bar above a list. Narrower than the list card it sits on, because
   it is a control and not content. */
.card-filter { display: flex; align-items: center; gap: 12px; padding: 14px 18px; }
.card-filter label { margin: 0; white-space: nowrap; }
.card-filter .pills { margin-bottom: 0; }
.card-filter select { width: auto; min-width: 260px; }
.card-filter .select2-container { width: auto !important; min-width: 260px; }
/* Inputs are width:100% by default, which in a flex row eats every control
   beside them. */
.card-filter input[type=search] { width: auto; min-width: 220px; }
.card-filter #w-q-count { white-space: nowrap; }

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

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

/* A command control sitting in a tab's prose. */
.cmd-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 12px; }
.cmd-row .muted { flex: 1 1 240px; font-size: 13px; }
/* A handover, not an action: the control opens a modal carrying a shell
   command to copy, so it must not dress like the buttons that run things. */
.cmd-cli {
  display: inline-flex; align-items: center; gap: 7px;
  background: transparent; color: var(--muted); font-weight: 600;
  border: 1px dashed var(--line-strong);
}
.cmd-cli:hover { background: var(--track); color: var(--ink); filter: none; }
.cmd-glyph { font-family: var(--font-mono); font-size: var(--text-xs, 12px); }
.cmd-tag {
  font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em; padding: 1px 6px; border-radius: 999px;
  background: var(--track); color: var(--muted);
}
.cmd-danger { background: var(--crit); }

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

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

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

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

@media (max-width: 900px) {
  .nav-toggle { display: inline-flex; }
  .topbar { gap: 10px; padding: 0 14px; }
  .brand-logo { height: 28px; }
  /* The bar keeps the logo and the bell; everything else moves into the
     drawer, where the account name (and switcher) and Log out now live. */
  .user-email, .topbar .logout,
  .topbar .account-switch, .topbar .account-name { display: none; }
  .account-switch-side { display: block; }
  /* With the switcher showing the current account, the static name above it
     is the same string twice; the role line stays. */
  .sidebar-account:has(.account-switch-side) .sidebar-account-name { display: none; }
  .nav-mobile-only { display: block; }

  /* The dropdown would overflow a narrow viewport if it stayed anchored to
     the bell; pin it to the screen edges instead. */
  .notify { position: static; }
  .notify-pop { left: 8px; right: 8px; width: auto; top: calc(var(--topbar-h) + 6px); position: fixed; }

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

  .content { padding: var(--content-pad-m, 18px 16px 36px); }
  /* Side-by-side panes and the stat tiles narrow before they overflow. */
  .pane-row { grid-template-columns: 1fr; }
  .tiles { grid-template-columns: repeat(2, 1fr); }
  /* The tab strip bleeds to the screen edges like a native tab bar, so the
     first and last tabs are not cut against a 16px gutter mid-swipe. */
  .tabs-wrap { margin-left: -16px; margin-right: -16px; }
  .tabs { padding: 0 16px; scroll-padding: 0 16px; }
  .tiles { grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; }
  .kv { grid-template-columns: 1fr; }
  .kv-label { padding-bottom: 0; border-bottom: 0; }
  .kv > :nth-last-child(2) { border-bottom: 0; }
  .card-filter { flex-wrap: wrap; }
  .card-filter select, .card-filter input[type=search],
  .card-filter .select2-container { min-width: 0; width: 100% !important; }

  /* iOS zooms the page toward any focused field under 16px; hold every text
     control at 16px here so focus never moves the viewport. Desktop keeps
     the denser sizes above. */
  input[type=text], input[type=email], input[type=password], input[type=url],
  input[type=number], input[type=search], select, textarea,
  .inline-num, .inline-text, .pagination-size select,
  .cmd-arg > input, .select2-search__field { font-size: 16px; }
}

/* Reduced motion: the global rule at the foot of this file. */

/* WordPress import cards on the Websites list — an in-flight migration before
   it becomes a real website row. */
.imp-card   { padding: 11px 16px; margin-bottom: 10px; }
.imp-head   { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }
/* Card layout, scoped so the import's own progress page keeps its own sizing:
   the bar and its one-line status stack in a column, the actions sit beside
   them. Tight vertical rhythm — the status reads directly under the bar. */
.imp-card .imp-row     { display: flex; align-items: center; gap: 16px; }
.imp-card .imp-main    { flex: 1 1 auto; min-width: 0; }
.imp-card .imp-bar     { height: 8px; margin: 0; }
.imp-card .imp-actions { flex: 0 0 auto; margin-top: 0; }
.imp-bar    { height: 10px; background: var(--track); border-radius: var(--radius-pill); overflow: hidden; }
.imp-bar-fill { display: block; height: 100%; border-radius: inherit; background: var(--ok); transition: width .4s ease; }
.imp-bar-run  { background: rgb(var(--info-rgb)); }
.imp-bar-ok   { background: var(--ok); }
.imp-bar-bad  { background: var(--crit); }
.imp-detail { margin-top: 5px; }
.imp-steps  { list-style: none; padding: 0; margin: 12px 0 0; display: grid; gap: 6px; }
.imp-step   { display: flex; align-items: center; gap: 8px; font-size: .9rem; color: var(--muted); }
.imp-step i { font-size: 18px; }
.imp-step-ok   { color: var(--ok-ink); }
.imp-step-run  { color: var(--info-ink); font-weight: 600; }
.imp-step-run i { animation: imp-spin 1.2s linear infinite; }
.imp-step-wait { color: var(--muted); }
.imp-step-crit { color: var(--crit-ink); font-weight: 600; }
@keyframes imp-spin { to { transform: rotate(360deg); } }
.imp-actions { margin-top: 12px; display: flex; gap: 8px; }

/* ---- /admin/performance: the three-way time split ----------------------
   One bar per row: amber is time queued on the session file lock, primary is
   time inside outbound dials to managed servers, grey is everything else
   (PHP + database). The bar is proportion only; the milliseconds ride in the
   segment titles and the aria-label. */
.perf-bar {
  display: inline-flex; width: 160px; height: 10px; border-radius: 5px;
  overflow: hidden; background: var(--line); vertical-align: middle;
}
.perf-seg { display: block; height: 100%; min-width: 2px; }
.perf-seg-wait   { background: var(--warn); }
.perf-seg-egress { background: var(--primary); }
.perf-seg-rest   { background: var(--muted-soft); }

/* The website Files tab — a breadcrumb row and a denser listing. Every cell
   but the name is nowrap; the name takes the slack (max-width:0 is the
   table-layout trick that lets it shrink) and ellipsises instead of wrapping
   "wp-content" onto two lines. */
.fm-crumbs { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; font-size: 14px;
             align-self: center; }
.fm-crumbs a { text-decoration: none; padding: 2px 6px; border-radius: 4px; font-family: var(--font-mono, monospace);
               font-size: 13px; display: inline-flex; align-items: center; }
.fm-crumbs a:hover { background: var(--track); }
.fm-crumbs .material-icons { font-size: 18px; }
table.list.fm-table td { padding: 6px 10px; white-space: nowrap; }
table.list.fm-table td.fm-name { max-width: 0; width: 100%; overflow: hidden; text-overflow: ellipsis; }
table.list.fm-table td.fm-name a { text-decoration: none; font-weight: 600; }
table.list.fm-table .fm-icon { font-size: 18px; vertical-align: -4px; margin-right: 6px; color: var(--muted); }
table.list.fm-table .fm-icon-d { color: var(--primary); }
table.list.fm-table td.fm-size,
table.list.fm-table td.fm-mtime { font-variant-numeric: tabular-nums; color: var(--muted); font-size: 13px; }
table.list.fm-table td.fm-mode code { font-size: 12px; }
table.list.fm-table td.fm-owner { color: var(--muted); font-size: 13px; }
table.list.fm-table td.fm-up a { text-decoration: none; display: inline-flex; align-items: center; gap: 4px; }
table.list.fm-table td.fm-up .material-icons { font-size: 16px; }
/* Up to six 30px icon buttons per row, one gap between them, each with its
   words in a tooltip. */
table.list.fm-table td.row-actions { padding-left: 4px; }
table.list.fm-table td.row-actions .fm-acts { display: inline-flex; gap: 4px; align-items: center; }
table.list.fm-table td.row-actions .material-icons { font-size: 16px; }

/* ------------------------------------------- Settings → Notifications table */
.nt-table th.nt-col, .nt-table td.nt-cell { text-align: center; white-space: nowrap; }
.nt-table td.nt-cell input[type="checkbox"] { margin: 0; }
.nt-table td.nt-extra { text-align: left; min-width: 220px; }
.nt-table td.nt-extra input { margin: 0; width: 100%; min-width: 200px; font-size: .9em; }
.nt-table tr.nt-cat td { background: var(--surface-alt); padding-top: 10px; padding-bottom: 6px; }
.nt-table td.nt-state { white-space: nowrap; }
.nt-table .tag { display: inline-block; padding: 1px 7px; border-radius: 10px; background: var(--surface-alt); }

@media (max-width: 480px) {
  .tiles { grid-template-columns: 1fr; }
}

/* ------------------------------------------------ accessibility floor
   WCAG 2.2 AA, applied the same way in portal.css (2026-10-27):
   - one focus ring: 2px solid, offset so it sits outside the control on
     the page ground where it always has 3:1 (a tinted box-shadow never did);
   - links in running text are underlined (colour alone is 3:1 against the
     ink, under the 4.5 text needs); navigation, buttons, tabs, tables and
     item names identify their links by place and keep the underline for
     hover;
   - nothing readable under 12px; placeholders and kickers in --muted;
   - every pointer target 24px or taller; numbers in tabular figures;
   - reduced motion switches every transition off, not three of them. */
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible,
textarea:focus-visible, summary:focus-visible, [tabindex]:focus-visible, .btn:focus-visible {
  outline: 2px solid var(--focus); outline-offset: 2px; box-shadow: none;
}
input:focus-visible, select:focus-visible, textarea:focus-visible { outline-offset: 0; }
a { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px;
    text-decoration-color: color-mix(in srgb, var(--primary) 45%, transparent); }
a:hover { text-decoration-color: currentColor; }
.nav-list a, .btn, .brand, .tabs a, .breadcrumb a, table.list a, .kv a, .item-name a, a.badge,
.pills a, .page-item, .notify-item, .tile a, .fm-crumbs a, .btn-drop a, .page-head a, .pane > h2 a {
  text-decoration: none;
}
table.list a:hover, .kv a:hover, .breadcrumb a:hover, .pane > h2 a:hover { text-decoration: underline; }
a.badge, .badge-pill { min-height: 24px; }
.tile-value, .kv-value, .inv-num { font-variant-numeric: tabular-nums; }
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important;
                           animation-iteration-count: 1 !important; scroll-behavior: auto !important; }
}

/* ------------------------------------------------------------ tooltips
   data-tip="…" on an icon button draws its words above it on hover and on
   keyboard focus. For sighted pointers only: the button also carries an
   aria-label, which is what a screen reader gets. Pure CSS so it works in
   the same markup on both sites and needs no script. */
[data-tip] { position: relative; }
[data-tip]::after {
  content: attr(data-tip); position: absolute; left: 50%; bottom: calc(100% + 8px);
  transform: translate(-50%, 4px); padding: 5px 9px; border-radius: var(--radius-pill, 999px);
  background: var(--ink); color: #fff; font-size: 12px; font-weight: 500; line-height: 1.3;
  white-space: nowrap; letter-spacing: 0; text-transform: none;
  opacity: 0; visibility: hidden; pointer-events: none; z-index: 60;
  transition: opacity .12s ease, transform .12s ease, visibility 0s linear .12s;
}
[data-tip]::before {
  content: ""; position: absolute; left: 50%; bottom: calc(100% + 3px);
  transform: translate(-50%, 4px); border: 5px solid transparent; border-top-color: var(--ink);
  border-bottom: 0; opacity: 0; visibility: hidden; pointer-events: none; z-index: 60;
  transition: opacity .12s ease, transform .12s ease, visibility 0s linear .12s;
}
[data-tip]:hover::after, [data-tip]:focus-visible::after, [data-tip].is-hover::after,
[data-tip]:hover::before, [data-tip]:focus-visible::before, [data-tip].is-hover::before {
  opacity: 1; visibility: visible; transform: translate(-50%, 0); transition-delay: 0s;
}
/* The first control in a row has no room on its left for a centred tip. */
[data-tip].tip-start::after { left: 0; transform: translate(0, 4px); }
[data-tip].tip-start:hover::after, [data-tip].tip-start:focus-visible::after { transform: translate(0, 0); }

/* ---------------------------------------------------------- chart tip
   Chart.js 2 paints its tooltip on the canvas, so a tall multi-series one
   is cut off at the canvas edge. Panel.chart() fills this box instead and
   hangs it ABOVE the hovered point, where the pane has room. */
.chart-tip {
  position: absolute; z-index: 50; pointer-events: none;
  transform: translateX(-50%); min-width: 120px; max-width: 320px;
  background: var(--ink); color: #fff; border-radius: var(--radius-lg);
  padding: 8px 10px; font-size: 12px; line-height: 1.4; box-shadow: var(--sh-pop);
  opacity: 0; transition: opacity .1s ease;
}
.chart-tip.is-on { opacity: 1; }
.chart-tip-title { font-weight: 600; margin-bottom: 4px; }
.chart-tip-row { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
.chart-tip-row i { width: 10px; height: 10px; border-radius: 2px; flex: none; }
.chart-tip-row b { font-weight: 600; margin-left: auto; font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------ utilities
   Named once so views carry no style="" of their own. Widths in em so a
   field sized for its content keeps that size at any zoom. */
.w-5 { width: 5em; } .w-7 { width: 7em; } .w-8 { width: 8em; } .w-11 { width: 11em; } .w-16 { width: 16em; }
.mw-8 { max-width: 8em; } .mw-10 { max-width: 10em; } .mw-16 { max-width: 16em; }
.mw-90 { max-width: 90px; } .mw-110 { max-width: 110px; } .mw-150 { max-width: 150px; }
.mw-160 { max-width: 160px; } .mw-180 { max-width: 180px; } .mw-220 { max-width: 220px; }
.mw-230 { max-width: 230px; } .mw-260 { max-width: 260px; } .mw-320 { max-width: 320px; } .mw-420 { max-width: 420px; }
.self-center { align-self: center; } .self-end { align-self: end; } .flex-1 { flex: 1; }
.mt-0 { margin-top: 0; } .mt-6 { margin-top: 6px; } .mt-8 { margin-top: 8px; } .mt-10 { margin-top: 10px; }
.mt-12 { margin-top: 12px; } .mt-14 { margin-top: 14px; } .mt-16 { margin-top: 16px; } .mt-18 { margin-top: 18px; }
.mt-22 { margin-top: 22px; } .mb-6 { margin-bottom: 6px; } .mb-10 { margin-bottom: 10px; }
.ml-6 { margin-left: 6px; } .ml-8 { margin-left: 8px; } .ml-auto { margin-left: auto; } .mr-6 { margin-right: 6px; }
.m-0 { margin: 0; } .ml-24 { margin-left: 24px; }
.prewrap { white-space: pre-wrap; word-break: break-word; }
.break-all { word-break: break-all; }
.text-right { text-align: right; }
/* The expanded row under a list row: its top padding would double the
   parent's bottom padding. */
tr.detail-row > td, tr.u-detail > td, tr.j-detail > td { padding-top: 0; }
/* A list row that opens on click (the invoice list): the pointer says so. */
table.list tr.clickable { cursor: pointer; }
table.list tr.clickable:hover td { background: var(--bg); }
/* The small marks beside a site name in the websites list (lock, rocket,
   guard): inline, text-coloured, sitting on the name's centre line. */
.site-mark { margin-left: 4px; vertical-align: -3px; display: inline-block; line-height: 0; }
.site-mark-glyph { font-size: 15px; vertical-align: -2px; line-height: 1; }
.site-mark-ok { color: var(--ok); }
.site-mark-off { color: var(--muted-soft); }
