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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

/* ── Profile info block ── */

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

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

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

/* ── Map table ── */

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

/* ── Login page ── */

/* ── Forbidden page ── */

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

/* /ingest — what arrives.
 *
 * Numbered 84 so it lands after 82-data (the page it is a sibling of) 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 range is a pair of links rather than a control: the panel holds no state
 * on the client anywhere, and a range in the URL is one a person can bookmark
 * and send. Drawn as a segmented pair so it reads as a choice with exactly two
 * options, which is what it is.
 */
.ingest-ranges {
	display: flex;
	gap: 2px;
	padding: 2px;
	border: 1px solid var(--border);
	border-radius: 99px;
	background: var(--surface-2);
}

.ingest-range {
	padding: 4px 12px;
	border-radius: 99px;
	color: var(--text-2);
	text-decoration: none;
	font-size: 13px;
	line-height: 20px;
	white-space: nowrap;
}

.ingest-range:hover {
	color: var(--text);
}

.ingest-range.is-active {
	background: var(--surface);
	color: var(--text);
	font-weight: 600;
}

/* A board that could not be read is worse than an empty one, because an empty
 * one is indistinguishable from a quiet fleet. The warning is a bar rather
 * than a line of dim text for exactly that reason.
 */
.ingest-warning {
	margin: 0 0 1rem;
	padding: 0.625rem 0.875rem;
	border-radius: var(--r);
	background: var(--status-pending-bg);
	color: var(--status-pending-fg);
}

/* A verdict and the arithmetic behind it stack rather than sit side by side:
 * three channel columns on one row leave no width for a chip and a count to
 * share a line, and a wrapped count reads as belonging to the next cell.
 */
.ingest-cell {
	white-space: nowrap;
}

.ingest-cell .chip {
	display: inline-block;
}

.ingest-cell .cell-dim {
	display: block;
	margin-top: 2px;
	font-size: 12px;
	font-variant-numeric: tabular-nums;
}

/* Counts are compared down the column, not read one at a time, so they are
 * right-aligned and tabular: a column of proportional digits cannot be scanned
 * for the row that is an order of magnitude bigger than its neighbours.
 */
