/* ═══════════════════════════════════════════════════════════════════════════
   Cachivo Proxy Panel — design layer
   Loaded AFTER themes/app/ecosystem so it wins on conflicts. Uses only design
   tokens (var(--…)) so both themes stay correct.

   Principles this file enforces, because the panel drifted from all four:

   1. COLOUR MEANS SOMETHING. Green = healthy/served, amber = needs attention,
      red = blocked/failed, blue = interactive. A tile is only coloured when its
      value has a state; four different accents on four neutral counters is
      decoration that trains people to ignore colour.
   2. NUMBERS ARE UI, NOT LOGS. Figures are set in the UI font with tabular
      numerals. Monospace is reserved for things that really are code:
      hostnames, IPs, config, rule expressions.
   3. THE PAGE IS AS WIDE AS THE SCREEN. Forms used to sit in a 620px column on
      a 1440px display with two thirds of the page empty, which reads as broken
      rather than spacious. Content flows into a responsive grid instead.
   4. ONE TITLE PER PAGE. The topbar used to repeat the page heading directly
      above the page heading.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── 0. Scale ─────────────────────────────────────────────────────────────── */
:root {
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 20px; --sp-6: 24px; --sp-8: 32px; --sp-10: 40px;
  --r-sm: 8px;  --r-md: 11px; --r-lg: 14px; --r-xl: 18px;
  /* One shared easing so every transition in the panel feels like one product. */
  --ease: cubic-bezier(.4, 0, .2, 1);
  --content-max: 1500px;
}

/* ── 1. Shell ─────────────────────────────────────────────────────────────
   `grid-template-columns: 248px 1fr` resolves to minmax(auto, 1fr): the column
   refuses to shrink below its content's min-content width, so one wide table
   pushed the entire shell past the viewport. minmax(0,1fr) + min-width:0 lets
   it shrink and hands overflow to the scroll containers built for it. */
.app-shell { grid-template-columns: 250px minmax(0, 1fr); }
.app-shell main.content { min-width: 0; }
.app-shell > * { min-width: 0; }
/* No-sidebar shell (the public share/traffic link, rendered with user=None).
   The base grid reserves a 250px sidebar column and its template-areas expect a
   "sidebar" column on every row, so with no sidebar element the content sat in
   column 2 behind a 250px empty band — the "shifted right / not full width" bug.
   Drop the sidebar column and area entirely so the content spans the row. */
.app-shell--no-sidebar {
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "header" "main" "footer";
}

/* Sortable table headers (table-sort.js). Click to sort; a caret shows the
   active column + direction. Kept subtle so unsorted headers read normally. */
