/* ============================================================
   ZeroRoute — Design System
   Hand-rolled CSS, no external dependencies.
   ============================================================ */

/* ── Custom properties (light theme) ── */
:root {
  /* Tells the engine which palette native widgets (select popups, scrollbars,
     autofill, date pickers) should use. Without it they render light-on-light
     inside the dark theme. */
  color-scheme: light;

  /* Accent.
     --accent is text/link/icon blue: it must clear 4.5:1 against the page.
     --accent-solid is the fill behind white button text, and must clear 4.5:1
     the other way round. In light they can be the same colour; in dark they
     cannot, which is why they are two tokens. */
  --accent:        #3563f0;
  --accent-hover:  #2f5ae0;
  --accent-subtle: #eef1ff;
  /* Text drawn on --accent-subtle (avatars, role badges, KPI icons) needs its
     own step: the link blue is too light against a tinted chip. */
  --accent-on-subtle: #2b55d6;
  --accent-solid:       var(--accent);
  --accent-solid-hover: var(--accent-hover);
  --on-accent:     #ffffff;
  --focus-ring:    rgba(53, 99, 240, .18);
  --scrim:         rgba(0, 0, 0, .45);

  /* Traffic direction — one pair for every chart, cell and legend */
  --traffic-down:  #1a66c2;
  --traffic-up:    #0f7a3d;

  /* Destructive */
  --danger-fg:     #b91c1c;
  --danger-border: #fca5a5;
  --danger-bg:     #fef2f2;

  /* Feedback banners (flash, alert, verification nudge) */
  --success-fg:     #166534;
  --success-border: #86efac;
  --success-bg:     #f0fdf4;
  --warning-fg:     #92400e;
  --warning-border: #fcd34d;
  --warning-bg:     #fffbeb;

  /* Admin role badge — the one purple in the system */
  --role-admin-fg: #6d28d9;
  --role-admin-bg: #ede9fe;

  /* Status */
  --status-alive-bg:   #d1fae5;
  --status-alive-fg:   #065f46;
  --status-pending-bg: #fef3c7;
  --status-pending-fg: #92400e;
  --status-dead-bg:    #fee2e2;
  --status-dead-fg:    #991b1b;
  --status-disabled-bg:#f3f4f6;
  --status-disabled-fg:#565e6b;

  /* Grays */
  --bg:            #f8f9fb;
  --surface:       #ffffff;
  --surface-2:     #f3f4f6;
  --input-bg:      #ffffff;
  --border:        #e5e7eb;
  --border-focus:  var(--accent);
  /* Three text weights, all of them readable: --text-3 is the quietest tone we
     ship, not an invisible one, so it still clears 4.5:1 on --surface and --bg. */
  --text:          #111827;
  --text-2:        #4b5563;
  --text-3:        #6b7280;

  /* Sidebar (light) */
  --sidebar-bg:    #f5f7fa;
  --sidebar-fg:    #525b68;
  --sidebar-fg-active: #111827;
  --sidebar-active-bg: rgba(61,107,255,0.10);
  --sidebar-border:    #e5e7eb;
  --sidebar-hover-bg:  rgba(17,24,39,0.04);
  --sidebar-w:     220px;

  /* Typography */
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", "Fira Code", Consolas, monospace;

  /* Radii */
  --r-sm:  6px;
  --r:     10px;
  --r-lg:  14px;

  /* Shadows */
  --shadow-sm: 0 1px 3px rgba(0,0,0,.07), 0 1px 2px rgba(0,0,0,.05);
  --shadow:    0 4px 12px rgba(0,0,0,.08), 0 1px 3px rgba(0,0,0,.05);
  --shadow-lg: 0 8px 24px rgba(0,0,0,.10);

  /* Semantic accent colors (status, callouts) */
  --ok: #10B981;
  --warn: #F59E0B;
  --danger: #EF4444;

  /* Font size scale */
  --fs-xs: 12px;
  --fs-sm: 13px;
  --fs-base: 14px;
  --fs-lg: 16px;
  --fs-xl: 20px;
  --fs-2xl: 28px;
  --fw-normal: 400;
  --fw-medium: 500;
  --fw-semibold: 600;
}

/* ── Dark theme (system preference) ──
   PAIRED BLOCK: every token here must also exist in the [data-theme="dark"]
   block near the bottom of this file, which serves the same palette when the
   user picks dark explicitly on a light OS. Edit them together. */
@media (prefers-color-scheme: dark) {
  [data-theme="system"] {
    color-scheme: dark;

    --accent:        #5b87ff;
    --accent-hover:  #7099ff;
    --accent-subtle: #1e2d5a;
    --accent-on-subtle: #8db0ff;
    --accent-solid:       #3563f0;
    --accent-solid-hover: #4571f5;
    --on-accent:     #ffffff;
    --focus-ring:    rgba(91, 135, 255, .28);
    --scrim:         rgba(0, 0, 0, .6);

    --traffic-down:  #6aa6f5;
    --traffic-up:    #3fb950;

    --danger-fg:     #f87171;
    --danger-border: #7f1d1d;
    --danger-bg:     #450a0a;

    --success-fg:     #86efac;
    --success-border: #166534;
    --success-bg:     #052e16;
    --warning-fg:     #fde68a;
    --warning-border: #a16207;
    --warning-bg:     #3a2e08;

    --role-admin-fg: #c4b5fd;
    --role-admin-bg: #2e1065;

    --status-alive-bg:   #064e3b;
    --status-alive-fg:   #6ee7b7;
    --status-pending-bg: #451a03;
    --status-pending-fg: #fbbf24;
    --status-dead-bg:    #450a0a;
    --status-dead-fg:    #fca5a5;
    --status-disabled-bg:#1f2937;
    --status-disabled-fg:#9ca3af;

    --bg:            #0d1117;
    --surface:       #161b22;
    --surface-2:     #21262d;
    --input-bg:      #0d1117;
    --border:        #30363d;
    --text:          #e6edf3;
    --text-2:        #8b949e;
    --text-3:        #7d8590;

    --ok: #34D399;
    --warn: #FBBF24;
    --danger: #F87171;

    --sidebar-bg:    #090d13;
    --sidebar-fg:    #7d8590;
    --sidebar-fg-active: #e6edf3;
    --sidebar-active-bg: rgba(255,255,255,0.06);
    --sidebar-border:    #21262d;
    --sidebar-hover-bg:  rgba(255,255,255,0.04);

    --shadow-sm: 0 1px 3px rgba(0,0,0,.3);
    --shadow:    0 4px 12px rgba(0,0,0,.35);
    --shadow-lg: 0 8px 24px rgba(0,0,0,.45);
  }
}

/* ── Reset ── */
*, *::before, *::after { box-sizing: border-box; }
/* 93.75% of the reader's own base size — the same 15px as before on a default
   browser, but it still grows when someone has set a larger default. */
html { font-size: 93.75%; }
body {
  margin: 0;
  font-family: var(--font-sans);
  background: var(--bg);
  color: var(--text);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
h1, h2, h3, h4 { margin: 0 0 .5em; font-weight: 600; line-height: 1.25; }
h2 { font-size: 22px; }
h3 { font-size: 18px; }
h4 { font-size: 1rem; }
p { margin: 0 0 .75em; }
code, pre { font-family: var(--font-mono); font-size: .875rem; }
pre {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: .75rem 1rem;
  overflow-x: auto;
  white-space: pre-wrap;
  word-break: break-all;
  line-height: 1.6;
}
small { font-size: .8125rem; color: var(--text-2); }
dl { margin: 0 0 1.5rem; }
dt { font-size: .75rem; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; color: var(--text-2); margin-top: .75rem; }
dd { margin: .2rem 0 0; }

/* ── Layout shell ── */
.app-shell {
  display: flex;
  min-height: 100vh;
}

/* ── Sidebar ── */
.sidebar {
  width: var(--sidebar-w);
  background: var(--sidebar-bg);
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  position: fixed;
  top: 0; left: 0; bottom: 0;
  z-index: 100;
  overflow-y: auto;
  border-right: 1px solid var(--sidebar-border);
}
.sidebar-brand {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: 1.25rem 1.25rem 1rem;
  color: var(--sidebar-fg-active);
  font-weight: 700;
  /* Deliberately off the --fs-* ramp: the nav sits at 13px, and a 16px brand
     read as just another row. The logo is the one place that outranks it. */
  font-size: 22px;
  letter-spacing: -.02em;
  text-decoration: none;
  border-bottom: 1px solid var(--sidebar-border);
  margin-bottom: .5rem;
}
.sidebar-brand:hover { text-decoration: none; color: var(--sidebar-fg-active); }
.sidebar-brand-icon {
  width: 34px; height: 34px;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.sidebar-brand-icon svg { display: block; width: 34px; height: 34px; }
/* Brand lockup: "zero" carries the weight, "route" recedes. Used anywhere the
   name appears as a logo — never inside running prose. */
.wordmark { font-weight: 650; letter-spacing: -.02em; }
.wordmark > span { font-weight: 350; color: var(--text-2); }
.sidebar-nav { flex: 1; padding: 0 .25rem; }
.sidebar-section { margin: 16px 0; }
.sidebar-section__label {
  padding: 0 16px 8px;
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-3);
}
.sidebar-link {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 16px;
  color: var(--sidebar-fg);
  text-decoration: none;
  font-size: var(--fs-sm);
  border-radius: var(--r-sm);
  margin: 0 8px;
  transition: background .12s, color .12s;
}
.sidebar-link:hover {
  color: var(--sidebar-fg-active);
  background: var(--sidebar-hover-bg);
  text-decoration: none;
}
.sidebar-link.is-active {
  color: var(--sidebar-fg-active);
  background: var(--sidebar-active-bg);
  font-weight: var(--fw-medium);
}
.sidebar-link svg { flex-shrink: 0; }
/* The unread count on the Messages link: pushed to the far edge of the row,
   past the label, rather than sitting beside it like every other chip. */
.chip--count { margin-left: auto; }
.sidebar-footer {
  padding: .875rem 1.25rem;
  border-top: 1px solid var(--sidebar-border);
  font-size: .8125rem;
}
.sidebar-email {
  color: var(--sidebar-fg);
  margin-bottom: .5rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Account switcher (sidebar footer) ── */
.account-list { margin-bottom: .5rem; }
.account-row-form { margin: 0; }
.account-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  width: 100%;
  padding: .3rem .4rem;
  margin: 0 -.4rem .125rem;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--sidebar-fg);
  font: inherit;
  text-align: left;
}
.account-row.is-active { color: var(--sidebar-fg-active); }
button.account-row { cursor: pointer; }
button.account-row:hover { background: var(--sidebar-active-bg); color: var(--sidebar-fg-active); }
.account-email {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.account-role {
  flex: none;
  font-size: .6875rem;
  padding: .1rem .45rem;
  border-radius: 999px;
  border: 1px solid var(--sidebar-border);
  color: var(--sidebar-fg);
  text-transform: lowercase;
}
.account-role--owner { border-color: var(--accent-solid); color: var(--accent-solid); }
.account-role--superuser { border-color: color-mix(in srgb, var(--accent-solid) 60%, var(--sidebar-border)); }
.account-add {
  display: inline-block;
  margin-top: .25rem;
  color: var(--sidebar-fg);
  text-decoration: none;
  font-size: .75rem;
  opacity: .8;
}
.account-add:hover { color: var(--sidebar-fg-active); opacity: 1; }

/* ── Main content area ── */
.main-area {
  flex: 1;
  margin-left: var(--sidebar-w);
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}
.main-area.no-sidebar { margin-left: 0; }

.page-content {
  flex: 1;
  width: 100%;
  padding: 2rem 2.5rem;
  max-width: 1160px;
  /* Centred, so a wide monitor doesn't leave the panel hugging the left edge */
  margin-inline: auto;
}
.page-content.wide { max-width: none; }

/* ── Flash / Alert ── */
.flash, .alert-success {
  display: flex;
  align-items: flex-start;
  gap: .75rem;
  background: var(--success-bg);
  border: 1px solid var(--success-border);
  border-radius: var(--r);
  padding: .875rem 1rem;
  margin-bottom: 1.5rem;
  color: var(--success-fg);
  font-size: .875rem;
}
.error, .alert-error {
  display: flex;
  align-items: flex-start;
  gap: .75rem;
  background: var(--danger-bg);
  border: 1px solid var(--danger-border);
  border-radius: var(--r);
  padding: .875rem 1rem;
  margin-bottom: 1.5rem;
  color: var(--danger-fg);
  font-size: .875rem;
}
/* An unenrolled node's install command. Not an error and not a success —
   something is waiting on the person, which is what the warning palette says. */
.alert-warn {
  background: var(--warning-bg);
  border: 1px solid var(--warning-border);
  color: var(--warning-fg);
  border-radius: var(--r);
  padding: .875rem 1rem;
  margin-top: 1rem;
  font-size: .875rem;
}
.alert-warn .callout-code { margin: .625rem 0; }

/* Destructive and re-issue actions sit apart from the access list they follow,
   so "Убрать" (one person) is never mistaken for "Удалить ноду" (everyone). */
.node-actions {
  display: flex;
  gap: .75rem;
  flex-wrap: wrap;
  margin-top: 1rem;
  padding-top: .875rem;
  border-top: 1px solid var(--border);
}

/* ── Email-verification banner (Wave 9) ── */
.email-verify-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  flex-wrap: wrap;
  background: var(--warning-bg);
  border: 1px solid var(--warning-border);
  border-radius: var(--r);
  padding: .75rem 1rem;
  margin-bottom: 1.5rem;
  color: var(--warning-fg);
  font-size: .875rem;
}
.email-verify-banner__form { margin: 0; }