.ingest-num {
	text-align: right;
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

.ingest-subhead {
	margin: 1.5rem 0 0.5rem;
	font-size: 15px;
	font-weight: 600;
	color: var(--text);
}

.ingest-lists {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
	gap: 1rem;
	margin: 1.25rem 0 0.5rem;
}

.ingest-list {
	padding: 1rem;
	border: 1px solid var(--border);
	border-radius: var(--r);
	min-width: 0;
}

.ingest-overdue {
	list-style: none;
	margin: 0.5rem 0 0;
	padding: 0;
}

/* Name on the left, how long it has been quiet on the right. A long node name
 * takes the wrap; the age is the column a reader is scanning and never moves.
 */
.ingest-overdue li {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	gap: 0.75rem;
	padding: 4px 0;
	border-bottom: 1px solid var(--border);
	min-width: 0;
}

.ingest-overdue li:last-child {
	border-bottom: none;
}

.ingest-overdue li > span:first-child {
	min-width: 0;
	overflow-wrap: anywhere;
}

.ingest-overdue li > span:last-child {
	white-space: nowrap;
}
/* The header banner about the entity mirror's age: a fact the reader must
   see before trusting a matrix drawn from a stale list of nodes. */
.stat-banner {
	margin: 0 0 1rem;
	padding: .5rem .75rem;
	border: 1px solid var(--warn, #c9a227);
	border-radius: var(--radius, 6px);
	background: color-mix(in srgb, var(--warn, #c9a227) 12%, transparent);
	font-size: .9rem;
}
/* A block whose data still lives in central until a later phase. */
.card-elsewhere {
	opacity: .75;
	border-style: dashed;
}
/* ── Mobile overrides for components defined below the main responsive block ──
   Same specificity as the base rule, so they only win by coming later in the
   file. Keep them here rather than in the block at the top: from up there,
   `align-items` on .setting-row and .inline-edit-form silently lost. */
@media (max-width: 768px) {
  .setting-row {
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
  }
  .setting-row__action { width: 100%; }
  .setting-row__action .btn-outline-sm,
  .setting-row__action .btn { width: 100%; justify-content: center; }

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

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

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

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

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

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

/* ─── The check matrix (/monitoring, "through each node") ───
   A table of latencies that used to be drawn as a table of verdicts. Every cell
   that answered wore the same green whether it took 2 ms or 450 ms, so the one
   quantity the measurement exists to produce was the one thing the matrix did
   not show. Three ideas replace the chip:

     the number is ink        — read exactly, aligned on its digits
     the rail is the time     — read at a glance, comparable across columns
     red is only ever broken  — which is what DESIGN.md already said

   The rail is 2px and lives under the number, so it costs the cell no width.
   That matters more than it sounds: ten resource columns plus a node name is
   already wider than a phone, and a chip pays for its meaning in horizontal
   space the table does not have. */
/* Qualified by .data-table, or they lose: `.data-table thead th` and
   `.data-table td` are two-class selectors and a bare `.check-th` is one, so
   the base rule's left-aligned header and 1rem padding win silently. */
.data-table thead th.check-th { text-align: right; }
.data-table td.check-td { padding: .6rem .75rem; vertical-align: middle; }

/* One measurement over one transport, which is now the whole cell: the rung it
   rode is named once by the pinned column instead of once per cell. */
.check-rung { display: flex; flex-direction: column; gap: 3px; min-width: 56px; }
.check-rung[data-hint] { cursor: help; }
/* The cell is a focus target now (roving tabindex — see matrix.js), so it needs
   a ring. Offset outwards rather than inset: the rung is 56px wide and an inner
   ring would sit on top of the number it is pointing at. :focus-visible only,
   so a tap does not leave a box behind. */
.check-rung:focus { outline: none; }
.check-rung:focus-visible {
  outline: 2px solid var(--accent-solid);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}

.check-line { display: flex; align-items: baseline; }
.check-num {
  margin-left: auto;
  font-size: 14px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* A failure spends the number's slot on a word. Smaller, because "response" is
   longer than "450" and the column must not widen when a node falls over. */
.check-num--bad {
  color: var(--danger-fg);
  font-size: 11.5px;
  letter-spacing: .01em;
}
.check-code {
  font-family: var(--font-mono);
  font-size: 10.5px;
  opacity: .8;
  margin-left: 3px;
}
/* Absence of a measurement is not a state to signal — it is a state to stay
   out of the way. Lighter and unbolded, never coloured. */
.check-num--quiet { color: var(--text-3); font-weight: 400; font-size: 13px; }

/* The rail. Filled length is the latency on a log scale fixed at 500 ms for the
   whole matrix (BarPct in checks_handlers.go), so two columns are comparable
   with each other and two screenshots are comparable across days. */
/* Fixed width, not the cell's — and that is the whole point of the rail rather
   than a detail of it. Columns are as wide as their headers, so "Instagram" is
   half again "GitHub"; a rail measured in percent of its cell would draw the
   same 96 ms at two different lengths and quietly destroy the one property the
   redesign exists for. 56px everywhere, hung off the right edge so it sits
   under the digits it belongs to. */
.check-bar,
.check-track {
  width: 56px;
  margin-left: auto;
}
.check-bar {
  height: 2px;
  /* rounded.chart — the radius every bar in the system already wears (rx="2"
     on the day bars). On a 2px rail it reads as a rounded cap. */
  border-radius: 2px;
  /* The unfilled part has to be visible or the fill reads as a segment floating
     at the left of the cell rather than as a portion of a scale. It is the
     track that makes 3 ms legible as "almost nothing" instead of "a short blue
     mark near some digits". */
  background: color-mix(in srgb, var(--text-3) 30%, transparent);
}
.check-bar > i {
  display: block;
  height: 100%;
  border-radius: 2px;
  background: var(--accent);
}
/* Nobody has measured this pair. The rail keeps its 2px so the row holds one
   baseline whatever its neighbours are, and draws nothing: a fleet nobody has
   swept yet should read as a page waiting for data, not as a grid of rules. */
.check-bar--none { background: transparent; }
/* Skipped: the path was there and nothing went down it. Dashed rather than
   blank, which is what "nobody has measured this yet" now looks like. */
.check-bar--dashed {
  background: repeating-linear-gradient(to right,
    color-mix(in srgb, var(--text-3) 34%, transparent) 0 3px,
    transparent 3px 6px);
}

/* A failure has no time to lay along the rail, but it has a distance. Four
   notches — DNS, TCP, TLS, first byte — filled up to the one that did not go
   through. The timings behind them were always measured and only ever readable
   by hovering. */
.check-track { display: flex; gap: 2px; height: 2px; }
.check-track > i {
  flex: 1;
  border-radius: 2px;
  background: color-mix(in srgb, var(--text-3) 14%, transparent);
}
.check-track > i.is-done  { background: color-mix(in srgb, var(--text-3) 42%, transparent); }
.check-track > i.is-broke { background: var(--danger-fg); }

/* ─── The three pinned columns ───
   Ten resources do not fit on any screen, so the matrix scrolls sideways — and
   a number that has lost its node, its path and its transport is not a
   measurement, it is a digit. Those three stay; the resources move past them.

   Fixed pixel widths, because a sticky column's `left` offset has to be the sum
   of the widths before it and there is no way to say "the width of the two
   columns to my left" in CSS. Changing one width means changing the offsets
   below it. */
.data-table thead th.check-col-node,
.data-table tbody td.check-col-node,
.data-table thead th.check-col-path,
.data-table tbody td.check-col-path,
.data-table thead th.check-col-rung,
.data-table tbody td.check-col-rung {
  position: sticky;
  z-index: 2;
}
.data-table thead th.check-col-node,
.data-table thead th.check-col-path,
.data-table thead th.check-col-rung { z-index: 3; }

/* 232px is what `zeroroute-node-frankfurt` needs beside its flag and inside the
   cell's own padding. At 200 it truncated one character short, which reads as a
   bug rather than as an abbreviation. */
.data-table thead th.check-col-node,
.data-table tbody td.check-col-node { left: 0;     width: 232px; min-width: 232px; }
.data-table thead th.check-col-path,
.data-table tbody td.check-col-path { left: 232px; width: 108px; min-width: 108px; }
.data-table thead th.check-col-rung,
.data-table tbody td.check-col-rung { left: 340px; width: 44px;  min-width: 44px; }

/* A sticky cell is transparent by default, so the scrolling numbers would run
   underneath it in plain sight. Each takes the bed its row is on. */
.data-table tbody td.check-col-node,
.data-table tbody td.check-col-path,
.data-table tbody td.check-col-rung { background: var(--surface); }
.data-table thead th.check-col-node,
.data-table thead th.check-col-path,
.data-table thead th.check-col-rung { background: var(--surface-2); }
.data-table tbody tr.row-baseline td.check-col-node,
.data-table tbody tr.row-baseline td.check-col-path,
.data-table tbody tr.row-baseline td.check-col-rung {
  background: color-mix(in srgb, var(--text-3) 6%, var(--surface));
}
/* The row highlight has to reach the pinned cells too, or hovering a row lights
   up everything except the part naming it. */
.data-table tbody tr:hover td.check-col-node,
.data-table tbody tr:hover td.check-col-path,
.data-table tbody tr:hover td.check-col-rung { background: var(--surface-2); }

/* One edge marks where the pinned block ends and the scroll begins. Drawn on
   the last pinned column only, so it reads as one seam rather than three. */
.data-table thead th.check-col-rung,
.data-table tbody td.check-col-rung { box-shadow: 1px 0 0 var(--border); }

/* The transport, said once per row instead of once per cell. Machine truth, so
   the mono face; quiet, because it labels the row rather than competing with
   the numbers on it. */
.data-table tbody td.check-col-rung {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: .03em;
  color: var(--text-3);
  white-space: nowrap;
}

/* Two rungs of one node are one node. The spanning name cell says so, and a
   fainter rule between them keeps the pair legible across ten columns without
   letting them read as two unrelated rows. */
.data-table tbody tr.is-tied {
  border-bottom-color: color-mix(in srgb, var(--border) 45%, transparent);
}

/* ─── The header row is pinned too ───
   The columns already stay while the resources scroll past them; the resource
   names themselves did not, so a fleet of any size scrolled its own headings
   off the top and left a grid of bare numbers — the same digit-without-its-
   meaning problem the pinned columns exist to prevent, in the other axis.

   `top: 0` alone would have done nothing, and the reason is worth writing down:
   a sticky cell pins against its nearest SCROLLING ancestor, and `overflow-x:
   auto` on .table-wrap already made that the wrapper rather than the page — on
   both axes, because a box that scrolls in one direction is a scroll container
   in the other too. A wrapper with no ceiling never scrolls vertically, so its
   header would pin to a line the document carries away. The max-height below is
   what turns the pin on; it is not decoration. */
.check-matrix-wrap {
  /* Only bites when the matrix is taller than the screen — a three-node fleet
     keeps its natural height and grows no inner scrollbar at all. dvh second:
     a phone should measure against the room its toolbars actually leave. */
  max-height: calc(100vh - 6rem);
  max-height: calc(100dvh - 6rem);
  overflow: auto;
  /* Deliberately no `overscroll-behavior`: reaching the last row should carry
     on scrolling the page, not stop the wheel dead inside the card. */
}
.check-matrix-wrap .data-table thead th {
  position: sticky;
  top: 0;
  /* Above the pinned tbody cells (2), which pass underneath it. */
  z-index: 3;
  /* border-collapse hands a cell's border to the TABLE, so a sticky header
     leaves its rule behind on the row it was collapsed with and floats over the
     numbers with no seam. A shadow is painted by the cell and travels with it. */
  border-bottom: 0;
  box-shadow: 0 1px 0 var(--border);
}
/* The three pinned columns cross the header, so those cells are stuck on both
   axes at once and have to outrank the resource headings sliding under them. */
.check-matrix-wrap .data-table thead th.check-col-node,
.check-matrix-wrap .data-table thead th.check-col-path,
.check-matrix-wrap .data-table thead th.check-col-rung { z-index: 4; }
/* The corner cell carries both seams: the one under the header, and the one
   down the edge of the pinned block that the rung column has always drawn. */
.check-matrix-wrap .data-table thead th.check-col-rung {
  box-shadow: 1px 0 0 var(--border), 0 1px 0 var(--border);
}

/* ─── The matrix's own two identity columns ───
   Both used to wrap, and a wrapped cell sets the height of its whole row:
   `zeroroute-node-frankfurt` broke into three lines and "relay / via 🇩🇪
   zeroroute-node-frankfurt" into four, so one row stood three times taller than
   the one below it for reasons that carried no information. */
.check-node {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
  white-space: nowrap;
}
.check-node__name {
  font-weight: 600;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
}
.check-path { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
/* The relay's country, with its name in the hint. Naming the entry matters —
   clients dial it — but spelling it out cost four lines to say what a flag says
   in one glyph. */
.check-path__via { display: inline-flex; align-items: center; cursor: help; }

/* The baseline is the yardstick every other number in the table is read
   against, and it was formatted as an ordinary row. A band and a caption, not a
   colour: it is not a state. */
.row-baseline { background: color-mix(in srgb, var(--text-3) 6%, transparent); }
.row-baseline .check-node__name { color: var(--text-2); font-weight: 600; }
.row-baseline-tag {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--text-3);
}

@media (max-width: 768px) {
  /* The rail keeps its meaning at any width, so the cell can give up padding
     before it gives up anything else. */
  .data-table td.check-td { padding: .5rem .6rem; }
  .check-rung { min-width: 46px; }
  .check-bar,
  .check-track { width: 44px; }
  .check-num { font-size: 13px; }
  .check-num--bad { font-size: 11px; }
  /* The pinned block is 384px on a desktop and a phone is 390px wide, so all
     three columns give ground here. These offsets are the running sum of these
     widths; change one and change the two below it.

     The path gives up the most: on a phone it keeps only its marker, because
     "direct" is what most nodes are and the exception is what has to survive
     the squeeze. The transport keeps its full 36px — it is half of what names
     the row now. */
  .data-table thead th.check-col-node,
  .data-table tbody td.check-col-node { left: 0;     width: 148px; min-width: 148px; max-width: 148px; }
  .data-table thead th.check-col-path,
  .data-table tbody td.check-col-path { left: 148px; width: 34px;  min-width: 34px; }
  .data-table thead th.check-col-rung,
  .data-table tbody td.check-col-rung { left: 182px; width: 36px;  min-width: 36px; }
  .check-path__word { display: none; }
  /* Stripped of its word, "direct" would be an empty pill saying nothing. The
     relay chip keeps its bed, because it still carries the relay's flag. */
  .check-path .chip--direct { display: none; }
  .data-table thead th.check-col-path { font-size: 0; }

  /* Every node here is `zeroroute-node-something`, so the fourteen characters
     that fit are the fourteen every node shares — an ellipsis cuts off exactly
     the part that identifies the row. Two lines cost height, which a phone has,
     instead of identity, which it does not. */
  /* block, not a wrapping flex row: as flex items the flag and the name are two
     things that wrap independently, and the flag ends up alone on the first
     line. Inline, the name simply flows around it. */
  .check-node { display: block; white-space: normal; }
  .check-node .country__flag { margin-right: 6px; }
  .check-node__name {
    max-width: 100%;
    overflow: visible;
    overflow-wrap: anywhere;
  }
  /* The baseline cell carries a name, a caption and a hint dot in a column
     sized for a node name, so on a phone the name is what gets squeezed —
     "Эталон" truncated to "Эта…". It is one row: let it take two lines. */
  .row-baseline .check-node { flex-wrap: wrap; white-space: normal; }
  .row-baseline .check-node__name { max-width: none; }
  /* The pinned identity column carries the row's name through the sideways
     scroll, and the baseline band has to travel with it or the pinned cell
     reads as belonging to the row below. */
  .row-baseline td:first-child {
    background: color-mix(in srgb, var(--text-3) 6%, var(--surface));
  }
}
