/*
 * components.css - the redesign component layer, ported from redesign/bento.html
 * with the VPN Services treatment from redesign/aurora.html.
 *
 * Load order: tokens.css -> custom.css -> components.css.
 * Everything here is `bo-` prefixed and consumes only the semantic tokens from
 * tokens.css, never a raw colour and never a legacy --dark-color-* name.
 *
 * Deliberate divergences from the mockups, all reasoned in redesign-plan.md:
 *
 *   - Theme selectors are body.dark, not :root[data-theme="dark"]. The mockups
 *     stamp data-theme on <html>; the panel puts the theme class on <body>
 *     (aThemeSwitch.html:50) and reserves html[data-theme] for ultra-dark.
 *   - The rail is 96px, not 84px. Menu labels do not fit at 84px in every
 *     locale: "Администраторы", "Настройки Xray", "Panel Settings" and
 *     "پیکربندی ایکس‌ری" all overrun. Labels wrap to two lines here.
 *   - Physical direction properties are logical throughout, for fa_IR/ar_EG.
 *     Charts and numeric strings are explicitly pinned LTR: a mirrored time
 *     axis is wrong, and bidi reordering mangles IPs and version numbers.
 *   - Durations come from --dur-fast/--dur-med so the anti-flicker guard
 *     (html[data-theme-animations='off']) and prefers-reduced-motion both
 *     apply without per-rule overrides.
 *   - No global reset. antd.min.css already sets box-sizing:border-box
 *     globally; re-declaring it would only risk fighting ant.
 */

/* ================================================================== *
 * base
 * ================================================================== */

/*
 * custom.css:164-171 pins html and body to height:100vh with overflow:hidden,
 * so the document itself never scrolls. Before the redesign the scroll
 * container was ant's .ant-layout-content. It is now .bo-main. Getting this
 * wrong does not produce a scrollbar in the wrong place, it silently CLIPS
 * every page longer than the viewport.
 */
.bo-shell {
  display:flex;
  height:100vh;
  overflow:hidden;
  font-family:var(--font-sans);
  background:var(--bg);
  color:var(--text);
  font-variant-numeric:tabular-nums;
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
}

.bo-shell svg { display:block; }
.bo-shell button { font-family:inherit; }

.bo-shell :is(ul,p,h1,h2,h3) { margin:0; padding:0; }

.bo-shell :is(button,[tabindex],a):focus-visible {
  outline:2px solid var(--accent);
  outline-offset:2px;
  border-radius:6px;
}

/* Numerals, IPs, versions: pin LTR so bidi cannot reorder them, and keep the
   figures tabular so columns of numbers line up. */
.bo-shell .num {
  font-variant-numeric:tabular-nums;
  font-feature-settings:'tnum';
  direction:ltr;
  unicode-bidi:isolate;
}

/* ================================================================== *
 * icons and dots
 * ================================================================== */

.bo-icon {
  width:18px; height:18px; flex-shrink:0;
  fill:none; stroke:currentColor; stroke-width:1.8;
  stroke-linecap:round; stroke-linejoin:round;
}
.bo-icon-14 { width:14px; height:14px; }
.bo-icon-16 { width:16px; height:16px; }
.bo-icon-fill { fill:currentColor; stroke:none; }

.bo-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; background:var(--off); }
.bo-dot--ok { background:var(--ok); }
.bo-dot--info { background:var(--info); }
.bo-dot--warn { background:var(--warn); }
.bo-dot--error { background:var(--error); }

/* Secondary text sitting beside primary text: a port after a remark, a traffic
   figure under an email. Used across the Clients page and the client forms to
   mean "here if you want it, not what you are reading". */
.bo-dim { color:var(--text-3); font-size:12px; }

/* ================================================================== *
 * shell: rail nav
 * ================================================================== */

/* height:100% of the fixed-height .bo-shell, not 100vh + sticky as in the
   mockup: the shell is the scroll boundary here, so the rail is simply a
   full-height flex child that scrolls its own overflow. */
.bo-rail {
  flex:0 0 124px; width:124px;
  height:100%;
  background:var(--surface);
  border-inline-end:1px solid var(--border);
  display:flex; flex-direction:column; align-items:center;
  padding:20px 0; gap:26px; overflow-y:auto;
  z-index:20;
}

.bo-rail-brand {
  display:flex; flex-direction:column; align-items:center; gap:6px;
  color:var(--accent); width:100%; padding-inline:14px;
}
.bo-rail-brand span {
  font-size:var(--fs-sm); font-weight:var(--fw-bold);
  color:var(--text); letter-spacing:.01em;
}
/* logo.png is 1456x368, a 3.96:1 wordmark. Sizing it by HEIGHT overflows the
   rail (40px tall needs 158px of width against a 124px rail), and max-width
   then squashed it horizontally because the height stayed fixed. Size by
   width and let the height follow, so it can never distort. */
.bo-rail-brand img { display:block; width:100%; max-width:88px; height:auto; }

.bo-rail-nav {
  display:flex; flex-direction:column; gap:5px;
  width:100%; align-items:center;
  flex:1 1 auto;
}

/* Theme switch and collapse control, pinned to the bottom of the rail. */
.bo-rail-foot {
  display:flex; flex-direction:column; align-items:center; gap:10px;
  width:100%; margin-top:auto;
}

.bo-rail-collapse {
  width:30px; height:30px; border-radius:50%;
  border:1px solid var(--border); background:var(--surface-2);
  color:var(--text-3); cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  transition:background var(--dur-fast) var(--ease),
             color var(--dur-fast) var(--ease),
             border-color var(--dur-fast) var(--ease);
}
.bo-rail-collapse:hover {
  background:var(--accent-weak); color:var(--accent); border-color:var(--accent);
}
/* The chevron points inward/outward, so it must mirror in RTL. */
[dir="rtl"] .bo-rail-collapse { transform:scaleX(-1); }

/* User-toggled collapse, persisted in localStorage. Distinct from the
   automatic narrow-screen collapse in the media query at the foot of
   this file. */
.bo-rail--collapsed { flex-basis:76px; width:76px; }
.bo-rail--collapsed .bo-rail-item { width:64px; }
.bo-rail--collapsed .bo-rail-item span,
.bo-rail--collapsed .bo-rail-brand span { display:none; }
.bo-rail--collapsed .bo-rail-brand img { max-width:52px; }