/* ── Card ── */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  padding: 1.5rem;
  margin-bottom: 1.5rem;
}
.card:hover { box-shadow: var(--shadow); }
.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* A long heading and an action cluster share this row. Without wrap the
     cluster is pushed past the card's edge — where .main-area's clip eats it
     silently, so the only button on the card simply is not there. */
  flex-wrap: wrap;
  gap: .75rem;
  margin-bottom: 1rem;
}

/* ── Page header ── */
.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 1.75rem;
  flex-wrap: wrap;
  gap: .75rem;
}
.page-title {
  font-size: 22px;
  font-weight: 700;
  margin: 0;
}
.page-subtitle {
  color: var(--text-2);
  font-size: .875rem;
  margin: .25rem 0 0;
}

/* ── Detail-page primitives ──
   Stat cards used to be a second, near-identical copy of the KPI strip; the
   strip won and every page now uses it. What is left here are the small
   building blocks the detail pages actually need. */

/* Small uppercase caption above a value (provider, region, CPU, …) */
.eyebrow {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-2);
}
/* Label / value / footnote stack, laid out in an auto-fitting grid */
.meta-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: .75rem;
}
.meta-value { font-weight: 500; margin-top: 2px; }
.meta-note  { font-size: 12px; color: var(--text-2); margin-top: 2px; }

/* ── Tables ── */
.table-wrap {
  overflow-x: auto;
}
.data-table {
  width: 100%;
  border-collapse: collapse;
}
.data-table thead th {
  text-align: left;
  font-size: .6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  /* --text-2, not --text-3. The quietest tone clears 4.5:1 on --surface and on
     --bg, which is what its own comment in 00-tokens.css claims — but a header
     sits on --surface-2, and there it lands at 4.4:1 light and 4.1:1 dark. At
     11px uppercase there is no large-text exemption to fall back on. One step
     up passes both (6.9:1 and 5.0:1) and is still two steps quieter than the
     rows it labels. */
  color: var(--text-2);
  background: var(--surface-2);
  padding: .7rem 1rem;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.data-table tbody tr {
  border-bottom: 1px solid var(--border);
  transition: background .1s;
}
.data-table tbody tr:last-child { border-bottom: none; }
.data-table tbody tr:hover { background: var(--surface-2); }
.data-table td {
  padding: .75rem 1rem;
  font-size: .875rem;
  vertical-align: middle;
}
.data-table td.mono {
  font-family: var(--font-mono);
  font-size: .8125rem;
  font-variant-numeric: tabular-nums;
}
.data-table .actions {
  text-align: right;
  white-space: nowrap;
}
.data-table .empty-row td {
  text-align: center;
  color: var(--text-2);
  padding: 2rem;
}

/* ── Buttons ── */
.btn {
  display: inline-flex;
  align-items: center;
  gap: .375rem;
  padding: .5rem 1rem;
  border-radius: var(--r-sm);
  font-size: .875rem;
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
  border: 1.5px solid transparent;
  line-height: 1.4;
  transition: background .12s, box-shadow .12s, border-color .12s, color .12s;
  text-decoration: none;
  white-space: nowrap;
}
.btn:hover { text-decoration: none; }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Primary */
.btn-primary, button.btn-primary, input[type=submit].btn-primary {
  background: var(--accent-solid);
  color: var(--on-accent);
  border-color: var(--accent-solid);
}
.btn-primary:hover { background: var(--accent-solid-hover); border-color: var(--accent-solid-hover); color: var(--on-accent); }

/* Secondary / outline */
.btn-secondary {
  background: transparent;
  color: var(--text);
  border-color: var(--border);
}
.btn-secondary:hover { background: var(--surface-2); color: var(--text); }

/* Destructive — one token trio, so the theme blocks no longer need to restate it */
.btn-danger {
  background: transparent;
  color: var(--danger-fg);
  border-color: var(--danger-border);
}
.btn-danger:hover { background: var(--danger-bg); border-color: var(--danger-fg); color: var(--danger-fg); }

/* Small */
.btn-sm { padding: .3125rem .75rem; font-size: .8125rem; }

/* Legacy compatibility — bare <button class="danger"> */
button.danger {
  display: inline-flex;
  align-items: center;
  padding: .3125rem .75rem;
  border-radius: var(--r-sm);
  font-size: .8125rem;
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
  background: transparent;
  color: var(--danger-fg);
  border: 1.5px solid var(--danger-border);
  transition: background .12s, border-color .12s;
}
button.danger:hover { background: var(--danger-bg); border-color: var(--danger-fg); }

/* Logout button in sidebar footer */
.btn-logout {
  display: inline-flex;
  align-items: center;
  gap: .375rem;
  padding: .375rem .75rem;
  border-radius: var(--r-sm);
  font-size: .8125rem;
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
  background: var(--sidebar-hover-bg);
  color: var(--sidebar-fg);
  border: 1px solid var(--sidebar-border);
  transition: background .12s, color .12s;
  width: 100%;
  justify-content: center;
}
.btn-logout:hover { background: var(--sidebar-active-bg); color: var(--sidebar-fg-active); }

/* ── Forms ── */
.form-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  padding: 2rem;
}

/* Every control in the panel gets the panel's look, whether or not the markup
   remembered a class. Element selectors keep this at specificity (0,0,1), so any
   class rule below still wins; the widget types opt out on the next rule. */
input, select, textarea {
  padding: 8px 12px;
  font-size: 14px;
  font-family: inherit;
  color: var(--text);
  background: var(--input-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  transition: border-color .12s, box-shadow .12s;
  outline: none;
  line-height: 1.5;
  max-width: 100%;
}
input[type=checkbox], input[type=radio], input[type=range],
input[type=color], input[type=file] { all: revert; accent-color: var(--accent); }

input:focus, select:focus, textarea:focus {
  border-color: var(--border-focus);
  box-shadow: 0 0 0 3px var(--focus-ring);
}
::placeholder { color: var(--text-3); opacity: 1; }

/* .form-control is the stacked, full-width variant of the same control */
.form-control { display: block; width: 100%; }
.form-control.mono {
  font-family: var(--font-mono);
  font-size: .875rem;
}
select { cursor: pointer; }

/* Label above a control; the pairing every form in the panel uses */
.field-label {
  display: block;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-2);
  margin-bottom: .375rem;
}

/* A checkbox and the sentence it belongs to. .field-label was doing this job on
   /monitoring/targets, which is a block, uppercase, letter-spaced caption meant
   to sit ABOVE a control — so the tick and its words became a full-width caps
   banner, and two of them stacked into a wall. */
.check-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  color: var(--text);
  white-space: nowrap;
  cursor: pointer;
}
.check-label input { margin: 0; flex: none; }
.check-label + .check-label { margin-left: 14px; }
/* A row of them stacks rather than stretches. */
.check-group { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; }
.check-group .check-label + .check-label { margin-left: 0; }
/* A row of controls that sit side by side and wrap on narrow screens */
.form-inline {
  display: flex;
  gap: .75rem;
  flex-wrap: wrap;
  align-items: flex-end;
  margin: 0;
}
.form-inline__note {
  flex-basis: 100%;
  font-size: 12px;
  color: var(--text-2);
  margin: 0;
}
.input-w-sm { width: 6rem; }
.input-w-md { width: 9rem; }
.input-w-lg { width: 18rem; }

.form-actions {
  display: flex;
  align-items: center;
  gap: .75rem;
  margin-top: 1.5rem;
}

/* The dotted `.status` badge is gone. It was the second of three vocabularies
   for a single fact: detail pages wore `.status` with a leading dot, lists
   wore `.chip`, and /me/devices wore `.status-pill` — so a node that was
   "alive" looked like three different things depending on which screen you
   were on. DESIGN.md names one: a state is always a chip. */

/* ── Role badge ── */
/* .role-badge was the second badge vocabulary for a role, used on exactly one
   line of one page. Roles wear .chip--owner / --superuser / --powered /
   --member everywhere else, including the row on /users that links here. */

/* ── Monospace cells ── */
.mono {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

/* ── Invite code ── */
.invite-code {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 20px;
  font-weight: 700;
  letter-spacing: .12em;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: .75rem 1.25rem;
  margin: .75rem 0;
}

/* ── Profile info block ── */

/* ── Device / traffic chart ── */
.traffic-chart-wrap {
  margin: 1rem 0 1.5rem;
}
.traffic-chart-wrap h4 {
  font-size: .875rem;
  color: var(--text-2);
  margin-bottom: .5rem;
}
.traffic-legend {
  font-size: .75rem;
  color: var(--text-2);
  margin-top: .375rem;
}
.traffic-summary { white-space: nowrap; font-variant-numeric: tabular-nums; }

/* Base chart box, shared with the SVGs the traffic popup builds in JS */
.traffic-chart { width: 100%; height: auto; display: block; }

/* Server-rendered day-bar charts: the SVG box and its two directions.
   Kept as classes rather than fill="…" attributes so the bars follow the theme
   and there is exactly one place where download-blue and upload-green live. */
.daybar-chart {
  width: 100%;
  max-width: 560px;
  height: 80px;
  display: block;
  overflow: visible;
}
.daybar--down { fill: var(--traffic-down); }
.daybar--up   { fill: var(--traffic-up); }
.daybar-axis  { fill: currentColor; opacity: .55; }
.text-down    { color: var(--traffic-down); }
.text-up      { color: var(--traffic-up); }
.muted { color: var(--text-3); }

/* ── Map table ── */

/* The matrix these paint is `.map-matrix` (map.html) — the selector said
   `.map-table`, a name no template ever used, so every cell rendered
   uncoloured and "reachable but slow" was indistinguishable from "fast". */
.map-matrix td.cell-none { color: var(--text-3); }
.map-matrix td.fresh-fast { background: var(--status-alive-bg); }
.map-matrix td.fresh-slow { background: var(--status-pending-bg); }
.map-matrix td.stale { background: var(--surface-2); color: var(--text-2); }
.map-matrix td.failed { background: var(--status-dead-bg); }

/* ── Login page ── */

/* ── Forbidden page ── */

.forbidden-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  padding: 3rem 2.5rem;
  max-width: 460px;
  text-align: center;
}

/* ── Section divider ── */
.section-title {
  font-size: .8125rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--text-2);
  margin: 1.75rem 0 .875rem;
  padding-bottom: .5rem;
  border-bottom: 1px solid var(--border);
}

/* ── Inline action group ── */
.action-group {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
}

/* ── Utilities ── */
.text-muted { color: var(--text-2); }
.text-sm    { font-size: .8125rem; }
.mt-0 { margin-top: 0; }
.mb-0 { margin-bottom: 0; }
.mt-1 { margin-top: .5rem; }
.mt-2 { margin-top: 1rem; }
.mt-3 { margin-top: 1.5rem; }

/* ── Inline rename form on /me ── */

/* ── Page layout (v3 — Phase B) ── */
/* .page is a transparent wrapper: the content column (width + padding) is owned
   by .page-content in the shell, so every page — with or without this wrapper —
   lines up with the ones that don't have it (admin overview, detail pages). */
.page { max-width: none; padding: 0; }
.page-header { margin-bottom: 28px; }
.page-header h1 { font-size: 32px; font-weight: 700; letter-spacing: -0.02em; margin: 0 0 4px; color: var(--text); }
.page-subtitle { font-size: 15px; color: var(--text-2); margin: 0; }

/* ── Card v3 overrides (no-padding variant + proper card-header) ── */
/* .card--flush: card with no outer padding; children own their spacing.
   Use for cards containing device lists, tables, etc. */
.card--flush { padding: 0; overflow: hidden; }
/* Upgrade card-header for v3: adds internal padding + bottom border separator */
.card--flush .card-header,
.card-header--v3 {
  padding: 14px 20px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 0;
}
/* Every card header, not just the flush ones. DESIGN.md's One Title Rule gives
   a card the 16px Headline; the rule used to be keyed to .card--flush and
   .card-header--v3 only, so a padded card's <h2> fell through to the global
   22px — nine near-Display headings under one Display heading on /help alone. */
.card-header h2 { font-size: 16px; font-weight: 600; margin: 0; color: var(--text); }