th.th-sortable { cursor: pointer; user-select: none; white-space: nowrap; }
th.th-sortable::after {
  content: "⇅"; margin-inline-start: 6px; opacity: .35; font-size: .85em;
}
th.th-sortable[aria-sort="ascending"]::after { content: "↑"; opacity: .9; }
th.th-sortable[aria-sort="descending"]::after { content: "↓"; opacity: .9; }
th.th-sortable:hover { color: var(--fg, inherit); }
th.th-sortable:focus-visible { outline: 2px solid var(--accent, #2563eb); outline-offset: -2px; }
@media (max-width: 880px) {
  .app-shell { grid-template-columns: minmax(0, 1fr); }
  /* With the drawer open the scrim (z-55) otherwise covers the sticky header
     (z-40), so the toggle never receives taps — it only "worked" because the
     scrim's own handler closed the drawer. Lift the header above the scrim but
     below the z-60 sidebar. */
  .app-shell.nav-open .site-header { z-index: 56; }
}
html, body { max-width: 100%; overflow-x: clip; }

/* Never let the dashboard depend on JS to be visible. ecosystem.css carries a
   marketing scroll-reveal (`html.life-on :where(.card,…){opacity:0}`) revealed
   by an IntersectionObserver. Fine for a landing page; on an operations panel a
   JS hiccup leaves real data invisible — which is exactly what happened to the
   KPI tiles. Inside the app shell content is always visible. */
html.life-on .app-shell :where(.card, .app-card, .pp-stat, .pp-stats, .page-head,
                               .prose h1, .prose h2, .prose p, .prose ul,
                               .sec, .panel-grid) {
  opacity: 1 !important;
  transform: none !important;
}

.app-shell main.content {
  padding: var(--sp-6) clamp(16px, 2.6vw, 34px) var(--sp-10);
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
  /* Centred with a ceiling: full-bleed on a 4K monitor turns tables into
     unreadable 3000px lines, but a fixed narrow column wastes a laptop. */
  width: 100%;
  max-width: var(--content-max);
  margin-inline: auto;
}
@media (max-width: 640px) {
  .app-shell main.content { padding: var(--sp-4) 14px var(--sp-8); gap: var(--sp-4); }
}

.app-shell .site-header {
  position: sticky; top: 0; z-index: 40;
  background: color-mix(in srgb, var(--bg-elev) 86%, transparent);
  backdrop-filter: saturate(1.8) blur(10px);
  -webkit-backdrop-filter: saturate(1.8) blur(10px);
  border-bottom: 1px solid var(--border);
}
/* The topbar used to repeat the page title in 20px bold immediately above the
   page title. It is a breadcrumb now: present for orientation, silent enough
   that the page owns its own heading. */
.site-header__brand > span {
  font-size: 13px; font-weight: 600; letter-spacing: .01em;
  color: var(--fg-muted);
}
@media (min-width: 881px) {
  /* The sidebar already carries the logo two centimetres to the left. */
  .site-header__brand > img { display: none; }
}

/* ── 2. Page header ───────────────────────────────────────────────────────── */
.page-head {
  display: flex; align-items: flex-start; gap: var(--sp-4); flex-wrap: wrap;
  padding-bottom: var(--sp-1);
}
.page-head__text { min-width: 0; flex: 1 1 320px; }
.page-head__title {
  margin: 0; font-size: clamp(21px, 2.5vw, 27px); font-weight: 700;
  letter-spacing: -0.025em; line-height: 1.2; color: var(--fg);
}
.page-head__sub {
  margin: 6px 0 0; color: var(--fg-muted); font-size: 14px; line-height: 1.55;
  max-width: 78ch;
}
.page-head__actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; align-items: center; }
@media (max-width: 560px) {
  .page-head__actions { width: 100%; }
  .page-head__actions > * { flex: 1 1 auto; }
}

/* ── 3. Layout helpers ────────────────────────────────────────────────────
   The fix for the dead space: settings pages stacked 620px-wide cards down the
   left of a 1440px screen. These let a page flow into columns instead. */
.panel-grid {
  display: grid; gap: var(--sp-5);
  grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
  /* Stretch, not start: three cards of different content lengths side by side
     ended at three different heights, which reads as unfinished. Equal height
     plus the submit pinned to a common baseline (below) makes a row of cards
     look like one row. */
  align-items: stretch;
}
/* Cards that are direct grid children fill their cell top to bottom… */
.panel-grid > .card,
.panel-grid > .stack > .card { display: flex; flex-direction: column; }
.panel-grid > .card > .card__body { flex: 1 1 auto; display: flex; flex-direction: column; }
.panel-grid > .card > .card__body > form { display: flex; flex-direction: column; flex: 1 1 auto; }
/* …and the action that ends the card sits on the floor, so the Save buttons of
   adjacent cards line up instead of floating at three different heights. */
.panel-grid > .card > .card__body > form > .btn:last-child,
.panel-grid > .card > .card__body > .btn:last-child { margin-top: auto; align-self: flex-start; }
.panel-grid--wide { grid-template-columns: repeat(auto-fit, minmax(460px, 1fr)); }
.panel-grid__full { grid-column: 1 / -1; }
@media (max-width: 700px) { .panel-grid { grid-template-columns: minmax(0, 1fr); } }

/* A column inside .panel-grid. Grid places one child per cell, so a tall card
   beside a short one leaves a hole under the short one; grouping the short ones
   into a stack is what balances the two columns. */
.stack { display: flex; flex-direction: column; gap: var(--sp-5); min-width: 0; }

/* The Save/Deploy bar that ends a long settings form. Sticky so the action is
   reachable without scrolling to the bottom of a two-screen page, and separated
   from the cards above — butted straight against them it read as another card
   rather than as the thing that commits the whole form. */
.save-bar {
  position: sticky; bottom: var(--sp-4); z-index: 30;
  margin-top: var(--sp-4);
  border-color: var(--border-strong);
  box-shadow: var(--shadow-lg);
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  backdrop-filter: saturate(1.6) blur(8px);
  -webkit-backdrop-filter: saturate(1.6) blur(8px);
}
.save-bar .card__body { padding: var(--sp-4) var(--sp-5); gap: var(--sp-3); }
@media (max-width: 560px) { .save-bar { position: static; } }

/* Section label above a group of cards — gives a long page a spine. */
.sec-label {
  font-size: 11.5px; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--fg-dim);
  margin: var(--sp-2) 0 calc(var(--sp-1) * -1);
}

/* ── 4. KPI tiles ─────────────────────────────────────────────────────────── */
.pp-stats {
  display: grid; gap: var(--sp-4);
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
}
@media (max-width: 620px) { .pp-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3); } }
@media (max-width: 380px) { .pp-stats { grid-template-columns: minmax(0, 1fr); } }

.pp-stat {
  position: relative; overflow: hidden;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: var(--sp-4) var(--sp-4) var(--sp-4);
  display: flex; align-items: flex-start; gap: var(--sp-3);
  box-shadow: var(--shadow-sm);
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease),
              transform .18s var(--ease);
}
/* No accent rail by default. A tile earns colour by having a STATE — see the
   rule-of-meaning at the top of this file. */
.pp-stat::before {
  content: ""; position: absolute; inset-inline-start: 0; inset-block: 0;
  width: 3px; background: var(--accent-rail, transparent);
}
a.pp-stat, .pp-stat--link { text-decoration: none; color: inherit; }
a.pp-stat:hover, .pp-stat--link:hover {
  border-color: var(--border-strong); box-shadow: var(--shadow); transform: translateY(-2px);
}
.pp-stat__icon {
  flex: 0 0 auto; width: 36px; height: 36px; border-radius: var(--r-md);
  display: grid; place-items: center;
  background: var(--bg-elev-2);
  /* NOT --accent-rail: a neutral tile sets that to `transparent` to suppress the
     rail, which would render the icon invisible too. */
  color: var(--stat-icon, var(--fg-dim));
}
.pp-stat__icon svg { width: 18px; height: 18px; }
/* These are <span>s: without display:block the label and value flow onto one
   line ("RESELLERS 0"). */
