/* ============================================================
   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;
  --fw-medium: 500;
}

/* ── 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 — 15px 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;
}

/* ── 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;
}
.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; }

/* ── Email-verification banner ── */
.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-subtitle {
  color: var(--text-2);
  font-size: .875rem;
  margin: .25rem 0 0;
}

/* ── Detail-page primitives ──
   The small building blocks the detail pages need; figures use the KPI strip. */

/* 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 .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 need not 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; }

/* 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 ── */
/* 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; }

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 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;
}

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

/* ── Profile info block ── */

/* ── Device / traffic chart ── */
.traffic-chart-wrap {
  margin: 1rem 0 1.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); }

/* ── 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);
}

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

/* ── 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; keyed to .card--flush alone, a padded card's <h2>
   would fall through to the global 22px — near-Display headings under one
   Display heading. */
.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; }

.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; }
.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; }

/* 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; }

/* ── 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; }

/* ── 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; without this the sticky rule above
     would give its first header cell one anyway — a single grey box in a
     borderless table's 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-danger-outline { width: 100%; justify-content: center; }

  /* 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 {
    flex-direction: column; align-items: stretch;
  }
  .form-actions .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 would chop those mid-word
     even when the column has room to wrap at the hyphen. */
  .mono { overflow-wrap: anywhere; }
  code, pre { word-break: break-all; overflow-wrap: anywhere; }

  /* Hero / cards on a narrow 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 and inherits the table rules above. */

}

/* ── 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);
}

/* ── 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)); }

/* 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); }

/* ─── 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 would push the row's button out of the card, where the mobile
   clip removes 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 two client roles
   stay neutral so a page of members reads calm. */
.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 here would win on source order and cost the button its
   inline-flex/gap (an icon beside a label on the baseline) and its hover
   (--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);
}
/* 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 would begin 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; }   /* the space a flex gap would 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;
}
/* Wraps rather than squeezes: the macOS tile carries a button per
   architecture, and a narrow tile puts the second under the first. */
.platform-action { margin-top: auto; display: flex; flex-wrap: wrap; gap: 8px; }
.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 would sit on the text baseline
   while its neighbours are 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. */

/* 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; }

.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, so for everyone else an <a> with no href must not wear the link's
   cursor, hover and "Open …" title. */
.is-inert {
  cursor: default;
  pointer-events: none;
}
.is-inert:hover { background: none; border-color: transparent; text-decoration: none; }

/* ─── 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, and each state carries a
   tint — without one it is 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 is not enough: 'private' and 'direct' would both resolve 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), giving two different
   questions 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 { 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 { 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--hidden  { background: var(--status-disabled-bg); color: var(--status-disabled-fg); border-style: dashed; }
.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); }

/* ─── 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; }

/* ─── 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; }

/* ─── 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;
}

/* 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;
}

/* 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 ─────────────────── */

/* 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 do not: the table overflows the card and the icons are 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 ("Навсегда" 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-actions-v3 {
  display: flex; align-items: center; gap: 12px;
  margin-top: 8px;
}
.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; }
.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); }

[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 refuses to shrink and pushes the Copy button off the right
     edge of a phone — where .main-area's clip removes 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;
  }
}

/* ── 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; }

/* ============================================================
   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 asks for
   ------------------------------------------------------------
   Every selector below is referenced by a template (or by
   traffic_popup.js) and defined nowhere earlier; without a rule
   the element falls back to browser defaults.
   ============================================================ */

/* 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: info and warning */
.chip--info     { background: var(--accent-subtle);       color: var(--accent-on-subtle); }
.chip--warning  { background: var(--status-pending-bg);   color: var(--status-pending-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; }

.setting-row--stacked { display: block; }

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

/* 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; }
.platform-hash { font-size: 11px; overflow-wrap: anywhere; }
.platform-caveat { font-size: 13px; margin: 1rem 0 0; }

/* A block worth HAVING and not worth showing: the notes box's own fences on
 * /relay, 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. */
  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; }