/* ── KPI strip (v4 — airy individual cards, no enclosing box) ── */
.kpi-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 20px;
  margin-bottom: 32px;
}
.kpi-card {
  padding: 20px 24px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  /* Values sit on one line across the strip even when a label wraps to two */
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 8px;
}
.kpi-card:hover { box-shadow: var(--shadow); }
.kpi-label { font-size: 13px; font-weight: 500; color: var(--text-2); margin-bottom: 10px; }
.kpi-value { font-size: 32px; font-weight: 700; letter-spacing: -0.02em; color: var(--text); line-height: 1.05; font-variant-numeric: tabular-nums; }

.kpi-card__top { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.kpi-card__top .kpi-label { margin-bottom: 0; }
.kpi-icon {
  width: 30px; height: 30px;
  border-radius: 10px;
  background: var(--accent-subtle);
  color: var(--accent-on-subtle);
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.kpi-icon svg { width: 16px; height: 16px; display: block; }
.kpi-icon--ok     { background: var(--status-alive-bg);   color: var(--status-alive-fg); }
.kpi-icon--warn   { background: var(--status-pending-bg); color: var(--status-pending-fg); }
.kpi-icon--danger { background: var(--status-dead-bg);    color: var(--status-dead-fg); }
.kpi-icon--muted  { background: var(--surface-2);         color: var(--text-3); }
.kpi-icon--down   { background: var(--accent-subtle);     color: var(--traffic-down); }
/* The upload glyph takes the status-green foreground rather than the chart
   green: on the tinted chip the chart tone drops under 4.5:1. */
.kpi-icon--up     { background: var(--status-alive-bg);   color: var(--status-alive-fg); }
/* Traffic KPIs carry the direction colour on the number itself, the same blue
   and green the charts and cells use. */
.kpi-value--down  { color: var(--traffic-down); }
.kpi-value--up    { color: var(--traffic-up); }
/* A KPI whose value is a state, not a number (device status) */
.kpi-value--chip  { line-height: 1; }

/* The bespoke device list (.device-list / .device-row grid, .device-icon,
   .device-hostname, .device-traffic-link, .bytes-indicator) is gone: /me/devices
   is a .data-table like every other list in the panel. It carried the only
   copy of the traffic pair painted in --accent/--ok rather than
   --traffic-down/--traffic-up, and a mobile grid whose named areas were
   assigned to elements that were not its grid items. */
.device-name { font-weight: 600; font-size: 14px; color: var(--text); }
.sparkline-svg { display: block; overflow: hidden; }

/* KPI card that's clickable for popup */
.kpi-card--link {
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  display: block;
  transition: background 0.12s, box-shadow 0.12s;
}
.kpi-card--link:hover {
  background: var(--surface-2);
  box-shadow: 0 0 0 1px var(--accent) inset;
}

/* traffic color accents reusable */
.traffic-up { color: var(--traffic-up); }
.traffic-down { color: var(--traffic-down); }

/* ── Dashboard traffic chart (v2 — themed server-side area chart) ── */
.chart-card { padding: 20px 24px; margin-bottom: 24px; }
.chart-card__head { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 8px; flex-wrap: wrap; }
.chart-card__head h2 { font-size: 16px; font-weight: 600; margin: 0; color: var(--text); }
.chart-legend { display: flex; align-items: center; gap: 16px; font-size: 12px; color: var(--text-2); }
.chart-legend__item { display: inline-flex; align-items: center; gap: 6px; }
.chart-dot { width: 10px; height: 10px; border-radius: 4px; display: inline-block; }
.chart-dot--down { background: var(--traffic-down); }
.chart-dot--up { background: var(--traffic-up); }
.chart-legend__range { color: var(--text-3); }
.traffic-chart-v2 { width: 100%; height: auto; display: block; margin-top: 4px; }

/* inline traffic sparkline inside table rows (servers list) */
.traffic-link--spark { display: inline-flex; align-items: center; gap: 12px; text-decoration: none; color: inherit; flex-wrap: wrap; }
.traffic-link--spark:hover .traffic-nums { text-decoration: underline; }
.row-spark { display: inline-flex; align-items: flex-end; line-height: 0; opacity: 0.95; }
.traffic-nums { white-space: nowrap; font-variant-numeric: tabular-nums; }
/* The window is the headline; the lifetime figure sits under it so a quiet
   week reads as quiet rather than as a node that has never carried anything. */
.traffic-figures { display: inline-flex; flex-direction: column; gap: 2px; align-items: flex-start; }
.traffic-lifetime {
  font-size: 12px;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* Host-load cell: three labelled percentages, clickable into the history. */
.node-load-link {
  display: inline-flex; flex-wrap: wrap; gap: .1rem .6rem;
  text-decoration: none; color: inherit;
  border-bottom: 1px dashed transparent;
  cursor: pointer;
}
.node-load-link:hover { border-bottom-color: var(--accent); text-decoration: none; }
/* The same two readings outside a table: on /me/nodes they sit under the node's
   name rather than in columns, so the row supplies the gap the cells used to.
   Wrapping is required — on a phone the traffic figure and the load figures do
   not fit on one line, and a nowrap here is what pushed the panel sideways. */
.node-stats {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 6px 20px;
  margin-top: 8px;
  font-size: 13px;
}
.node-stat { white-space: nowrap; font-variant-numeric: tabular-nums; }
.node-stat__key {
  font-size: 12px;
  letter-spacing: .05em;
  color: var(--text-3);
}

/* ── Geo map (/map fleet view) ── */
.geo-card { padding: 20px 24px; margin-bottom: 24px; }
.geo-map { width: 100%; height: auto; display: block; border-radius: 10px; overflow: hidden; }
.geo-ocean { fill: var(--surface-2); }
.geo-land { color: var(--text-3); opacity: 0.40; }
.geo-pin__halo { opacity: 0.22; }
.geo-pin__dot { stroke: var(--surface); stroke-width: 2; }
.geo-pin--alive   .geo-pin__halo, .geo-pin--alive   .geo-pin__dot { fill: var(--ok); }
.geo-pin--pending .geo-pin__halo, .geo-pin--pending .geo-pin__dot { fill: var(--warn); }
.geo-pin--dead    .geo-pin__halo, .geo-pin--dead    .geo-pin__dot { fill: var(--danger); }
.geo-pin__label { font-size: 17px; font-weight: 600; fill: var(--text); paint-order: stroke; stroke: var(--surface); stroke-width: 4px; stroke-linejoin: round; }
.geo-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
.geo-dot--alive { background: var(--ok); }
.geo-dot--pending { background: var(--warn); }
.geo-dot--dead { background: var(--danger); }

/* matrix server-header status dot */
.hdr-status { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: middle; background: var(--text-3); }
.hdr-status--alive { background: var(--ok); }
.hdr-status--pending { background: var(--warn); }
.hdr-status--dead { background: var(--danger); }

/* btn-icon variant: danger (delete) — visible trash icon */
.btn-icon--danger { color: var(--text-3); }
.btn-icon--danger:hover { background: var(--status-dead-bg); color: var(--danger); }
.btn-icon svg { display: block; }

/* The kebab menu is gone with the device list it hung off. It was the panel's
   only floating menu, it opened downwards inside a card with overflow:hidden
   (so the last row's Revoke was clipped away), and everything it held now sits
   in the row's action column or on the device's own page. */

/* ── Outline-sm button (card header "Add device") ── */
.btn-outline-sm {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 6px 14px;
  font-size: 13px; font-weight: 500;
  color: var(--text); background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  text-decoration: none;
  transition: background .12s, border-color .12s;
}
.btn-outline-sm:hover { background: var(--surface-2); border-color: var(--text-3); text-decoration: none; }

/* ── Empty state stub (full styling in Phase E Task 14) ── */

/* ── Mobile top bar (lives inside .main-area; hidden on desktop) ── */
.mobile-bar {
  display: none;
  align-items: center;
  gap: .75rem;
  height: 52px;
  padding: 0 .875rem;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 30;
}
.mobile-menu-btn {
  width: 38px; height: 38px;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; color: var(--text); border: 1px solid var(--border);
  border-radius: 10px; cursor: pointer; padding: 0;
}
.mobile-menu-btn:hover { background: var(--surface-2); }
.mobile-brand { font-size: 18px; letter-spacing: -.02em; }

.sidebar-backdrop {
  display: none;
  position: fixed; inset: 0;
  background: var(--scrim);
  z-index: 40;
}

/* ── Responsive ── */
@media (max-width: 768px) {
  .app-shell { display: block; }      /* don't fight the sidebar in flex flow */
  .mobile-bar { display: flex; }

  .sidebar {
    position: fixed;
    top: 0; bottom: 0; left: 0;
    width: min(260px, 80vw);
    transform: translateX(-100%);
    transition: transform 180ms ease;
    z-index: 60;
    box-shadow: var(--shadow-lg);
  }
  .main-area {
    margin-left: 0;
    width: 100%;
    min-width: 0;
    /* clip, not hidden: `overflow-x: hidden` computes overflow-y to `auto` and
       turns this box into a scroll container, which is what .mobile-bar's
       `position: sticky` then sticks to. That container never scrolls (the
       page does), so the hamburger scrolled away on every page. `clip` cuts
       stray wide content without creating a scrollport. */
    overflow-x: clip;
  }
  .page-content { padding: 1rem .875rem; min-width: 0; }

  body.sidebar-open { overflow: hidden; }
  body.sidebar-open .sidebar { transform: translateX(0); }
  body.sidebar-open .sidebar-backdrop { display: block; }

  /* NOTE: the same rules are repeated under `.is-embed` at the end of this
     file, without the breakpoint. Change one, change the other. */

  /* Tighter cards / typography */
  .card { padding: 1rem; }
  .page-content h1 { font-size: 22px; }
  .page-content h2 { font-size: 18px; }

  /* Tables: horizontal scroll inside .table-wrap so layout doesn't blow up */
  .table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; max-width: 100%; }
  .data-table { min-width: 540px; }

  /* The identity column stays put while the rest scrolls — otherwise you swipe
     to "last seen" and can no longer tell whose device you are looking at. */
  .data-table thead th:first-child,
  .data-table tbody td:first-child {
    position: sticky;
    left: 0;
    z-index: 1;
    box-shadow: 1px 0 0 var(--border);
  }
  .data-table tbody td:first-child { background: var(--surface); }
  .data-table thead th:first-child { background: var(--surface-2); }
  /* The pinned column is a landmark, not the content. Unbounded it took ~62%
     of a 390px screen (a node name wrapping to three lines beside two chips),
     leaving a slit for the columns you swiped over to read. The floor matters
     as much as the ceiling: below ~140px a name breaks mid-word and stops
     being recognisable, which is the one thing the pin exists to prevent. */
  .data-table thead th:first-child,
  .data-table tbody td:first-child { min-width: 140px; max-width: 46vw; }
  /* Emails and hostnames have no break opportunities at all, so they get
     `anywhere`. Names are read, not parsed: `break-word` lets them wrap between
     words and only splits a word that genuinely cannot fit. */
  .cell-user__email { overflow-wrap: anywhere; }
  .cell-user__name,
  .cell-ident__name,
  .cell-server-name { overflow-wrap: break-word; }
  /* .data-table--plain draws no header bed; the sticky rule above was giving
     its first header cell one anyway, so a borderless table grew a single grey
     box in its top-left corner. */
  .data-table--plain thead th:first-child { background: var(--surface); }
  /* Chips belong under the name here, not beside it — side by side they take
     the width the name needs. */
  .cell-server-name { flex-wrap: wrap; }

  /* The v3 action row is the same shape as .form-actions and needs the same
     mobile rule; without it "Save" and "Delete" share one 390px line. */
  .form-actions-v3 { flex-direction: column; align-items: stretch; }
  .form-actions-v3 .btn-primary-v3,
  .form-actions-v3 .btn-secondary-v3,
  .form-actions-v3 .btn-danger-outline { width: 100%; justify-content: center; }
  .form-actions-v3__right { margin-left: 0; }

  /* A card heading and its actions stop sharing a line: at 390px "Pending
     invitations (3)" plus a button wraps into two ragged halves. */
  .card-header { flex-direction: column; align-items: stretch; }
  .card-header__actions { justify-content: flex-start; }

  /* Form / action groups stack vertically on mobile */
  .form-actions, .action-group {
    flex-direction: column; align-items: stretch;
  }
  .form-actions .btn, .action-group .btn { width: 100%; }

  /* Inline forms stack: an 18rem field plus a button does not fit 362px, and a
     row that cannot wrap pushes its submit button off the screen entirely. */
  .form-inline { flex-direction: column; align-items: stretch; }
  .form-inline > div,
  .form-inline input,
  .form-inline select,
  .form-inline button { width: 100%; }
  .input-w-sm, .input-w-md, .input-w-lg { width: 100%; }

  /* A borderless list table has no wide columns to protect */
  .data-table--plain { min-width: 0; }

  /* Long machine strings break instead of overflowing. `break-all` only for
     the block forms, which really do carry pubkeys and install commands with
     no break opportunity in them; an inline .mono cell is as often a hostname
     ("thinkpad-x1") or a model name, and break-all chopped those mid-word even
     when the column had room to wrap at the hyphen. */
  .mono { overflow-wrap: anywhere; }
  code, pre { word-break: break-all; overflow-wrap: anywhere; }

  /* Hero / cards that previously assumed wide viewport */
  .traffic-chart-wrap svg { max-width: 100% !important; }

  /* ── v3 mobile overrides ── */
  /* .page stays transparent here too — .page-content owns the mobile gutter. */
  .page-header h1 { font-size: 22px; }

  /* Two up, not one. auto-fit with a 180px floor gives a single column at
     390px, and four KPI tiles at 20px/24px padding then cost a whole screen
     before the page's actual content starts. The strip is a bare grid on the
     page ground, so it takes no padding of its own — .page-content owns the
     gutter. */
  .kpi-strip {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
    padding: 0;
    margin-bottom: 20px;
  }
  .kpi-card { padding: 12px 14px; }
  .kpi-value { font-size: 22px; }

  .data-table { font-size: 13px; }
  .data-table thead th,
  .data-table tbody td { padding: 10px 12px; }

  .card-header { padding: 12px 16px; }
  .card-header__actions { gap: 4px; }

  .platform-grid { grid-template-columns: 1fr !important; }

  /* The device list is a table now and inherits the table rules above. */

  .map-matrix th, .map-matrix td { padding: 6px 8px; font-size: 12px; }

}

/* ── Theme switcher ── */
.theme-switcher {
  display: flex;
  gap: 4px;
  margin-top: 12px;
  padding: 4px;
  background: var(--sidebar-hover-bg);
  border: 1px solid var(--sidebar-border);
  border-radius: var(--r-sm);
}
.theme-switcher__btn {
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 6px;
  background: transparent;
  border: none;
  border-radius: 4px;
  color: var(--sidebar-fg);
  cursor: pointer;
}
.theme-switcher__btn:hover {
  color: var(--sidebar-fg-active);
}
/* The language picker reuses the theme switcher's segmented shape — same place
   in the footer, same job, so the same control — but its segments carry a word
   rather than a 16px icon. */
.theme-switcher__btn--text {
  padding: 5px 6px;
  font-size: 12px;
  font-weight: 600;
  font-family: inherit;
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.theme-switcher__btn.is-active {
  background: var(--sidebar-active-bg);
  color: var(--sidebar-fg-active);
}

/* ============================================================
   Theme: explicit dark override
   Activates when <html data-theme="dark"> regardless of OS setting.
   ============================================================ */
[data-theme="dark"] {
  color-scheme: dark;
  --scrim:         rgba(0, 0, 0, .6);

  --accent:        #5b87ff;
  --accent-hover:  #7099ff;
  --accent-subtle: #1e2d5a;
  --accent-on-subtle: #8db0ff;
  --accent-solid:       #3563f0;
  --accent-solid-hover: #4571f5;
  --on-accent:     #ffffff;
  --focus-ring:    rgba(91, 135, 255, .28);

  --traffic-down:  #6aa6f5;
  --traffic-up:    #3fb950;

  --danger-fg:     #f87171;
  --danger-border: #7f1d1d;
  --danger-bg:     #450a0a;

  --success-fg:     #86efac;
  --success-border: #166534;
  --success-bg:     #052e16;
  --warning-fg:     #fde68a;
  --warning-border: #a16207;
  --warning-bg:     #3a2e08;

  --role-admin-fg: #c4b5fd;
  --role-admin-bg: #2e1065;

  --bg:            #0d1117;
  --surface:       #161b22;
  --surface-2:     #21262d;
  --input-bg:      #0d1117;
  --border:        #30363d;

  --text:          #e6edf3;
  --text-2:        #8b949e;
  --text-3:        #7d8590;

  --status-alive-bg:   #064e3b;
  --status-alive-fg:   #6ee7b7;
  --status-pending-bg: #451a03;
  --status-pending-fg: #fbbf24;
  --status-dead-bg:    #450a0a;
  --status-dead-fg:    #fca5a5;
  --status-disabled-bg:#1f2937;
  --status-disabled-fg:#9ca3af;

  --ok: #34D399;
  --warn: #FBBF24;
  --danger: #F87171;

  --sidebar-bg:    #090d13;
  --sidebar-fg:    #7d8590;
  --sidebar-fg-active: #e6edf3;
  --sidebar-active-bg: rgba(255,255,255,0.06);
  --sidebar-border:    #21262d;
  --sidebar-hover-bg:  rgba(255,255,255,0.04);

  --shadow-sm: 0 1px 3px rgba(0,0,0,.3);
  --shadow:    0 4px 12px rgba(0,0,0,.35);
  --shadow-lg: 0 8px 24px rgba(0,0,0,.45);
}

/* Component dark-mode rules for explicit data-theme="dark"
   (mirrors the @media (prefers-color-scheme: dark) component blocks above
    so explicit dark mode works even on a light OS). */
[data-theme="dark"] .form-control { background: var(--surface-2); }

/* ── Clickable traffic cells (open popup on click) ───────────────────── */
.traffic-link {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  text-decoration: none;
  border-bottom: 1px dashed transparent;
  cursor: pointer;
  font-family: var(--font-mono);
}
.traffic-link:hover {
  text-decoration: none;
  border-bottom-color: var(--accent);
}

/* ── Traffic popup (universal admin chart modal) ──────────────────────── */
.traffic-popup {
  position: fixed; inset: 0;
  display: none;
  z-index: 1000;
}
body.traffic-popup-open .traffic-popup { display: block; }

.traffic-popup-backdrop {
  position: absolute; inset: 0;
  background: var(--scrim);
  backdrop-filter: blur(2px);
  cursor: pointer;
}

.traffic-popup-panel {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: min(720px, calc(100% - 2rem));
  max-height: calc(100vh - 2rem);
  overflow: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  box-shadow: var(--shadow-lg);
  padding: 1.25rem 1.5rem;
}

.traffic-popup-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1rem;
}

.traffic-popup-controls {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: 1rem;
}

.traffic-popup-totals {
  font-size: 14px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  text-align: right;
  line-height: 1.5;
}

.btn-group {
  display: inline-flex;
  gap: 0;
  border-radius: 6px;
  overflow: hidden;
  border: 1px solid var(--border);
}
.btn-group .btn {
  border: none;
  border-radius: 0;
  background: transparent;
}
.btn-group .btn + .btn { border-left: 1px solid var(--border); }
.btn-group .btn.is-active {
  background: var(--accent-subtle);
  color: var(--accent-on-subtle);
  font-weight: 600;
}

.traffic-popup-body { min-height: 160px; }

[data-theme="dark"] .traffic-popup-panel { background: var(--surface-2, var(--surface)); }

/* ── Map matrix (v3 — Phase B Task 2) ── */
.map-matrix-wrap { overflow-x: auto; }

.map-matrix {
  width: 100%;
  border-collapse: collapse;
}

.map-matrix th,
.map-matrix td {
  padding: 8px 12px;
  text-align: center;
  vertical-align: middle;
  white-space: nowrap;
}

/* Corner cell */
.map-corner {
  text-align: left;
  font-size: 12px;
  font-weight: 500;
  color: var(--text-2);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  position: sticky;
  left: 0;
  z-index: 2;
}

/* Server column headers */
.map-server-h {
  font-size: 12px;
  font-weight: 500;
  color: var(--text-2);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

/* Device row headers (sticky left column) */
.map-device-h {
  text-align: left;
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--surface);
  border-right: 1px solid var(--border);
  min-width: 160px;
}

.map-device-h .device-name {
  font-weight: 600;
  font-size: 14px;
  color: var(--text);
}

.map-device-h .device-owner {
  font-size: 12px;
  color: var(--text-2);
  font-weight: 400;
  margin-top: 2px;
}

/* Base cell */
.map-cell {
  min-width: 80px;
  height: 56px;
  border: 1px solid var(--border);
  font-size: 13px;
}

/* Status variants */
.map-cell--alive    { background: var(--status-alive-bg);    color: var(--status-alive-fg); }
.map-cell--pending  { background: var(--status-pending-bg);  color: var(--status-pending-fg); }
.map-cell--dead     { background: var(--status-dead-bg);     color: var(--status-dead-fg); }
.map-cell--disabled { background: var(--status-disabled-bg); color: var(--status-disabled-fg); }

/* Cell content */
.map-latency { font-weight: 600; line-height: 1.2; }
.map-age     { font-size: 11px; opacity: 0.7; margin-top: 2px; }

/* Legend row */
.map-legend {
  display: flex;
  gap: 16px;
  margin-top: 16px;
  flex-wrap: wrap;
  align-items: center;
}

.legend-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--text-2);
}