.pp-stat__body { min-width: 0; display: block; flex: 1 1 auto; }
.pp-stat__label {
  display: block;
  font-size: 11.5px; font-weight: 650; letter-spacing: .07em; text-transform: uppercase;
  color: var(--fg-dim); margin-bottom: var(--sp-1);
  /* Wrap rather than ellipsis: "ACTIVE SER…" and "POPS HEAL…" on a phone are
     worse than two short lines. */
  white-space: normal; overflow-wrap: anywhere; line-height: 1.35;
}
.pp-stat__value {
  display: block;
  font-size: clamp(26px, 3.4vw, 32px); font-weight: 700; line-height: 1.1;
  letter-spacing: -0.03em; color: var(--fg);
  font-variant-numeric: tabular-nums;
}
/* Small qualifier under a stat value, e.g. "at the web layer (L7)". */
.pp-stat__sub {
  display: block; margin-top: 3px;
  font-size: 11.5px; font-weight: 500; color: var(--fg-dim);
  white-space: normal; overflow-wrap: anywhere; line-height: 1.3;
}
.pp-stat__value small,
.pp-stat__value .pp-stat__of {
  font-size: .52em; font-weight: 600; color: var(--fg-dim); letter-spacing: 0;
}
.pp-stat__meta { display: block; font-size: 12.5px; color: var(--fg-muted); margin-top: var(--sp-1); }
.pp-stat--primary { --accent-rail: var(--primary); --stat-icon: var(--primary); }
.pp-stat--accent  { --accent-rail: var(--accent);  --stat-icon: var(--accent); }
.pp-stat--warn    { --accent-rail: var(--warn);    --stat-icon: var(--warn); }
.pp-stat--danger  { --accent-rail: var(--danger);  --stat-icon: var(--danger); }
.pp-stat--info    { --accent-rail: var(--info);    --stat-icon: var(--info); }
/* Explicitly neutral: a plain counter with no health meaning. Rail suppressed,
   icon still legible. */
.pp-stat--plain   { --accent-rail: transparent;    --stat-icon: var(--fg-dim); }

/* ── 5. Cards ─────────────────────────────────────────────────────────────── */
.card {
  border-radius: var(--r-lg);
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
}
.card__header {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--border);
}
.card__title { font-size: 15px; font-weight: 650; letter-spacing: -0.012em; color: var(--fg); }
/* The title block takes the slack so a trailing badge stays on the header line
   instead of wrapping under it and adding a row of height to the card.
   Basis 0, not auto: in a wrapping flex container the browser decides where to
   wrap from the flex-BASIS, and only shrinks items already on a line — so with
   `auto` a long card subtitle forced the badge onto its own row no matter how
   shrinkable the block was. */
.card__header > div:first-child { flex: 1 1 0%; min-width: 0; }
.card__sub { font-size: 13px; color: var(--fg-muted); margin-top: 3px; line-height: 1.5; max-width: 82ch; }
.card__header > .card__spacer { margin-inline-start: auto; }
.card__body { padding: var(--sp-5); }
.card__body--flush { padding: 0; }
/* Actions that belong to a card's CONTENT rather than to the card, sitting above
   it. In the header they compete with the description for the same flex row,
   which on a phone leaves the text a ~30ch column wrapping around a button; here
   they get their own line and the text gets the full width. */
.card__toolbar {
  display: flex; flex-wrap: wrap; gap: var(--sp-2); justify-content: flex-end;
  padding: var(--sp-3) var(--sp-4) 0;
}
@media (max-width: 560px) {
  .card__header { padding: var(--sp-3) var(--sp-4); }
  .card__body { padding: var(--sp-4); }
}
/* A card used as a disclosure (details/summary) must not show the default
   triangle next to a styled header. */
details.card > summary { list-style: none; cursor: pointer; }
details.card > summary::-webkit-details-marker { display: none; }
details.card > summary::after {
  content: ""; margin-inline-start: auto; width: 8px; height: 8px;
  border-right: 2px solid var(--fg-dim); border-bottom: 2px solid var(--fg-dim);
  transform: rotate(45deg); transition: transform .18s var(--ease);
}
details.card[open] > summary::after { transform: rotate(-135deg); }

/* ── 6. Tables ────────────────────────────────────────────────────────────── */
.table-wrap {
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
}
.table { width: 100%; border-collapse: collapse; font-size: 14px; }
.table th {
  text-align: start; font-size: 11px; font-weight: 700;
  letter-spacing: .07em; text-transform: uppercase; color: var(--fg-dim);
  padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--border);
  background: var(--bg-elev-2); white-space: nowrap;
}
.table th:first-child { border-start-start-radius: var(--r-lg); }
.table th:last-child  { border-start-end-radius: var(--r-lg); }
.table td {
  padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--border);
  vertical-align: middle; line-height: 1.5;
}
.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr { transition: background .12s var(--ease); }
.table tbody tr:hover { background: var(--bg-hover); }
.table .num { text-align: end; font-variant-numeric: tabular-nums; }
.table .actions { text-align: end; white-space: nowrap; }
.table .actions > * + * { margin-inline-start: var(--sp-1); }
.table__empty { text-align: center; color: var(--fg-muted); padding: var(--sp-8) var(--sp-4) !important; }
/* Row-level emphasis for a value that is the point of the row. */
.table .lead { font-weight: 600; color: var(--fg); }