.bo-rail-item {
  display:flex; flex-direction:column; align-items:center; gap:7px;
  width:108px; padding:11px 6px 10px;
  border-radius:var(--radius-md); border:none; background:transparent;
  color:var(--text-3); cursor:pointer; text-decoration:none;
  transition:background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
/* Two lines, because one is not enough for ru/fa/ar menu labels. */
.bo-rail-item span {
  font-size:12px; font-weight:var(--fw-semibold); letter-spacing:.01em;
  text-align:center; line-height:1.25;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
  overflow:hidden; overflow-wrap:anywhere;
}
/* The nav glyphs are ant <a-icon>, which renders at ~14px by default and looked
   undersized next to the 12px labels. */
.bo-rail-item .anticon { font-size:21px; line-height:1; }
.bo-rail-item:hover { color:var(--text-2); background:var(--surface-2); }
.bo-rail-item.is-active { background:var(--accent-weak); color:var(--accent); }

/* ================================================================== *
 * shell: topbar and content
 * ================================================================== */

/* The page scroll container. See the note on .bo-shell. */
.bo-main {
  flex:1 1 auto; min-width:0;
  display:flex; flex-direction:column;
  overflow-y:auto; overflow-x:hidden;
}

.bo-topbar {
  background:var(--surface); border-bottom:1px solid var(--border);
  position:sticky; top:0; z-index:10;
}
.bo-topbar-inner {
  max-width:1320px; margin:0 auto; padding:10px 24px;
  display:flex; align-items:center; justify-content:space-between;
  gap:var(--gap-sm); flex-wrap:wrap;
}
.bo-topbar-left {
  display:flex; align-items:center; gap:10px; flex-wrap:wrap; min-width:0;
}

.bo-host-chip,
.bo-health-pill {
  display:inline-flex; align-items:center; gap:7px;
  font-size:var(--fs-sm); font-weight:var(--fw-semibold);
  padding:6px 12px; border-radius:var(--radius-pill); white-space:nowrap;
}
.bo-host-chip { background:var(--surface-2); color:var(--text-2); }
.bo-host-chip .bo-icon { color:var(--text-3); }
.bo-health-pill { background:var(--ok-weak); color:var(--ok); }
.bo-health-pill--warn { background:var(--warn-weak); color:var(--warn); }
.bo-health-pill--error { background:var(--error-weak); color:var(--error); }

.bo-theme-toggle {
  width:34px; height:34px; border-radius:50%;
  border:1px solid var(--border); background:var(--surface-2);
  color:var(--text-2); display:flex; align-items:center; justify-content:center;
  cursor:pointer; flex:0 0 auto;
  transition:background var(--dur-fast) var(--ease),
             border-color var(--dur-fast) var(--ease),
             color var(--dur-fast) var(--ease);
}
.bo-theme-toggle:hover {
  background:var(--accent-weak); border-color:var(--accent); color:var(--accent);
}
/* Sun in light, moon in dark. The panel themes on body, not html. */
.bo-theme-toggle .bo-i-moon { display:none; }
body.dark .bo-theme-toggle .bo-i-sun { display:none; }
body.dark .bo-theme-toggle .bo-i-moon { display:block; }

/* Popover body for the rail theme control. */
.bo-theme-menu { display:flex; flex-direction:column; gap:var(--sp-2); min-width:150px; }
.bo-theme-row {
  display:flex; align-items:center; justify-content:space-between;
  gap:var(--sp-4); font-size:var(--fs-sm); color:var(--text-2);
  font-weight:var(--fw-medium); cursor:pointer;
}

.bo-topbar-title {
  font-size:var(--fs-md); font-weight:var(--fw-semibold);
  color:var(--text); margin:0; text-wrap:balance;
}

.bo-content { max-width:1320px; width:100%; margin:0 auto; padding:12px 22px 8px; }
.bo-page-title {
  font-size:24px; font-weight:var(--fw-semibold);
  margin-bottom:var(--sp-5); text-wrap:balance;
}

/* Section heading inside a page body, between tiles. */
.bo-section-head {
  display:flex; align-items:baseline; justify-content:space-between;
  gap:12px; margin-bottom:12px; flex-wrap:wrap;
}
.bo-section-title { font-size:var(--fs-md); font-weight:var(--fw-semibold); color:var(--text); }
.bo-section-meta { font-size:var(--fs-sm); color:var(--text-3); }

/* ================================================================== *
 * bento grid
 * ================================================================== */

.bo-bento {
  display:grid; grid-template-columns:1fr; gap:var(--gap-sm);
  grid-template-areas:
    "ident" "flow" "xray" "users" "manage" "vpnui" "vit" "svc";
}
@media (min-width:760px) {
  .bo-bento {
    grid-template-columns:repeat(4,1fr); gap:var(--gap-md);
    grid-template-areas:
      "ident ident ident ident"
      "flow flow flow flow"
      "xray xray users users"
      "manage manage vpnui vpnui"
      "vit vit vit vit"
      "svc svc svc svc";
  }
}
@media (min-width:1180px) {
  .bo-bento {
    grid-template-columns:repeat(12,1fr); gap:var(--gap-lg);
    grid-template-areas:
      "ident ident ident ident flow flow flow flow vit vit vit vit"
      "xray xray xray xray users users users users vit vit vit vit"
      "manage manage manage manage vpnui vpnui vpnui vpnui vit vit vit vit"
      "svc svc svc svc svc svc svc svc svc svc svc svc";
    /* Tiles STRETCH to fill their row (grid's default) rather than sizing to
       content. align-items:start was tried and is worse: it removes the dead
       space inside a card but dumps it between the cards instead, leaving
       ragged holes under Identity, Clients and System Status. The three
       columns carry near-equal total content (roughly 700 / 725 / 730px), so
       stretching lands them flush. .bo-tile distributes its own content, see
       the justify-content rule there. */
  }
}

.bo-t-ident { grid-area:ident; }
.bo-t-flow { grid-area:flow; }
.bo-t-vit { grid-area:vit; }
.bo-t-svc { grid-area:svc; }
.bo-t-xray { grid-area:xray; }
.bo-t-vpnui { grid-area:vpnui; }
.bo-t-users { grid-area:users; }
.bo-t-manage { grid-area:manage; }

/* ================================================================== *
 * tile
 * ================================================================== */

.bo-tile {
  background:var(--surface); border:1px solid var(--border);
  border-radius:var(--radius-xl); padding:14px 16px;
  box-shadow:var(--shadow-md); min-width:0;
  display:flex; flex-direction:column;
  transition:transform var(--dur-fast) var(--ease),
             box-shadow var(--dur-fast) var(--ease),
             border-color var(--dur-fast) var(--ease);
}
.bo-tile:hover { transform:translateY(-2px); border-color:var(--border-strong); }
/* Content sits at the top of a stretched tile. space-between was tried and
   looks worse: it strands the heading at the top and the body at the very
   bottom with a hole in the middle.
   For the tiles whose body is a grid of cells, that body grows to absorb the
   slack instead, so the card is filled rather than padded. */
.bo-tile > :last-child { margin-bottom:0; }
.bo-t-users .bo-people,
.bo-t-users .bo-person { flex:1 1 auto; }
.bo-t-users .bo-person { justify-content:center; }
.bo-t-xray .bo-btn-row,
.bo-t-manage .bo-btn-row { margin-top:auto; }
/* System Status spans three rows. Its own content is shorter, so the vitals
   list takes the slack. Capped growth (space-around, not space-between) keeps
   the four rows from being flung to the card edges. */
.bo-t-vit .bo-vit-list { flex:1 1 auto; justify-content:space-around; }

.bo-tile-head {
  display:flex; align-items:center; justify-content:space-between;
  gap:10px; margin-bottom:10px;
}
.bo-tile-title {
  font-size:var(--fs-sm); font-weight:var(--fw-bold);
  text-transform:uppercase; letter-spacing:.05em; color:var(--text-2);
  display:flex; align-items:center; gap:7px;
}
.bo-tile-meta { font-size:var(--fs-xs); color:var(--text-3); white-space:nowrap; }
/* Section heading inside a tile. Carries its own top spacing instead of a
   divider: the block above already ends on a rule, and two lines in one gap
   read as a duplicate. */
.bo-tile-head--sub { margin-top:12px; }
.bo-tile-caption { font-size:var(--fs-sm); color:var(--text-3); line-height:var(--lh-base); }
.bo-divider { height:1px; background:var(--border); margin:9px 0; }

/* ================================================================== *
 * identity tile
 * ================================================================== */

.bo-t-ident { justify-content:flex-start; }
.bo-ident-top { display:flex; align-items:center; gap:var(--gap-sm); }
.bo-ident-logo { width:58px; height:58px; flex:0 0 auto; }
.bo-ident-name { min-width:0; }
.bo-ident-os { font-size:24px; font-weight:var(--fw-bold); color:var(--text); line-height:1.15; }
.bo-ident-kernel { font-size:var(--fs-sm); color:var(--text-3); margin-top:5px; }
.bo-ident-chips { display:flex; flex-wrap:wrap; gap:var(--sp-2); margin-top:var(--sp-5); }

/* Identity metadata pill. Distinct from .bo-svc-chip below. */
.bo-chip {
  display:inline-flex; align-items:center; gap:7px;
  font-size:var(--fs-xs); font-weight:var(--fw-semibold);
  padding:6px 12px; border-radius:var(--radius-pill);
  background:var(--surface-2); color:var(--text-2);
}
.bo-chip .bo-icon { color:var(--text-3); }
.bo-chip--ok { background:var(--ok-weak); color:var(--ok); }
.bo-chip--ok .bo-dot { background:var(--ok); }

/* ================================================================== *
 * throughput
 * ================================================================== */

.bo-flow-cols { display:grid; grid-template-columns:1fr 1fr; gap:20px; }
@media (max-width:479px) { .bo-flow-cols { grid-template-columns:1fr; } }

.bo-flow-label {
  display:flex; align-items:center; gap:6px;
  font-size:11.5px; font-weight:var(--fw-bold);
  text-transform:uppercase; letter-spacing:.04em; color:var(--text-3);
}
.bo-flow-col--up .bo-flow-label .bo-icon { color:var(--accent); }
.bo-flow-col--down .bo-flow-label .bo-icon { color:var(--info); }
/* nowrap + a fixed line box: without these the figure re-wraps when the value
   grows a digit ("38.07 KB" -> "382.40 KB"), which resized the whole card on
   every 2-second poll. clamp() shrinks the type instead of wrapping. */
.bo-flow-value {
  font-size:clamp(18px,1.7vw,24px); font-weight:var(--fw-bold);
  letter-spacing:-.01em; margin-top:6px;
  white-space:nowrap; line-height:1.2; min-height:1.2em;
}
.bo-flow-col--up .bo-flow-value { color:var(--accent); }
.bo-flow-col--down .bo-flow-value { color:var(--info); }
.bo-unit { font-size:13px; font-weight:var(--fw-semibold); color:var(--text-3); margin-inline-start:3px; white-space:nowrap; }

/* Time flows left-to-right in every locale. Never mirror a chart. */
.bo-flow-chart { width:100%; height:96px; flex:0 0 96px; margin-top:10px; direction:ltr; }
.bo-flow-chart svg { display:block; width:100%; height:100%; overflow:visible; }

/* One total centred beneath each chart. The column template mirrors
   .bo-flow-cols exactly so each figure lines up under its own graph. */
.bo-flow-footer { display:grid; grid-template-columns:1fr 1fr; gap:20px; }
@media (max-width:479px) { .bo-flow-footer { grid-template-columns:1fr; } }
.bo-mini-stat { align-items:center; text-align:center; }
.bo-mini-stat--end { align-items:center; text-align:center; }
.bo-mini-stat { display:flex; flex-direction:column; gap:3px; }
.bo-mini-stat-label {
  font-size:var(--fs-xs); color:var(--text-3); font-weight:var(--fw-bold);
  text-transform:uppercase; letter-spacing:.04em;
}
.bo-mini-stat-value { font-size:18px; font-weight:var(--fw-bold); color:var(--text); }

/* ================================================================== *
 * vitals
 * ================================================================== */

.bo-vit-list { display:flex; flex-direction:column; gap:var(--gap-sm); }
@media (max-width:1179px) {
  .bo-vit-list { display:grid; grid-template-columns:repeat(2,1fr); gap:16px 22px; }
}
@media (max-width:479px) { .bo-vit-list { grid-template-columns:1fr; } }

.bo-vit-item { display:flex; flex-direction:column; gap:6px; }
.bo-vit-row { display:flex; align-items:center; gap:12px; }

.bo-ring-wrap { position:relative; width:44px; height:44px; flex:0 0 auto; }
.bo-ring-wrap svg { width:100%; height:100%; transform:rotate(-90deg); }
.bo-ring-track { fill:none; stroke:var(--border); stroke-width:4; }
/* The circumference below is 2*pi*r for r=16. Any JS that animates
   stroke-dashoffset MUST derive it from the same radius, not re-hardcode it. */
.bo-ring-progress {
  fill:none; stroke:var(--accent); stroke-width:4; stroke-linecap:round;
  stroke-dasharray:100.53; stroke-dashoffset:100.53;
  transition:stroke-dashoffset .8s cubic-bezier(.22,1,.36,1);
}
.bo-ring-progress--warn { stroke:var(--warn); }
.bo-ring-progress--error { stroke:var(--error); }
.bo-ring-icon {
  position:absolute; inset:0; display:flex;
  align-items:center; justify-content:center; color:var(--text-2);
}
.bo-ring-icon .bo-icon { width:15px; height:15px; }

.bo-vit-info { flex:1; min-width:0; }
.bo-vit-top { display:flex; align-items:baseline; justify-content:space-between; gap:8px; }
.bo-vit-name { font-size:13px; font-weight:var(--fw-semibold); color:var(--text); }
.bo-vit-pct { font-size:var(--fs-base); font-weight:var(--fw-bold); color:var(--text); }
.bo-vit-meta { font-size:11.5px; color:var(--text-3); margin-top:2px; }
.bo-vit-spark { width:100%; height:24px; direction:ltr; }
.bo-vit-spark svg { display:block; width:100%; height:100%; overflow:visible; }

.bo-load-row { display:flex; align-items:center; gap:var(--sp-2); padding:4px 0; }
.bo-load-label { font-size:var(--fs-xs); color:var(--text-3); width:22px; flex:0 0 22px; }
.bo-load-bar { flex:1; height:4px; border-radius:2px; background:var(--border); overflow:hidden; display:block; }
.bo-load-bar > span { display:block; height:100%; border-radius:2px; background:var(--accent); }
.bo-load-val {
  font-size:var(--fs-xs); font-weight:var(--fw-semibold); color:var(--text);
  width:36px; flex:0 0 36px; text-align:end;
}

/* ================================================================== *
 * VPN services
 *
 * aurora's detached-chip treatment (redesign/aurora.html:423-440) on bento's
 * single-attribute state mechanism. aurora encoded state twice, as parallel
 * modifier classes on both the dot and the label; one data-state attribute is
 * what you actually want to bind from Vue.
 *
 * These are buttons, not divs: the mockups' chips are inert, but a status chip
 * here navigates to that service on the Core Settings page.
 * ================================================================== */

/* 208px, not 178px: at the narrower track "OpenConnect", "AmneziaWG",
   "WireGuard (C)" and "MTProto Proxy" all ellipsised to nonsense
   ("OpenConn...", "Amn..."). The state label is pushed to its own line so the
   name gets the full width. */
/* Full-width band with the state label restored. 190px keeps the longest
   names ("OpenConnect (cisco)", "MTProto Proxy") off the ellipsis. */
.bo-svc-grid {
  display:grid; grid-template-columns:repeat(auto-fill,minmax(190px,1fr));
  gap:8px; align-content:start;
}

.bo-svc-chip {
  display:flex; align-items:center; gap:8px;
  background:var(--surface); border:1px solid var(--border);
  border-radius:var(--radius-md); padding:6px 9px;
  cursor:pointer; text-align:start; width:100%; font-family:inherit;
  transition:transform var(--dur-fast) var(--ease),
             border-color var(--dur-fast) var(--ease),
             box-shadow var(--dur-fast) var(--ease);
}
.bo-svc-chip:hover {
  transform:translateY(-1px);
  border-color:var(--border-strong);
  box-shadow:var(--shadow-sm);
}

.bo-svc-led { width:9px; height:9px; border-radius:50%; flex:0 0 auto; background:var(--off); }
/* Wraps rather than ellipsising: a truncated protocol name is useless, and
   these names are the whole point of the tile. */
.bo-svc-name {
  font-size:13.5px; font-weight:var(--fw-semibold); color:var(--text);
  flex:1; min-width:0; overflow-wrap:anywhere; line-height:1.3;
}
.bo-svc-state {
  font-size:var(--fs-xs); font-weight:var(--fw-bold);
  text-transform:uppercase; letter-spacing:.03em; color:var(--off);
  white-space:nowrap; flex:0 0 auto;
}

.bo-svc-chip[data-state="running"] .bo-svc-led { background:var(--ok); box-shadow:0 0 0 3px var(--ok-weak); }
.bo-svc-chip[data-state="running"] .bo-svc-state { color:var(--ok); }
.bo-svc-chip[data-state="idle"] .bo-svc-led { background:var(--info); box-shadow:0 0 0 3px var(--info-weak); }
.bo-svc-chip[data-state="idle"] .bo-svc-state { color:var(--info); }
.bo-svc-chip[data-state="stopped"] .bo-svc-led { background:var(--warn); box-shadow:0 0 0 3px var(--warn-weak); }
.bo-svc-chip[data-state="stopped"] .bo-svc-state { color:var(--warn); }
.bo-svc-chip[data-state="error"] .bo-svc-led { background:var(--error); box-shadow:0 0 0 3px var(--error-weak); }
.bo-svc-chip[data-state="error"] .bo-svc-state { color:var(--error); }
/* Not installed: present in the list but never started, so it reads as absent
   rather than as a fault. */
.bo-svc-chip[data-state="off"] { opacity:.6; }
.bo-svc-chip[data-state="off"] .bo-svc-led { background:var(--off); box-shadow:0 0 0 3px var(--off-weak); }
.bo-svc-chip[data-state="off"] .bo-svc-state { color:var(--off); }

/* ================================================================== *
 * status pills, key-value rows, buttons
 * ================================================================== */

.bo-status-pill {
  display:inline-flex; align-items:center; gap:6px;
  font-size:var(--fs-xs); font-weight:var(--fw-semibold);
  padding-block:4px; padding-inline:8px 10px;
  border-radius:var(--radius-pill);
  background:var(--off-weak); color:var(--off);
}
.bo-status-pill--ok { background:var(--ok-weak); color:var(--ok); }
.bo-status-pill--warn { background:var(--warn-weak); color:var(--warn); }
.bo-status-pill--error { background:var(--error-weak); color:var(--error); }

.bo-kv-row { display:flex; align-items:center; justify-content:space-between; gap:10px; padding:3px 0; }
.bo-kv-label { font-size:var(--fs-sm); color:var(--text-2); font-weight:var(--fw-medium); }
.bo-kv-value { font-size:var(--fs-base); font-weight:var(--fw-bold); color:var(--text); }

.bo-btn-row { display:flex; flex-wrap:wrap; gap:var(--sp-2); }
/* Compact action rows: four small buttons on ONE line, so a card is not made
   tall by a wrapped second row. Used by the merged Manage actions inside the
   identity card and by Xray. */
.bo-btn-row--compact,
.bo-t-xray .bo-btn-row { gap:6px; flex-wrap:nowrap; }
.bo-btn-row--compact .bo-icon-btn,
.bo-t-xray .bo-icon-btn {
  padding:6px 8px; font-size:var(--fs-xs);
  white-space:nowrap; min-width:0;
}

.bo-icon-btn {
  display:inline-flex; align-items:center; gap:6px;
  font-size:var(--fs-sm); font-weight:var(--fw-semibold);
  color:var(--text-2); background:var(--surface-2);
  border:1px solid var(--border); border-radius:var(--radius-sm);
  padding:7px 12px; cursor:pointer; font-family:inherit;
  transition:background var(--dur-fast) var(--ease),
             color var(--dur-fast) var(--ease),
             border-color var(--dur-fast) var(--ease),
             transform var(--dur-fast) var(--ease);
}
.bo-icon-btn:hover {
  background:var(--accent-weak); color:var(--accent);
  border-color:var(--accent); transform:translateY(-1px);
}
.bo-icon-btn--danger:hover { background:var(--error-weak); color:var(--error); border-color:var(--error); }
.bo-icon-btn--ghost { background:transparent; border-color:transparent; padding:5px 10px; }
.bo-icon-btn--accent {
  background:var(--accent-weak); border-color:var(--accent); color:var(--accent);
  padding:5px 10px; font-size:var(--fs-xs);
}
.bo-icon-btn--accent:hover { background:var(--accent); color:var(--on-accent); border-color:var(--accent); }

/* Info affordance sitting inline with a value. */
.bo-kv-with-action { display:inline-flex; align-items:center; gap:7px; }
.bo-inline-btn {
  border:none; background:transparent; color:var(--text-3); cursor:pointer;
  padding:0; display:inline-flex; align-items:center;
  transition:color var(--dur-fast) var(--ease);
}
.bo-inline-btn:hover { color:var(--accent); }
.bo-icon-btn--ghost:hover { border-color:var(--accent); }
.bo-icon-btn[disabled] { opacity:.5; cursor:not-allowed; transform:none; }

.bo-btn {
  font-size:13.5px; font-weight:var(--fw-semibold);
  border-radius:9px; padding:10px 18px; cursor:pointer;
  border:1px solid transparent; font-family:inherit;
  transition:filter var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
/* --accent-strong, not --accent: white on the brand green is 4.46:1 and misses
   WCAG AA. The two greens are visually near-identical. */
.bo-btn--primary { background:var(--accent-strong); color:var(--on-accent); align-self:flex-start; }
.bo-btn--icon { display:inline-flex; align-items:center; gap:7px; }
.bo-btn--primary:hover { filter:brightness(1.06); transform:translateY(-1px); }
/* Secondary action sharing a row with a primary one: outlined, so the primary
   keeps the only filled surface and the hierarchy survives both themes. */
.bo-btn--ghost {
  background:transparent; color:var(--text-2);
  border:1px solid var(--border); align-self:flex-start;
}
.bo-btn--ghost:hover { color:var(--accent); border-color:var(--accent); transform:translateY(-1px); }
.bo-btn[disabled] { opacity:.5; cursor:not-allowed; transform:none; filter:none; }

.bo-vpnui-body { display:flex; flex-direction:column; justify-content:space-between; gap:18px; flex:1; }
.bo-vpnui-version { font-size:23px; font-weight:var(--fw-bold); color:var(--text); }
.bo-status-line { display:flex; align-items:center; gap:6px; font-size:13px; font-weight:var(--fw-semibold); margin-top:var(--sp-2); }
.bo-status-line--ok { color:var(--ok); }
.bo-status-line--warn { color:var(--warn); }

/* The download-cancel button sits inline with the progress readout, so it is
   sized down to match that 12.5px line rather than the card's action row. */
.bo-update-cancel { padding:3px 9px; font-size:12px; gap:5px; }

/* Release-notes dialog (panel self-update). The notes pane scrolls on its own
   so a long changelog never pushes the dialog's own buttons off screen. */
.bo-relnotes-head {
  display:flex; align-items:center; gap:var(--sp-2); flex-wrap:wrap;
  padding-bottom:var(--sp-3); border-bottom:1px solid var(--border);
}
.bo-relnotes-head .bo-icon { color:var(--text-3); flex:none; }
.bo-relnotes-ver {
  font-size:15px; font-weight:var(--fw-bold); color:var(--text-3);
}
.bo-relnotes-ver--new { color:var(--accent); }
.bo-relnotes-date { font-size:12.5px; color:var(--text-3); margin-inline-start:auto; }
.bo-relnotes-link { display:inline-flex; color:var(--text-3); }
.bo-relnotes-link:hover { color:var(--accent); }
.bo-relnotes-label {
  margin-top:var(--sp-3); font-size:11.5px; font-weight:var(--fw-semibold);
  letter-spacing:.06em; text-transform:uppercase; color:var(--text-3);
}
.bo-relnotes {
  margin-top:var(--sp-2); max-height:46vh; overflow-y:auto; overflow-x:auto;
  font-size:13.5px; line-height:1.65; color:var(--text-2);
  overflow-wrap:anywhere;
}
.bo-relnotes h4 {
  margin:var(--sp-3) 0 var(--sp-1); font-size:13px; font-weight:var(--fw-bold);
  color:var(--text); letter-spacing:.01em;
}
.bo-relnotes h4:first-child { margin-top:0; }
.bo-relnotes ul { margin:0; padding-inline-start:1.15em; list-style:disc; }
.bo-relnotes li { margin:2px 0; }
.bo-relnotes p { margin:var(--sp-1) 0; }
.bo-relnotes code {
  font-family:var(--font-mono); font-size:12.5px;
  padding:1px 5px; border-radius:var(--radius-sm);
  background:var(--surface-2); color:var(--text);
}
.bo-relnotes a { color:var(--accent); }
.bo-relnotes-empty { color:var(--text-3); font-style:italic; }
.bo-relnotes-foot {
  display:flex; align-items:center; gap:6px; margin:var(--sp-3) 0 0;
  font-size:12.5px; color:var(--text-3);
}
.bo-relnotes-foot .bo-icon { flex:none; }

/* Post-update notice: where the pre-update database was snapshotted. A block and
   not a -foot row because the value is a filesystem path, which is long and has no
   spaces to break at, so it gets its own line and is allowed to wrap anywhere
   rather than widening the dialog. Selectable: the operator's next move is to copy
   it into an scp. */
.bo-update-backup {
  margin:var(--sp-3) 0 0; padding:8px 10px;
  border:1px solid var(--border); border-radius:var(--radius-sm);
  background:var(--surface-2); font-size:12.5px; color:var(--text-3);
}
.bo-update-backup-path {
  margin-top:4px; font-family:var(--font-mono); font-size:12px;
  color:var(--text); overflow-wrap:anywhere; user-select:all;
}

/* Backup filename picker. The preview is the dialog's answer, so it reads as a
   result panel under the ticks rather than as another form row, and it carries the
   monospace face because it is a filename. */
.bo-backup-name-desc { margin:0 0 var(--sp-3); font-size:13px; color:var(--text-3); }
.bo-backup-name-preview {
  margin-top:var(--sp-2); padding:8px 10px;
  border:1px solid var(--border); border-radius:var(--radius-sm);
  background:var(--surface-2);
}
.bo-backup-name-label { font-size:11.5px; color:var(--text-3); }
.bo-backup-name-file {
  display:block; margin-top:3px;
  font-family:var(--font-mono); font-size:12.5px;
  color:var(--text); overflow-wrap:anywhere; user-select:all;
}

/* Donate dialog (VPN-UI tile). Each row is chain / address / copy. The address
   is the payload, so it gets the monospace face and the room: the chain label
   is fixed-width and the button is intrinsic, leaving the middle column to
   absorb whatever is left. Addresses are long and unbreakable, hence the
   explicit wrap rather than an ellipsis — a truncated address invites copying
   half of one by hand. */
.bo-donate-intro { margin:0 0 var(--sp-3); font-size:13px; color:var(--text-3); }
.bo-donate-list { margin:0; padding:0; list-style:none; display:flex; flex-direction:column; gap:6px; }
.bo-donate-row {
  display:grid; grid-template-columns:96px minmax(0,1fr) auto;
  align-items:center; gap:var(--sp-2);
  padding:8px 10px; border:1px solid var(--border);
  border-radius:var(--radius-sm); background:var(--surface-2);
}
.bo-donate-chain {
  font-size:12px; font-weight:var(--fw-semibold); color:var(--text-3);
  letter-spacing:.02em;
}
.bo-donate-addr {
  font-family:var(--font-mono); font-size:12px; color:var(--text);
  background:none; padding:0; overflow-wrap:anywhere; user-select:all;
}
.bo-donate-copy { flex:none; }
.bo-icon-btn--ok { color:var(--ok); border-color:var(--ok); }

/* Under ~560px the three columns cannot all hold their minimum, so the row
   becomes two lines: chain and button on the first, the address spanning the
   second. Every cell is placed explicitly — grid auto-placement only moves
   forward, so once the full-width address claims row 2 the button would land
   on a row 3 of its own instead of back beside the chain. */
@media (max-width:560px) {
  .bo-donate-row { grid-template-columns:minmax(0,1fr) auto; row-gap:6px; }
  .bo-donate-chain { grid-column:1; grid-row:1; }
  .bo-donate-copy { grid-column:2; grid-row:1; }
  .bo-donate-addr { grid-column:1 / -1; grid-row:2; }
}

/* ================================================================== *
 * stat grids
 *
 * The hairline trick: 1px gap over a --border background, with opaque cells,
 * gives dividers without per-cell border arithmetic.
 * ================================================================== */

.bo-stat-grid {
  display:grid; grid-template-columns:repeat(2,1fr); gap:1px;
  background:var(--border); border:1px solid var(--border);
  border-radius:var(--radius-lg); overflow:hidden;
}
/* User status tile: total / online / depleted. Three equal cells on the same
   hairline-grid trick as .bo-stat-grid. */
.bo-users-grid { grid-template-columns:repeat(3,1fr); }
@media (max-width:520px) { .bo-users-grid { grid-template-columns:1fr; } }
.bo-users-value--online { color:var(--ok); }
.bo-users-value--depleted { color:var(--error); }

/* Editable server name. Always visible: a hover-only affordance is
   undiscoverable, and this is the one control on the tile. */
.bo-ident-nameline { display:flex; align-items:center; gap:8px; min-width:0; }
.bo-ident-edit {
  border:1px solid var(--border); background:var(--surface-2); color:var(--text-3);
  cursor:pointer; padding:4px; border-radius:6px; flex:0 0 auto;
  display:inline-flex; align-items:center; justify-content:center;
  transition:color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease),
             background var(--dur-fast) var(--ease);
}
.bo-ident-edit:hover { color:var(--accent); border-color:var(--accent); background:var(--accent-weak); }
.bo-ident-sub { font-size:var(--fs-base); color:var(--text-3); margin-top:4px; }

/* Labelled spec rows: tinted icon, key, value. One fact per line. */
/* Splitting Uptime into Panel + Server took this list from seven rows to eight.
   The tile must keep its footprint (it shares a bento row with Overall Speed and
   System Status, and growing it grows the whole row), so the extra row is paid
   for out of row padding and the list's top margin rather than out of the grid.
   Measured: at 1px the eight rows land inside the height the seven used, so the
   bento row height stays set by Overall Speed exactly as before. The 1px rules
   between rows still separate them; the padding was slack, not structure. */
.bo-spec { display:flex; flex-direction:column; gap:0; margin:15px 0 0; }
.bo-spec-row {
  display:grid; grid-template-columns:22px minmax(70px,auto) 1fr;
  align-items:center; gap:9px; padding:1px 0;
}
.bo-spec-row + .bo-spec-row { border-top:1px solid var(--border); }
.bo-spec-ico { display:inline-flex; align-items:center; justify-content:center; }
.bo-spec-key {
  font-size:var(--fs-sm); font-weight:var(--fw-bold); color:var(--text-3);
  text-transform:uppercase; letter-spacing:.04em; margin:0;
}
.bo-spec-val {
  font-size:var(--fs-base); font-weight:var(--fw-semibold); color:var(--text);
  margin:0; min-width:0; overflow-wrap:anywhere; text-align:end;
}
.bo-spec-val.bo-ok { color:var(--ok); }
.bo-spec-val.bo-warn { color:var(--warn); }
/* Each row's icon gets its own tint so a row can be found by colour first.
   These are the reserved status hues plus the accent, not new colours. */
.bo-spec-ico--os { color:var(--accent); }
.bo-spec-ico--kernel { color:var(--warn); }
.bo-spec-ico--cpu { color:var(--info); }
.bo-spec-ico--cores { color:var(--text-3); }
.bo-spec-ico--ram { color:var(--text-3); }

/* Resellers: one .bo-tile per account, laid out by auto-fill rather than by the
   Overview's named grid areas. The Overview has a fixed cast of tiles it can
   name; here the count is DATA, so the track list has to come from the content.
   Everything inside a card is the Overview's own vocabulary (.bo-tile-head,
   .bo-spec rows), so this section only owns the grid and the balance block. */
.bo-cards { display:grid; grid-template-columns:1fr; gap:var(--gap-md); }
@media (min-width:720px) {
  .bo-cards { grid-template-columns:repeat(auto-fill,minmax(330px,1fr)); }
}
.bo-rs-head {
  display:flex; align-items:flex-start; justify-content:space-between;
  gap:10px; margin-bottom:12px;
}
.bo-rs-name {
  font-size:var(--fs-lg,15px); font-weight:var(--fw-bold);
  color:var(--text); overflow-wrap:anywhere;
}
.bo-rs-sub { font-size:var(--fs-xs); color:var(--text-3); overflow-wrap:anywhere; }
/* The balance is why this page exists, so it gets the size an operator reads
   first. Tabular figures because these numbers sit in a column across cards and
   ragged digits make them look misaligned even when they are not. */
.bo-rs-bal {
  border:1px solid var(--border); border-radius:var(--radius-lg,10px);
  padding:10px 12px; background:var(--surface-2,transparent);
}
.bo-rs-bal-val {
  font-size:22px; font-weight:var(--fw-bold); line-height:1.2;
  font-variant-numeric:tabular-nums; overflow-wrap:anywhere;
}
.bo-rs-bal-val.bo-ok { color:var(--ok); }
.bo-rs-bal-val.bo-warn { color:var(--warn); }
.bo-rs-bal-label { font-size:var(--fs-xs); color:var(--text-3); margin-top:2px; }
.bo-spec-ico--virt { color:var(--info); }
.bo-spec-ico--loc { color:var(--ok); }
.bo-spec-val.bo-muted { color:var(--text-3); font-weight:var(--fw-medium,500); }
/* bo-muted as a plain utility. It used to exist ONLY compounded with
   .bo-spec-val above, so the two descriptions in the reseller-delete dialog -
   the only places that use it bare - matched no rule at all and rendered in the
   inherited colour, which is why that dialog looked untethered from the theme.
   Weight stays on the compound rule; standalone use is colour only. */
.bo-muted { color:var(--text-3); }
.bo-spec-ico--health { color:var(--ok); }
.bo-spec-ico--panel { color:var(--accent); }

/* Clients: three people marks, no column rules. Colour carries the state -
   accent for the total, ok for online, error for depleted. */
.bo-people {
  display:grid; grid-template-columns:repeat(3,1fr);
  gap:var(--sp-3); text-align:center;
}
@media (max-width:520px) { .bo-people { grid-template-columns:1fr; } }
.bo-person {
  display:flex; flex-direction:column; align-items:center; gap:4px;
  padding:9px 8px; border-radius:var(--radius-lg);
  background:var(--surface-2); border:1px solid var(--border);
}
.bo-person-ico { width:21px; height:21px; fill:none; stroke:currentColor;
  stroke-width:1.7; stroke-linecap:round; stroke-linejoin:round; }
.bo-person-val { font-size:21px; font-weight:var(--fw-bold); line-height:1.1; color:var(--text); }
.bo-person-key {
  font-size:var(--fs-xs); font-weight:var(--fw-semibold); color:var(--text-3);
  text-transform:uppercase; letter-spacing:.04em;
}
.bo-person--total { color:var(--text-2); }
.bo-person--online { color:var(--ok); }
.bo-person--online .bo-person-val { color:var(--ok); }
.bo-person--depleted { color:var(--error); }
.bo-person--depleted .bo-person-val { color:var(--error); }

/* Interface picker sitting in the throughput tile head. */
.bo-iface-pick { display:flex; align-items:center; gap:6px; }

/* Wider bands, e.g. the inbounds totals row. Collapses to 2-up then 1-up so
   the cells never crush their values. */
.bo-stat-grid--5 { grid-template-columns:repeat(5,1fr); }
@media (max-width:1100px) { .bo-stat-grid--5 { grid-template-columns:repeat(2,1fr); } }
@media (max-width:560px) { .bo-stat-grid--5 { grid-template-columns:1fr; } }

.bo-stat-cell { background:var(--surface); padding:14px 16px; display:flex; flex-direction:column; gap:5px; min-width:0; }
.bo-stat-cell-label {
  font-size:var(--fs-xs); font-weight:var(--fw-semibold);
  text-transform:uppercase; letter-spacing:.04em; color:var(--text-3);
}
.bo-stat-cell-value { font-size:15.5px; font-weight:var(--fw-bold); color:var(--text); }

/* Same centred-pair shape as the throughput totals, so the two cards rhyme.
   Figures are smaller than the throughput headline: these are secondary. */
.bo-conn-body { display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:12px; }
.bo-conn-stat { display:flex; flex-direction:column; align-items:center; gap:3px; }
.bo-conn-label {
  display:flex; align-items:center; gap:6px;
  font-size:11.5px; font-weight:var(--fw-bold);
  text-transform:uppercase; letter-spacing:.04em; color:var(--text-3);
}
.bo-conn-label .bo-icon { color:var(--text-3); }
.bo-conn-stat--tcp .bo-conn-label .bo-icon { color:var(--accent); }
.bo-conn-stat--udp .bo-conn-label .bo-icon { color:var(--info); }
.bo-conn-value { font-size:17px; font-weight:var(--fw-bold); color:var(--text); }
/* Sits directly under the figures, not pushed to the foot of a stretched
   tile, which left a visible void between them. */
.bo-conn-ratio {
  display:flex; gap:2px; height:6px; border-radius:3px;
  overflow:hidden; background:var(--surface); margin-top:0;
}
.bo-conn-ratio-seg { display:block; height:100%; }
.bo-conn-ratio-seg--tcp { background:var(--accent); }
.bo-conn-ratio-seg--udp { background:var(--info); }

/* ================================================================== *
 * IP addresses
 * ================================================================== */

.bo-ip-row { display:flex; align-items:baseline; justify-content:space-between; gap:12px; padding:8px 0; }
.bo-ip-row + .bo-ip-row { border-top:1px solid var(--border); }
.bo-ip-label { font-size:var(--fs-sm); font-weight:var(--fw-medium); color:var(--text-2); flex:0 0 auto; }
.bo-ip-value {
  font-size:var(--fs-base); font-weight:var(--fw-semibold); color:var(--text);
  text-align:end; overflow-wrap:anywhere;
  direction:ltr; unicode-bidi:isolate;
}
/* bento lacked this; aurora had it. Long IPv6 crowds the label on narrow screens. */
@media (max-width:479px) {
  .bo-ip-row { flex-direction:column; align-items:flex-start; gap:2px; }
  .bo-ip-value { text-align:start; }
}

.bo-foot {
  margin-top:34px; padding-top:var(--sp-4);
  border-top:1px solid var(--border);
  font-size:11.5px; color:var(--text-3); text-align:start;
}

/* ================================================================== *
 * narrow screens: rail collapses to icons only
 * ================================================================== */

@media (max-width:560px) {
  .bo-rail { flex-basis:76px; width:76px; padding:16px 0; gap:20px; }
  .bo-rail-item { width:64px; }
  .bo-rail-item span,
  .bo-rail-brand span { display:none; }
  .bo-topbar-inner { padding:12px 16px; }
  .bo-content { padding:20px 16px 40px; }
}

/* ================================================================== *
 * client action row
 *
 * The eight per-client actions (QR, edit, info, reset, freeze, delete, TXT,
 * PDF) were bare inline siblings in the table cell, so a narrow Menu column
 * wrapped them onto three ragged lines. One nowrap flex row keeps them on a
 * single line and lets the column size itself from their real width.
 * ================================================================== */

.bo-client-actions {
  display:flex; flex-wrap:nowrap; align-items:center;
  gap:7px; white-space:nowrap;
}
/* The markup sets fontSize:22px inline on each icon, which at eight icons is
   wider than any sane column. 18px keeps them comfortably tappable and fits
   the 200px the column now reserves. !important is needed to beat the inline
   style, not to win a specificity fight. */
.bo-client-actions .anticon { font-size:18px !important; }
.bo-client-actions svg { width:18px; height:18px; }
/* The inline marginInlineStart on the QR icons existed to space them apart by
   hand; the flex gap does that now, so neutralise it or the row is lopsided. */
.bo-client-actions .anticon { margin-inline-start:0 !important; }
.bo-client-actions .anticon,
.bo-client-actions svg { flex:0 0 auto; }

/* ================================================================== *
 * distro marks
 *
 * The Overview identity tile paints its distro logo as a CSS mask over
 * a solid --distro-ink, not as an <svg><use>. Two reasons: the browser
 * fetches only the one mark the machine needs (48 inlined symbols came
 * to 83KB), and the colour stays in CSS, where a theme can override it.
 *
 * Sources are web/assets/img/distro/<mark>.svg, generated from the
 * Simple Icons project (https://simpleicons.org, 16.27.0), which
 * publishes these marks under CC0 1.0. The path data is unmodified.
 * The brands themselves belong to their respective owners.
 *
 * Adding a distro means three things in step: the SVG file, a
 * .bo-distro--<mark> rule here, and a DISTRO_MARKS entry in index.html.
 * ================================================================== */

.bo-distro {
  display:block;
  background-color:var(--distro-ink,var(--text-2));
  /* -webkit- duplicates are for Safari before 15.4; the unprefixed
     longhands must stay paired with them or one engine sees a half
     configured mask and paints the full box. */
  -webkit-mask-repeat:no-repeat;   mask-repeat:no-repeat;
  -webkit-mask-position:center;    mask-position:center;
  -webkit-mask-size:contain;       mask-size:contain;
  /* No mask-mode: the source is an image, not an SVG <mask> element, so
     the default match-source already resolves to alpha. The marks are
     opaque on transparent, which is exactly what alpha wants. */
}

/* Brand hex, verbatim from Simple Icons. */
.bo-distro--ubuntu{--distro-ink:#E95420}
.bo-distro--debian{--distro-ink:#A81D33}
.bo-distro--fedora{--distro-ink:#51A2DA}
.bo-distro--arch{--distro-ink:#1793D1}
.bo-distro--centos{--distro-ink:#262577}
.bo-distro--almalinux{--distro-ink:#000000}
.bo-distro--rocky{--distro-ink:#10B981}
.bo-distro--rhel{--distro-ink:#EE0000}
.bo-distro--opensuse{--distro-ink:#73BA25}
.bo-distro--sles{--distro-ink:#0C322C}
.bo-distro--alpine{--distro-ink:#0D597F}
.bo-distro--linuxmint{--distro-ink:#86BE43}
.bo-distro--manjaro{--distro-ink:#35BFA4}
.bo-distro--gentoo{--distro-ink:#54487A}
.bo-distro--kali{--distro-ink:#557C94}
.bo-distro--pop{--distro-ink:#48B9C7}
.bo-distro--elementary{--distro-ink:#64BAFF}
.bo-distro--zorin{--distro-ink:#15A6F0}
.bo-distro--nixos{--distro-ink:#5277C3}
.bo-distro--void{--distro-ink:#478061}
.bo-distro--artix{--distro-ink:#10A0CC}
.bo-distro--endeavouros{--distro-ink:#7F7FFF}
.bo-distro--garuda{--distro-ink:#8839EF}
.bo-distro--devuan{--distro-ink:#004489}
.bo-distro--deepin{--distro-ink:#007CFF}
.bo-distro--mx{--distro-ink:#000000}
.bo-distro--parrot{--distro-ink:#15E0ED}
.bo-distro--raspbian{--distro-ink:#A22846}
.bo-distro--slackware{--distro-ink:#000000}
.bo-distro--solus{--distro-ink:#5294E2}
.bo-distro--neon{--distro-ink:#3DC08D}
.bo-distro--kubuntu{--distro-ink:#0079C1}
.bo-distro--xubuntu{--distro-ink:#0044AA}
.bo-distro--lubuntu{--distro-ink:#0068C8}
.bo-distro--ubuntu-mate{--distro-ink:#84A454}
.bo-distro--cachyos{--distro-ink:#00AA88}
.bo-distro--nobara{--distro-ink:#000000}
.bo-distro--asahi{--distro-ink:#A61200}
.bo-distro--omarchy{--distro-ink:#9ECE6A}
.bo-distro--qubes{--distro-ink:#3874D8}
.bo-distro--tails{--distro-ink:#56347C}
.bo-distro--photon{--distro-ink:#004480}
.bo-distro--openwrt{--distro-ink:#00B5E2}
.bo-distro--proxmox{--distro-ink:#E57000}
.bo-distro--freebsd{--distro-ink:#AB2B28}
.bo-distro--openbsd{--distro-ink:#F2CA30}
.bo-distro--netbsd{--distro-ink:#FF6600}
.bo-distro--linux{--distro-ink:#FCC624}

/* A logotype is exempt from WCAG contrast and these are not carrying
   meaning on their own, so the bar is legibility, not AA: a mark is
   only re-stepped when it drops under 2.2:1 on the surface it sits on,
   and then only as far as 2.8:1. Hue and chroma are held, so the brand
   still reads. 43 of 48 marks are exact in light, 35 in dark. */

/* Too pale on white. Scoped to :not(.dark) rather than left unscoped:
   these sit after the base block, so an unscoped rule would also hold
   in dark mode, where the brand hex is legible and should win. Tux at
   #FCC624 is 10.4:1 on the dark surface and only needs darkening on
   white. .light is not relied on: aThemeSwitch replaces the whole body
   class list, and light is the no-class default. */
body:not(.dark) .bo-distro--elementary{--distro-ink:#4AA0E4}
body:not(.dark) .bo-distro--parrot{--distro-ink:#0DAAB5}
body:not(.dark) .bo-distro--omarchy{--distro-ink:#7AA743}
body:not(.dark) .bo-distro--openbsd{--distro-ink:#B8970E}
body:not(.dark) .bo-distro--linux{--distro-ink:#BE9515}

/* Too dark on --surface. The four pure-black brands (AlmaLinux, MX,
   Nobara, Slackware) invert to near-white instead of lifting to grey,
   which is what those projects do with their own marks on dark. */
body.dark .bo-distro--centos{--distro-ink:#545DB4}
body.dark .bo-distro--almalinux{--distro-ink:#E9EDF3}
body.dark .bo-distro--sles{--distro-ink:#476C65}
body.dark .bo-distro--alpine{--distro-ink:#256A91}
body.dark .bo-distro--gentoo{--distro-ink:#695D91}
body.dark .bo-distro--devuan{--distro-ink:#2A66AE}
body.dark .bo-distro--mx{--distro-ink:#E9EDF3}
body.dark .bo-distro--slackware{--distro-ink:#E9EDF3}
body.dark .bo-distro--xubuntu{--distro-ink:#2260C8}
body.dark .bo-distro--nobara{--distro-ink:#E9EDF3}
body.dark .bo-distro--asahi{--distro-ink:#BD2F1D}
body.dark .bo-distro--tails{--distro-ink:#7655A0}
body.dark .bo-distro--photon{--distro-ink:#2C67A6}

/* Outbound test: the exit address the probe observed, under the delay tag.
   The IPs are `num` (LTR-pinned, tabular) so a v6 address cannot be bidi
   reordered in fa/ar, and they wrap rather than widen the column. */
.ob-exit {
  display:flex; align-items:flex-start; justify-content:center; gap:5px;
  margin-top:4px; line-height:1.35;
}
.ob-exit-flag { font-size:13px; flex:none; }
.ob-exit-ips { display:flex; flex-direction:column; min-width:0; }
/* Sized to match the delay tag above it (ant-tag's 12px), so the flag, the
   "123ms" and the address all read as one block of result rather than the
   address looking like a dimmed footnote. Blue is the reserved info hue. */
.ob-exit-flag { font-size:12px; line-height:20px; flex:none; }
.ob-exit-ip {
  font-size:12px; line-height:1.5; color:var(--info); font-weight:var(--fw-semibold);
  overflow-wrap:anywhere; word-break:break-all;
}

/* ============================================================
   Log viewer. One appearance for the panel log, Xray and every
   bundled daemon; see LogFormatter in assets/js/util/index.js.
   ============================================================ */
.log-line {
  font-family:var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
  font-size:12.5px; line-height:1.5; white-space:pre-wrap; overflow-wrap:anywhere;
  /* Flex, so a wrapped line indents under its own message instead of restarting
     in the badge column and reading like a new entry. A negative text-indent
     did the same job but dragged the badge out past the scroll container's edge
     and clipped it. */
  display:flex; align-items:baseline; gap:7px;
}
.log-body { flex:1 1 auto; min-width:0; }
.log-line:hover { background:var(--surface-2, rgba(127,127,127,.06)); }
/* The source badge is NEUTRAL by design: thirteen cores is well past the number
   hue can distinguish, the same reason protocol tags elsewhere carry no colour.
   Identity comes from the text; colour is reserved for severity.
   Deliberately BORDERLESS and unpadded vertically: a bordered, padded chip was
   taller than the text it labelled, so every row inherited the chip's height and
   the list gained a gap between lines that made it hard to scan. */
.log-src {
  flex:0 0 62px; padding:0 4px; border-radius:3px;
  background:var(--surface-2, rgba(127,127,127,.10));
  color:var(--text-3); font-weight:var(--fw-bold);
  font-size:10px; line-height:inherit; text-align:center;
  text-transform:uppercase; letter-spacing:.02em;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}

/* Core filter above the log pane. */
.log-filter {
  display:flex; align-items:center; flex-wrap:wrap; gap:4px 12px;
  margin:8px 0 0; padding:8px 10px;
  border:1px solid var(--border); border-radius:var(--radius-md);
  background:var(--surface-2, rgba(127,127,127,.05));
}
.log-filter-label {
  font-size:var(--fs-sm); font-weight:var(--fw-bold); color:var(--text-3);
  text-transform:uppercase; letter-spacing:.04em;
}
.log-filter .ant-checkbox-wrapper { margin-inline-start:0; font-size:12px; }
.log-filter .ant-btn-link { padding:0 4px; height:auto; font-size:12px; }

/* Live tail indicator. The dot pulses so an idle log that happens to produce
   no new lines still reads as "following" rather than "stuck". */
.log-live-dot {
  display:inline-block; width:7px; height:7px; border-radius:50%;
  margin-inline-end:6px; background:currentColor; vertical-align:middle;
  animation:log-live-pulse 1.6s var(--ease, ease-in-out) infinite;
}
@keyframes log-live-pulse {
  0%, 100% { opacity:1; transform:scale(1); }
  50%      { opacity:.35; transform:scale(.7); }
}
/* The panel disables animation globally while the theme is switching, and for
   readers who ask for reduced motion; a blinking dot is exactly what that
   preference is about. */
@media (prefers-reduced-motion: reduce) {
  .log-live-dot { animation:none; }
}
html[data-theme-animations='off'] .log-live-dot { animation:none; }
.log-time { color:var(--text-3); }
.log-lvl { font-weight:var(--fw-bold); }
.log-lvl.log-error { color:var(--error); }
.log-lvl.log-warn { color:var(--warn); }
.log-lvl.log-info { color:var(--ok); }
.log-lvl.log-debug { color:var(--info); }
.log-msg { color:var(--text); }
.log-empty { color:var(--text-3); padding:6px 0; }

/* Subscription block inside the per-protocol config modals (component/aSubBlock).
   Same two-column shape as the config rows those modals already use: text on the
   left, QR on the right, wrapping to one column when there is no room. The QR keeps
   a white plate in both themes because that is what scanners need. */
.cfg-sub-block {
  padding:14px 0;
  border-block-start:1px solid var(--border);
  margin-block-start:4px;
}
.cfg-sub-head { margin-block-end:8px; }
.cfg-sub-body { display:flex; flex-wrap:wrap; gap:16px; align-items:flex-start; }
.cfg-sub-text { flex:1 1 300px; min-width:260px; }
.cfg-sub-qr { flex:0 0 auto; display:flex; gap:8px; }
.cfg-sub-qr canvas {
  width:172px; height:172px; cursor:pointer;
  background:#fff; border-radius:var(--radius-sm); padding:6px;
}

/* ================================================================== *
 * client form (Clients page add / edit)
 *
 * A summary rail on the left - who the account is and what it has left -
 * with the section tabs beneath it, and one pane on the right. The rail
 * stays put while the operator moves between sections, so a credential
 * can be rotated without losing sight of whose it is.
 *
 * On ADD there is nothing stored to summarise, so the rail previews what
 * is being typed rather than sitting empty.
 * ================================================================== */

/* Sized to its CONTENT, not to the tallest tab.

   This was a fixed height set by whichever section was tallest, on the theory
   that a modal which resizes between tabs makes the footer jump. In practice it
   just made the window too big: Identity is the section every operator fills in
   and the one that must never scroll, and pinning the height to Credentials left
   ~90px of dead space under it on every add.

   So: no fixed height. The modal is as tall as the section on screen, with a
   floor so the short sections do not collapse into a letterbox, and a viewport
   ceiling so a long one cannot push the footer (and the save button with it) off
   a short screen. Identity comes in well under the ceiling at any realistic size,
   which is what "never scrolls" means here; a section that does exceed it scrolls
   inside the pane rather than growing the window. */
.bo-cf {
  display:flex; align-items:stretch; margin:-24px;
  min-height:340px; max-height:calc(100vh - 210px);
}
.bo-cf-rail {
  flex:0 0 200px; border-inline-end:1px solid var(--border);
  background:var(--surface-2); display:flex; flex-direction:column;
}
/* min-height:0 is what lets the pane actually scroll: a flex item defaults to
   min-height:auto, which refuses to shrink below its content and pushes the
   overflow out of the modal instead of into a scrollbar. */
.bo-cf-pane { flex:1 1 auto; min-width:0; min-height:0; padding:20px 22px; overflow-y:auto; }

.bo-cf-summary { padding:16px; border-bottom:1px solid var(--border); }
.bo-cf-who {
  font-size:15px; font-weight:600; line-height:1.3; margin-bottom:2px;
  word-break:break-all; color:var(--text);
}
.bo-cf-sub { font-size:11.5px; color:var(--text-3); margin-bottom:12px; line-height:1.4; }
.bo-cf-kv {
  display:flex; justify-content:space-between; align-items:baseline;
  font-size:12px; padding:3px 0; color:var(--text-2);
}
.bo-cf-kv b { color:var(--text); font-weight:600; font-variant-numeric:tabular-nums; }

.bo-cf-tabs { padding:10px; display:flex; flex-direction:column; gap:2px; }
.bo-cf-tab {
  display:flex; align-items:center; gap:9px; width:100%;
  font:inherit; font-size:13.5px; text-align:start; cursor:pointer;
  padding:8px 11px; border:0; border-radius:7px;
  background:transparent; color:var(--text-2);
}
.bo-cf-tab:hover { color:var(--text); background:var(--surface); }
.bo-cf-tab[aria-selected="true"] {
  background:var(--surface); color:var(--accent);
  font-weight:600; box-shadow:0 1px 2px rgba(16,24,40,.07);
}
.bo-cf-tab:focus-visible { outline:2px solid var(--accent); outline-offset:1px; }

.bo-cf-field { margin-bottom:14px; }
.bo-cf-field > label {
  display:flex; align-items:center; gap:6px; flex-wrap:wrap;
  font-size:12px; font-weight:600; color:var(--text-2); margin-bottom:5px;
}
.bo-cf-field > label .ant-tag { margin-inline-end:0; font-size:11px; line-height:17px; }
.bo-cf-hint { font-size:11.5px; color:var(--text-3); margin-top:4px; }

/* A setting that belongs to the field above it rather than beside it: mtproto's ad
   tag under mtproto's secret. The rule and the indent are the whole of it, and they
   are what say "part of that" instead of "next thing down" - as a sibling row it
   read as a form-level setting, which is exactly what it is not. */
.bo-cf-nest {
  margin-top:9px; padding-inline-start:12px;
  border-inline-start:2px solid var(--border-strong);
}
/* The switch row's own separator would cut the group it is heading. */
.bo-cf-nest > .bo-cf-switch { padding:0 0 8px; border-bottom:0; }
/* Loud enough to stop an operator, small enough to stay inside the group. An
   a-alert here spans the pane and so reads as a warning about the whole form. */
.bo-cf-warnnote {
  display:flex; gap:6px; margin-top:7px; padding:7px 9px; border-radius:6px;
  background:var(--warn-weak); color:var(--warn);
  font-size:11.5px; line-height:1.5;
}
.bo-cf-warnnote > .anticon { flex:0 0 auto; margin-top:2px; }
/* Dimmed, not hidden: a field that vanishes as inbounds are ticked makes the
   form shift under the operator mid-edit. */
.bo-cf-unused { opacity:.5; }
.bo-cf-mono input { font-family:var(--font-mono, ui-monospace, monospace); font-size:12.5px; }

/* An input with a trailing icon button, matching the addonAfter that a-input
   gets for free. a-input-number and a-date-picker have no addon slot, so the
   pairing is built here rather than faked with absolute positioning. */
.bo-cf-withicon { display:flex; align-items:stretch; }
.bo-cf-withicon > *:first-child { flex:1 1 auto; min-width:0; }
.bo-cf-withicon > *:first-child .ant-input-number,
.bo-cf-withicon > *:first-child .ant-input {
  border-start-end-radius:0; border-end-end-radius:0;
}
.bo-cf-iconbtn {
  flex:0 0 auto; display:inline-flex; align-items:center; justify-content:center;
  width:34px; font:inherit; cursor:pointer; color:var(--text-2);
  background:var(--surface-2); border:1px solid var(--border); border-inline-start:0;
  border-start-end-radius:2px; border-end-end-radius:2px;
}
.bo-cf-iconbtn:hover { color:var(--accent); }
.bo-cf-iconbtn:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }

.bo-cf-switch {
  display:flex; align-items:center; justify-content:space-between; gap:14px;
  padding:11px 0; border-bottom:1px solid var(--border);
}
.bo-cf-switch:last-child { border-bottom:0; }
.bo-cf-t { font-size:13.5px; color:var(--text); }
.bo-cf-d { font-size:11.5px; color:var(--text-3); margin-top:2px; }

.bo-cf-ib {
  border:1px solid var(--border); border-radius:7px;
  padding:7px 10px; margin-bottom:7px;
}
.bo-cf-ib.on { border-color:var(--accent); background:var(--accent-weak); }
.bo-cf-ib .ant-checkbox-wrapper { width:100%; }

/* The bulk form reuses the shell above, and adds three things to it.

   A rail value that is a name rather than a figure (the target inbound) can be
   arbitrarily long, so it is truncated instead of pushing its own label out of
   the rail. */
.bo-cf-ell { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

/* The shape of the generated emails, under the count. Monospace so the eight
   x's standing in for the random stem line up with the real thing. */
.bo-cf-shape {
  font-family:var(--font-mono, ui-monospace, monospace);
  font-size:11px; word-break:break-all;
}

/* One row per account the batch would create. */
.bo-cf-list { border:1px solid var(--border); border-radius:7px; overflow:hidden; }
.bo-cf-row {
  display:flex; align-items:center; justify-content:space-between; gap:8px;
  padding:6px 10px; border-bottom:1px solid var(--border);
  font-family:var(--font-mono, ui-monospace, monospace);
  font-size:12.5px; color:var(--text); word-break:break-all;
}
.bo-cf-row:last-child { border-bottom:0; }
.bo-cf-row.clash { background:var(--error-weak); color:var(--error); }
.bo-cf-row .ant-tag { margin-inline-end:0; flex:0 0 auto; }
/* The minted half of a name, and the row counts beside it: present, but not
   what the operator is here to read. */
.bo-cf-stem { color:var(--text-3); }

/* The rail stacks above the pane once there is no room beside it. */
@media (max-width:640px) {
  .bo-cf { flex-direction:column; height:auto; }
  .bo-cf-rail { flex:0 0 auto; border-inline-end:0; border-bottom:1px solid var(--border); }
  .bo-cf-tabs { flex-direction:row; overflow-x:auto; }
  .bo-cf-tab { width:auto; white-space:nowrap; }
  .bo-cf-pane { overflow-y:visible; }
}

/* ================================================================== *
 * clients ledger (the Clients page table)
 *
 * The row used to spend its width on a badge, a wrapping tag list and five
 * 28px buttons crammed against the right edge, while the two figures an
 * operator actually reads - how much is left, how long is left - were the
 * smallest grey text on the line. This inverts that: state collapses to one
 * lamp, the actions collapse to a switch and one menu, and the space goes to
 * the figures.
 * ================================================================== */

/* One glyph for the whole account state, with the label in its tooltip. Hue is
   doing real work here (it is the reserved status scale), which is why the
   protocol chips beside it must not also be carrying identity hues. */
.bo-lamp { width:8px; height:8px; border-radius:50%; flex:0 0 auto; background:var(--off); }
.bo-lamp--ok { background:var(--ok); }
.bo-lamp--warn { background:var(--warn); }
.bo-lamp--error { background:var(--error); }
.bo-lamp--off { background:var(--off); }
/* Online is a ring rather than a sixth colour: it is orthogonal to health, so
   an account can be online AND nearly out of data. */
.bo-lamp--live { box-shadow:0 0 0 3px var(--ok-weak); }

.bo-lg-who { display:flex; align-items:center; gap:9px; min-width:0; }
.bo-lg-names { min-width:0; }
.bo-lg-name { font-weight:500; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.bo-lg-note {
  font-size:11.5px; color:var(--text-3);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}

.bo-lg-figs {
  font-size:12px; color:var(--text-2); margin-bottom:5px; white-space:nowrap;
  font-variant-numeric:tabular-nums;
}
.bo-lg-figs b { color:var(--text); font-weight:600; }

.bo-meter { height:4px; border-radius:999px; background:var(--surface-2); overflow:hidden; }
.bo-meter > i { display:block; height:100%; background:var(--accent); border-radius:999px; }
.bo-meter--warn > i { background:var(--warn); }
.bo-meter--error > i { background:var(--error); }
/* Unlimited is not 0% and not 100%, so it gets neither. A dashed rail says
   "there is no scale here" without pretending to a value. */
.bo-meter--inf {
  background:repeating-linear-gradient(90deg,var(--border) 0 5px,transparent 5px 10px);
}

.bo-lg-rel { font-size:13px; white-space:nowrap; }
.bo-lg-rel--warn { color:var(--warn); }
.bo-lg-rel--error { color:var(--error); }
.bo-lg-rel--info { color:var(--info); }
.bo-lg-abs { font-size:11px; color:var(--text-3); white-space:nowrap; font-variant-numeric:tabular-nums; }

.bo-lg-serving { display:flex; align-items:center; gap:4px; flex-wrap:wrap; }
.bo-lg-serving .ant-tag { margin:0; }
.bo-lg-port {
  font-family:var(--font-mono, ui-monospace, monospace);
  font-weight:400; opacity:.72; margin-inline-start:3px;
}

.bo-lg-seen { font-size:12px; color:var(--text-3); white-space:nowrap; }
.bo-lg-seen--live { color:var(--ok); }

.bo-lg-acts { display:inline-flex; align-items:center; gap:8px; justify-content:flex-end; }

/* Denser rows for a big fleet. The two lines that go are the two that repeat
   what is already visible elsewhere: the comment (searchable) and the absolute
   date (the relative one above it is the one being scanned).

   !important because the rule being beaten is antd's own size variant,
   `.ant-table-middle > .ant-table-content > .ant-table-body > table >
   .ant-table-tbody > tr > td` - seven classes deep, in four near-identical
   flavours for the scroll and fixed-column cases. Matching that depth would mean
   copying all four and re-copying them on any antd bump; this is the override
   that stays correct. It is not winning a fight with our own cascade. */
.bo-shell .bo-lg--compact .ant-table-tbody > tr > td { padding:6px 14px !important; }
.bo-lg--compact .bo-lg-note,
.bo-lg--compact .bo-lg-abs { display:none; }
.bo-lg--compact .bo-lg-figs { margin-bottom:3px; }

/* ================================================================== *
 * account memberships (the Clients page expander)
 *
 * One block per inbound serving the account. This was a NESTED a-table per
 * inbound: four inbounds meant four column headers, four copies of the same
 * email, and four repeats of the account-wide quota and expiry - the figures
 * the row above already shows once. What actually differs per inbound is the
 * protocol, the credential the customer installs and the bytes that inbound
 * moved, so those are the three columns, and the account-wide controls live on
 * the account row where they belong.
 * ================================================================== */

/* ONE bordered list with hairline dividers, not a stack of separate cards.
   Each membership used to be its own rounded box with an 8px gap under it, which
   read as three unrelated things floating in the expanded row - and the gaps
   were pure empty space in a panel that is short of vertical room. They belong
   to one account, so they are one list. */
.bo-mb-list {
  display:flex; flex-direction:column;
  border:1px solid var(--border); border-radius:9px;
  background:var(--surface); overflow:hidden;
}
.bo-mb {
  display:flex; flex-wrap:wrap; align-items:center; gap:10px 16px;
  padding:9px 14px;
  border-bottom:1px solid var(--border);
}
.bo-mb:last-child { border-bottom:0; }
/* Disabled ON THIS INBOUND ONLY - the account itself is still enabled, so the
   row is recessed rather than marked with a status hue. */
.bo-mb.off { opacity:.55; }

.bo-mb-id { display:flex; align-items:center; gap:7px; flex:1 1 210px; min-width:0; }

/* Live on THIS inbound. Hollow at rest rather than filled-grey, so a row of
   offline memberships reads as an absence of light instead of a row of dots
   competing with the account lamp above it. */
.bo-mb-lamp {
  width:7px; height:7px; border-radius:50%; flex:0 0 auto;
  border:1px solid var(--border); background:transparent;
}
.bo-mb-lamp--live { border-color:var(--ok); background:var(--ok); box-shadow:0 0 0 2px var(--ok-weak); }
/* Connected, but on one of this account's Xray-native inbounds and the core does
   not say which. Filled in the same hue with no ring: it is the same evidence,
   held less certainly, and giving it a second colour would read as a third state
   an operator has to learn. */
.bo-mb-lamp--maybe { border-color:var(--ok); background:var(--ok-weak); }
.bo-mb-name {
  font-weight:500; color:var(--text);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.bo-mb-port { color:var(--text-3); font-variant-numeric:tabular-nums; flex:0 0 auto; }

/* The credential this inbound authenticates on. Monospace because it is copied,
   truncated because a uuid is 36 characters and the whole row is not about it. */
.bo-mb-cred {
  flex:1 1 260px; min-width:0; display:flex; align-items:center; gap:10px;
  font-family:var(--font-mono, ui-monospace, monospace); font-size:12px;
  color:var(--text-2);
}
/* One copyable artifact. There can be two of them on the dial-in protocols, whose
   customer needs a username AND a password, so each is its own click target rather
   than the whole cell being one. */
.bo-mb-cred-one {
  min-width:0; display:flex; align-items:center; gap:5px; cursor:pointer;
}
.bo-mb-cred-one:hover { color:var(--accent); }
/* The tiny label in front of a value. Without it a bare login name and a bare
   password are two indistinguishable strings sitting next to each other. */
.bo-mb-cred-k {
  flex:0 0 auto; font-family:var(--ui, inherit); font-size:10px;
  text-transform:uppercase; letter-spacing:.04em; color:var(--text-3);
}
.bo-mb-cred-v { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.bo-mb-cred .anticon { flex:0 0 auto; font-size:12px; }
/* A share link is far too long for the cell, so the tooltip carries the whole of it
   and wraps rather than running off the edge of the screen. */
.bo-mb-cred-full {
  max-width:420px; margin-top:4px; word-break:break-all; white-space:normal;
  font-family:var(--font-mono, ui-monospace, monospace); font-size:11px; opacity:.85;
}

.bo-mb-use {
  flex:0 0 auto; display:flex; align-items:center; gap:8px;
  font-size:12px; color:var(--text-2); font-variant-numeric:tabular-nums;
}
.bo-mb-use b { color:var(--text); font-weight:600; }

/* "shared": this membership's figure is the account's Xray-native remainder, not
   this one inbound's own bytes. Deliberately a quiet label in the neutral ink and
   NOT one of the four status hues: it qualifies a number, it does not report a
   health state, and colouring it would put a warning on an ordinary vless account. */
.bo-mb-shared {
  font-size:11px; color:var(--text-3, var(--text-2)); font-style:italic;
  white-space:nowrap;
}
.bo-mb-shared-note { max-width:260px; white-space:normal; color:var(--text-2); }

.bo-mb-actions { flex:0 0 auto; margin-inline-start:auto; }

/* ---------------------------------------------------------------------------
 * GRE: the far side's Outbound form, box by box (modals/greConfigModal)
 *
 * Here rather than in a <style> inside that modal for the reason the SSL block
 * below states: Vue strips a <style> out of a template while compiling it, and
 * the block disappears with no warning in a production build.
 *
 * Every selector is under .gre-out. The rules are shaped like a definition list
 * and would otherwise reach every two-column layout in the panel.
 * --------------------------------------------------------------------------- */
.gre-out {
  margin: 12px 0;
  border: 1px solid var(--border);
  border-radius: 9px;
  overflow: hidden;
}
.gre-out-h {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px;
  padding: 8px 12px;
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
}
.gre-out-h span { font-size: 12px; color: var(--text-3); }
.gre-out-row {
  display: grid;
  grid-template-columns: minmax(140px, 34%) 1fr;
  gap: 2px 12px;
  padding: 7px 12px;
  border-bottom: 1px solid var(--border);
}
.gre-out-row:last-child { border-bottom: 0; }
/* A box that must be left empty is recessed rather than hidden: blank is a
   decision the operator has to make deliberately, and a missing row reads as an
   answer nobody gave. */
.gre-out-row--blank { opacity: .72; }
.gre-out-k { font-size: 12px; color: var(--text-2); }
.gre-out-v {
  display: flex; align-items: center; gap: 6px; min-width: 0;
  font-family: var(--font-mono, ui-monospace, monospace); font-size: 12px;
  color: var(--text); cursor: pointer; word-break: break-all;
}
.gre-out-v:hover { color: var(--accent); }
.gre-out-v--none {
  font-family: var(--ui, inherit); font-style: italic;
  color: var(--text-3); cursor: default;
}
.gre-out-v--none:hover { color: var(--text-3); }
/* The note spans both columns so a full sentence is not squeezed into the value
   column, which is sized for an address. */
.gre-out-n {
  grid-column: 1 / -1;
  font-size: 11px; line-height: 1.45; color: var(--text-3);
}

/* ---------------------------------------------------------------------------
 * SSL certificate manager (Panel Settings, the SSL tab)
 *
 * Here rather than in a <style> block inside settings/panel/ssl.html: Vue strips
 * <style> out of a template while compiling it, and the production build has the
 * warning about that compiled out, so such a block disappears without a trace and
 * every rule in it is silently dead.
 *
 * Scoped under .settings-page, which settings.html stamps on #app, so nothing
 * here can reach the other pages.
 * ------------------------------------------------------------------------ */

/* THE HERO: the answer to "do I have a certificate, is it healthy, is it being
   used", in that order, in one card. The name is the largest thing on the tab
   because it is the first thing an operator checks. */
.settings-page .ssl-hero {
  border:1px solid var(--border); border-radius:10px; background:var(--surface);
  padding:18px 20px; margin-bottom:14px;
}
.settings-page .ssl-hero-title {
  font-size:20px; font-weight:600; line-height:1.3; word-break:break-all;
}
.settings-page .ssl-hero-sub {
  color:var(--text-2); font-size:12.5px; line-height:1.6; margin-top:4px;
  word-break:break-word;
}

/* Health in one line. The dot inherits the line's colour, and the line always
   carries the words as well: colour is the second channel here, never the only
   one. */
.settings-page .ssl-health {
  display:flex; align-items:center; gap:8px;
  margin-top:12px; font-size:15px; font-weight:500;
}
.settings-page .ssl-health-dot {
  width:9px; height:9px; border-radius:50%; flex:none;
  background:currentColor;
}
.settings-page .ssl-health-ok { color:var(--ok); }
.settings-page .ssl-health-warn { color:var(--warn); }
.settings-page .ssl-health-bad { color:var(--error); }

.settings-page .ssl-flags { display:flex; flex-wrap:wrap; gap:6px; margin-top:10px; }
.settings-page .ssl-flags .ant-tag { margin:0; }

.settings-page .ssl-inuse {
  display:flex; align-items:center; gap:8px; margin-top:12px;
  color:var(--ok); font-size:13px;
}

.settings-page .ssl-alert { margin:12px 0; }
.settings-page .ssl-alert-btn { margin-top:10px; }
.settings-page .ssl-alert-list { margin:4px 0 0; padding-inline-start:18px; }

.settings-page .ssl-actions { margin-top:8px; }

/* The disruptive consent, beside the buttons it changes. A row rather than a
   line in a form, because agreeing to it disconnects live users. */
.settings-page .ssl-disruptive {
  border:1px solid var(--border); border-radius:8px; background:var(--surface);
  padding:10px 14px; margin-bottom:12px;
}
.settings-page .ssl-disruptive-sub {
  color:var(--text-2); font-size:12px; line-height:1.5;
  margin-top:4px; padding-inline-start:24px;
}

.settings-page .ssl-console { margin-bottom:14px; }
.settings-page .ssl-form { margin-bottom:14px; }
.settings-page .ssl-form-actions { padding:4px 20px; }
.settings-page .ssl-form-alert { margin:8px 20px 0; }

/* THE VALIDATION CHOICE as four situations to recognise, not four protocols to
   know. Each is a card the whole width of the form so the sub-line has room to
   say what the operator is agreeing to. */
.settings-page .ssl-choices { padding:14px 20px 4px; }
.settings-page .ssl-choices-label { font-weight:600; margin-bottom:10px; }
.settings-page .ssl-choices-group { display:flex; flex-direction:column; gap:8px; }
.settings-page .ssl-choice {
  display:flex; align-items:flex-start; gap:6px; margin:0;
  padding:10px 12px; border:1px solid var(--border); border-radius:8px;
  background:var(--surface); white-space:normal;
}
.settings-page .ssl-choice.ant-radio-wrapper-checked {
  border-color:var(--accent); background:var(--accent-weak);
}
.settings-page .ssl-choice > span:last-child {
  display:flex; flex-direction:column; gap:3px; padding-inline-end:0;
}
.settings-page .ssl-choice-title { font-weight:500; line-height:1.4; }
.settings-page .ssl-choice-sub {
  color:var(--text-2); font-size:12.5px; line-height:1.5;
}

.settings-page .ssl-advanced { background:transparent; }

.settings-page .ssl-details { margin-bottom:8px; }
.settings-page .ssl-section-title { font-weight:600; margin:22px 0 2px; }
.settings-page .ssl-details .ssl-section-title:first-child { margin-top:0; }
.settings-page .ssl-section-sub {
  color:var(--text-2); font-size:12.5px; line-height:1.5; margin-bottom:8px;
}
.settings-page .ssl-paths { line-height:1.8; word-break:break-all; }
.settings-page .ssl-budget {
  display:flex; flex-wrap:wrap; align-items:center; gap:6px 24px;
}
.settings-page .ssl-empty { margin:10px; }
.settings-page .ssl-refresh { margin-top:16px; text-align:right; }
.settings-page .ssl-warn-text { color:var(--warn); }

/* Certificate facts as a two-column definition list: label left, value right,
   wrapping the value rather than the label so a long SAN list cannot push the
   column out. */
.settings-page .ssl-kv {
  display:grid; grid-template-columns:minmax(120px, 180px) 1fr;
  gap:6px 16px; font-size:13px;
}
.settings-page .ssl-kv dt { margin:0; color:var(--text-2); }
.settings-page .ssl-kv dd { margin:0; word-break:break-word; }

.settings-page .ssl-mono {
  font-family:var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
  font-size:12px;
}

.settings-page .ssl-budget-count { font-size:22px; font-weight:600; line-height:1; }

/* The action hierarchy made visual. The two free actions are normal-sized
   buttons in the card at the top; the one that spends metered budget is small,
   quiet, and at the bottom of a form the operator had to open. They are NOT
   equivalent and they are deliberately not styled as if they were. Each row
   carries its consequence in the note beside it, since a disabled antd control
   opens no tooltip and a reason only in a tooltip is a reason nobody reads. */
.settings-page .ssl-action {
  display:flex; flex-wrap:wrap; align-items:baseline; gap:8px 12px; padding:10px 0;
}
.settings-page .ssl-action-note {
  flex:1 1 320px; min-width:220px; color:var(--text-2);
  font-size:12.5px; line-height:1.5;
}
.settings-page .ssl-action-last {
  border-top:1px dashed var(--border); margin-top:6px; padding-top:12px;
}

.settings-page .ssl-term .term-body { height:320px; max-height:320px; }