.legend-chip--alive    { color: var(--status-alive-fg); }
.legend-chip--pending  { color: var(--status-pending-fg); }
.legend-chip--dead     { color: var(--status-dead-fg); }
.legend-chip--disabled { color: var(--status-disabled-fg); }

/* ─── Auth screens (login, forbidden) ─── */
.auth-shell {
  min-height: 100vh;
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
  background: var(--surface);
}
.auth-card {
  width: 100%; max-width: 420px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r);
  box-shadow: var(--shadow);
  padding: 32px;
}
.auth-brand {
  width: 48px; height: 48px;
  display: flex; align-items: center; justify-content: center;
  margin: 0 auto 16px;
}
.auth-brand svg { display: block; width: 48px; height: 48px; }
.auth-title {
  font-size: 24px; font-weight: 700;
  text-align: center; margin: 0 0 4px;
  color: var(--text);
}
.auth-subtitle {
  font-size: 13px; color: var(--text-2);
  text-align: center; margin: 0 0 24px;
}
.auth-error {
  padding: 12px 16px;
  background: var(--status-dead-bg);
  color: var(--status-dead-fg);
  border-radius: var(--r-sm);
  font-size: 14px;
  margin-bottom: 16px;
}
.auth-form { display: flex; flex-direction: column; gap: 16px; }
.form-field { display: flex; flex-direction: column; gap: 6px; }
.form-field label { font-size: 13px; font-weight: 600; color: var(--text); }
/* Look and focus ring come from the base input rule; only layout lives here. */
.form-field input,
.form-field select,
.form-field textarea { display: block; width: 100%; }
.btn-primary-block {
  display: block;
  width: 100%;
  padding: 10px 16px;
  background: var(--accent-solid); color: var(--on-accent);
  border: none; border-radius: var(--r-sm);
  font-size: 14px; font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  text-align: center;
  transition: background 0.12s;
}
.btn-primary-block:hover { background: var(--accent-solid-hover); text-decoration: none; }
.auth-footer {
  font-size: 13px; color: var(--text-2);
  text-align: center; margin: 24px 0 0;
}
.auth-footer a { color: var(--accent); text-decoration: none; }
.auth-footer a:hover { text-decoration: underline; }

/* ─── Settings rows (Account, etc.) ─── */
.settings { padding: 0; }
.setting-row {
  display: flex; align-items: center; gap: 16px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--border);
}
.setting-row:last-child { border-bottom: none; }
.setting-row__main { flex: 1; min-width: 0; }
.setting-row__label { font-size: 14px; font-weight: 600; color: var(--text); margin-bottom: 2px; }
/* An email is one unbroken token and this is where the panel prints it: with
   no wrap rule it pushed the row's button out of the card, where the mobile
   clip removed it without a trace. */
.setting-row__value { font-size: 14px; color: var(--text-2); overflow-wrap: anywhere; }
.setting-row__desc { font-size: 13px; color: var(--text-2); margin-top: 2px; overflow-wrap: anywhere; }
.setting-row__action { flex-shrink: 0; }

/* An expandable row stacks: the summary keeps the row layout, and the form it
   reveals spans the full width instead of being squeezed into the action
   column — labelled fields need the room to stay readable. */
.setting-row--expandable { display: block; }
.setting-row__head {
  display: flex; align-items: center; gap: 16px;
  cursor: pointer;
  list-style: none;
}
.setting-row__head::-webkit-details-marker { display: none; }
.setting-row--expandable details[open] > .setting-row__head {
  margin-bottom: 14px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--border);
}
.setting-row__head .setting-row__main { display: block; }
.setting-row__head .setting-row__label,
.setting-row__head .setting-row__value,
.setting-row__head .setting-row__desc { display: block; }
/* The summary's affordance must not be squeezed by the value beside it. */
.setting-row__head > .btn-outline-sm { flex-shrink: 0; }
.setting-row__head .setting-row__value .chip { margin-left: 6px; vertical-align: middle; }

/* Pending email change: shown under the row, outside the disclosure, so it
   stays visible whether or not the change form is open. */
.pending-change {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
  margin-top: 12px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
}
.pending-change__text { font-size: 13px; color: var(--text-2); }
.pending-change__form { margin: 0; }