/* ── 6b. Interactive rows ─────────────────────────────────────────────────
   A row whose only clickable thing was one small link meant hunting for the
   hostname to open a server. The WHOLE row navigates now (data-row-href), and
   the per-row buttons collapse into one menu so the list stops being a wall of
   competing controls. */
.table tr[data-row-href] { cursor: pointer; }
.table tr[data-row-href]:focus-visible {
  outline: 2px solid var(--ring); outline-offset: -2px;
}
/* Anything genuinely interactive inside a clickable row keeps its own hit area;
   the row handler ignores clicks that land on these. */
.table tr[data-row-href] :where(a, button, input, select, textarea, label) {
  cursor: auto;
}
.table tr[data-row-href] :where(a, button) { cursor: pointer; }

.row-menu { position: relative; display: inline-block; }
.row-menu__btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; padding: 0; border-radius: var(--r-sm);
  border: 1px solid transparent; background: transparent;
  color: var(--fg-dim); cursor: pointer;
  transition: background .15s var(--ease), color .15s var(--ease);
}
.row-menu__btn:hover, .row-menu:focus-within .row-menu__btn {
  background: var(--bg-hover); color: var(--fg); border-color: var(--border);
}
.row-menu__btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring); }
.row-menu__pop {
  position: fixed; z-index: 70; min-width: 190px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-md); box-shadow: var(--shadow-lg);
  padding: var(--sp-1); display: none;
  /* The menu is positioned in viewport coordinates by JS, so a row near the
     bottom of a scrolled table opens upward instead of off-screen. */
}
/* The pop is portaled to <body> when open (see theme-toggle.js), so the
   open-state class lives on the popup itself, not on its former parent. */
.row-menu__pop.is-open { display: block; }
/* Menu items are <button>/<a>, one per action, so keyboard and screen readers
   get the same list the mouse does. */
.row-menu__pop :where(button, a) {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%;
  padding: 9px 11px; border: 0; background: transparent; border-radius: var(--r-sm);
  font: inherit; font-size: 13.5px; font-weight: 550; color: var(--fg-muted);
  text-align: start; text-decoration: none; cursor: pointer; white-space: nowrap;
  min-height: 0;
}
.row-menu__pop :where(button, a):hover { background: var(--bg-hover); color: var(--fg); }
.row-menu__pop form { display: block; margin: 0; }
.row-menu__pop .is-danger { color: var(--fg-muted); }
.row-menu__pop .is-danger:hover {
  background: color-mix(in srgb, var(--danger) 12%, transparent); color: var(--danger);
}
.row-menu__sep { height: 1px; background: var(--border); margin: var(--sp-1) 0; }
/* A menu that also edits: the inline port/share boxes used to sit naked in the
   table cell with no labels, and overflowed the card on a phone. */
.row-menu__pop--form { min-width: 250px; }
.row-menu__form { padding: var(--sp-3) 11px var(--sp-1); }
.row-menu__form .field { margin-bottom: var(--sp-3); }
.row-menu__form .field__label { margin-bottom: 5px; }
.row-menu__form .input { min-height: 36px; padding: 7px 10px; font-size: 13px; }
.row-menu__form .field__hint { margin-top: 5px; font-size: 12px; }
.row-menu__form .btn { width: 100%; margin-bottom: var(--sp-2); }
/* No mobile override: the popup is portaled to <body> and positioned in
   viewport coordinates, so it behaves identically at every width. */

/* Mobile: wide tables become stacked cards. Each <td> carries data-label. */
@media (max-width: 720px) {
  .table--stack thead { display: none; }
  .table--stack, .table--stack tbody, .table--stack tr, .table--stack td { display: block; width: 100%; }
  .table--stack tr {
    border: 1px solid var(--border); border-radius: var(--r-md);
    margin-bottom: var(--sp-3); padding: var(--sp-1) 2px; background: var(--surface);
    box-shadow: var(--shadow-sm);
  }
  .table--stack tr:hover { background: var(--surface); }
  .table--stack td {
    display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
    border-bottom: 1px dashed var(--border); padding: var(--sp-3); text-align: start !important;
  }
  .table--stack td:last-child { border-bottom: 0; }
  .table--stack td::before {
    content: attr(data-label);
    font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
    color: var(--fg-dim); flex: 0 0 auto;
  }
  .table--stack td[data-label=""]::before { content: none; }
  .table--stack .actions { justify-content: flex-end; }
  .table--stack .table__empty { display: block; border: 0; }
}

/* ── 7. Forms ─────────────────────────────────────────────────────────────── */
.form-grid-auto {
  display: grid; gap: var(--sp-4);
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  align-items: start;
}
@media (max-width: 620px) { .form-grid-auto { grid-template-columns: minmax(0, 1fr); } }
.form-grid-auto .field { min-width: 0; margin-bottom: 0; }
/* A submit button sharing the grid must line up with the INPUTS beside it, not
   with the top of the cell and not with the bottom. `align-self:end` sank it to
   the bottom of a row whose height came from a neighbour's hint text; plain
   `start` put it level with the labels. Reserving a label-sized box above the
   button is what actually aligns it, and it stays aligned when a neighbouring
   field grows a hint. */
