/* swg-panel — "Relay": a covert network-operations instrument.
   Self-hosted fonts (offline-safe, no third-party fetch). Cool blue-slate field,
   one warm-amber signal, a cool jewel-tone status spectrum. */
@font-face{font-family:"JetBrains Mono";src:url(vendor/fonts/jetbrains-mono-400.woff2) format("woff2");font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:"JetBrains Mono";src:url(vendor/fonts/jetbrains-mono-500.woff2) format("woff2");font-weight:500;font-style:normal;font-display:swap}
/* Cyrillic rides in its own @font-face with a unicode-range, rather than being folded into the Latin
   subsets. Two reasons: an English reader never downloads it, and the Latin files stay byte-identical, so
   the look nobody asked me to change cannot drift. The vendored Latin subsets carry NO Cyrillic at all —
   every Russian glyph was falling through to the OS font, which is why Russian read a size larger. */
@font-face{font-family:"JetBrains Mono";src:url(vendor/fonts/jetbrains-mono-400-cyrillic.woff2) format("woff2");font-weight:400;font-style:normal;font-display:swap;unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}
@font-face{font-family:"JetBrains Mono";src:url(vendor/fonts/jetbrains-mono-500-cyrillic.woff2) format("woff2");font-weight:500;font-style:normal;font-display:swap;unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}
/* Hanken Grotesk has no Cyrillic AT ALL — 0 of the 64 Russian letters — so Onest carries that script under
   the same family name, restricted by unicode-range. It was drawn for Latin and Cyrillic together, which
   matters here because this panel mixes them constantly ("Нода fra-edge-01").

   size-adjust is the point of the pairing, not a flourish. Onest's x-height is 527 per 1000em against
   Hanken's 493; left alone, every Russian word sits a size larger than the Latin beside it, which is
   exactly the complaint that started this. 493/527 = 93.5% brings the two x-heights level, so a mixed line
   reads as ONE typeface. Weights are declared separately because the panel uses all four. */
@font-face{font-family:"Hanken Grotesk";src:url(vendor/fonts/onest-400-cyrillic.woff2) format("woff2");font-weight:400;font-style:normal;font-display:swap;size-adjust:93.5%;unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}
@font-face{font-family:"Hanken Grotesk";src:url(vendor/fonts/onest-500-cyrillic.woff2) format("woff2");font-weight:500;font-style:normal;font-display:swap;size-adjust:93.5%;unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}
@font-face{font-family:"Hanken Grotesk";src:url(vendor/fonts/onest-600-cyrillic.woff2) format("woff2");font-weight:600;font-style:normal;font-display:swap;size-adjust:93.5%;unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}
@font-face{font-family:"Hanken Grotesk";src:url(vendor/fonts/onest-700-cyrillic.woff2) format("woff2");font-weight:700;font-style:normal;font-display:swap;size-adjust:93.5%;unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}
@font-face{font-family:"Hanken Grotesk";src:url(vendor/fonts/hanken-400.woff2) format("woff2");font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:"Hanken Grotesk";src:url(vendor/fonts/hanken-500.woff2) format("woff2");font-weight:500;font-style:normal;font-display:swap}
@font-face{font-family:"Hanken Grotesk";src:url(vendor/fonts/hanken-600.woff2) format("woff2");font-weight:600;font-style:normal;font-display:swap}
@font-face{font-family:"Hanken Grotesk";src:url(vendor/fonts/hanken-700.woff2) format("woff2");font-weight:700;font-style:normal;font-display:swap}
:root{
  --flowdot:rgba(240,248,255,.82);   /* travelling signal-flow dots (dark) */
  --bg:#0A0E15; --bg2:#070A10; --surface:#10151E; --surface-2:#0C1019; --raise:#161D29; --row-hover:#1E2836;
  --line:#1D2734; --line-bright:#2A3A4E55; --line-solid:#2A3A4E;
  --ink:#E7EEF6; --ink-2:#C2D0E0; --dim:#8EA0B7; --faint:#536679; --hint:#D2DBE6;
  --glow-1:#15212F; --glow-2:#121A27;   /* the two body backdrop radial glows */
  --track:#0A0F18;                       /* inset meter / usage-bar troughs */
  --appbar-bg:rgba(10,14,20,.72);        /* frosted top bar */
  --qr-scrim:rgba(0,0,0,.82);            /* full-screen QR zoom backdrop */
  --online:#3FD89A; --ready:#4FA8F0; --pending:#9A8BF0; --partial:#E2C84A; --fault:#F2994A;
  --partial-r:#F19AA4;   /* the "partial" badge's soft-red (a problem that isn't a stopper) */
  --dangling:#F26B82; --orphan:#B79BD6; --unknown:#62748B; --removing:#E2C84A;
  --brand:#1FC8D6; --brand-2:#5FE2EC; --brand-ink:#04232A; --smart:#F472B6;   /* smart routing — rose/pink, distinct from cascade purple */
  --relay:#7C5CFF; --relay-2:#B9A6FF;   /* datapath RELAY — electric violet: the deliberate, more expensive path,
                                           deliberately NOT the theme cyan so the two modes never read as one control in two states */
  --tp-rx:#1FC8D6; --tp-tx:#818CF8;   /* throughput chart: rx (down) cyan, tx (up) indigo — kept distinct */
  --rate-up:#4FA8F0;   /* throughput UP value/arrow — a fixed blue, decoupled from the ready-status colour so it stays this light in both modes (upload reads slightly lighter than the green download) */
  --sel-ink:var(--brand);   /* text colour for a SELECTED brand-tinted control (Online pill, etc.); darkened on light */
  --dd-hover:color-mix(in srgb,var(--brand) 14%,transparent); --dd-sel:color-mix(in srgb,var(--brand) 22%,transparent);   /* ONE hover/selected tint for every dropdown row app-wide */
  --tport-bare:#7DC4F0; --tport-dock:#A974F2;   /* deploy-method pills (bare-metal blue / docker purple) */
  --tport-nix:#7EB6D9;   /* the PLATFORM pill, which sits BESIDE a deploy pill rather than replacing one — a
                            NixOS host running our image is a container AND declaratively managed. Cooler and
                            lower-chroma than either deploy colour on purpose: it qualifies that pill, it does
                            not compete with it. */
  --tport-os:#6FB3A8;    /* …and the same pill on a host the panel CAN still touch. Teal, because it is the one
                            hue this palette had left: blue is the bare-metal pill, violet the docker one, and
                            green/amber/rose all carry status meaning. Held at the platform pill's low chroma so
                            it still qualifies the deploy pill rather than competing — the HUE says declarative
                            or not, the restraint says "this is a label". */
  --sans:"Hanken Grotesk",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,system-ui,sans-serif;
  --mono:"JetBrains Mono",ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
  --r:11px; --r-sm:8px; --shadow:0 24px 60px -14px rgba(0,0,0,.62);
  --ease:cubic-bezier(.2,.7,.2,1);
}
/* LIGHT theme — the whole palette is variable-driven, so this only re-defines the structural surfaces / ink /
   lines / troughs / glows / shadow. The status + brand accents (green/cyan/orange/etc.) carry across both modes;
   the per-mode brand accent is injected by applyThemeColors(). Selected via <html data-theme="light">. */
:root[data-theme="light"]{
  --flowdot:rgba(255,255,255,.9);   /* travelling signal-flow dots (light) — bright pips on the coloured lines */
  --bg:#EDF1F7; --bg2:#E1E7F0; --surface:#FFFFFF; --surface-2:#F4F7FB; --raise:#FFFFFF; --row-hover:#E7EEF7;
  --line:#DCE3EC; --line-bright:#C3D0DE99; --line-solid:#C3D0DE;
  --ink:#101822; --ink-2:#33424F; --dim:#586878; --faint:#8695A4; --hint:#101822;
  --glow-1:#DEE9F6; --glow-2:#E7EEF7;
  --track:#E2E8F0;
  --appbar-bg:rgba(255,255,255,.78);
  --qr-scrim:rgba(226,232,240,.92);   /* light grey (the --track family) so the white QR sits on grey, not black */
  --shadow:0 20px 50px -18px rgba(30,52,84,.22);
  /* Status accents re-cut for a WHITE background: the dark-mode neons are too light to read here, so each is
     deepened to hold ~4.5:1 as text while its 12–16% tint stays a soft, legible chip. Same hues, more body. */
  --online:#0E9E63; --ready:#2B7CD3; --pending:#6A50D6; --partial:#B07A16; --fault:#D9772A;
  --partial-r:#D65468; --dangling:#D63A55; --orphan:#8A5CC4; --unknown:#5E7085; --removing:#B07A16;
  --smart:#D6247E;
  --tport-bare:#2472C4; --tport-dock:#8B45D0;   /* deploy pills deepened for white (point 6) */
  --tport-nix:#2E6C93; --tport-os:#2C7A6E;   /* …and both platform pills with them */
  --sel-ink:color-mix(in srgb, var(--brand) 58%, #000);   /* a SELECTED control's text is a darker shade of the theme on white */
}
/* LIGHT-mode component tweaks that a variable swap can't express. */
/* Hover-bubble (peers/users online list, "connected via", status "why") headers read a touch darker on white. */
:root[data-theme="light"]{--relay:#6D3FF0; --relay-2:#4B22C4}   /* light: darken so violet-on-white stays readable */
:root[data-theme="light"] .turnbub{color:var(--ink-2)}
/* the online-peers bubble header sits on a tinted band (a bit darker than the theme) with a darker-theme label. */
:root[data-theme="light"] .onlpop .onpop-h{color:var(--sel-ink);background:color-mix(in srgb,var(--brand) 20%,transparent);border-radius:7px}
:root[data-theme="light"] .onlpop a.onpop-h:hover{color:var(--ink)}   /* dark on hover — always visible even if the theme colour is light (cyan-on-cyan-band washes out) */
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;scrollbar-gutter:stable}   /* reserve the scrollbar space so the layout doesn't jump when one appears (e.g. a card re-render during a turn-proxy restart) */
/* Thin, dark, arrow-less scrollbars everywhere (matches the inner containers like .targetpick) so the panel
   looks the same in any browser — not the chunky classic bar some Chromium builds fall back to. */
*{scrollbar-width:thin;scrollbar-color:var(--line-solid) transparent}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-thumb{background:var(--line-solid);border-radius:6px;border:2px solid transparent;background-clip:padding-box}
::-webkit-scrollbar-thumb:hover{background:var(--dim);background-clip:padding-box}
::-webkit-scrollbar-track,::-webkit-scrollbar-corner{background:transparent}
::-webkit-scrollbar-button{display:none;width:0;height:0}
body{
  margin:0;color:var(--ink);font-family:var(--sans);font-size:15px;line-height:1.5;min-height:100vh;
  -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
  background:
    radial-gradient(1200px 560px at 88% -10%, var(--glow-1) 0%, transparent 60%),
    radial-gradient(820px 480px at -6% -6%, var(--glow-2) 0%, transparent 55%),
    var(--bg);
  background-attachment:fixed;
}
.mono{font-family:var(--mono)}
.dim{color:var(--dim)} .faint{color:var(--faint)}
a{color:inherit;text-decoration:none}
::selection{background:color-mix(in srgb,var(--brand) 32%,transparent);color:var(--ink)}   /* --ink adapts per theme so selected text stays legible in light AND dark */
:focus-visible{outline:2px solid var(--brand);outline-offset:2px;border-radius:5px}
/* buttons / toggles / tabs carry their own hover + active/selected feedback, so the full-strength global ring read
   as a stray heavy border on them — soften it here, but keep it VISIBLE for keyboard navigation (:focus-visible ⇒
   plain mouse clicks stay ring-free). Text inputs keep their own focus styling. */
button:focus-visible,[role=button]:focus-visible,.tabs a:focus-visible,summary:focus-visible,label:focus-visible{outline:2px solid color-mix(in srgb,var(--brand) 60%,transparent);outline-offset:2px}
@media(prefers-reduced-motion:reduce){*{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}
button{font-family:inherit}
h1,h2,h3{font-family:var(--sans)}

/* ───── app bar (command strip) ───── */
.appbar{position:sticky;top:0;z-index:30;display:flex;align-items:center;gap:22px;height:56px;padding:0 22px;
  background:var(--appbar-bg);backdrop-filter:blur(16px) saturate(1.3);border-bottom:1px solid var(--line)}
.brand{display:flex;align-items:center;gap:10px}
/* indicator-LED mark: an amber status light */
.brand-mark{width:18px;height:18px;border-radius:6px;background:radial-gradient(circle at 50% 38%,var(--brand-2),var(--brand));
  box-shadow:0 0 0 1px color-mix(in srgb,var(--brand) 35%,transparent),0 0 14px color-mix(in srgb,var(--brand) 50%,transparent);position:relative}
.brand-mark::after{content:"";position:absolute;inset:6px;border-radius:50%;background:var(--bg)}
.brand-name{font-weight:700;letter-spacing:.01em;font-size:16px;color:var(--brand)}
.brand-name span{color:var(--ink-2);font-weight:600}
.appver{font-family:var(--mono);font-size:10.5px;color:var(--faint);white-space:nowrap;letter-spacing:-.02em;margin-right:16px}
.appver b{color:var(--dim);font-weight:600}
.tabs{display:flex;gap:2px}
.tabs a{padding:8px 11px;font-family:var(--mono);font-size:10.5px;text-transform:uppercase;letter-spacing:.1em;
  color:var(--faint);font-weight:500;transition:color .15s;position:relative}
.tabs a:hover{color:var(--ink-2)}
/* The tab row and the icon cluster share one line with no wrap and no scroll, so the bar has a width
   below which they collide. Translation moves that threshold: Russian's tabs run ~30px wider than
   English's (ПОЛЬЗОВАТЕЛИ vs USERS), which pulled the collision from ~1153px up to ~1243px. Trimming the
   horizontal padding before that band buys ~40px in EVERY language — it fixes English's own limit too,
   rather than making one translation fit and leaving the next one to rediscover this. */
@media(max-width:1300px){.tabs a{padding:8px 7px}}
.tabs a.active{color:var(--ink)}
.tabs a.active::after{content:"";position:absolute;left:11px;right:11px;bottom:-1px;height:2px;border-radius:2px;
  background:var(--brand);box-shadow:0 0 9px color-mix(in srgb,var(--brand) 55%,transparent)}
.appbar-right{margin-left:auto;display:flex;align-items:center;gap:14px}
/* NOWRAP ON EVERY HEADER PILL. These are single-token status chips — a count and a word, a version, an
   action — and the English happens to be short enough that none of them ever wrapped. Russian is not:
   "FIX 2 ISSUES" becomes "ИСПРАВИТЬ 2 ПРОБЛЕМЫ" and broke onto FOUR lines, taking the whole header bar with
   it. A pill that wraps is not a pill, and there is nothing here worth wrapping — they are short by
   construction, so let them size to their text. */
.livepill,.onlinetag,.onl-toggle,.tport{white-space:nowrap}
.livepill{display:flex;align-items:center;gap:7px;font-size:11px;text-transform:uppercase;letter-spacing:.08em;
  color:var(--dim);font-family:var(--mono);padding:5px 11px;border:1px solid var(--line-solid);border-radius:99px;background:var(--surface-2)}
.livepill b{color:var(--online);font-weight:600;font-size:13px;letter-spacing:-.02em}
/* the node-detail update lifecycle pills (update to … / updating / up to date) — sized to match the reporting
   and docker/bare-metal badges (10px, tighter padding), a touch smaller than a normal livepill.
   Asymmetric top/bottom padding nudges the mono label ~1px down so it optically centres against the icon
   (the icon is pulled back up 1px so it stays put). */
.updpill,.upd-busy,.upd-uptodate{font-size:10px;padding:4px 9px 2px;gap:6px;letter-spacing:.06em;line-height:1}
button.upd-uptodate{cursor:pointer}   /* up-to-date pill is clickable → re-run/repair (heals the datapath with no new version) */
button.upd-uptodate:hover{border-color:var(--online);background:color-mix(in srgb,var(--online) 12%,transparent)}
.updpill b,.upd-busy b,.upd-uptodate b{font-size:11px}
.updpill svg,.upd-busy svg,.upd-uptodate svg{width:11px;height:11px;position:relative;top:-1px}
/* on the panel header (appbar) these pills sit next to the ONLINE / bare-metal pills — stretch to the same
   row height so the header reads as one uniform band; align-items:center keeps the label+icon centred, and
   the 1px padding-top bias still nudges the mono label down to sit on the icon's centre line */
.appbar-right .updpill,.appbar-right .upd-busy,.appbar-right .upd-uptodate{font-size:11px;padding:9px 12px 8px;letter-spacing:.08em}
.appbar-right .updpill b,.appbar-right .upd-busy b,.appbar-right .upd-uptodate b{font-size:12px}
.appbar-right .updpill svg,.appbar-right .upd-busy svg,.appbar-right .upd-uptodate svg{width:13px;height:13px}
.livepill .dot{width:7px;height:7px;border-radius:50%;background:var(--online);box-shadow:0 0 8px color-mix(in srgb,var(--online) 55%,transparent);animation:pulse 2.6s infinite}
@keyframes pulse{0%,100%{opacity:1;box-shadow:0 0 0 0 color-mix(in srgb,var(--online) 45%,transparent)}50%{opacity:.55}70%{box-shadow:0 0 0 6px color-mix(in srgb,var(--online) 0%,transparent)}}
.livepill.off b{color:var(--dim)}                /* no peers online → grey count … */
.livepill.off .dot{display:none}                 /* … and no glowing dot */
/* this panel host is mid re-install/convert (header slot, holds through the panel's own downtime) */
.hostproc-tag{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--partial);border:1px solid var(--partial);border-radius:99px;padding:4px 11px;background:color-mix(in srgb,var(--partial) 10%,transparent)}   /* in-progress = YELLOW (matches the .upd-busy pill, so the header tag doesn't flip yellow→purple mid-update) */
.hostproc-tag.ok{color:var(--online);border-color:var(--online);background:rgba(63,216,154,.1)}        /* host re-install succeeded — green, auto-clears */
.hostproc-tag.aborted{color:var(--unknown);border-color:var(--unknown);background:color-mix(in srgb,var(--unknown) 12%,transparent)}  /* host re-install aborted — grey, dismissable */
.hostproc-tag.fail,.hostproc-tag.procuninstall{color:var(--dangling);border-color:var(--dangling);background:color-mix(in srgb,var(--dangling) 10%,transparent)}   /* failed / uninstalling — RED (error popup + × on fail) */
.hostproc-tag.procconvert{color:var(--pending);border-color:var(--pending);background:color-mix(in srgb,var(--pending) 10%,transparent)}   /* converting — PURPLE */
.hostproc-tag .xbtn{margin-left:4px;padding:1px}
.updslot{display:inline-flex;align-items:center;gap:14px}   /* match .appbar-right gap so HC ↔ update spacing is even */
/* the update-to pill is TEXT-ONLY (no icon), so it drops the icon-optical padding nudge from .updpill,.upd-busy,…
   above and uses SYMMETRIC vertical padding — the "update … to <version>" content sits dead-centre in the pill. */
.updpill{cursor:pointer;border-color:color-mix(in srgb,var(--ready) 42%,var(--line-solid));padding:4px 10px 3px;font-size:9.5px}   /* small (node) pill: a hair smaller; near-symmetric padding (a touch more top) optically centres the caps */
.appbar-right .updpill{padding:8px 12px;font-size:11px}
.updpill b{color:var(--ready);font-size:10.5px;font-weight:650}     /* the version — blue, only a hair larger than the label so the pill reads balanced */
.appbar-right .updpill b{font-size:11.5px}
.updpill:hover{border-color:var(--ready);background:color-mix(in srgb,var(--ready) 12%,transparent)}
/* fix pill — self-heal issues found; amber (attention), distinct from the blue "update to" pill */
.fixpill{border-color:color-mix(in srgb,#e0a83a 45%,var(--line-solid));color:#b7791f}
.fixpill b{color:#b7791f}
.fixpill svg{width:12px;height:12px;position:relative;top:-1px}
.fixpill:hover{border-color:#e0a83a;background:color-mix(in srgb,#e0a83a 12%,transparent)}
/* turn-family updates. Deliberately NOT the fixpill amber: amber in this product means something is broken and
   wants attention, and a server merely being a build behind is neither. --ready is the palette's existing
   "available" blue and adapts per theme, so the badge reads as an offer rather than an alarm. */
/* A version pair reads as one movement, old → new. The arrow and the middot are separators, not data, so they
   are muted and given room. They are set in the MONO face, like the numbers they sit between: in the body face
   they carry different metrics, land off the digits' baseline and read as an underscore — which no amount of
   nudging fixes, it only moves the mismatch around. The destination is what is being offered, so it keeps the
   ink while the current version stays quiet. */
.verarrow{padding:0 5px;color:var(--faint);font-family:var(--mono)}
.vernew{color:var(--ink)}
/* Which servers this row restarts, named. The sheet's whole warning is that an update drops that server's
   clients — "2 servers" states the count and hides the one thing needed to judge whether now is a quiet
   moment. Chips inherit --tfc from the row, so a glance ties them to their fork without repeating its name. */
.tus-ids{display:flex;flex-wrap:wrap;gap:5px}
.tus-id{background:color-mix(in srgb,var(--tfc,var(--muted)) 13%,transparent);color:var(--tfc,var(--dim))}
/* One fork, several nodes: the sub-lines hang off a spine in the FORK's colour, and carry no fill of their
   own. A tinted row background was the first attempt and it was wrong — a few percent of each fork colour
   washed over the card reads as three different shades of grime rather than one system, and it fights the
   node's own dot for the same job. The spine spends the fork colour once, in a place nothing else uses. */
.tus-nodes{display:flex;flex-direction:column;margin:0 0 0 3px;
  border-left:2px solid color-mix(in srgb,var(--tfc,var(--muted)) 55%,transparent)}
.tus-node{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:6px 0 6px 11px}
.tus-node + .tus-node{border-top:1px solid var(--line)}
.tus-nn{display:inline-flex;align-items:center;gap:6px;font-weight:600;font-size:12.5px}
.tus-ndot{width:7px;height:7px;border-radius:50%;flex:none}
/* The bubble's foot IS its action, so it reads as a link rather than inheriting .hub-act's button chrome. */
.hostupd-bub .hub-foot.hub-act{appearance:none;display:block;width:100%;text-align:left;border:0;
  border-top:1px solid var(--line);border-radius:0;background:none;font:inherit;font-size:10.5px;
  color:var(--brand);cursor:pointer;padding:8px 0 0;margin-top:12px}
.hostupd-bub .hub-foot.hub-act:hover{color:var(--brand);text-decoration:underline}
.turnpill{border-color:color-mix(in srgb,var(--ready) 45%,var(--line-solid));color:var(--ready)}
.turnpill b{color:var(--ready)}
.turnpill:hover{border-color:var(--ready);background:color-mix(in srgb,var(--ready) 12%,transparent)}
:root[data-theme="dark"] .fixpill,:root[data-theme="dark"] .fixpill b{color:#e6b84f}

.btn{cursor:pointer;border-radius:var(--r-sm);font-size:13px;border:1px solid transparent;transition:all .15s var(--ease);
  display:inline-flex;align-items:center;gap:7px;white-space:nowrap}
/* GLOBAL: every icon centres its own glyph so it sits dead-centre in a button/row — kills the inline-baseline gap
   that made SVGs sit high. Flex containers (.btn etc.) then centre the .ic item vertically via align-items. */
.ic{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}
.ic svg{display:block}
.btn-primary{background:var(--brand);color:var(--brand-ink);font-weight:700;padding:9px 15px}
.btn-primary:hover{filter:brightness(1.08);transform:translateY(-1px)}
.btn-primary .plus{font-size:16px;line-height:0;margin-top:-1px}
.btn-ghost{background:var(--surface-2);color:var(--dim);border-color:var(--line-solid);padding:8px 13px}
.btn-ghost:hover{color:var(--ink);border-color:var(--brand)}
.btn-ghost.danger{color:var(--dangling)}
.btn-ghost.danger:hover{color:var(--dangling);border-color:var(--dangling)}
.btn-mini{background:transparent;color:var(--dim);border-color:var(--line-solid);padding:5px 11px;font-size:12px;border-radius:7px}
.btn-mini:hover{color:var(--brand);border-color:var(--brand)}
/* cyan accent for panel-header actions only (Add peer, Load new interface, Edit interface, Adopt all…) */
.panel-head .btn-mini{color:var(--brand);border-color:color-mix(in srgb,var(--brand) 32%,var(--line-solid))}
.panel-head .btn-mini:hover{color:var(--brand);border-color:var(--brand);background:color-mix(in srgb,var(--brand) 10%,transparent)}
.btn-mini.warn:hover{color:var(--dangling);border-color:var(--dangling)}
.btn:disabled{opacity:.45;cursor:not-allowed;transform:none;filter:none}
.iconbtn:disabled,.livepill:disabled{opacity:.35;cursor:not-allowed;pointer-events:none}   /* down/converting node → only recovery actions stay live */

/* ───── view + screens ───── */
.view{max-width:1260px;margin:0 auto;padding:18px 22px 64px}
.screen{animation:rise .32s var(--ease)}
@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.crumb{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--dim);margin-bottom:16px;flex-wrap:wrap}
.crumb a{color:var(--dim)} .crumb a:hover{color:var(--ink)}
.crumb .sep{color:var(--faint)}
.crumb b{color:var(--ink);font-weight:600}
.lbl{font-size:10.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--dim);font-weight:600}
.card{background:linear-gradient(180deg,var(--raise),var(--surface));border:1px solid var(--line);border-radius:var(--r);padding:22px}
.section-title{display:flex;align-items:baseline;gap:9px;margin:22px 2px 9px;scroll-margin-top:72px}   /* clear the 56px sticky appbar + a ~16px buffer when the rail jump-nav scrolls to a section */
.section-title h2{margin:0;font-family:var(--mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--dim);font-weight:500}
.section-title .count{font-family:var(--mono);font-size:11px;color:var(--faint)}
.section-title .nodehint{font-size:11.5px;color:var(--faint);font-weight:400;letter-spacing:normal;text-transform:none;margin-left:8px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.grow{flex:1}   /* generic flex spacer — several rows (activity, flow footer, turn load) relied on this and only had it scoped */
.section-title .grow{flex:1}
@media(max-width:860px){.section-title .nodehint{display:none}}

/* ───── dashboard controls (overview): node selector + range ───── */
.dashbar{display:flex;align-items:flex-start;gap:14px;flex-wrap:wrap;margin-bottom:14px}
.snbadge{--c:var(--brand);display:inline-flex;align-items:center;gap:6px;font:inherit;font-family:var(--mono);font-size:11.5px;line-height:1;
  padding:5px 11px;border-radius:99px;border:1px solid var(--line);background:none;color:var(--dim);cursor:pointer;
  transition:color .12s,border-color .12s,background .12s,box-shadow .12s;white-space:nowrap}
.ndot{width:7px;height:7px;border-radius:50%;background:var(--c);flex:none}   .snbadge:hover{color:var(--ink);border-color:color-mix(in srgb,var(--c) 50%,var(--line))}
.snbadge.on{color:var(--ink);font-weight:600;border-color:color-mix(in srgb,var(--c) 60%,transparent);background:color-mix(in srgb,var(--c) 15%,transparent)}
/* Peers/Mesh traffic badges — sit on the two traffic-doughnut card headers (right-aligned), shared state.
   Small + no dot; the ON state uses the saturated component colour (peers = cyan, mesh = violet). */
.dcard-traf{display:inline-flex;align-items:center;gap:5px}
.tbadge{--c:var(--brand);font:inherit;font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.04em;line-height:1;
  padding:4px 9px;border-radius:99px;border:1px solid var(--line);background:none;color:var(--faint);cursor:pointer;
  transition:color .12s,border-color .12s,background .12s;white-space:nowrap}
.tbadge.peers{--c:var(--tp-rx)} .tbadge.mesh{--c:#9B8AFF}
.tbadge:hover{color:var(--dim);border-color:color-mix(in srgb,var(--c) 45%,var(--line))}
.tbadge.on{color:var(--c);border-color:color-mix(in srgb,var(--c) 55%,transparent);background:color-mix(in srgb,var(--c) 16%,transparent)}

/* Side rail — three identical menus (jump · ranges · nodes) pinned right-centre, ALWAYS visible. Each is a column of
   icons that slides its labels out when the panel is hovered; each row highlights on hover with the ICON in the theme
   colour (label stays neutral). */
.dashrail{position:fixed;top:50%;right:0;transform:translateY(-50%);z-index:60;pointer-events:none}
.dashrail-stack{display:flex;flex-direction:column;align-items:flex-end;gap:11px;max-height:94vh;pointer-events:none}
.railpanel{display:flex;flex-direction:column;gap:2px;padding:8px 9px;pointer-events:auto;
  background:color-mix(in srgb,var(--surface) 82%,transparent);
  border:1px solid color-mix(in srgb,var(--line-solid) 70%,transparent);border-right:none;border-radius:14px 0 0 14px;
  box-shadow:0 12px 40px rgba(0,0,0,.24);overflow-x:hidden}
.railmenu-nodes{overflow-y:auto;max-height:50vh}
/* uniform menu row */
.railmenu-b{appearance:none;border:none;background:none;cursor:pointer;font:inherit;font-family:var(--mono);font-size:11px;font-weight:600;letter-spacing:.02em;
  color:var(--ink-2);border-radius:8px;white-space:nowrap;display:flex;align-items:center;justify-content:flex-end;width:100%;
  transition:background .12s}
.railmenu-ic{display:flex;align-items:center;justify-content:center;flex:none;width:28px;height:28px;color:var(--dim);transition:color .12s}
.railmenu-ic svg{width:17px;height:17px}
.railmenu-emoji{font-size:15px;line-height:1}
.railmenu-t{max-width:0;opacity:0;margin-left:0;overflow:hidden;white-space:nowrap;transition:max-width .3s var(--ease),opacity .24s,margin-left .3s var(--ease)}
.railpanel:hover .railmenu-t{max-width:150px;opacity:1;margin-left:5px}                 /* hover the panel → slide labels out */
.railmenu-b:hover{background:color-mix(in srgb,var(--brand) 12%,transparent)}            /* row highlight */
.railmenu-b:hover .railmenu-ic{color:var(--brand)}                                      /* highlighted icon → theme colour (label stays neutral) */
.railmenu-b.on .railmenu-ic{color:var(--brand)}                                         /* active range → theme-coloured icon */
/* nodes menu — coloured dot: bright when selected, dimmed when off */
.railnode-dot{width:10px;height:10px;border-radius:50%;background:var(--c);transition:opacity .12s,box-shadow .12s}
.railmenu-b.node.off .railnode-dot{opacity:.32}
.railmenu-b.node.on .railnode-dot{box-shadow:0 0 6px 1px color-mix(in srgb,var(--c) 60%,transparent)}   /* overview: original subtle glow */
/* SELECTED node — the STRONG glow + tinted bg + name in the node's colour is ONLY on the detail-screen nav rail (.noderail), not the Overview toggle rail */
.noderail .railmenu-b.node.on{background:color-mix(in srgb,var(--c) 14%,transparent);border-radius:8px}
.noderail .railmenu-b.node.on .railnode-dot{box-shadow:0 0 11px 2px color-mix(in srgb,var(--c) 90%,transparent),0 0 3px 1px color-mix(in srgb,var(--c) 70%,transparent)}
.railmenu-b.node .railmenu-t{color:var(--ink-2)}
.noderail .railmenu-b.node.on .railmenu-t{color:var(--c);font-weight:700}
.railmenu-b.node.off .railmenu-t{color:var(--faint)}
/* live dot — dimmed & static when live is NOT the selected range; bright + glowing + animated only when selected */
.rlive-dot{position:relative;width:9px;height:9px;border-radius:50%;background:var(--online);opacity:.4}
.railmenu-b.on .rlive-dot{opacity:1;box-shadow:0 0 6px 1px color-mix(in srgb,var(--online) 75%,transparent)}
.railmenu-b.on .rlive-dot::after{content:"";position:absolute;inset:-4px;border-radius:50%;border:1.5px solid var(--online);animation:rlivePulse 1.5s ease-out infinite}
@keyframes rlivePulse{0%{transform:scale(.55);opacity:.75}100%{transform:scale(1.9);opacity:0}}

/* ───── doughnuts (overview) ───── */
.donutgrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
@media(max-width:720px){.donutgrid{grid-template-columns:1fr}}
.donutcard{border:1px solid var(--line);border-radius:var(--r);background:var(--surface);padding:14px 16px 16px}
.donutcard-h{display:flex;align-items:center;gap:8px;margin-bottom:10px;min-height:24px}   /* uniform height so the badge row never nudges the doughnut down vs a title-only card */
.donutcard-h h3{font-size:12.5px;font-weight:600;color:var(--ink);margin:0;letter-spacing:.01em}
.donut-body{display:flex;align-items:center;justify-content:center;gap:22px}   /* doughnut + legend are both fixed-width → the group width is constant, so centring pins the doughnut X and the values' right edge identically across cards, with equal left/right insets */
/* no loading dim — we keep the previous range's data on screen through a fetch, so dimming just reads as a flicker */
.mring{position:relative;flex:none}
.mring svg{display:block}
.mring-seg{cursor:pointer;transition:opacity .12s}
.mring-seg.dim{opacity:.28}
.mring-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;pointer-events:none;padding:0 11%}
.mrc-def{display:flex;flex-direction:column;gap:1px}
.mrc-def .mrc-k{font-family:var(--mono);font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--faint)}
.mrc-def .mrc-tot{font-size:19px;font-weight:600;color:var(--ink);font-family:var(--mono);white-space:nowrap;line-height:1.2}
.mrc-def .mrc-tot.up{color:var(--rate-up)}.mrc-def .mrc-tot.dn{color:var(--online)}
.mrc-hov .mrc-name{font-size:12px;font-weight:600;line-height:1.15;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
.mrc-hov .mrc-val{font-size:15px;font-weight:600;color:var(--ink);font-family:var(--mono);white-space:nowrap}
.mrc-hov .mrc-sub{font-size:10.5px;color:var(--dim);margin-top:1px}
.mring-leg{display:flex;flex-direction:column;gap:5px;flex:0 0 240px;min-width:0;font-size:12px}   /* fixed width → swatches pin left AND values right-align to the same edge across every card */
.mrl-row{display:flex;align-items:center;gap:7px;min-width:0;padding:2px 5px;margin:0 -5px;border-radius:5px;transition:opacity .12s,background .12s}
.mrl-row.dim{opacity:.3}
.mrl-row.on{background:var(--row-hover)}
.mrl-nm{cursor:default}
.mrl-dv,.mrl-uv{font-family:var(--mono);font-size:11px;flex:none;cursor:default;transition:opacity .12s}
.mrl-dv{color:var(--online)}.mrl-uv{color:var(--rate-up)}
.mrl-dv.vdim,.mrl-uv.vdim{opacity:.28}
.mrl-sw{width:9px;height:9px;border-radius:50%;flex:none;background:var(--sw);
  box-shadow:0 0 6px color-mix(in srgb,var(--sw) 60%,transparent),0 0 2px color-mix(in srgb,var(--sw) 90%,transparent)}
.mrl-nm{color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:150px}   /* cap so a very long server name ellipsizes instead of spreading the legend wide */
.mrl-v{font-family:var(--mono);font-size:11px;color:var(--ink-2);flex:none}
.donut-note{font-family:var(--mono);font-size:10px;color:var(--faint);margin-top:10px;letter-spacing:.02em}

/* ───── live trend cards (overview) ───── */
.trends{display:grid;grid-template-columns:2fr 1fr;gap:10px;margin-top:10px}
@media(max-width:760px){.trends{grid-template-columns:1fr}}
.trendcard{border:1px solid var(--line);border-radius:var(--r);background:var(--surface);padding:12px 14px 10px}
.trend-now{font-family:var(--mono);font-size:18px;font-weight:600;color:var(--online)}
/* online-peers block chart: one seated bar per bucket (fixed low count per range) */
.oblk-wrap{position:relative;display:flex;align-items:flex-end;gap:3px;width:100%}
.oblk{flex:1 1 0;min-width:0;height:100%;display:flex;align-items:flex-end;justify-content:stretch;background:var(--track);border-radius:2px;overflow:hidden;transition:background .12s}
.oblk.hot{background:var(--row-hover)}
.oblk i{display:block;width:100%;border-radius:2px 2px 0 0;transition:height .25s ease}
.harea-empty{display:flex;align-items:center;justify-content:center;height:70px;color:var(--faint);font-size:12px;font-family:var(--mono)}

/* ───── traffic-flow node map (overview) ───── */
.flowcard{border:1px solid var(--line);border-radius:var(--r);background:var(--surface);padding:8px 10px 6px;position:relative}
/* signal-flow map (redesign P1) — SVG flows + HTML badges (like the wg/awg tags) + hover bubbles */
.flowmap2{position:relative;width:100%;max-width:960px;margin:0 auto;container-type:inline-size}
.flowmap2 svg{display:block;width:100%;height:auto}
/* animated "signal flow" — round dashes travelling along each line in its flow direction (egress out / ingress in); speed ∝ traffic (set inline) */
.fm2-flowdot{stroke-dasharray:0.01 15;stroke-linejoin:round;animation:fm2flow linear infinite;pointer-events:none}   /* dense round pips (the original look) */
@keyframes fm2flow{from{stroke-dashoffset:0}to{stroke-dashoffset:-15}}
.fm2-flowdot.dim{opacity:.08}
.fm2-pulse{stroke:var(--flowdot);opacity:.5}           /* soft travelling segment (SMIL dashoffset, no blur → cheap) */
.fm2-pulse.dim{opacity:.06}
.fm2-chev{opacity:.9}                                   /* travelling arrows (SMIL animateMotion — cheapest of the three) */
.fm2-chev.dim{opacity:.08}
@media (prefers-reduced-motion:reduce){.fm2-flowdot,.fm2-pulse,.fm2-chev{animation:none;visibility:hidden}}
/* flow-animation picker (top-right of the map) — faint until you hover the map */
.fm2-anim{position:absolute;top:9px;right:9px;display:flex;gap:2px;padding:2px;border-radius:8px;background:color-mix(in srgb,var(--surface) 82%,transparent);border:1px solid var(--line);z-index:4;opacity:.4;transition:opacity .15s}
.flowcard:hover .fm2-anim{opacity:1}
.fm2-anim-b{display:flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:none;background:none;border-radius:6px;color:var(--faint);cursor:pointer;transition:color .12s,background .12s}
.fm2-anim-b:hover{background:color-mix(in srgb,var(--brand) 12%,transparent);color:var(--ink)}
.fm2-anim-b.on{background:color-mix(in srgb,var(--brand) 20%,transparent);color:var(--brand)}
.fm2-anim-b svg{width:15px;height:15px;display:block}
.fm2-flow{opacity:.72;cursor:pointer;transition:opacity .12s}   /* dark: original balance (looked best on dark) */
.fm2-flow.dim{opacity:.12}
.fm2-flow.lit{opacity:.98}
:root[data-theme="light"] .fm2-flow{opacity:.86}                /* light: pull resting/dimmed lines closer to the lit colour */
:root[data-theme="light"] .fm2-flow.dim{opacity:.26}
:root[data-theme="light"] .fm2-flow.lit{opacity:1}
/* node badge — pill in the node colour, like an interface tag; sized by traffic via inline font-size */
.fm2-nb{position:absolute;transform:translate(-50%,-50%);cursor:pointer;appearance:none;
  font-family:var(--mono);font-weight:600;line-height:1;letter-spacing:.01em;padding:.5em .78em;border-radius:8px;
  border:1.5px solid color-mix(in srgb,var(--c) 70%,transparent);background:color-mix(in srgb,var(--c) 22%,var(--surface));
  color:var(--c);white-space:nowrap;box-shadow:0 0 0 4px color-mix(in srgb,var(--c) 9%,transparent),0 2px 8px rgba(0,0,0,.3);
  transition:opacity .12s,transform .1s;z-index:2}
.fm2-nb:hover{transform:translate(-50%,-50%) scale(1.05)}
.fm2-sb{position:absolute;transform:translate(-50%,-50%);cursor:pointer;appearance:none;border-radius:50%;
  display:flex;align-items:center;justify-content:center;padding:0;
  border:1.5px solid color-mix(in srgb,var(--c) 68%,transparent);background:color-mix(in srgb,var(--c) 20%,var(--surface));
  color:var(--c);box-shadow:0 0 0 3px color-mix(in srgb,var(--c) 8%,transparent),0 2px 8px rgba(0,0,0,.3);transition:opacity .12s,transform .1s;z-index:2}
.fm2-sb svg{width:var(--isz,62%);height:var(--isz,62%);display:block}   /* icon size set inline; grows proportionally with the circle */
.fm2-sb::before{content:"";position:absolute;inset:-6px;border-radius:50%}   /* hit area a touch larger than the circle; icon stays centred (circle is centred in it) */
.fm2-sb:hover{transform:translate(-50%,-50%) scale(1.12)}
/* dim = darken but stay OPAQUE, so a de-emphasised badge still covers (hides) the flow ends cut off inside it */
.fm2-nb.dim,.fm2-sb.dim{filter:brightness(.48) saturate(.55)}
/* related to the hovered element: badge recedes (subdued opaque bg) but its NAME / ICON stays full colour so you can read the link */
.fm2-nb.reldim{background:color-mix(in srgb,var(--c) 8%,var(--surface));border-color:color-mix(in srgb,var(--c) 30%,transparent);box-shadow:0 2px 8px rgba(0,0,0,.3)}
.fm2-sb.reldim{background:color-mix(in srgb,var(--c) 8%,var(--surface));border-color:color-mix(in srgb,var(--c) 30%,transparent);box-shadow:0 2px 8px rgba(0,0,0,.3)}
.fm2-bub{position:absolute;transform:translate(-50%,-118%);pointer-events:none;z-index:5;background:var(--surface-2);
  border:1px solid var(--line-solid);border-radius:9px;padding:8px 11px;min-width:130px;box-shadow:0 6px 22px rgba(0,0,0,.42)}
.fm2-bub-h{margin-bottom:7px;display:flex;flex-direction:column;gap:2px;font:600 12.5px/1.1 var(--sans)}
.fm2-bub-k{font:9px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--faint)}
.fm2-bub-r{display:flex;justify-content:space-between;gap:18px;font:11.5px/1.7 var(--mono)}
.fm2-bub-r b{color:var(--ink)}

/* ───── hero stats (overview) ───── */
.statgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px}
@media(max-width:760px){.statgrid{grid-template-columns:repeat(2,1fr)}}
.stat{background:var(--surface);border:1px solid var(--line);border-radius:var(--r);
  padding:13px 14px;position:relative;overflow:hidden;display:flex;gap:11px;align-items:flex-start}
a.stat{text-decoration:none;color:inherit;transition:border-color .15s var(--ease),background .15s var(--ease)}
a.stat.clk:hover{border-color:var(--line-solid);background:var(--raise)}
.stat-ic{width:30px;height:30px;border-radius:8px;flex:none;display:inline-flex;align-items:center;justify-content:center;
  background:color-mix(in srgb,var(--brand) 14%,transparent);color:var(--brand)}
.stat-ic svg{width:16px;height:16px}
.stat-c{min-width:0}
.stat .k{font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);font-weight:500}
.stat.accent{border-color:color-mix(in srgb,var(--brand) 28%,transparent)}
.stat.accent .stat-ic{background:color-mix(in srgb,var(--online) 16%,transparent);color:var(--online)}
.stat .v{font-family:var(--mono);font-size:23px;font-weight:600;color:var(--ink);margin-top:6px;letter-spacing:-.02em;line-height:1.1}
.stat .v small{font-size:13px;color:var(--dim);font-weight:500}
.stat .sub{font-size:11.5px;color:var(--faint);margin-top:3px;font-family:var(--mono)}
.stat.accent .v{color:var(--online)}
.stat .spark{position:absolute;right:14px;top:14px;opacity:.9}

/* ───── Overview: Protection (blocking activity) ───── */
.protcard{background:linear-gradient(180deg,var(--raise),var(--surface));border:1px solid var(--line);border-radius:var(--r);overflow:hidden}
.prot-metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1px;background:var(--line)}
.prot-tile{background:var(--surface);padding:15px 16px 13px;min-width:0;position:relative;display:flex;flex-direction:column}   /* flex-col so a Popover <span> tile stacks its rows exactly like a plain <div> tile */
/* ── "who" bubble — which user / which peers triggered this metric (rendered in a Popover portal) ── */
.prot-k{border-bottom:1px dotted var(--line-solid);padding-bottom:2px}   /* divider under every tile label (consistent across the 3) */
.prot-tile.haswho{transition:background .12s;cursor:help}   /* whole tile is hoverable (has a "who" bubble) → help cursor across it */
.prot-tile.haswho .prot-k{border-bottom-color:var(--dim)}
.prot-tile.haswho:hover,.prot-tile.haswho.on{background:var(--raise);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--brand) 22%,transparent)}   /* highlight while its bubble is open */
/* who bubble: lighter than the card (--raise vs --surface) + brand-tinted border + deeper shadow so it stands out */
.deppop.prot-who-pop{overflow:visible;max-height:none;min-width:250px;max-width:430px;padding:11px 14px;
  background:var(--raise);border-color:color-mix(in srgb,var(--brand) 34%,var(--line-solid));box-shadow:0 16px 40px rgba(0,0,0,.6)}
.prot-who{display:flex;flex-direction:column}
.prot-who-h{font:10px/1 var(--mono);font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-2);margin-bottom:9px;display:flex;align-items:center}
.prot-who-sub{margin-left:8px;font-weight:600;letter-spacing:.06em;color:var(--faint);text-transform:none}
.prot-who-tot{margin-left:auto;color:var(--brand);font-weight:700}
.prot-who-list{display:flex;flex-direction:column;gap:4px}
.prot-who-list.scroll{max-height:330px;overflow-y:auto;margin:0 -6px;padding:0 6px}   /* ~15 rows, then scroll */
.prot-who-r{display:flex;align-items:baseline;gap:14px;min-width:0}   /* one row per peer: "user · peer" ······ when */
.prot-who-id{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.prot-who-nm{color:var(--ink-2);font-weight:600;font-size:12.5px}
.prot-who-un{color:var(--faint);font-weight:500;font-style:italic;font-size:12.5px}
.prot-who-sep{color:var(--faint);margin:0 6px}
.prot-who-pn{color:var(--dim);font-family:var(--mono);font-size:11px}
.prot-who-ago{margin-left:auto;font-family:var(--mono);font-size:10.5px;font-weight:600;flex:none;white-space:nowrap}
.prot-cat-row{padding:2px 0}
.prot-cat-nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ink-2);font-family:var(--sans);font-weight:600;font-size:12.5px;text-transform:capitalize}
.prot-cat-n{font-size:11px;font-weight:700}
.prot-who-pg{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:9px;padding-top:8px;border-top:1px solid var(--line)}
.prot-who-pglbl{font-family:var(--mono);font-size:10px;color:var(--faint);white-space:nowrap}
.prot-who-pgbtns{display:flex;gap:6px;flex:none}
.prot-who-pgb{appearance:none;font:inherit;font-family:var(--mono);font-size:11px;font-weight:600;padding:3px 11px;border-radius:7px;
  border:1px solid var(--line-solid);background:var(--surface);color:var(--dim);cursor:pointer;transition:color .12s,border-color .12s}
.prot-who-pgb:hover:not(:disabled){color:var(--brand);border-color:var(--brand)}
.prot-who-pgb:disabled{opacity:.38;cursor:default}
.prot-top{display:flex;align-items:center;gap:14px}   /* number on the left, label+sub stacked on the right — same row */
.prot-tc{min-width:0;flex:1}
.prot-v{font-family:var(--mono);font-size:30px;font-weight:600;letter-spacing:-.02em;line-height:1;color:var(--ink);flex:none}
.prot-k{font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--dim);font-weight:500}
.prot-sub{font-family:var(--mono);font-size:11px;color:var(--faint);margin-top:3px}
.prot-spark{margin-top:9px;height:24px}
.prot-spark .spark{display:block;width:100%;height:24px}
.prot-cov{display:flex;align-items:center;gap:11px;flex-wrap:wrap;padding:12px 16px;border-top:1px solid var(--line);background:var(--surface-2)}
.prot-cov-ic{width:26px;height:26px;flex:none;border-radius:8px;display:inline-flex;align-items:center;justify-content:center;
  background:color-mix(in srgb,var(--brand) 13%,transparent);color:var(--brand)}
.prot-cov-ic svg{width:15px;height:15px}
.prot-cov-txt{font-size:12.5px;color:var(--dim);line-height:1.5;min-width:0}
.prot-cov-txt b{color:var(--ink-2);font-family:var(--mono);font-weight:600;letter-spacing:-.01em}
.prot-mech{display:flex;gap:6px;flex-wrap:wrap;margin-left:auto}
.prot-chip{font-family:var(--mono);font-size:10px;letter-spacing:.03em;color:var(--dim);padding:3px 9px;border-radius:99px;
  border:1px solid var(--line-solid);background:var(--surface);white-space:nowrap}
.prot-chip i{color:var(--faint);font-style:normal}
@media(max-width:760px){.prot-mech{margin-left:0;width:100%}}

/* ───── fleet tiles ───── */
.fleet{display:grid;grid-template-columns:repeat(auto-fill,minmax(248px,1fr));gap:14px}
.tile{display:block;width:100%;text-align:left;cursor:pointer;background:linear-gradient(180deg,var(--raise),var(--surface));
  border:1px solid var(--line);border-radius:var(--r);padding:15px 16px;overflow:hidden;position:relative;
  transition:transform .18s var(--ease),border-color .25s,box-shadow .25s}
.tile:hover{transform:translateY(-2px);border-color:var(--line-solid);box-shadow:var(--shadow)}
.tile.stale{opacity:.6}
.tile .arrow{position:absolute;right:14px;top:15px;color:var(--faint);transition:transform .2s,color .2s}
.tile:hover .arrow{color:var(--dim);transform:translateX(2px)}
.dot{width:9px;height:9px;border-radius:50%;flex:none;background:var(--unknown)}
.dot.live{background:var(--online);animation:pulse 2.6s infinite}
.dot.stale{background:var(--unknown)}
.tport{display:inline-flex;align-items:center;font-family:var(--mono);font-size:10px;font-weight:500;letter-spacing:.06em;text-transform:uppercase;
  padding:2px 7px;border-radius:6px;margin-left:2px}   /* same shape as the wg/awg .iftype badge; colour per .docker/.baremetal */
.tport.baremetal{color:var(--tport-bare);background:color-mix(in srgb,var(--tport-bare) 14%,transparent)}
.tport.docker{color:var(--tport-dock);background:color-mix(in srgb,var(--tport-dock) 16%,transparent)}
.tport.nix{color:var(--tport-nix);background:color-mix(in srgb,var(--tport-nix) 13%,transparent)}   /* platform, beside the deploy pill — never instead of it */
/* the same platform pill on a node whose installation the panel CAN still touch. Every node reports its distro
   now, so the pill can no longer double as "this host is declarative" the way it did when only NixOS had one —
   that meaning moves to the colour, and the muted one is just a label. */
.tport.os{color:var(--tport-os);background:color-mix(in srgb,var(--tport-os) 13%,transparent)}
/* the node's OS bubble: the distribution's own name as the header, then label/value rows. A two-column grid so
   the values line up whatever the labels translate to — the reason this is not just a `title`. It rides a
   Popover rather than the CSS-only .turnbub because the node card clips (see platformPill). */
/* keeps .deppop's standard 5px outer padding and 8px row inset — same frame as every other hover
   bubble (deprow / cpu-row / onpop-h); only the header stays a tone-coloured title, not an eyebrow. */
.osbub{min-width:210px;display:flex;flex-direction:column;gap:1px}
.osbub-h{padding:4px 8px 6px;font-weight:600;font-size:12px;letter-spacing:.01em}
.osbub-r{display:grid;grid-template-columns:max-content 1fr;gap:12px;align-items:baseline;padding:4px 8px;border-radius:6px}
.osbub-k{color:var(--muted);font-size:11px}
.osbub-v{color:var(--ink);font-family:var(--mono);font-size:11.5px;text-align:right}
.platwrap{display:inline-flex;align-items:center}
/* panel-header deploy tag keeps the bordered livepill pill geometry (overrides the node-badge look above) */
#host-tport{font-size:11px;letter-spacing:.08em;border-radius:99px;padding:5px 11px;margin-left:0;border:1px solid var(--line-solid)}
#host-tport.baremetal{border-color:color-mix(in srgb,var(--tport-bare) 40%,transparent);background:color-mix(in srgb,var(--tport-bare) 9%,transparent)}
#host-tport.docker{border-color:color-mix(in srgb,var(--tport-dock) 45%,transparent);background:color-mix(in srgb,var(--tport-dock) 10%,transparent)}
/* "reporting" — bare status tag matching .nstat (the "updating" style); the only one that keeps a glowing, blinking dot */
.reporting{display:inline-flex;align-items:center;gap:6px;line-height:normal;font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:.05em;
  font-weight:700;color:var(--online)}   /* line-height:normal (not 1) so the label baseline matches the .tport bare-metal/docker pill exactly */
.reporting::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--online);box-shadow:0 0 8px rgba(63,216,154,.7);animation:blink 2.6s infinite}
/* one-line node ribbon: interface/turn tags left, sync/uptime right */
.noderibbon{display:flex;align-items:center;gap:12px;margin:0 0 22px;flex-wrap:wrap}
.node-nodata{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:15px;min-height:46vh;color:var(--unknown);text-align:center}   /* node not reporting + no snapshot: a single centered message instead of empty grids */
.node-nodata svg{width:40px;height:40px;opacity:.5}
.node-nodata p{font-size:15px;margin:0;letter-spacing:.02em}
.nr-tags{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.nr-sync{display:flex;align-items:center;gap:14px;font-family:var(--mono);font-size:11.5px;color:var(--dim);white-space:nowrap}
.meter{margin-top:9px;height:4px;border-radius:3px;background:var(--track);overflow:hidden}
.meter>i{display:block;height:100%;border-radius:3px;background:var(--m,var(--online));transition:width .6s var(--ease)}

/* ───── cards / panels ───── */
.panel{background:var(--surface);border:1px solid var(--line);border-radius:var(--r)}

/* ───── attention list ───── */
.attn{display:flex;flex-direction:column;gap:8px}
.attn-row{display:flex;align-items:center;gap:12px;padding:11px 14px;background:var(--surface);border:1px solid var(--line);
  border-radius:var(--r-sm);cursor:pointer;transition:border-color .15s,background .15s}
.attn-row:hover{border-color:var(--line-solid);background:var(--surface-2)}
.attn-row .name{color:var(--ink);font-weight:500}
.attn-row .why{font-size:12px;color:var(--dim);font-family:var(--mono)}
.attn-row .grow{flex:1}
.allclear{display:flex;align-items:center;gap:10px;padding:18px;color:var(--dim);font-size:13px;
  background:var(--surface);border:1px dashed var(--line-solid);border-radius:var(--r)}
.allclear svg{color:var(--online)}

/* ───── tables ───── */
.tablewrap{background:var(--surface);border:1px solid var(--line);border-radius:var(--r);overflow:hidden}
table{width:100%;border-collapse:collapse}
thead th{text-align:left;padding:8px 14px;font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);font-weight:500;
  font-weight:600;border-bottom:1px solid var(--line);background:var(--surface-2)}
tbody td{padding:9px 14px;border-bottom:1px solid var(--line);font-size:13.5px;vertical-align:middle}
tbody tr:last-child td{border-bottom:0}
tbody tr.clk{cursor:pointer;transition:background .15s}
tbody tr.clk:hover{background:var(--surface-2)}
tbody tr.flash{animation:flash 2.6s var(--ease)}
.ifcard.flash{animation:cardflash 2.4s var(--ease)}
@keyframes cardflash{
  0%{background:color-mix(in srgb,var(--ready) 22%,transparent);border-color:var(--ready)}
  70%{background:color-mix(in srgb,var(--ready) 10%,transparent)}
  100%{background:transparent}}
@media (prefers-reduced-motion:reduce){.ifcard.flash{animation:none;border-color:var(--ready)}}
@keyframes flash{0%{background:color-mix(in srgb,var(--online) 20%,transparent)}100%{background:transparent}}
/* just-created peer row — a one-shot branded glow that ramps 0 → peak → 0 over 1.5s (all peer grids) */
.peergrid tbody tr.pcreate td{animation:pcreate 1.5s var(--ease)}
@keyframes pcreate{0%{background:transparent}42%{background:color-mix(in srgb,var(--brand) 24%,transparent)}100%{background:transparent}}
.c-name{color:var(--ink);font-weight:500}
.addr{font-family:var(--mono);font-size:12.5px;color:var(--dim)}
.when{font-family:var(--mono);font-size:12px;color:var(--dim)}
.rate{font-family:var(--mono);font-size:12px;color:var(--dim);white-space:nowrap}
.rowarrow{color:var(--faint);transition:transform .15s,color .15s}
tbody tr.clk:hover .rowarrow{color:var(--dim);transform:translateX(2px)}
/* panel-host service health → needs attention (critical = crimson like a dangling peer, warn = amber) */
.attn-row.svc{cursor:pointer}
.attn-row.svc.critical{border-color:color-mix(in srgb,var(--dangling) 42%,var(--line))}
.svc-badge{display:inline-flex;align-items:center;gap:5px;font-family:var(--mono);font-size:11px;letter-spacing:.05em;text-transform:uppercase;padding:3px 9px;border-radius:99px;white-space:nowrap}
.svc-badge svg{width:12px;height:12px}
.svc-badge.critical{color:var(--dangling);background:color-mix(in srgb,var(--dangling) 15%,transparent)}
.svc-badge.warn{color:var(--partial);background:color-mix(in srgb,var(--partial) 15%,transparent)}
.svc-modal{display:flex;flex-direction:column;gap:12px;min-width:min(440px,86vw)}
.svc-item{border:1px solid var(--line);border-left-width:3px;border-radius:8px;padding:11px 13px;background:var(--surface)}
.svc-item.critical{border-left-color:var(--dangling)}
.svc-item.warn{border-left-color:var(--partial)}
.svc-head{display:flex;align-items:center;gap:8px}
.svc-dot{width:8px;height:8px;border-radius:50%;flex:none}
.svc-dot.critical{background:var(--dangling)}
.svc-dot.warn{background:var(--partial)}
.svc-tag{margin-left:auto;font-family:var(--mono);font-size:10px;letter-spacing:.06em;text-transform:uppercase;padding:2px 7px;border-radius:99px}
.svc-tag.critical{color:var(--dangling);background:color-mix(in srgb,var(--dangling) 14%,transparent)}
.svc-tag.warn{color:var(--partial);background:color-mix(in srgb,var(--partial) 14%,transparent)}
.svc-msg{margin-top:6px;color:var(--ink);font-size:13px}
.svc-cmd{margin-top:7px;font-size:11px;color:var(--dim)}
.svc-cmd code{font-family:var(--mono);background:var(--surface-2);padding:1px 5px;border-radius:4px;color:var(--dim)}
.svc-foot{color:var(--faint);font-size:12px;line-height:1.5}
.empty{padding:40px 16px;text-align:center;color:var(--faint);font-size:13px}
.empty b{display:block;color:var(--dim);margin-bottom:4px;font-weight:600;font-size:14px}

/* ───── status badges ───── */
.badge{display:inline-flex;align-items:center;gap:5px;padding:3px 10px 3px 8px;border-radius:99px;font-size:11px;line-height:1;
  font-weight:700;letter-spacing:.01em;border:1px solid color-mix(in srgb,currentColor 28%,transparent)}   /* subtle colour-matched border (grey for the stale/unknown badge) — matches the docker/reporting header pills */
.badge::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}
.badge.ic{padding-left:7px}
.badge.ic::before{display:none}
.badge.ic svg{width:11px;height:11px}
.b-online{background:color-mix(in srgb,var(--online) 13%,transparent);color:var(--online)} .b-online::before{animation:blink 2.6s infinite}
@keyframes blink{0%,100%{opacity:1}50%{opacity:.4}}
/* Peer status badges. Thirteen of these were deleted in a86b6ce together with the .ustat set, leaving every
   chip except online/blocked with no colour at all — a lost peer read the same ink as a ready one. Restored
   from a86b6ce^; the stray comments that commit welded onto surviving lines are back on their own rules. */
.b-ready{background:color-mix(in srgb,var(--ready) 12%,transparent);color:var(--ready)}   /* blue — deployed everywhere, awaiting first connection */
.b-partial{background:color-mix(in srgb,var(--partial-r) 16%,transparent);color:var(--partial-r)}   /* LIGHT RED — a problem that isn't a stopper (present on some targets, missing on others) */
/* ORANGE faults: blocked = reaching the server but no handshake; faulty = handshake up but no inbound data */
.b-blocked,.b-faulty{background:color-mix(in srgb,var(--fault) 15%,transparent);color:var(--fault)}
.b-pending{background:color-mix(in srgb,var(--pending) 14%,transparent);color:var(--pending)}   /* violet — awaiting enrolment */
.b-removing{background:rgba(242,107,130,.14);color:var(--dangling)}
.b-dangling{background:color-mix(in srgb,var(--dangling) 14%,transparent);color:var(--dangling)}
.b-orphan{background:rgba(95,117,105,.16);color:var(--unknown)}   /* GREY — unmanaged/external peer, informational */
.b-creating{background:rgba(232,192,75,.16);color:var(--partial)}   /* YELLOW — being created */
.b-rotating{background:rgba(232,192,75,.16);color:var(--partial)}   /* YELLOW — key rotation in flight */
.b-disabled{background:color-mix(in srgb,var(--dangling) 15%,transparent);color:var(--dangling)}   /* RED — access revoked (settled) */
.b-expired{background:color-mix(in srgb,var(--dangling) 15%,transparent);color:var(--dangling)}    /* RED — access date passed */
.b-expiring{background:color-mix(in srgb,var(--fault) 15%,transparent);color:var(--fault)}          /* ORANGE — about to expire */
.b-blocking{background:rgba(232,192,75,.16);color:var(--partial)}   /* AMBER — block converging on the node */
.b-restoring{background:color-mix(in srgb,var(--ready) 12%,transparent);color:var(--ready)}   /* BLUE — restoring after unblock */
/* access-lifecycle flag icon shown BEFORE the peer title / username when the status badge didn't already say it
   (i.e. a non-"ready" status) — red circle = blocked, red triangle = expired, orange triangle = about to expire */
.lc-ic{display:inline-flex;align-items:center;flex:none;margin-right:5px;vertical-align:middle}
.lc-ic svg{width:13px;height:13px}
.lc-blocked,.lc-expired{color:var(--dangling)} .lc-expiring{color:var(--fault)}
.u-name .lc-ic{margin-right:0}
.ifc-orph{color:#F0913C;font-weight:600}
.b-issue{background:color-mix(in srgb,var(--partial) 15%,transparent);color:var(--partial)}
.b-uncat{background:color-mix(in srgb,var(--partial) 15%,transparent);color:var(--partial)}   /* peer resolves over encrypted DNS → not category-routed. Amber: it works, it just isn't routed */

/* ───── users / peers (first-class model) ───── */
.tagchip{display:inline-flex;align-items:center;padding:2px 9px;border-radius:99px;font-size:11px;font-weight:600;
  background:rgba(125,211,252,.12);color:#7dd3fc}
.selwrap.mini{min-width:130px;padding:5px 8px;font-size:12px}
.row2{display:flex;gap:12px}
.row2 .field{flex:1;min-width:0}
/* node detail: interface + turn-proxy cards (drill-in) */
.ifgrid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}
@media(max-width:1000px){.ifgrid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:560px){.ifgrid{grid-template-columns:1fr}}
.ifcard{display:block;border:1px solid var(--line-solid);border-radius:var(--r);padding:14px;background:var(--surface-2);text-decoration:none;color:inherit;transition:border-color .15s var(--ease),transform .15s var(--ease)}
.ifcard.pending{border-style:dashed;opacity:.5}   /* in-progress placeholder (installing/adopting/creating) → dimmed like any attention state */
.ifcard.pending .tg-warn svg{width:11px;height:11px;margin-right:3px}
.chiprow{display:flex;gap:8px}
a.ifcard:hover{border-color:var(--brand);box-shadow:0 6px 16px rgba(0,0,0,.28)}   /* lift via shadow, NOT transform — a transform would become the containing block for the online-count popover (position:fixed) and mis-anchor it */
.ifcard.tp{cursor:default}
.ifcard.tp.clickable{cursor:pointer}
.ifcard.down{opacity:.5;border-color:color-mix(in srgb,var(--dangling) 35%,var(--line-solid))}
.ifcard.down:hover{opacity:.72}
/* Two distinct dims, and they can co-occur:
     .down   — THIS card's own state needs attention (iface down/stopped/error, proxy failed, mesh link down).
               Red-tinted border: something here is wrong.
     .locked — the NODE forbids editing right now (offline, converting, re-installing, updating). Nothing is
               wrong with the card, so no red: just muted, matching the disabled buttons. Every card on the
               page carries it together, so a node-wide state never dims one section and leaves the rest lit. */
.ifcard.locked{opacity:.55}
.ifcard.locked:hover{opacity:.75}
.ifcard.locked.down{opacity:.45}             /* both → keep the red border, dim a little further */
.ifcard.tp.clickable:hover{border-color:var(--turn,#A974F2);transform:translateY(-1px)}
.cmderr{display:inline-flex;align-items:center;color:var(--dangling);cursor:pointer}
.cmderr:hover{color:#ff8585}
.cmderr.warn{color:var(--partial)}
.cmderr.warn:hover{color:#f0d97a}
.xbtn{display:inline-flex;align-items:center;justify-content:center;background:transparent;border:none;color:var(--dim);cursor:pointer;padding:2px;margin-left:3px;border-radius:5px}
.xbtn:hover{color:var(--dangling);background:rgba(240,90,90,.12)}
.ifcard-top{display:flex;align-items:center;gap:8px;margin-bottom:10px;overflow:hidden}   /* ONE line always: to fit a wide status, the name ellipsizes and the turn badges compact (turn→t, in JS); badges+status never shrink or wrap. Reverts to full when no wide status is shown. */
.ifcard-top .ifname{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}   /* the name gives up space first (aw…) */
.ifcard-top>.iftype,.ifcard-top>.drag-grip{flex:none}
.ifcard-top .tg,.ifcard-top .tg-off,.ifcard-top .tg-busy{flex:none;white-space:nowrap}   /* status badges stay whole on the line */
.tg-turn.mini{padding-left:7px;padding-right:7px;text-transform:uppercase}   /* compact "t" badge */
/* hover edit affordance on an interface card: absolutely positioned in the card's top-right (so its padded
   hit-area escapes .ifcard-top's overflow clip and is hoverable on every side), hidden until card hover;
   hovering the area highlights only the icon (no area background); click opens Edit (not the detail page).
   The header reserves a right gutter so badges never sit under it. */
/* the edit button sits inline right after the interface name, revealed on card hover */
/* the interface name IS the edit trigger: name + pencil are one button, so hovering the name reveals/highlights
   the pencil and a click opens the edit modal (the rest of the card still links to the detail page) */
/* the WG/AWG tag now lives INSIDE the button, so tag + name + pencil are one enlarged, hoverable edit target
   (padding + a hover surface make the whole area clickable from every side; negative margin keeps layout put) */
.ifc-edit{flex:0 1 auto;min-width:0;background:none;border:0;padding:4px 8px;margin:-4px -6px -4px -4px;border-radius:8px;cursor:pointer;color:inherit;font:inherit;display:inline-flex;align-items:center;gap:6px;transition:background .12s}
.ifc-edit:hover{background:var(--surface-2)}
.ifc-edit .iftype{flex:none}
.ifc-edit .ifname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ifc-pic{flex:none;display:inline-flex;align-items:center;margin-top:-1px;opacity:0;transition:opacity .12s}
.ifcard:hover .ifc-pic{opacity:1}
.ifc-pic .ic{display:flex;color:var(--faint);transition:color .12s}
.ifc-pic .ic svg{width:14px;height:14px}
.ifcard:hover .ifc-pic .ic{animation:edithint 1s ease 1}   /* one-shot grey→cyan→grey pulse so the operator notices it */
.ifc-edit:hover .ifc-pic .ic{color:var(--brand)}
@keyframes edithint{0%,100%{color:var(--faint)}45%{color:var(--brand)}}
.ifcard-top .tg{flex:none}          /* status tags keep their full width — the name ellipsis-shrinks instead */
.ifcard-top .onlinetag{flex:none}   /* connections count in a turn card header — don't stretch */
/* turn-card header must NOT clip: the connection-count hover pill uses a negative-margin box (like the
   interface-card online count) that breathes into the card's padding — overflow:hidden would shear its
   top/right edge. The name still ellipsizes via .ifname's own overflow, so one-line layout is preserved. */
.ifcard.tp .ifcard-top{overflow:visible}
/* a status tag carrying a node message is clickable (opens it) — pointer + hover highlight as the affordance */
/* any error/detail tag that opens a popup on click — on hover just brighten its text/icon colour so it
   reads as clickable (no background box; the popup carries the details, so no native title tooltip) */
.tg-click{cursor:pointer;transition:filter .12s}
.tg-click:hover{filter:brightness(1.45)}
/* the name truncates (…) so a long fork name never pushes the status tag + cancel × out of the card */
/* the NAME never loses the squeeze: a long status tag beside it truncated `awg0` to "a." — tags shrink first */
.ifcard-top .tg{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ifcard-top .ifname{flex:0 1 auto;min-width:3.5ch}
.ifcard-top .ifname{font-weight:600;color:var(--ink);font-family:var(--mono);font-size:13.5px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ifcard-top .iftype,.ifcard-top .tg-busy,.ifcard-top .tg-ok,.ifcard-top .tg-warn,.ifcard-top .tg-del,.ifcard-top .cmderr,.ifcard-top .xbtn{flex:none}
.ifcard-top .rowarrow{color:var(--faint)}
.ifcard-top .grow{flex:1 1 0;min-width:0}
.ifcard-rows{display:flex;flex-direction:column;gap:6px}
/* drag-to-reorder: grip handle + drag/drop affordances (cards, turn cards, node cards) */
.drag-grip{flex:none;display:inline-flex;align-items:center;justify-content:center;width:16px;height:18px;margin:-2px 2px -2px -3px;
  color:var(--dim);cursor:grab;opacity:.6;border-radius:5px;transition:opacity .12s,color .12s,background .12s}
.drag-grip:hover{opacity:1;color:var(--ink-2);background:var(--surface-2)}
.drag-grip:active{cursor:grabbing}
.ifcard:hover .drag-grip,.ncard:hover .drag-grip,.ifcard.tp:hover .drag-grip{opacity:.9}
.dragging,.ifcard.dragging,.ncard.dragging{opacity:.18}   /* the source card left in place while dragging — beat .ifcard.down's opacity */
/* drop indicator: a cyan edge on each card flanking the gap (l/r for grids, t/b for vertical lists) */
.drop-l{box-shadow:inset 3px 0 0 var(--brand)}
.drop-r{box-shadow:inset -3px 0 0 var(--brand)}
.drop-t{box-shadow:inset 0 3px 0 var(--brand)}
.drop-b{box-shadow:inset 0 -3px 0 var(--brand)}
/* our own translucent card preview that follows the cursor while dragging (a clone pinned to the body) */
.drag-ghost{opacity:.82;background:var(--raise);border:1px solid var(--brand);border-radius:var(--r);
  box-shadow:0 20px 46px -10px rgba(0,0,0,.6);overflow:hidden;transition:none}
.drag-ghost .drag-grip{opacity:1;color:var(--brand)}
.ifrow{display:flex;justify-content:space-between;gap:10px;font-size:12px}
.ifrow .l{color:var(--dim)} .ifrow .r{font-family:var(--mono);color:var(--ink-2)}
.ifrow .r.addr{font-size:11px}
/* A row whose label is a SENTENCE: it takes the full width and what it applies to sits under it, rather
   than wrapping to two lines beside a single name marooned in the right-hand column. */
/* Section headings INSIDE a notice box: the shared .seclabel underlines itself, which puts a rule between
   a heading and its own rows — the two things that belong together — while the boundary that matters, one
   section ending and the next beginning, had no line at all. So: no underline here, and a divider above a
   heading that FOLLOWS another section's rows. */
.notice .seclabel{border-bottom:none;padding-bottom:0;margin:14px 0 6px}
.notice .seclabel:first-child{margin-top:6px}
.notice .ifcard-rows + .seclabel{border-top:1px solid var(--line);padding-top:12px}
/* a changed address: what it belongs to on the left, the mapping on the right, both mono so the two
   columns line up down the list and neither has to wrap */
.addrrow{gap:14px;font-family:var(--mono);font-size:12px}
.addrrow .l{color:var(--ink-2)} .addrrow .r{white-space:nowrap}
.ifrow-stack{display:block}
.ifrow-stack .r{display:block;margin-top:3px}
.iftype{font-family:var(--mono);font-size:10px;font-weight:500;letter-spacing:.06em;text-transform:uppercase;padding:2px 7px;border-radius:6px}
.iftype.wg{background:rgba(63,216,154,.14);color:var(--online)}
.iftype.awg{background:rgba(31,200,214,.15);color:var(--brand)}
.iftype.wdtt{background:rgba(129,181,18,.16);color:#81B512}   /* WDTT type badge fallback until applyThemeColors injects the tunable colour */
.iftype.csqtt{background:rgba(249,115,22,.16);color:#F97316}   /* csqtt type badge fallback until applyThemeColors injects the tunable colour */
/* Approach-B adoption candidates — a wg/awg interface found on the node the panel doesn't manage yet: orange
   "found" cards in the same grid → open to Adopt or Ignore. */
.ifcard.candidate{border-color:color-mix(in srgb,var(--fault) 45%,var(--line-solid));background:color-mix(in srgb,var(--fault) 6%,var(--surface-2))}
a.ifcard.candidate:hover{border-color:var(--fault);box-shadow:0 6px 16px rgba(0,0,0,.28)}
.tg-cand{display:inline-flex;align-items:center;gap:5px;border-radius:99px;padding:1px 8px;font-family:var(--mono);font-size:9.5px;font-weight:700;line-height:1.45;text-transform:uppercase;letter-spacing:.04em;white-space:nowrap;background:rgba(240,145,60,.13);color:#F0913C;border:1px solid color-mix(in srgb,#F0913C 30%,transparent)}
.tg-cand svg{width:11px;height:11px}
/* A dormant WDTT card has no interface page to open, so the whole card IS the adopt action — give it the same
   affordance the other clickable cards have, in the candidate's own colour. */
.ifcard.candidate.clickable{cursor:pointer}
.ifcard.candidate.clickable:hover{border-color:var(--fault);transform:translateY(-1px)}
.ifcard.candidate.ignored{border-color:var(--line-solid);background:var(--surface-2);opacity:.6}   /* dismissed — muted, at the end of the grid; open to un-ignore */
a.ifcard.candidate.ignored:hover{opacity:.85;border-color:var(--brand)}
.tg-ign{display:inline-flex;align-items:center;gap:5px;border-radius:99px;padding:1px 8px;font-family:var(--mono);font-size:9.5px;font-weight:700;line-height:1.45;text-transform:uppercase;letter-spacing:.04em;white-space:nowrap;background:color-mix(in srgb,var(--unknown) 16%,transparent);color:var(--unknown);border:1px solid color-mix(in srgb,var(--unknown) 30%,transparent)}
.tg-ign svg{width:11px;height:11px}
/* Type picker: one pill per protocol, the chosen one outlined in ITS OWN colour — the same wg=green,
   awg=teal, wdtt=lime language the interface badges use, so the choice reads the same as the result. */
/* "Adopt existing" — a MODE toggle sharing the protocol row, deliberately NOT another protocol chip: the type
   is chosen once and applies whether the interface is created or taken over. Same on/off control as the Live
   screen's Online button, in the fault orange every adoption surface uses. */
.adoptsw{appearance:none;flex:none;margin-left:auto;height:30px;padding:0 14px;border:1px solid var(--line-solid);border-radius:99px;
  background:var(--surface-2);color:var(--dim);font:inherit;font-size:12px;font-weight:600;cursor:pointer;
  transition:background .12s var(--ease),color .12s var(--ease),border-color .12s var(--ease)}
.adoptsw:hover{color:var(--ink);border-color:color-mix(in srgb,var(--fault) 45%,var(--line-solid))}
.adoptsw.on{background:color-mix(in srgb,var(--fault) 18%,transparent);color:var(--fault);border-color:color-mix(in srgb,var(--fault) 55%,transparent)}
.typepick{display:flex;gap:8px;flex-wrap:wrap}
.tp-opt{appearance:none;font:inherit;font-size:12px;padding:7px 16px;border-radius:99px;cursor:pointer;
  background:var(--surface-2);border:1px solid var(--line-solid);color:var(--dim);
  transition:border-color .12s,background .12s,color .12s}
.tp-opt:hover:not(:disabled){border-color:color-mix(in srgb,var(--tpc) 50%,var(--line-solid));color:var(--text)}
.tp-opt.on{border-color:var(--tpc);background:color-mix(in srgb,var(--tpc) 12%,transparent);color:var(--tpc);font-weight:600}
.tp-opt:disabled{opacity:.45;cursor:not-allowed}
.iftype.turn{color:var(--tfc,#B49BF7);background:color-mix(in srgb,var(--tfc,#A78BF5) 17%,transparent)}
.iftype.orph{background:color-mix(in srgb,var(--fault) 15%,transparent);color:var(--fault)}   /* unmanaged: type is NOT known yet — never claim wg/awg here */
.iftype.muted{background:color-mix(in srgb,var(--unknown) 13%,transparent);color:var(--unknown)}

.ptitle{display:inline-flex;align-items:center;gap:4px}
.ptitle b{color:var(--ink);font-size:13.5px}
.rowacts{white-space:nowrap}
.rowacts .btn-mini{margin-left:6px}
/* pinned, explained failure on a row (optimistic rollback) */
.rowerr{display:inline-flex;align-items:center;gap:5px;margin-left:8px;padding:2px 8px;border-radius:99px;
  background:color-mix(in srgb,var(--dangling) 13%,transparent);color:var(--dangling);font-size:11px;font-weight:600;white-space:nowrap;max-width:280px;overflow:hidden;text-overflow:ellipsis}
.rowerr svg{flex:none}
.rowerr-x{background:none;border:none;color:var(--dangling);cursor:pointer;font-size:14px;line-height:1;padding:0 0 0 2px;opacity:.7}
.rowerr-x:hover{opacity:1}
/* multi-target picker (new peer) */
.targetpick{display:flex;flex-direction:column;gap:6px;max-height:478px;overflow-y:auto;padding-right:3px;scrollbar-width:thin;scrollbar-color:var(--line-solid) transparent}   /* ~10 rows, then scroll */
.targetpick::-webkit-scrollbar{width:10px}
.targetpick::-webkit-scrollbar-track{background:transparent}
.targetpick::-webkit-scrollbar-thumb{background:var(--line-solid);border-radius:8px;border:2px solid var(--surface)}
.targetpick::-webkit-scrollbar-thumb:hover{background:var(--dim)}
.targetopt{display:flex;align-items:center;gap:10px;border:1px solid var(--line);border-radius:10px;padding:7px 10px;background:var(--surface-2)}
.targetopt.sel{border-color:var(--line-solid)}
.targetopt .topt-main{display:flex;align-items:center;gap:9px;flex:1;cursor:pointer;min-width:0}
.targetopt.locked .topt-main{cursor:default}
.targetopt .box{width:17px;height:17px;border:1.5px solid var(--line-solid);border-radius:5px;display:inline-flex;align-items:center;justify-content:center;flex:none;color:var(--brand)}
.targetopt.sel .box{border-color:var(--brand);background:color-mix(in srgb,var(--brand) 16%,transparent)}
.targetopt .nm{font-weight:600}
.targetopt .tp{font-size:13.5px;color:var(--faint);font-family:var(--mono)}
/* an existing deployment whose interface is gone — flagged so it reads as removable-only, not a fresh option */
.targetopt.missing{border-color:color-mix(in srgb,var(--dangling) 34%,var(--line-solid));background:color-mix(in srgb,var(--dangling) 5%,transparent)}
.topt-missing{font-family:var(--mono);font-size:9px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--dangling);border:1px solid color-mix(in srgb,var(--dangling) 34%,transparent);border-radius:99px;padding:1px 6px}
.targetopt .tg{flex:none;margin-right:2px}   /* wg/awg type tag sits just before the address */
/* The row's right-hand controls as ONE fixed-width, LEFT-ANCHORED column, so the first thing in it starts at
   the same x on every row — the ★ in the edit sheet's Deployments list, the protocol badge in the picker.
   The row was a plain right-packed flex, which only lines up while every row carries the same controls: a
   self-contained turn row has two badges, no address box and no gear, so it is a different width than a
   wg/awg row and its badge landed in its own place. Now a row with less in it leaves slack on the RIGHT
   instead of dragging its leading badge out of line. Width = a full wg/awg row: badge 44 + address 130 +
   gear 28 + two 10px gaps. `hasprim` adds a FRONT badge (and, on a mixed-kind peer, the ★ toggle): 232 for
   chip+address+gear+gaps, plus ~64 of badge before it starts to ellipsis. */
.targetopt .topt-right{flex:none;display:flex;align-items:center;gap:10px;width:222px}
.targetopt .topt-right.hasprim{width:296px}   /* fits the full row: kind chip + front badge + address + gear */
/* BOTH pickers use hasprim. The narrow 222 has no room for a front badge, and .tgt-front is the only
   shrinkable item in the row — so on a turn-fronted interface it was squeezed to 10px and its text ran
   under the address box. Whether a row HAS a front badge cannot be known before render (TargetFrontBadge
   decides from live turn/wdtt state), and widening only the rows that do would stagger the leading chip,
   which is the misalignment the fixed width exists to prevent. Widening all of them costs slack on the
   right of a bare row, which is exactly what the note above says is the acceptable trade. */
.targetopt .topt-right > .tg{min-width:44px;justify-content:center;margin-right:0}   /* uniform badge width, so the column after it lines up too */
.targetopt .topt-right .tgt-front{min-width:0;flex:0 1 auto;overflow:hidden}         /* a long proxy title shrinks rather than shoving the row —
   `flex:0 1 auto` is load-bearing: `.targetopt .tg` sets flex:none, so the min-width:0 alone never let it give
   way and the row overflowed its fixed-width box instead (the gear fell off the right edge as soon as a rotate
   made it appear). It also overrides the 44px floor one line up: that floor exists to line the ADDRESS column
   up across rows, which the fixed-width box already guarantees — the front badge is the one item here whose
   width is content, so it is the one that yields when a row carries ★ + chip + badge + address + gear. */
.targetopt .topt-ip{width:130px;flex:none;padding:5px 8px;font-size:12.5px}
.targetopt .topt-right .topt-ip.faint{width:auto;padding-left:2px}                    /* "auto" is a word, not a field — it needs no address-box width */
.targetopt .topt-ip.existing{display:inline-flex;align-items:center;justify-content:flex-end;font-family:var(--mono);color:var(--ink-2);text-align:right}   /* already-deployed address, read-only */
.targetopt .topt-ip[readonly]{color:var(--ink-2);text-align:right;font-family:var(--mono)}
/* Per-deployment settings (DNS/MTU/routing) live behind this gear — see TargetSettingsSheet. It sits after the
   address box on wg/awg rows only; a wdtt/csqtt row has no client config, and that missing gear is the tell.
   `.on` = this deployment differs from its interface's defaults, so a row's customisation is visible unopened. */
.targetopt .topt-gear{flex:none;width:28px;height:28px;padding:0;color:var(--faint)}
.targetopt .topt-gear:hover{color:var(--ink)}
.targetopt .topt-gear.on{color:var(--brand);border-color:color-mix(in srgb,var(--brand) 40%,transparent);background:color-mix(in srgb,var(--brand) 10%,transparent)}
.targetpick.scroll{max-height:232px}   /* Add-peers carousel grid: scroll after ~5 rows */

/* Add-peers — two grids (this user's peers · unassigned pool). Each peer is a bordered block; a multi-deployment
   peer stacks its targets under one border, primary row tinted and tagged, backups beneath, so it reads as one identity. */
.pg2-sec{margin:0 0 20px}
.pg2-sec:last-child{margin:0}
.pg2-hdr{display:flex;align-items:center;gap:10px;margin:0 0 9px}
.pg2-hdr label{margin:0;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--dim)}
.pg2-fresh{flex:none}
.pg2{display:grid;grid-template-columns:auto minmax(120px,1fr) max-content max-content max-content 66px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--line-solid) transparent}
.pg2::-webkit-scrollbar{width:9px}
.pg2::-webkit-scrollbar-track{background:transparent}
.pg2::-webkit-scrollbar-thumb{background:var(--line-solid);border-radius:6px;border:2px solid transparent;background-clip:padding-box}
.pg2::-webkit-scrollbar-thumb:hover{background:var(--dim);background-clip:padding-box}
.pg2-peer{grid-column:1/-1;display:grid;grid-template-columns:subgrid;border:1px solid var(--line-solid);border-radius:10px;background:var(--surface-2);margin:0 0 8px;overflow:hidden}
.pg2-peer:last-child{margin:0}
.pg2-peer.pcreate{animation:pcreate 1.5s var(--ease)}   /* same glow as a just-created users/peers-grid row */
.pg2-dep{grid-column:1/-1;display:grid;grid-template-columns:subgrid;align-items:center;column-gap:12px;padding:8px 11px}
.pg2-dep+.pg2-dep{border-top:1px dashed var(--line)}
.pg2-dep.clk{cursor:pointer;transition:background .12s}
.pg2-dep.clk:hover{background:var(--row-hover)}
.pg2-dep.primary{background:color-mix(in srgb,var(--brand) 7%,transparent)}
.pg2-dep.primary.clk:hover{background:color-mix(in srgb,var(--brand) 12%,transparent)}
.pg2-c{min-width:0}
.pg2-title{display:flex;align-items:center;gap:10px}
.pg2-nm{font-weight:600;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pg2-rel{flex:none;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;border-radius:4px;padding:1.5px 6px}
.pg2-rel.primary{color:var(--brand);background:color-mix(in srgb,var(--brand) 15%,transparent)}
.pg2-bk{font-style:italic;font-weight:400;color:var(--dim)}
/* The interface name and the address are their OWN columns of the outer grid, not a nested one: every row
   is a subgrid of .pg2, so a max-content track sizes to the widest row and the names/addresses line up down
   the list. A nested grid per row could only align within that row, which is what left them ragged. */
.pg2-tags{display:flex;align-items:center;gap:6px;min-width:0;justify-content:flex-end}   /* shared track, packed
  RIGHT: a one-badge row then sits against the interface name it belongs to, not stranded a chip-width away */
.pg2-tags .tgt-front{max-width:none;flex:none}   /* the fork name identifies the server — never the thing that clips */
.pg2-ifn{color:var(--muted);font-family:var(--mono);font-size:12px;white-space:nowrap;justify-self:start}
.pg2-ip{color:var(--ink-2);font-family:var(--mono);font-size:12px;white-space:nowrap;justify-self:end;margin-right:8px}   /* clear of the row actions */
.pg2-ctl{display:flex;justify-content:flex-end;gap:5px}
.pg2-act{flex:none;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;border-radius:7px;border:1px solid var(--line);background:transparent;color:var(--dim);cursor:pointer;transition:all .14s}
.pg2-act svg{width:17px;height:17px}
.pg2-act.add:hover{color:var(--brand);border-color:var(--brand)}
.pg2-act.free:hover{color:var(--online);border-color:var(--online)}
.pg2-act.mine:hover{color:#e0574d;border-color:#e0574d}
.pg2-empty{padding:16px;text-align:center;color:var(--dim);border:1px dashed var(--line-solid);border-radius:10px;font-size:13px}
.lockeduser{padding:10px 12px;border:1px solid var(--line);border-radius:var(--r-sm);background:var(--surface-2);color:var(--ink);font-weight:500}

/* ───── toolbar (users) ───── */
.toolbar{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:14px}
.search{flex:1;min-width:180px;position:relative}
/* compact search living in a panel header (interface peers), next to the title */
.panel-head .search.hdr{flex:1;min-width:160px;margin-left:6px}   /* dynamic width — fill the header row */
.panel-head .search.hdr ~ .grow{flex:0}   /* don't let the spacer split the slack; the search absorbs it all */
.panel-head .search.hdr input{height:32px}
.search svg{position:absolute;left:12px;top:50%;transform:translateY(-50%);opacity:.5;pointer-events:none}
input,select{background:var(--surface-2);border:1px solid var(--line-solid);color:var(--ink);border-radius:var(--r-sm);
  padding:9px 12px;font-family:var(--mono);font-size:13px;transition:border-color .15s,box-shadow .15s;width:100%}
input::placeholder{color:var(--faint)}
input:focus,select:focus{outline:none;border-color:var(--brand)}
/* read-only field value — matches an input's box but visibly non-editable (create-time settings shown in an edit sheet) */
.ro-field{background:var(--surface);border:1px dashed var(--line-solid);color:var(--dim);border-radius:var(--r-sm);
  padding:9px 12px;font-size:13px;width:100%;box-sizing:border-box;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* read-only row that carries an action. The button must never be the thing that gets clipped — .ro-field's own
   overflow:hidden would eat it, and a wider sheet only moves the breakpoint (a longer translation re-breaks it).
   So the identity and the button hold their size and the descriptive note is what truncates. */
.ro-field.ro-act{display:flex;align-items:center;gap:8px;overflow:visible}
.ro-act>.mono,.ro-act>.btn{flex:0 0 auto}
.ro-act>.ro-note{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
textarea.ta{background:var(--surface-2);border:1px solid var(--line-solid);color:var(--ink);border-radius:var(--r-sm);padding:10px 12px;font-family:var(--mono);font-size:13px;width:100%;resize:vertical;line-height:1.6;min-height:84px;word-break:break-all}
textarea.ta:focus{outline:none;border-color:var(--brand)}
textarea.ta::placeholder{color:var(--faint)}
textarea.ta{scrollbar-width:thin;scrollbar-color:var(--line-solid) transparent}
textarea.ta::-webkit-scrollbar{width:10px}
textarea.ta::-webkit-scrollbar-track{background:transparent}
textarea.ta::-webkit-scrollbar-thumb{background:var(--line-solid);border-radius:8px;border:2px solid var(--surface-2)}
textarea.ta::-webkit-scrollbar-thumb:hover{background:var(--dim)}
.search input{padding-left:34px}
/* The hand-drawn caret for a native <select> lived here. There are no native selects in this SPA any more —
   every one is the shared `Dropdown`, which draws its own — so the rule had nothing left to style. The
   grouped `input,select{…}` above stays: it costs nothing and it means a stray future <select> is merely
   unstyled rather than unreadable, while still looking wrong enough to send its author to `Dropdown`. */
.selwrap{width:auto;min-width:150px}
/* ⚠️ `.selwrap` IS A SIZING HOOK NOW, NOT A NATIVE <select>. Every one of those became the shared Dropdown,
   which renders `.dropdown` — and `.dropdown{min-width:0}` is declared LATER in this file, so a bare
   `.selwrap` would lose the tie and every toolbar control would collapse to its text. This rule wins on
   specificity (0,2,0). It is placed HERE, early, on purpose: the parent rules that resize a particular slot
   (`.tf-trail`, `.pager-size`, `.turninst`, `.field`, `.rrrow`) are also (0,2,0), so they beat it only by
   coming later. Move this below them and they all silently stop applying. */
.dropdown.selwrap{width:auto;min-width:150px}
/* in a form field a dropdown should match the text inputs (full width), not size to its content */
.field .selwrap:not(.mini){width:100%;min-width:0}
.chips{display:flex;gap:7px;flex-wrap:wrap}
.chip{font-size:12px;padding:6px 12px;border-radius:99px;cursor:pointer;background:var(--surface-2);
  border:1px solid var(--line);color:var(--dim);display:flex;align-items:center;gap:6px;transition:all .15s}
.chip:hover{color:var(--ink)}
.chip.on{color:var(--ink);border-color:var(--brand);background:color-mix(in srgb,var(--brand) 10%,transparent)}
/* protocol picker (Load new interface): AmneziaWG=purple, WireGuard=green, Existing=orange when selected */
.proto3{flex-wrap:wrap}
.chip.c-awg.on{color:#C9B6FF;border-color:#B49BF7;background:color-mix(in srgb,#B49BF7 16%,transparent)}
.chip.c-wg.on{color:#8DEFC4;border-color:var(--online);background:color-mix(in srgb,var(--online) 14%,transparent)}
.chip.c-ex.on{color:#F6B873;border-color:#F0913C;background:color-mix(in srgb,#F0913C 16%,transparent)}
.chip.c-wdtt.on{color:#C4B0FA;border-color:#A78BFA;background:color-mix(in srgb,#A78BFA 16%,transparent)}   /* WDTT — violet, matches the turn-family */
.chip .swatch{width:7px;height:7px;border-radius:50%;background:var(--cc,var(--dim))}
.chip b{font-family:var(--mono);font-weight:600;color:var(--ink-2)}
.meta{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-sm);padding:13px 14px}
.meta .k{font-size:10px;letter-spacing:.11em;text-transform:uppercase;color:var(--dim);font-weight:600}
.meta .v{font-family:var(--mono);font-size:13px;color:var(--ink-2);margin-top:6px;word-break:break-all}
/* universal gap between a screen's header line(s) and its content */
.detail-head{display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap;margin-bottom:22px}
.detail-head:has(+ .noderibbon){margin-bottom:14px}   /* node detail: space between the title and the tags/sync ribbon */
.detail-head .title{display:flex;align-items:center;gap:12px}
.dh-ver{display:flex;align-items:center;gap:8px;align-self:center;margin-right:-4px}   /* version + check/update + node controls, level with the title */
.dh-sep{width:1px;height:18px;background:var(--line-solid);margin:0 3px}
.dh-ver .nm-ver{margin-left:0}
.upd-busy{pointer-events:none;cursor:default;color:var(--partial);border-color:color-mix(in srgb,var(--partial) 45%,var(--line-solid))}
.updspin{animation:spin .8s linear infinite;transform-box:view-box;transform-origin:center;margin-left:1px}   /* any updspin arrow animates — header upd-busy AND .hostproc-tag. view-box (not fill-box): the viewBox centre is known at parse time, so it spins perfectly centred even on a fresh load (fill-box needs the SVG laid out first → off-centre after Ctrl+Shift+R) */
/* "panel updated — hard reload" modal */
.updone p{margin:0 0 18px;line-height:1.6;color:var(--ink-2);font-size:13.5px}
.updone p:last-child{margin-bottom:0}
.updone b{color:var(--ink);font-family:var(--mono)}
.updone-hint{display:flex;align-items:center;gap:7px;color:var(--brand);font-weight:600;letter-spacing:.01em}
.updone kbd{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;padding:0 8px;
  font-family:var(--mono);font-size:11.5px;font-weight:600;color:var(--ink);background:var(--surface-2);
  border:1px solid var(--line-solid);border-bottom-width:2px;border-radius:6px}
/* "check for updates": cyan + spinning while polling, back to grey when done. The extra
   .appbar-right selector beats `.appbar-right .iconbtn{color:--dim}` (same specificity, later). */
.iconbtn.checking, .appbar-right .iconbtn.checking{color:var(--brand);pointer-events:none}
/* Spin the whole BUTTON, not the root <svg>. transform-box:view-box/fill-box on a root HTML-embedded SVG isn't
   reliably honoured by Chrome on first paint (esp. right after Ctrl+Shift+R) → the icon sometimes span around an
   off-centre point. .iconbtn is a fixed 30/36px flex-CENTRED box, so its border-box centre exists from parse time
   and the icon sits dead-centre → rotating the button pivots exactly on the icon's own centre, every time. */
.iconbtn.checking{animation:spin .8s linear infinite;transform-origin:center}
.detail-head .title h1{margin:0;font-family:var(--mono);font-size:22px;line-height:1;font-weight:600;color:var(--ink)}
.detail-head .grow{flex:1}

/* ───── user profile ───── */
.identity{display:flex;flex-wrap:wrap;gap:22px;margin:16px 0 4px}
.identity .item .k{font-size:10px;letter-spacing:.11em;text-transform:uppercase;color:var(--dim);font-weight:600}
.identity .item .v{font-family:var(--mono);font-size:13.5px;color:var(--ink-2);margin-top:5px;display:flex;align-items:center;gap:8px}
.copybtn{background:none;border:0;color:var(--faint);cursor:pointer;padding:0;line-height:0}
.copybtn:hover{color:var(--ready)}
.deploy{background:var(--surface);border:1px solid var(--line);border-radius:var(--r);overflow:hidden;
  display:flex;flex-direction:column}
.deploy-head{display:flex;align-items:center;gap:10px;padding:13px 15px;border-bottom:1px solid var(--line)}
.deploy-head .nm{font-family:var(--mono);font-size:12.5px;font-weight:600;color:var(--ink)}
.deploy-head .nmlink{cursor:pointer} .deploy-head .nmlink:hover{text-decoration:underline}
.deploy-head .grow{flex:1}
.wdtt-link{flex:1;min-width:0;align-self:center;font-size:11.5px;line-height:1.55;word-break:break-all;color:var(--fg)}
/* carousel WDTT card: body fills between header and the (bottom-pinned) actions, link vertically centred — so Copy/
   Alternatives line up with the QR cards' Config/Copy row and the short link sits mid-card */
.qrrow .deploy .deploy-body{flex:1;align-items:center;justify-content:center}
/* link instead of a QR: STACK. The body is a flex row (a QR beside its meta), and there a sibling — the
   "no VK call link" hint, or the meta table — keeps its intrinsic width while the link's flex:1;min-width:0
   lets it collapse to zero, so break-all sets the wdtt:// link one character per line. The first selector
   is not redundant: `.deploy-body-stack` alone loses to the align-items:center on the line above. */
.qrrow .deploy .deploy-body-stack, .deploy-body-stack{flex-direction:column;align-items:stretch;justify-content:center;gap:8px}
.deploy-body-stack .wdtt-link{flex:0 0 auto;align-self:stretch}
.deploy-body{padding:15px;display:flex;gap:15px;position:relative}
.deploy .qr{background:#fff;border-radius:var(--r-sm);padding:8px;line-height:0;flex:none;cursor:zoom-in}
.deploy .qr .qrimg{display:block;width:min(300px,72vw);height:auto;image-rendering:pixelated;image-rendering:crisp-edges}
.qr-overlay{position:fixed;inset:0;z-index:9999;background:var(--qr-scrim);display:flex;cursor:zoom-out;padding:24px;overflow:auto}
/* margin:auto (not align/justify-center) keeps the block reachable when it overflows a short window */
.qr-overlay-inner{display:flex;flex-direction:column;align-items:center;gap:16px;margin:auto;max-width:100%}
.qr-overlay-card{background:#fff;padding:18px;border-radius:12px;line-height:0;max-width:100%}
/* bound by BOTH axes and reserve ~160px for the card padding + caption, so the caption never gets pushed off */
.qr-overlay-card .qrimg{display:block;width:auto;height:auto;max-width:min(86vw,820px);max-height:min(calc(100vh - 160px),820px);image-rendering:pixelated;image-rendering:crisp-edges}
.qr-overlay-cap{color:var(--ink);font-size:14px;font-weight:600;display:flex;align-items:center;justify-content:center;gap:10px;flex-wrap:wrap;text-align:center}
.qr-overlay-cap .qrc-srv{font-family:var(--mono)}
.deploy .qr-fail,.deploy .qr-none{width:240px;height:240px;max-width:60vw;display:flex;align-items:center;justify-content:center;text-align:center;
  font-size:11px;color:var(--faint);background:var(--surface-2);border-radius:var(--r-sm);padding:10px;line-height:1.4}
.deploy .dmeta{flex:1;min-width:0;display:flex;flex-direction:column;gap:9px}
.deploy .dmeta .row{display:flex;justify-content:space-between;gap:8px;font-size:12px}
.deploy .dmeta .row .k{color:var(--dim)} .deploy .dmeta .row .vv{font-family:var(--mono);color:var(--ink-2)}
.deploy .acts{padding:0 15px 15px;display:flex;gap:8px}

.notice{display:flex;gap:9px;align-items:flex-start;padding:11px 14px;border-radius:var(--r-sm);font-size:12.5px;margin-bottom:16px}   /* consistent gap below every notice; :last-child trims it */
.notice:last-child{margin-bottom:0}
.notice.warn{background:rgba(232,192,75,.08);border:1px solid rgba(232,192,75,.25);color:var(--pending)}
/* Insecure context: the panel is on plain http, so Web Crypto is absent and every key operation fails.
   A full-width bar above the app chrome — not a toast (it must not be dismissible or time out) and not a
   blocking screen (monitoring still works; only crypto does not). Same warn palette as .notice.warn. */
.insecure-banner{padding:9px 16px;font-size:13px;line-height:1.45;color:var(--pending);
  background:rgba(232,192,75,.10);border-bottom:1px solid rgba(232,192,75,.28)}
.insecure-banner b{margin-right:6px}
.notice svg{flex:none;margin-top:1px}
/* error/log modal body — captured installer output / command errors, shown line-by-line, scrollable */
.logview{font-family:var(--mono);font-size:12.5px;line-height:1.55;color:var(--ink-2);background:var(--surface-2);border:1px solid var(--line-solid);border-radius:var(--r-sm);padding:10px 12px;max-height:48vh;overflow:auto}
.logline{white-space:pre-wrap;word-break:break-word;min-height:1.55em}
.logview.raw{color:var(--dim)}
.btn.logtoggle{margin-right:auto}   /* pin the raw/rendered toggle to the left of the footer */
/* full-width attention banner (e.g. store_configs off) — sits at the top of a screen */
.banner{display:flex;gap:11px;align-items:flex-start;margin-bottom:16px;padding:12px 15px;border-radius:var(--r-sm);font-size:13px;line-height:1.5}
.banner svg{flex:none;margin-top:2px}
.banner.warn{background:rgba(232,192,75,.1);border:1px solid rgba(232,192,75,.35);color:var(--pending)}
.banner.warn svg{color:var(--partial)}
.banner b{color:var(--ink)}
.banner code{font-family:var(--mono);font-size:11.5px;padding:1px 5px;border-radius:4px;background:rgba(255,255,255,.07);color:var(--ink)}
.banner .cmdrow{margin-top:10px}
.banner .tokenbox{font-size:11.5px;padding:9px 11px}

/* ───── modal / create sheet ───── */
.overlay{position:fixed;inset:0;background:rgba(4,8,6,.66);backdrop-filter:blur(4px);z-index:50;
  display:none;align-items:flex-start;justify-content:center;padding:48px 18px;overflow:auto}
.overlay.show{display:flex;animation:fade .2s}
@keyframes fade{from{opacity:0}to{opacity:1}}
/* 560 was too narrow for the widest footer we ship — the user editor's Delete / Rotate all keys / Block row
   plus Cancel + Save wrapped onto three lines. 640 fits it in both languages with room to spare. */
.sheet{background:var(--surface);border:1px solid var(--line-solid);border-radius:16px;width:min(640px,100%);
  box-shadow:var(--shadow);animation:pop .24s var(--ease)}
@keyframes pop{from{opacity:0;transform:translateY(14px) scale(.98)}to{opacity:1;transform:none}}
.sheet-head{display:flex;align-items:center;padding:18px 20px;border-bottom:1px solid var(--line)}
/* the drops readout sits beside Turn IPs in the sheet header — its own control, with real air between
   them so they don't read as one widget */
.pxdrop-hd{margin-right:10px;cursor:default}
.pxdrop-hd b{font-family:var(--mono);font-variant-numeric:tabular-nums;font-weight:600}
.sheet-head h3{margin:0;font-size:15px;color:var(--ink);font-weight:650;letter-spacing:.01em}
.sheet-head h3 .sheet-ver{font-family:var(--mono);font-size:12px;font-weight:500;color:var(--ready);margin-left:4px}   /* deployed version shown right in the turn-proxy sheet title */
.sheet-head h3 .sheet-verset{margin-left:6px;padding:2px;vertical-align:middle;color:var(--muted)}   /* gear beside the version → fork Server defaults (version/rollback) */
.sheet-head h3 .sheet-verset:hover{color:var(--fg)}
.sheet-head h3 .sheet-verset svg{width:14px;height:14px;display:block}
.sheet-head .x{margin-left:auto;background:none;border:0;color:var(--dim);cursor:pointer;font-size:22px;line-height:1;position:relative;top:-2px}   /* the "×" glyph renders ~2.5px LOW in its box — pull it up so its optical centre lines up with the header text row */
.sheet-head .x:hover{color:var(--ink)}
/* Blocked-access treatment: a modal whose peer/user is blocked reads red at a glance — a red border + soft
   ring, a faintly red-tinted header, and a solid BLOCKED chip. Restrained on purpose (no full-red fill) so the
   configs/QRs inside stay legible. */
.sheet.blocked{border-color:color-mix(in srgb,var(--dangling) 60%,transparent);
  box-shadow:var(--shadow),0 0 0 3px color-mix(in srgb,var(--dangling) 22%,transparent)}
.sheet.blocked .sheet-head{background:color-mix(in srgb,var(--dangling) 9%,transparent);
  border-bottom-color:color-mix(in srgb,var(--dangling) 30%,transparent)}
.blocked-tag{display:inline-flex;align-items:center;gap:4px;margin-left:10px;padding:3px 9px;border-radius:99px;
  background:var(--dangling);color:#fff;font-size:10.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;line-height:1}
.blocked-tag svg{width:11px;height:11px}
.sheet-body{padding:20px}
.field{margin-bottom:15px}
/* two settings fields per row (turn client-settings forms); textarea/wide fields span both columns */
.fld2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));column-gap:20px}
.fld2>.field.span2{grid-column:1/-1}
@media (max-width:560px){.fld2{grid-template-columns:1fr}}
.field>label{display:block;font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--dim);font-weight:600;margin-bottom:7px}
.hint{color:var(--hint)}   /* bare settings-description hint: slightly darker than --ink on dark (scoped .field/.subpanel hints override) */
.field .hint{font-size:11.5px;color:var(--faint);margin-top:6px}
/* validation: invalid field + error hint */
input.bad,.bad{border-color:var(--dangling) !important;box-shadow:0 0 0 3px color-mix(in srgb,var(--dangling) 12%,transparent)}
.hint.err{color:var(--dangling)}
/* the middle severity: something is not ready YET (registering, not up) as against something that FAILED */
.hint.warnish{color:var(--fault)}
.advbad{color:var(--dangling);font-size:11px;font-weight:600;margin-left:6px}
/* per-user VK link field: an editable icon-box mirroring the sub-link box; amber (empty/invalid) → blue (set) */
.vkfield.warn .vkbox{border-color:#e0a85f;background:color-mix(in srgb,#e0a85f 8%,var(--bg))}
.vkfield.warn .vkbox > .ic{color:#e0a85f}
.vkfield.set .vkbox{border-color:#3b9eff;background:color-mix(in srgb,#3b9eff 9%,var(--bg))}
.vkfield.set .vkbox > .ic{color:#3b9eff}
.hint.vk-warn{color:#b5741a;line-height:1.5}
:root[data-theme="dark"] .hint.vk-warn{color:#e0a85f}
/* multi-VK-link manager (VkLinksSheet) — a CRUD grid of rows: [primary radio] [url box] [delete] */
.vklinks-grid{display:flex;flex-direction:column;gap:8px;margin:12px 0}
.vklinks-row{display:flex;align-items:center;gap:10px}
.vklinks-row.warn .vkbox{border-color:#e0a85f;background:color-mix(in srgb,#e0a85f 8%,var(--bg))}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .hint.vk-warn{color:#e0a85f}}
.inline{display:flex;gap:8px}.inline input{flex:1}
.advtoggle{background:none;border:0;color:var(--dim);font-size:12px;cursor:pointer;padding:2px 0;display:flex;align-items:center;gap:6px}
.advtoggle:hover{color:var(--ink)}
.adv{display:none}.adv.open{display:block;margin-top:6px}
.sheet-foot{display:flex;gap:10px;align-items:center;padding:16px 20px;border-top:1px solid var(--line)}
.sheet-foot .grow{flex:1}
.discard-msg{display:inline-flex;align-items:center;gap:7px;font-size:12.5px;color:var(--pending)}
.discard-msg svg{flex:none}
.formmsg{font-size:12.5px;font-family:var(--mono);min-height:16px;margin-top:10px}
.formmsg.err{color:var(--dangling)} .formmsg.ok{color:var(--online)} .formmsg.work{color:var(--dim)} .formmsg.warn{color:#F0913C}

/* ───── toasts ───── */
/* ⚠️ THE TOP LAYER, BY DEFINITION. A toast is what the panel says happened — a refused choice, a failed
   save — and it is worthless behind the thing that caused it. At z-index 70 it sat UNDER every popover
   (80) and every dropdown popup (90), so the refusal you triggered by clicking a row in an open list was
   posted behind that list. Above the QR zoom (9999) too: if a save fails while an enlarged code is open,
   that is exactly the moment the message matters. Nothing in this file may out-rank it. */
.toasts{position:fixed;right:20px;bottom:20px;z-index:10000;display:flex;flex-direction:column;gap:10px;max-width:360px}
.toast{background:var(--raise);border:1px solid var(--line-solid);border-left-width:3px;border-radius:10px;padding:12px 14px;
  font-size:13px;color:var(--ink);box-shadow:var(--shadow);display:flex;gap:10px;align-items:flex-start;animation:slidein .25s var(--ease)}
.toast.ok{border-left-color:var(--online)} .toast.err{border-left-color:var(--dangling)} .toast.info{border-left-color:var(--ready)}
.toast.warn{border-left-color:var(--partial)}   /* the save went through, but not all of it — §5.4's dropped-entries line */
.toast svg{flex:none;margin-top:1px}
@keyframes slidein{from{transform:translateX(22px);opacity:0}to{transform:none;opacity:1}}
.toast.out{animation:slideout .25s var(--ease) forwards}
@keyframes slideout{to{transform:translateX(22px);opacity:0}}

.loading{display:flex;align-items:center;justify-content:center;padding:60px;color:var(--dim);gap:12px;font-family:var(--mono);font-size:13px}
.spin{width:16px;height:16px;border:2px solid var(--line-solid);border-top-color:var(--brand);border-radius:50%;animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.ic-spin{display:inline-flex;animation:spin .8s linear infinite}

/* ───── responsive: collapse tables to cards ───── */
@media(max-width:680px){
  .appbar{gap:14px;padding:0 14px}.tabs a{padding:7px 11px}   /* keep just the panel version on small screens */
  .view{padding:16px 13px 70px}
  .statgrid{grid-template-columns:1fr 1fr}
  thead{display:none}
  tbody td{display:flex;justify-content:space-between;align-items:center;gap:14px;padding:8px 14px;border:0;text-align:right}
  tbody td::before{content:attr(data-label);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--dim);font-weight:600}
  tbody tr.clk{display:block;border:1px solid var(--line);border-radius:var(--r-sm);margin:10px 10px;background:var(--surface);padding:5px 0}
  .deploy-body{flex-direction:column;align-items:center}
}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}

/* ───────── nodes management screen ───────── */
/* nodes list: one full-width block per line, actions on the right */
.nodegrid{display:flex;flex-direction:column;gap:15px}
/* grid: title row spans the full width (so CPU load can reach over the controls);
   data row (col 1) and controls (col 2) share the second row, so they line up. */
/* 4-column grid: each line's items share columns so they align vertically — name/peers, mesh/interfaces,
   cpu/throughput, gear/trash. Turn-proxies span the middle columns on a third row. */
/* 7-column grid. Left cluster: gutter · name/peers · interfaces/turn (hug peers). Flex gap. Right cluster:
   mesh (hugs cpu) · cpu/throughput (right-aligned metric) · gear/trash. */
/* FIXED column widths so every card's data lines up at the same x (cross-card alignment): the name/peers
   column is fixed (name spans past it into the gap), so interfaces/turn always start at the same x; the right
   cluster (mesh · cpu/throughput · icons) is fixed and right-anchored, so it's identical across cards. */
.ncard{display:grid;grid-template-columns:16px 124px minmax(0,auto) minmax(14px,1fr) 148px 316px 28px;align-items:center;column-gap:14px;row-gap:15px;cursor:pointer;
  background:var(--surface);border:1px solid var(--line);border-radius:var(--r);overflow:hidden;transition:background .15s;padding:16px}
.ncard.removing{opacity:.82;border-style:dashed}
.ncard:hover{background:var(--surface-2)}
.nc-gutter{grid-column:1;display:flex;align-items:center;gap:6px}
.nc-name{grid-column:2 / 5;display:flex;align-items:center;gap:9px;min-width:0}   /* spans past the fixed peers col so long names+badges have room */
.nc-peers{grid-column:2}
.nc-ifaces{grid-column:3;min-width:0}          /* interfaces hug peers (start at a fixed x) */
.nc-turn{grid-column:3 / 7;min-width:0}         /* turn-proxies start by peers, room to the right */
.nc-mesh{grid-column:5;min-width:0;justify-self:end}   /* mesh sits at the right of its col, hugging cpu */
.nc-cpu.nm-item{grid-column:6;justify-self:end;gap:9px}    /* right-aligned in col6; label↔bar = 9 (needs .nm-item specificity to beat the generic .nm-item gap:8) */
/* CPU meter → per-vCPU bubble. The bar+number is always the hover target; the triangle is only an
   attention marker, shown when at least one vCPU is saturated. */
.cpupop{display:inline-flex;align-items:center;cursor:default}
.health-cols > .cpupop{display:block;min-width:0}      /* the Popover span is the grid item — keep the column shape */
.cpu-warn{display:inline-flex;align-items:center;margin-left:5px;color:var(--dangling)}
.cpu-warn svg{width:13px;height:13px}
.cpu-hot-n{color:var(--dangling);font-weight:600}
.deppop.cpu-bubble{min-width:264px;width:max-content}
.cpu-bubble .cpu-row{display:grid;grid-template-columns:auto 1fr 38px 58px 14px;align-items:center;gap:9px;padding:4px 8px;border-radius:6px;white-space:nowrap}
.cpu-bubble .cpu-row.hot .on-name{color:var(--dangling)}
.cpu-bubble .cpu-corebar{width:100%;min-width:56px}
.cpu-bubble .cpu-use{font-family:var(--mono);font-size:11px;text-align:right}
.cpu-bubble .cpu-idle{font-family:var(--mono);font-size:10px;color:var(--faint);text-align:right}
.cpu-bubble .cpu-sat{display:inline-flex;color:var(--dangling)}
.cpu-bubble .cpu-sat svg{width:12px;height:12px}
.nc-thru{grid-column:6;justify-self:end}   /* right-aligned so the upload "/s" sits exactly under the cpu number */
.nc-ctl{grid-column:7;flex:none}
.ncard .nname{font-family:var(--mono);font-size:15px;line-height:1;font-weight:600;color:var(--ink)}
.ncard:hover .nname{color:var(--brand)}
.upd-dot{display:inline-flex;align-items:center;color:#F0913C;flex:none}
.nm-ver{font-family:var(--mono);font-size:11px;color:var(--dim);display:inline-flex;align-items:center;gap:8px;margin-left:12px}
.nm-ver.out{color:#F0913C}
.spin.sm{width:10px;height:10px;border-width:1.5px}
.ncard .grow{flex:1}
.nstat{display:inline-flex;align-items:center;gap:6px;line-height:normal;font-size:10px;letter-spacing:.05em;text-transform:uppercase;font-weight:700;font-family:var(--mono)}   /* line-height:normal matches .reporting + the .tport pills so every status tag's label sits on the same baseline */
.nstat::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor}
.nstat.online{color:var(--online)} .nstat.online::before{animation:pulse 2.6s infinite}
.nstat.offline{color:var(--unknown)} .nstat.offline::before{display:none}   /* offline: GREY, info icon (no dot) */
.nstat.dangling{color:var(--pending)}
.nstat.stale{color:var(--unknown)} .nstat.stale::before{display:none}      /* stale / no fresh snapshot: GREY, info icon */
.nstat.down{color:var(--dangling)} .nstat.down::before{display:none}       /* down on the node: RED, warn icon */
.nstat.stopped{color:var(--unknown)} .nstat.stopped::before{display:none}  /* stopped by operator: GREY, stop icon */
.nstat.removing{color:var(--dangling)} .nstat.removing::before{display:none}  /* flagged for removal: RED, trash icon */
.nstat.procbusy{color:var(--partial)} .nstat.procbusy::before{display:none}        /* active work (re-installing / updating / installing): YELLOW */
.nstat.procconvert{color:var(--pending)} .nstat.procconvert::before{display:none}  /* converting: PURPLE */
.nstat.procuninstall{color:var(--dangling)} .nstat.procuninstall::before{display:none}  /* uninstalling: RED */
.nstat.enroll{color:#F0913C} .nstat.enroll::before{display:none}   /* awaiting enroll: ORANGE clock (node hasn't checked in yet) */
.nstat.uninst{color:var(--unknown)} .nstat.uninst::before{display:none}   /* uninstalled: GREY (kept for re-install; recover with the same token) */
/* migrated: the OLD box is still alive and still serving its peers, locked out of the panel by a rotated
   token and nothing else. YELLOW because it is a half-finished state, not a fault — and on the node's own
   page the tag is a BUTTON (the rollback / "it's gone" door), because a badge nobody can act on stops
   being read. The button resets only the UA chrome; its colour still comes from .nstat.superseded. */
.nstat.superseded{color:var(--partial)} .nstat.superseded::before{display:none}
/* T-10: the same badge one operation over. PENDING borrows .superseded — both mean "there is a live
   thing out there this panel no longer fully owns". DONE is neutral rather than green: the node moving
   away is a completed fact, not a health state, and colouring it like an OK would read as an all-clear
   about a node this panel has stopped controlling. */
.nstat.xferdone{color:var(--unknown)} .nstat.xferdone::before{display:none}
button.nstat.asbtn{background:none;border:0;padding:0;cursor:pointer;font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.05em;text-transform:uppercase}
button.nstat.asbtn:hover{text-decoration:underline}
.nstat.procok{color:var(--online)} .nstat.procok::before{display:none}        /* success: green ✓, auto-clears after ~5s (no ×) */
.nstat.procaborted{color:var(--unknown)} .nstat.procaborted::before{display:none}   /* aborted: grey, dismissable via × */
.nstat.procfail{color:var(--dangling)} .nstat.procfail::before{display:none}   /* failed: red, error popup + dismissable × */
.nstat .xbtn{margin-left:1px;padding:1px}
/* data row: four fixed-start columns so peers/interfaces/turn/throughput line up across
   cards regardless of content. Peers fits 555/999, interfaces ~two 8-char tags, turn takes
   the rest, throughput (no label) sits at the right with down/up each a fixed width. */
/* Peers on the left; to its right two rows — interfaces (+throughput, right) then turn-proxies (+controls,
   right). Tags wrap to new lines when there are too many; the right-hand item stays pinned top-right. */
.nmeta{display:grid;grid-template-columns:118px 1fr;gap:10px 18px;align-items:start}
.nm-item{display:flex;align-items:baseline;gap:8px;min-width:0}   /* stack the control icons vertically at the card's right */
.nm-l{font-family:var(--mono);font-size:9px;text-transform:uppercase;letter-spacing:.08em;color:var(--faint);white-space:nowrap}
.nm-v{font-family:var(--mono);font-size:12.5px;color:var(--ink-2)}
.nm-v.nm-peers{font-size:15px;color:var(--ink)}
.nm-v.nm-peers small{font-size:.72em;color:var(--faint)}
.nm-v .down{color:var(--online)} .nm-v .up{color:var(--rate-up)}
/* idle throughput greys out like every other rate readout (rateCell/xferCell) — colour only once bytes move */
.nm-v.thru.idle .down,.nm-v.thru.idle .up{color:var(--faint)}
.nm-thru{display:flex;align-items:baseline;gap:8px}   /* throughput numbers, right-aligned; gap matches CPU load's label↔value spacing */
.nm-thru .thru{display:flex;gap:16px}
.nm-thru .down,.nm-thru .up{min-width:74px;white-space:nowrap;text-align:right}
.nm-cpu{display:flex;align-items:center;gap:5px;min-width:0}   /* bar↔value-box = 5 → bar↔value-number ≈ 6 (2px more than the 4px label↔bar gap) */
.nm-cpu .hm-bar{width:204px;flex:none}   /* long bar; right edge sits ~6px off the cpu number, left edge 9px off the CPU LOAD title (title stays 3px-left-shifted) */
/* reserve room for up to 333.33 and right-align, so the bar->value gap shrinks as the number grows */
.nm-cpu .nm-v{flex:none;min-width:31px;text-align:right}
@media(max-width:680px){.nmeta{grid-template-columns:1fr}}
.btn-danger{background:color-mix(in srgb,var(--dangling) 10%,transparent);color:var(--dangling);border:1px solid color-mix(in srgb,var(--dangling) 32%,transparent);padding:5px 11px;font-size:12px;border-radius:7px;cursor:pointer;transition:all .15s var(--ease)}
/* Set expiry / Reset expiry — same size + outlined style as .btn-danger (Block) so the footer trio matches; neutral when no expiry, orange once one is set. */
.btn-exp{background:transparent;color:var(--dim);border:1px solid var(--line-solid);padding:5px 11px;font-size:12px;border-radius:7px}
.btn-exp:hover{color:var(--ink);border-color:color-mix(in srgb,var(--dim) 45%,var(--line-solid))}
.btn-exp.on{color:#e0a85f;background:color-mix(in srgb,#e0a85f 10%,transparent);border-color:color-mix(in srgb,#e0a85f 34%,transparent)}
.btn-exp.on:hover{background:color-mix(in srgb,#e0a85f 16%,transparent);border-color:color-mix(in srgb,#e0a85f 46%,transparent)}
.btn-danger:hover{background:color-mix(in srgb,var(--dangling) 20%,transparent)}
.btn-danger:disabled{opacity:.5;cursor:not-allowed}
.btn-warn{background:color-mix(in srgb,#C0821A 11%,transparent);color:#C0821A;border:1px solid color-mix(in srgb,#C0821A 34%,transparent);padding:5px 11px;font-size:12px;border-radius:7px;cursor:pointer;transition:all .15s var(--ease);display:inline-flex;align-items:center;gap:5px}
.btn-warn .ic svg{width:13px;height:13px}
.btn-warn:hover{background:color-mix(in srgb,#C0821A 20%,transparent)}
.btn-warn:disabled{opacity:.5;cursor:not-allowed}
:root[data-theme="dark"] .btn-warn{color:#EBB43C;border-color:color-mix(in srgb,#EBB43C 34%,transparent)}
.btn-ok{background:color-mix(in srgb,#2E9E52 11%,transparent);color:#2E9E52;border:1px solid color-mix(in srgb,#2E9E52 34%,transparent);padding:5px 11px;font-size:12px;border-radius:7px;cursor:pointer;transition:all .15s var(--ease);display:inline-flex;align-items:center;gap:5px}
.btn-ok .ic svg{width:13px;height:13px}
.btn-ok:hover{background:color-mix(in srgb,#2E9E52 20%,transparent)}
.btn-ok:disabled{opacity:.5;cursor:not-allowed}
:root[data-theme="dark"] .btn-ok{color:#43C46E;border-color:color-mix(in srgb,#43C46E 34%,transparent)}
.confirm-type b.ct-learn{color:#C0821A}
:root[data-theme="dark"] .confirm-type b.ct-learn{color:#EBB43C}
.confirm-type b.ct-all{color:var(--dangling)}
/* one-time token + enrollment command */
.cmdrow{display:flex;gap:8px;align-items:stretch}
.cmdrow .tokenbox{flex:1}
.tokenbox{font-family:var(--mono);font-size:12.5px;color:var(--ink);background:var(--bg);border:1px solid var(--line-solid);border-radius:var(--r-sm);padding:11px 13px;word-break:break-all;user-select:all;line-height:1.55}
/* A config BLOCK rather than a one-liner (the NixOS enrolment snippet): it keeps its own line breaks,
   and a line too long for the sheet scrolls INSIDE the box. `min-width:0` is the load-bearing half —
   a flex item will not shrink below its max-content width without it, so one long line pushes the
   whole modal wider instead. Measured on the enrolment sheet: 638 → 748px, and back to 638 with this. */
.tokenbox.block{white-space:pre;word-break:normal;min-width:0;overflow-x:auto}
.copyaction{flex:none;background:var(--surface-2);border:1px solid var(--line-solid);border-radius:var(--r-sm);color:var(--dim);cursor:pointer;display:flex;align-items:center;gap:6px;font-size:12px;padding:0 13px;white-space:nowrap;transition:all .15s var(--ease)}
.copyaction:hover{color:var(--ink);border-color:var(--brand)}
.copyaction svg{flex:none}
/* node name + inline colour swatch (Node settings / Add node) — the swatch opens the native palette */
.namerow{display:flex;align-items:center;gap:9px}
.namerow input:not(.tf-color){flex:1}
.namerow .tf-color{width:34px;height:34px}

/* node health meters */
.health{display:flex;flex-direction:column;gap:12px}
/* CPU / Memory / Disk side by side — each takes a third */
.health-cols{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(0,1fr);gap:18px}   /* one equal column per meter — only BOUNDED percentages get a bar (cpu/mem/disk). Load is unbounded and system-wide, so it is a plain number in the CPU bubble, never a clamped bar. */
.hcol{display:flex;flex-direction:column;gap:6px;min-width:0}
.hcol-top{display:flex;justify-content:space-between;align-items:baseline;gap:8px}
.hcol-l{font-family:var(--mono);font-size:9px;text-transform:uppercase;letter-spacing:.08em;color:var(--faint);white-space:nowrap}
.hcol-v{font-family:var(--mono);color:var(--ink-2);font-size:11px;white-space:nowrap}
.health-hist{position:relative}
.health-hist .hist-cap{font-family:var(--mono);font-size:9px;text-transform:uppercase;letter-spacing:.1em;color:var(--faint);display:block;margin-bottom:3px}
.harea{display:block;width:100%}
.harea-wrap{position:relative;width:100%}
.ch-guide{position:absolute;top:0;bottom:0;width:1px;background:rgba(231,238,246,.28);pointer-events:none}
.ch-dot{position:absolute;width:7px;height:7px;border-radius:50%;border:1.5px solid var(--surface);transform:translate(-50%,-50%);pointer-events:none}
.ch-tip{position:absolute;top:2px;background:var(--raise);border:1px solid var(--line-solid);border-radius:6px;padding:3px 8px;font-size:11px;font-family:var(--mono);color:var(--ink);white-space:nowrap;pointer-events:none;z-index:6;box-shadow:0 4px 14px rgba(0,0,0,.4)}
.harea-empty{font-family:var(--mono);font-size:10.5px;color:var(--faint);padding:6px 0}
.spark{flex:none;opacity:.85}
.hm-bar{height:5px;border-radius:3px;background:var(--track);overflow:hidden}
.hm-fill{display:block;height:100%;border-radius:3px;background:var(--online);transition:width .6s var(--ease)}
.hm-fill.warn{background:var(--fault)}
.hm-fill.hot{background:var(--dangling)}
.halerts{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:2px}
.halert{display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:600;padding:2px 8px;border-radius:99px}
.halert svg{flex:none;width:12px;height:12px}
.halert.hot{background:color-mix(in srgb,var(--dangling) 14%,transparent);color:var(--dangling)}
.halert.warn{background:rgba(232,192,75,.14);color:var(--pending)}
.actlist{display:flex;flex-direction:column;gap:1px;border:1px solid var(--line);border-radius:var(--r);overflow:hidden}
.act-row{display:flex;align-items:center;gap:10px;padding:9px 13px;background:var(--surface);text-decoration:none;color:inherit;transition:background .12s}
.act-row:hover{background:var(--surface-2)}
.act-row .name{color:var(--ink-2);font-size:13px}
.act-row .when{font-family:var(--mono);font-size:11.5px;color:var(--faint)}
/* Live screen — Peers/Users mode toggle (segmented switch: active Peers = green, active Users = blue) */
.pmode{display:inline-flex;flex:none;border:1px solid var(--line-solid);border-radius:9px;overflow:hidden;background:var(--surface-2);height:36px}
.pm-opt{appearance:none;border:none;background:none;cursor:pointer;font:inherit;font-size:12.5px;font-weight:600;padding:0 16px;color:var(--dim);transition:background .12s var(--ease),color .12s var(--ease);white-space:nowrap}
.pm-opt:hover{color:var(--ink)}
.pm-peers.on{background:color-mix(in srgb,var(--online) 20%,transparent);color:var(--online)}
.pm-users.on{background:color-mix(in srgb,var(--ready) 20%,transparent);color:var(--ready)}
/* Online filter — a single click-to-toggle pill (cyan when on, grey when off) instead of a checkbox */
.onlbtn{appearance:none;flex:none;height:36px;padding:0 18px;border:1px solid var(--line-solid);border-radius:9px;background:var(--surface-2);color:var(--dim);font:inherit;font-size:12.5px;font-weight:600;cursor:pointer;transition:background .12s var(--ease),color .12s var(--ease),border-color .12s var(--ease)}
.onlbtn:hover{color:var(--ink)}
.onlbtn.on{background:color-mix(in srgb,var(--brand) 20%,transparent);color:var(--sel-ink);border-color:color-mix(in srgb,var(--brand) 45%,transparent)}
/* endpoint cell: "Local turn-proxy" (fork-coloured) — hover shows the same Connected-via bubble as the dot */
.peergrid .turnep{cursor:help}
.turnep.off{opacity:.65;font-style:italic}   /* offline: it WAS relayed — attribution kept, just muted */
.condot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--unknown)}
/* online / turn dots glow with the same animated pulse as the peer-grid status badge (.sdot) */
.condot.on{background:var(--online);color:var(--online);animation:sdotpulse 1.6s ease-in-out infinite}
.condot.turn{background:var(--tfc);color:var(--tfc);animation:sdotpulse 1.6s ease-in-out infinite}
.condot.faulty{background:var(--fault);color:var(--fault);animation:sdotpulse 1.6s ease-in-out infinite}   /* orange, pulsing — handshake up but no data */
.condot.blocked{background:var(--fault)}   /* orange, static — reaching but no handshake */
/* online, but resolving over encrypted DNS: connected (so it pulses) yet not category-routed (so it is
   amber, not green) — the live view's equivalent of the grid's amber status pill. */
.condot.uncat{background:var(--partial);color:var(--partial);animation:sdotpulse 1.6s ease-in-out infinite}
.condot.off{background:var(--faint)}

/* ───── inline metadata tags (dense, colored row signature) ───── */
.tags{display:inline-flex;gap:4px;flex-wrap:wrap;margin-left:9px;vertical-align:middle}
.tg{display:inline-flex;align-items:center;font-family:var(--mono);font-size:9px;font-weight:500;text-transform:uppercase;
  letter-spacing:.04em;padding:2px 5px;border-radius:4px;line-height:1.45;white-space:nowrap}
.tg-wg{background:rgba(63,216,154,.13);color:var(--online)}
.tg-awg{background:rgba(31,200,214,.14);color:var(--brand)}
.tg-wdtt{background:rgba(129,181,18,.16);color:#81B512}   /* WDTT type tag; applyThemeColors injects the tunable colour */
.tg-csqtt{background:rgba(249,115,22,.16);color:#F97316}   /* csqtt type tag; applyThemeColors injects the tunable colour */
.tg-turn{color:var(--tfc,#B49BF7);background:color-mix(in srgb,var(--tfc,#A78BF5) 17%,transparent)}
/* A protocol tag that is also the publish switch for that kind of config (peer view / edit / targets).
   It has to keep reading as a tag first — a dotted underline on hover is the only affordance, and OFF is
   the loud state: struck through, greyed, with an eye-off glyph, because that's the state you can't infer. */
.pubtg{border:0;cursor:pointer;font:inherit;font-family:var(--mono);font-size:9px;font-weight:500;gap:3px}
.pubtg:hover{box-shadow:inset 0 -1px 0 currentColor}
.pubtg:focus-visible{outline:2px solid var(--brand);outline-offset:1px}
.pubtg.off{background:color-mix(in srgb,var(--unknown) 13%,transparent)!important;color:var(--unknown)!important;text-decoration:line-through}
/* the hidden-marker sits INSIDE the chip, so it has to ride the text's centre line — not its baseline */
.pubtg.off .ic{align-self:center;line-height:0}
.pubtg.off svg{width:9px;height:9px;display:block;text-decoration:none}
.pubtg.lone{cursor:default}
.pubtg.lone:hover{box-shadow:none}
/* not currently connected: the tag keeps its protocol colour (grey said "unknown kind", which it isn't) and
   just reads quieter. Full strength on hover, so it is still legible while you are aiming at it. */
/* Offline: a fade, not a disappearance — .5 read as broken rather than idle. Applies to a plain tag too, so the
   interface badge and the turn badge beside it fade together. NOT stacked on .off: a source the operator has
   switched off the subscription page already says so with its own grey + strikethrough, and dimming that as well
   makes two different states look like one. */
.pubtg.dim,.tg.dim{opacity:.72}
.pubtg.dim.off{opacity:1}
.pubtg.dim:hover{opacity:1}
/* the turn front-badge doubling as a publish switch: it is a <button>, so re-assert the chip layout */
.tgt-front.pubtg{display:inline-flex;align-items:center}
.tgt-front .turnx{margin-left:4px;font-weight:600;opacity:.85}
/* RAW-IP mode is on: amber, the same register as every other "this needs your attention" tag — it trades
   WireGuard's handshake for throughput, so it never reads as a neutral capability chip. */
.tg-raw{background:color-mix(in srgb,#FACC15 15%,transparent);color:#FACC15}   /* RAW = a qWDTT capability → the qWDTT fork colour; applyForkColors re-injects the operator's pick */
.rawport{color:#FACC15;font-weight:500}   /* the RAW port riding along in a Listen row — same hue as .tg-raw */
/* The RAW note under a peer's config — a FOOTNOTE, below the QR row, never inside it. It sat in
   .deploy-body (a flex row) with white-space:nowrap, which made it an unshrinkable sibling and collapsed
   the QR column to 0px: the note appeared to have replaced the config. It wraps, and it stays out of that row. */
/* the general turn-proxy accent (matches the turn-section panel icon .panel-ic.t-turn) */
.tf-gen{--tfc:#7C5CFF}
/* collapsed "turn ×N" badge (>3 proxies forward to one iface): general colour + a highlighted, ringed area.
   Ring is INSET (not an outward glow) so .ifcard-top's overflow:hidden can't clip it at the card's right edge. */
.turncollwrap{display:inline-flex;cursor:pointer}
.tg-turn.turncoll{gap:4px;margin-right:1px;background:color-mix(in srgb,var(--tfc) 24%,transparent);
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--tfc) 60%,transparent)}
.tg-turn.turncoll .turnx{font-weight:700;font-size:.92em;letter-spacing:.02em;opacity:.9}
.turncollwrap.on .tg-turn.turncoll{background:color-mix(in srgb,var(--tfc) 32%,transparent)}   /* brighten while the bubble is open */
/* the hover bubble listing each forwarding proxy — fork badge (own colour) + optional title, no header */
.deppop.turnlist{min-width:0;width:max-content;padding:7px;display:flex;flex-direction:column;gap:5px}
.turnlist-row{display:flex;align-items:center;gap:8px;white-space:nowrap}
.turnlist-row.muted{opacity:.5}
.turnlist-ttl{font-size:11.5px;color:var(--dim)}
/* "Connected via <proxy>" hover bubble on a turn-proxied interface badge */
.turnwrap{position:relative;display:inline-flex;align-items:center;gap:5px}
.turnbub-t{color:var(--ink);font-weight:600}
/* turn-connected status badge: fork colour + a glowing animated dot in place of the normal status dot */
.badge.b-turn{background:color-mix(in srgb,var(--tfc) 13%,transparent);color:var(--tfc);border-color:color-mix(in srgb,var(--tfc) 32%,transparent)}
/* online + turn badges use a glowing animated dot (.sdot, in the badge's own colour) instead of the ::before dot / check */
.badge.b-online::before,.badge.b-turn::before{display:none}
.badge .sdot{width:6px;height:6px;border-radius:50%;background:currentColor;flex:none;animation:sdotpulse 1.6s ease-in-out infinite}
@keyframes sdotpulse{0%,100%{box-shadow:0 0 5px currentColor,0 0 2px currentColor;opacity:1}50%{box-shadow:0 0 9px currentColor,0 0 3px currentColor;opacity:.7}}
.turnbub{position:absolute;top:calc(100% + 7px);left:0;white-space:nowrap;
  background:var(--surface-2);border:1px solid var(--line-solid);border-radius:8px;padding:6px 9px;font-size:11px;
  color:var(--dim);display:none;align-items:center;gap:6px;z-index:40;box-shadow:0 6px 22px rgba(0,0,0,.45);text-transform:none;letter-spacing:0}
.turnwrap:hover .turnbub{display:inline-flex}
/* transparent bridge over the gap so moving from the tag onto the bubble keeps it open (bubble is a
   descendant of .turnwrap, so hovering the bridge keeps .turnwrap:hover true) */
.turnbub::before{content:"";position:absolute;left:0;right:0;top:-9px;height:9px}
/* blocked/faulty "why" bubble — same surface as the turn bubble but allowed to wrap onto a couple of lines,
   with a colour-headed first line (the status name) over the explanation. */
.turnbub.statusbub{white-space:normal;flex-direction:column;align-items:flex-start;gap:3px;width:250px;line-height:1.45}
.statusbub-h{display:inline-flex;align-items:center;gap:5px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;font-size:10.5px}
.statusbub-h svg{width:12px;height:12px}
/* status pills (busy/ok/warn/del) — ONE uniform shape; colour per state. Defined after .tg so the
   pill shape wins even when written as "tg tg-busy" on cards (the type tags .tg-wg/.tg-awg stay small). */
/* status pills — ONE shape/size/font for all; only colour differs (busy=cyan, ok=green, warn=yellow, del=red, off=grey) */
.tg-busy,.tg-ok,.tg-warn,.tg-del,.tg-off,.tg-pending,.tg-convert,.tg-ready{display:inline-flex;align-items:center;gap:5px;border-radius:99px;padding:1px 8px;font-family:var(--mono);font-size:9.5px;font-weight:700;line-height:1.45;text-transform:uppercase;letter-spacing:.04em;white-space:nowrap;border:1px solid transparent}
.tg-busy{background:rgba(232,192,75,.16);color:var(--partial);border-color:color-mix(in srgb,var(--partial) 32%,transparent)}   /* active work: installing / creating — YELLOW */
.tg-pending{background:rgba(240,145,60,.13);color:#F0913C;border-color:color-mix(in srgb,#F0913C 30%,transparent)}   /* queued / awaiting the node — ORANGE */
.tg-convert{background:color-mix(in srgb,var(--pending) 16%,transparent);color:var(--pending);border-color:color-mix(in srgb,var(--pending) 32%,transparent)}   /* converting bare↔docker — PURPLE */
.tg-ready{background:color-mix(in srgb,var(--ready) 13%,transparent);color:var(--ready);border-color:color-mix(in srgb,var(--ready) 30%,transparent)}   /* just came up — BLUE (5s flash, then no tag) */
.tg-busy.del{background:rgba(240,90,90,.15);color:var(--dangling);border-color:color-mix(in srgb,var(--dangling) 38%,transparent)}
.tg-busy.warn{background:rgba(232,192,75,.16);color:var(--partial);border-color:color-mix(in srgb,var(--partial) 38%,transparent)}
.tg-ok{background:rgba(63,216,154,.16);color:var(--online);border-color:color-mix(in srgb,var(--online) 30%,transparent)}
/* bare traffic/cascade badges — coloured icon + text, no pill background */
.egb{display:inline-flex;align-items:center;gap:4px;font-family:var(--mono);font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;line-height:1;white-space:nowrap}
.egb svg{width:13px;height:13px}
.egb-fwd{color:var(--brand)}              /* smart / → node — cyan */
.egb-direct{color:#5B8CFF}                /* direct — blue */
.egb-cascade{color:var(--pending)}        /* cascade — Node-connections icon colour (purple) */
.egb-smart{color:var(--smart)}            /* smart routing — rose/pink */
/* exit DEVICE — deliberately the same blue as `direct`, because that is what it is: this node putting the
   traffic on the internet, just by another road. The two families the eye needs to separate are "this node
   exits" (blue) from "another node exits" (cyan/purple), and the icon + the exit's own name carry the rest.
   Its own class so decision 14's per-exit colour has somewhere to land without inventing a hue now. */
.egb-exit{color:#5B8CFF}
.egb-gone{color:var(--unknown)}           /* …and the exit it named is gone: stated, never shown as `direct` */
/* on a node-connection card, the cascade badge + status dot sit at the TOP of the header (flex-start) so the
   dot is equidistant from the card's top and right borders (both = the card padding), not centred on the name */
.ifcard.tp .ifcard-top .egb,.ifcard.tp .ifcard-top .lkdot{align-self:flex-start}
.carry-tags{display:inline-flex;flex-wrap:wrap;gap:4px;justify-content:flex-end}   /* Carrying: forwarded interface tags, right-aligned */
/* connection-modal Forwarding / smart-cascade list */
.fwd-head{display:flex;align-items:center;gap:7px;margin-bottom:8px;font-family:var(--mono);font-size:12px}
.fwd-to{color:var(--faint)}
.fwd-ifaces{display:flex;flex-wrap:wrap;gap:5px}   /* cascade: all forwarded ifaces on one line */
.fwd-list{display:flex;flex-direction:column;gap:7px}   /* smart cascade: one iface + its rules per line */
.fwd-row{display:flex;align-items:center;gap:7px;font-family:var(--mono);font-size:12px;color:var(--dim);flex-wrap:wrap}

/* ── Panel settings screen: sticky Back/Save header + left section rail + content pane ── */
.setscreen{max-width:1000px}
.sethead{display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-right:4px}   /* Save sits ~4px inside the content-card right edge */
.sethead .grow{flex:1}   /* push Back/Save to the right; the two grows centre the saved-flash between title and buttons */
.savedflash{color:var(--online);font-weight:600;font-size:13px;white-space:nowrap;display:inline-flex;align-items:center;gap:6px}
.savedflash svg{width:15px;height:15px}
.sethead b{font-size:18px;letter-spacing:-.01em}
.setbody{display:flex;gap:20px;align-items:flex-start}
.setrail{flex:none;width:188px;display:flex;flex-direction:column;gap:2px;position:sticky;top:16px}
.setrail-i{display:flex;align-items:center;justify-content:space-between;text-align:left;background:none;border:0;color:var(--faint);font:inherit;font-size:13px;padding:8px 12px;border-radius:8px;cursor:pointer;transition:background .12s,color .12s}
/* save-confirmation modal: the list of modified values + the reprovisioning warning */
.savediff-h{font-weight:600;margin-bottom:8px}
.savediff ul{margin:0;padding-left:18px;display:flex;flex-direction:column;gap:5px}
.savediff li{font-size:13px;color:var(--ink)}
.savediff-w{margin-top:12px;padding-top:10px;border-top:1px solid var(--line-solid);color:var(--partial);font-size:12.5px;line-height:1.5}
.savediff-note{margin-top:10px;color:var(--muted);font-size:12px;line-height:1.5}
.setrail-i:hover{color:var(--ink);background:color-mix(in srgb,var(--ink) 4%,transparent)}
.setrail-i.on{color:var(--ink);background:color-mix(in srgb,var(--brand) 16%,transparent);font-weight:600}
.setpane{flex:1;min-width:0;position:relative}   /* anchors the node-badge bar to the card's top-right */
.setpane .card{max-width:none}
.georefresh{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin-top:16px}
/* Geo data → list providers + update schedule */
.provlist{display:flex;flex-direction:column;gap:6px;margin-bottom:6px}
.provrow{display:flex;align-items:center;gap:12px;padding:7px 11px;border:1px solid var(--line);border-radius:9px;background:color-mix(in srgb,var(--ink) 2%,transparent)}
/* disabled row: dim the content but NOT the colour swatch (.tswrow) — opacity on the whole row would wash out its
   picker + preview bubbles, so exclude them so the colours stay solid/visible */
.provrow.off > :not(.tswrow):not(.prov-swro){opacity:.55}
.cl-row.off > :not(.tswrow){opacity:.55}
.bprov-meta{display:flex;flex-direction:column;align-items:flex-start;gap:2px;min-width:0;flex:1 1 auto}
.bprov-top{display:flex;align-items:center;gap:9px;min-width:0}
.bprov-note{font-size:11.5px;color:var(--dim);line-height:1.3;white-space:normal}
.bprow{align-items:center}
.prov-name{font-size:13.5px;font-weight:600;color:var(--ink);text-decoration:none;white-space:nowrap}
.prov-name:hover{color:var(--brand)}
.prov-tiers{display:flex;gap:4px}
.prov-upd{font-size:11px;color:var(--dim);font-variant-numeric:tabular-nums}
.prov-upd.never{opacity:.6;font-style:italic}
/* Curated has no switch (§6.5) — this holds its column so every row's swatch stays on one line. */
.prov-nosw{width:34px;height:19px;flex:none}
/* "in use" — a rule names this list, so the node holds it whether or not it is pinned (§1.4). Quiet: it is a
   statement of fact beside the title, not a status to scan for. */
.lg-inuse{font-size:10.5px;color:var(--dim);border:1px solid var(--line);border-radius:5px;padding:1px 5px;white-space:nowrap}
.fleet-note{font-size:10.5px;color:var(--dim);white-space:nowrap}
.prov-always{font-size:11px;color:var(--dim);white-space:nowrap}   /* stands where "updated 3h ago" does on a fetched provider */
/* "used by 3 rules on 2 interfaces" — the blast radius of the switch to its left. --dim, matching .prov-upd
   beside it rather than the quieter --faint of the repo link: this is a figure read before a decision. */
.prov-use{font-size:11px;color:var(--dim);font-variant-numeric:tabular-nums;white-space:nowrap}
.prov-st{display:inline-flex;align-items:center;gap:4px;font-size:11.5px;font-weight:600;white-space:nowrap}
.prov-st .ic svg{width:13px;height:13px}
.prov-st.upd{color:var(--dim)}
.prov-st.ok{color:var(--online)}
.prov-st.err{color:var(--partial)}
.prov-repo{font-family:var(--mono);font-size:11.5px;color:var(--faint);white-space:nowrap;text-decoration:none}   /* GitHub repo link on a routing-list provider row */
.prov-repo:hover{color:var(--ready);text-decoration:underline}
.provrow .tf-arrow{display:inline-flex;animation:spin 1s linear infinite}
.provrow .tf-arrow .ic svg{width:13px;height:13px}
.schedrow{display:flex;gap:18px;flex-wrap:wrap;align-items:flex-start}
.schedrow .field{flex:1;min-width:180px}
.timein{width:100%;box-sizing:border-box;padding:8px 10px;font-family:var(--mono);font-size:13px;color:var(--ink);background:var(--bg);border:1px solid var(--line-solid);border-radius:7px}
.timein:disabled{opacity:.5}
.cllist{display:flex;flex-direction:column;gap:6px}
.cl-row{display:flex;align-items:center;gap:10px;padding:7px 11px;border:1px solid var(--line);border-radius:9px;background:color-mix(in srgb,var(--ink) 2%,transparent)}
.cl-row .grow{flex:1}
.tf-name{font-weight:600;color:var(--ink);font-family:var(--mono);font-size:12.5px;text-decoration:none;cursor:pointer}
.tf-name:hover{text-decoration:underline dotted;text-underline-offset:3px}
/* version chip — tinted with the fork's picker colour (--tfc, set on .tf-verwrap) so the picker visibly drives it */
.tf-verwrap{position:relative;display:inline-flex}
.tf-ver{font-family:var(--mono);font-size:11px;color:var(--tfc,var(--ready));background:color-mix(in srgb,var(--tfc,var(--ready)) 13%,transparent);padding:1px 7px;border-radius:99px;white-space:nowrap;cursor:default;border:1px solid color-mix(in srgb,var(--tfc,var(--ready)) 22%,transparent)}
.tf-ver.none{color:var(--faint);background:none;font-style:italic}
/* held: the fork is pinned/rolled back — same fork-colour fill as a normal version chip, but the held (warn) border
   and a RED block icon so it reads as "pinned, won't auto-update" */
.tf-ver.held{display:inline-flex;align-items:center;gap:3px;border-color:color-mix(in srgb,var(--warn) 55%,transparent)}
.tf-ver.held svg{width:11px;height:11px;color:var(--dangling)}
/* per-node version inside the hover bubble */
.tf-vg-ver{margin-left:auto;padding-left:10px;font-family:var(--mono);font-size:10px;color:var(--muted);display:inline-flex;align-items:center;gap:3px;white-space:nowrap}
.tf-vg-ver.held{color:var(--warn)}
.tf-vg-ver.held svg{width:10px;height:10px;color:var(--dangling)}
.fvp-pend{display:inline-flex;align-items:center;gap:3px;font-size:10px;color:var(--warn)}
.fvp-pend svg{width:11px;height:11px}
/* fork Version & rollback (Server defaults modal): one block per node running the fork */
.fvp{display:flex;flex-direction:column;gap:10px}
.fvp-node{border:1px solid var(--line);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:8px}
.fvp-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.fvp-dot{width:8px;height:8px;border-radius:99px;flex:0 0 auto}
.fvp-nm{font-size:13px}
.fvp-ver{font-size:12px;color:var(--muted)}
.fvp-act{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
/* ⚠️ SAME TIE AS `.selwrap` ABOVE: `.dropdown{min-width:0}` is declared later, so a bare `.fvp-sel` loses
   and this control silently shrank from 260px to 150px when it stopped being a <select>. Qualified to
   (0,2,0), and it sits AFTER `.dropdown.selwrap` so it still wins the pair the caller passes together. */
.dropdown.fvp-sel{min-width:260px;max-width:100%}
/* hover bubble: the nodes running this fork (+ live updating/updated during an Update) */
.tf-verpop{position:absolute;top:calc(100% + 6px);left:0;z-index:40;min-width:max-content;display:none;flex-direction:column;gap:4px;
  padding:7px 9px;border:1px solid var(--line-solid);border-radius:9px;background:var(--surface-2);box-shadow:0 8px 26px rgba(0,0,0,.45);
  max-height:70vh;overflow-y:auto;scrollbar-color:var(--faint) transparent}   /* one row per node — a large fleet would otherwise run off the screen */
.tf-verpop::-webkit-scrollbar-thumb{background:var(--faint)}
.tf-verwrap:hover .tf-verpop{display:flex}
.tf-vg-node{display:flex;align-items:center;gap:6px;font-size:11.5px}
/* appbar panel version + info icon → the changelog bubble reuses the update bubble's .deppop.hostupd-bub + .hub-* look */
.appver-wrap{display:inline-flex;align-items:center;gap:4px}
.appver-info{display:inline-flex;align-items:center;color:var(--faint);position:relative;top:-0.5px}
.appver-wrap:hover .appver-info{color:var(--brand)}
.appver-info svg{width:12px;height:12px;display:block}
/* ‹ older / › newer buttons — grouped together just after the version number, vertically centred with it */
.verbub .hub-h{align-items:center}
.hub-nav-group{display:inline-flex;align-items:center;justify-content:center;gap:16px}
.hub-nav{appearance:none;border:1px solid var(--line);background:none;color:var(--fg);cursor:pointer;border-radius:6px;padding:2px 9px;font-size:11px;font-weight:600;line-height:1.4;flex:0 0 auto;display:inline-flex;align-items:center;gap:3px}
.hub-nav:hover:not(:disabled){border-color:var(--brand);color:var(--brand)}
.hub-nav:disabled{opacity:.3;cursor:default}
.hub-cur{font-family:var(--mono);font-size:9.5px;font-weight:600;color:var(--online);border:1px solid color-mix(in srgb,var(--online) 40%,transparent);border-radius:99px;padding:0 6px;margin-left:4px}
.tf-vg-dot{width:7px;height:7px;border-radius:50%;flex:none}
.tf-vg-nm{color:var(--ink);font-family:var(--mono);font-size:11px}
.tf-vg-st{margin-left:auto;display:inline-flex;align-items:center;gap:3px;font-size:10px;font-family:var(--mono);padding:0 5px;border-radius:99px}
.tf-vg-st svg{width:11px;height:11px}
.tf-vg-st.upd{color:#F0913C;background:color-mix(in srgb,#F0913C 15%,transparent)}
.tf-vg-st.ok{color:var(--online);background:color-mix(in srgb,var(--online) 15%,transparent)}
/* update-check status per fork */
.tf-chk{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-family:var(--mono);white-space:nowrap;color:var(--partial)}
.tf-chk.ok{color:var(--online)}
.tf-chk.upd{color:#F0913C}
.tf-chk.upd svg{width:13px;height:13px}
.tf-chk.ok svg{width:13px;height:13px}
button.tf-updbtn{background:none;border:none;cursor:pointer;padding:2px 5px;border-radius:6px;line-height:1}   /* clickable "update to …" — updates every deployed instance of this fork */
button.tf-updbtn:hover{background:color-mix(in srgb,#F0913C 15%,transparent)}
.tf-arrow{display:inline-flex;align-items:center;justify-content:center;width:13px;height:13px;line-height:0;color:var(--partial)}
.tf-arrow svg{display:block;width:13px;height:13px;transform-box:view-box;transform-origin:center;animation:spin .8s linear infinite}   /* spin the SVG on its viewBox centre (same pattern as .updspin / .iconbtn) so it's centred even on a fresh load */
/* which interface types the fork's client supports — wg always; awg only for the non-WG-only forks */
.cl-caps{display:inline-flex;align-items:center;gap:4px;flex:none;margin-left:-2px;cursor:help}
.warntext{color:var(--partial);font-weight:600}
/* per-fork colour picker (native input, themed to a small rounded swatch) */
.tf-color{width:24px;height:24px;flex:none;padding:0;border:1px solid var(--line-solid);border-radius:6px;background:none;cursor:pointer}
.tf-color::-webkit-color-swatch-wrapper{padding:2px}
.tf-color::-webkit-color-swatch{border:none;border-radius:4px}
.tf-color::-moz-color-swatch{border:none;border-radius:4px}
.cllist.dimmed{opacity:.5;pointer-events:none}
.tf-gear{flex:none;margin-left:4px}
/* per-OS platform buttons: fill = native(emerald)/crossplatform(sky) @30% · border = plain(red)/obfs(neutral) · icon = sidecar(yellow)/app(ink) */
.tf-plats{display:flex;align-items:center;gap:9px;margin-right:12px}
.tf-plat{width:32px;height:32px;flex:none;border-radius:8px;display:grid;place-items:center;border:1px solid var(--line-solid);background:transparent;color:var(--ink-2);cursor:pointer;padding:0;transition:transform .1s,box-shadow .1s}
.tf-plat:hover{transform:translateY(-1px);box-shadow:0 3px 8px -3px rgba(30,52,84,.4)}
.tf-plat .ic{display:grid;place-items:center;line-height:0}
.tf-plat .ic svg{width:20px;height:20px;display:block}
/* solid-neon fill by client kind (cli's hue wins over nat/cross for a CLI); plain = dark-red glyph; off = 25% ghosted grey tile */
.tf-plat.nat,.tf-plat.cross,.tf-plat.cli{color:#06121c;background:linear-gradient(158deg,color-mix(in srgb,var(--pc) 94%,#fff 6%),color-mix(in srgb,var(--pc) 72%,#000 14%));border:1px solid color-mix(in srgb,var(--pc) 50%,#fff 10%);box-shadow:0 4px 12px -5px color-mix(in srgb,var(--pc) 55%,transparent),inset 0 1px 0 color-mix(in srgb,#fff 40%,transparent)}
.tf-plat.nat{--pc:#14E39A}
.tf-plat.cross{--pc:#38CCF5}
.tf-plat.cli{--pc:#FFC531}
.tf-plat.plain{color:#9E2E1B}
.tf-plat.off{cursor:not-allowed;color:#06121c;background:linear-gradient(158deg,#9dabc0,#7a8698);border:1px solid #7f8ca0;box-shadow:none;opacity:.25}
.tf-plat.off:hover{transform:none;box-shadow:none}
/* the per-OS hover bubble is purely informational — never let it (or its .turnbub::before bridge) capture the
   pointer, so it can't block the buttons: it shows on button-hover and the pointer moves freely underneath. */
.tf-plbub{pointer-events:none}
/* a disabled fork dims its row via opacity — keep the platform GROUP at full opacity (so the hover bubble stays solid)
   and dim just the buttons instead; the bubble is a sibling of the button, so it reads clearly even when the fork is off */
.cl-row.off > .tf-plats{opacity:1}
.cl-row.off .tf-plat:not(.off){opacity:.5}
.cl-row.off .tf-plat.off{opacity:.2}
/* hover bubble on a platform button: app · by author / with <core> core, obfuscation type aligned right */
.tf-platwrap{display:inline-flex}
.tf-plbub{left:auto;right:18px;top:calc(100% - 9px);flex-direction:row;align-items:flex-start;gap:20px;white-space:nowrap;z-index:5}
.tf-plbub-l{display:flex;flex-direction:column;gap:2px}
.tf-plbub-app{color:var(--ink);font-weight:600}
.tf-plbub-by{color:var(--dim);font-weight:400}
.tf-plbub-core{color:var(--dim)}
.tf-plbub-obf{margin-left:auto;align-self:center;font-family:var(--mono);font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:#A974F2;background:color-mix(in srgb,#A974F2 16%,transparent);border-radius:4px;padding:2px 6px}
.tf-plbub-obf.plain{color:var(--dangling);background:color-mix(in srgb,var(--dangling) 15%,transparent)}
/* Server "Clients" sheet — equal-width OS segmented control (icon over label; greyed when the fork has no app for
   that OS) + the per-app config reference */
/* Per-OS app matrix — one two-line button per OS (icon · app name · author in author colour); hover bubble = tags +
   offer; click makes the OS active and expands its app list below. Wraps responsively (auto-fit) on narrow modals. */
.osapp-hd{font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--dim);font-weight:600;margin-bottom:8px}
.osapp{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px}
@media (max-width:640px){.osapp{grid-template-columns:repeat(auto-fit,minmax(120px,1fr))}}
.osapp-cell{position:relative;display:flex;align-items:center;gap:8px;padding:8px 16px 8px 9px;border:1px solid var(--line-solid);border-radius:10px;background:var(--surface);color:var(--ink);cursor:pointer;text-align:left;min-width:0;transition:border-color .12s,background .12s}   /* right pad clears the corner caret so a one-line name never underlaps it */
.osapp-cell:not(.off):hover{border-color:color-mix(in srgb,var(--brand) 40%,var(--line-solid))}
.osapp-cell.on{border-color:var(--brand);background:color-mix(in srgb,var(--brand) 9%,transparent)}
.osapp-cell.open{border-color:var(--brand)}
.osapp-cell.off{opacity:.4;cursor:not-allowed}
.osapp-ic{display:flex;align-items:center;justify-content:center;flex:none;color:var(--ink-2)}
.osapp-ic svg{width:22px;height:22px}
.osapp-cell.on .osapp-ic{color:var(--brand)}
.osapp-col{display:flex;flex-direction:column;min-width:0;gap:1px}
.osapp-name{font-size:11px;font-weight:700;white-space:nowrap}   /* app name always on ONE line (modal widened to fit the longest) */
.osapp-auth{font-size:9px;font-weight:600;white-space:nowrap}   /* author (+ core) always on ONE line */
.osapp-dim{color:var(--dim);font-weight:600}
.osapp-car{position:absolute;top:7px;right:8px;color:var(--muted);font-size:10px}   /* corner-anchored so it never eats the app-name's width */
.osapp-bub{position:absolute;z-index:60;left:50%;top:calc(100% + 8px);transform:translateX(-50%);display:none;flex-direction:column;gap:7px;width:max-content;max-width:min(90vw,520px);padding:10px 12px;background:var(--surface-2);border:1px solid var(--line-solid);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.24)}
.osapp-cell:first-child .osapp-bub{left:0;right:auto;transform:none}   /* edge cells anchor to the grid edge so a wide bubble never clips off the sheet */
.osapp-cell:last-child .osapp-bub{left:auto;right:0;transform:none}
.osapp-cell:not(.off):hover .osapp-bub{display:flex}
.osapp.menuopen .osapp-cell:not(.off):hover .osapp-bub{display:none}   /* a dropdown is expanded → suppress bubbles (out-specifies the hover rule) */
.osapp-bub .ce-badges{align-self:flex-end}                            /* tags on their own line, right-aligned */
.osapp-bub-txt{font-size:11px;line-height:1.45;color:var(--ink-2);white-space:nowrap}   /* text stays on ONE line; badges wrap to the line below */
.osapp-menu{margin-top:10px;background:var(--surface);border:1px solid var(--line-solid);border-radius:10px;padding:6px;display:flex;flex-direction:column;gap:1px}
.osapp-menu-hd{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--dim);font-weight:600;padding:4px 8px 6px}
/* styled OS dropdown (glyph + name), used in the turn-config modal — mirrors the Clients-sheet OS visuals */
/* space-between, NOT margin-left:auto on the app control. Identical while both fit on one line — Device left,
   App right — but margin-left:auto keeps pushing right after the row WRAPS, so the app control sat alone and
   right-aligned on a second line, staggered under the device. With space-between a wrapped line holds one item
   and starts it at the left, so the pair simply stacks. Language-proof: the Russian labels are longer than the
   English and wrap sooner, which is exactly when this mattered. */
.turncfg-devrow{display:flex;align-items:center;justify-content:space-between;gap:14px 20px;flex-wrap:nowrap;margin:0 0 12px}
.turncfg-devrow > .turncfg-os,.turncfg-devrow > .turncfg-app{min-width:0}
.turncfg-app .appdd{min-width:0;max-width:100%}
.turncfg-app .appdd .osdd-btn{width:210px;min-width:0;max-width:100%}
.turncfg-app .appdd .osdd-btn > *{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.turncfg-devrow .turncfg-os{margin:0}
.turncfg-os{display:flex;align-items:center;gap:12px;margin:0 0 12px}
.turncfg-os > label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-2)}
.osdd{position:relative;display:inline-block}
.osdd-btn{display:inline-flex;align-items:center;gap:8px;padding:7px 11px;border:1px solid var(--line-solid);border-radius:8px;background:var(--surface-2);color:var(--ink);font-family:var(--mono);font-size:12px;font-weight:600;cursor:pointer;min-width:150px}
.osdd-btn:hover,.osdd-btn.open{border-color:color-mix(in srgb,var(--brand) 45%,var(--line-solid))}
.osdd-btn .osdd-car{margin-left:auto;color:var(--dim);font-size:10px}
.osdd-ic{display:flex;align-items:center}
.osdd-lbl{white-space:nowrap}
.osdd-menu{position:absolute;top:calc(100% + 4px);left:0;z-index:50;min-width:100%;background:var(--surface);border:1px solid var(--line-solid);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.18);padding:4px;display:flex;flex-direction:column;gap:1px}
.osdd-opt{display:flex;align-items:center;gap:8px;padding:7px 9px;border:0;border-radius:6px;background:none;color:var(--ink-2);font-family:var(--mono);font-size:12px;font-weight:600;cursor:pointer;text-align:left;white-space:nowrap}
.osdd-opt:hover{background:color-mix(in srgb,var(--brand) 9%,transparent);color:var(--ink)}
.osdd-opt.on{color:var(--brand)}
.osdd-opt .ic{margin-left:auto}
/* Turn client picker — styled dropdown; each row = name·by author·[with core] + three badges (relation / obfuscation / kind) */
.cpick-opt{display:flex;align-items:center;gap:10px;width:100%;text-align:left;padding:8px 10px;border:0;border-radius:6px;background:none;color:var(--ink-2);cursor:pointer;font:inherit}
.cpick-opt:hover{background:color-mix(in srgb,var(--brand) 9%,transparent);color:var(--ink)}
.cpick-opt.on{background:color-mix(in srgb,var(--brand) 12%,transparent);color:var(--ink)}
/* "Don't offer" — last in the list and set apart by a rule above it: it withholds the server from an OS rather
   than choosing an app for it, so it should never read as one more app in the same run. */
.cpick-none{margin-top:4px;padding-top:9px;border-top:1px solid var(--line);border-radius:0 0 6px 6px;justify-content:space-between}
.cpick-none.on{color:var(--fault);background:color-mix(in srgb,var(--fault) 11%,transparent)}
.cpick-none-hint{font-size:11px;color:var(--faint);white-space:nowrap}
.ce-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px}
.ce-by{color:var(--muted)}
.ce-auth{font-weight:700}
.ce-badges{display:flex;align-items:center;gap:5px;flex:0 0 auto}
.ce-tag{display:inline-flex;align-items:center;font-family:var(--mono);font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;padding:2px 5px;border-radius:4px;line-height:1.45;white-space:nowrap;background:color-mix(in srgb,currentColor 15%,transparent)}
.ce-native{color:var(--online)}
.ce-cross{color:var(--ready)}
.ce-obf{color:#A974F2}
.ce-plain{color:var(--dangling)}
.ce-cli{color:#D9B84A}
.ce-app{color:#5FA8E0}
/* autostart indicator — ⚡ opens the app · ⧉ manual (copy the link) */
.ce-auto{display:inline-flex;align-items:center}
.ce-auto svg{width:13px;height:13px}
.ce-auto.on{color:var(--online)}
.ce-auto.off{color:var(--faint);opacity:.75}
.appdd .osdd-lbl .ce-auto{margin-left:5px}
/* ★ primary-connection toggle on a peer's deployment cards */
.primtog{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border:1px solid var(--line-solid);border-radius:999px;background:none;color:var(--muted);cursor:pointer;font-size:11px;font-weight:600;line-height:1.5;white-space:nowrap}
.primtog:hover:not(:disabled){border-color:#E0B93E;color:#E0B93E}
.primtog.on{border-color:#E0B93E;color:#E0B93E}
.primtog.bk{border-color:color-mix(in srgb,var(--partial) 55%,transparent);color:var(--partial)}
.primtog.bk:hover:not(:disabled){border-color:var(--partial);color:var(--partial)}
.primtog svg{width:13px;height:13px;flex:0 0 auto}
.primtog:disabled{cursor:default}
.primstar{font-size:13px;line-height:1}
/* client-roster drift check modal */
.rosterlist{display:flex;flex-direction:column;gap:8px}
.roster-row{border:1px solid var(--line-solid);border-radius:9px;padding:9px 12px}
.roster-row.changed{border-color:color-mix(in srgb,var(--fault) 40%,var(--line-solid))}
.roster-hd{display:flex;align-items:center;gap:10px;margin-bottom:3px}
.roster-nm{font-weight:600;font-size:13px}
.roster-by{font-size:11.5px;color:var(--dim);font-weight:600}
.roster-file{display:flex;align-items:center;gap:10px;font-size:11.5px;padding:2px 0;color:var(--ink-2);flex-wrap:wrap}
.roster-file a{color:var(--brand);text-decoration:none}
.roster-file a:hover{text-decoration:underline}
.roster-file a.faint{color:var(--dim)}
.fd-sum{font-family:var(--mono);font-size:11px;font-weight:600;white-space:nowrap}
.fd-sum.add{color:var(--online)}
.fd-sum.rem{color:var(--dangling)}
.fd-approx{font-size:10px;color:var(--dim);font-style:italic;cursor:help;white-space:nowrap}
.revlist{display:flex;flex-direction:column;gap:2px}
.rev-all{display:flex;align-items:center;gap:8px;padding:4px 2px 8px;font-size:11px;font-weight:600;color:var(--dim);text-transform:uppercase;letter-spacing:.04em;border-bottom:1px solid var(--line);margin-bottom:4px}
.rev-item{display:flex;align-items:center;gap:8px;padding:5px 2px;font-size:12px;border-radius:6px;cursor:pointer}
.rev-item:hover{background:var(--surface-2)}
.rev-item input[type=checkbox],.rev-all input[type=checkbox]{width:15px;height:15px;flex:0 0 auto;margin:0;accent-color:var(--brand);cursor:pointer}
.rev-item.disabled{cursor:default;opacity:.72}
.rev-item.disabled:hover{background:none}
.rev-file{color:var(--ink-2);font-size:11px}
/* roster grid: per-field counters, version picker, needs-wiring tag */
.rc-counts{display:inline-flex;gap:5px;align-items:center}
.rc-cnt{font-family:var(--mono);font-size:11px;font-weight:700;border-radius:5px;padding:1px 6px;border:1px solid transparent;white-space:nowrap}
.rc-cnt.add{color:var(--online);background:color-mix(in srgb,var(--online) 13%,transparent);border-color:color-mix(in srgb,var(--online) 32%,transparent)}
.rc-cnt.rem{color:var(--dangling);background:color-mix(in srgb,var(--dangling) 13%,transparent);border-color:color-mix(in srgb,var(--dangling) 32%,transparent)}
.rc-cnt.wire{color:var(--fault);background:color-mix(in srgb,var(--fault) 13%,transparent);border-color:color-mix(in srgb,var(--fault) 32%,transparent)}
.rc-cnt.val{color:var(--ready);background:color-mix(in srgb,var(--ready) 13%,transparent);border-color:color-mix(in srgb,var(--ready) 32%,transparent)}
.fd-sum.val{color:var(--ready)}
/* the roster version pin — a <select> until it became the shared Dropdown, so the look moves to its button */
.rc-ver{max-width:170px;min-width:0}
.rc-ver .ddbtn{font-family:var(--mono);font-size:11px;padding:3px 6px;border-radius:6px;gap:5px}
.rc-wtag{font-size:10px;font-weight:600;color:var(--fault);background:color-mix(in srgb,var(--fault) 12%,transparent);border-radius:5px;padding:1px 6px;white-space:nowrap}
/* Server-defaults ExecStart box: a read-only auto/obfuscation block above an editable free-flags textarea, as one field */
.obfrow{display:flex;align-items:center;gap:16px;flex-wrap:wrap}
.obfctl{display:flex;align-items:center;gap:8px;text-transform:none;letter-spacing:0;font-size:12px;cursor:pointer}
.execbox{border:1px solid var(--line);border-radius:8px;overflow:hidden;background:var(--bg)}
.execbox-auto{background:var(--surface-2);color:var(--dim);font-family:var(--mono);font-size:12px;line-height:1.55;padding:9px 11px;white-space:pre-wrap;word-break:break-word;border-bottom:1px dashed var(--line-solid);user-select:text}
.execbox-extra{display:block;width:100%;box-sizing:border-box;border:0;background:transparent;padding:9px 11px;font-family:var(--mono);font-size:12px;line-height:1.55;color:var(--ink);resize:vertical;min-height:54px;outline:none;box-shadow:none}
.execbox-extra:focus{outline:none;box-shadow:none}
/* palette pickers (Panel settings → Interfaces / Display): a concept label + a themed (dark|light) swatch pair */
.palrow{display:flex;flex-wrap:wrap;gap:12px 40px;margin:0 0 6px}
.palcell{display:inline-flex;align-items:center;gap:11px}
.palcell .pallbl{font-size:13px;color:var(--ink-2);min-width:88px}
.palcell.sw1 .pallbl{min-width:0}   /* swatch-first layout: label takes its natural width (no reserved column) */
/* themed swatch pair: two label-less pickers (dark, light); hover shows the colour previewed on that mode's bg */
.tswrow{display:inline-flex;gap:7px;align-items:center}
.tsw{position:relative;display:inline-flex}
/* preview sits directly UNDER the swatch on hover; while the OS picker is OPEN (focus-within) it slides 5px to the
   LEFT of the swatch so the picker (which opens below/over the swatch) can't cover it, then returns under on close. */
.tsw-bub{position:absolute;top:calc(100% + 8px);left:50%;transform:translateX(-50%);display:none;
  flex-direction:column;align-items:center;gap:8px;padding:11px 13px 8px;border-radius:10px;
  border:1px solid var(--line-solid);z-index:70;box-shadow:0 12px 30px rgba(0,0,0,.4);white-space:nowrap}
.tsw:hover .tsw-bub,.tsw:focus-within .tsw-bub{display:inline-flex}
.tsw:focus-within .tsw-bub{top:50%;left:auto;right:calc(100% + 5px);transform:translateY(-50%)}
.tsw-bub::before{content:"";position:absolute;left:0;right:0;top:-9px;height:9px}
/* Each preview sits on the REAL backdrop of the theme it previews, so you judge the colour against the
   background it will actually be read on — hardcoded --bg/--ink per theme, NOT the variables, because the
   light preview must stay light while you are in dark mode and vice versa.
   ⚠ These class names are BUILT ("tsw-" + mode), so they appear nowhere as literals and read as unused to a
   grep-based dead-CSS audit. Deleting them makes both bubbles transparent. Do not "clean up". */
.tsw-dark{background:#0A0E15;color:#E7EEF6}
.tsw-light{background:#EDF1F7;color:#101822}
.tsw-cap{font-family:var(--mono);font-size:8px;text-transform:uppercase;letter-spacing:.14em;opacity:.5}
/* theme-accent preview: an outline button + a filled control chip, both in the picked colour */
.tsw-theme{display:inline-flex;align-items:center;gap:8px}
.tsw-btn{font-size:11px;padding:4px 9px;border-radius:7px;border:1px solid currentColor;font-family:var(--sans)}
.tsw-chip{width:15px;height:15px;border-radius:4px;background:currentColor}
/* peer-health DETECTION toggles (checkbox + example badge + explanation) — Interfaces settings */
.condrow{display:grid;grid-template-columns:34px 88px 1fr;align-items:center;gap:16px;padding:11px 0;border-top:1px solid var(--line)}
.condrow:first-of-type{border-top:none;padding-top:2px}
.cond-b{display:flex;align-items:center}
.cond-t{font-size:12.5px;color:var(--dim);line-height:1.45}
.vklink-in{width:100%;max-width:560px}
/* turn-proxy client configs modal */
.turncfg{display:flex;flex-direction:column;gap:14px}
.turntabs{display:flex;flex-wrap:wrap;gap:8px}
.turntab{font-size:12px}
.turncfg-item>.hint{font-size:11px;line-height:1.5}
/* redundant same-fork proxies: title left, dropdown right, on one line */
.turninst{display:flex;align-items:center;gap:12px;justify-content:flex-end;margin-bottom:12px}   /* right-aligned: label sits next to its dropdown */
.turninst label{flex:none;font-size:11px;color:var(--faint);text-transform:uppercase;letter-spacing:.06em}
.turninst .selwrap{min-width:220px;max-width:360px}
.turncfg-item{display:flex;flex-direction:column;gap:4px}
.turncfg-qr{display:flex;justify-content:center;padding:10px 0}
.turncfg-qr .qr{background:#fff;border:1px solid var(--line);border-radius:var(--r-sm);padding:8px;line-height:0;cursor:zoom-in}
.turncfg-qr .qr .qrimg{display:block;width:min(208px,58vw);height:auto;image-rendering:pixelated;image-rendering:crisp-edges}
.turncfg-qr.qr-pending{color:var(--faint);font-size:12px;padding:34px 0}
.turncfg-head{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
.turncfg-foot{display:flex;align-items:center;gap:8px;margin-top:6px}
.turncfg-foot .grow{flex:1}
.tcf-label{font-size:11px;color:var(--faint);text-transform:uppercase;letter-spacing:.06em}
.turncfg-cmd{position:relative;margin-bottom:6px}
.turncfg-cmd .tokenbox{font-size:11.5px;line-height:1.5;color:var(--dim);background:var(--surface-2);border-radius:8px;padding:9px 11px 30px}
.cmd-copy{position:absolute;right:7px;bottom:7px;width:26px;height:26px;border-radius:6px;border:1px solid var(--line-solid);
  background:var(--surface);color:var(--dim);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:all .12s}
.cmd-copy:hover{color:var(--ink);border-color:var(--brand);background:var(--raise)}
/* hovering the command box sparks the copy button so it's discoverable — plain cyan border like the other buttons */
.turncfg-cmd:hover .cmd-copy{color:var(--brand);border-color:var(--brand);background:color-mix(in srgb,var(--brand) 10%,transparent)}
.turncfg-ta{width:100%;box-sizing:border-box;font-family:var(--mono);font-size:11.5px;line-height:1.5;color:var(--dim);
  background:var(--surface-2);border:1px solid var(--line-solid);border-radius:8px;padding:9px 11px;
  white-space:pre-wrap;word-break:break-all;overflow:hidden;resize:none;min-height:52px;
  /* block, not the inline-block a textarea defaults to: as an inline box it sat on the text baseline and left
     ~6.5px of descender space under it inside the wrapper. The copy button is positioned against the WRAPPER,
     so that phantom strip made "7px from the bottom" land 0.5px under the textarea while the right stayed 7 —
     the button looked stuck to the bottom border on one axis and inset on the other. */
  display:block}
.turnhead{display:flex;align-items:center;gap:10px}
.turnhead .grow{flex:1}
/* ───── Settings → Integrations (external API + webhooks) ───── */
.notice.ok{background:color-mix(in srgb,var(--online) 9%,transparent);border:1px solid color-mix(in srgb,var(--online) 28%,transparent);color:var(--online)}
.tok-add{display:flex;gap:8px;margin:0 0 10px}
.tok-add input{flex:1}
.tokreveal{display:flex;align-items:center;gap:8px;margin:0 0 14px;padding:9px 11px;border:1px solid var(--line-solid);border-radius:9px;background:var(--bg)}
.tokval{flex:1;font-family:var(--mono);font-size:12.5px;color:var(--ink);word-break:break-all;user-select:all}
.toklist{display:flex;flex-direction:column;gap:7px;margin:2px 0 4px}
.tokrow{display:flex;align-items:center;gap:10px;padding:8px 11px;border:1px solid var(--line);border-radius:9px;background:color-mix(in srgb,var(--ink) 2%,transparent)}
.tokrow.off{opacity:.6}
.tokrow-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.tokrow-label{font-size:13px;color:var(--ink);font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tokrow-label.mono{font-family:var(--mono);font-weight:500;font-size:12px}
.tokrow-meta{font-size:11px;color:var(--faint)}
.wh-events{display:grid;grid-template-columns:1fr 1fr;gap:8px 16px;margin:2px 0 14px}
.wh-ev{display:flex;align-items:center;gap:8px;font-size:12.5px;cursor:pointer}
.wh-ev .mono{font-family:var(--mono);font-size:12px;color:var(--ink)}
.wh-ev-lbl{color:var(--faint);font-size:11.5px}
.wh-en{display:flex;align-items:center;gap:9px;font-size:12.5px;color:var(--dim)}
.apiendpoints{display:flex;flex-direction:column;gap:4px;margin:2px 0 6px}
.apiep{display:flex;align-items:center;gap:10px;font-size:12px;padding:5px 0;border-bottom:1px solid var(--line)}
.apiep:last-child{border-bottom:none}
.apiep-m{font-family:var(--mono);font-size:10.5px;font-weight:700;color:var(--online);background:color-mix(in srgb,var(--online) 12%,transparent);padding:1px 7px;border-radius:5px;flex:none}
.apiep .mono{font-family:var(--mono);color:var(--ink);flex:none}
.apiep-d{color:var(--faint);margin-left:auto;font-size:11.5px}
.apisnip{margin:10px 0 0;border:1px solid var(--line-solid);border-radius:9px;overflow:hidden}
.apisnip-h{display:flex;align-items:center;justify-content:space-between;padding:7px 11px;font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--faint);background:color-mix(in srgb,var(--ink) 3%,transparent);border-bottom:1px solid var(--line)}
.apisnip-c{display:block;font-family:var(--mono);font-size:12px;color:var(--ink);padding:11px 13px;white-space:pre;overflow-x:auto;line-height:1.5}
/* ───── turn-proxy collected client IPs (header bubble + settings grid) ───── */
.tipdot{width:7px;height:7px;border-radius:50%;background:var(--dim);flex:none;opacity:.55}
.tipdot.on{background:var(--online);opacity:1;animation:sdotpulse 1.6s ease-in-out infinite}
.turnips-wrap{margin-left:auto}                        /* push the "Turn IPs" control to the right of the sheet head… */
.turnips-wrap + .x{margin-left:8px}                    /* …and sit it right next to the close button (no split gap) */
.turnips-hd{font-size:12px;color:var(--dim);cursor:pointer;padding:3px 9px;border:1px solid var(--line-solid);border-radius:7px;white-space:nowrap}
.turnips-wrap.on .turnips-hd,.turnips-hd:hover{color:var(--brand);border-color:var(--brand)}
.turnips-hd b{color:var(--ink);font-weight:700}
.turnips-pop{min-width:220px;max-width:320px}          /* inherit the standard .deppop 5px padding — same as the other hover bubbles */
.tipbub-row{display:flex;align-items:center;gap:9px;padding:5px 8px;border-radius:6px;font-size:12.5px}
.tipbub-row:hover{background:var(--surface)}
.tipbub-ip{font-family:var(--mono);color:var(--ink)}
.tipbub-when{font-size:11px;color:var(--dim);white-space:nowrap}
.tipbub-more{font-size:11px;color:var(--dim);padding:4px 8px 2px}
.tipbub-empty{font-size:12px;color:var(--dim);padding:6px 8px}
.tipbub-flush{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;padding:8px 8px 5px;margin-top:3px;border:0;border-top:1px solid var(--line);background:none;color:var(--dangling);font-size:11.5px;font-family:inherit;cursor:pointer}
.tipbub-flush:hover{filter:brightness(1.15)}
.tipbub-flush .ic svg{width:13px;height:13px}
.tipgrid{border:1px solid var(--line);border-radius:9px;overflow:hidden}
.tipgrid-h,.tiprow{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1fr) minmax(0,1.4fr) 30px;align-items:center;gap:10px;padding:8px 12px}
.tipgrid-h{font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);background:color-mix(in srgb,var(--ink) 3%,transparent)}
.tiprow{border-top:1px solid var(--line)}
.tiprow:hover{background:color-mix(in srgb,var(--ink) 2%,transparent)}
.tip-ip{display:flex;align-items:center;gap:8px;font-family:var(--mono);font-size:12.5px;color:var(--ink);min-width:0}
.tip-last{font-size:12px;color:var(--dim)}
.tip-by{display:flex;flex-wrap:wrap;gap:4px;min-width:0}
.tipfoot{display:flex;justify-content:flex-end;margin-top:10px}
/* on/off switch */
/* Access&TLS: the co-located node's loopback port — static, theme-tinted text (not an editable field) */
.localnode-note{margin:6px 2px 12px;font-size:12.5px;line-height:1.6;color:color-mix(in srgb,var(--brand) 42%,var(--ink))}
.localnode-note b{color:var(--brand);font-weight:700}
/* interface-key escrow toggle row (encryption settings) */
.ivk-escrow{margin:12px 0 4px;padding-top:12px;border-top:1px solid var(--line)}
.ivk-esc-row{display:flex;align-items:flex-start;gap:11px;font-size:13px;line-height:1.55;color:var(--dim);cursor:pointer}
.ivk-esc-row b{color:var(--ink)}
.field>label.toggle-row{display:flex;align-items:center;gap:12px;margin-bottom:7px}   /* switch + a one-line title; must set display:flex here — .field>label{display:block} out-specifies .ivk-esc-row's flex, so without this the row is block and align-items does nothing. The description sits below as a normal .hint */
.swt{position:relative;display:inline-flex;align-items:center;width:34px;height:19px;flex:none;cursor:pointer}
.swt input{position:absolute;inset:0;width:100%;height:100%;margin:0;opacity:0;cursor:pointer}
.swt .track{width:34px;height:19px;border-radius:99px;background:var(--surface-2);border:1px solid var(--line-solid);transition:background .15s,border-color .15s}
.swt .knob{position:absolute;left:3px;top:3px;width:13px;height:13px;border-radius:50%;background:var(--dim);transition:transform .15s,background .15s}
.swt input:checked~.track{background:color-mix(in srgb,var(--brand) 28%,transparent);border-color:var(--brand)}
.swt input:checked~.knob{transform:translateX(15px);background:var(--brand)}
/* header user-icon dropdown */
.acct-wrap{position:relative;display:inline-flex}
@media (max-width:680px){.setbody{flex-direction:column}.setrail{flex-direction:row;flex-wrap:wrap;width:auto;position:static}}
/* per-node selector: a clean wrapping row ABOVE the section card, right-aligned (the .snbadge look is unified
   with the Overview .dchip up top). */
.setnodes{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:6px;margin-bottom:12px}
/* unsaved-edit markers: a cyan dot on a rail section that changed, a cyan glow on a node badge changed in THIS section */
.dirtydot{width:7px;height:7px;border-radius:50%;background:var(--brand);flex:none;box-shadow:0 0 6px var(--brand)}
.snbadge.dirty{border-color:var(--brand);box-shadow:0 0 0 1px var(--brand),0 0 9px color-mix(in srgb,var(--brand) 55%,transparent)}
/* Back/Save now live in a footer BELOW the content card, right-aligned + nudged a touch past the card edge */
.setfoot{display:flex;align-items:center;gap:10px;margin-top:18px;margin-right:-4px}
.setfoot .grow{flex:1}   /* expand the spacer so Back/Save sit right (the saved-flash stays left) */
.setfoot .btn{padding:9px 26px;min-width:104px;justify-content:center}   /* Back + Save: wide + equal */
.setfoot .savedflash{margin-left:2px}
/* The destination control on a rule row and on the catch-all. Both were native <select class="selwrap">
   sized by `.tf-trail .selwrap`; they are the shared `Dropdown` now, so the slot has to name that instead —
   same width, same place, so the two kinds of row still line up with each other down the list. */
.rrdest{display:block;min-width:0}
.rrdest .ddbtn{font-size:12.5px;padding:7px 10px}
.rrcatch{min-width:0}   /* the routing catch-all ("everything else") — matches the per-rule route-to control exactly */
/* Phase 3 smart-routing rule editor. Row + catch-all footer share ONE grid template (grip · dropdown · arrow ·
   dropdown · xbtn), so the "everything else" dropdown + arrow line up exactly under the per-rule ones. The side
   columns are `auto` (icon-width) and identical in both, so the two 1fr dropdown columns come out equal. A flex
   row can't do this reliably — a <span> and a <select> don't split a flex track equally. */
.rrlist{display:flex;flex-direction:column;gap:6px;margin-top:2px}
/* TWO COLUMNS: the grip, then the field across everything else. The destination select used to be a third
   and fourth column, which capped the target box at half the dialog however wide the dialog got — and the
   box is the part that holds an unbounded number of badges. It now rides in the field's own foot, right
   aligned and level with the entry count (`.tf-trail`), so the box gets the full width and the two controls
   still read as one line. */
.rrrow{--rr-rail:16px;--rr-gap:8px;display:grid;grid-template-columns:var(--rr-rail) minmax(0,1fr);align-items:start;gap:var(--rr-gap);padding:5px 8px;border:1px solid var(--line);border-radius:8px;background:color-mix(in srgb,var(--ink) 2%,transparent)}
.rrrow .drag-grip{margin-top:7px}   /* grid is top-aligned now, so the grip lines up with the box, not the row */
/* The destination, tucked to the right of the foot. `margin-left:auto` rather than a spacer so a long
   sentence in the foot pushes it to its own line instead of squeezing the select to nothing. */
/* EXPLICIT WIDTHS, not intrinsic ones. The base rule is `input,select{width:100%}`, so a select left to size
   itself here takes the whole trail and shoves the arrow and the × outside the row — and because the trail's
   own max-content is then computed FROM that select, the two chase each other to a width that fits neither.
   A fixed dropdown width also makes the rule rows and the catch-all agree by construction rather than by two
   intrinsic sizes happening to land on the same number, which they did not: 234 against 238. */
.tf-trail{margin-left:auto;display:flex;align-items:center;gap:8px;flex:0 0 auto;max-width:100%}
/* 190 → 224 → 272. Each bump has had the same cause and a longer winner: at 190 it was "Напрямую (эта
   нода)"; at 224 it is an exit, whose label is a name AND a device — "ВАРП выход — wgx-736a24bf" — and
   those clipped to "wgx-736a24…", which is the half that tells two exits apart. Both the rule rows and the
   catch-all read from this one rule, so they keep matching widths down the list. */
.tf-trail .selwrap,.tf-trail .rrdest{width:272px;min-width:0;flex:0 0 272px}
/* The trailing button is a × on a rule and a gear on the catch-all; they must occupy the SAME box or the two
   dropdowns sit a few pixels apart all the way down the list, which reads as a wobble rather than a column. */
.tf-trail .xbtn{width:20px;height:20px;padding:0;margin-left:0;flex:none}
/* IN THE RAIL, DIRECTLY UNDER THE TEXT-VIEW TOGGLE — not in the foot's flow. It used to sit beside the
   destination dropdown, one careless click from it; then at the foot's left edge, where it crowded the
   entry count. The rail is the row's column of controls-on-the-field, and this is one: taken out of flow
   so it lines up under `</>` and the count keeps the line's left edge. Red at rest, because a destructive
   control that only reveals itself on hover is one the operator finds by accident. */
/* TWO ALIGNMENTS AT ONCE, which is why it is absolute INSIDE the pair rather than in the foot: vertical
   comes from `.tf-lstart`, whose box is the entry count's own line, so 50% is that line's middle whatever
   the destination select does to the foot's height; horizontal comes from the same rail expression the
   text-view toggle uses, so the two controls share a column. Out of flow, so the count keeps the box's
   left edge instead of being pushed right by a button sitting outside the box. */
.tf-lstart{position:relative;display:flex;align-items:center;gap:6px;min-width:0;flex:0 1 auto}
.tf-lead{position:absolute;left:calc(-1 * (var(--rr-rail,16px) + var(--rr-gap,8px) + 2px));top:50%;
  transform:translateY(-50%);display:flex;align-items:center;justify-content:center;
  width:var(--rr-rail,16px);flex:none}
.tf-lead .xbtn{width:18px;height:18px;padding:0;margin:0;flex:none;color:var(--dangling);opacity:.85}
.tf-lead .xbtn:hover{opacity:1;background:color-mix(in srgb,var(--dangling) 14%,transparent)}
.tf-lead .xbtn .ic svg{width:15px;height:15px}
.tf-foot .tf-trail{margin-right:-2px}   /* cancel the foot's own 2px inset so the control lines up with the box above it */
/* custom IPs/domains rule: a full-width auto-growing textarea on its own grid row. Starts one row tall (like a
   textbox); JS grows it to fit wrapped content (resize off, no scrollbar). */
.rrdoms{grid-column:1/-1;width:100%;box-sizing:border-box;display:block;font-family:var(--mono);font-size:12px;line-height:1.55;padding:5px 8px;border:1px solid var(--line);border-radius:7px;background:var(--bg);color:var(--ink);resize:none;overflow:hidden}
.rrdoms::placeholder{color:var(--faint)}
.rrrow.warn{border-color:color-mix(in srgb,var(--partial) 45%,transparent)}
.rrrow .drag-grip{cursor:grab;opacity:.62}.rrrow:hover .drag-grip{opacity:.95}
.rrrow .selwrap{min-width:0}
.rrrow .catpick{min-width:0}
.rrarrow{color:var(--faint)}
/* Searchable provider-catalog category picker (CatPicker) — the button mirrors .selwrap; the popover is portal'd to <body>. */
.catpick{position:relative;min-width:0}
.catpick-btn{display:flex;align-items:center;gap:6px;width:100%;min-width:0;padding:7px 9px;font-family:inherit;font-size:13px;
  color:var(--ink);background:var(--bg);border:1px solid var(--line-solid);border-radius:7px;cursor:pointer;text-align:left;transition:border-color .12s}
.catpick-btn:hover{border-color:color-mix(in srgb,var(--brand) 40%,var(--line-solid))}
.catpick-btn.on{border-color:var(--brand)}
.catpick-lbl{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.catpick-caret{color:var(--faint);font-size:11px;flex-shrink:0}
.catpick-pop{position:fixed;z-index:90;max-width:min(440px,92vw);background:var(--surface-2);border:1px solid var(--line-solid);
  border-radius:var(--r-sm);box-shadow:0 12px 34px rgba(0,0,0,.5);overflow:hidden;display:flex;flex-direction:column}
.catpick-pop.flip{transform:translateY(-100%)}   /* opened upward: anchor the popover's bottom to the trigger's top */
.catpick-search{display:flex;align-items:center;gap:7px;padding:9px 11px;border-bottom:1px solid var(--line)}
.catpick-search .ic{color:var(--faint);flex-shrink:0}
.catpick-search input{flex:1;min-width:0;border:0;background:none;color:var(--ink);font-family:inherit;font-size:13px;outline:none}
.catpick-search input::placeholder{color:var(--faint)}
.catpick-list{max-height:var(--catpick-maxh,min(46vh,380px));overflow-y:auto;padding:5px}
.catpick-grp{font-family:var(--mono);font-size:9.5px;letter-spacing:.11em;text-transform:uppercase;color:var(--dim);padding:8px 8px 4px}
.catpick-row{display:flex;align-items:center;gap:8px;width:100%;padding:7px 9px;border:0;border-radius:6px;background:none;
  color:var(--ink);font-family:inherit;font-size:13px;text-align:left;cursor:pointer}
.catpick-row:hover:not(:disabled){background:var(--dd-hover);color:var(--ink)}
.catpick-row.sel{background:var(--dd-sel);color:var(--sel-ink)}
.catpick-row.off{cursor:not-allowed;color:var(--faint)}
.catpick-row.off .capb{opacity:.5}
.catpick-rlbl{flex:1;min-width:0;display:flex;align-items:center;gap:7px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.catpick-rlbl .ic{color:var(--faint);flex-shrink:0}
/* provider source tag — colour-coded per provider via --pc (falls back neutral for Custom/built-in). Readable on
   light+dark and on a brand-highlighted row. */
.catpick-src{font-family:var(--mono);font-size:9px;letter-spacing:.03em;flex-shrink:0;padding:1px 5px;border-radius:5px;
  color:var(--pc,var(--ink-2));border:1px solid color-mix(in srgb,var(--pc,var(--line-solid)) 40%,transparent);
  background:color-mix(in srgb,var(--pc,var(--ink)) 13%,var(--surface))}
.catpick-src.legacy{color:var(--dim);border-color:var(--line-solid);background:var(--surface)}
/* swatch preview pill — transparent base so it adapts to the dark/light preview cells (like .tg-turn) */
.sw-sample{font-family:var(--mono);font-size:9px;letter-spacing:.03em;padding:1px 6px;border-radius:5px;white-space:nowrap;
  color:var(--pc);border:1px solid color-mix(in srgb,var(--pc) 38%,transparent);background:color-mix(in srgb,var(--pc) 16%,transparent)}
/* rich catalog-browse row (Add from catalog): title + dimmed id + provider · description/sample · counts · caps */
.catrow{display:flex;align-items:flex-start;gap:9px;width:100%;padding:8px 9px;border:0;border-radius:7px;background:none;
  color:var(--ink);font:inherit;text-align:left;cursor:pointer}
.catrow:hover{background:var(--dd-hover)}
.catrow.sel{background:var(--dd-sel)}
.catrow.off{opacity:.5;cursor:not-allowed}
.catrow.off:hover{background:none}
.catrow .catpick-tick{margin-top:2px}
.catrow-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.catrow-l1{display:flex;align-items:baseline;gap:8px;min-width:0}
.catrow-title{font-size:13.5px;font-weight:600;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.catrow-id{font-family:var(--mono);font-size:10.5px;color:var(--faint);flex-shrink:0}
.catrow-heavy{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--partial);border:1px solid color-mix(in srgb,var(--partial) 45%,transparent);border-radius:4px;padding:0 4px;flex-shrink:0}
.catrow-l1 .catpick-src{margin-left:auto}
.catrow-l2{font-size:11.5px;color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.catrow-desc{color:var(--ink-2)}
.catrow-eg{color:var(--faint);font-family:var(--mono);font-size:10.5px}
.catrow-right{display:flex;align-items:center;gap:8px;flex-shrink:0;align-self:center}
.catrow-size{font-size:11px;color:var(--dim);white-space:nowrap;font-variant-numeric:tabular-nums}
/* "view this list on GitHub" info link, on catalog rows + chips — clearer colour + a larger hit area */
.catrow-info,.ccchip-info{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;margin:-4px -2px;
  color:var(--brand);flex-shrink:0;border-radius:6px;transition:color .12s,background .12s}
.catrow-info:hover,.ccchip-info:hover{color:var(--brand);background:color-mix(in srgb,var(--brand) 14%,transparent)}
.catrow-info .ic svg,.ccchip-info .ic svg{width:16px;height:16px}
/* curated preset info icon → hover bubble with the list's description (no external URL to link) */
.descinfo{position:relative;cursor:help;outline:none}
.descbub{position:absolute;bottom:calc(100% + 8px);right:0;display:none;z-index:90;width:max-content;max-width:240px;
  padding:7px 10px;border-radius:8px;background:var(--surface);border:1px solid var(--line-solid);color:var(--ink);
  font-size:11.5px;line-height:1.45;font-weight:500;text-align:left;white-space:normal;box-shadow:0 10px 26px rgba(0,0,0,.4)}
.descinfo:hover .descbub,.descinfo:focus .descbub,.descinfo:focus-within .descbub{display:block}
/* ── Routing lists redesign: mode banner + two list grids + fleet popover ── */
.rmode-banner{position:relative;display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:14px;align-items:start;padding:11px 16px 14px;border-radius:10px;margin-bottom:22px;
  border:1px solid var(--line-solid);background:color-mix(in srgb,var(--mc,var(--brand)) 6%,var(--surface-2));border-left:3px solid var(--mc,var(--brand))}
.rmode-info{display:inline-flex;flex:none}
.rmode-infobtn{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:6px;color:var(--brand);cursor:help;transition:color .12s,background .12s}
.rmode-infobtn .ic svg{width:14px;height:14px}
.rmode-info.on .rmode-infobtn,.rmode-infobtn:hover{color:var(--brand);background:color-mix(in srgb,var(--brand) 14%,transparent)}
.rmode-info-pop{max-width:420px;font-size:12.5px;line-height:1.55;color:var(--dim)}
.rmode-info-body{padding:5px 8px;text-align:justify}
.rmode-info-body b{color:var(--ink);font-weight:600}
.rmode-desc{grid-column:1 / -1;font-size:12px;color:var(--dim);line-height:1.55;margin-top:11px;max-width:none}
.rmode-tabs{display:flex;gap:8px;margin:0 0 14px}
.rmtab{--mc:var(--brand);flex:0 0 auto;width:54px;height:44px;display:inline-flex;align-items:center;justify-content:center;
  border:1px solid var(--line-solid);background:var(--surface-2);border-radius:9px;color:var(--faint);cursor:pointer;
  transition:color .12s,background .12s,border-color .12s,box-shadow .12s}
.rmtab .ic svg{width:22px;height:22px}
.rmtab:hover{color:var(--mc);border-color:color-mix(in srgb,var(--mc) 55%,var(--line-solid));background:color-mix(in srgb,var(--mc) 8%,var(--surface-2))}
.rmtab.on{color:#fff;background:var(--mc);border-color:var(--mc);box-shadow:0 3px 10px color-mix(in srgb,var(--mc) 38%,transparent)}
/* Routing & Blocking — context caption (left) + tab control (right), sharing one ruled baseline */
.rltabs{display:flex;align-items:flex-end;gap:16px;margin:16px 0 14px;border-bottom:1px solid var(--line-solid)}
.rltab-cap{flex:1;min-width:0;padding-bottom:8px;color:var(--ink);font-size:15px;font-weight:600;letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.rltab-cap b{font-weight:700;font-size:16px}
.rltab-group{display:flex;gap:4px;margin-left:auto}
/* the declarative install's two arms, right-aligned ON the tab row: a switch, not a second tablist.
   Each side carries its run-model's own colour — the same tokens the bare-metal / docker pills use. */
.armsw{display:inline-flex;margin-left:auto;align-self:center;margin-bottom:7px;border:1px solid var(--line);
  border-radius:20px;overflow:hidden;background:var(--surface-2)}
.armsw button{appearance:none;border:0;background:transparent;font:inherit;font-size:11.5px;font-weight:700;
  padding:5px 13px;color:var(--muted);cursor:pointer;white-space:nowrap;transition:color .12s,background .12s}
.armsw button + button{border-left:1px solid var(--line)}
.armsw button:hover{color:var(--ink)}
.armsw button.on.bare{color:var(--tport-bare);background:color-mix(in srgb,var(--tport-bare) 16%,transparent)}
.armsw button.on.ctr{color:var(--tport-dock);background:color-mix(in srgb,var(--tport-dock) 16%,transparent)}
@media (max-width:560px){.armsw button{font-size:11px;padding:5px 9px}}
.rltab-note{display:flex;gap:7px;align-items:flex-start;margin:-4px 0 16px;color:var(--faint);font-size:12px;line-height:1.5}
.rltab-note svg{flex:0 0 auto;width:14px;height:14px;margin-top:1px;opacity:.8}
.rltab-warn{display:flex;gap:7px;align-items:flex-start;margin:-11px 0 16px;color:var(--partial);font-size:12px;line-height:1.5}
.rltab-warn svg{flex:0 0 auto;width:14px;height:14px;margin-top:1px}
.rltab-warn b{font-weight:700}
.rltab{flex:0 0 auto;display:inline-flex;align-items:center;gap:7px;padding:8px 16px 9px;border:0;background:none;cursor:pointer;
  color:var(--muted);font:inherit;font-weight:600;font-size:13px;border-bottom:2px solid transparent;margin-bottom:-1px}
.rltab .ic svg{width:16px;height:16px}
.rltab:hover{color:var(--ink)}
.rltab.on{color:var(--brand);border-bottom-color:var(--brand)}
/* Blocking tab — click-to-expand category rows: avail · name/lists-under · count · match · Default ON/OFF · + Add list · caret */
.bkitem{border-top:1px solid var(--line)}
.bkitem:first-child{border-top:none}
.bkitem.open{border-top-color:transparent;background:color-mix(in srgb,var(--brand) 5%,transparent);border-radius:10px}
.bkitem.open + .bkitem{border-top-color:transparent}
.bkrow{display:grid;grid-template-columns:40px minmax(0,1fr) auto auto auto auto auto auto;align-items:center;gap:14px;padding:10px 4px;cursor:pointer}
.bk-kind{display:flex;align-items:center;justify-content:flex-end}
.bk-size{display:flex;align-items:center;justify-content:flex-end}
.bk-count{font-family:var(--mono);font-size:10.5px;color:var(--faint);font-variant-numeric:tabular-nums;white-space:nowrap}
.bk-lrow .bk-count{flex-shrink:0}
.bkrow:hover{background:color-mix(in srgb,var(--ink) 2.5%,transparent)}
.bkrow.lg-lock{opacity:.5}
.bk-cat{min-width:0}
.bk-catline{display:flex;align-items:baseline;gap:7px;min-width:0}
.bk-catline .lg-title{flex:0 1 auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bk-fleet{display:flex;align-items:center;gap:8px;white-space:nowrap}
.bk-nabadge{flex-shrink:0;font-family:var(--mono);font-size:9px;letter-spacing:.05em;font-weight:600;white-space:nowrap;padding:1px 5px;border-radius:5px;color:#b7791f;background:color-mix(in srgb,#e0a83a 15%,transparent)}
:root[data-theme="dark"] .bk-nabadge{color:#e6b84f}
.bk-lists{font-family:var(--mono);font-size:10.5px;line-height:1.35;color:var(--faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:1px}
.bk-cap{display:flex;align-items:center;justify-content:flex-end;gap:4px}
/* the 9px badge, though box-centred, reads ~2px high next to the larger pill/chip text — nudge its baseline onto theirs */
.bk-cap .capb,.bk-kind .capb{position:relative;top:1.5px}
.bk-nocap{color:var(--faint);font-size:12px}
.bk-defchip{border:1px solid var(--line-solid);background:none;color:var(--muted);font:inherit;font-size:11px;font-weight:600;padding:3px 11px;border-radius:999px;cursor:pointer;white-space:nowrap}
.bk-defchip:hover{color:var(--ink);border-color:var(--muted)}
.bk-defchip.on{color:var(--brand);border-color:var(--brand);background:color-mix(in srgb,var(--brand) 12%,transparent)}
.bk-defchip.on:hover{background:color-mix(in srgb,var(--brand) 18%,transparent)}

/* Per-interface Block-traffic section: grouped drop-category chips. ON = protection active → green (--online) with an
   orange ⊘ block glyph (--fault); OFF = quiet dashed outline. Green carries "protected", orange ⊘ says "blocking". */
.blk-field label{display:block}
.blk-grp{margin-top:11px}
.blk-grp:first-of-type{margin-top:9px}
.blk-gtitle{font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);margin:0 0 7px}
.blk-chips{display:flex;flex-wrap:wrap;gap:7px}
.blk-empty{margin:0}
.blkchip{display:inline-flex;align-items:center;gap:7px;font-size:12px;font-weight:600;padding:6px 12px;border-radius:99px;cursor:pointer;
  background:var(--surface-2);color:var(--dim);border:1px dashed var(--line-solid);white-space:nowrap;transition:background .12s,border-color .12s,color .12s}
.blkchip:hover{color:var(--ink-2)}
.blkchip-g{font-size:13px;line-height:1;color:var(--dim);opacity:.5;flex:none}   /* ⊘ block glyph */
.blkchip.on{color:var(--online);border:1px solid color-mix(in srgb,var(--online) 62%,transparent);background:color-mix(in srgb,var(--online) 12%,transparent)}
.blkchip.on .blkchip-g{color:var(--fault);opacity:1}
.blkchip.inert{opacity:.5;cursor:not-allowed}
.blkchip.inert.on{opacity:.7}
.blkchip-tag{font-family:var(--mono);font-size:9px;letter-spacing:.05em;font-weight:600;padding:1px 5px;border-radius:5px;color:var(--brand);background:color-mix(in srgb,var(--brand) 15%,transparent)}

/* Reusable disclosure section (interface + turn-proxy modals): full-width collapsible with a caret, title and a
   right-aligned summary/count. Open header connects seamlessly to its body. */
.disc{display:flex;align-items:center;gap:10px;width:100%;box-sizing:border-box;padding:11px 13px;border:1px solid var(--line);border-radius:10px;
  background:var(--surface-2);cursor:pointer;margin-top:9px;text-align:left}
.disc:hover{border-color:var(--line-bright)}
.disc-car{color:var(--dim);font-size:10px;width:9px;flex:none;transition:transform .15s}
.disc.open{border-color:var(--line-bright);border-bottom-left-radius:0;border-bottom-right-radius:0}
.disc.open .disc-car{transform:rotate(90deg)}
.disc-t{font-size:13px;font-weight:600;color:var(--ink)}
.disc-sum{margin-left:auto;font-family:var(--mono);font-size:11px;color:var(--faint)}
.disc-sum b{font-weight:700}
.disc-sum.on b{color:var(--online)}      /* active filters — green */
.disc-sum.route b{color:var(--smart)}    /* routing rule count — rose */
/* A named divider BETWEEN groups inside one disclosure — the protocol settings and the NAT source card
   sit in the same "Advanced settings" body and two unlabelled runs of fields read as one long list. Not a
   nested accordion on purpose: only one protocol block ever applies, so collapsing it would add a click to
   reach settings that are always relevant. First one in a body has no rule above it — the disclosure's own
   border already is one. */
.field.secdiv{margin:4px 0 10px}
.field.secdiv>label{margin:0;padding-top:11px;border-top:1px solid var(--line-bright);color:var(--fg)}
.disc-body>.field.secdiv:first-child>label{padding-top:0;border-top:none}
.disc-body{border:1px solid var(--line-bright);border-top:none;border-radius:0 0 10px 10px;padding:13px 13px 4px;
  background:color-mix(in srgb,var(--surface-2) 45%,transparent)}
.disc-body>.field:last-child{margin-bottom:6px}
.bk-addwrap .btn-mini{white-space:nowrap}
.bk-chev{flex-shrink:0;color:var(--faint);font-size:9px;line-height:1;transition:transform .15s ease}
.bkrow:hover .bk-chev{color:var(--muted)}
.bkrow.open .bk-chev{transform:rotate(180deg);color:var(--brand)}
.bk-expand{display:flex;flex-direction:column;gap:0;padding:2px 10px 12px 68px}
.bk-lrow{display:flex;align-items:center;gap:10px;padding:5px 4px 5px 2px}
.bk-lrow.bk-ldis{opacity:.5}
.bkitem.bk-dis .bk-cat,.bkitem.bk-dis .bk-size{opacity:.5}   /* category with no enforceable list in this mode — dimmed, but still expandable to add an IP list */
.bk-lrow .capb{flex-shrink:0}
.bk-linfo{display:flex;flex-direction:column;gap:0;min-width:0;flex:0 1 auto}
.bk-ltop{display:flex;align-items:center;gap:8px;min-width:0}
.bk-llabel{flex:0 1 auto;font-size:13px;font-weight:600;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bk-lprov{flex-shrink:0;font-family:var(--mono);font-size:9px;font-weight:700;letter-spacing:.03em;text-transform:uppercase;color:var(--pc,var(--faint));background:color-mix(in srgb,var(--pc,var(--ink)) 11%,transparent);border:1px solid color-mix(in srgb,var(--pc,var(--ink)) 26%,transparent);padding:1px 6px;border-radius:5px}
.bk-ldesc{min-width:0;font-size:11px;color:var(--faint);line-height:1.25;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bk-lremove{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;margin:-4px -2px;border:0;background:none;color:var(--muted);cursor:pointer;border-radius:7px}
.bk-lremove svg{width:15px;height:15px}
.bk-lremove:hover{color:#e0524a;background:color-mix(in srgb,#e0524a 12%,transparent)}
.bk-empty{color:var(--faint);font-size:12px;padding:2px 0}
.bk-catfoot{display:flex;align-items:center;margin-top:8px}
.bk-delcat{display:inline-flex;align-items:center;gap:6px;border:1px solid color-mix(in srgb,#e0524a 45%,var(--line-solid));background:none;color:#e0524a;font:inherit;font-size:12px;font-weight:600;padding:5px 13px;border-radius:8px;cursor:pointer}
.bk-delcat svg{width:13px;height:13px}
.bk-delcat:hover{background:color-mix(in srgb,#e0524a 12%,transparent);border-color:color-mix(in srgb,#e0524a 60%,var(--line-solid))}
.bk-pick{width:288px;max-height:340px;display:flex;flex-direction:column}
.bk-picksearch{width:100%;box-sizing:border-box;padding:9px 11px;border:0;border-bottom:1px solid var(--line);background:none;color:var(--ink);font:inherit;font-size:13px;outline:none}
.bk-picklist{overflow-y:auto;padding:6px;scrollbar-width:thin}
.bk-pickprov{display:flex;align-items:center;gap:6px;font-size:10px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--pc,var(--faint));padding:7px 8px 3px}
.bk-pickprov::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--pc,var(--faint));box-shadow:0 0 5px color-mix(in srgb,var(--pc,transparent) 70%,transparent)}
.bk-pickitem{display:flex;flex-direction:column;width:100%;text-align:left;border:0;background:none;color:var(--ink);font:inherit;padding:6px 8px;border-radius:6px;cursor:pointer;gap:1px}
.bk-pilabel{font-size:13px;font-weight:600}
.bk-pidesc{font-size:11px;color:var(--muted)}
.bk-pickitem:hover{background:color-mix(in srgb,var(--brand) 12%,transparent)}
.bk-pickitem:hover .bk-pilabel{color:var(--brand)}
.bk-pickempty{color:var(--faint);font-size:12px;padding:10px 8px}
/* THE BUTTONS ARE A COLUMN NOW, not an overlay. `.rd-headside` used to be `position:absolute` in the card's
   top-right, which takes no space — so every full-width block under it (`.rd-head`, `.rd-lines`) had to
   reserve a gutter by hand and hope the number was big enough. It was not: `.rd-head` guessed 200px while
   the column measures 248, and the +/− lines cleared the buttons by 25px purely on string length. Measured
   on the live card, one longer benefit line ran straight underneath.
   As a real grid column spanning the head and the lines, nothing CAN overlap it, no block needs a gutter,
   and the left text still reclaims the full width from the spec strip down — which is what the absolute
   positioning was reaching for in the first place. */
.rd-head{display:flex;align-items:flex-start;gap:12px;margin-bottom:7px;grid-column:1}
.rd-headmain{display:flex;flex-direction:column;gap:5px;flex:1;min-width:0}   /* title, then summary right below it (top-aligned — no longer stretched to the buttons) */
.rd-titlerow{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
.rd-ic{display:inline-flex;color:var(--mc)}.rd-ic svg{width:20px;height:20px}
.rd-name{font-size:15px;color:var(--ink);font-weight:700}
.rd-head .rmode-tabs{margin:0;flex:none;align-self:flex-start}   /* the 4 mode icons live in the header now */
.rd-adds{font-size:12.5px;color:var(--ink);font-weight:700;line-height:1.5}
.rd-lines{display:flex;flex-direction:column;gap:3px;margin-bottom:8px;grid-column:1 / -1}   /* even spacing between every +/- row */
.rmc-tag{font-family:var(--mono);font-size:9px;letter-spacing:.03em;color:var(--mc);padding:2px 7px;border-radius:20px;background:color-mix(in srgb,var(--mc) 14%,transparent);white-space:nowrap}
.rmc-bene,.rmc-cost{font-size:12px;line-height:1.45;display:flex;gap:7px;align-items:baseline}
.rmc-bene b,.rmc-cost b{flex:none;font-family:var(--mono);font-weight:700;font-size:12.5px}
.rmc-bene{color:var(--dim)}.rmc-bene b{color:#3EA76B}
.rmc-cost{color:var(--faint)}.rmc-cost b{color:#C0821A}
:root[data-theme="dark"] .rmc-bene b{color:#57C98A}:root[data-theme="dark"] .rmc-cost b{color:#EBB43C}
/* The capability strip: three facts, label column aligned, no rules or box around it. The card already
   carries a +/− list and a tag pill; a bordered panel here would be a third device saying the same thing at
   the same volume. Sits between the trade-offs and the prose because it is the last thing read before the
   paragraph, and BELOW the absolutely-positioned mode buttons so it cannot slide under them. */
.rd-spec{grid-column:1 / -1;display:grid;grid-template-columns:auto minmax(0,1fr);gap:2px 12px;margin-top:10px;align-items:baseline}
.rds-k{font-family:var(--mono);font-size:11px;color:var(--faint);white-space:nowrap}
.rds-v{font-size:12px;color:var(--dim);line-height:1.5}
.rmode-banner .rmode-desc{margin-top:8px}
/* ROW 1 ONLY. It used to span the lines as well, which cost them half the card — measured 456px against a
   Russian benefit line needing 663px, so every mode wrapped in Russian while English fit by luck. With the
   lists line gone the button row is SHORTER than the title+summary beside it, so spanning one row wastes
   no space at all and hands `.rd-lines` the full 714px, where the widest line in either language fits. */
.rd-headside{grid-column:2;grid-row:1;display:flex;flex-direction:column;align-items:flex-end;gap:13px;min-width:0}
.rd-headside .rmode-tabs{align-self:flex-end}
/* status bar ABOVE the mode card (outside it): "<node> currently runs on <mode>" + status under it, reset/info right */
.rmode-runbar{display:flex;align-items:baseline;gap:10px;margin-bottom:12px}   /* status text (left) and caption (right) share a baseline */
.rmr-left{display:flex;flex-direction:column;align-items:flex-start;gap:7px;min-width:0}   /* status (+ reset when there's no 2nd status line), left-aligned */
.rmr-left .rmode-health{margin-top:0}
.rmr-right{display:flex;flex-direction:column;align-items:flex-end;gap:7px;flex:none}   /* caption + toggle + info on top, reset under (when degraded), right-aligned */
.rmr-rtop{display:flex;align-items:center;gap:7px}
.rmr-actions{display:flex;align-items:center;gap:8px;flex:none}   /* default-routing runbar: reset + info, right-aligned (no status/toggle) */
.learn-toggle{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;padding:0;border-radius:6px;border:none;background:transparent;color:#D65B5B;cursor:pointer;transition:color .12s,background .12s}   /* IP-learning OFF = red */
.learn-toggle:hover{background:color-mix(in srgb,currentColor 15%,transparent)}
.learn-toggle.on{color:#3EA76B}   /* ON = green */
:root[data-theme="dark"] .learn-toggle{color:#E06B6B}
:root[data-theme="dark"] .learn-toggle.on{color:#57C98A}
.learn-toggle .ic svg{width:15px;height:15px}
.rmr-title{font-size:13px;color:var(--dim)}
.rmr-node{color:var(--ink);font-weight:700}   /* node name: bold, default ink (no node colour) */
.rmr-mode{color:var(--ink);font-weight:600}
/* live host-layer health strip (engine alive? + reset count) below the mode detail */
.rmode-health{display:flex;align-items:center;flex-wrap:wrap;gap:7px;row-gap:2px;margin-top:12px;font-size:12px;color:var(--dim)}
.rmode-health > b,.rmode-health > span{white-space:nowrap}   /* wrap the strip between units, never mid-phrase */
.rmode-health .rmh-note{white-space:normal}
.rmode-health b{color:var(--ink);font-weight:600}
.rmh-dot{width:8px;height:8px;border-radius:50%;flex:none}
.rmode-health.ok .rmh-dot{background:#3EA76B}
.rmode-health.down{color:#D65B5B}
.rmode-health.down b{color:#D65B5B}
.rmode-health.down .rmh-dot{background:#D65B5B}
.rmh-sep{color:var(--faint)}
.rmh-note{flex-basis:100%;color:var(--faint);font-style:italic}   /* degraded note drops onto its own line under the status */
.rmh-info{display:inline-flex}
.rmh-infobtn{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;color:var(--brand);cursor:help;transition:color .12s}
.rmh-info.on .rmh-infobtn,.rmh-infobtn:hover{color:var(--brand)}
.rmh-infobtn .ic svg{width:14px;height:14px}
:root[data-theme="dark"] .rmode-health.ok .rmh-dot{background:#57C98A}
/* Reset routing as a red circle-arrows icon (after the info icon) with a hover bubble (no native title) */
.rmode-resetwrap{display:inline-flex;flex:none}
.rmode-reset-ic{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:6px;flex:none;
  border:none;background:none;color:#D65B5B;cursor:pointer;padding:0;transition:color .12s,background .12s}
.rmode-reset-ic .ic svg{width:14px;height:14px}
.rmode-reset-ic:hover{color:#E86B6B;background:color-mix(in srgb,#D65B5B 15%,transparent)}
/* ⚠️ .deppop AND .rmode-reset-pop ARE THE SAME SPECIFICITY, and .deppop is declared later — so its
   padding:5px won and this prose sat hard against the bubble's edge, in --dim, while every other prose
   popover in the panel (.prot-who-pop, .hostupd-bub) pads to 11-14px on --raise. Qualified with .deppop to
   outrank it by specificity rather than by hoping about source order. */
.deppop.rmode-reset-pop{max-width:300px;padding:11px 14px;background:var(--raise);
  border-color:var(--line-solid);box-shadow:0 16px 40px rgba(0,0,0,.45);
  white-space:normal;font-size:12px;line-height:1.55;color:var(--ink-2);letter-spacing:0;text-transform:none}
/* type-to-confirm field inside a destructive ConfirmSheet */
.confirm-type{display:flex;flex-direction:column;gap:6px;margin:13px 2px 2px}
.confirm-type>span{font-size:12.5px;color:var(--dim)}
.confirm-type b{color:#D65B5B;font-family:var(--mono);letter-spacing:.08em}
.ctype-input{font-family:var(--mono);font-size:13px;letter-spacing:.06em;padding:8px 11px;border-radius:8px;border:1px solid var(--line-solid);background:var(--surface-2);color:var(--ink);outline:none;transition:border-color .12s}
.ctype-input:focus{border-color:color-mix(in srgb,#D65B5B 55%,var(--line-solid))}
/* live ASN-resolution feedback under a rule/list target field */
.asn-hint{display:flex;flex-wrap:wrap;gap:6px;margin-top:5px}
.asn-tok{font-family:var(--mono);font-size:10.5px;padding:2px 7px;border-radius:20px;white-space:nowrap}
.asn-tok.load{color:var(--faint);background:color-mix(in srgb,var(--faint) 12%,transparent)}
.asn-tok.ok{color:#3EA76B;background:color-mix(in srgb,#3EA76B 13%,transparent)}
.asn-tok.bad{color:#D65B5B;background:color-mix(in srgb,#D65B5B 12%,transparent)}
:root[data-theme="dark"] .asn-tok.ok{color:#57C98A}
/* Reusable styled dropdown (Dropdown/ModeSelect) — one look everywhere, replacing native <select> option lists. */
.dropdown{position:relative;min-width:0}
.field .dropdown{width:100%}   /* field dropdowns fill their column like the native selects did */
.ddbtn{display:flex;align-items:center;gap:8px;width:100%;min-width:0;justify-content:space-between;text-align:left;cursor:pointer;
  background:var(--surface-2);border:1px solid var(--line-solid);color:var(--ink);border-radius:var(--r-sm);
  padding:9px 12px;font-family:var(--mono);font-size:13px;transition:border-color .15s,box-shadow .15s}
.ddbtn:hover,.ddbtn.on{border-color:color-mix(in srgb,var(--brand) 45%,var(--line-solid))}
.ddbtn .catpick-caret{color:var(--dim);font-size:11px}
.ddbtn:disabled{opacity:.55;cursor:not-allowed}
.ddlbl{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.ddbtn .catpick-caret{flex:none}
.ddpop{position:fixed;z-index:90;max-width:min(480px,92vw);max-height:var(--ddmaxh,320px);overflow-y:auto;
  background:var(--surface-2);border:1px solid var(--line-solid);border-radius:var(--r-sm);box-shadow:0 12px 34px rgba(0,0,0,.5);padding:5px}
.ddpop.flip{transform:translateY(-100%)}
.ddgrp{font-family:var(--mono);font-size:9.5px;letter-spacing:.11em;text-transform:uppercase;color:var(--dim);font-weight:700;padding:8px 8px 4px}
.ddopt{display:block;width:100%;text-align:left;padding:8px 10px;border:0;border-radius:6px;background:none;color:var(--ink);font:inherit;font-size:13px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ddopt:hover:not(:disabled){background:var(--dd-hover);color:var(--ink)}
.ddopt.sel{background:var(--dd-sel);color:var(--sel-ink);font-weight:600}
/* A row with its own control is selected AS A ROW. The tint has to reach past the label button and under
   the switch, or a selected row reads as a half-painted one. The inner button then stops painting its own,
   and the hover follows the same rule. */
.ddrow.sel{background:var(--dd-sel);border-radius:6px}
.ddrow.sel>.ddopt.sel{background:none}
.ddrow:hover:not(.sel){background:var(--dd-hover);border-radius:6px}
.ddrow:hover:not(.sel)>.ddopt:hover{background:none}
/* ── THE TWO LEVELS OF THE LIST ────────────────────────────────────────────────────────────────────────
   A row under a heading is indented under it; a row that answers the question on its own — "Auto", the
   node's default, a mode — is TOP LEVEL and bold, because it is a peer of the headings rather than one
   more item beneath them. Structure the reader can see without reading. */
.ddpop>.ddopt,.ddpop>.ddrow>.ddopt{font-weight:600}
.ddpop [role=group] .ddopt{padding-left:22px}
.ddopt.off{color:var(--faint);cursor:not-allowed}
/* TURNED OFF, not unavailable. `.off` is a row that cannot be chosen; `.dim` is one that CAN — an exit
   switched off still has to be selectable, because an interface pointed at it keeps that selection. */
.ddopt.dim{opacity:.45}
.ddrow:has(.ddopt.dim) .ddx{opacity:.75}
/* THE ARROW-KEY CURSOR. Focus moves for real between rows, so the focused one has to be as obvious as the
   hovered one or arrowing through a long list is navigating blind. `:focus-visible`, not `:focus`, so
   selecting with the mouse (which hands focus back to the trigger) never paints a ring. */
.ddopt:focus-visible{background:var(--dd-hover);outline:2px solid color-mix(in srgb,var(--brand) 70%,transparent);outline-offset:-2px}
/* ⚠️ ON A ROW WITH ITS OWN CONTROLS THE RING BELONGS TO THE ROW. Drawn on the inner button it stopped dead
   before the pencil and the switch, so the selected row wore a squarish box around two thirds of itself —
   which reads as a rendering fault, not as focus. Moved out to the row; the indicator is KEPT, because
   arrowing through a list with nothing showing where you are is the thing it exists to prevent. */
.ddrow>.ddopt:focus-visible{outline:none;background:none}
.ddrow:has(>.ddopt:focus-visible){outline:2px solid color-mix(in srgb,var(--brand) 70%,transparent);outline-offset:-2px;border-radius:6px}
.ddpop:focus{outline:none}
/* A row that carries its own control (`extra`). The control is a SIBLING of the option button, never inside
   it: nested interactive elements are invalid and the inner one stops taking clicks. */
.ddrow{display:flex;align-items:center;gap:4px}
.ddrow>.ddopt{flex:1 1 auto;width:auto;min-width:0}
.ddx{flex:none;display:flex;align-items:center;gap:4px;padding-right:6px}
/* The row's own rename affordance. Quiet until the row is under the pointer — a pencil on every line is
   noise, an invisible one is a feature nobody finds, so it fades in with the row. */
.ddpen{display:flex;align-items:center;justify-content:center;width:22px;height:22px;padding:0;border:0;
  border-radius:5px;background:none;color:var(--faint);cursor:pointer;opacity:.35;transition:opacity .12s,color .12s}
.ddrow:hover .ddpen,.ddrow.sel .ddpen,.ddpen:focus-visible{opacity:1}
.ddpen:hover{color:var(--brand);background:color-mix(in srgb,var(--brand) 14%,transparent)}
.ddpen svg{width:13px;height:13px}
/* Remove — same quiet-until-hovered treatment as the pencil, in the destructive colour so the two are never
   mistaken for each other at a glance. Only on rows the operator ADDED; a discovered device has no record. */
.ddrm{display:flex;align-items:center;justify-content:center;width:22px;height:22px;padding:0;border:0;
  border-radius:5px;background:none;color:var(--dangling);cursor:pointer;opacity:.35;transition:opacity .12s,background .12s}
.ddrow:hover .ddrm,.ddrow.sel .ddrm,.ddrm:focus-visible{opacity:1}
.ddrm:hover{background:color-mix(in srgb,var(--dangling) 16%,transparent)}
.ddrm svg{width:12px;height:12px}
/* ── ASKING, IN THE ROW ─────────────────────────────────────────────────────────────────────────────────
   Takes the whole control slot: a "delete?" beside a live switch reads as two questions, and the switch is
   the one thing that must not stay clickable while a delete is pending. */
.ddconf{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
.ddconf-q{font-size:11.5px;color:var(--dangling)}
.ddconf-y,.ddconf-n{padding:2px 9px;border-radius:6px;font-size:11.5px;font-weight:600;cursor:pointer;border:1px solid transparent}
.ddconf-y{background:color-mix(in srgb,var(--dangling) 16%,transparent);color:var(--dangling);
  border-color:color-mix(in srgb,var(--dangling) 45%,transparent)}
.ddconf-y:hover{background:color-mix(in srgb,var(--dangling) 26%,transparent)}
.ddconf-n{background:none;color:var(--dim)}
.ddconf-n:hover{color:var(--ink);background:var(--dd-hover)}
/* the row being asked about sits still — no hover tint competing with the question */
.ddrow:has(.ddconf):hover{background:none}
.ddopt.arming{opacity:.75}
/* Title + device + the two buttons, wrapping to their own line for the hint. */
/* Three fields on ONE line, and the actions on the right where the thing pressed last belongs. It used to
   live in a single ~370px column, which is why each field wrapped onto a line of its own and the buttons
   needed a row to themselves; spanning the whole band, they all fit. */
.exname{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:8px;flex:1 1 100%;min-width:0}
/* ⚠️ WIDE ENOUGH FOR THEIR OWN PLACEHOLDERS. This form lives in one column of a two-column row, so a basis
   that let both inputs share that column left each about 180px — and "Название — необязательно" was cut to
   "Название — необязател", which is a placeholder that cannot do its job. The basis is now larger than half
   the column, so they wrap to one per line and each gets the full width instead. */
/* ⚠️ EACH FIELD IS A LABELLED COLUMN NOW. Three boxes in a row with the field name written inside them as
   placeholder text told you what they were only until you typed — after that the form was three anonymous
   boxes and you had to remember which was which. The name moves out to a label that stays; the placeholder
   keeps only the EXAMPLE, which is the half that is still worth reading when the box is empty.
   Smaller than a `.field>label`: these sit inside a control, not above a section. */
.exname-f{display:flex;flex-direction:column;min-width:0}
.exname-f>input{width:100%;min-width:0}
.exname-lbl{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint);
  font-weight:600;margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.exname-t{flex:1 1 150px;min-width:120px}
.exname-d{flex:1 1 190px;min-width:150px}
.exname-d input{font-family:var(--mono)}   /* mono is wide — its placeholder needs the room */
/* The source picker is a dropdown that GROWS a second row when "custom" is chosen, so it gets a flex column
   of its own rather than sitting inline — otherwise the text field it opens pushes the buttons around. */
.exname-eg{flex:1 1 170px;min-width:150px}
/* The typed gateway, cards only. Narrower than the others: an IPv4 literal is a known width, and it only
   appears when the node could not detect one, so it should not push the fields that are always there. */
.exname-gw{flex:1 1 140px;min-width:120px}
.exname-gw input{font-family:var(--mono)}
/* the actions have no label above them, so they sit on the CONTROL's line, not the label's */
.exname-act{flex:none;margin-left:auto;display:flex;gap:6px;align-self:flex-end}
/* the one line that is about the KIND of thing being made, so it spans everything below the fields */
.exname-hint{flex-basis:100%;margin-top:2px}
/* Manage — the fuller answer to the same question the "Custom interface…" row asks, so it rides in that row
   rather than adding a second one. Always visible: it is a way OUT of the list, not a per-row control. */
.ddmanage{padding:3px 9px;font-size:11px;white-space:nowrap}
/* ── THE WAYS-OUT GRID ─────────────────────────────────────────────────────────────────────────────────
   Seven columns, so every row answers the same questions in the same places.

   ⚠️ NO COLUMN GAP — the separators are per-cell `border-bottom`, and a `gap` puts unpainted alleys through
   every one of them, so the rules came out as seven dashes instead of a line. The breathing room is cell
   PADDING instead, which the border runs underneath. */
/* ⚠️ THE TRACKS MUST FIT. With minimums that added up to more than the sheet, the grid overflowed and
   TYPE ran underneath USED BY — two columns printing on top of each other. Type and Used-by are sized to
   their content (a pill, a short count) with a floor, and only Title/Device flex. */
/* ⚠️ NO `align-items:center` ON THE GRID. Centre the ITEMS and each cell shrinks to its own content, so
   the seven `border-bottom`s in a row land at seven different heights — which is exactly the stepped,
   broken divider under the switches and the trash. The tracks STRETCH (the default) so every cell in a row
   is the same height and the rule is one line; the content is centred inside each cell instead. */
/* ⚠️ FIXED PIXEL TRACKS ARE A MEASUREMENT OF ONE LANGUAGE. These were sized against English and Russian
   promptly overflowed them — «Не используется» wrapped to two lines in an 88px column and the KILL-SWITCH
   header was cut mid-word. The three label columns size to their CONTENT with a floor, so each language
   gets the width its own words need; only Title and Device flex. */
.exgrid{display:grid;font-size:12.5px;margin:0 -10px}
/* Two shapes, one set of cell rules. The track count has to be in the SELECTOR as well as the template —
   `:nth-child` counts siblings, and a grid's cells are all siblings, so "first in the row" is 7n+1 in one
   and 8n+1 in the other. Getting that wrong is what put the trash icons 8px apart. */
.exgrid.g7{grid-template-columns:minmax(120px,1.4fr) minmax(115px,1.3fr) minmax(88px,max-content)
  minmax(84px,max-content) minmax(76px,max-content) minmax(62px,max-content) 62px}

/* Nine now: "Latency" sits beside the exit IP, because the two answer the same question — what is on the
   far end of this tunnel, and is it worth routing through. Kept narrow (max-content over a 64px floor): the
   value is "128 ms" or an em dash and never grows. */
.exgrid.g8{grid-template-columns:minmax(110px,1.3fr) minmax(130px,1.2fr) minmax(70px,max-content)
  minmax(110px,max-content) minmax(64px,max-content) minmax(76px,max-content) minmax(62px,max-content) 40px 40px}
/* the manage grid: the same seven plus "Leaves as", the source the traffic actually carries */
.exgrid.g8x{grid-template-columns:minmax(95px,1fr) minmax(100px,0.9fr) minmax(70px,max-content)
  minmax(178px,1.4fr) minmax(84px,max-content) minmax(76px,max-content) minmax(62px,max-content) 62px}
/* A device the node has never reported, left on Auto — the one cell here that fails silently. Marked on the
   WRAPPER, not on an input: the control is a dropdown now, and what is risky is the CHOICE, not a typo. */
.exg-inw.risky .ddbtn{border-color:color-mix(in srgb,var(--fault) 60%,transparent)}
/* ⚠️ THE WRAPPER MUST BE POSITIONED. Without `position:relative` the marker anchors to whatever ancestor
   happens to be positioned — which here was the modal itself, so five warning triangles stacked up on the
   backdrop OUTSIDE the sheet, pointing at nothing. It also has to be a flex box that fills the track, or
   the input inside it shrinks to its default size and the marker sits past its right edge. */
/* ⚠️ `position:relative` is what the warning marker anchors to — without it the marker anchors to the modal
   and stacks up on the backdrop, outside the sheet, pointing at nothing. The cell is ONE control tall now
   (the picker is a combobox), so the row height no longer changes when custom is chosen. */
.exg-inw{position:relative;display:flex;flex-direction:column;align-items:stretch;width:100%;min-width:0}
/* OUTSIDE the control, not on top of it. Sat inside the dropdown it landed between the label and the caret,
   where it read as part of the value and clipped the text it was warning about. Pinned to the DROPDOWN's
   row rather than the cell's centre, because the cell grows downward when the custom field opens and a
   centred marker would drift away from the control it is about. */
.exg-warn{position:absolute;top:10px;right:0;display:flex;color:var(--fault);pointer-events:auto;cursor:help}
.exg-warn svg{width:13px;height:13px}
/* the gutter the marker sits in — taken out of the CONTROL's width, not out of the next column's */
.exg-inw.risky{padding-right:19px}

/* ⚠️ THE ROW IS AN ELEMENT NOW, and that is what makes both of these possible. A CSS grid's cells are all
   siblings, so there is nothing to hover and nothing that means "first in the row" — which is why the trash
   icons once sat 8px apart under `:last-child`, and why only the hovered CELL used to light up.
   `display:contents` gives each row a real element for events and scoping while its cells stay grid items,
   so `:first-child`/`:last-child` finally mean what they say and the whole row highlights together. */
.exg-r{display:contents}
/* ⚠️ THE HIGHLIGHT NEEDS TO EXTEND PAST THE TEXT, or the row lights up with its first letter and its last
   icon flush against the edge of the tint. The outer cells keep real padding and the grid is pulled out by
   the same amount, so the columns still line up with everything else in the pane while the hover bleeds
   into the margin — the text is inside the highlight rather than defining it. */
.exg-r>*:first-child{padding-left:10px}
.exg-r>*:last-child{padding-right:10px}
/* A whole row opens the editor, so the cells that are not controls say so. */
.exg-c.clk{cursor:pointer}
.exgrid.g8 .exg-r:hover>.exg-c{background:color-mix(in srgb,var(--brand) 7%,transparent)}
.exg-ed{display:flex;align-items:center;justify-content:center;width:26px;height:26px;padding:0;border:0;
  border-radius:6px;background:none;color:var(--faint);cursor:pointer;transition:background .12s,color .12s}
.exg-ed:hover{background:color-mix(in srgb,var(--brand) 14%,transparent);color:var(--brand)}
.exg-ed svg{width:14px;height:14px}
/* ── THE EXIT EDITOR'S HEADER ───────────────────────────────────────────────────────────────────────────
   Three weights, in one line, with air between them: the NAME is the loud thing, the kind and the device
   are metadata beside it, and what depends on it sits far right where a count belongs. Everything after the
   title lives in one flex box so the spacing is a rhythm rather than four separate margins — jammed up
   against the h3 they read as one run-on label. */
.exed-hx{flex:1;min-width:0;display:flex;align-items:center;gap:11px;margin:0 14px 0 16px}
.sheet-head .exed-hx .exg-t{flex:none}
.exed-dev{font-size:12.5px;color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.exed-used{font-size:11.5px;color:var(--faint);display:inline-flex;align-items:center;gap:5px;white-space:nowrap}
/* Headers never wrap and never truncate — a column head that reads "КТО ИСПОЛЬЗУ" names nothing. They are
   part of what sizes the track (max-content above), so the track grows to fit the word instead. */
.exg-h{font-family:var(--mono);font-size:9.5px;letter-spacing:.09em;text-transform:uppercase;color:var(--dim);
  padding:0 8px 6px;border-bottom:1px solid var(--line-solid);white-space:nowrap}
/* ⚠️ `:first-child` / `:last-child` DO NOT MEAN "first/last in the row". Every cell of a CSS grid is a
   sibling, so those matched the first and last cell of the WHOLE grid — one title lost its left padding and
   one action cell lost its right, which is why the trash icons sat 8px apart down the column. Count by the
   track instead: with seven columns, 7n+1 is the first of each row and 7n the last. */
/* Rows are a LIST, not a form: 6px of padding on a 28px control gives a 40px row, which keeps six of them
   on screen at once — the whole point of a grid over a stack of cards. */
.exg-c{padding:6px 8px;border-bottom:1px solid var(--line);min-width:0;display:flex;align-items:center;gap:7px}
.exg-c input{padding:5px 9px;font-size:12.5px}
.exg-c.dim{opacity:.5}
/* A switch under a left-aligned header reads as knocked out of true; both are centred so the column is a
   column. The two are fixed-width for the same reason — rows must not shift as titles get longer. */
.exg-h.c,.exg-c.c{justify-content:center;text-align:center}
.exg-c input{width:100%;min-width:0}
.exg-c .mono,.exg-c input.mono{font-family:var(--mono)}
.exg-dev{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
/* Health. Hollow is "the node has not mentioned it", which is a different thing from down and must not
   borrow its colour. */
.exg-dot{flex:none;width:7px;height:7px;border-radius:50%;cursor:help}
.exg-dot.up{background:var(--online)}
.exg-dot.down{background:var(--fault)}
.exg-dot.bad{background:var(--dangling)}
.exg-dot.unknown{background:none;border:1px solid var(--line-solid)}
/* the dot answers "what state", the marker answers "you need to do something about it" — a 7px circle is
   enough for up/down, which are ordinary, and not enough for a failed registration. */
.exg-h8{display:inline-flex;align-items:center;gap:5px}
.ddwarn{display:inline-flex;align-items:center;cursor:help;flex:none}
/* The marker that OPENS its reason reads as a control: a pointer rather than the help cursor, and a hover
   that says it will do something. The plain (busy) marker keeps `cursor:help` above. */
.ddwarn-clk{cursor:pointer;border-radius:4px;padding:1px;margin:-1px;transition:background .12s}
.ddwarn-clk:hover{background:color-mix(in srgb,currentColor 16%,transparent)}
.ddwarn svg{width:13px;height:13px}
.ddwarn.warn{color:var(--fault)}
.ddwarn.bad{color:var(--dangling)}
/* IN PROGRESS, NOT A FAULT. A WARP account still registering wore the amber warning triangle, so creating
   an exit looked like breaking one. Muted rather than coloured — nothing is wrong, something is happening —
   and turning slowly enough to read as "working", not "loading". The global prefers-reduced-motion rule
   already flattens this to a still icon. */
.ddwarn.busy{color:var(--dim);cursor:default}
.ddwarn.busy svg{animation:ddspin 1.8s linear infinite}
@keyframes ddspin{to{transform:rotate(360deg)}}
.exg-act{justify-content:flex-end;gap:5px}
.exg-t{font-size:10.5px;padding:2px 8px;border-radius:99px;white-space:nowrap;font-weight:600}
.exg-t.warp{background:color-mix(in srgb,var(--online) 15%,transparent);color:var(--online)}
/* WARP+ — the one row in the list somebody paid for. Same green, so it reads as the same family, but lit:
   a ring plus a soft bloom is enough to find it at a glance without inventing a second colour. */
/* The "+" carries the meaning, so it gets the weight and the accent — and 1px up, because a "+" sits on the
   baseline and reads LOW next to capitals. */
.exg-plus{color:var(--fault);font-weight:900;position:relative;top:-1px;margin-left:1px}
.exg-t.plus{background:color-mix(in srgb,var(--online) 24%,transparent);color:var(--online);font-weight:700;
  box-shadow:0 0 0 1px color-mix(in srgb,var(--online) 55%,transparent),
             0 0 10px color-mix(in srgb,var(--online) 45%,transparent)}
.exg-t.found{background:color-mix(in srgb,var(--ready) 15%,transparent);color:var(--ready)}
.exg-t.custom{background:color-mix(in srgb,var(--pending) 15%,transparent);color:var(--pending)}
/* A pasted profile, coloured by the tunnel it actually is — the SAME pair `.iftype.wg` / `.iftype.awg` use
   for interfaces, so the vocabulary is learned once. Before this both wore the WARP green and the one
   difference that decides whether the tunnel works was invisible. */
.exg-t.exwg{background:color-mix(in srgb,var(--online) 15%,transparent);color:var(--online)}
.exg-t.exawg{background:color-mix(in srgb,var(--brand) 16%,transparent);color:var(--brand)}
/* Not a type — a state, and the only one that means "this row is not real yet". Amber, because it is the
   colour the rest of the panel uses for "not settled". */
.exg-t.unsaved{background:color-mix(in srgb,var(--fault) 16%,transparent);color:var(--fault)}
.exg-bad{font-size:10.5px;color:var(--dangling);white-space:nowrap}
/* Remove — quiet until the pointer is on it, then unmistakably destructive. */
.exg-rm{display:flex;align-items:center;justify-content:center;width:26px;height:26px;padding:0;border:0;
  border-radius:6px;background:none;color:var(--faint);cursor:pointer;transition:background .12s,color .12s}
.exg-rm:hover{background:color-mix(in srgb,var(--dangling) 16%,transparent);color:var(--dangling)}
.exg-rm svg{width:14px;height:14px}
/* Adding one takes the same two questions the grid asks: what you call it, what the kernel calls it. */
/* Used by — a count that opens the list on hover, because the count alone is the one thing that is never
   actionable ("3 users" tells you to be careful; it does not tell you WHICH three). */
.exu-wrap{display:inline-flex}
.exu-n{cursor:help;border-bottom:1px dotted var(--line-solid);white-space:nowrap}
.exg-c .faint{white-space:nowrap}
/* Inherits .deppop wholesale — surface, border, radius, shadow and its 5px padding — and the rows are the
   shared .deprow, so this reads as the same control as every other hover list rather than a private box. */
.exu-pop{min-width:250px;max-width:340px;font-size:12px}
.exu-r{gap:12px}
.exu-r b{font-family:var(--mono);font-weight:600}
.exu-r .faint{font-size:11px}
.exu-f{margin:3px 3px 1px;padding:7px 5px 2px;border-top:1px solid var(--line);color:var(--faint);
  font-size:11px;line-height:1.45;white-space:normal}
.exname .btn{flex:none}
.lgrid-head{display:flex;align-items:center;gap:10px;margin-bottom:6px}
.lg-htitle{display:flex;align-items:center;gap:9px}   /* title · count · sub — all vertically centred on one line */
.lg-htitle .seclabel{border-bottom:none;padding-bottom:0;margin:0}   /* no underline under Provider/Custom list headers */
.lg-count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:18px;padding:0 6px;font-family:var(--mono);
  font-size:11px;color:var(--dim);background:color-mix(in srgb,var(--ink) 9%,transparent);border-radius:20px}
.lg-sub{font-size:11px}
/* list grid — flex rows (no header), every cell vertically centred, clean separators */
.lgrid{display:flex;flex-direction:column}
/* The pin column is gone with the pin — this grid is a usage view now, not a control panel. What was the
   40px switch is reclaimed by the name, and the fleet-assign popover's 86px becomes the interface chips,
   which need room for real interface names rather than a count. */
.lgrow{display:grid;grid-template-columns:minmax(0,1fr) 120px minmax(0,150px) 78px 52px;align-items:center;gap:12px;
  padding:9px 4px;border-top:1px solid var(--line)}   /* pull · category(fills, truncates last) · size · fleet · caps · actions — fixed right cols keep rows vertically aligned */
.lgrow:first-child{border-top:none}
.lgrow:hover{background:color-mix(in srgb,var(--ink) 2.5%,transparent)}
.lgrow.lg-lock{opacity:.5}
.lgrow.clk{cursor:pointer}   /* the whole row opens the editor; its buttons opt out in JS */
/* STILL USED — by the BLOCKING grid, which keeps its per-node switch. The routing grid dropped its pin
   column, and deleting this rule with it would have silently unaligned the other tab's switches: one class,
   two grids. */
.lg-pull{justify-self:start;display:flex;align-items:center}
.lg-cat{display:flex;flex-direction:column;justify-content:center;gap:0;min-width:0}   /* title + tag on top, id tight under */
.lg-catmain{display:flex;align-items:center;gap:8px;min-width:0;line-height:1.25}
.lg-title{font-weight:600;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
button.lg-title{border:0;background:none;padding:0;font:inherit;font-weight:600;color:var(--ink);cursor:pointer}
button.lg-title:hover{color:var(--brand)}
.lg-id{font-family:var(--mono);font-size:10.5px;line-height:1;color:var(--faint);align-self:flex-start;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
button.lg-id{border:0;background:none;padding:0;cursor:pointer}button.lg-id:hover{color:var(--brand)}
.lg-caps{display:flex;justify-content:flex-end}
/* The interfaces asking for a list, each a way straight into the rule that asks. Chips rather than plain
   text because they are the one clickable thing in the row, and they inherit the badge language the rule
   field already uses rather than inventing a third. */
.lg-used{display:flex;flex-wrap:wrap;gap:4px;justify-content:flex-end}
.lg-ifchip{font-family:var(--mono);font-size:10.5px;color:var(--dim);background:var(--surface-2);
  border:1px solid var(--line-solid);border-radius:5px;padding:2px 6px;cursor:pointer;white-space:nowrap;
  transition:color .12s,border-color .12s,background .12s}
.lg-ifchip:hover{color:var(--brand);border-color:color-mix(in srgb,var(--brand) 45%,var(--line-solid));
  background:color-mix(in srgb,var(--brand) 10%,transparent)}
.lg-unused{font-size:10.5px;color:var(--faint);white-space:nowrap;text-align:right}
.lg-size{color:var(--dim);white-space:nowrap;font-variant-numeric:tabular-nums;font-size:12px;text-align:right;justify-self:end}
.lg-act{display:flex;align-items:center;justify-content:flex-end;gap:10px}
.lg-act .ccchip-info,.lg-act .ccchip-x{margin:0}
.lg-legend{margin-top:18px;padding-top:13px;border-top:1px solid var(--line);display:flex;flex-direction:column;gap:6px;font-size:11.5px;color:var(--dim)}
.lg-leg-row{display:flex;align-items:baseline;gap:0}
.lg-leg-row .capb,.lg-leg-row .bk-nabadge{margin-right:7px}
.btn-add{background:var(--brand);color:var(--brand-ink);font-weight:600;padding:6px 12px;font-size:12.5px;border-radius:7px;display:inline-flex;align-items:center;gap:5px;border:0}
.btn-add:hover{filter:brightness(1.07)}
.btn-add .ic svg{width:14px;height:14px}
.catpick-pop.wide{max-width:none}
/* fleet-assignment popover */
.fleet-trig{cursor:pointer;font-size:12px;color:var(--dim);white-space:nowrap;padding:3px 8px;border:1px solid var(--line-solid);border-radius:20px;display:inline-flex;align-items:center;gap:5px}
.fleet-trig:hover{color:var(--ink);border-color:color-mix(in srgb,var(--brand) 40%,var(--line-solid))}
.fleet-trig b{color:var(--ink);font-variant-numeric:tabular-nums}
.fleet-caret{font-size:9px;color:var(--faint)}
.fleetpop{padding:6px}
.fleetlist{display:flex;flex-direction:column;gap:1px;min-width:170px}
.fleetlist-h{font-family:var(--mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint);padding:3px 8px 5px}
.fleetrow{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;white-space:nowrap}
.fleetrow:hover{background:var(--surface)}
.fleetrow.off{opacity:.5;cursor:not-allowed}
.swt-off{opacity:.5;cursor:not-allowed}
.fleet-dot{width:7px;height:7px;border-radius:50%;background:var(--c);flex:none}
.fleet-nm{font-size:12.5px;color:var(--ink)}
.catpick-empty{padding:16px 10px;text-align:center;color:var(--faint);font-size:12.5px}
.capbs{display:flex;gap:4px;flex-shrink:0}
.capb{font-family:var(--mono);font-size:9px;letter-spacing:.05em;font-weight:600;padding:1px 5px;border-radius:5px}
.capb.ip{color:var(--brand);background:color-mix(in srgb,var(--brand) 15%,transparent)}
.capb.host{color:var(--smart);background:color-mix(in srgb,var(--smart) 15%,transparent)}
.capb.custom{color:var(--brand);background:color-mix(in srgb,var(--brand) 15%,transparent)}
.catpick-foot{display:flex;align-items:center;gap:8px;padding:7px 11px;border-top:1px solid var(--line)}
.catpick-count{font-family:var(--mono);font-size:10.5px;color:var(--dim);font-variant-numeric:tabular-nums}
.catpick-nav{display:flex;align-items:center;gap:6px}
.catpick-note{padding:8px 11px;border-top:1px solid var(--line);font-size:11px;line-height:1.45;color:var(--dim);background:color-mix(in srgb,var(--smart) 6%,transparent)}
.catpick-note b{color:var(--ink-2);font-weight:600}
.catpick-tick{display:inline-flex;justify-content:center;width:14px;color:var(--brand);font-size:12px;flex-shrink:0}
.catpick-add .btn-mini.on{border-color:var(--brand);color:var(--brand)}
.ccchip-x{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;padding:0;border:0;border-radius:50%;background:none;color:var(--faint);cursor:pointer}
/* Uses --dangling like every other destructive control (.btn-danger, .iconbtn.danger). It previously
   referenced an undefined token, which made both declarations invalid-at-computed-value-time — so the
   delete "x" had NO hover state at all. Never reference a custom property that isn't defined in :root. */
.ccchip-x:hover:not(:disabled){background:color-mix(in srgb,var(--dangling) 18%,transparent);color:var(--dangling)}
/* Disabled × — a rule holds this list, so unpinning it would be a no-op (§1.4). It has to LOOK unavailable:
   the button kept its pointer cursor and full colour, which is a control that lies about being clickable. */
.ccchip-x:disabled{opacity:.35;cursor:not-allowed}
.ccchip-x .ic svg{width:12px;height:12px}
/* list size summary ("142 hosts · 38 nets") + hover sample tooltip */
.listsize{cursor:default;font-size:11px;color:var(--dim);white-space:nowrap;font-variant-numeric:tabular-nums}
.listsize:hover{color:var(--ink-2)}
.rrrow .rrlint{grid-column:1/-1}   /* lint messages drop to their own full-width grid row */
.rrlint{font-size:10px;color:var(--partial);font-family:var(--mono)}
/* the catch-all footer reuses the row's grid template + the row's content inset (transparent padding+border) so
   its columns start/end identically. The grip/xbtn ghosts are invisible width-matched spacers for cols 1 & 5. */
.rrfoot{display:grid;grid-template-columns:16px minmax(0,1fr) auto minmax(0,1fr) auto;align-items:center;gap:8px;margin-top:8px;padding:0 8px;border:1px solid transparent}
.rrfoot-lead{min-width:0;display:flex;align-items:center;gap:8px}
.rrfoot-label{font-size:13.5px;color:var(--dim);font-weight:600;white-space:nowrap;margin-left:auto}   /* right-align within the column so it sits next to the → arrow */
/* Add-rule: flush with the grey rule-row's left border above (undo the 8px row padding), a touch larger, bold cyan + */
.rrfoot-lead .btn-mini{margin-left:-8px;padding:7px 13px}
.rrfoot-lead .btn-mini .ic{color:var(--brand)}
.rrfoot-lead .btn-mini .ic svg{width:16px;height:16px}
/* square icon-only mini button (the gear shortcuts next to Add rule / Create interface / Setup proxy):
   a definite 1:1 box (aspect-ratio can't resolve with two auto dims) with the glyph dead-centre. */
.btn-mini.ico{width:28px;height:28px;padding:0;gap:0;justify-content:center;flex:none}
.rrfoot-lead .btn-mini .ic path{stroke-width:2.75}
/* routing-lists gear in the footer's last column: its box is pinned to the rule-row X width so the grid's
   auto track (and thus the catch-all dropdown) never shifts; a larger glyph overflows the box harmlessly. */
/* node-connection link health: a single glowing dot (green up / amber connecting / red down) */
.lkdot{width:9px;height:9px;border-radius:50%;flex:none;background:var(--unknown);--g:var(--unknown)}
.lkdot.up{background:var(--online);--g:var(--online);animation:pulse 2.6s infinite}
.lkdot.connecting{background:var(--partial);--g:var(--partial);animation:pulse 1.5s infinite}
.lkdot.down{background:var(--dangling);--g:var(--dangling)}
.lkdot.up,.lkdot.connecting,.lkdot.down{box-shadow:0 0 8px -1px var(--g)}
.tg-warn{background:rgba(232,192,75,.16);color:var(--partial);border-color:color-mix(in srgb,var(--partial) 35%,transparent)}
.tg-del{background:rgba(242,107,130,.16);color:var(--dangling);border-color:color-mix(in srgb,var(--dangling) 35%,transparent)}   /* interface being deleted */
.tg-off{background:color-mix(in srgb,var(--unknown) 16%,transparent);color:var(--unknown);border-color:color-mix(in srgb,var(--unknown) 30%,transparent)}   /* stopped / stale — a deliberate, non-error state */
.tg-busy svg,.tg-ok svg,.tg-warn svg,.tg-del svg,.tg-off svg{width:11px;height:11px}
.ifcard-top .tg-busy,.ifcard-top .tg-ok,.ifcard-top .tg-warn,.ifcard-top .tg-del,.ifcard-top .tg-off{flex:none}
/* inactive items (offline/dangling/disconnected): grey the tags out */
.tg.muted{background:color-mix(in srgb,var(--unknown) 13%,transparent)!important;color:var(--unknown)!important;box-shadow:none!important}
/* rate cell: lights up when traffic is flowing */
.ratecell{font-family:var(--mono);color:var(--faint);font-size:12px;white-space:nowrap}
.ratecell .up{opacity:.7}
.ratecell.live{color:var(--online)}
.ratecell.live .up{color:var(--rate-up);opacity:1}

/* ───── titled icon-headed panels (information grouping) ───── */
.panel{background:var(--surface);border:1px solid var(--line);border-radius:var(--r);margin:0 0 14px;overflow:hidden}
.panel-head{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--line)}
.panel-ic{width:27px;height:27px;border-radius:8px;flex:none;display:inline-flex;align-items:center;justify-content:center;
  background:color-mix(in srgb,var(--brand) 15%,transparent);color:var(--brand)}
.panel-ic .ic{display:flex;align-items:center;justify-content:center}   /* kill the inline baseline gap so the glyph centres H+V */
.panel-ic svg{width:15px;height:15px;display:block}   /* matches the WINGS-N turn-fork accent */
.panel-head h3{margin:0;font-size:14px;font-weight:600;color:var(--ink);letter-spacing:-.01em}
.panel-count{font-family:var(--mono);font-size:11.5px;color:var(--faint)}
.panel-head .grow{flex:1}
.panel-body{padding:14px}
.panel-body.flush{padding:0}
/* key/value definition rows inside a panel */
.dl{display:grid;grid-template-columns:auto 1fr;gap:9px 18px;align-items:baseline}
.dl dt{font-size:12.5px;color:var(--dim);white-space:nowrap}
.dl dd{margin:0;font-family:var(--mono);font-size:12.5px;color:var(--ink-2);text-align:right;word-break:break-all}
/* interface-detail summary: endpoint / server address / dns / mtu on one line */
.iface-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px 22px}
.iface-grid .ig-item{display:flex;flex-direction:column;gap:6px;min-width:0}
.ig-l{font-family:var(--mono);font-size:9px;text-transform:uppercase;letter-spacing:.08em;color:var(--faint)}
.ig-v{font-family:var(--mono);font-size:12.5px;color:var(--ink-2);word-break:break-all}
.iface-amnezia{display:flex;flex-direction:column;gap:6px;margin-top:16px}
.iface-amnezia .ig-v{display:flex;flex-direction:column;gap:3px}
/* edit-interface intro: one sentence per line, label colour, small */
.iface-intro{display:flex;flex-direction:column;gap:4px;margin:0 0 16px;font-size:11px;line-height:1.4;color:var(--dim)}
/* header "up to date" flash (5s after a check-for-updates that found nothing) */
.upd-uptodate{pointer-events:none;cursor:default;color:var(--online);border-color:color-mix(in srgb,var(--online) 42%,var(--line-solid))}
.iface-intro.big{gap:9px;margin:4px 0 18px;font-size:13px;line-height:1.55;color:var(--ink-2)}
/* AmneziaWG param editor: four columns grouped by prefix (J / S / H / I) */
.awg-cols{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:9px 10px;align-items:start}
.awg-col{display:flex;flex-direction:column;gap:9px;min-width:0}
.awg-f{display:flex;flex-direction:column;gap:3px;min-width:0}
.awg-f>span{font-family:var(--mono);font-size:9px;text-transform:uppercase;letter-spacing:.06em;color:var(--faint)}
.awg-f input{font-size:12px;padding:6px 8px}
@media(max-width:560px){.awg-cols{grid-template-columns:repeat(2,minmax(0,1fr))}}
/* interface public key inline next to its label with a small copy icon */
.ipk-field{display:flex;align-items:center;gap:10px;margin-top:4px;margin-bottom:-6px}
.ipk-field>label{margin:0;white-space:nowrap}
.ipk-field .grow{flex:1}
.ipk-val{font-family:var(--mono);font-size:12px;color:var(--ink-2);word-break:break-all;min-width:0;text-align:right}
.subnet-row{margin-bottom:14px}   /* breathing room before the Endpoint / Listen-port row */
/* Wraps, never clips: the tail of this line is the INSTRUCTION ("delete & recreate to change"), so an
   ellipsis eats the one piece of information the row exists to give. It fit on one line in English and
   didn't in Russian. baseline keeps the label level with the first line when the hint runs to two. */
.subnet-row{align-items:baseline}
.subnet-row .ipk-val{text-align:left;word-break:normal;white-space:normal;overflow:visible}
.seclabel{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--faint);margin:18px 0 8px;padding-bottom:6px;border-bottom:1px solid var(--line)}
.awg-val{font-family:var(--mono);font-size:12px;color:var(--ink);padding:5px 7px;background:var(--surface-2);border:1px solid var(--line-solid);border-radius:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ipk-custom{margin-top:6px;width:100%;font-family:var(--mono);font-size:12.5px}
/* the node's Default exit: the picker, and a gear to the room behind it */
.exdd-row{display:flex;align-items:center;gap:7px}
.exdd-row>.dropdown{flex:1 1 auto;min-width:0}
.exdd-gear{flex:none;display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;
  padding:0;border:1px solid var(--line-solid);border-radius:var(--r-sm);background:var(--surface-2);
  color:var(--dim);cursor:pointer;transition:border-color .15s,color .15s}
.exdd-gear:hover{border-color:color-mix(in srgb,var(--brand) 45%,var(--line-solid));color:var(--ink)}
.exdd-gear svg{width:15px;height:15px}

/* THE COMBOBOX — a text field wearing the dropdown's trigger as its frame. The input is absolutely placed
   over the label region, so what is left exposed is the caret: click the text to type, click the caret to
   reopen the list and go back to Auto.
   ⚠️ `--dd-cover` IS THE ENTIRE TARGET. Everything left of it is covered by the input and types; this strip
   is the only way back to the list, so it is sized as a control rather than as padding around a glyph — the
   caret is ~8px and sat in a 30px gutter that read, and behaved, like a decoration. */
.ipk-combo{position:relative;display:block;--dd-cover:42px}
.ipk-combo .ddlbl{visibility:hidden}          /* the input renders in its place */
.ipk-combo .ddbtn{cursor:default;padding-right:17px}   /* centres the caret in the strip it owns */
/* The button is a SIBLING of the input, not its ancestor, so `:hover` fires only when the pointer is over
   the exposed strip — which makes it honest feedback about where the target actually is. */
.ipk-combo .ddbtn:hover{cursor:pointer}
.ipk-combo .ddbtn .catpick-caret{cursor:pointer;font-size:12px;transition:color .12s}
.ipk-combo .ddbtn:hover .catpick-caret{color:var(--ink)}
.ipk-combo>.ipk-over{position:absolute;left:1px;top:1px;bottom:1px;right:var(--dd-cover);width:auto;margin:0;
  padding:0 0 0 11px;border:0;background:transparent;border-radius:var(--r-sm) 0 0 var(--r-sm);
  font-family:var(--mono);font-size:13px;color:var(--ink);outline:none;min-width:0}
.ipk-combo>.ipk-over:focus{box-shadow:none}
/* EXTERNAL EXITS list (Settings -> Network, per node). Rows are separated by a hairline rather than boxed:
   the card they sit in is already a surface, and a card-in-card per row would out-shout the ingress/egress
   forms above them for something that is usually one or two entries. `.row2` gets a wrap only HERE, so a
   narrow viewport stacks name and device instead of squashing both. */
.exrow{padding:14px 0;border-top:1px solid var(--line)}
.exrow:first-of-type{border-top:0;padding-top:2px}
/* The row's header strip: what kind of exit this is, and what is relying on it. Quiet — it is orientation,
   not a control, so it sits above the fields at label weight rather than competing with them. */
.exrow-h{display:flex;align-items:center;gap:10px;margin-bottom:9px}
.exrow-used{font-size:11.5px;color:var(--faint);display:inline-flex;align-items:center;gap:5px}
.exrow .row2{flex-wrap:wrap}
.exrow .row2>.field{min-width:210px}
.exrow-opts{display:flex;align-items:center;gap:20px;margin-top:12px}
.exsw{display:inline-flex;align-items:center;gap:8px;font-size:12px;color:var(--ink-2);cursor:pointer;user-select:none}
.exrow>.hint{font-size:11.5px;color:var(--faint);margin-top:8px}
.exstat .exwarn{color:var(--fault)}   /* ORANGE — the one clause about what BREAKS, inside an otherwise faint line */
.exrow>.hint .err{color:var(--dangling)}
/* node egress: "where it leaves" beside "as what" — one line, stacking rather than squashing. */
.egopt-mode{font-weight:700}   /* the smart-routing entry is a MODE — same weight as the optgroup labels it sits beside */
.egrow{flex-wrap:wrap;position:relative}
.egrow>.field{min-width:210px}
/* Only the CREATE form takes the row's place, and creating is reached by CHOOSING "Custom interface…" from
   the list — which closes the list on its way out, as any choice does. So there is no popup anchored to
   this trigger while the fields are gone, and they can simply not be rendered. (An earlier version kept
   them laid out with `visibility:hidden` to protect an anchor that, in this case, is never there.) */
.egrow.editing>.field{display:none}
.egrow.editing>.exname{margin-top:0}
.exdial{gap:8px}
.exdial .dropdown{min-width:170px}
.exdef input{accent-color:var(--brand);width:15px;height:15px;margin:0;cursor:pointer}
.exadd{display:flex;flex-wrap:wrap;gap:8px}   /* the three ways to get an exit, equal weight — none is the "real" one */
/* .exprof — see `.tf-text, .exprof` below: one look for every box an operator types a config into. */
/* mesh-health tag (node-detail header + nodes-list) + its hover bubble */
.mh-pop{display:inline-flex}
.mh-tag{display:inline-flex;align-items:center;gap:16px;white-space:nowrap;cursor:default}   /* label↔value = value↔value = 16 (identical, roomy enough for 2-digit counts like 20/20) */
.mh-tag-hdr{gap:7px}
.mh-lbl-hdr{font-size:14px;font-weight:600;color:var(--ink);letter-spacing:-.01em}   /* clear gap before CPU load */
.mh-grp{display:inline-flex;align-items:center;gap:4px}      /* ↓ + number as one tight unit */
.mh-grp + .mh-grp{margin-left:0}                            /* green↔blue = the 12px tag gap, identical to label↔value */
.mh-num{font-family:var(--mono);font-weight:400;font-size:12px}          /* numbers NOT bold */
.mh-num-hdr{font-family:inherit;font-size:14px;font-weight:600;letter-spacing:-.01em}   /* header number: SAME sans font/size/weight as the label text (not the mono used in the nodes-list tag) */
/* numbers: all-up → arrow colour (inbound green · outbound blue); partial → orange; none → red */
.mh-num.mhn-down{color:var(--online)} .mh-num.mhn-up{color:var(--ready)} .mh-num.mhn-warn{color:#F0913C} .mh-num.mhn-bad{color:var(--dangling)}
.mh-ar{font-family:var(--mono);line-height:1;font-size:12.5px}   /* identical to the throughput arrows: same MONO glyph, size + weight */
.mh-ar.mh-down.s-up{color:var(--online)}
.mh-ar.mh-up.s-up{color:var(--ready)}
/* GRID, not flex: the leg columns have to line up down the bubble, and a per-row flex sizes each row on its
   own contents — which is what made 2ms / 18ms / 1ms sit at three different x positions. Every row shares one
   fixed template, so the columns align without the container needing to be the grid (which would cost the
   per-row hover box). Same frame as the other bubbles' rows: 5px 8px, 6px radius, baseline alignment. */
.mh-row{display:grid;grid-template-columns:1fr 5ch max-content;gap:12px;align-items:baseline;padding:5px 8px;border-radius:6px;white-space:nowrap}
.mh-bubble.has-loss .mh-row{grid-template-columns:1fr 7ch 5ch max-content}   /* loss column only when a leg has loss */
.mh-row:hover{background:var(--surface)}
.mh-rn{font-family:var(--mono);font-size:12.5px}
.mh-rn.mh-bold{font-weight:700} .mh-rn.mh-dim{opacity:.5}
/* the numeric columns: same mono/size the other bubbles use for values (.onlpop .on-ct), right-aligned so
   the digits stack rather than the words. */
.mh-loss,.mh-rtt{font-family:var(--mono);font-size:12px;text-align:right;font-variant-numeric:tabular-nums}
.mh-rtt{color:var(--muted)}
.mh-loss{color:var(--muted)}
/* header: .onpop-h is scoped to .onlpop, so this bubble was rendering its title with no rule at all. Same
   eyebrow as every other bubble rather than a second look. */
.mh-bubble .onpop-h{display:block;font-family:var(--mono);font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--dim);padding:4px 8px 6px}
.mh-rar{display:inline-flex;gap:6px;justify-content:flex-end}
/* loss/drop colour now comes from lossColor() (js/charts.js) as an inline value, so one ramp serves
   every surface and there is no second copy of the thresholds living in CSS. */
.mh-rar .mh-ar{font-size:13px}   /* small arrows, matching the line */
.mh-rar .mh-ar.s-up{text-shadow:0 0 8px currentColor}   /* static glow only for up status — green (down) / blue (up); yellow + red get no glow */
.deppop.mh-bubble{width:max-content;min-width:120px}   /* dynamic width — sized to the longest node name */

/* ───── interface drops bubble ─────
   Same vocabulary as the mesh bubble above (mono uppercase micro-labels, tabular numerals, 5px/8px rows,
   6px radius, hover surface) so a percentage looks the same wherever the panel shows one. The dotted
   underline on the trigger is the only affordance: the figure sits inside a link to the interface page,
   and without it there is nothing to say the number itself holds more. */
.dp-num{font-family:var(--mono);font-variant-numeric:tabular-nums;border-bottom:1px dotted color-mix(in srgb,currentColor 45%,transparent)}
.drops-pop{display:inline-block;cursor:default}
.deppop.dp-bubble{width:max-content;min-width:232px;max-width:376px}
.dp-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:10px 8px 7px;margin-bottom:4px;border-bottom:1px solid var(--line)}
.dp-pct{font-family:var(--mono);font-size:16px;line-height:1;font-variant-numeric:tabular-nums}
.dp-sub{font-size:11px;color:var(--dim)}
/* header carries the Reset action on its right — same mini-button grammar the panels use.
   ⚠️ `.onlpop .onpop-h{display:block}` is 0,2,0; a bare `.dp-h` never won, so the button sat inline
   against the title. Match the depth. */
.onlpop .onpop-h.dp-h{display:flex;align-items:center;justify-content:space-between;gap:10px}
.dp-reset{flex:none;font-family:var(--mono);font-size:9px;letter-spacing:.06em;text-transform:uppercase;
  line-height:1;padding:3px 7px;border-radius:6px;cursor:pointer;color:var(--dim);
  background:transparent;border:1px solid var(--line-solid);transition:color .12s,border-color .12s}
.dp-reset:hover{color:var(--ink-2);border-color:color-mix(in srgb,var(--ink) 30%,transparent)}
.dp-reset:disabled{opacity:.55;cursor:default}
.dp-reset.ok{color:var(--online);border-color:color-mix(in srgb,var(--online) 45%,transparent)}
/* the since-boot rate: the row's own size, not a headline — it qualifies the ratio beside it */
.dp-life{font-size:12px;font-variant-numeric:tabular-nums}
.dp-row{display:grid;grid-template-columns:max-content 1fr;gap:14px;align-items:baseline;padding:4px 8px;border-radius:6px;white-space:nowrap}
.dp-row:hover{background:var(--surface)}
.dp-l{font-family:var(--mono);font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--faint)}
.dp-v{display:flex;justify-content:flex-end;gap:13px;font-family:var(--mono);font-size:12px;font-variant-numeric:tabular-nums;color:var(--ink-2)}
/* kind label dim, its count solid — the eye should land on the number that is not zero */
.dp-kind{color:var(--dim)}
.dp-kind b{margin-left:2px;font-weight:600;color:var(--ink-2)}
.dp-kind.zero,.dp-kind.zero b{color:var(--faint);font-weight:400}
.dp-hint{margin-top:4px;padding:8px 8px 3px;border-top:1px solid var(--line);font-size:11.5px;line-height:1.45;color:var(--ink-2);white-space:normal}
.dp-foot{padding:5px 8px 1px;font-size:10.5px;line-height:1.4;color:var(--faint);white-space:normal}
/* node↔node connection info card (NOC-style: status pill + protocol tag + labeled grid) */
.conncard{border:1px solid var(--line);border-radius:12px;padding:15px 17px;background:color-mix(in srgb,var(--ink) 2.5%,transparent)}
.conncard-top{display:flex;align-items:center;gap:9px;margin-bottom:16px}
.lkpill{display:inline-flex;align-items:center;gap:7px;padding:3px 10px 3px 9px;border-radius:999px;border:1px solid var(--line);font-family:var(--mono);font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;line-height:1;color:var(--ink-2)}
.lkpill.up{color:var(--online);border-color:color-mix(in srgb,var(--online) 38%,transparent);background:color-mix(in srgb,var(--online) 12%,transparent)}
.lkpill.connecting{color:var(--partial);border-color:color-mix(in srgb,var(--partial) 38%,transparent);background:color-mix(in srgb,var(--partial) 12%,transparent)}
.lkpill.down{color:var(--dangling);border-color:color-mix(in srgb,var(--dangling) 38%,transparent);background:color-mix(in srgb,var(--dangling) 12%,transparent)}
.lkpill .lkdot{width:6px;height:6px}   /* match the node-header "reporting" pill dot */
.conn-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px 20px}
.conn-cell{display:flex;flex-direction:column;gap:5px;min-width:0}
.conn-cell .cl{font-size:10px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--faint)}
.conn-cell .cv{font-family:var(--mono);font-size:13px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.35}
.conn-cell .cv a{color:var(--brand)}
.ipk-field .copybtn:hover{color:var(--ready)}
@media(max-width:760px){.iface-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}

/* ───── fleet: merged node card (main block + health block) ───── */
.fleet2{display:flex;flex-direction:column;gap:10px}
/* Info left, health + CPU history right. The badges no longer live in the stats row (see .fnode-ifs), so the
   left column needs less width than it did — the graphs get some of it. Not all: at 0.78 the info side read
   as cramped, the badges crowding the node name. 0.86 sits between the original 0.9 and that, so the graphs
   still gain. The floor on the left is its stats row staying on ONE line with the longest label set
   (Russian, 3-digit online count); verified at this ratio. */
.fnode{display:grid;grid-template-columns:minmax(0,0.86fr) minmax(0,1.2fr);border:1px solid var(--line);border-radius:var(--r);
  background:var(--surface);overflow:hidden;text-decoration:none;color:inherit;transition:border-color .15s,background .15s}
.fnode:hover{border-color:var(--line-solid);background:var(--raise)}
.fnode.stale{opacity:.82}
.fnode-main{padding:13px 15px;display:flex;flex-direction:column;gap:11px;min-width:0}
.fnode-top .fnode-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}   /* a long node name gives way, never the badges */
.fnode-top{display:flex;align-items:center;gap:8px}
.fnode-name{font-family:var(--mono);font-weight:600;font-size:14px;color:var(--ink)}
.fnode-top .tport{font-family:var(--mono);font-size:9px;text-transform:uppercase;letter-spacing:.06em;color:var(--faint);background:var(--surface-2);padding:2px 6px;border-radius:5px}
.fnode-top .rowarrow{color:var(--faint)}
.fnode-stats{display:flex;gap:22px;flex-wrap:wrap;align-items:center}
/* Interface badges ride the NAME row, right-aligned — not the stats row. In the stats row they were a flex
   sibling of three translated labels, so their position depended on the width of the words: in Russian, with
   a 3-digit online count ("100 пользователей"), the row overflowed and flex-wrap dropped the badges onto a
   line of their own. A layout that only holds in one language isn't a layout. The name row has slack in every
   language, and type badges belong next to the node's identity anyway. */
.fnode-ifs{display:flex;gap:6px;align-items:center;flex:none;margin-left:auto}
.fnode-ifs .iftype{font-size:10px;padding:2.5px 8px;font-weight:600}   /* same .iftype badge as the node interfaces, just a touch bigger */
.fnode-stats .fl{display:block;font-family:var(--mono);font-size:9px;text-transform:uppercase;letter-spacing:.08em;color:var(--faint);margin-bottom:3px}
.fnode-stats .fv{font-family:var(--mono);font-size:12.5px;color:var(--ink-2)}
.fnode-health{padding:13px 15px;border-left:1px solid var(--line);background:var(--surface-2);display:flex;align-items:center;min-width:0}
.fnode-health .health{width:100%}
.fnode-nohealth{font-family:var(--mono);font-size:11px;color:var(--faint)}
@media(max-width:680px){.fnode{grid-template-columns:1fr}.fnode-health{border-left:none;border-top:1px solid var(--line)}}

/* ───── recent activity (action-first) ───── */
.act-row{display:flex;align-items:center;gap:11px;padding:9px 13px;background:var(--surface);text-decoration:none;color:inherit;transition:background .12s;border-bottom:1px solid var(--line)}
.actlist{border:1px solid var(--line);border-radius:var(--r);overflow:hidden}
.act-row:last-child{border-bottom:0}
.act-row:hover{background:var(--surface-2)}
.act-ic{width:26px;height:26px;border-radius:7px;flex:none;display:inline-flex;align-items:center;justify-content:center;background:var(--surface-2);color:var(--dim)}
.act-ic svg{width:14px;height:14px}
.act-what{font-size:13.5px;color:var(--ink-2)}
.act-name{font-family:var(--mono);font-size:12.5px;color:var(--ink);font-weight:500}
.act-detail{font-family:var(--mono);font-size:11px;color:var(--faint);padding:2px 7px;border-radius:5px;background:var(--surface-2)}
.act-row .act-arrow{color:var(--faint);display:flex;transition:color .15s,transform .15s}
.act-row .act-arrow svg{width:14px;height:14px}
.act-row:hover .act-arrow{color:var(--dim);transform:translateX(2px)}
.act-row.noclk{cursor:default}
.act-row.noclk:hover{background:var(--surface)}
.act-more{font-family:var(--mono);font-size:11.5px;color:var(--brand);text-decoration:none;letter-spacing:.02em}
.act-more:hover{text-decoration:underline}
.act-morewrap{display:flex;justify-content:flex-end;margin-top:9px}   /* "Show all history" sits under the feed, right-aligned */
/* Overview empty-state — no nodes on the panel yet (fresh install / all deleted) */
.nonodes{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;min-height:64vh;gap:15px;padding:24px}
.nonodes-ic{width:64px;height:64px;border-radius:16px;display:flex;align-items:center;justify-content:center;background:var(--surface-2);color:var(--dim)}
.nonodes-ic svg{width:30px;height:30px}
.nonodes h2{margin:0;font-size:20px;color:var(--ink);letter-spacing:-.01em}
.nonodes p{margin:0;max-width:440px;color:var(--dim);font-size:13.5px;line-height:1.6}
.nonodes .btn{margin-top:4px}
/* activity history grid — same bordered list, plus a category pill + delete control per row */
.acthist{display:flex;flex-direction:column;border:1px solid var(--line);border-radius:var(--r);overflow:hidden}
.acthist .act-row{border-bottom:1px solid var(--line)}
.acthist .act-row:last-child{border-bottom:0}
.act-link{display:inline-flex;align-items:center;gap:9px;text-decoration:none;color:inherit;min-width:0}
.act-link:hover .act-what{color:var(--brand)}
.act-cat{font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--dim);background:var(--surface-2);border:1px solid var(--line);padding:2px 7px;border-radius:5px;white-space:nowrap}
.acthist .iconbtn{width:26px;height:26px}
/* grouped Needs-attention row: the count leads, so a bolded number reads at a glance */
.attn-row .name b{color:var(--ink);font-weight:700}

/* ───── name cells ───── */
.namecell{display:flex;align-items:center;gap:9px;min-width:0}
.namecell>span{min-width:0;overflow:hidden;text-overflow:ellipsis}

a.tg{cursor:pointer;transition:filter .15s}
a.tg:hover{filter:brightness(1.25)}

/* ───── throughput area chart ───── */
.tp-wrap{display:flex;flex-direction:column;gap:8px}
/* A value and its unit are ONE reading: "124 K/s" must never break, or the number stacks over the unit and
   momentarily reads as two figures. Flex items default to min-width:auto, whose min-content here is just the
   longest token ("124"), so each stat could shrink and wrap internally — nowrap + no-shrink pins them whole.
   When the row genuinely runs out of width (longer non-English labels beside the range control), the ROW wraps
   and the control drops to its own line, which costs one line instead of mangling every number. */
.tp-legend{display:flex;flex-wrap:wrap;align-items:center;gap:14px;row-gap:6px;font-family:var(--mono);font-size:11px;color:var(--ink-2)}
.tp-legend .tp-k,.tp-legend .tp-peak,.tp-legend .tp-scale{white-space:nowrap;flex:0 0 auto}
.tp-legend .tp-k{display:inline-flex;align-items:center;gap:6px}
.tp-legend .sw{width:10px;height:3px;border-radius:2px;display:inline-block}
.tp-legend .sw.rx{background:var(--tp-rx)} .tp-legend .sw.tx{background:var(--tp-tx)}
.tp-legend .tp-peak{color:var(--faint)}
.tp-legend .tp-scale{color:var(--faint);border:1px solid var(--line);border-radius:99px;padding:0 7px;font-size:9.5px;letter-spacing:.03em}

/* ───── ranked horizontal bars ───── */
.rankcard{border:1px solid var(--line);border-radius:var(--r);background:var(--surface);padding:6px 4px}
.rankbars{display:flex;flex-direction:column}
.rb{position:relative;display:grid;grid-template-columns:140px 1fr auto;align-items:center;gap:14px;padding:8px 12px;text-decoration:none;color:inherit;border-radius:7px;transition:background .12s}
.rb:not(.static):hover{background:var(--surface-2)}
.rb.static{cursor:default}
.rb-label{display:flex;align-items:center;gap:6px;min-width:0;font-family:var(--mono);font-size:12.5px;color:var(--ink)}
.rb-nm{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rb-n{flex:none;display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:15px;padding:0 5px;border-radius:7px;background:var(--track);color:var(--dim);font-size:9.5px;font-weight:700;letter-spacing:.02em}
.rb-track{height:8px;border-radius:5px;background:var(--track);overflow:hidden}
.rb-track i{display:block;height:100%;border-radius:5px;transition:width .6s var(--ease);opacity:.9}
.rb-val{font-family:var(--mono);font-size:11.5px;color:var(--dim);white-space:nowrap}
@media(max-width:640px){.rb{grid-template-columns:96px 1fr}.rb-val{display:none}.rb-bub{left:122px}}   /* 12 + 96 + 14 */
/* per-peer breakdown popover for an aggregated talker — a compact mini rank-bar list (name · bar · value),
   sized to 30% of the row/content width. Hover bridge (::before) keeps it open across the gap. */
.rb-bub{position:absolute;top:calc(100% - 2px);left:166px;z-index:40;width:50%;min-width:190px;box-sizing:border-box;display:none;   /* left = row padding(12) + label col(140) + gap(14): aligns with the bar's left end */
  grid-template-columns:minmax(0,max-content) 1fr max-content;align-items:center;gap:7px 12px;
  background:var(--surface-2);border:1px solid var(--line-solid);border-radius:8px;padding:10px 13px;
  box-shadow:0 8px 26px rgba(0,0,0,.45);white-space:nowrap}
.rb.hasbub:hover .rb-bub{display:grid}
.rb-bub::before{content:"";position:absolute;left:0;right:0;top:-9px;height:9px}
.rb-bub-n{display:flex;align-items:center;gap:6px;min-width:0;font-family:var(--mono);font-size:11px;color:var(--ink)}
.rb-bub-n .tg{flex:none;min-width:30px;justify-content:center}   /* uniform tag width (wg==awg) so the names line up across rows */
.rb-bub-nm{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.rb-bub-track{height:7px;border-radius:5px;background:var(--track);overflow:hidden}
.rb-bub-track i{display:block;height:100%;border-radius:5px;opacity:.9}
.rb-bub-v{font-family:var(--mono);font-size:10.5px;color:var(--dim);text-align:right}
.ug-empty{padding:14px;font-size:13px;color:var(--dim)}

/* Users list — collapsible rows (status · name · tag · note · peers · last · rate · total · controls) */
.urows{display:flex;flex-direction:column;gap:8px}
/* overflow VISIBLE so the Nodes hover bubble (in the header) isn't clipped; rounded corners kept via explicit
   radii on the header (full when collapsed, top-only when open) and the body (bottom). */
.urow{border:1px solid var(--line);border-radius:var(--r);background:var(--surface);overflow:visible;transition:border-color .12s;scroll-margin-top:96px}   /* scrolled-to user lands a bit below the top, clear of the sticky header */
.urow>.urow-head{border-radius:calc(var(--r) - 1px)}
.urow.open>.urow-head{border-radius:calc(var(--r) - 1px) calc(var(--r) - 1px) 0 0}
.urow-body{border-radius:0 0 calc(var(--r) - 1px) calc(var(--r) - 1px)}
.urow.open{border-color:color-mix(in srgb,var(--brand) 42%,var(--line-solid));box-shadow:0 0 0 1px color-mix(in srgb,var(--brand) 18%,transparent)}
.urow.open>.urow-head{background:color-mix(in srgb,var(--brand) 7%,transparent)}
/* wrap: five buttons in one row leave ~0 slack even in English (477px of buttons in a 472px row), so any
   language with longer words overflows the sheet. Wrapping engages only when they don't fit — English keeps
   its single line, Russian takes a second one instead of being cut off. */
.editfoot{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:20px}
.editfoot .grow{flex:1}
/* one-line rows (status · name+tag+note · peers/online · last-online · rate · total · controls). The per-row
   labels are gone — the sortable header line carries the titles. Deterministic tracks so every row + the header
   line up. A touch taller than a peer row, but a single line. */
/* The right cluster (Online / Rate / Total / actions) is one fixed 524px track holding a GAP-FREE sub-grid whose
   inner widths (86/168/158/112) + 14px cell padding exactly mirror the expanded peer table — so those three
   columns land pixel-aligned above the peer grid's, regardless of the (gapped) left columns. */
.urow-head,.uhead{display:grid;grid-template-columns:20px 74px minmax(150px,1fr) auto;align-items:center;gap:12px;padding:0 14px}
/* the right cluster mirrors the embedded peer table's right columns EXACTLY (peers↔address 168, nodes↔node 120,
   then online 86 / rate 168 / total 158 / acts 112) as a gap-free sub-grid, anchored to the row's right edge — so
   Peers/Nodes/Online/Rate/Total all land pixel-aligned above the peer grid. Live drops the acts track. */
.u-right{display:grid;grid-template-columns:168px 120px 86px 168px 158px 112px;align-items:center;min-width:0}
.u-right.live{grid-template-columns:168px 120px 86px 168px 158px}
.u-right>*{padding:0 14px;box-sizing:border-box;min-width:0}
.urow-head{padding:11px 14px;cursor:pointer;transition:background .12s}
/* sortable header line above the users list */
.uhead{padding:2px 14px 7px;font-family:var(--mono);font-size:9.5px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--faint)}
.uhead .clk{cursor:pointer;user-select:none;white-space:nowrap} .uhead .clk:hover{color:var(--ink)}
.uhead .uh-r{justify-self:end;text-align:right}
.urow-head:hover{background:var(--surface-2)}
.u-exp{display:flex;color:var(--faint);transition:transform .16s}
.u-exp svg{width:14px;height:14px}
.urow.open .u-exp{transform:rotate(90deg);color:var(--dim)}
/* user-row status: bare node-style tag (dot + uppercase mono label), like the nodes list "reporting/offline"
   but a touch smaller — NOT the pill Badge used inside the grids */
.ustat{display:inline-flex;align-items:center;gap:6px;font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;line-height:normal;white-space:nowrap;color:var(--unknown)}
.ustat::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor;flex:none}
/* The status COLOUR vocabulary. Twelve of these rules were deleted wholesale in a86b6ce (the 1.6.0-beta
   release commit) — nothing replaced them, so every status except blocked/expired/expiring fell through to
   .ustat's grey default and the roster lost its colour coding: an ONLINE user read the same grey as a lost
   one. Restored from a86b6ce^, with the comments re-attached to the rules they describe (the surviving
   s-expiring had inherited "blue — restoring after unblock", which belongs to s-restoring). */
.ustat.s-online{color:var(--online)} .ustat.s-online::before{box-shadow:0 0 8px currentColor;animation:blink 2.6s infinite}
.ustat.s-ready{color:var(--ready)}
.ustat.s-partial{color:#F19AA4}
.ustat.s-pending{color:var(--pending)}
.ustat.s-creating,.ustat.s-rotating{color:var(--partial)}
.ustat.s-dangling,.ustat.s-removing{color:var(--dangling)}
.ustat.s-unknown,.ustat.s-empty,.ustat.s-off{color:var(--unknown)}   /* s-off = Live "offline" (grey, no animation) */
.ustat.s-blocked,.ustat.s-faulty{color:var(--fault)}   /* orange — a peer is blocked / faulty */
.ustat.s-disabled,.ustat.s-expired{color:var(--dangling)}   /* red — access revoked / expired */
.ustat.s-expiring{color:var(--fault)}   /* orange — about to expire */
.ustat.s-blocking{color:var(--partial)}   /* amber — block converging */
.ustat.s-restoring{color:var(--ready)}   /* blue — restoring after unblock */
/* name row: name · tag · note all on one line, name + note ellipsize */
.u-name{display:flex;align-items:center;gap:8px;min-width:0;font-size:14px;font-weight:600}
.u-name>a{color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:0 1 auto} .u-name>a:hover{color:var(--brand)}
.u-name>.un{color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:0 1 auto}   /* plain (non-link) username */
.u-note{font-size:11.5px;font-weight:400;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;flex:0 1 auto}
/* peers / online counts on one line — online GREEN only when actually online (>0) */
.u-counts{font-size:12.5px;white-space:nowrap}
.u-counts .u-pc{color:var(--ink-2)}
.u-counts .u-onc{color:var(--dim);font-weight:600} .u-counts .u-onc.on{color:var(--online)}   /* green only when >0 */
.u-counts .u-dot{color:var(--faint)}
/* servers column: node badges the user has peers on (up to 3 + "N more"); hover → by-node bubble w/ iface tags */
.u-servers{min-width:0;overflow:visible;font-size:12.5px}   /* visible so the hover bubble isn't clipped */
.srvwrap{max-width:100%;cursor:default}
.srvchips{display:inline-flex;align-items:center;gap:6px;min-width:0}
.nsrv{font-family:var(--mono);font-size:9.5px;font-weight:500;letter-spacing:.02em;padding:2px 7px;border-radius:5px;flex:none;
  color:var(--c);background:color-mix(in srgb,var(--c) 15%,transparent);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:92px}
/* multi-node aggregate: a theme-coloured badge with a server icon (like the "exits via" node badge) */
.nsrv-agg{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;padding:3px 9px;border-radius:6px;flex:none;
  color:var(--sel-ink);background:color-mix(in srgb,var(--brand) 16%,transparent);white-space:nowrap}
.nsrv-agg svg{width:12px;height:12px}
/* one row per (node, interface): node badge · iface badge · peer count — as a 3-col grid so all rows align. */
.turnbub.servbub{grid-template-columns:max-content max-content max-content;align-items:center;gap:6px 12px;white-space:nowrap;padding:10px 12px;max-width:none}
.turnwrap:hover .turnbub.servbub{display:grid}
.servbub-row{display:contents}
.servbub-pc{font-size:11px;color:var(--dim);justify-self:end}
/* .uh-srv is just a hook for the responsive-hide; the header inherits the standard .uhead .clk look (weight/colour). */
/* value columns (labels removed — the header carries the titles); rate/total right-align under their headers */
.u-last{font-size:11.5px;white-space:nowrap;color:var(--ink-2)}
.u-thru,.u-total{justify-self:end;text-align:right;white-space:nowrap}
.u-never{font-size:11px;color:var(--faint)}
.u-acts{display:flex;align-items:center;gap:3px;justify-content:flex-end}
.urow-body{border-top:1px solid var(--line);padding:12px 14px;background:var(--surface-2)}
.peerspanel{display:flex;flex-direction:column}
.toolbar.sub{margin-bottom:10px;gap:10px}
.urow-body .peergrid{background:var(--surface)}
@media (max-width:900px){
  .urow-head,.uhead{grid-template-columns:20px auto 1fr auto}
  .u-counts,.u-servers,.uh-srv,.u-last,.u-thru,.u-total,.uhead .clk:nth-child(n+4){display:none}
}
.iconbtn{cursor:pointer;width:30px;height:30px;border-radius:7px;border:1px solid transparent;background:transparent;color:var(--dim);
  display:inline-flex;align-items:center;justify-content:center;transition:all .14s}
.iconbtn[hidden]{display:none}   /* the display:inline-flex above otherwise defeats the hidden attribute (e.g. the vault-lock padlock) */
.iconbtn .ic{display:flex;align-items:center;justify-content:center}   /* kill the inline baseline gap so the glyph centres vertically (was sitting high) */
.iconbtn:hover{color:var(--ink);background:var(--raise);border-color:var(--line-solid)}
.iconbtn.danger:hover{color:var(--dangling);border-color:var(--dangling)}
/* Restore (recreate a missing interface — green, the recovery action) + Correct (fix an out-of-subnet IP — amber) */
.iconbtn.restore{color:var(--online)}
.iconbtn.restore:hover{color:var(--online);border-color:var(--online);background:color-mix(in srgb,var(--online) 12%,transparent)}
.iconbtn.correct{color:var(--fault)}
.iconbtn.correct:hover{color:var(--fault);border-color:var(--fault);background:color-mix(in srgb,var(--fault) 12%,transparent)}
/* QR (show configs) — the panel's theme/brand colour, in the peers + users grids */
.iconbtn.qr{color:var(--brand)}
.iconbtn.qr:hover{color:var(--brand);border-color:var(--brand);background:color-mix(in srgb,var(--brand) 12%,transparent)}
/* Toolbar batch buttons — same tones as the per-row icons */
.btn-restore{background:color-mix(in srgb,var(--online) 12%,transparent);color:var(--online);border:1px solid color-mix(in srgb,var(--online) 34%,transparent);font-weight:700;padding:9px 13px;border-radius:7px;cursor:pointer;transition:all .15s var(--ease);display:inline-flex;align-items:center;gap:6px}
.btn-restore:hover{background:color-mix(in srgb,var(--online) 20%,transparent)}
.btn-correct{background:color-mix(in srgb,var(--fault) 12%,transparent);color:var(--fault);border:1px solid color-mix(in srgb,var(--fault) 34%,transparent);font-weight:700;padding:9px 13px;border-radius:7px;cursor:pointer;transition:all .15s var(--ease);display:inline-flex;align-items:center;gap:6px}
.btn-correct:hover{background:color-mix(in srgb,var(--fault) 20%,transparent)}
/* fix buttons INSIDE modals reuse the ghost button shape (like Targets / Block / Rotate keys) — just recoloured,
   matching the .btn-ghost.danger pattern — so they don't read heavier than their neighbours */
.btn-ghost.restore{color:var(--online)}
.btn-ghost.restore:hover{color:var(--online);border-color:var(--online)}
.btn-ghost.correct{color:var(--fault)}
.btn-ghost.correct:hover{color:var(--fault);border-color:var(--fault)}
/* missing-interface card on the node detail (expected but gone). The Restore control sits in the header next to
   the MISSING tag (a green pill at the same weight), and the body text is justified. */
.ifcard.missing{border-color:color-mix(in srgb,var(--dangling) 40%,var(--line-solid));background:color-mix(in srgb,var(--dangling) 5%,transparent)}
/* Keep the head on ONE line: the status belongs at the top-right of the card, and wrapping dropped it onto a
   second row under the action. The name yields instead — it is repeated in the sentence below anyway. */
.ifcard.missing .ifcard-top{overflow:hidden;flex-wrap:nowrap;row-gap:6px}
.ifcard.missing .ifcard-top .ifname{min-width:0;overflow:hidden;text-overflow:ellipsis}
.mi-text{color:var(--dim);font-size:12px;line-height:1.5;text-align:justify}
.mi-ok{color:var(--online);font-weight:600}
.mi-bad{color:var(--dangling);font-weight:600}
.mi-restore{display:inline-flex;align-items:center;gap:4px;font-family:var(--mono);font-size:9.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--online);border:1px solid color-mix(in srgb,var(--online) 34%,transparent);border-radius:99px;padding:1px 8px;background:transparent;cursor:pointer;line-height:1.45;white-space:nowrap}
.mi-restore:hover{background:color-mix(in srgb,var(--online) 12%,transparent)}
.mi-restore:disabled{opacity:.5;cursor:not-allowed}
.mi-restore .ic svg{width:11px;height:11px}
/* GHOST interface — lost + keyless (recreate fresh + rekey, not restore). Crimson, semi-transparent phantom
   card (opaque on hover, like .ifcard.down); the recreate affordances reuse --dangling everywhere. */
.ifcard.ghost{opacity:.5;border-color:color-mix(in srgb,var(--dangling) 40%,var(--line-solid));background:color-mix(in srgb,var(--dangling) 5%,transparent)}
.ifcard.ghost:hover{opacity:.85}
.ifcard.ghost .ifcard-top{overflow:hidden;flex-wrap:nowrap;row-gap:6px}
.ifcard.ghost .ifcard-top .ifname{min-width:0;overflow:hidden;text-overflow:ellipsis}
.iftype.gh{background:color-mix(in srgb,var(--dangling) 14%,transparent);color:var(--dangling)}
.mi-restore.ghost{color:var(--dangling);border-color:color-mix(in srgb,var(--dangling) 40%,transparent)}
.mi-restore.ghost:hover{background:color-mix(in srgb,var(--dangling) 12%,transparent)}
.iconbtn.ghost{color:var(--dangling)}
.iconbtn.ghost:hover{color:var(--dangling);border-color:var(--dangling);background:color-mix(in srgb,var(--dangling) 12%,transparent)}
.btn-mini.ghost,.btn-ghost.gh{color:var(--dangling)}
.btn-mini.ghost:hover,.btn-ghost.gh:hover{border-color:var(--dangling);background:color-mix(in srgb,var(--dangling) 10%,transparent)}
.notice.danger{background:color-mix(in srgb,var(--dangling) 9%,transparent);border:1px solid color-mix(in srgb,var(--dangling) 30%,transparent);color:var(--dim)}
/* Restore on the interface-detail (read-only) screen keeps the full 'Restore interface' btn-mini styling */
.btn-mini.restore{color:var(--online);border-color:color-mix(in srgb,var(--online) 34%,var(--line-solid))}
.btn-mini.restore:hover{color:var(--online);border-color:var(--online);background:color-mix(in srgb,var(--online) 10%,transparent)}
/* "N updates" in the Turn-proxies panel head. Same --ready blue as the header's .turnpill, because it is the
   same fact scoped to one node — an operator who has learned that colour in the header should not have to
   learn it again here. Follows .restore's shape so the two attention buttons in a panel head read as a pair.
   Beats .panel-head .btn-mini's brand colour on specificity (two classes), which is the point. */
.btn-mini.updbtn{color:var(--ready);border-color:color-mix(in srgb,var(--ready) 40%,var(--line-solid))}
.btn-mini.updbtn:hover{color:var(--ready);border-color:var(--ready);background:color-mix(in srgb,var(--ready) 10%,transparent)}
/* Confirm-modal list of what's about to be restored/corrected */
.restore-list{margin:10px 0 2px;padding-left:18px;max-height:200px;overflow:auto;font-size:13px;line-height:1.7}
.restore-list li{color:var(--ink)}
.restore-list .rl-new{color:var(--fault);font-size:11px;border:1px solid color-mix(in srgb,var(--fault) 34%,transparent);border-radius:5px;padding:0 5px;margin-left:4px}
.iconbtn-add:hover{color:var(--brand);border-color:var(--brand);background:color-mix(in srgb,var(--brand) 12%,transparent)}   /* Add-peer (+) highlights in the theme colour */
/* down-node Recover button (replaces the rotate-token icon): green, labelled */
.iconbtn.recover{width:auto;gap:5px;padding:0 10px;color:var(--online);border-color:color-mix(in srgb,var(--online) 45%,transparent);background:color-mix(in srgb,var(--online) 12%,transparent);font-family:var(--mono);font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase}
.iconbtn.recover:hover{color:#04110b;background:var(--online);border-color:var(--online)}
.iconbtn.recover svg{width:14px;height:14px}
.linkbtn{background:none;border:0;color:var(--brand);cursor:pointer;padding:0;font:inherit}
.linkbtn.danger{color:var(--dangling)}
/* server-key drift resolution — two equal-width actions: green Restore (safe) / red Adopt (destructive) */
.keydrift-acts{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:11px}
.keydrift-acts.one{grid-template-columns:1fr}   /* re-seeded node: Adopt is the only option */
.kd-act{display:flex;flex-direction:column;gap:5px;min-width:0}
.kd-btn{width:100%;display:inline-flex;align-items:center;justify-content:center;padding:8px 12px;font-size:12.5px;font-weight:600;border-radius:7px;cursor:pointer;border:1px solid;transition:all .15s var(--ease)}
.kd-btn.kd-restore{background:color-mix(in srgb,var(--online) 11%,transparent);color:var(--online);border-color:color-mix(in srgb,var(--online) 34%,transparent)}
.kd-btn.kd-restore:hover{background:color-mix(in srgb,var(--online) 20%,transparent)}
.kd-btn.kd-adopt{background:color-mix(in srgb,var(--dangling) 11%,transparent);color:var(--dangling);border-color:color-mix(in srgb,var(--dangling) 34%,transparent)}
.kd-btn.kd-adopt:hover{background:color-mix(in srgb,var(--dangling) 20%,transparent)}
.kd-hint{font-size:11.5px;line-height:1.35}
.advtoggle{background:none;border:0;color:var(--ink-2);cursor:pointer;font:inherit;font-size:12.5px;font-weight:600;display:flex;align-items:center;gap:7px;margin:16px 0 2px;padding:0;letter-spacing:.02em}
.advtoggle:hover{color:var(--brand)}
.advtoggle .advcaret{font-size:10px;color:var(--faint);width:10px;display:inline-block}
/* One peer's QR cards on a single line — never wraps; pages with ‹ › when there are more than 3.
   Used by BOTH the peer QR modal and each peer row of the user QR modal. */
.qrrowwrap{display:flex;flex-direction:column;gap:10px}
.qrrow{display:flex;gap:14px;overflow-x:auto;scroll-snap-type:x mandatory;scrollbar-width:none;padding-bottom:2px}
.qrrow:has(> .deploy:only-child),.qrrow:has(> .upc-wrap:only-child){justify-content:center}
.qrrow::-webkit-scrollbar{display:none}
/* every carousel item is a uniform 256px column, whether a bare .deploy (peer modal) or a .upc-wrap (user modal) —
   uniform width is what keeps the scroll step and the "N of M" range honest */
.qrrow > .deploy,.qrrow > .upc-wrap{width:292px;flex:none;scroll-snap-align:start}   /* = QR_ITEM in peer-ui.js */
.qrrow > .upc-wrap{display:flex;flex-direction:column}   /* let the inner .deploy fill the stretched wrapper height */
.qrrow > .upc-wrap > .deploy{width:100%;flex:1}
.qrrow .deploy .qr,.qrrow .deploy .qr-none,.qrrow .deploy .qr-fail{flex:1;min-width:0}
.qrrow .deploy .qr-none,.qrrow .deploy .qr-fail{width:auto;height:auto;aspect-ratio:1/1}
.qrrow .deploy .qr .qrimg{width:100%}
/* The VK-fallback note is a FOOTNOTE of the card, not a column in .deploy-body — see the note beside .tg-raw,
   which is the same trap: prose inside that flex row is an unshrinkable sibling and the QR (min-width:0)
   collapses to 0px beside it, so the card looks as though the note replaced the config. */
.deploy > .vk-note{margin:0 15px 12px;color:#e0a545}
.qrrow .deploy .acts{justify-content:center}
/* equal-height carousel cards: stretch each to the tallest, pin the action row to the bottom so short cards fill */
.qrrow{align-items:stretch}
.qrrow > .deploy,.qrrow > .upc-wrap{align-self:stretch}
.qrrow > .deploy .acts{margin-top:auto}
/* app picker (turn/WDTT config): a Device-style dropdown reading "<app> by <author>", pushed to the right */
.turncfg-app{display:flex;align-items:center;gap:12px}   /* placed by the row's space-between, not a margin */
.turncfg-app > label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-2)}
.appdd .osdd-btn{min-width:210px}   /* narrowed inside .turncfg-devrow, which must stay one line */
.appdd .osdd-menu{left:auto;right:0}   /* right-aligned menu (the picker sits at the row's right edge) */
.appdd .osdd-lbl{display:inline-flex;align-items:center;gap:2px}
.appdd .app-by{color:var(--dim);font-weight:400}
.app-plain{color:var(--dangling);font-size:8.5px;font-weight:800;letter-spacing:.05em;border:1px solid color-mix(in srgb,var(--dangling) 42%,transparent);border-radius:4px;padding:0 4px;margin-left:4px;line-height:1.5}
/* copy icon on config/link textareas — same affordance as the CLI command box */
.turncfg-tawrap{position:relative}
/* Mirrors the subscription page's .cfgcopy exactly (sub.css): a corner OVERLAY, not a button beside the text —
   borderless, brand-coloured, and filled with the SAME background as the box so it masks whatever it covers.
   3px on both axes — tighter into the corner than the sub page's 5px, still clear of where the box's 8px radius
   starts to curve away from the button's own 6px one. */
.turncfg-tawrap .cmd-copy{right:3px;bottom:3px;border:none;color:var(--brand);background:var(--surface-2)}
.turncfg-tawrap .cmd-copy:active{transform:scale(.9)}
/* ...and the box itself is the affordance, as it is there: clicking anywhere in it copies, so it says so. */
.turncfg-tawrap .turncfg-ta{cursor:pointer;transition:border-color .15s,box-shadow .15s}
.turncfg-tawrap .turncfg-ta:hover{border-color:var(--brand);box-shadow:inset 0 0 0 1px var(--brand)}
/* No position override and no reserved padding: the button keeps .cmd-copy's own bottom-right corner and sits
   OVER the text, which is how every other copyable block in the product behaves (the subscription page, the
   wg/awg config views). Reserving a 40px gutter for it made this one box the odd one out and cost a column of
   text on a 292px card. */
/* A config shown as text on a peer card gets the SAME box the Alternatives sheet uses — a readonly textarea
   with the copy button in its corner — so "select it" and "copy it" work identically wherever a link appears.
   Capped here because a card is 292px wide and shares its height with its neighbours. */
.qrrow .deploy .turncfg-tawrap{width:100%}
.qrrow .deploy .turncfg-ta{max-height:190px;font-size:11px}
.qrnav{display:flex;align-items:center;justify-content:center;gap:16px}
.qrnavbtn{appearance:none;width:34px;height:34px;border-radius:50%;border:1px solid var(--line);background:var(--panel);color:var(--ink);font-size:18px;line-height:1;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0}
.qrnavbtn:disabled{opacity:.35;cursor:default}
.qrnavbtn:active:not(:disabled){transform:scale(.94)}
/* ── redesigned peer/user QR modals ── */
.qrhd{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
.qrhd-nm{font-weight:700}
.qrhd-tag{font-size:12px;font-weight:600;color:var(--faint);letter-spacing:.02em}   /* peer count sits next to the username, with a clear gap; the subscription status goes on the far right */
.qrfoot-count{align-self:center;font-family:var(--mono);font-size:10.5px;font-weight:600;letter-spacing:.04em;color:var(--faint);text-transform:uppercase;white-space:nowrap}
/* subscription lifecycle colours — the whole sentence takes one colour: green active / orange about-to-expire /
   red blocked|expired. Used by the subscription sentence (.substat) and the peer modal's clause. */
.s-active{color:var(--online)} .s-blocked,.s-expired{color:var(--dangling)} .s-expiring{color:var(--fault)}
/* the subscription sentence ("Subscription is active", "… is about to expire on …", "… expired on …"). header =
   the modal title row: Alice left · sentence centred (flex:1 fills the gap) · peer count right. center = its own
   centred line at the top of the edit-user card. */
.substat{font-size:12px;font-weight:600;white-space:nowrap}
/* …and in the modal title row it belongs on the RIGHT, beside the close button — the peer count moved next
   to the name, so nothing else claims that end. The rule went missing when the count moved: `.substat-hr`
   was still being emitted, matched nothing, and the sentence fell back to sitting flush against the title.
   The auto margin has to be on the SENTENCE, not shared with the button: two auto margins split the free
   space between them, which parks the sentence mid-row instead of at the end. */
.sheet-head .substat-hr{margin-left:auto}
.sheet-head .substat-hr ~ .x,.sheet-head .substat-hr ~ .sheet-back{margin-left:14px}
/* access-expiry date input (edit-user / edit-peer) */
.daterow{display:flex;align-items:center;gap:8px}
.datein{font:inherit;padding:7px 10px;border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--ink);color-scheme:light dark}
.unlockpanel{margin:0 0 16px;padding:13px 15px;border:1px solid var(--line);border-radius:12px;background:var(--panel-2,rgba(127,127,127,.05));display:flex;flex-direction:column;gap:10px}
.unlockpanel-msg{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--ink)}
.unlockpanel-msg .ic{color:var(--brand);flex:none}
.unlockpanel-row{display:flex;gap:8px;align-items:center}
.unlockpanel-row .subpw{flex:1}
.unlockpanel .vp-keep-row{font-size:12.5px;color:var(--muted)}
.sublink{margin:0 0 16px;display:flex;flex-direction:column;gap:9px}
/* enabled sub-link: green textbox + Rotate/Disable on the same line */
.sublink.sublink-row{flex-direction:row;align-items:center;gap:8px}
.sublink-row .suburl{flex:1;min-width:0}
.sublink-row .btn{flex:none}
.vkfield-row{display:flex;gap:8px;align-items:center}
/* the box mirrors .suburl exactly (icon + padding + radius); the input inside is borderless, so heights match */
.vkbox{flex:1;min-width:0;box-sizing:border-box;min-height:30px;display:flex;align-items:center;gap:9px;padding:5px 11px;border:1px solid var(--line);border-radius:8px;transition:border-color .12s,background .12s}
.vkbox > .ic{flex:none}
.vkbox-input{flex:1;min-width:0;border:0;background:transparent;padding:0;font-family:var(--mono);font-size:12px;line-height:16px;color:var(--ink);outline:none;box-shadow:none}
.vkbox-input:focus{box-shadow:none;outline:none;border:0}   /* the .vkbox draws the border; kill the global input:focus glow so there's no bluish inner border */
/* save status sits at the LEFT of the button group, before the Save button (margin-right:auto pushes it left) */
.vk-status{margin-right:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;white-space:nowrap}
.vk-status.ok{color:var(--online)}
.savedmsg{font-size:11.5px;font-weight:600;white-space:normal;line-height:1.35;max-width:62%;align-items:flex-start}
.savedmsg .ic{margin-top:1px}
.obfctl-lbl{color:var(--ink-2);font-weight:600}
.notgen{font-size:11.5px;font-weight:600;color:#e0a85f;white-space:nowrap}
.vk-status.err{color:var(--dangling)}
.vk-status .ic{width:14px;height:14px}
.vkfield-row .btn{flex:none}
/* fixed-width trailing button group → the VK input and the sub-link box line up to the SAME width, and every
   button (Save / Rotate / Disable) is the same size so Save sits right under Disable */
.fieldbtns{flex:none;display:flex;gap:8px;justify-content:flex-end;align-items:center}
.fbtn{width:100px;flex:none;justify-content:center;text-align:center;white-space:nowrap;padding-left:8px;padding-right:8px}   /* the 4 link-row buttons all one width; the input shrinks to fit */
.sublink-field{margin-bottom:14px}
.sublink-field .sublink{margin:0}
/* user-modal peer card: the whole card opens the peer modal (except the QR + action buttons).
   `.hot` is set by JS only while the pointer is over a clickable region, so hovering the QR or a button
   never lights the card up. */
.upc-wrap{cursor:pointer;border-radius:var(--r);transition:box-shadow .12s}
.upc-wrap.upc-static{cursor:default}
.upc-wrap.hot .deploy{border-color:color-mix(in srgb,var(--brand) 55%,var(--line-solid))}
.upc-wrap.hot .upc-head{background:color-mix(in srgb,var(--brand) 9%,transparent)}
.upc-head{padding:10px 14px;border-bottom:1px solid var(--line);display:flex;flex-direction:column;gap:5px;transition:background .12s}
.upc-l1,.upc-l2{display:flex;align-items:center;gap:7px}
.upc-nm{font-family:var(--mono);font-weight:700;font-size:13px;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.upc-srv{font-family:var(--mono);font-size:12px;font-weight:600}
.upc-deps{font-family:var(--mono);font-size:10.5px;font-weight:600;color:var(--faint);letter-spacing:.02em}
/* Primary badge overlays the gap above the QR (peer modal): centered and tucked just above the QR, so it
   never pushes the QR down — the QR stays aligned with the sibling cards */
.qr-primary{position:absolute;top:11px;left:50%;transform:translateX(-50%);z-index:1;font-family:var(--mono);font-size:9px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--online);border:1px solid color-mix(in srgb,var(--online) 40%,transparent);border-radius:5px;padding:1px 6px;line-height:1.3;background:var(--surface)}
.qr-backup{color:var(--partial);border-color:color-mix(in srgb,var(--partial) 40%,transparent)}
/* "← Go back" in place of × (child modal opened from the user view) */
.sheet-back{appearance:none;margin-left:auto;display:inline-flex;align-items:center;gap:5px;background:transparent;border:1px solid var(--line-solid);border-radius:8px;color:var(--ink);font:inherit;font-size:12.5px;font-weight:600;padding:5px 11px;cursor:pointer}
.sheet-back:hover{background:var(--raise);border-color:var(--brand)}
.sheet-back .ic{width:14px;height:14px}
/* disabled-subscription: message + Enable button on one line */
.sublink-off{flex-direction:row;align-items:center;justify-content:space-between;gap:14px}
.sublink-off-msg{font-size:12px;color:var(--muted,#8a97ad);line-height:1.4}
.sublink-off .btn{flex:none}
.qrnavcount{font-family:var(--mono);font-size:12px;color:var(--muted);min-width:44px;text-align:center}
.nmwrap{display:flex;flex-direction:column;gap:1px;min-width:0}
.fieldrow{display:flex;gap:14px;flex-wrap:wrap}
.fieldrow > .field{flex:1;min-width:140px;margin:0}
/* Access & TLS */
.mt8{margin-top:8px}
/* declarative host: a config block that can be copied out, and the one-line chooser above it. Wide
   Nix lines scroll inside the block — the settings card must not scroll sideways. */
.nixblock{white-space:pre;overflow-x:auto;padding:10px 12px;border-radius:8px;border:1px solid var(--line);
  background:var(--code-bg,rgba(127,127,127,.09));margin:0 0 8px;font-size:.85em;line-height:1.5}
.segrow{display:flex;gap:6px;margin:0 0 10px}
.seg{appearance:none;border:1px solid var(--line);background:transparent;color:var(--muted);font:inherit;font-size:12px;
  font-weight:700;padding:5px 13px;border-radius:20px;cursor:pointer}
.seg:hover{color:var(--brand);border-color:color-mix(in srgb,var(--brand) 55%,var(--line-solid))}
.seg.on{background:color-mix(in srgb,var(--brand) 16%,transparent);border-color:var(--brand);color:var(--brand)}
/* ── Datapath switch ───────────────────────────────────────────────────────────────────────────────────
   ONE control with two halves, not two buttons that happen to be adjacent: a single track, equal-width
   sides, and the chosen one filled. Equal widths are fixed rather than content-sized, so the switch does
   not jump when the labels change length between languages ("Transit"/"Relay" vs «Транзит»/«Релей»).
   Forward wears the theme's own colour; Relay wears violet, so an operator can see at a glance which of
   their links are doing something unusual. */
.dpsw{display:inline-flex;gap:3px;padding:3px;border:1px solid var(--line);border-radius:22px;
  background:var(--surface-2)}
.dpsw button{appearance:none;border:0;background:transparent;color:var(--dim);font:inherit;font-size:12.5px;
  font-weight:700;padding:6px 14px;min-width:112px;text-align:center;border-radius:19px;cursor:pointer;
  transition:background .15s var(--ease),color .15s var(--ease)}
.dpsw button:hover:not(.on):not(:disabled){color:var(--ink-2)}
.dpsw button:disabled{opacity:.42;cursor:not-allowed}
.dpsw button.on{background:color-mix(in srgb,var(--brand) 20%,transparent);color:var(--brand)}
.dpsw button.on.sw-relay{background:color-mix(in srgb,var(--relay) 28%,transparent);color:var(--relay-2);
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--relay) 50%,transparent)}
/* a percentage typed by hand — the spinners are noise at this size and nudge the value by accident */
.dp-cpu input::-webkit-outer-spin-button,.dp-cpu input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.dp-cpu input{-moz-appearance:textfield;appearance:textfield}
/* The section's own name sits ON the control row, in the same register as the cap's label, so the whole
   datapath choice reads as one line rather than a heading with things under it. The rule that used to come
   from .seclabel moves onto the section wrapper, so the separation from the dial fields above is kept. */
.dp-sec{border-top:1px solid var(--line);margin:18px 0 0;padding-top:13px}
/* ⚠️ SCOPED TO .dp-sec ON PURPOSE. `.dp-row` / `.dp-l` were already the drops/loss BUBBLE's row classes,
   defined further up as a two-column grid whose value column is pinned right. Redefining them unscoped here
   — later in the file, same specificity — silently won everywhere and collapsed every bubble row to a
   left-packed flex line, so labels and numbers ran together with no right edge to read down. */
.dp-sec .dp-row{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin:0 0 9px}
.dp-sec .dp-l{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--faint);white-space:nowrap}
.dp-cpu{display:flex;align-items:center;gap:9px;margin-left:6px}
.dp-cpu input{width:78px;text-align:right;padding:5px 9px}
.dp-cpu>span.u{color:var(--faint);font-size:12.5px}
.dp-why{margin:0 0 2px}                       /* the explanation sits full width under the row it explains */
.dp-why + .dp-why{margin-top:7px}
.subaddr{display:flex;flex-direction:column;gap:6px;padding:12px 14px;border:1px solid var(--line);border-radius:10px;background:var(--card-2,rgba(127,127,127,.05))}
.subaddr-row{display:flex;gap:12px;align-items:baseline}
.subaddr-k{flex:0 0 96px;color:var(--muted);font-size:12.5px}
.subaddr-v{flex:1;min-width:0;word-break:break-all;font-size:13px}
.subaddr.wide .subaddr-k{flex-basis:158px}   /* named services, not one-word labels */
@media (max-width:520px){.subaddr-row{flex-direction:column;gap:2px}.subaddr.wide .subaddr-k{flex-basis:auto}}
.ci{display:inline-flex;align-items:center;justify-content:center;width:15px;height:15px;border-radius:50%;
  font-size:11px;font-weight:700;font-style:normal;color:var(--brand-ink);background:var(--warn,#e0a030);cursor:help;vertical-align:middle}
/* inline "port not reachable" hint — exclamation-in-triangle in the warn colour (no circle background).
   Sized to the 10.5px uppercase label and nudged up so it sits on the caps line, not below it. */
.ciw{display:inline-flex;align-items:center;color:var(--dangling);cursor:help;vertical-align:middle;position:relative;top:-2px}
.ciw svg{width:13px;height:13px}
/* subscription languages picker (Settings → Subscriptions) */
.sublangs{display:flex;flex-direction:column;gap:8px}
.sublang{display:flex;align-items:center;gap:12px;padding:8px 12px;border:1px solid var(--line);border-radius:9px}
.sublang.on{border-color:color-mix(in srgb,var(--brand) 40%,var(--line))}
.sublang-en{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:600;color:var(--ink)}
.sublang-en input{width:16px;height:16px;accent-color:var(--brand)}
.sublang-def{margin-left:auto;appearance:none;border:1px solid var(--line);background:transparent;color:var(--muted);font:inherit;font-size:12px;font-weight:700;padding:5px 11px;border-radius:20px;cursor:pointer}
.sublang-def.on{background:color-mix(in srgb,var(--brand) 16%,transparent);border-color:var(--brand);color:var(--brand)}
.sublang-def:disabled{opacity:.4;cursor:default}
.subpw{width:100%;max-width:420px;padding:8px 11px;border:1px solid var(--line);border-radius:9px;background:var(--bg);color:var(--ink);font:inherit;font-size:13px}
/* vault-unlock prompt (encryption key needed for a user-triggered action) */
.vaultprompt{display:flex;flex-direction:column;gap:12px}
.vaultprompt .vp-reason{margin:0;color:var(--ink-2);line-height:1.55}
.vaultprompt .vp-skip{margin-top:2px}
.vp-keep-row{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px}
.vp-keep-row input{flex:none;width:16px;height:16px;padding:0;margin:0 0 0 -2px;cursor:pointer;accent-color:var(--brand)}   /* base input{width:100%;padding} otherwise blows the checkbox up to full width; -2px aligns the glyph with the text above */
.vp-keep-row + .hint{margin-top:5px;font-size:11.5px;color:var(--faint);line-height:1.45}   /* form-style explanation under the checkbox (also covers the unlock bar, outside .field) */
.vaultprompt .vp-keep,.notice .vp-keep{margin-top:2px}
/* standout reassurance beside "re-distribute the configs" warnings — subscriptions auto-update */
.sub-auto{display:flex;align-items:flex-start;gap:8px;margin-top:10px;padding:8px 11px;border-radius:8px;
  background:rgba(63,216,154,.10);border:1px solid rgba(63,216,154,.34);color:var(--ink);font-size:12.5px;line-height:1.45}
.sub-auto svg{flex:none;margin-top:1px;width:15px;height:15px;color:#2fb883}
.sub-auto b{color:var(--ink)}
/* the URL + copy row — one hover target; clicking anywhere copies */
.suburl{display:flex;align-items:center;gap:9px;padding:5px 11px;border:1px solid #22c55e;border-radius:8px;background:color-mix(in srgb,#22c55e 16%,var(--bg));cursor:pointer;transition:border-color .12s,background .12s}
.suburl:hover{border-color:#22c55e;background:color-mix(in srgb,#22c55e 26%,var(--bg))}
.suburl > .ic{color:#22c55e;flex:none}
.suburl-txt{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--mono);font-size:12px;color:var(--ink)}
.suburl-copy{flex:none;display:inline-flex;color:var(--muted,#8a97ad)}
.suburl:hover .suburl-copy{color:#22c55e}
.suburl-copy.ok{color:var(--online)}

/* ───── login page ───── */
.login-wrap{min-height:calc(100vh - 56px);display:flex;align-items:center;justify-content:center;padding:24px}
.login-card{width:100%;max-width:360px;background:var(--surface);border:1px solid var(--line);border-radius:var(--r);padding:26px 24px;box-shadow:var(--shadow)}
.login-brand{display:flex;align-items:center;gap:10px;margin-bottom:16px}
.login-card h2{margin:0 0 18px;font-size:18px;font-weight:600;color:var(--ink)}
.login-card .field{margin-bottom:12px}
/* header icon buttons (account / logout) */
.iconbtn.lg{width:36px;height:36px;border-radius:9px}
.appbar-right .iconbtn{color:var(--dim)}
/* logged-out: keep only the wordmark in the bar */
body.loggedout .tabs,body.loggedout .appbar-right,body.loggedout .appver{display:none}

/* ───── history range tabs (live/day/week/month) ───── */
.chartwrap{display:flex;flex-direction:column;gap:8px}
.chart-head{display:flex;align-items:center;gap:10px 18px;min-height:22px;flex-wrap:wrap}
.chart-head .hist-cap{margin:0}
/* node-detail Health: meters occupy ~60% (each ~20%), range tabs ride the same row, right */
.chart-head .health-cols{flex:1 1 58%;min-width:300px;gap:14px 22px}
.rangetabs{display:inline-flex;gap:2px;background:var(--surface-2);border:1px solid var(--line);border-radius:8px;padding:2px}
.panel-tools{display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end}   /* header-hosted controls: Peers/Mesh + range picker */
.rtab{cursor:pointer;border:0;background:transparent;color:var(--dim);font-family:var(--mono);font-size:10px;text-transform:uppercase;
  letter-spacing:.06em;padding:4px 9px;border-radius:6px;transition:all .14s}
.rtab:hover{color:var(--ink-2)}
.rtab.on{background:var(--brand);color:var(--brand-ink);font-weight:600}
/* "paused" pill: node isn't reporting right now, but the last-collected data stays charted */
.rt-paused{display:inline-flex;align-items:center;gap:5px;font-family:var(--mono);font-size:9.5px;text-transform:uppercase;
  letter-spacing:.07em;color:var(--pending);background:color-mix(in srgb,var(--pending) 14%,transparent);
  border:1px solid color-mix(in srgb,var(--pending) 34%,transparent);border-radius:6px;padding:3px 8px;white-space:nowrap}
.rt-paused::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--pending);animation:rtpulse 1.6s var(--ease) infinite}
@keyframes rtpulse{0%,100%{opacity:1}50%{opacity:.3}}

/* node-detail: push sync/uptime + chart range tabs to the right */
.noderibbon .nr-sync{margin-left:auto}
.chart-head .grow{flex:1}
.tp-legend .grow{flex:1}
/* plain (info) notice — used by the node-removal sheet */
.notice{background:var(--surface-2);color:var(--ink-2)}
.notice .ic{color:var(--ready);flex:none}

/* ───── peers grid ───── */
/* clickable sortable headers (order-by) */
.peergrid th.clk{cursor:pointer;user-select:none}
.peergrid th.clk:hover{color:var(--ink)}
/* Live grid: status is just a green (online) / grey (offline) dot — a tiny column; the rest shifts left */
/* live grids are now always `loc` (dot + iface badge in the status) → the old bare-dot 26px rule only applies to a
   non-loc live grid, if any. loc live status width is set by the h-status rules. */
.peergrid.live:not(.loc) th:first-child,.peergrid.live:not(.loc) td.c-status{width:26px;text-align:center;padding-left:12px;padding-right:0}
/* cumulative transfer — SAME colours as the rate cell: faint when nothing moved, green ↓ / blue ↑ once it has */
.xfer{font-family:var(--mono);color:var(--faint);white-space:nowrap} .xfer .up{opacity:.7}
.xfer.live{color:var(--online)} .xfer.live .up{color:var(--rate-up);opacity:1}
.peergrid .xfer{font-size:11.5px}
.peergrid td.c-rate,.peergrid td.c-total,.peergrid th{white-space:nowrap}   /* keep 4-digit rates (1023 MB/s) on one line */
/* Rate/Total are numeric — right-align header + values so the header text ends right above where the values end */
.peergrid th.h-rate,.peergrid th.h-total,.peergrid td.c-rate,.peergrid td.c-total{text-align:right}
.peergrid td.rowacts{text-align:right;white-space:nowrap}
/* the user-EXPANDED peer grid uses a FIXED column layout so its Online/Rate/Total/actions columns sit at
   stable, content-independent x-positions — the user-row header is aligned to exactly these widths so the
   two line up vertically regardless of the data. (The standalone Peers screen stays auto-width.) */
/* All peer grids use FIXED column layout. Standalone grids (interface detail / Peers module / live Peers) get
   balanced % widths (Title roomy, the rest even). Embedded (user-expanded) grids override with px widths that
   mirror the user-row .u-right so Peers/Nodes/Online/Rate/Total align vertically above them. */
.peergrid{width:100%;table-layout:fixed}
.peergrid th.h-status{width:9%}
.peergrid.loc th.h-status{width:18%}   /* loc grids put the iface badge next to the status: it must hold a
   long interface name (qwdtt10009) as well as the status chip. table-layout is FIXED, so this % is the
   real width — at 11% the cell needed 167px and got 118, and the badge spilled out of it. */
.peergrid th.h-user{width:14%}
.peergrid.loc th.h-user{width:11%}   /* the loc grid's user cell holds only the compact assign picker,
   so its share goes to the status cell, which carries the interface name too */
.peergrid th.h-title{width:13%}
.peergrid th.h-ep{width:13%}
.peergrid th.h-addr{width:11%}
.peergrid th.h-node{width:16%}   /* aggregate view packs node name + iface badge here — give it room so names don't ellipsize */
.peergrid th.h-online{width:8%}
.peergrid th.h-rate{width:15%}
.peergrid th.h-total{width:14%}
.peergrid th.h-acts{width:11%}
.urow-body .peergrid th.h-status{width:150px}
.urow-body .peergrid th.h-title,.urow-body .peergrid th.h-ep{width:auto}   /* flexible left in the embedded fixed layout */
.urow-body .peergrid th.h-addr{width:168px}   /* ↔ user-row Peers */
.urow-body .peergrid th.h-node{width:152px}   /* ↔ user-row Nodes — fit full names (spb-rostelecom / sgp-digitalocean) */
.urow-body .peergrid th.h-online{width:86px}
.urow-body .peergrid th.h-rate{width:168px}
.urow-body .peergrid th.h-total{width:158px}
.urow-body .peergrid th.h-acts{width:112px}
/* embedded (loc) grid: the interface badge sits beside the STATUS so you see which iface is online. */
.peergrid td.c-status{white-space:nowrap}
.peergrid td.c-status>*{vertical-align:middle}
/* ⚠️ THE INTERFACE BADGE LIVES IN THE STATUS CELL, so its right edge is the boundary with the title — with
   no padding there the two run together and read as one string ("AWG11003Router Папа"). And because the
   cell is nowrap and unbounded, a long interface name pushes the title further on that row alone, so the
   column never lines up down the grid. Give the cell a right gutter, and cap the badge so one long name
   cannot set the column width for everybody. */
.peergrid.loc td.c-status{padding-right:15px}
.peergrid.loc td.c-status>.tg{margin-left:8px;max-width:104px;overflow:hidden;text-overflow:ellipsis;display:inline-block}
.peergrid.loc td.c-status>.badge{max-width:132px;overflow:hidden;text-overflow:ellipsis}
/* live grids (standalone + embedded): a touch more air between the status dot and the iface badge. */
.peergrid.loc.live td.c-status>.tg{margin-left:9px}
.peergrid.loc.live td.c-status>.turnwrap{gap:9px}
/* in LIVE embedded the status has just a dot + iface (no wide pill) → narrower, so Title sits closer to it. */
.urow-body .peergrid.loc.live th.h-status,.urow-body .peergrid.loc.live td.c-status{width:112px;text-align:left;padding-left:14px}
/* a long title / user name must not widen the table past its wrap: the fixed layout ellipsis-truncates cells
   instead of widening, so the wrap never needs to scroll — and we keep it overflow:visible so hover bubbles
   (status "why", nodes breakdown, "connected via") are NEVER clipped by the wrap, wherever the row sits.
   The wrap gave the rounded frame via its clip; with overflow:visible the table rounds its own corner cells
   (peergrid cells use only bottom-borders, so border-collapse:separate renders identically). */
.tablewrap:has(>table.peergrid){overflow:visible}
table.peergrid{border-collapse:separate;border-spacing:0}
.peergrid thead th:first-child{border-top-left-radius:calc(var(--r) - 1px)}
.peergrid thead th:last-child{border-top-right-radius:calc(var(--r) - 1px)}
.peergrid tbody tr:last-child td:first-child{border-bottom-left-radius:calc(var(--r) - 1px)}
.peergrid tbody tr:last-child td:last-child{border-bottom-right-radius:calc(var(--r) - 1px)}
/* A cell that doesn't fit CUTS with an ellipsis; it never wraps. nowrap on the CELL is the part that matters:
   the title cell holds a lifecycle icon next to the text, and without it the icon wrapped onto its own line
   above the title (the text truncated fine — the icon was what broke the row's alignment). */
.peergrid td.c-name{white-space:nowrap}
/* Truncate against the CELL, not a guess. A flat 180px was wider than the title column itself (13% of the
   grid ≈ 142px at 1500px), so a long title ran past its cell and printed over the IF badge next to it. */
.peergrid td.c-name>b,.peergrid td.c-name>span{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}
.peergrid td.c-name>.lc-ic{vertical-align:bottom}   /* share the baseline with the truncated text, not the line above */
.peergrid td.usercell .namecell>span{max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* fleet-wide column: server name (left) + interface tag (right) on one line */
.peergrid .srvcell{display:flex;align-items:center;justify-content:space-between;gap:10px;min-width:0}
.peergrid .srv-name{font-family:var(--mono);font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* The badge in this cell — an interface name, or a GROUPED row's "4 interfaces" count — never shrinks;
   only the node name beside it may ellipsize, and only when the column genuinely runs out. Without this
   the wider count badge squeezed the name to "msk-m…" in a column that had room for both. */
.peergrid .srvcell > .tg,.peergrid .srvcell .tgt-frontpop{flex:none}
/* The grouped IF badge's bubble: one deployment per line. It reuses .deppop's row styling, so it needs
   no layout of its own — only the room .tgt-frontbub would otherwise constrain. */
.iflistbub{min-width:230px;width:max-content}
/* user cell is a link to the user screen — blue (matches the upload total) so admins know it navigates away */
.peergrid .namecell{color:var(--ready);justify-content:space-between;width:100%} .peergrid .namecell:hover{text-decoration:underline}   /* Live/Users lists: username is plain text, not a link */
.peergrid .namecell svg{width:14px;height:14px;color:var(--ready);flex:none;opacity:0;transition:opacity .12s;position:relative;top:3px}   /* nudge down to sit on the text's baseline */
/* hovering an assigned user's cell tints the whole row blue + reveals the user icon */
.peergrid tbody tr.clk:has(td.usercell.linked:hover){background:color-mix(in srgb,var(--ready) 9%,transparent)}
.peergrid td.usercell.linked:hover .namecell svg{opacity:1}
/* peers-grid pagination bar */
.pager{display:flex;align-items:center;justify-content:flex-end;gap:12px;margin-top:14px;padding:6px 2px;font-size:12.5px}
.pager-size{display:flex;align-items:center;gap:9px;font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--dim)}
.pager-size .selwrap{width:auto;min-width:62px;padding:6px 26px 6px 10px}
/* …and the padding has to reach the BUTTON — on a <select> it sat on the control itself. The 26px right
   inset was room for the native caret drawn as a background image; the Dropdown draws its own, so this is
   symmetric again. */
.pager-size .dropdown.selwrap{padding:0}
.pager-size .dropdown.selwrap .ddbtn{padding:6px 10px;font-size:12.5px}
.pager-info{font-family:var(--mono);color:var(--faint)}
.pager-pg{font-family:var(--mono);min-width:52px;text-align:center;color:var(--ink-2)}
.pager .btn{padding:6px 12px}
/* extra-deployment count next to the address; hover or tap opens a bubble (.deppop) of the others */
.depmore{position:relative;margin-left:7px;padding:1px 5px;border-radius:5px;color:var(--ready);font-family:var(--mono);font-size:11px;cursor:pointer;
  background:color-mix(in srgb,var(--ready) 10%,transparent);border:1px solid color-mix(in srgb,var(--ready) 28%,transparent);transition:background .12s,border-color .12s;user-select:none}
.depmore:hover,.depmore.on{background:color-mix(in srgb,var(--ready) 20%,transparent);border-color:color-mix(in srgb,var(--ready) 55%,transparent)}
.deppop{position:fixed;z-index:80;min-width:210px;padding:5px;background:var(--surface-2);border:1px solid var(--line-solid);
  border-radius:var(--r-sm);box-shadow:0 12px 34px rgba(0,0,0,.5);cursor:default}
.deppop .deprow{display:flex;align-items:center;gap:9px;padding:6px 8px;border-radius:6px;white-space:nowrap}
/* header update-widget hover bubbles (update-to / fix-issues) — rich content in place of the plain title tooltip */
.hostupd-bub{z-index:90;min-width:440px;max-width:680px;padding:14px 17px 12px;white-space:normal;line-height:1.5;text-transform:none;letter-spacing:0;font-family:var(--sans);
  display:flex;flex-direction:column;max-height:70vh}   /* a release with a long list of notes would otherwise run off the bottom of the screen */
/* Only the NOTES scroll: the header carries the ‹ Prev / Next › nav and the version being read, and the
   footer carries the click-to-update line — scrolling either of those away would hide the way out of a
   long entry. The list bleeds 6px into the bubble's own padding so its scrollbar sits in that gutter and
   the notes stay aligned with the header above them. */
.hostupd-bub > .hub-h,.hostupd-bub > .hub-foot{flex:0 0 auto}
.hostupd-bub > .hub-list{overflow-y:auto;min-height:0;padding-right:6px;margin-right:-6px;scrollbar-color:var(--faint) transparent}
.hostupd-bub > .hub-list::-webkit-scrollbar-thumb{background:var(--faint)}   /* Safari < 18.2, which has no scrollbar-color (elsewhere the two agree, so only the colour is restated) */
.hub-h{display:grid;grid-template-columns:1fr auto 1fr;align-items:baseline;gap:10px;margin-bottom:11px;padding-bottom:10px;border-bottom:1px solid var(--line)}
.hub-title{justify-self:start;min-width:0}
.hub-h > .hub-nav-group{justify-self:center}
.hub-h > .hub-date{justify-self:end}
/* ⚠️ THE BUBBLE'S OWN ACTION EXISTS FOR TOUCH. Once this bubble opens it covers the badge that opened
   it, and a phone has no mouseout — so without a control in here the only exit is a tap elsewhere,
   which used to close the update modal underneath instead. Top-right, beside the date, so it reads as
   the header's action rather than a row in the notes. min-height keeps it tappable in a dense bubble. */
/* ⚠️ grid-column:3 EXPLICITLY. .hub-h is `1fr auto 1fr`, and this bubble has neither nav buttons nor
   a date — so as the second child the button was auto-placed into the MIDDLE column and sat against
   the title. Naming the column pins it to the right edge whatever else the header does or doesn't hold. */
.hub-h > .hub-end{grid-column:3;justify-self:end;display:inline-flex;align-items:center;gap:9px}
.hub-act{appearance:none;border:1px solid var(--brand);background:color-mix(in srgb,var(--brand) 14%,transparent);
  color:var(--brand);cursor:pointer;border-radius:6px;padding:3px 12px;min-height:26px;font-size:11px;font-weight:700;
  line-height:1.4;flex:0 0 auto;white-space:nowrap;display:inline-flex;align-items:center}
.hub-act:hover{background:var(--brand);color:var(--brand-ink)}
.hub-act:active{transform:translateY(1px)}
.hub-title{font-size:13px;font-weight:600;color:var(--ink)}
.hub-title b{color:var(--ready);font-weight:700}
.hub-date{flex:none;font-family:var(--mono);font-size:10.5px;color:var(--faint);font-weight:400}
.hub-list{display:flex;flex-direction:column;gap:7px}
.hub-row{display:flex;gap:10px;font-size:12px;color:var(--dim);align-items:flex-start}
.hub-row.faint{color:var(--faint)}
.hub-bul{flex:none;width:5px;height:5px;border-radius:50%;background:var(--ready);opacity:.7;margin-top:6px}
.hub-txt{flex:1;min-width:0}
.hub-txt b{color:var(--ink-2);font-weight:600}
.hub-dot{width:7px;height:7px;border-radius:50%;flex:none;position:relative;top:5px;background:var(--muted)}
.hub-dot.critical{background:#e0524a}
.hub-dot.warn{background:#e0a83a}
.hub-foot{margin-top:12px;font-size:10.5px;color:var(--faint);border-top:1px solid var(--line);padding-top:8px}
.deppop .deprow:hover{background:var(--surface)}
.deppop .dep-name{font-size:12px;color:var(--ink)}
.deppop .dep-ip{margin-left:auto;font-size:11.5px;color:var(--ink-2)}
/* online users / peers breakdown bubble + its trigger tag */
/* the hover trigger: a slightly enlarged hit area + a clearer highlight (the header uses .bare = none) */
.onlinetag{cursor:pointer;border-radius:6px;padding:3px 7px;margin:-3px -4px;transition:background .12s,box-shadow .12s}
.onlinetag:hover,.onlinetag.on{background:rgba(63,216,154,.2);box-shadow:inset 0 0 0 1px rgba(63,216,154,.45)}
.onlinetag.bare,.onlinetag.bare:hover,.onlinetag.bare.on{background:none;box-shadow:none;padding:0;margin:0;display:inline-flex;align-items:center;gap:7px}
.onl-toggle{display:inline-flex;align-items:center;gap:7px}   /* click-to-toggle wrapper — keep the dot/count laid out as flex items (a plain inline span would collapse the dot) */
.onlinetag.nm-peerpop{display:inline-flex;align-items:baseline;gap:8px}
.oncount.on{color:var(--online)}   /* first number green when any peer is online */
.ifc-conn .oncount{font-size:12px}   /* turn-card connection count — match the interface-card "Peers N/M" size (.ifrow is 12px) */
.onlpop{min-width:210px;max-height:50vh;overflow:auto}
.onlpop .onpop-h{display:block;font-family:var(--mono);font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--dim);padding:4px 8px 6px;text-decoration:none}
.onlpop a.onpop-h:hover{color:var(--brand)}
.onlpop .onrow{display:flex;align-items:baseline;justify-content:space-between;gap:14px;padding:5px 8px;border-radius:6px;white-space:nowrap}
.onlpop .onrow:hover{background:var(--surface)}
.onlpop .hrow{display:block;white-space:normal}   /* health-issue rows: let long descriptions wrap */
.onlpop .hrow .on-name{font-size:12px;line-height:1.35}
.onlpop .onrow.un .on-name{color:var(--dim);font-style:italic}
.onlpop .on-name{font-size:12.5px;color:var(--ink)}
.onlpop .on-ct{font-size:12px;color:var(--online);font-family:var(--mono)}
.onlpop .on-user{font-size:11.5px}
.uncatpop{width:262px;min-width:0}   /* one wrapping sentence — sized to read, not to fill */
.onlpop .onpop-orph{display:block;font-size:12.5px;color:#F0913C;text-decoration:none;padding:8px 8px 4px;margin-top:3px;border-top:1px solid var(--line)}
.onlpop a.onpop-orph:hover{color:#C7741E}
.onlpop .onpop-viewall{display:block;font-size:11.5px;color:var(--brand);text-decoration:none;padding:7px 8px 4px;margin-top:3px;border-top:1px solid var(--line)}
.onlpop .onpop-viewall:hover{text-decoration:underline}
.assigncell{display:flex;align-items:center;gap:6px}
.assigncell .usercombo{min-width:96px;max-width:132px}   /* the in-table "Assign to…" picker: kept compact
   so the width it doesn't need goes to the status/iface cell beside it */
/* searchable user picker */
.usercombo{position:relative;min-width:150px}
.uc-input{width:100%;background:var(--surface-2);border:1px solid var(--line-solid);border-radius:7px;color:var(--ink);font:inherit;font-size:12.5px;padding:5px 7px}
.uc-input:focus{outline:none;border-color:var(--brand)}
.uc-list{position:absolute;z-index:40;top:calc(100% + 4px);left:0;min-width:200px;max-height:260px;overflow:auto;
  background:var(--raise);border:1px solid var(--line-solid);border-radius:9px;box-shadow:var(--shadow);padding:4px}
/* portaled to <body> and anchored to the input (position:fixed) so the list is never clipped by a grid/table
   overflow:hidden; z-index above modals so it works inside a Sheet too */
.uc-list.uc-pop{position:fixed;z-index:90}
.uc-opt{display:flex;align-items:center;gap:8px;width:100%;border:0;background:transparent;color:var(--ink);cursor:pointer;
  padding:6px 8px;border-radius:6px;font:inherit;font-size:13px;text-align:left}
.uc-opt:hover{background:var(--surface-2)}
.uc-empty{padding:8px;color:var(--faint);font-size:12px}
/* peer view sheet */
.pv-head{display:flex;align-items:center;gap:12px}
.pv-id{flex:1;min-width:0}
.pv-sub{font-size:13px;color:var(--dim)}
.pv-sub .pv-user{color:var(--ready)} .pv-sub .pv-user:hover{text-decoration:underline}
.pv-deps{display:flex;flex-direction:column;gap:8px}
.pv-dep{border:1px solid var(--line);border-radius:var(--r-sm);padding:10px 12px;background:var(--surface-2)}
.pv-dep.hl{border-color:var(--brand)}
.pv-dep-top{display:flex;align-items:center;gap:8px;margin-bottom:8px}
/* server↔rate, interface↔total, address↔last line up in 3 columns, two rows */
.pv-dep-grid{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1fr) minmax(0,1fr);gap:8px 18px;font-family:var(--mono);font-size:12px;color:var(--ink-2)}
.pv-dep-grid>span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pv-dep-grid .k{color:var(--faint);font-size:9px;text-transform:uppercase;letter-spacing:.06em;margin-right:6px}
/* A RAW peer carries TWO addresses and the cell is nowrap+ellipsis, so the second one was truncated to "…" —
   the chip rendered and could not be read. Only this cell, only when a raw address exists: the card grows one
   line for a RAW deployment and every other kind keeps the tight two-row grid. */
.pv-dep-grid > span.pv-addr-raw{white-space:normal;overflow:visible;text-overflow:clip;line-height:1.7}

/* ───── Two-factor (TOTP) card — Settings → Authentication ───── */
.btnrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.twofa-setup{display:flex;gap:20px;align-items:flex-start;flex-wrap:wrap}
.twofa-qr{width:200px;height:200px;border-radius:10px;background:#fff;padding:8px;flex:0 0 auto;box-shadow:0 0 0 1px var(--line)}
.twofa-qr.empty{display:flex;align-items:center;justify-content:center;color:var(--faint);font-size:12px;background:var(--surface-2)}
.twofa-manual{flex:1 1 240px;min-width:220px}
.twofa-manual>label{display:block;font-size:11px;color:var(--faint);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px}
.twofa-secret{display:inline-block;font-family:var(--mono);font-size:14px;letter-spacing:.12em;word-break:break-all;background:var(--surface-2);border:1px solid var(--line);border-radius:8px;padding:8px 10px;color:var(--ink)}
.twofa-codes{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
.twofa-codes code{font-family:var(--mono);font-size:14px;letter-spacing:.08em;text-align:center;background:var(--surface-2);border:1px solid var(--line);border-radius:8px;padding:9px 6px;color:var(--ink)}

/* Address-migration ribbon — shown on the OLD panel address after a rebind confirms elsewhere: this tab keeps
   working through the node-migration grace, then stops. Fixed to the top of the viewport so the countdown is
   impossible to miss; deep red, high z-index above the header. */

/* "You're on a previous panel address" — persistent, informational (amber, not the migrate ribbon's alarm-red),
   shown on any tab loaded at a non-current panel address so an old address reads as expected, not a fault. The
   sticky .appbar is pushed down by the measured ribbon height so it's never hidden. */
.addr-old-ribbon{position:fixed;top:0;left:0;right:0;z-index:31;display:flex;align-items:center;justify-content:center;
  gap:12px;flex-wrap:wrap;padding:9px 16px;background:#c62b42;color:#fff;font-size:13px;font-weight:600;
  box-shadow:0 2px 10px rgba(0,0,0,.28);border-bottom:1px solid rgba(0,0,0,.25)}
.addr-old-ribbon a{color:#fff;text-decoration:underline;text-underline-offset:2px}
.addr-old-ribbon .btn-mini{background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.5);color:#fff;
  text-decoration:none;padding:3px 10px;border-radius:7px;font-weight:700;white-space:nowrap}
.addr-old-ribbon .btn-mini:hover{background:rgba(255,255,255,.28)}
body.has-oldaddr .appbar{margin-top:var(--oldaddr-h,40px);top:var(--oldaddr-h,40px)}

/* Settings → Interfaces → Ignored interfaces: a plain list, one action. Deliberately quieter than an ifcard —
   these are interfaces the operator dismissed, so they read as a record, not as something needing attention. */
.ignlist{display:flex;flex-direction:column;gap:1px;background:var(--line);border:1px solid var(--line);border-radius:var(--r-sm);overflow:hidden}
.ignrow{display:flex;align-items:center;gap:10px;padding:9px 12px;background:var(--surface)}
.ignrow-n{font-family:var(--mono);font-size:13px;color:var(--ink);text-decoration:none}
.ignrow-n:hover{color:var(--brand)}
.ignrow-node{font-size:11px;color:var(--dim)}
.ignrow-f{font-family:var(--mono);font-size:11px;color:var(--faint)}

/* Targets grid: what sits IN FRONT of the interface (WDTT server / turn-proxy). Takes the fork's own colour via
   --tgc so it reads as "that fork", matching the tags used on the node screen. Kept to the same 9px tag scale as
   the wg/awg chip beside it — it is context, not a second status. */
.tgt-front{color:var(--tgc,var(--turn,#A974F2));background:color-mix(in srgb,var(--tgc,#A974F2) 16%,transparent);
  max-width:12ch;overflow:hidden;text-overflow:ellipsis;display:inline-block}
.tgt-front-many{cursor:default}   /* keeps .tg's uppercase: reads TURN ×3, like every other protocol chip */
.tgt-front-many b{margin-left:4px;font-weight:600}
.tgt-frontpop{display:inline-flex}
.tgt-frontbub{padding:8px 10px;min-width:150px}
.tgt-frontlist{display:flex;flex-wrap:wrap;gap:5px;align-items:center}
.tgt-frontlist .tgt-front{max-width:18ch}
.tgt-frontlbl{display:block;width:100%;font-size:10px;color:var(--dim);margin-bottom:2px}

/* JetBrains Mono ships PROGRAMMING LIGATURES, and every mono surface in this panel shows something the reader
   must be able to transcribe exactly — a DoH URL, a public key, a config line, a CLI command. A glyph that
   draws two characters as one is therefore a correctness bug, not a style: `//` renders merged with a gap to
   its left and reads as a space plus a single slash, which is how "https://1.1.1.1" was reported as
   "https: /1.1.1.1" — the value was right the whole time, only the rendering lied. (`->`, `!=`, `::` and `www`
   ligate too.) Turn them off wherever literal text is shown or typed; prose keeps its ordinary fi/fl. */
input, textarea, select, .mono, pre, code, kbd, samp { font-variant-ligatures: none; font-feature-settings: "liga" 0, "clig" 0, "calt" 0; }

/* ── Shared VK call-link pool (Settings → Turn proxies) + per-user link origin ────────────────────
   A pool link is the panel's to rewrite; a personal one is the operator's. The two behave differently
   on a pool edit, so they never look alike: pool rows carry the shared "users" mark and the accent,
   personal rows stay neutral. Dead rows stay legible — the operator still has to read and fix them. */
.vkpool{display:flex;flex-direction:column;gap:8px;margin:10px 0 4px}
.vkpool-row{display:flex;align-items:center;gap:8px}
.vkpool-row .vkbox{border-color:color-mix(in srgb,var(--brand) 42%,var(--line))}
.vkpool-row.warn .vkbox{border-color:#e0a85f;background:color-mix(in srgb,#e0a85f 8%,var(--bg))}
/* A dead row KEEPS ITS BORDER, in grey. Dropping the border made the row look like it had stopped being a
   field at all — it reads as removed rather than retired, and the operator can still edit and revive it. */
.vkpool-row.dead .vkbox{border-color:var(--line-solid);background:color-mix(in srgb,var(--line) 22%,var(--bg))}
.vkpool-row.dead .vkbox > .ic{color:var(--faint)}
.vkpool-row.dead .vkbox-input{text-decoration:line-through;color:var(--faint)}
.vkpool-held{flex:none;min-width:34px;text-align:center;font-size:11px;font-variant-numeric:tabular-nums;
  color:var(--ink);padding:3px 7px;border-radius:6px;background:color-mix(in srgb,var(--brand) 14%,transparent)}
.vkpool-held.none{color:var(--faint);background:transparent}
.vkpool-dead{flex:none}
.vkpool-dead.on{color:#e0a85f}                    /* the row is dead: this button now offers "mark as alive" */
/* The save affordance lives INSIDE the field, so it reads as "apply what you just typed" and can never be
   confused with the row's alive/dead tick sitting further right. Only rendered while the text differs. */
.vkbox-save{flex:none;margin-left:6px;padding:2px 7px;color:var(--brand);
  border-color:color-mix(in srgb,var(--brand) 45%,var(--line))}
.vkbox-save:hover:not(:disabled){background:color-mix(in srgb,var(--brand) 12%,transparent)}
.vkpool-empty{padding:12px;border:1px dashed var(--line);border-radius:8px;color:var(--faint);font-size:12px}
.vkpool-foot{display:flex;align-items:center;gap:8px;margin-top:10px}
.vkpool-foot .grow{flex:1}
/* The two kinds of link are told apart by the FIELD itself now that the ПУЛ/ЛИЧНАЯ badges are gone: a pool
   link carries the brand border and the shared-people icon, a personal one the blue border and the chain.
   Without the blue the personal rows read as unstyled — as if they were missing something the others had. */
.vkbox.frompool{border-color:color-mix(in srgb,var(--brand) 42%,var(--line))}
.vkbox.frompool > .ic{color:var(--brand)}
.vklinks-row .vkbox:not(.frompool){border-color:color-mix(in srgb,var(--ready) 55%,var(--line))}
.vklinks-row .vkbox:not(.frompool) > .ic{color:var(--ready)}
.vklinks-star{flex:none;padding:3px 7px}          /* the primary marker: same star control as a peer's deployments */
.vkbox-input[readonly]{cursor:default}            /* a pool link is edited in the pool, not here */

/* The measured verdict that the datapath choice actually turns on — stated where the choice is made. */
.dp-verdict{display:flex;align-items:center;gap:7px;margin-top:9px}
.dp-verdict.ok{color:var(--online)}
.dp-verdict.warn{color:var(--partial)}
.dp-verdict .ic{flex:none}

/* ── VK pool: sort bar, pager, bulk paste, sheet columns ─────────────────────────────────────────── */
/* Sort arrows sit directly over their column, reusing the ROW's width classes so they line up by
   construction rather than by a hand-tuned pixel value that drifts the moment a column changes. */
.vkpool-head{display:flex;align-items:center;gap:8px;margin:0 0 7px;padding:0;min-height:18px}
.vkpool-head .grow{flex:1}
/* Big enough to read and dark enough to see: at 10px in --faint the mark was there in the DOM and absent on
   the screen, which is the same as not shipping it. --dim for the resting state, brand when it is the one
   sorting. Tabular width so swapping ↕ for ↓ does not nudge the column. */
.vkpool-head .vkh{background:none;border:0;padding:0 2px;cursor:pointer;color:var(--dim);
  font-size:12px;line-height:1;letter-spacing:0;text-align:center;font-variant-numeric:tabular-nums}
.vkpool-head .vkh:hover{color:var(--ink)}
.vkpool-head .vkh.on{color:var(--brand);font-weight:700}
.vkpool-head .vkh-pad{visibility:hidden;pointer-events:none}
.vkpool-pager{display:flex;align-items:center;justify-content:center;gap:12px;margin-top:9px;font-size:11.5px}
.vkpool-pager-in{margin-top:0}
/* ⚠️ NOT reusing .vkpool-foot here — that one is the paste box's own row. Three columns so the pager stays
   optically centred while the two buttons hold the edges, whatever their labels say in any language. */
.vkpool-bar{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:10px;margin-top:10px}
.vkpool-bar > :last-child{justify-self:end}
.vkpool-bar > :first-child{justify-self:start}
.vkpool-add{width:100%;margin-top:2px}
.vkpool-paste{width:100%;box-sizing:border-box;resize:vertical;min-height:64px;font-family:var(--mono);
  font-size:12px;line-height:1.5;padding:8px 11px;border:1px solid var(--line);border-radius:8px;
  background:var(--bg);color:var(--ink)}
.vkpool-paste:focus{outline:none;border-color:color-mix(in srgb,var(--brand) 55%,var(--line))}
/* "added" reads as context, not data — same weight as the holder count it sits beside. */
.vkpool-added{flex:none;min-width:66px;text-align:right;font-size:11px;color:var(--faint);white-space:nowrap}

/* Pool totals — what the whole pool adds up to, above the rows on both surfaces. */
.vkpool-tot{display:flex;align-items:center;gap:6px;font-size:11.5px;color:var(--ink-2)}
.vkpool-tot.big{font-size:13px}
.vkpool-tot .vkpool-dot{color:var(--faint)}
.vkpool-tot .vk-dead-n{color:#e0a85f}

/* The retype phrase, made takeable: click the word or the mark to copy it. Reads as text, behaves as a button. */
.ctype-copy{display:inline-flex;align-items:center;gap:5px;padding:1px 5px;margin:0 1px;border:0;border-radius:5px;
  background:color-mix(in srgb,var(--brand) 10%,transparent);color:inherit;font:inherit;cursor:pointer;vertical-align:baseline}
.ctype-copy:hover{background:color-mix(in srgb,var(--brand) 20%,transparent)}
.ctype-copy .mono{font-family:var(--mono);text-transform:none;letter-spacing:0}
.ctype-copy .mono.b{font-weight:700}
.ctype-copy .ic{opacity:.65;flex:none}
.ctype-copy:hover .ic{opacity:1}

/* ── The routing rule builder's field ─────────────────────────────────────────────────────────────────────
   One control where the category dropdown, its "Custom IPs / domains…" entry and the targets textarea used to
   be. Nothing here invents an identity: the badge is .blkchip's pill at .capb's scale, a list keeps the
   provider colour it already has everywhere else, and amber is --partial. Two states are new because nothing
   in the panel expressed them — PREPARING (the panel is still fetching this list) and INERT (kept, and this
   node's engine cannot run it), and inert deliberately borrows .blkchip.inert's dashed-and-dimmed treatment
   plus its ⊘, so the operator learns one visual for "present, not in force" across routing and blocking. */
/* Both follow .rrrow's two columns now. The catch-all keeps the SAME shape as a rule row on purpose — its
   arrow, dropdown and trailing button have to land under the rule rows' own, or the one line that says where
   everything else goes stops reading as part of the same list. Flex rather than a grid column for the tail,
   so it right-aligns against whatever width the lead takes, exactly like `.tf-trail` does in the foot. */
.rrrow-b{grid-template-columns:16px minmax(0,1fr)}
.rrfoot{display:flex;align-items:center;gap:8px}
.rrfoot .rrfoot-lead{flex:1 1 auto}
.tfield{position:relative;min-width:0;display:flex;flex-direction:column;gap:4px}
/* TWO ROWS TALL BEFORE ANYTHING IS IN IT. A rule usually names several things, so a box sized for one badge
   grew the moment the second was added and every row below it jumped — and an empty one read as a
   single-value field. 68px = two 25px badge rows + the 6px row gap + 10px padding + 2px border.
   `align-content:flex-start` keeps the first row AT THE TOP: with `align-items:center` alone the lines are
   stretched and one badge floats in the middle of a box twice its height. */
.tf-box{display:flex;flex-wrap:wrap;align-items:center;align-content:flex-start;gap:6px;min-height:68px;
  padding:5px 7px;cursor:text;
  border:1px solid var(--line-solid);border-radius:7px;background:var(--bg);transition:border-color .12s}
.tf-box:hover{border-color:color-mix(in srgb,var(--brand) 40%,var(--line-solid))}
.tf-box:focus-within{border-color:var(--brand)}
.tfield.locked .tf-box{cursor:default;background:none;border-style:dashed}
.tf-in{flex:1 1 150px;min-width:110px;border:0;background:none;outline:none;color:var(--ink);font:inherit;font-size:12.5px;padding:3px 2px}
.tf-in::placeholder{color:var(--faint)}
/* A token that means nothing anywhere is marked where it was typed and left there to be fixed — never
   swallowed, never a modal (§5.3). */
.tf-in.bad{text-decoration:underline wavy color-mix(in srgb,var(--dangling) 75%,transparent);text-underline-offset:3px}

.tfbadge{display:inline-flex;align-items:center;gap:5px;max-width:100%;padding:2px 4px 2px 9px;border-radius:99px;
  font-size:12px;font-weight:600;white-space:nowrap;color:var(--ink);background:var(--surface-2);
  border:1px solid var(--line-solid);transition:opacity .12s,border-color .12s}
/* A whole zone or a text match reaches orders of magnitude further than it looks — the same amber the panel
   already uses for "this works, and you should know what it costs". */
.tfbadge.wide{border-color:color-mix(in srgb,var(--partial) 50%,transparent);background:color-mix(in srgb,var(--partial) 9%,var(--surface-2))}
.tfbadge.inert{opacity:.55;border-style:dashed;background:none;padding-left:6px}
.tfbadge.inert:hover{opacity:.8}
.tfb-g{font-size:12px;line-height:1;color:var(--dim);flex:none}
.tfb-main{display:inline-flex;align-items:center;gap:6px;min-width:0;border:0;background:none;padding:0;color:inherit;font:inherit;cursor:pointer;text-align:left}
.tfield.locked .tfb-main{cursor:default}
/* The badge currently open in the box below it. It stays in the row (see `edit`), so it has to read as
   "this one is being worked on" rather than as a second copy of what you are typing. */
.tfbadge.editing{opacity:.45;outline:1px dashed color-mix(in srgb,var(--brand) 55%,transparent);outline-offset:1px}
.tfb-lbl{min-width:0;overflow:hidden;text-overflow:ellipsis}
.tfbadge.tgt .tfb-lbl{font-family:var(--mono);font-size:11.5px;font-weight:500}
.capb.kind{color:var(--dim);background:color-mix(in srgb,var(--ink) 9%,transparent)}
.capb.wide{color:var(--partial);background:color-mix(in srgb,var(--partial) 16%,transparent)}
/* Collapsed rules summary: how many targets this node's engine cannot match. Amber, not red —
   nothing is broken or lost, the operator changed the engine and these are waiting for it back. */
.rs-off{color:var(--partial)}
.tfbadge.degraded .capb{color:var(--partial);background:color-mix(in srgb,var(--partial) 16%,transparent)}
.tfb-size{font-size:10.5px;font-weight:500;color:var(--dim);white-space:nowrap;font-variant-numeric:tabular-nums}
/* The panel is fetching this list right now. It pulses rather than spins: it is a wait, not a task. */
.tfb-size.preparing{color:var(--partial);animation:tfprep 1.8s ease-in-out infinite}
@keyframes tfprep{0%,100%{opacity:.55}50%{opacity:1}}
@media (prefers-reduced-motion:reduce){.tfb-size.preparing{animation:none;opacity:.8}}
/* Tried, and nothing came back. --partial-r is the panel's "a problem that isn't a stopper" — the rule saves
   and routes as soon as the list lands; this is the retry. */
.tfb-size.notready{color:var(--partial-r);border:0;background:none;font:inherit;font-size:10.5px;cursor:pointer;text-decoration:underline dotted;padding:0}
/* NOT FOUND — a different state from "not ready" and it has to look like one. `notready` is amber and
   dotted-underlined because it is a wait you can retry; this is a rule that matches nothing until the list
   comes back, so it takes the panel's problem colour and offers no retry (there is nothing to re-ask). */
.tfb-size.missing{color:var(--dangling);font-weight:600}
.tfb-fix,.tfb-x{display:inline-flex;align-items:center;justify-content:center;width:19px;height:19px;padding:0;border:0;
  border-radius:50%;background:none;color:var(--faint);cursor:pointer;flex:none}
.tfb-fix:hover{background:color-mix(in srgb,var(--brand) 16%,transparent);color:var(--brand)}
.tfb-x:hover{background:color-mix(in srgb,var(--dangling) 18%,transparent);color:var(--dangling)}
.tfb-fix .ic svg,.tfb-x .ic svg{width:11px;height:11px}
.tf-more{font-size:11px;font-weight:600;color:var(--dim);background:none;border:1px dashed var(--line-solid);
  border-radius:99px;padding:3px 10px;cursor:pointer}
.tf-more:hover{color:var(--ink);border-color:var(--line-bright)}

.tf-foot{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 10px;padding:0 2px;font-size:11px;line-height:1.5}
/* THE MESSAGE GETS ITS OWN LINE, and the destination never moves. Sharing the row with the entry count
   meant a long lint ("did you mean the whole .tester zone?") pushed the destination control sideways and
   down — the control jumping while you type is worse than the second line it costs. `order` puts it last
   in layout while it stays second in the DOM, so the count and the destination keep line one to themselves. */
/* ⚠️ ON THE LINE, NOT UNDER IT. `flex-basis:100%` gave the hint a row of its own below the destination,
   which cost a line of height to say something the operator reads while typing — beside the control, where
   the eye already is. It sits between the leading × and the right-aligned destination and SHRINKS (the
   destination keeps its fixed width), and the foot still wraps when there is genuinely no room. */
.tf-foot .tf-lint,.tf-foot .tf-say{flex:1 1 auto;min-width:0}
/* TEXT VIEW. The badge box and the textarea are the same field in two readings, so the textarea borrows the
   box's own border, radius and focus ring rather than looking like a different control that replaced it.
   Monospace because every line is an address, a range or an AS number, and a fixed advance is what makes a
   column of them scannable — the same reason the badges use it.
   ⚠️ AND `.exprof` — the pasted WireGuard profile on the exit sheet — SHARES IT. That box set only width,
   font and resize, so it fell through to the browser's own textarea: in DARK theme a near-black rectangle
   whose hairline border disappears into the sheet behind it, with no radius, no padding and no focus ring,
   sitting two fields above a routing box that has all four. Reported from the panel. Shared rather than
   copied — a second set of the same declarations is how two controls that must look alike stop looking
   alike, and this file already carries that scar elsewhere. */
.tf-text,.exprof{width:100%;box-sizing:border-box;min-height:96px;resize:vertical;padding:7px 9px;
  border:1px solid var(--line-solid);border-radius:7px;background:var(--bg);color:var(--ink);
  font-family:var(--mono);font-size:12px;line-height:1.7;outline:none;transition:border-color .12s}
.tf-text:hover,.exprof:hover{border-color:color-mix(in srgb,var(--brand) 40%,var(--line-solid))}
.tf-text:focus,.exprof:focus{border-color:var(--brand)}
.tf-text::placeholder,.exprof::placeholder{color:var(--faint)}
/* The lists the text cannot carry, kept in sight above it so nothing looks deleted while you edit. */
.tf-keep{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-bottom:6px}
.tf-keepnote{font-size:10.5px;color:var(--faint)}
/* IN THE GRIP COLUMN, under the drag handle. `.rrrow` is `16px | 1fr` with an 8px gap, so −24px from the
   field's own left edge lands it in that rail, centred on the same 16px the grip uses. Absolute because the
   state it toggles belongs to the field: putting the button in the row's markup would have meant lifting
   `asText` out of the component that owns it. One control, two states — same place, same shape, only the
   pressed style changes, so the way back is exactly where the way in was.
   `top:30px` clears the grip rather than sitting on it: the grip is 18px tall starting 7px down, so the two
   were drawn one on top of the other at 9px — both centred in the same rail, which reads as one smudged
   control instead of two.
   The offset is DERIVED from the row's own `--rr-rail`/`--rr-gap` rather than restating them: this rule and
   `.rrrow` sit 1600 lines apart, and a hard-coded −24px is a number that silently stops matching the grid it
   was measured against. The fallbacks keep it sane if the field is ever rendered outside a row. */
/* Brand-coloured and a little larger: it is the only way into the text view and it sits alone in the rail,
   where a faint 14px glyph read as decoration. Dropped down to clear the box's first badge row. */
.tf-vtog{position:absolute;left:calc(-1 * (var(--rr-rail,16px) + var(--rr-gap,8px)));top:38px;
  width:var(--rr-rail,16px);height:18px;display:inline-flex;align-items:center;
  justify-content:center;color:var(--brand);background:none;border:0;border-radius:4px;padding:0;
  cursor:pointer;opacity:.85;transition:color .12s,background .12s,opacity .12s}
.rrrow:hover .tf-vtog{opacity:.95}
.tf-vtog .ic svg{width:16px;height:16px}
.tf-vtog:hover{color:var(--brand);opacity:1;background:color-mix(in srgb,var(--brand) 10%,transparent)}
.tf-vtog.on{color:var(--brand);opacity:1;background:color-mix(in srgb,var(--brand) 12%,transparent)}
.tf-vtog:focus-visible{outline:2px solid var(--brand);outline-offset:1px}
/* THE FOOT VARIANT — the custom-list sheet, which has no row and therefore no grip column to sit in. Static
   rather than absolute (the rail offset put it outside the sheet entirely), labelled, and pushed right by the
   foot's own flex. Brand-coloured at rest, not faint: in a rule row it is one control among several and stays
   quiet; here it is the only way to reach the text view, so it has to read as something you can press. */
.tf-vtog.inline{position:static;width:auto;height:auto;gap:5px;padding:1px 6px;opacity:1;
  color:var(--brand);font:inherit;font-size:11px;line-height:1.5}
.tf-vtog.inline:hover{background:color-mix(in srgb,var(--brand) 12%,transparent)}
.tf-meter{color:var(--dim);font-variant-numeric:tabular-nums;white-space:nowrap}
.tf-say{color:var(--faint);min-width:0}
.tf-say b{color:var(--ink-2);font-weight:600}
.tf-lint{color:var(--partial);min-width:0}
/* `err`, NOT `bad`. `.bad` is the panel-wide INVALID-INPUT style — `box-shadow:0 0 0 3px` plus a
   `border-color:!important` — meant for a control the operator has to fix. On a text span it drew a red
   ring around the sentence, which read as "this message is itself broken". Same class name, different
   thing; renamed rather than cancelled, so the collision cannot come back through a third user. */
.tf-lint.err{color:var(--partial-r)}
.tf-live{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}

/* The field's dropdown reuses the catalog popover wholesale — same rows, same paging, same placement rule. */
.tfpop .catpick-list{padding:5px 5px 7px}
.tfrow{border-radius:7px}
.tfrow.act{background:var(--dd-hover)}
.tfrow.act .catrow:hover{background:none}
.tfrow-why{display:flex;flex-wrap:wrap;gap:6px;padding:0 9px 8px 32px;font-size:11px;line-height:1.45;color:var(--partial)}
/* "Use what you typed" — pinned above the lists, because inference is never silent: the reading and its
   consequence are on screen before the badge exists. */
.tfuse{display:flex;flex-wrap:wrap;align-items:center;gap:9px;width:100%;padding:8px 9px;border:0;border-radius:7px;
  background:none;color:var(--ink);font:inherit;text-align:left;cursor:pointer}
.tfuse.act{background:var(--dd-hover)}
.tfuse.off{cursor:not-allowed}
.tfuse.off .tfbadge{opacity:.55;border-style:dashed;background:none}
.tfuse-say{font-size:11.5px;color:var(--dim);min-width:0}
.tfuse.off .tfuse-say{color:var(--partial)}