/* ─── Status chips ─── */
.chip {
  display: inline-block;
  padding: 2px 10px;
  border-radius: 99px;
  font-size: 12px;
  font-weight: 600;
  line-height: 18px;
}
/* Role chips: staff roles wear the accent-family badge (the old 'admin'
   colour), the two client roles stay neutral so a page of members reads
   calm. chip--admin is gone with the role itself. */
.chip--owner     { background: var(--role-admin-bg); color: var(--role-admin-fg); }
.chip--superuser { background: var(--role-admin-bg); color: var(--role-admin-fg); }
.chip--powered   { background: var(--status-pending-bg); color: var(--status-pending-fg); }
.chip--member    { background: var(--status-disabled-bg); color: var(--status-disabled-fg); }

/* ─── Buttons (small variants) ─── */
.btn-primary-sm {
  padding: 6px 12px;
  background: var(--accent-solid); color: var(--on-accent);
  border: none; border-radius: 6px;
  font-size: 13px; font-weight: 500;
  font-family: inherit;
  cursor: pointer;
}
/* --accent-hover is the LINK blue's hover step; a filled button needs the
   solid one, or white text on it falls to ~2.7:1 in the dark theme. */
.btn-primary-sm:hover { background: var(--accent-solid-hover); }
/* .btn-outline-sm is declared once, up at the card-header group. A second
   declaration used to live here and won on source order, which cost the
   button its inline-flex/gap (so an icon beside a label sat on the baseline)
   and made hover fill with --surface — the card's own colour, i.e. no hover
   at all. */

/* ─── Inline edit (details/summary) ─── */
.inline-edit summary { list-style: none; cursor: pointer; }
.inline-edit summary::-webkit-details-marker { display: none; }
/* Stacked disclosures need air between them. Four in a row, each with a
   button-styled summary, touched edge to edge and read as one segmented
   control rather than as four separate things to open. */
.inline-edit + .inline-edit { margin-top: 8px; }
.inline-edit-form {
  display: flex; gap: 8px; align-items: center;
  margin-top: 8px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}
.inline-edit-form--stacked { flex-direction: column; align-items: stretch; }
/* Panel variant: a real labelled form, one field per line, capped at a readable
   measure rather than stretched across the card. The rule that separates it from
   the row lives on the summary instead, so it spans the whole row rather than
   stopping at the form's measure. */
.inline-edit-form--panel {
  display: block;
  max-width: 420px;
  margin-top: 0;
  padding-top: 0;
  border-top: none;
}
.inline-edit-form--panel .form-field + .form-field { margin-top: 12px; }
.inline-edit-form--panel input {
  width: 100%;
  padding: 8px 12px;
  font-size: 14px;
}
.inline-edit-form__hint {
  margin: 8px 0 0;
  font-size: 13px;
  color: var(--text-2);
  line-height: 1.5;
}
.inline-edit-form__actions { margin-top: 16px; }

/* Способ подключения: две карточки-варианта. Подпись под каждым названием
   обязательна — выбор между "свой адрес" и "за роутером" человек делает по
   описанию своей машины, а не по термину. */
/* Путь из двух звеньев: вход (релей) → выход (сама нода). Стрелка обязательна —
   без неё два флага подряд читаются как одно место, записанное дважды. */
/* Text flow, not flex. A wrapping flex line breaks BETWEEN its items, so in a
   column too narrow for both legs the arrow began the next line and read as a
   bullet in front of the exit country — the exact "two places listed twice" the
   arrow is here to prevent. As text it cannot: the markup leaves no whitespace
   between a leg and the arrow that follows it, so the only break opportunity is
   AFTER the arrow, and a wrapped path ends its first line with "→". */
.path { display: block; }
.path__leg { display: inline; }
.path .country__flag { margin-right: 4px; }   /* what the flex gap used to give */
.path .path__arrow { margin-left: 4px; }      /* the space after it is the other half */
.path__arrow { color: var(--text-2); }
/* The access chips live with the other name-cell chips — see "Chips for server
   metadata" below, where both axes are defined together. */

.access-kind { border: 0; padding: 0; margin: 0; }
.access-kind legend {
  padding: 0;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-2);
}
.access-kind__option {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 2px 10px;
  align-items: start;
  margin-top: 10px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  cursor: pointer;
}
.access-kind__option:has(input:checked) {
  border-color: var(--accent);
  background: var(--surface-2);
}
.access-kind__option input { flex: none; margin-top: 3px; }
.access-kind__title { font-size: 14px; font-weight: 600; }
.access-kind__hint {
  grid-column: 2;
  font-size: 13px;
  color: var(--text-2);
  line-height: 1.5;
}
.inline-edit-form input {
  flex: 1;
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 13px;
  background: var(--bg);
  color: var(--text);
  font-family: inherit;
}

/* ─── Dark theme overrides ─── */

[data-theme="dark"] .form-field input { background: var(--surface-2); }

/* ─── Downloads page ─── */
.platform-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  padding: 20px;
  background: var(--surface);
  border-radius: var(--r);
}
.platform-card {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.platform-card--disabled { opacity: 0.6; }
.platform-icon {
  width: 32px; height: 32px;
  color: var(--text);
  margin-bottom: 4px;
}
.platform-icon-svg {
  width: 32px; height: 32px;
}
.platform-name { font-size: 16px; font-weight: 600; color: var(--text); }
.platform-name__version {
  font-size: 12px; color: var(--text-2);
  font-weight: 400;
  margin-left: 8px;
}
.platform-desc {
  font-size: 13px;
  color: var(--text-2);
  margin: 4px 0 12px;
  flex: 1;
}
.platform-action { margin-top: auto; }
.badge-disabled {
  display: inline-block;
  padding: 8px 16px;
  background: var(--surface-2); color: var(--text-3);
  border-radius: 6px;
  font-size: 14px;
}
.downloads-cta {
  text-align: center;
  margin: 24px 0;
  color: var(--text-2);
  font-size: 14px;
}
.downloads-cta a { color: var(--accent); text-decoration: none; }
.downloads-cta a:hover { text-decoration: underline; }

/* ─── Users list — data table v3 extensions ─── */
.data-table__actions { text-align: right; white-space: nowrap; }
.data-table__actions a,
.data-table__actions button { margin-left: 4px; vertical-align: middle; }
/* The action cluster is usually a link beside a one-button <form>. `.inline-form`
   is `display: inline`, so the button inside it sat on the text baseline while
   its neighbours were centred — a few pixels low, on every row of every list. */
.data-table__actions .inline-form { display: inline-flex; vertical-align: middle; }

.cell-dim { color: var(--text-2); font-size: 13px; }

/* Counts in a table read as a column, so they get tabular figures: the digits
   line up between rows instead of drifting, which is what makes "12" next to
   "7" scannable as sizes rather than as words. A zero is muted — nothing there
   yet is not the same news as a number. */
.cell-num { font-variant-numeric: tabular-nums; font-size: 14px; color: var(--text); }
.cell-num--zero { color: var(--text-3); }

/* A member's machines on the family page: a plain named list, because the
   parent is reading who is signed in, not operating the devices — every
   control on them belongs to their owner. */
/* .family-devices is gone: a member's machines are a .cell-list inside the
   Devices column of the members table, which is where the question "whose
   machine is this" is actually asked. */

/* Location cell: country on top, the raw location/region muted beneath it. */
.cell-location { line-height: 1.35; }
.country { display: flex; align-items: center; gap: 6px; font-size: 14px; color: var(--text); }
/* Emoji flags ignore the text colour, so give them their own line-height and
   let them sit slightly larger than the label they precede. */
.country__flag { font-size: 16px; line-height: 1; }
.country__where { display: block; }

/* A node name you can click to rename: it stays a heading until hovered, so
   the row does not look like a form. */
.node-name-edit { cursor: pointer; list-style: none; display: inline; }
.node-name-edit::-webkit-details-marker { display: none; }
.node-name-edit:hover { text-decoration: underline dotted; text-underline-offset: 3px; }

.cell-user { display: flex; align-items: center; gap: 12px; }
.cell-user__meta { display: flex; flex-direction: column; }
.cell-user__name { font-weight: 600; font-size: 14px; color: var(--text); }
.cell-user__email { font-size: 12px; color: var(--text-2); }

.avatar-initials {
  width: 32px; height: 32px;
  border-radius: 50%;
  background: var(--accent-subtle);
  color: var(--accent-on-subtle);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 600;
  flex-shrink: 0;
}

.card-header__actions { display: flex; gap: 8px; align-items: center; }

.btn-icon {
  width: 32px; height: 32px;
  background: transparent; border: none;
  color: var(--text-2);
  cursor: pointer;
  border-radius: 6px;
  font-size: 18px;
  display: inline-flex; align-items: center; justify-content: center;
}
.btn-icon:hover { background: var(--surface-2); color: var(--text); }

.link-danger {
  background: transparent; border: none;
  /* --danger is the fill tone (3.76:1 as text); --danger-fg is the one that
     clears AA in both themes. */
  color: var(--danger-fg);
  cursor: pointer;
  font-size: 13px; font-weight: 500;
  padding: 4px 8px;
  font-family: inherit;
}
.link-danger:hover { text-decoration: underline; }

.inline-form { display: inline; }

/* A cell that looks like a control for the role that has one and reads as
   plain data for the role that does not. The traffic popup answers only an
   owner, and the same markup was rendering an <a> with no href but with the
   link's cursor, hover and "Open …" title for everyone else. */
.is-inert {
  cursor: default;
  pointer-events: none;
}
.is-inert:hover { background: none; border-color: transparent; text-decoration: none; }

/* ─── Status dot prefix (used in KPI labels) ─── */
.status-dot {
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 50%;
  margin-right: 4px;
  vertical-align: middle;
}
.status-dot--alive    { background: var(--ok); }
.status-dot--pending  { background: var(--warn); }
.status-dot--dead     { background: var(--danger); }
.status-dot--disabled { background: var(--text-3); }

/* ─── Chips for server status ─── */
.chip--alive    { background: var(--status-alive-bg);    color: var(--status-alive-fg); }
.chip--pending  { background: var(--status-pending-bg);  color: var(--status-pending-fg); }
.chip--dead     { background: var(--status-dead-bg);     color: var(--status-dead-fg); }
.chip--disabled { background: var(--status-disabled-bg); color: var(--status-disabled-fg); }
/* A check verdict is not a node's life sign, so it gets its own names — but it
   is the same two-state fact, so it borrows the same two tints rather than
   introducing a third green. /monitoring asked for these and got a
   transparent pill for months. */
.chip--ok  { background: var(--status-alive-bg); color: var(--status-alive-fg); }
.chip--bad { background: var(--status-dead-bg);  color: var(--status-dead-fg); }
/* A chip that carries no state — a kind or a label. It still needs a bed:
   a bare .chip is pill-shaped nothing, which is what "Inactive" and "status"
   rendered as. */
.chip--neutral { background: var(--surface-2); color: var(--text-2); }
/* A peer assignment is pending → active → removing. Only 'pending' had a tint;
   the other two rendered as bare text inside a pill-shaped nothing. */
.chip--active   { background: var(--status-alive-bg);    color: var(--status-alive-fg); }
.chip--removing { background: var(--status-pending-bg);  color: var(--status-pending-fg); }

/* ─── Chips for server metadata ─── */
/* Two independent axes ride beside the name: who may use the node
   (public/private) and how clients reach it (direct/relay). Both are metadata,
   so both stay quieter than the status chip in the next column — and they share
   one base, or the row reads as two unrelated badge systems that happened to
   land together.
   Two variables carry the meaning, and each carries exactly one thing:
     hue  = which axis    — visibility is neutral, access is accent
     fill = which value   — the default is an outline, the exception is filled
   Hue alone was not enough: 'private' and 'direct' both resolved to a grey fill
   that is one shade apart in dark mode and byte-identical in light
   (--status-disabled-bg and --surface-2 are both #f3f4f6), so two different
   questions had the same answer. The transparent border on the filled variants
   keeps every chip the same height, so the column holds one baseline. */
.chip--visibility,
.chip--access,
.chip--service { font-weight: 500; padding: 1px 8px; border: 1px solid transparent; }
/* Each further axis sits a little apart from the one before, so the group reads
   as separate answers rather than one long label. */
.chip--access,
.chip--service { margin-left: 4px; }

.chip--public  { border-color: var(--border); color: var(--text-3); }
.chip--private { background: var(--status-disabled-bg); color: var(--status-disabled-fg); }
.chip--direct  { border-color: color-mix(in srgb, var(--accent) 30%, transparent); color: var(--accent-on-subtle); }
.chip--relay   { background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent-solid); }

/* The third axis: is the node in service — the switch that decides who is given
   it at all. It follows the same fill rule as the two above, so the exception
   is the one that carries weight: in service is the ordinary state and stays an
   outline, out of service is filled and stops the eye. Its hue is neither of
   the other two, because "who may use it" and "how they reach it" are not
   answers to this question. */