.form-grid-auto .field--action { align-self: start; }
.form-grid-auto .field--action::before {
  content: "";
  display: block;
  /* Must equal .field__label's box exactly: font-size x line-height, plus its
     margin. `1em` was 12.5px against the label's 17.5px, which left every
     in-grid submit button sitting 5px high — visible, and under the threshold
     of anything that measures only gross misalignment. */
  height: calc(12.5px * 1.4);
  margin-bottom: var(--sp-2);
}
@media (max-width: 620px) {
  /* Single column: there is nothing to line up with, so drop the spacer. */
  .form-grid-auto .field--action::before { display: none; }
}

.field { display: block; margin-bottom: var(--sp-4); }
.field__label {
  display: block; font-size: 12.5px; font-weight: 650;
  color: var(--fg-muted); margin-bottom: var(--sp-2); letter-spacing: .005em;
  /* Pinned, not inherited: .field--action reserves a box of exactly this height
     to line a submit button up with the inputs beside it, and an inherited
     line-height would silently drift the two apart. */
  line-height: 1.4;
}
.field__hint {
  display: block; font-size: 12.5px; color: var(--fg-dim);
  margin-top: var(--sp-2); line-height: 1.55; max-width: 74ch;
}
.input, .select, textarea.input {
  width: 100%; border-radius: var(--r-md); border: 1px solid var(--border);
  background: var(--bg-elev); color: var(--fg);
  padding: 10px 13px; font-size: 14px; min-height: 42px;
  font-family: inherit;
  transition: border-color .15s var(--ease), box-shadow .15s var(--ease), background .15s var(--ease);
}
.input:hover:not(:focus), .select:hover:not(:focus) { border-color: var(--border-strong); }
.input:focus, .select:focus, textarea.input:focus {
  outline: none; border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--ring);
}
.input::placeholder { color: var(--fg-dim); opacity: .85; }
.input[readonly] { background: var(--bg-elev-2); color: var(--fg-muted); }
select.input, .select {
  /* Native select arrows differ wildly per platform; one chevron everywhere. */
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 17px) 50%, calc(100% - 12px) 50%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-inline-end: 34px;
}
/* Checkbox rows read as a list of decisions, not a wall of ticks. */
.check-row {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-3) 0; cursor: pointer;
}
.check-row + .check-row { border-top: 1px solid var(--border); }
.check-row input[type="checkbox"] {
  width: 18px; height: 18px; margin-top: 1px; flex: 0 0 auto;
  accent-color: var(--primary); cursor: pointer;
}
.check-row__text { min-width: 0; }
/* These are <span>s: without display:block the title and its hint run together
   on one line ("Origin lock — only accept traffic from Cloudflare Anyone who
   discovers a PoP's IP can…"). */
.check-row__title { display: block; font-weight: 600; font-size: 14px; color: var(--fg); }
.check-row__hint { display: block; font-size: 12.5px; color: var(--fg-dim); margin-top: 4px; line-height: 1.55; }

/* ── 8. Buttons ───────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  border-radius: var(--r-md); font-weight: 600; font-size: 14px;
  padding: 10px 16px; min-height: 40px; cursor: pointer;
  border: 1px solid transparent; white-space: nowrap;
  transition: background .15s var(--ease), border-color .15s var(--ease),
              box-shadow .15s var(--ease), transform .08s var(--ease);
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring); }
.btn--sm { padding: 7px 13px; min-height: 34px; font-size: 13px; border-radius: var(--r-sm); }
.btn--xs { padding: 5px 10px; min-height: 30px; font-size: 12.5px; border-radius: 7px; }
@media (pointer: coarse) {
  .btn--xs, .btn--sm { min-height: 40px; padding-inline: 14px; }
  .copy-btn { min-width: 40px; min-height: 40px; display: inline-flex;
              align-items: center; justify-content: center; }
}
.btn--primary {
  background: var(--primary); color: var(--primary-fg, #fff);
  box-shadow: 0 1px 2px rgba(15,23,42,.16);
}
.btn--primary:hover { background: var(--primary-hov); box-shadow: var(--shadow); }
.btn--accent { background: var(--accent); color: #fff; }
.btn--accent:hover { background: var(--accent-hov); }
/* Destructive actions are quiet until you reach for them. A table with eleven
   red Delete buttons puts the most dangerous control in the strongest colour on
   the page, eleven times over. */
.btn--danger {
  background: transparent; color: var(--fg-muted);
  border-color: var(--border);
}
.btn--danger:hover {
  background: color-mix(in srgb, var(--danger) 10%, transparent);
  color: var(--danger); border-color: color-mix(in srgb, var(--danger) 45%, transparent);
}
.btn--ghost { background: transparent; color: var(--fg-muted); border-color: var(--border); }
.btn--ghost:hover { background: var(--bg-hover); color: var(--fg); border-color: var(--border-strong); }

/* ── 9. Segmented control (range pickers, settings tabs) ─────────────────── */
.rng, .seg {
  display: inline-flex; gap: 3px; flex-wrap: wrap;
  background: var(--bg-elev-2); border: 1px solid var(--border);
  border-radius: var(--r-md); padding: 3px;
}
.rng > a, .rng > button, .seg > a, .seg > button {
  appearance: none; border: 0; background: transparent; cursor: pointer;
  font-size: 13px; font-weight: 600; color: var(--fg-muted);
  padding: 6px 13px; border-radius: var(--r-sm); text-decoration: none;
  line-height: 1.4; transition: background .15s var(--ease), color .15s var(--ease);
}
.rng > a:hover, .seg > a:hover { background: var(--bg-hover); color: var(--fg); }
.rng > a.is-on, .rng > .is-on, .seg > a.is-on, .seg > .is-on {
  background: var(--surface); color: var(--fg); box-shadow: var(--shadow-sm);
}

/* ── 10. Badges ───────────────────────────────────────────────────────────── */
.badge {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 650; line-height: 1;
  padding: 5px 10px; border-radius: 999px;
  border: 1px solid transparent; white-space: nowrap;
  color: var(--fg-muted); background: var(--bg-elev-2); border-color: var(--border);
}
.badge::before { content: ""; width: 6px; height: 6px; border-radius: 999px; background: currentColor; }
.badge--accent { color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); border-color: color-mix(in srgb, var(--accent) 26%, transparent); }
.badge--warn   { color: var(--warn);   background: color-mix(in srgb, var(--warn) 14%, transparent);   border-color: color-mix(in srgb, var(--warn) 28%, transparent); }
.badge--danger { color: var(--danger); background: color-mix(in srgb, var(--danger) 12%, transparent); border-color: color-mix(in srgb, var(--danger) 26%, transparent); }
.badge--info   { color: var(--info);   background: color-mix(in srgb, var(--info) 12%, transparent);   border-color: color-mix(in srgb, var(--info) 26%, transparent); }
.badge--muted  { color: var(--fg-muted); background: var(--bg-elev-2); border-color: var(--border); }
/* A label, not a status — no dot. */
.badge--tag::before { content: none; }

/* ── 11. Empty states ─────────────────────────────────────────────────────── */
.empty { text-align: center; padding: var(--sp-10) var(--sp-5); color: var(--fg-muted); }
.empty__icon {
  width: 48px; height: 48px; margin: 0 auto var(--sp-3); border-radius: var(--r-lg);
  display: grid; place-items: center;
  background: var(--bg-elev-2); color: var(--fg-dim);
}
.empty__icon svg { width: 22px; height: 22px; }
.empty__title { font-weight: 650; color: var(--fg); margin-bottom: var(--sp-1); font-size: 15px; }
.empty__hint { font-size: 13.5px; max-width: 420px; margin: 0 auto; line-height: 1.6; }

/* ── 12. Flash + inline notices ───────────────────────────────────────────── */
.pp-flash {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  border-radius: var(--r-md); padding: var(--sp-3) var(--sp-4); font-size: 14px;
  border: 1px solid var(--border); background: var(--surface);
  box-shadow: var(--shadow-sm);
}
.pp-flash--ok  { border-color: color-mix(in srgb, var(--accent) 34%, transparent); background: color-mix(in srgb, var(--accent) 9%, var(--surface)); }
.pp-flash--err { border-color: color-mix(in srgb, var(--danger) 34%, transparent); background: color-mix(in srgb, var(--danger) 9%, var(--surface)); }
.pp-flash svg { flex: 0 0 auto; width: 18px; height: 18px; margin-top: 1px; }
.pp-flash--ok svg  { color: var(--accent); }
.pp-flash--err svg { color: var(--danger); }

/* `.alert` inherits display:flex from ecosystem.css, which turns every inline
   run of a multi-element message into its own flex column — text came out
   scrambled. Force a single flow child and normal text flow. */
.alert {
  display: block !important;
  border-radius: var(--r-md); padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--border); background: var(--surface);
  font-size: 13.5px; line-height: 1.6; color: var(--fg-muted);
  box-shadow: var(--shadow-sm);
}
.alert > div { display: block; }
.alert strong { color: var(--fg); }
.alert--ok   { border-color: color-mix(in srgb, var(--accent) 34%, transparent); background: color-mix(in srgb, var(--accent) 9%, var(--surface)); }
.alert--err  { border-color: color-mix(in srgb, var(--danger) 34%, transparent); background: color-mix(in srgb, var(--danger) 9%, var(--surface)); }
.alert--warn { border-color: color-mix(in srgb, var(--warn) 36%, transparent);   background: color-mix(in srgb, var(--warn) 9%, var(--surface)); }

/* A notice with a leading status chip — the Firewall / WAF banners. */
.notice { display: flex; align-items: flex-start; gap: var(--sp-4); flex-wrap: wrap; }
.notice__body { min-width: 0; flex: 1 1 320px; }
.notice__lead { font-size: 14.5px; color: var(--fg); }
.notice__lead strong { font-weight: 700; font-variant-numeric: tabular-nums; }
.notice__hint { font-size: 13px; color: var(--fg-muted); margin-top: var(--sp-1); line-height: 1.6; }

/* ── 13. Sidebar ─────────────────────────────────────────────────────────
   ecosystem.css already carries the house style (44px rows, 16px icons, an
   active state combining a soft gradient with an inset rail). An earlier
   override flattened it and looked worse. Only spacing is adjusted. */
.app-sidebar__nav a { margin-inline: var(--sp-2); }

/* ── 14. Theme crossfade ─────────────────────────────────────────────────
   ecosystem.css fades a fixed list of surfaces while <html> carries
   .theme-fading (~380ms). Components introduced here are not in that list, so
   the page half-faded and half-snapped. */