.chip--in-service     { border-color: color-mix(in srgb, var(--ok) 35%, transparent); color: var(--status-alive-fg); }
.chip--out-of-service { background: var(--status-disabled-bg); color: var(--status-disabled-fg); }

/* ─── Server name cell with provider badge ─── */
.cell-server-name { display: flex; align-items: center; gap: 8px; }
/* For the fleet owner the name doubles as the way into the node's own page, and
   it keeps the row's colour rather than turning accent: one coloured word per
   row would make the column read as a list of links before it reads as a list
   of machines. .cell-user__name and .cell-ident__name already work this way,
   and the base rule's hover underline is what says it is clickable. */
.server-name { font-weight: 600; color: var(--text); }

/* ─── Identity cell ───
   The first column of every list: the row's name, the chips that qualify it,
   and an optional second line for the thing that disambiguates two rows with
   the same name (an email under a person, a hostname under a device). It is
   named for the job rather than the subject because a node, a device, a person
   and a browser session all need exactly this shape. */
.cell-ident { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; min-width: 0; }
.cell-ident__name { font-weight: 600; color: var(--text); }
/* flex-basis 100% is what puts the sub-line under the name instead of beside
   the last chip. */
.cell-ident__sub { flex: 0 0 100%; font-size: 12px; color: var(--text-2); overflow-wrap: break-word; }

/* A short list inside one cell — a member's machines, a node's tenants. Not a
   nested table: these are names, not rows, and the cell already sits in the
   column that names them. */
.cell-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }

/* A cell whose job is "state, and the button that changes it". The chip says
   what is true now; the button says what pressing it will do. They stack so
   the pair reads top-to-bottom rather than competing for one line. */
.cell-toggle { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }

/* "Runs / stops, and until when" — the chip and the sentence beside it, on one
   line, because the second half only means anything as a qualifier of the
   first. In a table cell they stack instead: .cell-toggle owns that. */
.access-state { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* A form that belongs to the table above it — "add a row" — so it sits inside
   the same flush card, under the table's last rule rather than in a card of
   its own. */
.card-footer-form {
  padding: 14px 20px;
  border-top: 1px solid var(--border);
}
.card-footer-form .form-inline { margin: 0; }

/* ─── Provider badge ─── */
.provider-badge {
  display: inline-block;
  width: 26px; height: 20px;
  border-radius: 4px;
  text-align: center;
  line-height: 20px;
  font-size: 10px;
  font-weight: 700;
  color: white;
  flex-shrink: 0;
  letter-spacing: 0;
}
.provider-badge--aws      { background: #FF9900; }
.provider-badge--gcp      { background: #4285F4; }
.provider-badge--azure    { background: #0078D4; }
.provider-badge--do       { background: #0080FF; }
.provider-badge--hetzner  { background: #D50C2D; }
.provider-badge--linode   { background: #00A95C; }
.provider-badge--vultr    { background: #007BFC; }
.provider-badge--ovh      { background: #123F6D; }
.provider-badge--scaleway { background: #4F0599; }
.provider-badge--unknown  { background: var(--text-3); }

/* ─── Callout code block (bootstrap token) ─── */
.callout-code {
  margin-top: 8px;
  padding: 12px;
  background: var(--surface-2);
  border-radius: var(--r-sm);
  font-family: ui-monospace, monospace;
  font-size: 12px;
  overflow-x: auto;
  white-space: pre;
  color: inherit;
  border: none;
}

/* ─── Devices list ─── */
.cell-device__name { font-weight: 600; font-size: 14px; color: var(--text); }
.cell-device__host { font-family: ui-monospace, monospace; font-size: 12px; color: var(--text-2); margin-top: 2px; }
.pubkey { font-size: 12px; vertical-align: middle; }
.btn-copy {
  background: transparent; border: none;
  color: var(--text-2);
  cursor: pointer;
  padding: 2px 4px;
  margin-left: 4px;
  border-radius: 4px;
  font-size: 14px;
}
.btn-copy:hover { background: var(--surface-2); color: var(--text); }

/* 76-family — the household page.
 *
 * Sits after the table layers it builds on (70s) and before the help and
 * responsive layers that override everything (80s, 90s), which is what the
 * two-digit prefix is for: the layers are joined in name order and the number
 * IS this file's place in the cascade.
 */

/* The seat ledger under the page title: a fact, not a heading, so it reads at
 * the weight of the subtitle beside it rather than competing with the name of
 * the page. */
.seat-ledger {
	margin: 0.25rem 0 0;
	color: var(--text-muted);
	font-size: 0.875rem;
	font-variant-numeric: tabular-nums;
}

/* Codes nobody has entered yet. Muted against the member rows above them:
 * these are not people, and the eye should reach the household first. */
.pending-invite td {
	color: var(--text-muted);
}

.pending-invite code.mono {
	color: var(--text);
}

/* A code shown exactly once — the replacement handed to a member after their
 * code was reset. Large and monospaced because it is about to be read aloud or
 * copied by hand off this screen, and there is no second chance to look. */
.code-once {
	margin: 0.75rem 0;
	font-size: 1.5rem;
	letter-spacing: 0.08em;
	text-align: center;
}

.code-once code {
	user-select: all;
}

/* The account code's cover. The <details> does the revealing; this only makes
 * the closed state read as "there is something here, deliberately not shown"
 * rather than as an empty row. */
.code-cover > summary {
	cursor: pointer;
}

.code-qr {
	display: block;
	width: 180px;
	max-width: 100%;
	margin: 0.5rem auto;
	image-rendering: pixelated;
}

/* ── The members table after the 2026-09-10 redesign ─────────────────── */

/* The count that opens a member's devices dialog: a small outline button
 * with the phone glyph, so it reads as "there is a list behind this" rather
 * than as a bare number. */
.family-devices-btn {
	gap: 6px;
	font-variant-numeric: tabular-nums;
}

/* A chip that IS the switch. The element is a <button>, which the browser
 * dresses with its own border, font and padding; strip that back to the chip
 * every other state in the panel wears, and say "pressable" with the cursor
 * and a ring on hover rather than with a second control beside it. */
button.chip--toggle {
	border: 0;
	font-family: inherit;
	cursor: pointer;
	appearance: none;
	-webkit-appearance: none;
}
button.chip--toggle:hover,
button.chip--toggle:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 1px;
}

/* Nine columns have to fit a desktop card, and at the panel's default cell
 * padding they did not: the table overflowed the card and the icons were
 * cut off at its edge, scrolled out of sight with nothing to say so. So the
 * two long headers may wrap, the cells lose a third of their side padding,
 * the access picker is a rem narrower (measured: "Навсегда" still reads
 * whole at 8rem) and the icons are 28px.
 *
 * thead/tbody in the selectors: the panel's own rules are written as
 * `.data-table thead th`, and a selector one level shorter loses to them
 * whatever layer it sits in. */
.family-table thead th {
	white-space: normal;
}
.family-table thead th,
.family-table tbody td {
	padding-left: 0.625rem;
	padding-right: 0.625rem;
}
.family-table .input-w-md {
	width: 8rem;
}
/* A figure that wraps ("685.6" over "MB") is two facts to the eye. */
.family-table td.mono {
	white-space: nowrap;
}

/* The row of icons at the end of a member's row. It never wraps: a table
 * column cannot be narrower than its content's min-content width, and that is
 * the only floor Chrome honours here — `min-width` on a cell is not one. The
 * rename form lives in a dialog for the same reason: unfolding inside this
 * cell it widened the column past the card. */
.family-actions {
	display: flex;
	align-items: center;
	gap: 2px;
	flex-wrap: nowrap;
}
.family-actions .btn-icon {
	width: 28px;
	height: 28px;
}

/* The access cell: the state on one line, the picker under it. */
.family-table .access-state {
	white-space: nowrap;
}
.family-table .inline-form {
	display: inline-flex;
	align-items: center;
	gap: 4px;
}

/* One member's machines, and one member's rename. A native <dialog>: the
 * browser owns focus, Esc and the stacking; this only draws it as a card of
 * the panel's own surface. The padding is on the inner panel, not the dialog,
 * so a click that lands on the dialog element itself is a click on the
 * backdrop and closes it. */
.family-dialog {
	padding: 0;
	border: 1px solid var(--border);
	border-radius: var(--r);
	background: var(--surface);
	color: var(--text);
	box-shadow: var(--shadow-lg);
	width: min(760px, calc(100% - 2rem));
	max-height: calc(100vh - 2rem);
}
.family-dialog--small {
	width: min(420px, calc(100% - 2rem));
}
.family-dialog::backdrop {
	background: var(--scrim);
	backdrop-filter: blur(2px);
}
.family-dialog__panel {
	padding: 1rem 1.25rem 1.25rem;
}
.family-dialog__header {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 1rem;
	margin-bottom: 0.75rem;
}
.family-dialog__header h2 {
	margin: 0;
	font-size: 1rem;
}
.family-dialog__form {
	display: flex;
	flex-direction: column;
	gap: 0.5rem;
	align-items: flex-start;
}
.family-dialog__form input {
	width: 100%;
}
/* ─── Form layout (forms in cards) ─── */
.form-card-v3 { padding: 20px; }
/* Inside a padded .card the form must not pad again: 24px of card plus 20px of
   form inset every field by 44px, so the fields sat visibly further in than the
   card's own heading. In a .card--flush the form keeps its padding, because
   there the card has none. */
.card:not(.card--flush) > .form-card-v3 { padding: 0; }
.form-row {
  display: flex; flex-direction: column;
  gap: 6px;
  margin-bottom: 16px;
}
.form-row label { font-size: 13px; font-weight: 600; color: var(--text); }
.form-row input,
.form-row select,
.form-row textarea { width: 100%; }
.form-row__help {
  font-size: 12px;
  color: var(--text-2);
  margin-top: 4px;
}
.form-row__inline-link {
  font-size: 12px;
  color: var(--accent);
  text-decoration: none;
  float: right;
  margin-top: -22px;
}
.form-row__inline-link:hover { text-decoration: underline; }

.form-actions-v3 {
  display: flex; align-items: center; gap: 12px;
  margin-top: 8px;
}
.form-actions-v3__right { margin-left: auto; }
.btn-primary-v3 {
  display: inline-flex; align-items: center;
  padding: 10px 20px;
  background: var(--accent-solid); color: var(--on-accent);
  border: none; border-radius: var(--r-sm);
  font-size: 14px; font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  text-decoration: none;
  transition: background 0.12s;
}
.btn-primary-v3:hover { background: var(--accent-solid-hover); color: var(--on-accent); text-decoration: none; }
.btn-link-v3 {
  color: var(--accent);
  text-decoration: none;
  font-size: 14px;
}
.btn-link-v3:hover { text-decoration: underline; }
/* The v3 family had a primary, a link and a danger outline but no plain
   secondary, so a form needing one reached for .btn-outline-sm — a shorter
   button from a different scale, sitting 8px lower than the Save beside it. */
.btn-secondary-v3 {
  display: inline-flex; align-items: center;
  padding: 10px 20px;
  background: transparent; color: var(--text);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  font-size: 14px; font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  text-decoration: none;
  transition: background 0.12s, border-color 0.12s;
}
.btn-secondary-v3:hover { background: var(--surface-2); border-color: var(--text-3); text-decoration: none; }
.btn-danger-outline {
  display: inline-flex; align-items: center;
  padding: 8px 16px;
  background: transparent;
  color: var(--danger-fg);
  border: 1px solid var(--danger-border);
  border-radius: var(--r-sm);
  font-size: 14px; font-weight: 500;
  font-family: inherit;
  cursor: pointer;
  transition: background 0.12s;
}
.btn-danger-outline:hover { background: var(--status-dead-bg); }

/* .form-row--readonly is gone with its only user: /users/{id}/edit printed
   three facts as readonly inputs — focusable, tabbable controls that accept
   nothing — where the panel's .meta-grid says the same thing without pretending
   to be a form. */
[data-theme="dark"] .form-row input,
[data-theme="dark"] .form-row select,
[data-theme="dark"] .form-row textarea { background: var(--surface-2); }
@media (prefers-color-scheme: dark) {
  [data-theme="system"] .form-row input,
  [data-theme="system"] .form-row select,
  [data-theme="system"] .form-row textarea { background: var(--surface-2); }
}

/* ─── Success card (invite_success, etc.) ─── */
.success-card {
  max-width: 600px;
  margin: 0 auto;
  padding: 32px;
  text-align: center;
}
.success-card__icon { margin-bottom: 16px; display: flex; justify-content: center; }
.success-card__title {
  font-size: 22px; font-weight: 700;
  margin: 0 0 4px;
  color: var(--text);
}
.success-card__expires {
  font-size: 13px; color: var(--text-2);
  margin: 0 0 24px;
}
.success-card__hint {
  font-size: 13px; color: var(--text-2);
  margin: 16px 0 24px;
}
.success-card__actions {
  display: flex; justify-content: center; gap: 12px;
  align-items: center;
  border-top: 1px solid var(--border);
  padding-top: 20px;
  margin-top: 8px;
}

/* ─── Copy-input group (read-only URL + Copy button) ─── */
.copy-input-group {
  display: flex; gap: 8px;
  /* wrap + min-width:0: an invitation URL is one unbreakable token, so without
     both the input refused to shrink and pushed the Copy button off the right
     edge of a phone — where .main-area's clip removed it. */
  flex-wrap: wrap;
  margin: 0 auto;
  max-width: 480px;
}
.copy-input {
  flex: 1 1 240px;
  min-width: 0;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--text);
  font-family: ui-monospace, monospace;
  font-size: 12px;
}

/* ─── Info / warning callouts (form inline) ─── */
.callout {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 12px 16px;
  border-radius: var(--r-sm);
  font-size: 13px;
  margin-bottom: 16px;
}
.callout svg { flex-shrink: 0; margin-top: 2px; }
.callout--info {
  background: var(--accent-subtle);
  color: var(--text);
  border: 1px solid var(--accent);
}
.callout--info svg { color: var(--accent); }

/* ─── Mono input inside form-row ─── */
.form-row__mono {
  font-family: ui-monospace, monospace;
  font-size: 12px;
}

/* ─── Forbidden screen ─── */
.forbidden-card { text-align: center; padding: 40px 32px; }
.icon-circle {
  width: 64px; height: 64px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  margin: 0 auto 24px;
}
.icon-circle--danger {
  background: var(--status-dead-bg);
  color: var(--danger);
}
.forbidden-body {
  font-size: 14px; color: var(--text-2);
  margin: 0 0 28px;
  max-width: 320px;
  margin-left: auto;
  margin-right: auto;
}
.forbidden-actions {
  display: flex; flex-direction: column;
  gap: 12px; align-items: center;
}
.logout-mini { margin: 0; }
.logout-mini button {
  background: transparent;
  border: none;
  padding: 0;
  font-family: inherit;
  font-size: 14px;
  cursor: pointer;
}

/* ─── Empty state (lists) ─── */
.empty-state {
  display: flex; flex-direction: column; align-items: center;
  padding: 64px 24px;
  text-align: center;
  color: var(--text-2);
}
.empty-state__icon { width: 48px; height: 48px; color: var(--text-3); margin-bottom: 16px; }
.empty-state__title { font-size: 16px; font-weight: 600; margin: 0 0 6px; color: var(--text); }
.empty-state__desc { font-size: 13px; margin: 0 0 20px; max-width: 320px; }
.empty-state .btn-primary-v3 { font-size: 14px; }

/* ─── Accessibility ─── */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}
/* Inputs already have explicit focus rings, skip them for :focus-visible */
input:focus-visible,
select:focus-visible,
textarea:focus-visible { outline: none; }

/* Visually hidden but readable by screen readers */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* Skip link: first tab stop on every page, so keyboard users don't walk the
   whole sidebar before reaching the thing they came for. */
.skip-link {
  position: absolute;
  left: 8px;
  top: -60px;
  z-index: 100;
  padding: 10px 16px;
  border-radius: var(--r-sm);
  background: var(--accent-solid);
  color: var(--on-accent);
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  transition: top .12s;
}
.skip-link:focus { top: 8px; color: var(--on-accent); text-decoration: none; }

/* Nothing in the panel animates to convey meaning — the drawer, the hovers and
   the popup all settle into the same visible state — so dropping the tween for
   readers who ask for less motion costs no information. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── Wave 3: dashboard security banners + device liveness pills ───────────── */
.security-banner {
  display: flex;
  align-items: center;
  gap: .625rem;
  padding: .75rem 1rem;
  margin-bottom: 1rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  font-size: .9rem;
  background: var(--accent-subtle);
}
.security-banner--new { border-color: var(--warn); background: color-mix(in srgb, var(--warn) 12%, transparent); }
.security-banner--offline { border-color: var(--danger); background: color-mix(in srgb, var(--danger) 10%, transparent); }
.security-banner__icon { flex: 0 0 auto; color: var(--warn); }
.security-banner--offline .security-banner__icon { color: var(--danger); }
.security-banner__text { flex: 1 1 auto; color: var(--text); }
.security-banner form { margin: 0; }
/* Answer first, revoke second: the harmless way out sits closest to the text,
   and the pair wraps as one block rather than stranding a lone button. */
.security-banner__actions { flex: 0 0 auto; display: flex; align-items: center; gap: .5rem; }

/* .device-name-link and .status-pill went with the bespoke device list: an
   identity cell's link is .cell-ident__name, and online/offline is a .chip
   like every other state in the panel. Three pill vocabularies for one fact
   was two too many. */

/* ============================================================
   Help / guide page — illustrated, theme-aware diagrams.
   All visuals are inline SVG using design tokens or
   currentColor, so they invert correctly in dark mode.
   ============================================================ */

/* Section-header icon: matches nav icons (1.25–1.5em, currentColor,
   accent on hover). */
.help-h {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0;
}
.help-h__icon {
  width: 1.4em; height: 1.4em;
  flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-sm);
  color: var(--accent-on-subtle);
  background: var(--accent-subtle);
  transition: background .12s, color .12s;
}
.help-h__icon svg { width: 1em; height: 1em; display: block; }
.card:hover .help-h__icon { background: var(--accent-solid); color: var(--on-accent); }

/* Generic figure wrapper (panel inside a card section). */
.help-figure {
  margin: 18px 0 0;
  padding: 18px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r);
}

/* ── 1. Architecture diagram ── */
.help-arch {
  display: flex;
  align-items: stretch;
  gap: 8px;
  flex-wrap: wrap;
}
.help-arch__col {
  flex: 1 1 0;
  min-width: 150px;
  display: flex;
}
.help-node,
.help-servers {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 16px 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  text-align: center;
}
.help-node--accent {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent) inset;
}
.help-node__icon {
  width: 30px; height: 30px;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--text-2);
}
.help-node--accent .help-node__icon { color: var(--accent); }
.help-node__icon + .help-node__icon { margin-top: -2px; }
.help-node__icon svg { width: 26px; height: 26px; display: block; }
.help-node__label {
  font-size: 13px; font-weight: 600; color: var(--text);
}
.help-node__sub {
  font-size: 11px; color: var(--text-2);
  font-family: var(--font-mono);
  word-break: break-all;
}

/* Server-node row inside the "WireGuard servers" column */
.help-servers { gap: 4px; }
.help-server {
  position: relative;
  width: 30px; height: 30px;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--text-3);
}
.help-server svg { width: 26px; height: 26px; display: block; }
.help-server--best { color: var(--ok); }
.help-server__star {
  position: absolute;
  top: -4px; right: -6px;
  width: 13px !important; height: 13px !important;
  color: var(--warn);
}
.help-servers .help-server { margin-bottom: 2px; }
.help-servers .help-node__label { margin-top: 4px; }

/* Labelled connector arrows between columns */
.help-arch__link {
  flex: 0 1 90px;
  min-width: 64px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  color: var(--text-3);
}
.help-arch__link-label {
  font-size: 10.5px;
  color: var(--text-2);
  text-align: center;
  line-height: 1.3;
}
.help-flow-arrow { width: 56px; height: 20px; display: block; }

/* ── 2. Get started in 3 steps ── */
.help-steps {
  list-style: none;
  margin: 18px 0 0;
  padding: 18px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  flex-wrap: wrap;
}
.help-step {
  flex: 1 1 0;
  min-width: 130px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 16px 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  position: relative;
}
.help-step__num {
  position: absolute;
  top: -10px; left: 50%;
  transform: translateX(-50%);
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--accent-solid);
  color: var(--on-accent);
  font-size: 12px; font-weight: 700;
  display: inline-flex; align-items: center; justify-content: center;
  font-variant-numeric: tabular-nums;
}
.help-step__icon {
  width: 34px; height: 34px;
  margin-top: 6px;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--accent);
}
.help-step__icon svg { width: 28px; height: 28px; display: block; }
.help-step__cap {
  font-size: 13px; font-weight: 600; color: var(--text);
  text-align: center;
}
.help-step__arrow {
  flex: 0 0 auto;
  width: 26px; height: 26px;
  color: var(--text-3);
  display: inline-flex; align-items: center; justify-content: center;
}
.help-step__arrow svg { width: 22px; height: 22px; display: block; }