html.theme-fading .app-shell,
html.theme-fading .app-shell main.content,
html.theme-fading .app-sidebar,
html.theme-fading .app-sidebar__nav a,
html.theme-fading .app-footer,
html.theme-fading .pp-stat,
html.theme-fading .pp-stat__icon,
html.theme-fading .pp-flash,
html.theme-fading .alert,
html.theme-fading .page-head__title,
html.theme-fading .page-head__sub,
html.theme-fading .card,
html.theme-fading .card__header,
html.theme-fading .card__title,
html.theme-fading .card__sub,
html.theme-fading .table,
html.theme-fading .table th,
html.theme-fading .table td,
html.theme-fading .badge,
html.theme-fading .rng,
html.theme-fading .empty__icon,
html.theme-fading .input,
html.theme-fading .select {
  transition: background-color 380ms ease, color 380ms ease,
              border-color 380ms ease, box-shadow 380ms ease !important;
}
/* Address reveal — an endpoint is identified by its NAME everywhere in the
   panel; the address it points at is one deliberate click away, so a page can be
   read (or screenshotted) without handing out a customer's origin. */
.pp-reveal { margin-top: 2px; }
.pp-reveal > summary {
  cursor: pointer;
  font-size: 11.5px;
  color: var(--muted);
  list-style: none;
}
.pp-reveal > summary::-webkit-details-marker { display: none; }
.pp-reveal > summary::before { content: "\2026\00a0"; }
.pp-reveal > summary:hover { color: var(--fg); }
.pp-reveal[open] > summary { color: var(--fg); }
.pp-reveal > .mono { font-size: 11.5px; word-break: break-all; }

@media (prefers-reduced-motion: reduce) {
  html.theme-fading * { transition: none !important; }
}

/* ── 15. Utilities ────────────────────────────────────────────────────────── */
.mono { font-family: 'Fira Code', ui-monospace, SFMono-Regular, Menlo, monospace; }
.truncate { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fg-dim { color: var(--fg-dim); }
/* A figure that has crossed a line the reader needs to notice — a node near its
   uplink, a quota nearly spent. Text colour only: a badge would give a number the
   visual weight of a status, and these live inside dense numeric columns. */
.fg-warn { color: var(--warn); }
/* The same idea one step further: a figure that is not merely worth noticing but
   is the problem itself — an address of ours on the block list, a PoP that has
   never applied a config. Text colour for the same reason as .fg-warn. */
.fg-danger { color: var(--danger); }
.num-tab { font-variant-numeric: tabular-nums; }
.copy-btn { border: 0; background: transparent; color: var(--fg-dim); cursor: pointer; padding: 3px 6px; border-radius: 6px; }
.copy-btn:hover { background: var(--bg-hover); color: var(--primary); }

/* Preformatted output that must never widen the page (config, log tails). */
.pre-scroll {
  font-family: 'Fira Code', ui-monospace, monospace; font-size: 11.5px;
  line-height: 1.55; color: var(--fg-muted);
  background: var(--bg-elev-2); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--sp-3);
  max-height: 190px; overflow: auto; white-space: pre; margin: 0;
  scrollbar-width: thin;
}
/* Inline code-ish value inside prose or a table cell. */
.chip-code {
  font-family: 'Fira Code', ui-monospace, monospace; font-size: 12px;
  background: var(--bg-elev-2); border: 1px solid var(--border);
  border-radius: 6px; padding: 2px 7px; color: var(--fg-muted);
}

/* A cascade that takes customer sites offline must be an explicit, visible
   choice next to the button that performs it — not a hidden default. */
.rm-opt {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--fg-muted); cursor: pointer;
  user-select: none; white-space: nowrap;
}
.rm-opt input { accent-color: var(--danger); cursor: pointer; margin: 0; }
.rm-opt:hover { color: var(--fg); }

/* Address reveal — an endpoint is identified by its NAME everywhere in the
   panel; the address it points at is one deliberate click away, so a page can be
   read (or screenshotted) without handing out a customer's origin. */
.pp-reveal { margin-top: 2px; }
.pp-reveal > summary {
  cursor: pointer;
  font-size: 11.5px;
  color: var(--muted);
  list-style: none;
}
.pp-reveal > summary::-webkit-details-marker { display: none; }
.pp-reveal > summary::before { content: "\2026\00a0"; }
.pp-reveal > summary:hover { color: var(--fg); }
.pp-reveal[open] > summary { color: var(--fg); }
.pp-reveal > .mono { font-size: 11.5px; word-break: break-all; }

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* --- collapsible cards ------------------------------------------------------
   A settings page is a long column of cards an operator visits to change one
   thing. Showing every form expanded means scrolling past six of them to reach
   the seventh, so a card can fold to its header and the page becomes a list of
   what is available rather than a wall of inputs.

   Built on <details>, so it works with no JavaScript, is keyboard-operable and
   announces its state to a screen reader for free. The marker is replaced with
   our own chevron because the native triangle sits at a different baseline in
   every browser. */
.card--fold > summary {
  list-style: none;
  cursor: pointer;
  user-select: none;
}
.card--fold > summary::-webkit-details-marker { display: none; }
.card--fold > summary .card__header { margin: 0; }
.card--fold > summary::after {
  content: "";
  position: absolute;
  inset-inline-end: var(--sp-4);
  top: 50%;
  width: 8px; height: 8px;
  margin-top: -6px;
  border-right: 2px solid var(--fg-muted);
  border-bottom: 2px solid var(--fg-muted);
  transform: rotate(45deg);
  transition: transform .15s ease;
  pointer-events: none;
}
.card--fold[open] > summary::after { transform: rotate(-135deg); margin-top: -2px; }
.card--fold > summary { position: relative; }
.card--fold > summary:hover .card__title { color: var(--primary); }
.card--fold > summary:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
/* Keep room for the chevron so a long title never runs under it. */
.card--fold > summary .card__header { padding-inline-end: calc(var(--sp-5) + 14px); }