/* ── 3. Connection modes ── */
.help-modes {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px;
  margin: 4px 0 0;
}
.help-mode-card {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 16px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r);
}
.help-mode-fig {
  margin: 0;
  height: 84px;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
}

/* Auto: server grid + star + auto-switch arrow */
.help-mode-grid {
  display: grid;
  grid-template-columns: repeat(3, 12px);
  gap: 10px;
}
.help-dot {
  width: 12px; height: 12px;
  border-radius: 4px;
  background: var(--border);
  display: inline-flex; align-items: center; justify-content: center;
}
.help-dot--best {
  background: transparent;
  color: var(--warn);
}
.help-dot--best svg { width: 16px; height: 16px; display: block; }
.help-mode-auto-arrow {
  position: absolute;
  top: 12px; left: 50%;
  transform: translateX(-50%);
  width: 48px; height: 20px;
  color: var(--accent);
  opacity: .9;
}

/* Preferred: numbered 1·2·3 ordered stack */
.help-rank {
  list-style: none;
  margin: 0; padding: 0;
  display: flex;
  flex-direction: column;
  gap: 7px;
  width: 70%;
}
.help-rank__row {
  display: flex;
  align-items: center;
  gap: 8px;
}
.help-rank__n {
  width: 18px; height: 18px;
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--accent-subtle);
  color: var(--accent-on-subtle);
  font-size: 11px; font-weight: 700;
  display: inline-flex; align-items: center; justify-content: center;
  font-variant-numeric: tabular-nums;
}
.help-rank__bar {
  flex: 1;
  height: 8px;
  border-radius: 99px;
  background: var(--border);
}
.help-rank__row:first-child .help-rank__bar { background: var(--accent); }

/* Manual: single pinned server */
.help-pin {
  width: 40px; height: 40px;
  color: var(--accent);
  display: inline-flex; align-items: center; justify-content: center;
}
.help-pin svg { width: 36px; height: 36px; display: block; }

/* ── 4. Connection-state pills ── */
.help-states {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 0 0 16px;
}
.help-pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 14px;
  border-radius: 99px;
  font-size: 13px;
  font-weight: 600;
  border: 1px solid currentColor;
}
.help-pill svg { width: 16px; height: 16px; display: block; }
.help-pill--ok     { color: var(--ok);     background: color-mix(in srgb, var(--ok) 12%, transparent); }
.help-pill--warn   { color: var(--warn);   background: color-mix(in srgb, var(--warn) 14%, transparent); }
.help-pill--danger { color: var(--danger); background: color-mix(in srgb, var(--danger) 12%, transparent); }

/* ── Help: responsive collapse ── */
@media (max-width: 768px) {
  .help-modes { grid-template-columns: 1fr; }

  /* Steps + arch stack vertically; rotate connector arrows to point down */
  .help-steps { flex-direction: column; align-items: stretch; }
  .help-step { width: 100%; min-width: 0; }
  .help-step__arrow { transform: rotate(90deg); align-self: center; }

  .help-arch { flex-direction: column; align-items: stretch; }
  .help-arch__col { width: 100%; min-width: 0; }
  .help-arch__link { flex-basis: auto; }
  .help-arch__link .help-flow-arrow { transform: rotate(90deg); }
}

/* /data — the fleet's configuration.
 *
 * Numbered 82 so it lands after the shared card and table rules it builds on
 * (and after 80-help, which is the last of the page layers), and before the
 * late overrides at 85 and above. The layers are joined in lexical name order,
 * so this prefix IS the cascade position — see TestCSSLayersDeclareTheirOrder.
 */

/* The ceiling is drawn rather than listed because it is the one number here
 * with a cost: a list past it is refused WHOLE by the client, and a bar shows
 * headroom at a glance where "1540 / 8192" makes you do arithmetic.
 */
.data-ceiling {
	margin: 1.25rem 0 1.75rem;
}

.data-ceiling__bar {
	height: 0.5rem;
	border-radius: 99px;
	background: var(--surface-2);
	overflow: hidden;
}

.data-ceiling__fill {
	display: block;
	height: 100%;
	border-radius: inherit;
	background: var(--accent-solid);
	/* Deliberately not animated. The bar's length is written once, server-side,
	 * into the inline style, and nothing on the page changes it afterwards —
	 * motion here could never fire, and would only claim a behaviour this
	 * element does not have. An empty bar still shows: the visible track is the
	 * parent, not this fill.
	 */
}

/* Past four fifths, the headroom is the story rather than the usage — and
 * raising the ceiling is not a fix available to whoever is reading this, so the
 * colour is a warning, not an error.
 */
.data-ceiling__fill.is-full {
	background: var(--warn);
}

.data-ceiling__label {
	margin: 0.5rem 0 0.25rem;
	font-variant-numeric: tabular-nums;
}

.data-groups {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
	gap: 1rem;
	margin-bottom: 1.5rem;
}

.data-group {
	padding: 1rem;
	border: 1px solid var(--border);
	border-radius: var(--r);
}

/* Counts line up column-wise so two groups can be compared by eye rather than
 * read one at a time.
 */
.data-group .kpi-value,
.data-group .cell-dim {
	font-variant-numeric: tabular-nums;
}

/* The tabs are links, not a widget: the panel holds no client-side state
 * anywhere, and a tab in the URL is one a person can bookmark and send. Styled
 * as an underlined row rather than as buttons, so it reads as navigation inside
 * the card — which is what it is.
 */
.data-tabs {
	display: flex;
	flex-wrap: wrap;
	gap: 0.25rem;
	border-bottom: 1px solid var(--border);
	margin-bottom: 1rem;
}

.data-tab {
	padding: 0.5rem 0.875rem;
	color: var(--text-2);
	text-decoration: none;
	border-bottom: 2px solid transparent;
	/* Pulled onto the row's own border so the active mark and that edge read as
	 * one line rather than two.
	 */
	margin-bottom: -1px;
}

.data-tab:hover {
	color: var(--text);
}

.data-tab.is-active {
	color: var(--text);
	border-bottom-color: var(--accent-solid);
	font-weight: 600;
}

/* The host table's address column is the widest thing on the page and the least
 * urgent, so it is allowed to wrap rather than to push the columns that answer
 * the question off the edge.
 */
.data-table td.mono.cell-dim {
	word-break: break-all;
}
/* /feedback — what people typed into the app's form.
 *
 * Numbered 83 so it lands after the shared card and table rules (20) and
 * the last page layer (82-data), and before the late overrides at 85 and
 * above. The layers are joined in lexical name order, so this prefix IS the
 * cascade position — see TestCSSLayersDeclareTheirOrder.
 */

/* The message is the column that matters, and it is the one a person typed
 * with line breaks in it: keep them, wrap long lines, and give it the width
 * the other cells do not need. A cap on width keeps one long rant from
 * pushing the build column off the right edge on a laptop.
 */
.feedback-table__text {
	white-space: pre-wrap;
	overflow-wrap: anywhere;
	min-width: 18rem;
	max-width: 40rem;
}

/* The build cell stacks three short facts; no wrapping inside any of them,
 * or "0.14.0 (7)" breaks after the version.
 */
.feedback-table .cell-dim .mono { white-space: nowrap; }
/* ============================================================
   Classes the markup was already asking for
   ------------------------------------------------------------
   Every selector below was referenced by a template (or by
   traffic_popup.js) but had no rule, so the element fell back to
   browser defaults — the announcements form rendering as white
   native controls on a dark card was this, and so was the login
   page's "Password Forgot password?" running together.
   ============================================================ */

/* Page header: text on the left, actions on the right */
.page-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
.page-header__text { min-width: 0; }
.page-header__actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* Card variants */
.card--tight  { padding: .875rem 1.25rem; margin: 0 0 1rem; }
.card--narrow { max-width: 760px; }

/* Two equal columns on desktop, stacked on narrow screens */
.admin-top-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 1rem;
  margin-top: 1rem;
}

/* Stacked form inside a card */
.form-stack {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin-top: 12px;
}
.form-inline--divided {
  margin-top: 1rem;
  padding-top: .875rem;
  border-top: 1px solid var(--border);
}
.label-hint { font-weight: 400; color: var(--text-2); }

/* Login: label and its "Forgot password?" link on one line */
.form-field__label-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
.form-field__aux { font-size: 12px; }

/* Chips: severity and on/off state (announcements) */
.chip--info     { background: var(--accent-subtle);       color: var(--accent-on-subtle); }
.chip--warning  { background: var(--status-pending-bg);   color: var(--status-pending-fg); }
.chip--critical { background: var(--status-dead-bg);      color: var(--status-dead-fg); }
.chip--active   { background: var(--status-alive-bg);     color: var(--status-alive-fg); }

/* Detail-page bits used by server_detail */
.meta-value--wrap { overflow-wrap: anywhere; }
.meta-grid--dense { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.node-history-link { display: flex; justify-content: flex-end; margin-bottom: .5rem; }
.node-transfer {
  margin-top: 1rem;
  padding-top: .875rem;
  border-top: 1px solid var(--border);
}
.node-transfer__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  flex-wrap: wrap;
  gap: .5rem;
}
.node-chart-wrap { margin-top: .875rem; }
.quota-bar {
  margin-top: .5rem;
  height: 8px;
  border-radius: var(--r-sm);
  background: var(--surface-2);
  overflow: hidden;
}
.quota-bar__fill { height: 100%; background: var(--accent); }
.quota-bar__fill--over { background: var(--danger); }
.access-list { margin-top: 1rem; padding-top: .875rem; border-top: 1px solid var(--border); }
.access-list__add { margin-top: .875rem; }

/* A data table with no card around it (access lists inside a section) */
.data-table--plain thead th { background: transparent; padding-left: 0; padding-right: 0; }
.data-table--plain tbody td { padding-left: 0; padding-right: 0; }

/* Host-load cell in the servers list */
.node-cpu { white-space: nowrap; }
.card-header__actions { display: flex; align-items: center; gap: 8px; }

/* Map matrix: what a dash in a cell means */
.map-hint { font-size: 12px; color: var(--text-2); margin: .75rem 0 0; }

/* Settings section intro paragraph */
.settings__intro { margin: 0 0 1rem; padding: 0 1rem; }
.setting-row--stacked { display: block; }

/* Invite / enrollment QR */
.qr-block { text-align: center; margin: 16px 0; }
.qr-block img { max-width: 100%; height: auto; }

/* Second half of a split form card (user_edit) */
.form-card-v3--continued { padding-top: 0; }

/* Verification banner text */
.email-verify-banner__text { flex: 1; min-width: 0; }

/* Traffic popup header + states (markup lives in base.html and the JS) */
.traffic-popup-title { margin: 0 0 .25rem; }
.traffic-popup-subject { margin: 0; font-size: 13px; }
.traffic-popup-placeholder { text-align: center; padding: 1rem; margin: 0; }
.traffic-popup-error { text-align: center; padding: 2rem 1rem; color: var(--danger); margin: 0; }

/* Prose lists inside help / install guides */
.prose-list { margin: 0; padding-left: 20px; line-height: 1.6; }

/* A KPI whose value is a phrase ("4 minutes ago") rather than a number */
.kpi-value--sm { font-size: 20px; }

/* Spacing utilities the templates use alongside .mb-0 / .mt-* */

.mb-2 { margin-bottom: 1rem; }

/* Map pin group (dot + label are styled individually) */
.geo-pin { pointer-events: none; }

/* Downloads: the "your own node" half of the page */
.page-header--section { margin-top: 2rem; }
.node-cta { margin: 0 0 1rem; }
.node-cta__note { margin-left: .75rem; font-size: 13px; }
.platform-hash { font-size: 11px; overflow-wrap: anywhere; }
.platform-caveat { font-size: 13px; margin: 1rem 0 0; }


/* Which plane carried the traffic — the breakdown under a total.
 *
 * Sits in 85 rather than beside .traffic-nums in 40-layout because it is a
 * later addition that leans on that family and must not be able to change it:
 * the two-digit prefix is the cascade, so a rule here can only override what
 * precedes it. The pieces themselves (.chip, .traffic-nums, .traffic-down /
 * .traffic-up and their tokens) are all reused unchanged; this only lays them
 * out.
 *
 * A grid rather than flex, so the byte figures line up in a column across
 * rows whatever the chips are called — with tabular-nums already on
 * .traffic-nums, that makes the two planes visually comparable at a glance,
 * which is the entire question the block answers. */
.proto-split {
  display: grid;
  grid-template-columns: max-content max-content;
  gap: 6px 12px;
  align-items: center;
  margin: 12px 0 22px;
}
.proto-split__row { display: contents; }
/* The caption spans both columns so the chips stay in theirs. Same size and
   colour as the chart legends it sits above, because it is the same kind of
   thing: a label for the figures beside it, not a heading for a section. */
.proto-split__caption {
  grid-column: 1 / -1;
  font-size: 12px;
  color: var(--text-2);
  margin-bottom: 2px;
}

/* A block worth HAVING and not worth showing: the notes box's own fences on
 * /monitoring, read once when something looks wrong and never otherwise.
 * Folded by default — nine rows of numbers nobody came looking for would
 * outweigh the four counts above them, which are what that section is for.
 *
 * The native marker is replaced rather than kept: every other disclosure in
 * this panel hides it too, and one page showing a platform triangle while the
 * rest do not reads as an unfinished page rather than a different control. */
.quiet-block { margin: 22px 0; }
.quiet-block__head {
  cursor: pointer;
  list-style: none;
  padding: 6px 0;
  font-size: 14px;
  font-weight: 600;
  color: var(--text-2);
}
.quiet-block__head::-webkit-details-marker { display: none; }
.quiet-block__head::before { content: "\25B8\00a0"; }
.quiet-block[open] > .quiet-block__head::before { content: "\25BE\00a0"; }

/* Messages: an unread row gets the same faint accent wash chip--info uses
   for its badge, and a heavier title — the two together are what "not yet
   opened" looks like on this table. */
.is-unread { background: var(--accent-subtle); }
.is-unread .cell-ident__name { font-weight: 700; }
/* ── Mobile overrides for components defined below the main responsive block ──
   Same specificity as the base rule, so they only win by coming later in the
   file. Keep them here rather than in the block at the top: from up there,
   `align-items` on .setting-row and .inline-edit-form silently lost. */
@media (max-width: 768px) {
  .setting-row {
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
  }
  .setting-row__action { width: 100%; }
  .setting-row__action .btn-outline-sm,
  .setting-row__action .btn { width: 100%; justify-content: center; }

  .inline-edit-form { flex-direction: column; align-items: stretch; }
  .inline-edit-form input,
  .inline-edit-form button { width: 100%; }

  /* Two controls plus a sentence don't fit one phone-width line: let the pair
     drop under the text instead of squeezing the sentence to a column. */
  .security-banner { flex-wrap: wrap; }
  .security-banner__actions { margin-left: auto; }
}

/* ── Embedded panel (the iOS / Mac web view) ──
   The drawer rules above are keyed to the phone breakpoint. An embedded panel
   is a phone-shaped surface at any width — the Mac and iPad web views are wider
   than 768px and would otherwise show a hamburger that opens nothing. Same
   rules, no breakpoint. */
.is-embed .app-shell { display: block; }
.is-embed .mobile-bar { display: flex; }
.is-embed .sidebar {
  position: fixed;
  top: 0; bottom: 0; left: 0;
  width: min(260px, 80vw);
  transform: translateX(-100%);
  transition: transform 180ms ease;
  z-index: 60;
  box-shadow: var(--shadow-lg);
}
.is-embed .main-area {
  margin-left: 0;
  width: 100%;
  min-width: 0;
  overflow-x: clip;                   /* see the breakpoint copy above */
}
.is-embed.sidebar-open { overflow: hidden; }
.is-embed.sidebar-open .sidebar { transform: translateX(0); }
.is-embed.sidebar-open .sidebar-backdrop { display: block; }
/* The client draws its own nav bar directly above the panel: a back chevron at
   the leading edge, the screen title centered. A hamburger sitting right under
   that chevron reads as a second back button, so in app mode it moves to the
   trailing edge and the wordmark takes the leading one. DOM order is unchanged
   (the button still comes first for the keyboard). */
.is-embed .mobile-bar { justify-content: space-between; }
.is-embed .mobile-menu-btn { order: 2; }

/* ─── Hover hints ───
   The layer lives on <body> (see hints.js) because every table here scrolls
   inside an overflow container, which would clip a tooltip drawn in place. */
.hint-layer {
  position: absolute;
  z-index: 200;
  max-width: 340px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-lg);
  font-size: 12px;
  line-height: 1.45;
  /* The hints are written as short lines rather than a paragraph: the step that
     broke, then the detail, then the timings. pre-line keeps that shape. */
  white-space: pre-line;
  pointer-events: none;
}
.hint-layer[hidden] { display: none; }

/* The (i) beside a column heading. A button, not a span: it has to be
   reachable by keyboard, since the hint is the only place the column's
   meaning is written down. */
.hint-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  margin-left: 5px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 99px;
  background: transparent;
  color: var(--text-3);
  /* The `micro` step from DESIGN.md's ramp, not a bespoke 9px: the glyph is an
     italic "i" on line-height 1, and 10px still clears the dot's 12px inner
     box. It was the stylesheet's only off-ramp size. */
  font-size: 10px;
  font-weight: 700;
  font-style: italic;
  line-height: 1;
  cursor: help;
  vertical-align: middle;
}
.hint-dot:hover,
.hint-dot:focus-visible { color: var(--text); border-color: var(--text-3); }

/* A measured cell explains itself on hover too, so the chip can stay short. */
.chip[data-hint] { cursor: help; }