/* --- navigation loading veil -------------------------------------------------
   Some pages genuinely have to wait on a third party before they can render —
   the Cloudflare zone read is a couple of seconds when its cache is cold. A
   blank tab for two seconds reads as "broken", so say what is happening.

   Only after a delay: showing it instantly would make every fast page flash a
   spinner, which feels slower than no spinner at all. */
.pp-veil {
  position: fixed; inset: 0; z-index: 9999;
  display: flex; flex-direction: column; gap: 14px;
  align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  backdrop-filter: blur(2px);
  font-size: 14px; font-weight: 600; color: var(--fg-muted);
  animation: pp-veil-in .12s ease both;
}
@keyframes pp-veil-in { from { opacity: 0 } to { opacity: 1 } }
.pp-veil__ring {
  width: 34px; height: 34px; border-radius: 50%;
  border: 3px solid color-mix(in srgb, var(--fg-muted) 25%, transparent);
  border-top-color: var(--primary);
  animation: pp-spin .7s linear infinite;
}
@keyframes pp-spin { to { transform: rotate(360deg) } }
@media (prefers-reduced-motion: reduce) {
  .pp-veil__ring { animation-duration: 2.4s }
  .pp-veil { animation: none }
}

/* --- global search ----------------------------------------------------------
   A panel this size has things scattered across a dozen pages. Typing a hostname
   fragment should find it wherever it lives, rather than requiring you to know
   which page owns it. */
.pp-search__btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0;
  border: 1px solid var(--border); border-radius: var(--r-sm);
  background: var(--surface); color: var(--fg-muted); cursor: pointer;
}
.pp-search__btn:hover { color: var(--fg); border-color: var(--border-strong); }
.pp-search {
  position: fixed; inset: 0; z-index: 9998;
  display: flex; align-items: flex-start; justify-content: center;
  padding: 10vh 16px 16px;
  background: color-mix(in srgb, var(--bg) 70%, transparent);
  backdrop-filter: blur(3px);
}
.pp-search[hidden] { display: none; }
.pp-search__panel {
  width: min(680px, 100%);
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--r-md); box-shadow: var(--shadow-lg);
  overflow: hidden; display: flex; flex-direction: column; max-height: 70vh;
}
.pp-search__input {
  width: 100%; border: 0; outline: 0; background: transparent;
  padding: 16px 18px; font-size: 16px; color: var(--fg);
  border-bottom: 1px solid var(--border);
}
.pp-search__results { overflow-y: auto; }
.pp-search__row {
  display: flex; gap: 12px; align-items: baseline;
  padding: 10px 18px; text-decoration: none; color: var(--fg);
  border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
}
.pp-search__row:hover, .pp-search__row.is-on { background: color-mix(in srgb, var(--primary) 10%, transparent); }
.pp-search__kind {
  font-size: 11px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em; color: var(--fg-muted); flex: 0 0 96px;
}
.pp-search__label { font-weight: 600; word-break: break-all; }
.pp-search__sub { font-size: 12.5px; color: var(--fg-muted); margin-inline-start: auto; }
.pp-search__row mark {
  background: color-mix(in srgb, var(--accent) 30%, transparent);
  color: inherit; border-radius: 3px; padding: 0 1px;
}
.pp-search__empty { padding: 18px; color: var(--fg-muted); font-size: 14px; }

/* --- refusal evidence ------------------------------------------------------ */
/* The case against one address: which rule refused it and what it asked for.
   Dense on purpose — it lives inside a table cell and is read by comparing rows,
   not by admiring one.

   Lives here rather than in a page's own <style> because TWO pages render it now:
   the operator's Allowed & Blocked, and the customer's own share/traffic view.
   A second copy is how the two silently drift into describing the same refusal
   differently. */
.ev { margin-top: 6px; }
.ev > summary { cursor: pointer; font-size: 12px; }
.ev__note { font-size: 12px; max-width: 42ch; }
.ev__chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 5px; }
.ev__chips .badge { font-size: 11px; padding: 3px 8px; }
.ev__list { list-style: none; margin: 6px 0 0; padding: 0; }
.ev__list li {
  display: flex; align-items: baseline; gap: 8px;
  padding: 3px 0; border-top: 1px solid var(--border);
}
/* The path is the point of the row, so it takes the space and everything else
   keeps its own width. break-all, not ellipsis: the tail of a probe
   (…/wp-admin/setup-config.php) is the half that names it. */
.ev__uri { font-size: 11.5px; word-break: break-all; flex: 1 1 auto; }
.ev__n { font-size: 11.5px; white-space: nowrap; }
.ev__why { font-size: 11px; white-space: nowrap; }
/* Method and status keep their own column width so the paths below them line up
   and a run of identical refusals reads as one shape, not twenty strings. */
.ev__m {
  font-size: 10.5px; font-weight: 600; color: var(--fg-dim);
  min-width: 4ch; flex: 0 0 auto;
}
.ev__st { font-size: 11px; flex: 0 0 auto; min-width: 3ch; text-align: right; }
.ev__st.is-err { color: var(--danger, #dc2626); }
.ev__hosts, .ev__none { font-size: 11.5px; margin-top: 6px; }
