/* BLACK MESA panel — frosted-glass console.
 *
 * Visual language follows the reference the operator supplied: an abstract dark ground
 * that gives the surfaces something to refract, heavy frosted glass for every panel,
 * hairline borders with a lit top edge, and one saturated red reserved for marking the
 * active item and unread state. White stays the accent for text and primary actions;
 * colour is still reserved strictly for state (online, offline, SYSTEM, danger).
 *
 * WHY THE GRID IS GONE
 *
 * The previous build used a flat black ground with a fixed white grid showing through
 * opaque-ish cards. That is a different aesthetic — technical, flat — and it cannot
 * coexist with frosted glass: the grid behind a blur becomes a grey smear. Both are
 * legitimate; the reference chose glass, so the grid is replaced by soft light blooms
 * that the blur can actually work on. A smooth gradient under frosted glass reads as
 * depth, and a hard grid reads as dirt.
 *
 * WHY BACKDROP-FILTER AND NOT A PNG
 *
 * The frosted look comes from `backdrop-filter` blurring whatever is behind the
 * element, not from a pre-blurred background image. That matters because the panels sit
 * at different heights and sizes: a bitmap could only ever match one of them, and every
 * card would have to be hand-positioned against it. Blur is computed from reality, so
 * it stays correct when a card moves or grows.
 */

:root {
  /* Ground. Not pure black: the reference's background is a mid-dark grey that falls
     away to near-black at the edges, which is what gives the glass its depth. A #000
     ground makes every blurred surface read as a flat dark rectangle. */
  --bg: #0a0a0c;
  --bg-lift: #4b4b54;

  /* Glass. Three parts, and all three are needed:
       - the gradient is the pane itself, translucent enough to show the ground;
       - the blur is what makes it frosted rather than merely transparent;
       - the inset highlight is the lit top edge that reads as thickness. */
  --glass: linear-gradient(150deg, rgba(72, 72, 80, 0.50), rgba(26, 26, 30, 0.42));
  --glass-flat: rgba(32, 32, 37, 0.52);
  --glass-raised: linear-gradient(150deg, rgba(86, 86, 96, 0.58), rgba(34, 34, 39, 0.50));
  --glass-sunken: rgba(0, 0, 0, 0.28);
  --glass-hover: rgba(255, 255, 255, 0.05);

  --glass-line: rgba(255, 255, 255, 0.10);
  --glass-line-strong: rgba(255, 255, 255, 0.18);
  --glass-hi: inset 0 1px 0 rgba(255, 255, 255, 0.13);
  --glass-hi-soft: inset 0 1px 0 rgba(255, 255, 255, 0.06);

  --blur: 26px;
  --blur-sm: 14px;

  /* Text. Pure white for headings, then two translucent steps. Using white at fixed
     alpha rather than three grey hexes keeps the hierarchy consistent on any ground. */
  --text: #ffffff;
  --text-dim: rgba(255, 255, 255, 0.64);
  --text-faint: rgba(255, 255, 255, 0.38);

  --accent: #ffffff;
  --accent-ink: #0b0b0d;

  /* The one saturated colour. The reference marks the active row and the unread dot
     with the same red, and that repetition is what makes it read as a system rather
     than as two unrelated decorations. */
  --red: #ff4a4a;
  --red-glow: rgba(255, 74, 74, 0.45);

  --ok: #39d98a;
  --warn: #ffb020;
  --danger: #ff5d73;

  /* The monospace stack, under one name.
   *
   * Three rules referenced var(--mono) and the name was never declared, so the
   * declaration was dropped in each: the affected text -- the VM metadata line, the
   * sysinfo path, the startup command -- silently fell back to the body font. The
   * literal stack was already written out in full in two other rules (.console and
   * .mono), which is how the same font ended up specified two ways and missing a third
   * time.
   *
   * Declared here so all of them agree, including the two that hard-code it: those
   * still work, and the next reader has one place to change it. */
  --mono: "JetBrains Mono", "Consolas", "Courier New", ui-monospace, monospace;

  --r-xl: 24px;
  --r-lg: 20px;
  --r-md: 14px;
  --r-sm: 11px;
  --r-xs: 9px;

  /* Two shadows, not one: a wide soft one for the lift, and a tight dark one to seat
     the edge. A single large blur reads as haze and the card floats without weight. */
  --shadow: 0 28px 60px -16px rgba(0, 0, 0, 0.8), 0 6px 20px rgba(0, 0, 0, 0.45);
  --shadow-sm: 0 10px 26px -10px rgba(0, 0, 0, 0.7);
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html { background: var(--bg); }

body {
  min-height: 100vh;
  background: var(--bg);
  color: var(--text);
  font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
               "Helvetica Neue", system-ui, sans-serif;
  font-size: 14px;
  line-height: 1.5;
  letter-spacing: 0.1px;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

/* The ground the glass sits on.
 *
 * This is not decoration — it is load-bearing. `backdrop-filter` blurs whatever is
 * behind the element, so a flat ground produces no visible blur at all and every pane
 * reads as a plain dark rectangle. The first version of this file used three soft
 * gradients at 5–10% alpha over near-black; the result was exactly that, and it looked
 * like the old flat design with rounded corners.
 *
 * So the ground has to carry real structure: distinct lobes with a light side and a
 * dark side, sized so a card sitting on one spans both. The lobes are what the blur
 * smears into the soft, uneven wash that reads as frosted glass.
 *
 * Six layered gradients, in this order:
 *   1–2. the primary lit form, upper right, matching where the reference's rose is lit,
 *        plus a weaker counter-form low and left so the light is not one-sided;
 *   3–5. three smaller lobes at different angles. These are the "petals": overlapping
 *        curved surfaces at different depths, which is what gives the ground the
 *        dimensional look the reference has;
 *   6.   the base, falling from grey to near-black.
 *
 * `inset: -25%` then `blur()` is deliberate: a blurred layer keeps a visible edge if it
 * ends exactly at the viewport, so the layer is oversized and the soft edge falls
 * outside the visible area.
 */
body::before {
  content: "";
  position: fixed;
  inset: -25%;
  z-index: 0;
  pointer-events: none;
  background:
    /* Petal contours. Each is a thin bright ring, not a filled disc: after the 44px
       blur a ring becomes a soft glowing edge, and several of them at different sizes
       and centres read as overlapping curved surfaces at different depths. Filled
       discs would just brighten the middle of the frame and lose all structure. */
    radial-gradient(40% 31% at 73% 23%, transparent 51%, rgba(255, 255, 255, 0.15) 53%,
                    transparent 57%),
    radial-gradient(52% 41% at 78% 31%, transparent 57%, rgba(255, 255, 255, 0.12) 59%,
                    transparent 63%),
    radial-gradient(64% 50% at 83% 41%, transparent 61%, rgba(255, 255, 255, 0.09) 63%,
                    transparent 67%),
    radial-gradient(46% 38% at 66% 68%, transparent 55%, rgba(255, 255, 255, 0.07) 57%,
                    transparent 61%),
    radial-gradient(56% 50% at 70% 21%, rgba(255, 255, 255, 0.33),
                    rgba(255, 255, 255, 0.10) 44%, transparent 72%),
    radial-gradient(48% 46% at 22% 78%, rgba(255, 255, 255, 0.21), transparent 66%),
    radial-gradient(32% 28% at 62% 60%, rgba(255, 255, 255, 0.17), transparent 70%),
    radial-gradient(28% 32% at 37% 33%, rgba(255, 255, 255, 0.15), transparent 72%),
    conic-gradient(from 202deg at 63% 54%,
      rgba(255, 255, 255, 0.23) 0deg,
      rgba(255, 255, 255, 0.00) 68deg,
      rgba(255, 255, 255, 0.16) 138deg,
      rgba(255, 255, 255, 0.00) 212deg,
      rgba(255, 255, 255, 0.20) 292deg,
      rgba(255, 255, 255, 0.23) 360deg),
    radial-gradient(140% 115% at 12% -6%, var(--bg-lift) 0%, #2b2b31 30%, #141417 66%, #0a0a0c 100%);
  filter: blur(44px) saturate(125%);
}

/* Vignette and a faint film grain.
 *
 * Both are kept deliberately weak. A heavy vignette was the second mistake in the
 * first version: at 0.55 black over the whole ground it cancelled the lobes and put
 * the design back to flat. The job here is only to stop the eye at the viewport edge
 * and to dither the gradients so they do not band on a 6-bit panel.
 */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E"),
    radial-gradient(130% 110% at 50% 42%, transparent 58%, rgba(0, 0, 0, 0.38) 100%);
  opacity: 0.3;
  mix-blend-mode: overlay;
}

/* ------------------------------------------------------------------ layout */

/* The fixed header sits at top: 20px with a height of roughly 78px, so 132px of top
   padding clears it with room to breathe. */
.shell { position: relative; z-index: 1; padding: 132px 24px 64px; }
.wrap { max-width: 1480px; margin: 0 auto; }

/* ------------------------------------------------------------------ header */

.topbar {
  position: fixed;
  top: 20px;
  left: 50%;
  transform: translateX(-50%);
  width: calc(100% - 48px);
  max-width: 1432px;
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 13px 18px;
  background: var(--glass-raised);
  backdrop-filter: blur(var(--blur)) saturate(150%);
  -webkit-backdrop-filter: blur(var(--blur)) saturate(150%);
  border: 1px solid var(--glass-line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow), var(--glass-hi);
}

/* The wordmark replaces the earlier square logo tile. The reference has the product
   name set large and tight in the header, and a text mark is also what you want here:
   it makes the panel identifiable in a screenshot without a legend.
 *
 * The tagline that used to sit under it is gone, so the block no longer needs to stack:
 * it was a two-row column for one line of text, and the 1px gap plus the line-height on
 * the name left the header a few pixels taller than the single line it now holds. */
.wordmark {
  display: flex;
  align-items: center;
  user-select: none;
  -webkit-user-select: none;
  flex: none;
}

.wordmark .name {
  font-size: 1.28em;
  font-weight: 700;
  letter-spacing: -0.015em;
  text-transform: none;
  line-height: 1;
}

.spacer { flex: 1; }

/* ------------------------------------------------------------------ nav */

/* Section tabs. The active one is filled glass with a red underline; that is the
   reference's language — red marks where you are — translated from a vertical menu to
   a horizontal strip, where a left bar would be meaningless. */
.nav {
  display: flex;
  gap: 3px;
  margin-right: 4px;
  padding: 3px;
  background: var(--glass-sunken);
  border: 1px solid rgba(255, 255, 255, 0.05);
  border-radius: var(--r-md);
  box-shadow: var(--glass-hi-soft);
}

.nav-item {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 9px 13px;
  border: none;
  border-radius: var(--r-xs);
  background: transparent;
  color: var(--text-dim);
  font-size: 0.82em;
  font-weight: 500;
  letter-spacing: 0.2px;
  text-transform: none;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.16s ease, color 0.16s ease;
}
.nav-item svg {
  width: 15px;
  height: 15px;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
  opacity: 0.85;
}
.nav-item:hover { background: var(--glass-hover); color: var(--text); }
.nav-item.active {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.07));
  color: var(--text);
  font-weight: 600;
  box-shadow: var(--glass-hi-soft);
}
/* The red marker. A short centred rule rather than a full-width underline: at 18px it
   reads as a deliberate mark, at full width it reads as a border. */
.nav-item.active::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 3px;
  transform: translateX(-50%);
  width: 18px;
  height: 2px;
  border-radius: 2px;
  background: var(--red);
  box-shadow: 0 0 8px var(--red-glow);
}

/* One view at a time. `display: none` rather than a height animation: the views
   differ wildly in length, and animating between them looks like a glitch. */
.view { display: none; }
.view.active { display: block; }

@media (max-width: 1180px) {
  /* Labels would push the header past the edge; the icons carry the meaning. */
  .nav-item span { display: none; }
  .nav-item { padding: 10px 11px; }
}

/* ------------------------------------------------------------------ cards */

/* The pane. Same three ingredients as the topbar, one step darker so the header reads
   as the raised surface and the content as the recessed one. */
.card {
  position: relative;
  background: var(--glass);
  backdrop-filter: blur(var(--blur)) saturate(140%);
  -webkit-backdrop-filter: blur(var(--blur)) saturate(140%);
  border: 1px solid var(--glass-line);
  border-radius: var(--r-lg);
  padding: 24px;
  margin-bottom: 20px;
  box-shadow: var(--shadow), var(--glass-hi);
  transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
  border-color: var(--glass-line-strong);
  box-shadow: var(--shadow), var(--glass-hi), 0 0 0 1px rgba(255, 255, 255, 0.03);
}

/* A specular sheen across the top of every card, as if light is falling on glass.
   One pseudo-element, no extra markup, and it is the detail that separates "blurred
   box" from "pane of glass".
 *
 * `border-radius: inherit` and a plain linear fade, NOT an elliptical radius. The first
 * version reshaped the pseudo-element into a dome to suggest curvature, and the dome's
 * own edge was visible as a hard arc cutting across the top of every card — a CSS
 * artefact that looked exactly like a rendering bug. The gradient alone gives the
 * impression of curvature without drawing an edge anywhere. */
.card::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(180deg,
    rgba(255, 255, 255, 0.075) 0%,
    rgba(255, 255, 255, 0.022) 26%,
    rgba(255, 255, 255, 0.000) 46%);
  pointer-events: none;
}

.card-title {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 0.78em;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 1.5px;
  color: var(--text-dim);
  margin-bottom: 18px;
  padding-bottom: 14px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.055);
}
/* Defaults for card-title icons.
 *
 * Most of them set stroke and fill inline, but one did not — the monitor with the down
 * arrow on the install card. With no `fill` an SVG defaults to solid black, so on a dark
 * card it rendered as an invisible blob, and the card looked like it had no icon at all.
 * These defaults make an icon render whether or not the inline attributes are present.
 *
 * CSS beats a presentation attribute, so `stroke: #fff` would override the inline
 * `stroke="white"` on the other icons — which is harmless, because that is the same
 * colour they were already asking for. */
.card-title svg {
  width: 17px;
  height: 17px;
  flex: none;
  opacity: 0.9;
  fill: none;
  stroke: #fff;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}.card-title .sub {
  margin-left: auto;
  font-size: 0.92em;
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  color: var(--text-faint);
}

.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; }
@media (max-width: 1000px) { .grid-2 { grid-template-columns: 1fr; } }

/* ------------------------------------------------------------------ inputs */

/* Recessed, not raised: a field is a hole in the glass, so it gets the sunken fill and
   the inverted highlight (a dark inner top edge instead of a lit one). */
input, select, textarea {
  width: 100%;
  padding: 11px 14px;
  background: var(--glass-sunken);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: var(--r-sm);
  color: var(--text);
  font-family: inherit;
  font-size: 0.9em;
  outline: none;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.45);
  transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
}
input:focus, select:focus, textarea:focus {
  border-color: rgba(255, 255, 255, 0.42);
  background: rgba(0, 0, 0, 0.34);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.45),
              0 0 0 3px rgba(255, 255, 255, 0.07);
}
input::placeholder { color: var(--text-faint); }
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
input[type="number"] { -moz-appearance: textfield; }

select {
  appearance: none;
  cursor: pointer;
  padding-right: 32px;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23aaaaaa' stroke-width='1.6' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
}

/* ------------------------------------------------------- custom select */
/*
 * A native <select> can be styled as a closed control but its dropdown is drawn by the
 * OS, so it can never match the glass. ui.js wraps each select in `.cselect` and renders
 * a real popup; the select itself stays in the DOM as the value holder and is hidden
 * here rather than removed, because app.js reads and writes it directly.
 *
 * `position: absolute` with `opacity: 0` rather than `display: none`: a display-none
 * control cannot receive focus, and the panel's keyboard path (tab to the trigger, arrow
 * through the options) depends on the select still being a real, focusable form control
 * for screen readers even though the trigger is what the eye sees.
 */
.cselect-native {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: 0;
  border: 0;
  opacity: 0;
  pointer-events: none;
}

.cselect {
  position: relative;
  display: inline-block;
  min-width: 158px;
  font-size: 0.9em;
}

.cselect-trigger {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 11px 13px;
  background: var(--glass-sunken);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: var(--r-sm);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.45);
  color: var(--text);
  font: inherit;
  font-size: 0.9em;
  font-weight: 500;
  text-transform: none;
  letter-spacing: 0.1px;
  cursor: pointer;
  text-align: left;
  transition: border-color 0.18s ease, background 0.18s ease, box-shadow 0.18s ease;
}
.cselect-trigger:hover {
  background: rgba(0, 0, 0, 0.34);
  border-color: rgba(255, 255, 255, 0.2);
}
.cselect.open .cselect-trigger,
.cselect-trigger:focus-visible {
  border-color: rgba(255, 255, 255, 0.42);
  background: rgba(0, 0, 0, 0.4);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.45),
              0 0 0 3px rgba(255, 255, 255, 0.07);
  outline: none;
}
.cselect-trigger:disabled { opacity: 0.4; cursor: default; }

.cselect-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cselect-label.placeholder { color: var(--text-faint); }

.cselect-chev {
  width: 14px;
  height: 14px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.55;
  transition: transform 0.2s ease;
}
.cselect.open .cselect-chev { transform: rotate(180deg); }

/* The popup. Same glass as the panels, one step raised so it clearly floats above the
   card it came from. */
.cselect-pop {
  position: absolute;
  z-index: 60;
  top: calc(100% + 6px);
  left: 0;
  min-width: 100%;
  max-height: 268px;
  overflow-y: auto;
  padding: 5px;
  background: var(--glass-raised);
  backdrop-filter: blur(var(--blur)) saturate(150%);
  -webkit-backdrop-filter: blur(var(--blur)) saturate(150%);
  border: 1px solid var(--glass-line-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow), var(--glass-hi);
  animation: cselect-in 0.14s ease-out;
}

@keyframes cselect-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .cselect-pop { animation: none; }
}

.cselect-pop::-webkit-scrollbar { width: 6px; }
.cselect-pop::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.16);
  border-radius: 3px;
}

.cselect-opt {
  display: block;
  width: 100%;
  padding: 9px 11px;
  border: none;
  border-radius: var(--r-xs);
  background: transparent;
  box-shadow: none;
  color: var(--text-dim);
  font: inherit;
  font-size: 0.88em;
  font-weight: 500;
  text-transform: none;
  letter-spacing: 0.1px;
  text-align: left;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: background 0.12s ease, color 0.12s ease;
}
.cselect-opt:hover { background: rgba(255, 255, 255, 0.09); color: var(--text); }
.cselect-opt.active {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.07));
  color: #fff;
  font-weight: 600;
  box-shadow: var(--glass-hi-soft);
}
/* The picked row carries the same red mark the active nav item does, so "this is the
   one you chose" reads the same everywhere in the panel. */
.cselect-opt.active::before {
  content: "";
  display: inline-block;
  width: 3px;
  height: 12px;
  margin-right: 8px;
  border-radius: 2px;
  background: var(--red);
  box-shadow: 0 0 8px var(--red-glow);
  vertical-align: -1px;
}

/* -------------------------------------------------------- checkbox row */
/*
 * `.check` was used in four places in the markup and had no rule at all — the label and
 * its box rendered as bare inline elements, which is why the install and Telegram
 * options looked like a broken line of text rather than a control.
 *
 * The whole row is the label, so the text is part of the hit area.
 */
.check {
  display: inline-flex;
  align-items: flex-start;
  gap: 10px;
  padding: 7px 2px;
  color: var(--text-dim);
  font-size: 0.86em;
  line-height: 1.45;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  transition: color 0.15s ease;
}
.check:hover { color: var(--text); }
.check > span { padding-top: 1px; }

/* The box itself, drawn rather than native.
 *
 * `appearance: none` removes the OS widget so the glass language applies here too; the
 * tick is a data-URI stroke, so there is no font dependency and no pseudo-element
 * gymnastics. */
.check input[type="checkbox"] {
  flex: 0 0 auto;
  appearance: none;
  -webkit-appearance: none;
  width: 18px;
  height: 18px;
  margin: 0;
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: 6px;
  background: var(--glass-sunken);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.45);
  cursor: pointer;
  position: relative;
  transition: background 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease;
}
.check input[type="checkbox"]:hover { border-color: rgba(255, 255, 255, 0.4); }
.check input[type="checkbox"]:checked {
  background: linear-gradient(180deg, #ffffff, #dcdce2);
  border-color: #fff;
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.08),
              inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
.check input[type="checkbox"]:checked::after {
  content: "";
  position: absolute;
  inset: 0;
  background: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230b0b0d' stroke-width='3.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7'/%3E%3C/svg%3E") center / 13px 13px no-repeat;
}
.check input[type="checkbox"]:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.22);
}
.check input[type="checkbox"]:disabled { opacity: 0.4; cursor: default; }

/* A checkbox the panel renders itself, for the on/off switches in the System view. */
input[type="checkbox"]:not(.check input) { accent-color: #fff; }

/* ------------------------------------------------------- described option */
/*
 * A checkbox with a title and an explanation, for choices that change what the software
 * does rather than ones that merely toggle a filter. The bare `.check` row said what was
 * on but not what it changed, which is exactly the gap that made the install-mode block
 * read as a mess.
 */
.modes { display: flex; flex-direction: column; gap: 8px; }

.mode {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  /* Tightened from 14px 16px. The two source cards sit above the screen viewport, and
     every pixel they spend is a pixel of the remote desktop pushed below the fold. The
     descriptions are still fully readable; they simply wrap one line less often. */
  padding: 11px 14px;
  background: var(--glass-flat);
  border: 1px solid rgba(255, 255, 255, 0.09);
  border-radius: var(--r-md);
  box-shadow: var(--glass-hi-soft);
  cursor: pointer;
  transition: background 0.16s ease, border-color 0.16s ease;
}
.mode:hover { background: rgba(255, 255, 255, 0.06); border-color: var(--glass-line-strong); }
.mode:has(input:checked) {
  border-color: rgba(255, 255, 255, 0.26);
  background: linear-gradient(150deg, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.03));
}

.mode input[type="checkbox"] {
  flex: 0 0 auto;
  appearance: none;
  -webkit-appearance: none;
  width: 18px;
  height: 18px;
  margin: 2px 0 0;
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: 6px;
  background: var(--glass-sunken);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.45);
  cursor: pointer;
  position: relative;
}
.mode input[type="checkbox"]:checked {
  background: linear-gradient(180deg, #ffffff, #dcdce2);
  border-color: #fff;
}
.mode input[type="checkbox"]:checked::after {
  content: "";
  position: absolute;
  inset: 0;
  background: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230b0b0d' stroke-width='3.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7'/%3E%3C/svg%3E") center / 13px 13px no-repeat;
}
.mode input[type="checkbox"]:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.22);
}

.mode-body { display: flex; flex-direction: column; gap: 4px; min-width: 0; }

.mode-head {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.mode-head b { font-size: 0.92em; font-weight: 600; }

.mode-tag {
  font-style: normal;
  font-size: 0.66em;
  font-weight: 700;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: 999px;
  color: var(--warn);
  background: rgba(255, 176, 32, 0.12);
  border: 1px solid rgba(255, 176, 32, 0.35);
}

.mode-note {
  font-size: 0.8em;
  line-height: 1.5;
  color: var(--text-faint);
}

.field { margin-bottom: 14px; }
.field label {
  display: block;
  margin-bottom: 7px;
  font-size: 0.71em;
  text-transform: uppercase;
  letter-spacing: 1.2px;
  color: var(--text-faint);
  font-weight: 600;
}

/* A row of fields and the buttons that act on them.
 *
 * `align-items: flex-end`, NOT `center`, and that is the whole point of this rule.
 *
 * A `.field` is a label stacked on an input, so it is taller than the control it wraps.
 * Centring the row centres the whole `.field` box, which puts a bare button beside it
 * 11 px above the inputs it belongs to — measured, not guessed — because the label's
 * height plus its margin pushes the input down inside its own box. The button has no
 * label, so it sits at the top.
 *
 * Aligning to the end lines up the BOTTOM edges of everything in the row, which lands
 * the button exactly on the inputs whether or not the fields beside it carry labels.
 * That is the property worth having: it stays correct when one field in a row gains a
 * label and its neighbours do not.
 *
 * A field with no label is unaffected — its bottom edge is the input's bottom edge. */
.row { display: flex; gap: 10px; align-items: flex-end; }
.row.wrap { flex-wrap: wrap; }
.row + .row { margin-top: 10px; }
.row .field { margin-bottom: 0; flex: 1; }
.row .btn { flex: none; }

/* Rows that hold only controls and no fields.
 *
 * With `flex-end` a row of bare buttons and inputs would sit on their bottom edges,
 * which looks wrong when nothing is stacked: the search field in the agent filter row,
 * for instance, has no label and would drop below the buttons beside it. These get
 * centring back, because with nothing to align to, centre is what reads as level. */
.row.controls { align-items: center; }

/* ----------------------------------------------------------------- buttons */

button, .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 11px 18px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.11), rgba(255, 255, 255, 0.05));
  color: var(--text);
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: var(--r-sm);
  font-family: inherit;
  font-size: 0.82em;
  font-weight: 600;
  letter-spacing: 0.3px;
  text-transform: none;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: var(--glass-hi-soft);
  transition: background 0.18s ease, border-color 0.18s ease,
              transform 0.08s ease, box-shadow 0.18s ease;
}
button:hover:not(:disabled), .btn:hover:not(:disabled) {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.09));
  border-color: rgba(255, 255, 255, 0.3);
}
button:active:not(:disabled) { transform: scale(0.98); }
button:disabled { opacity: 0.35; cursor: default; }

button.primary, .btn.primary {
  background: linear-gradient(180deg, var(--accent), #e2e2e6);
  color: var(--accent-ink);
  border-color: rgba(255, 255, 255, 0.9);
  box-shadow: 0 6px 18px -6px rgba(255, 255, 255, 0.45),
              inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
button.primary:hover:not(:disabled) {
  background: linear-gradient(180deg, #ffffff, #f2f2f5);
  border-color: #fff;
}

button.danger {
  background: linear-gradient(180deg, rgba(255, 93, 115, 0.24), rgba(255, 93, 115, 0.12));
  border-color: rgba(255, 93, 115, 0.45);
  color: #ffb3bf;
}
button.danger:hover:not(:disabled) {
  background: linear-gradient(180deg, rgba(255, 93, 115, 0.34), rgba(255, 93, 115, 0.18));
  border-color: rgba(255, 93, 115, 0.6);
}

button.ghost { background: transparent; border-color: transparent; box-shadow: none; }
button.ghost:hover:not(:disabled) { background: var(--glass-hover); border-color: var(--glass-line); }

button.sm { padding: 7px 12px; font-size: 0.75em; }
button.wide { width: 100%; }

/* The account chip, borrowed from the reference's footer row: avatar, name, subtitle,
   chevron. It replaces the bare "log out" button, and it carries more than the button
   did — the panel now says who is signed in. */
.account {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 6px 12px 6px 6px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--glass-line);
  border-radius: 999px;
  box-shadow: var(--glass-hi-soft);
  cursor: pointer;
}
.account:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.10);
  border-color: var(--glass-line-strong);
}
.account .avatar {
  width: 30px;
  height: 30px;
  flex: none;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.8em;
  font-weight: 700;
  letter-spacing: 0.5px;
  color: #fff;
  background: linear-gradient(160deg, rgba(255, 255, 255, 0.24), rgba(255, 255, 255, 0.06));
  border: 1px solid rgba(255, 255, 255, 0.35);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
.account .account-meta {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  line-height: 1.2;
  min-width: 0;
}
.account .account-meta b { font-size: 0.82em; font-weight: 600; }
.account .account-meta span { font-size: 0.68em; color: var(--text-faint); }
.account .chev {
  width: 14px;
  height: 14px;
  flex: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
  opacity: 0.55;
}

/* ------------------------------------------------------------------- tabs */

.tabs {
  display: flex;
  gap: 4px;
  padding: 4px;
  background: var(--glass-sunken);
  border: 1px solid rgba(255, 255, 255, 0.05);
  border-radius: var(--r-md);
  box-shadow: var(--glass-hi-soft);
  width: fit-content;
}
.tab {
  border: none;
  background: transparent;
  color: var(--text-dim);
  padding: 8px 16px;
  border-radius: var(--r-xs);
  font-size: 0.78em;
  text-transform: none;
  letter-spacing: 0.2px;
  box-shadow: none;
}
.tab:hover { background: var(--glass-hover); }
.tab.active {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.08));
  color: #fff;
  font-weight: 600;
  box-shadow: var(--glass-hi-soft);
}

/* ------------------------------------------------------------------ tiles */

.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); gap: 12px; }
.tile {
  position: relative;
  padding: 16px 18px;
  background: var(--glass-flat);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: var(--r-md);
  box-shadow: var(--glass-hi-soft);
  overflow: hidden;
}
/* A tinted blob in the corner, keyed to the tile's state. Subtle enough to be felt
   rather than read, which is what keeps a wall of tiles from becoming a traffic light. */
.tile::after {
  content: "";
  position: absolute;
  right: -30px;
  top: -30px;
  width: 90px;
  height: 90px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.06);
  filter: blur(18px);
  pointer-events: none;
}
.tile-value {
  font-size: 1.95em;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
.tile-label {
  margin-top: 5px;
  font-size: 0.69em;
  text-transform: uppercase;
  letter-spacing: 0.9px;
  color: var(--text-faint);
  font-weight: 600;
}
.tile.ok .tile-value { color: var(--ok); }
.tile.ok::after { background: rgba(57, 217, 138, 0.22); }
.tile.warn .tile-value { color: var(--warn); }
.tile.warn::after { background: rgba(255, 176, 32, 0.22); }
.tile.danger .tile-value { color: var(--danger); }
.tile.danger::after { background: rgba(255, 93, 115, 0.24); }

/* ------------------------------------------------------------------ chart */

.chart { margin-top: 20px; }
#chart { width: 100%; height: 130px; display: block; }
.chart-label { margin-top: 6px; font-size: 0.72em; color: var(--text-faint); letter-spacing: 0.4px; }

/* ------------------------------------------------------------------ table */

.thead, .agent-row {
  display: grid;
  grid-template-columns: 92px 1.3fr 1fr 1.3fr 1fr 84px 140px;
  gap: 12px;
  align-items: center;
  padding: 13px 10px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.055);
}
.thead {
  font-size: 0.68em;
  text-transform: uppercase;
  letter-spacing: 1.2px;
  color: var(--text-faint);
  font-weight: 700;
}
.agent-row {
  font-size: 0.88em;
  border-radius: var(--r-sm);
  transition: background 0.15s ease;
}
.agent-row:last-child { border-bottom: none; }
.agent-row:hover { background: rgba(255, 255, 255, 0.045); }
.agent-row > * { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row-actions { display: flex; justify-content: flex-end; gap: 6px; }

/* ------------------------------------------------------------------ pills */

.pill {
  display: inline-block;
  padding: 4px 11px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.07);
  color: var(--text-dim);
  font-size: 0.71em;
  font-weight: 600;
  letter-spacing: 0.4px;
  box-shadow: var(--glass-hi-soft);
  white-space: nowrap;
}
.pill.online {
  background: rgba(57, 217, 138, 0.16);
  border-color: rgba(57, 217, 138, 0.38);
  color: var(--ok);
}
.pill.offline {
  background: rgba(255, 93, 115, 0.16);
  border-color: rgba(255, 93, 115, 0.38);
  color: var(--danger);
}
.pill.removed { background: rgba(255, 255, 255, 0.06); color: var(--text-faint); }

.badges { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.badge {
  padding: 3px 10px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--glass-line);
  font-size: 0.68em;
  font-weight: 700;
  letter-spacing: 0.5px;
  color: var(--text-dim);
  text-transform: uppercase;
  white-space: nowrap;
}
.badge.ok { color: var(--ok); border-color: rgba(57, 217, 138, 0.4); background: rgba(57, 217, 138, 0.1); }
.badge.warn { color: var(--warn); border-color: rgba(255, 176, 32, 0.4); background: rgba(255, 176, 32, 0.1); }
.badge.danger { color: var(--danger); border-color: rgba(255, 93, 115, 0.4); background: rgba(255, 93, 115, 0.1); }

/* ------------------------------------------------------------------ lists */

.list { display: flex; flex-direction: column; gap: 2px; }
.list.scroll { max-height: 280px; overflow-y: auto; padding-right: 4px; }
.list.scroll::-webkit-scrollbar { width: 6px; }
.list.scroll::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.14);
  border-radius: 3px;
}
.list.scroll::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.24); }

/* Rows. The reference's menu rows are tall and airy with a big hit area, so the
   minimum height goes up from 34px and the radius grows with it: a 52px row with an
   8px radius looks like a mistake, and a 34px row with a 12px radius looks like a
   button. The two have to move together. */
.item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: var(--r-sm);
  font-size: 0.86em;
  min-height: 44px;
  transition: background 0.14s ease;
}
.item.click { cursor: pointer; }
.item.click:hover { background: rgba(255, 255, 255, 0.05); }
.item .grow { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Inline SVG icons.
 *
 * Every one is a stroked outline on currentColor, so an icon inherits the colour of
 * whatever row it sits in and needs no per-state rule. Emoji were used here first
 * and were wrong for three reasons: they render differently on every platform, they
 * carry their own colour and ignore the palette, and they cannot be sized against
 * the text baseline reliably. */
.icon {
  width: 16px;
  height: 16px;
  flex: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
  opacity: 0.7;
}
.icon.dir { opacity: 0.9; }

/* The refresh arrow's head is a filled triangle while its arc is stroked.
 *
 * The .icon rule sets fill: none for every icon, which is right for the stroked outlines
 * everything else uses and wrong for this one -- an unfilled arrowhead disappears and
 * leaves a bare arc, which is the "broken circle" the redesign exists to avoid. The
 * second path is the head, selected by position rather than by adding a class inside the
 * svg: the ICON builder emits bare <path> elements and giving it a per-path attribute
 * would mean changing the shared helper for one icon. */
.icon-refresh path:nth-child(2) {
  fill: currentColor;
  stroke: none;
}

/* A row's leading icon and its label are one unit; the name gets the remaining
   space so long paths truncate instead of pushing the controls off the row. */
.item .label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  flex: none;
  border-radius: var(--r-xs);
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-dim);
  cursor: pointer;
  box-shadow: none;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.icon-btn:hover {
  background: rgba(255, 255, 255, 0.09);
  border-color: var(--glass-line);
  color: var(--text);
}
.icon-btn.danger:hover {
  background: rgba(255, 93, 115, 0.18);
  border-color: rgba(255, 93, 115, 0.45);
  color: #ffb3bf;
}
.icon-btn svg {
  width: 14px;
  height: 14px;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
}

.empty { padding: 32px 0; text-align: center; color: var(--text-faint); font-size: 0.85em; }

/* A key/value report, used for the selected machine's sysinfo.
 *
 * Two columns rather than a definition list: the values include long unbroken strings
 * (a processor model, an image path) and the label column has to stay narrow and fixed
 * so the values do not jitter between rows. */
.kv {
  display: grid;
  grid-template-columns: minmax(130px, 168px) 1fr;
  gap: 1px 16px;
  margin-top: 14px;
  padding: 4px 0;
}
.kv:empty { display: none; }

.kv-key {
  padding: 9px 0;
  font-size: 0.72em;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.9px;
  color: var(--text-faint);
  border-top: 1px solid rgba(255, 255, 255, 0.05);
}

.kv-val {
  padding: 7px 0;
  font-size: 0.88em;
  color: var(--text);
  border-top: 1px solid rgba(255, 255, 255, 0.05);
  overflow-wrap: anywhere;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 8px;
}

@media (max-width: 700px) {
  .kv { grid-template-columns: 1fr; gap: 0; }
  .kv-key { padding-bottom: 0; border-top: 1px solid rgba(255, 255, 255, 0.05); }
  .kv-val { padding-top: 2px; border-top: none; }
}

.muted { color: var(--text-dim); }
.faint { color: var(--text-faint); }

/* A quieter size for the explanatory lines that sit under a field or a control.
   `.small` was used in seventeen places in the markup and had no rule at all, so every
   one of those lines rendered at full body size. */
.small { font-size: 0.78em; line-height: 1.5; }

.mono { font-family: var(--mono); }
.nowrap { white-space: nowrap; }

/* ----------------------------------------------------------------- notify */

.notice {
  padding: 13px 15px;
  border-radius: var(--r-sm);
  border: 1px solid var(--glass-line);
  background: rgba(255, 255, 255, 0.045);
  box-shadow: var(--glass-hi-soft);
  font-size: 0.85em;
  /* Air above, always.
   *
   * .notice carried no vertical margin at all, and there is a rule for .row + .row but
   * none for .row + .notice. So a notice that followed a row of buttons sat flush
   * against it -- the two read as one block, and the notice looked like a caption on
   * the buttons rather than a separate statement about the machine. That is exactly the
   * report from the Virtual Machines tab, where "VirtualBox is not installed" was glued
   * under the refresh and create buttons.
   *
   * Expressed on the element rather than as a selector list of preceding siblings,
   * because the set of things that can precede a notice is open: a row, a list, a
   * progress bar, a table, a form. One rule covers all of them.
   *
   * The two exclusions matter. :first-child is for a notice at the top of a tab, which
   * needs no leading space. h4 + .notice is because a heading already carries a 12px
   * bottom margin; without the exclusion the gap doubles to 24px and the heading
   * detaches from the text it introduces.
   *
   * An inline margin-top on the element still wins, so the several places that set one
   * by hand are unaffected. */
  margin-top: 12px;
}

.notice:first-child { margin-top: 0; }
h4 + .notice { margin-top: 0; }
.notice.warn { border-color: rgba(255, 176, 32, 0.42); color: var(--warn); background: rgba(255, 176, 32, 0.07); }
.notice.ok { border-color: rgba(57, 217, 138, 0.42); color: var(--ok); background: rgba(57, 217, 138, 0.07); }
.notice.danger { border-color: rgba(255, 93, 115, 0.42); color: var(--danger); background: rgba(255, 93, 115, 0.07); }

/* -------------------------------------------------------------- viewports */

.viewport {
  margin: 12px 0;
  border: 1px solid var(--glass-line-strong);
  border-radius: var(--r-md);
  /* NO SCROLLBAR.
   *
   * This was `overflow: auto`, on the reasoning that a picture too tall for the box stays
   * reachable through a scrollbar. In practice the bar was the reported fault rather than
   * the cure: it appeared because the picture was sized from the width while the box was
   * bounded by height, so there was always something to scroll to -- and an operator who
   * sees a remote desktop cut off at the fold reads that as the agent sending a truncated
   * frame, not as a scrollable box.
   *
   * The picture is now sized to fit inside the box on both axes (see the rule below), so
   * there is nothing to scroll to and the bar would only steal width. `hidden` also stops
   * the half-pixel overflow that a fractional scale occasionally leaves, which used to put
   * a one-pixel scrollbar on an otherwise complete picture. */
  overflow: hidden;
  /* Deliberately opaque black, not glass. This is a remote desktop: anything showing
     through it would be mistaken for part of the remote screen, and the operator maps
     clicks against what they see. */
  background: #000;
  position: relative;
  min-height: 60px;
  max-height: 84vh;
  box-shadow: var(--shadow-sm);
}

/* THE WHOLE DESKTOP IS SHOWN, NOT A CROPPED PART OF IT.
 *
 * WHY THE PREVIOUS RULE DID NOT DO THAT.
 *
 * It bounded the width only -- `max-width: 100%` with `width: auto; height: auto` -- on the
 * argument that the height then follows in proportion and nothing can overflow. That is true
 * only while the width is the binding constraint. The parent carries `max-height: 84vh`, and
 * a wide remote desktop (3440x1440, 2560x1080, or any 21:9 panel) in a tall-but-narrow panel
 * window is taller than that: the picture keeps its natural height, the parent clips or
 * scrolls, and the bottom of the remote screen is the part that goes.
 *
 * So BOTH axes are bounded now, and the picture is scaled by the tighter of the two. The
 * height bound is expressed against the viewport, minus the fixed chrome above and below the
 * viewport (page header, toolbar, capture hint), which is why it is a calc and not a bare vh.
 *
 * `object-fit: contain` is what makes the two bounds cooperate instead of fighting. The
 * element is sized to the picture's own box, and the picture is letterboxed inside it with
 * its aspect ratio intact -- so no pixel is ever outside the element, and the pointer mapping
 * in contentRect() keeps working, because the drawn rectangle is a pure scale of the natural
 * one.
 *
 * The selector is a CHILD combinator. `.viewport img` also matched the pictures inside the
 * every-display grid, and since showing that view hides this one, the rule
 * `.viewport.show-all img { display: none }` hid those as well -- which is why that view
 * appeared empty. `> img` matches only the single picture this rule is about. */
.viewport > img {
  display: block;
  margin: 0 auto;
  max-width: 100%;
  /* THE PICTURE GETS THE WHOLE VERTICAL BUDGET THE BOX ALLOWS.
   *
   * This read `calc(84vh - 220px)` first, on the reasoning that the page chrome above the
   * viewport should be subtracted so the whole picture lands inside the window. Measured
   * in the real panel, that produced a 992x620 picture inside a 1430x622 box: the height
   * bound was binding, so a third of the available width went unused and the remote
   * desktop was rendered 31% smaller than the panel could show it.
   *
   * A remote desktop is read, not admired. Legibility beats seeing the entire frame at
   * once, and the operator can scroll -- which they had to do anyway, because the chrome
   * above the viewport is taller than the window. So the bound is now the box's own
   * 84vh, less the 2px border and a little slack, which renders the same frame at
   * 1331x832.
   *
   * `scrollIntoView` on start (see revealScreen in app.js) is what covers the case this
   * gives up: the operator does not have to find the picture after pressing start. */
  max-height: calc(84vh - 8px);
  width: auto;
  height: auto;
  /* Keeps the aspect ratio when both bounds are live, instead of squashing the image to
     fill a box that is not its shape. */
  object-fit: contain;
  outline: none;
  /* The slide on a display change reads as paging through screens. Short enough not to
     delay a click: the frame behind it is already the new one. */
  transition: transform 0.2s ease, opacity 0.2s ease;
}
.viewport > img:focus { box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.4); }

/* Paging. The old picture leaves towards the side the new one comes from, which is what
   makes "next" and "previous" feel like directions rather than two names for "switch". */
.viewport.switch-next > img { transform: translateX(-7%); opacity: 0; }
.viewport.switch-prev > img { transform: translateX(7%); opacity: 0; }

/* The every-display view is the agent capturing all monitors as ONE image, exactly as the
   stream did before any of this -- `monitor=-1`. There is no separate grid of pictures:
   that was a second way to do the same thing, and it is the reason the view looked empty.
   Nothing extra is needed here; the single picture rule above covers it. */

.screen-arrow {
  position: absolute;
  top: 50%;
  /* The translate is part of the resting transform, so hovering or pressing cannot move the
     button. An earlier version animated transform on hover and the arrow visibly jumped. */
  transform: translateY(-50%);
  width: 40px;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--glass-line-strong);
  border-radius: var(--r-sm);
  background: rgba(10, 12, 16, 0.62);
  color: #fff;
  cursor: pointer;
  padding: 0;
  /* Only opacity and background animate. Nothing that can shift the button's position. */
  transition: opacity 0.16s ease, background 0.16s ease;
  opacity: 0;
}
.screen-arrow svg { width: 20px; height: 20px; stroke: currentColor; fill: none; stroke-width: 2; }
.viewport:hover .screen-arrow.is-shown,
.screen-arrow.is-shown:focus-visible { opacity: 1; }
.screen-arrow:hover { background: rgba(30, 36, 46, 0.85); }
.screen-arrow:active { background: rgba(50, 60, 76, 0.9); }
.screen-arrow.prev { left: 10px; }
.screen-arrow.next { right: 10px; }

/* AN ARROW EXISTS ONLY WHEN THERE IS SOMEWHERE TO GO.
 *
 * At the first display there is no previous, at the last there is no next, and with one
 * display there is neither. Hiding is by `hidden` rather than by opacity so the button is
 * also removed from the keyboard order: an invisible-but-focusable control that does
 * nothing is worse than no control. */
.screen-arrow[hidden] { display: none; }

.screen-label {
  position: absolute;
  bottom: 8px;
  left: 50%;
  transform: translateX(-50%);
  padding: 3px 10px;
  border-radius: 999px;
  background: rgba(10, 12, 16, 0.66);
  color: #e8ecf2;
  font-size: 12px;
  line-height: 1.5;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.16s ease;
}
.viewport:hover .screen-label { opacity: 1; }

/* Every display at a glance. */
.screen-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 12px 0;
}
/* The strip and the button row below it are separate flex containers, so the gap has to be
   on both or the button sits flush against the last tile. */
.screen-strip + .row { gap: 10px; margin-top: 12px; }
.screen-tile {
  flex: 0 0 auto;
  min-width: 108px;
  padding: 9px 12px;
  border: 1px solid var(--glass-line-strong);
  border-radius: var(--r-sm);
  background: rgba(255, 255, 255, 0.03);
  color: var(--text-dim, #aab);
  cursor: pointer;
  text-align: left;
  line-height: 1.35;
}
.screen-tile:hover { background: rgba(255, 255, 255, 0.07); }
/* The current one is marked, because "which am I looking at" is the question the strip
   exists to answer. */
.screen-tile.active {
  border-color: var(--accent, #4c8dff);
  color: #fff;
  background: rgba(76, 141, 255, 0.14);
}
.screen-tile b { display: block; font-size: 13px; font-weight: 600; }
.screen-tile span { display: block; font-size: 11px; opacity: 0.75; }
.screen-tile .primary-mark {
  display: inline-block;
  margin-left: 6px;
  padding: 0 5px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.14);
  font-size: 10px;
}

/* Overlay shown while a capture is in flight.
 *
 * A snapshot can take several seconds: the first one after the camera has been closed
 * has to reopen the device and wait for the sensor. Without this the panel showed
 * nothing at all for that time and the button looked broken. */
.capture-loading {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  background: rgba(10, 10, 12, 0.72);
  backdrop-filter: blur(var(--blur-sm));
  -webkit-backdrop-filter: blur(var(--blur-sm));
  z-index: 2;
}

.capture-loading-text {
  font-size: 0.82em;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

/* A ring that spins. Pure CSS: no image to load and nothing to fail. */
.spinner {
  width: 30px;
  height: 30px;
  border: 2px solid rgba(255, 255, 255, 0.14);
  border-top-color: rgba(255, 255, 255, 0.9);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* Accessibility: a rotating ring is decoration for anyone who asked for less motion. */
@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; border-top-color: rgba(255, 255, 255, 0.5); }
}

.console {
  padding: 15px;
  background: rgba(0, 0, 0, 0.42);
  border: 1px solid rgba(255, 255, 255, 0.07);
  border-radius: var(--r-md);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5);
  font-family: var(--mono);
  font-size: 0.79em;
  line-height: 1.65;
  max-height: 300px;
  overflow-y: auto;
  white-space: pre-wrap;
  word-break: break-word;
}
.console::-webkit-scrollbar { width: 6px; }
.console::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.14); border-radius: 3px; }

/* ------------------------------------------------------------ cloudflare */

/* Numbered setup steps. A wall of instructions gets skimmed; numbered steps with a
   visible marker get followed. */
.cf-step {
  display: flex;
  gap: 12px;
  padding: 13px 0;
  border-top: 1px solid rgba(255, 255, 255, 0.055);
}
.cf-step:first-of-type { border-top: none; }

.cf-num {
  flex: none;
  width: 23px;
  height: 23px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.09);
  border: 1px solid var(--glass-line);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.72em;
  font-weight: 700;
  color: var(--text-dim);
  box-shadow: var(--glass-hi-soft);
}

.cf-table {
  margin-top: 8px;
  border-collapse: collapse;
  font-size: 0.85em;
  width: 100%;
  max-width: 460px;
}
.cf-table td {
  padding: 4px 10px 4px 0;
  vertical-align: top;
}
.cf-table td:first-child {
  color: var(--text-faint);
  white-space: nowrap;
  width: 130px;
}
.cf-table tr + tr td { border-top: 1px solid rgba(255, 255, 255, 0.04); }

/* Domain row: the primary marker has to be readable at a glance, because the whole
   point of the flag is knowing which address the fleet prefers. */
.domain-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 13px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: var(--r-sm);
  background: rgba(0, 0, 0, 0.22);
  box-shadow: var(--glass-hi-soft);
}
.domain-row.primary {
  border-color: rgba(57, 217, 138, 0.42);
  background: rgba(57, 217, 138, 0.07);
}
.domain-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.domain-note { font-size: 0.75em; color: var(--text-dim); margin-top: 2px; }
.domain-check { font-size: 0.72em; margin-top: 3px; }
.domain-check.ok { color: var(--ok); }
.domain-check.bad { color: var(--danger); }

/* The nameserver answer.
 *
 * `white-space: normal` is not decoration: `.domain-name` above sets `nowrap`, which is
 * right for a domain and wrong for a list of nameservers -- without this the NS line
 * runs off the card and the "should be" line beside it is clipped by the overflow
 * rule on the parent. The value is inherited, so it has to be turned back off here. */
.domain-ns {
  font-size: 0.72em;
  margin-top: 3px;
  white-space: normal;
  word-break: break-word;
  line-height: 1.35;
}
.domain-ns.ok { color: var(--ok); }
.domain-ns.bad { color: var(--danger); }

/* ------------------------------------------------------------- breadcrumb */

/* A path bar rather than a single text field. A deep path is the normal case for an
   administrator, and being able to step back one level at a time is what makes a
   filesystem browser usable at all. */
.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2px;
  margin-top: 12px;
  padding: 8px 10px;
  background: var(--glass-sunken);
  border: 1px solid rgba(255, 255, 255, 0.07);
  border-radius: var(--r-sm);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.45);
  font-size: 0.82em;
  min-height: 40px;
}

.crumb {
  padding: 4px 8px;
  border-radius: 7px;
  cursor: pointer;
  color: var(--text-dim);
  white-space: nowrap;
  transition: background 0.15s ease, color 0.15s ease;
}
.crumb:hover { background: rgba(255, 255, 255, 0.09); color: var(--text); }
.crumb.current { color: var(--text); font-weight: 600; cursor: default; }
.crumb.current:hover { background: transparent; }

.crumb-sep { color: var(--text-faint); padding: 0 1px; user-select: none; }

/* Drive tile: the browser opens on these, so they carry more information than a
   list row — letter, label and free space. */
.drive {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: var(--r-md);
  background: var(--glass-flat);
  box-shadow: var(--glass-hi-soft);
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease, transform 0.1s ease;
}
.drive:hover {
  border-color: rgba(255, 255, 255, 0.26);
  background: rgba(255, 255, 255, 0.06);
  transform: translateY(-1px);
}
.drive.off { opacity: 0.4; cursor: default; }
.drive.off:hover { border-color: rgba(255, 255, 255, 0.08); background: var(--glass-flat); transform: none; }

.drive-letter {
  font-size: 1.3em;
  font-weight: 700;
  min-width: 44px;
  letter-spacing: 0.3px;
}
.drive-info { flex: 1; min-width: 0; }
.drive-label {
  font-size: 0.86em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.drive-meta { font-size: 0.72em; color: var(--text-faint); margin-top: 2px; }

/* Free-space bar. Reads at a glance, which matters when scanning a machine that is
   nearly full. */
.drive-bar {
  height: 4px;
  margin-top: 7px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.09);
  overflow: hidden;
}
.drive-fill { height: 100%; background: #fff; border-radius: 2px; transition: width 0.3s ease; }
.drive-fill.low { background: var(--warn); }
.drive-fill.critical { background: var(--danger); }

/* Pointer capture.
 *
 * While the pointer is locked the local cursor is hidden by the browser, so a
 * border is the only cue that the mode is active — without it the operator cannot
 * tell whether their movement is going to the remote machine. */
body.locked .remote-surface {
  outline: 2px solid var(--ok);
  outline-offset: -2px;
}

.capture-hint {
  font-size: 0.78em;
  color: var(--ok);
  min-height: 1.1em;
  margin-top: 8px;
}

.capture-hint:empty { min-height: 0; }

/* The picture is the control surface, so it takes the crosshair and refuses text
   selection, which would otherwise start a drag whenever the operator sweeps
   across it. */
.remote-surface {
  cursor: crosshair;
  user-select: none;
  -webkit-user-drag: none;
}

/* --------------------------------------------------------- system section */

/* The selected machine's tab.
 *
 * Styled like the other tabs, with one addition: a dot that reflects the machine's
 * state. The name is the hostname, so the tab answers "which machine am I looking at"
 * without the operator having to remember what they clicked. */
#navClient { position: relative; }

.nav-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--text-faint);
  margin-left: 2px;
  flex: 0 0 auto;
  transition: background 0.2s ease, box-shadow 0.2s ease;
}
.nav-dot.online {
  background: var(--ok);
  box-shadow: 0 0 8px rgba(57, 217, 138, 0.8);
}
.nav-dot.offline { background: var(--danger); }

/* An unread marker, in the reference's red. Used where a count or a new event needs to
   pull the eye without a number beside it. */
.dot-red {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--red);
  box-shadow: 0 0 9px var(--red-glow);
  flex: 0 0 auto;
}

/* The hostname can be long, and the tab must not grow without limit. */
#navClientName {
  max-width: 150px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Icon inside a sub-tab. Sized in em so it follows the label. */
.subtab-icon {
  width: 1.05em;
  height: 1.05em;
  vertical-align: -0.15em;
  margin-right: 5px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* A second row of sub-tabs inside the System tab. Indented and quieter than the
 * outer row, so the two levels are not confused for each other. */
.subtabs.nested {
  margin-left: 0;
  margin-top: -6px;
  background: rgba(0, 0, 0, 0.2);
}
.subtabs.nested .subtab { font-size: 0.75em; padding: 6px 10px; }

/* The output console, outside the tab strip.
 *
 * Separated by a rule rather than a card: it is part of the same page as the tabs
 * above it, not a separate panel, and it should read as the bottom of the page. */
.output-block {
  margin-top: 20px;
  padding-top: 14px;
  border-top: 1px solid rgba(255, 255, 255, 0.055);
}
.output-block .console {
  margin-top: 8px;
  max-height: 260px;
  overflow-y: auto;
}

/* A narrow field, for the small numeric inputs that sit in a row of buttons. */
.field.compact { max-width: 130px; }
.field.compact label { font-size: 0.7em; }

/* Virtual machine card. */
.vm-card {
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: var(--r-md);
  padding: 13px 15px;
  margin-bottom: 10px;
  background: var(--glass-flat);
  box-shadow: var(--glass-hi-soft);
}
.vm-head {
  display: flex;
  /* flex-start, not center. The name block is two lines tall (name plus meta) and a
     centred pill floated in the middle of it, visually detached from the name it
     describes. A status marker belongs level with the first line. */
  align-items: flex-start;
  gap: 10px;
  flex-wrap: wrap;
}
.vm-name { font-weight: 600; flex: 1; min-width: 0; }
.vm-name > div:first-child {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.vm-meta {
  font-size: 0.75em;
  color: var(--text-faint);
  font-family: var(--mono);
  margin-top: 2px;
}
.vm-actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }

/* The state pill.
 *
 * A flex item in .vm-head, and it was overlaying the name beside it. The cause is the
 * combination of uppercase text at 0.68em with a fixed vertical padding and no line
 * height: the line box the pill occupies is shorter than the glyphs plus padding, and
 * because the item is `flex: 0 0 auto` with no height it took its content height while
 * the neighbouring .vm-name grew to its own two lines. Under align-items: center the
 * two then sat at different centres and the pill's background painted across the name.
 *
 * line-height plus align-self is what fixes it: an explicit line box makes the pill's
 * height predictable, and align-self keeps it at the top of the row where a status
 * marker belongs rather than floating in the vertical middle of a two-line name. */
.vm-state {
  font-size: 0.68em;
  line-height: 1.5;
  padding: 3px 9px;
  border-radius: 999px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  flex: 0 0 auto;
  align-self: flex-start;
  font-weight: 700;
  border: 1px solid transparent;
  white-space: nowrap;
}
.vm-state.running {
  background: rgba(57, 217, 138, 0.16);
  border-color: rgba(57, 217, 138, 0.38);
  color: var(--ok);
}
.vm-state.stopped {
  background: rgba(255, 255, 255, 0.07);
  border-color: rgba(255, 255, 255, 0.12);
  color: var(--text-dim);
}
.vm-state.saved {
  background: rgba(255, 176, 32, 0.16);
  border-color: rgba(255, 176, 32, 0.38);
  color: var(--warn);
}

/* Sub-tabs inside the System view. */
.subtabs {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
  padding: 4px;
  background: var(--glass-sunken);
  border: 1px solid rgba(255, 255, 255, 0.07);
  border-radius: var(--r-md);
  box-shadow: var(--glass-hi-soft);
  margin-bottom: 16px;
}

.subtab {
  flex: 1 1 auto;
  padding: 8px 12px;
  border: none;
  border-radius: var(--r-xs);
  background: transparent;
  color: var(--text-dim);
  font-size: 0.78em;
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: none;
  transition: background 0.15s ease, color 0.15s ease;
}
.subtab:hover { background: var(--glass-hover); color: var(--text); }
.subtab.active {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.08));
  color: #fff;
  font-weight: 600;
  box-shadow: var(--glass-hi-soft);
}

.sysview { display: none; }
.sysview.active { display: block; }

/* Gauges: three across on a wide screen, stacked on a narrow one. */
.gauges {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px;
  margin-top: 14px;
}

.gauge {
  padding: 15px;
  background: var(--glass-flat);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: var(--r-md);
  box-shadow: var(--glass-hi-soft);
}

.gauge-value {
  font-size: 1.75em;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1.05;
  letter-spacing: -0.02em;
}

.gauge-label {
  font-size: 0.69em;
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 0.8px;
  margin-top: 3px;
  font-weight: 600;
}

.gauge canvas {
  display: block;
  width: 100%;
  height: 40px;
  margin-top: 8px;
}

/* Registry value row. Columns rather than a flowing line, so a long list stays
   scannable. */
.reg-row {
  display: grid;
  grid-template-columns: minmax(140px, 1.2fr) 110px minmax(160px, 2fr) auto;
  gap: 10px;
  align-items: center;
  padding: 8px 10px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
  border-radius: 7px;
  font-size: 0.84em;
}
.reg-row:hover { background: rgba(255, 255, 255, 0.04); }
.reg-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.reg-type { color: var(--text-faint); font-size: 0.85em; }
.reg-value {
  font-family: var(--mono);
  font-size: 0.9em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Startup entry: a toggle, the name, and where it was found. */
.startup-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 11px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
  border-radius: 7px;
}
.startup-row:hover { background: rgba(255, 255, 255, 0.04); }

/* min-width: 0 is required, not decorative.
 *
 * A flex item defaults to min-width: auto, which refuses to shrink below its content.
 * The adapter name is long and unbroken, so the item kept its intrinsic width, pushed
 * the trailing label out of the card, and the text wrapped one letter per line down
 * the right edge. Allowing the item to shrink lets the ellipsis below take effect. */
.startup-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
}
.startup-name > div:first-child {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.startup-cmd {
  font-size: 0.74em;
  color: var(--text-faint);
  font-family: var(--mono);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin-top: 2px;
}

/* The trailing location label is the first thing to give way.
 *
 * flex-shrink must be 0 here: with a shrink factor the label was squeezed to nothing
 * and its text vanished, because the name beside it is `flex: 1 1 auto` and claims the
 * free space first. Fixed basis plus the ellipsis keeps "HKLM\Run" readable at every
 * width, and the row still fits because the name shrinks instead. */
.startup-row > span.faint {
  flex: 0 0 auto;
  max-width: 22%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: right;
}

/* The checkbox must never be compressed. A flex item with a default basis of auto can
   be shrunk by its siblings, and a checkbox squeezed to zero width renders as a
   centred stub with no visible box. */
.startup-row input[type="checkbox"] {
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  margin: 0;
  accent-color: #fff;
  cursor: pointer;
}

/* A toggle that cannot be used has to LOOK like one.
 *
 * Services and scheduler tasks are listed for completeness but are not switchable from
 * here -- the element carries `disabled` and a title explaining where to change it
 * instead. There was no rule for the disabled state, so a dead checkbox rendered
 * exactly like a live one: same brightness, same cursor, same accent on hover. The
 * operator clicks it, nothing happens, and the panel looks broken rather than
 * deliberate.
 *
 * Reduced opacity plus `not-allowed` is the affordance that says "shown, not editable"
 * before the click rather than after it. The title on the element carries the reason.
 */
.startup-row input[type="checkbox"]:disabled {
  opacity: 0.28;
  cursor: not-allowed;
}

/* The row dims with its toggle, so the two read as one unit. Without this a struck-out
   name sat beside a full-brightness box and the pair looked like a rendering fault. */
.startup-row:has(input[type="checkbox"]:disabled) .startup-name > div:first-child {
  opacity: 0.55;
}

.startup-off .startup-name > div:first-child { opacity: 0.5; text-decoration: line-through; }

/* A device with a problem code gets a marker; the row stays plain so a long list is
   not a wall of colour. */
.dev-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 11px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
  font-size: 0.85em;
}
.dev-row:hover { background: rgba(255, 255, 255, 0.04); }
/* Same reason as .startup-name: without min-width the long device name refuses to
   shrink and pushes the class and status markers out of view. */
.dev-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dev-class { color: var(--text-faint); font-size: 0.86em; white-space: nowrap; flex: 0 0 auto; }
.dev-problem { color: var(--danger); font-size: 0.82em; flex: 0 0 auto; white-space: nowrap; }
.dev-ok { color: var(--ok); font-size: 0.82em; flex: 0 0 auto; }

/* The network adapter row carries a second line of detail, so it needs the same
   shrink permission. */
.item > .grow { min-width: 0; overflow: hidden; }

/* --------------------------------------------------------------- fullscreen */

/* Fullscreen remote view.
 *
 * `position: fixed` over the viewport, with the image letterboxed: the remote
 * desktop's aspect ratio rarely matches the local screen, and stretching it would
 * misalign the pointer mapping the input events depend on. `object-fit: contain`
 * keeps every pixel where the agent will map a click.
 *
 * The control bar is an overlay rather than a row above the image, so the picture
 * gets the whole screen while Stop stays reachable without leaving the view.
 */
.fs-view {
  position: fixed;
  inset: 0;
  z-index: 80;
  background: #000;
  display: flex;
  align-items: center;
  justify-content: center;
}

.fs-view img {
  max-width: 100vw;
  max-height: 100vh;
  width: auto;
  height: auto;
  object-fit: contain;
  display: block;
  outline: none;
  cursor: crosshair;
}

/* The bar sits at 25% opacity until the pointer comes near, so it does not cover
   the remote taskbar. */
.fs-bar {
  position: fixed;
  left: 50%;
  bottom: 16px;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  background: var(--glass-raised);
  backdrop-filter: blur(var(--blur)) saturate(150%);
  -webkit-backdrop-filter: blur(var(--blur)) saturate(150%);
  border: 1px solid var(--glass-line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow), var(--glass-hi);
  opacity: 0.28;
  transition: opacity 0.2s ease;
  z-index: 81;
}
.fs-bar:hover { opacity: 1; }

.progress { margin-top: 12px; }

.progress-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  margin-bottom: 7px;
  font-size: 0.8em;
}
.progress-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.progress-stats { color: var(--text-dim); white-space: nowrap; font-variant-numeric: tabular-nums; }

.progress-track {
  height: 6px;
  background: rgba(255, 255, 255, 0.09);
  border-radius: 3px;
  overflow: hidden;
}
.progress-fill {
  height: 100%;
  width: 0;
  background: linear-gradient(90deg, #e8e8ec, #fff);
  border-radius: 3px;
  box-shadow: 0 0 12px rgba(255, 255, 255, 0.4);
  transition: width 0.15s linear;
}
.progress-fill.done { background: var(--ok); box-shadow: 0 0 12px rgba(57, 217, 138, 0.5); }
.progress-fill.failed { background: var(--danger); box-shadow: 0 0 12px rgba(255, 93, 115, 0.5); }

/* ------------------------------------------------------------------ modal */

.modal {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(4, 4, 6, 0.6);
  backdrop-filter: blur(10px) saturate(120%);
  -webkit-backdrop-filter: blur(10px) saturate(120%);
}

/* The login card is narrower than a content card and centred, but it must never
    exceed the viewport: the first version carried an inline max-width of 440px,
    which on a 430px phone pushed the submit button off screen. */
.login {
  max-width: 440px;
  width: 100%;
  margin: 40px auto;
}
.modal-card {
  width: 100%;
  max-width: 440px;
  padding: 28px;
  background: var(--glass-raised);
  backdrop-filter: blur(var(--blur)) saturate(150%);
  -webkit-backdrop-filter: blur(var(--blur)) saturate(150%);
  border: 1px solid var(--glass-line-strong);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow), var(--glass-hi);
}
.modal-card h3 { font-size: 1.15em; font-weight: 700; margin-bottom: 10px; letter-spacing: -0.01em; }
.modal-card p { color: var(--text-dim); font-size: 0.9em; margin-bottom: 16px; }
.modal-card input { margin-bottom: 4px; }
.modal-actions { display: flex; gap: 10px; margin-top: 18px; }
.modal-actions button { flex: 1; }

/* ------------------------------------------------------------------- util */

.hidden { display: none !important; }
.section { margin-bottom: 20px; }

/* Section headings, in the reference's "MENU" style: small, uppercase, widely
   tracked, and dim enough to sit under the content rather than beside it. */
h4 {
  margin: 24px 0 12px;
  font-size: 0.7em;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 1.6px;
  color: var(--text-faint);
}
h4:first-of-type { margin-top: 4px; }

/* Selection and scrollbars, so the chrome matches the surface. */
::selection { background: rgba(255, 255, 255, 0.22); }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.11);
  border: 3px solid transparent;
  background-clip: content-box;
  border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.2); background-clip: content-box; }

/* --------------------------------------------------------------- fallback */

/* Without backdrop-filter the panels would be translucent with nothing behind them,
   and text would sit on the page background at unpredictable contrast. Raising the
   alpha keeps every surface readable; the glass is simply lost. This is a real case —
   an older WebView, or the flag disabled for performance — so it is handled rather
   than assumed away. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .card, .topbar, .fs-bar, .modal-card { background: rgba(26, 26, 30, 0.95); }
  .modal { background: rgba(4, 4, 6, 0.88); }
}

/* --------------------------------------------------------------- responsive */

@media (max-width: 1100px) {
  .thead { display: none; }
  .agent-row { grid-template-columns: 1fr 1fr; gap: 6px 12px; padding: 14px 10px; }
  .agent-row .row-actions { grid-column: 1 / -1; justify-content: flex-start; }
}

@media (max-width: 700px) {
  /* The header is shorter here — the nav labels collapse to icons and the account chip
     drops to its avatar — so the reserved space shrinks with it. Keeping the desktop
     132px would leave a visible gap above the first card.

     `width: calc(100% - 20px)` also matters: with the fixed 48px inset the header
     was wider than a 390px phone viewport, and the logout button was pushed off
     the right edge where it could not be tapped. */
  .shell { padding: 108px 14px 44px; }
  .card { padding: 18px; border-radius: var(--r-md); }
  .card::before { border-radius: var(--r-md) var(--r-md) 50% 50% / var(--r-md) var(--r-md) 22% 22%; }
  .topbar {
    padding: 11px 12px;
    width: calc(100% - 20px);
    max-width: none;
    top: 10px;
    gap: 9px;
    border-radius: var(--r-md);
  }
  .wordmark .name { font-size: 1.05em; }

  /* The account chip collapses to the avatar on a phone: the name is the first thing
     that can go, and the chevron goes with it. */
  .account { padding: 4px; gap: 0; }
  .account .account-meta, .account .chev { display: none; }

  .tiles { grid-template-columns: repeat(2, 1fr); }
  .row { flex-wrap: wrap; }
  .row .btn { flex: 1 1 auto; }
  .login { margin: 20px auto; }
}

/* ------------------------------------------------------- dependencies tab */

/* A row per dependency. The layout mirrors .agent-row in the client list, so the two
   read as the same kind of thing: a labelled entry with actions on the right. */
.dep-row {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 12px 14px;
  /* --glass-line and --glass-flat, not --line and --panel-2: those two names are not
     defined in this stylesheet, so both declarations were dropped and every dependency
     row rendered with no border and no background -- a flat run of text where the
     design calls for a card. */
  border: 1px solid var(--glass-line);
  border-radius: 10px;
  margin-bottom: 8px;
  background: var(--glass-flat);
}

.dep-head {
  display: flex;
  align-items: center;
  gap: 10px;
}

.dep-facts {
  display: flex;
  flex-direction: column;
  gap: 2px;
  opacity: 0.85;
}

.dep-row .row-actions {
  align-self: flex-end;
}
/* --------------------------------------------------------------- data tables */

/* The container for one or more tables. Not a <pre> any more: the collection output is
   structured now, and monospace-with-newlines was the thing that made it unreadable. */
.table-host {
  display: flex;
  flex-direction: column;
  gap: 14px;
  font-family: inherit;
  white-space: normal;
}

.table-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 6px;
}

.table-title {
  font-weight: 600;
}

.table-count {
  font-size: 11px;
  padding: 1px 7px;
  border-radius: 9px;
  background: rgba(255, 255, 255, 0.08);
  color: var(--text-dim);
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
}

/* A sticky header, because the tables are long by nature and scrolling a thousand rows
   without knowing which column is which is the problem this replaced.
 *
 * WHY THIS RULE WAS INVISIBLE
 *
 * The three declarations below were written against --panel-2, --line and --faint, and
 * none of those names is defined anywhere in this stylesheet -- the palette uses
 * --glass-flat, --glass-line and --text-faint. An undefined custom property does not
 * fall back to something sensible: the declaration is simply dropped, and the property
 * takes its initial value. So the header had a transparent background, no border, and
 * inherited body colour -- visually indistinguishable from the first row of data. That
 * is exactly the report: the header "мешается с данными".
 *
 * The names were almost certainly correct in an earlier palette. They are corrected
 * here rather than aliased, because adding three dead custom properties to :root to
 * keep three typos working is how a palette ends up with two names for everything. */
.data-table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  text-align: left;
  font-weight: 600;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-faint);
  padding: 7px 10px;
  /* Opaque, not translucent: a sticky header sits over the rows as they scroll under
     it, so anything showing through reads as smeared text. --glass-flat is a flat
     colour rather than a gradient, which is what position:sticky needs. */
  background: #17171b;
  border-bottom: 1px solid var(--glass-line-strong);
  white-space: nowrap;
}

/* The first header cell keeps the table's left edge aligned with the rows. */
.data-table thead th:first-child { border-top-left-radius: var(--r-xs); }
.data-table thead th:last-child { border-top-right-radius: var(--r-xs); }

.data-table tbody td {
  padding: 6px 10px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.045);
  vertical-align: top;
  /* Short values stay on one line so a column of keys lines up; long ones are allowed to
     wrap, which is what the "wide" class opts into. */
  white-space: nowrap;
  max-width: 340px;
  overflow: hidden;
  text-overflow: ellipsis;
}

.data-table tbody td.wide {
  white-space: normal;
  word-break: break-all;
  max-width: 460px;
}

.data-table tbody tr:hover td {
  background: rgba(255, 255, 255, 0.028);
}

/* A value that is masked until clicked. The dots are drawn by the class, not by replacing
   the text, so a copy of the cell still yields the real value. */
.secret {
  cursor: pointer;
  border-bottom: 1px dotted rgba(255, 255, 255, 0.25);
}

/* The blur is deliberately LIGHT.
   The first version used `color: transparent` with a heavy text-shadow, which hid the
   value so completely that an operator could not tell a password from an empty cell
   without clicking every row -- and a table like this is mostly read, not copied from.
   A soft blur reads as "there is something here" and still takes one click to reveal. */
.secret:not(.revealed) {
  filter: blur(3.5px);
  opacity: 0.75;
}

.secret.revealed {
  color: var(--accent, #7fb2ff);
  filter: none;
  opacity: 1;
}

.copy-col {
  width: 34px;
  text-align: right;
}

/* The per-row copy button.
 *
 * A fixed 26px box holding a 14px icon, rather than padding around a text glyph. The
 * old version was `padding: 1px 6px` on a character, so its width depended on which
 * character was in it -- the tick that replaced the copy glyph on click was a different
 * width, and the column shifted every time the button was pressed. A fixed box means
 * the confirmation can be shown without moving anything.
 *
 * Dimmed to 0.32 at rest so a thousand rows do not turn into a thousand buttons, and
 * brought up on row hover. The transition covers the reveal, the press, and the colour
 * change the copied state uses, so none of the three snaps. */
.copy-one {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  opacity: 0.32;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-xs);
  cursor: pointer;
  color: inherit;
  transition: opacity 0.16s ease, background 0.16s ease,
              border-color 0.16s ease, transform 0.1s ease, color 0.16s ease;
}

.copy-one .copy-icon {
  width: 14px;
  height: 14px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.data-table tbody tr:hover .copy-one {
  opacity: 0.85;
}

.copy-one:hover {
  opacity: 1;
  border-color: var(--glass-line-strong);
  background: rgba(255, 255, 255, 0.06);
}

/* Held down: a small inward push. transform rather than a size change, so the layout
   around it is untouched and nothing reflows on press. */
.copy-one:active { transform: scale(0.9); }

/* The confirmation. Colour only -- the icon swaps but the box does not, so the row
   stays put. */
.copy-one.copied {
  opacity: 1;
  color: var(--ok);
  border-color: rgba(57, 217, 138, 0.42);
  background: rgba(57, 217, 138, 0.1);
}

/* Keyboard users get the same affordance as the mouse. */
.copy-one:focus-visible {
  opacity: 1;
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
/* ------------------------------------------------------------- Cloudflare rows */

/* A row of fields and buttons that wraps on a narrow window. The token field is given
   more room than the account id because it is the one that is pasted, and pasting into a
   two-centimetre box is how a truncated token gets saved. */
.cf-row {
  display: flex;
  align-items: flex-end;
  gap: 10px;
  flex-wrap: wrap;
}

/* THE FIELD'S BOTTOM MARGIN IS WHAT MADE THE BUTTONS SIT LOW.
 *
 * `.field` carries `margin-bottom: 14px` for stacked form layouts, and a margin is part
 * of the element's box for alignment purposes. This row aligns on the bottom edge
 * (`align-items: flex-end`), so the field's box ended 14px below its input and the
 * buttons -- which have no such margin -- lined up with that, not with the inputs. The
 * result was two buttons floating a line below the fields they act on.
 *
 * Zeroing it here, rather than removing it from `.field`, keeps the spacing correct
 * everywhere the fields are stacked normally. */
.cf-row .field {
  min-width: 200px;
  margin-bottom: 0;
}

/* Equal buttons, matched to the input height.
 *
 * They had different widths because each sized itself to its own label -- "Сохранить"
 * is longer than "Убрать" -- and neither matched the inputs beside them, so the row
 * ended in a ragged step. A shared min-width and the input's own box height make the
 * four controls read as one line.
 *
 * 39px is the measured height of an input in this row: 11px padding top and bottom, a
 * 1px border on each side, and a ~15px line box at 0.9em. The first attempt used 43px,
 * which aligned the bottoms (the row is flex-end) but left the buttons standing 4px
 * proud of the fields -- visible as a step along the top edge. */
.cf-row > button {
  min-width: 110px;
  height: 39px;
  padding: 0 16px;
  flex: 0 0 auto;
}

/* The zone summary under the token row. */
#cfZones { line-height: 1.7; }

/* A zone that backs one of the configured domains, rather than merely being visible to
   the token. Marked because it is the only part of the list that is actionable. */
.cf-zone-used { color: var(--ok); }
/* --------------------------------------------------------------- build picker */

/* One registered agent build.
 *
 * A row rather than a card: these are compared against each other -- which is signed,
 * which was tried, which is active -- and facts in a column are read by scanning down
 * them. Cards would space the same fields differently in each one and make the list
 * slower to read the longer it gets. */
.build-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 6px 14px;
  align-items: center;
  padding: 12px 14px;
  border: 1px solid var(--glass-line);
  border-radius: var(--r-sm);
  background: var(--glass);
  margin-bottom: 8px;
}

/* The active build is marked on the left edge so it is findable without reading. */
.build-row.primary {
  border-color: rgba(255, 255, 255, 0.34);
  box-shadow: inset 3px 0 0 var(--ok);
}

/* A build whose file is gone. Dimmed rather than hidden: it is still a record of
 * something that was built, and hiding it would make a missing file look like the build
 * was never registered. */
.build-row.gone { opacity: 0.55; }

.build-head {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  min-width: 0;
}

.build-id {
  font-size: 0.82em;
  color: var(--text-faint);
}

.build-label {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.build-meta { grid-column: 1; }

.build-verdict {
  grid-column: 1;
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: 2px;
}

/* The chosen verdict is filled in, so the state of the row is visible without reading
 * the labels -- "which of these did I already check" is the question this answers. */
.build-verdict button.on.ok { background: rgba(60, 200, 120, 0.22); border-color: var(--ok); }
.build-verdict button.on.danger { background: rgba(220, 80, 80, 0.22); border-color: var(--danger); }

.build-actions {
  grid-column: 2;
  grid-row: 1 / span 3;
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-self: center;
}

.build-actions button { min-width: 88px; }

.warn-text { color: var(--warn); }

/* --------------------------------------------------------- source switches */

/* The visible screen and HVNC are not two settings of one thing, they are two different
   desktops, and the choice has to be legible before anything is typed. Laid out as a row
   of described cards rather than a checkbox: the descriptions are the part that stops an
   operator typing into the session the user is watching by mistake. */
#srcModes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 10px;
}

/* Only the checked-state tint is specific to this pair. The circle, the text column and
   the card itself come from the base .mode rules below, which the Screen tab shares --
   it used to carry its own copy of all of it, and the copy referenced --line, a variable
   that is defined nowhere in this file, so the unchecked circle had no visible border. */

#srcModes .mode:has(input:checked) {
  border-color: rgba(127, 178, 255, 0.45);
  background: rgba(127, 178, 255, 0.06);
}

/* ------------------------------------------------------- install mode pair */

/* The two install modes, as a pair of described cards.
 *
 * WHY THIS BLOCK DID NOT EXIST AND WHY IT MATTERS
 *
 * `.mode.picked` was defined below and `#srcModes .mode` was defined for the Screen
 * tab, but the base `.mode` and its `.modes` container never were. The markup uses both
 * (`<div class="modes">` with two `<label class="mode">` children), so the two cards had
 * no layout at all: they collapsed into adjacent runs of text, the radio circles sat
 * jammed against the first letter of each heading, and `.picked` highlighted a box that
 * had no border or background to highlight.
 *
 * The grid is auto-fit rather than a fixed two columns so the pair stacks on a narrow
 * window instead of squeezing both notes into half the width each.
 */
.modes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 10px;
  margin: 12px 0 4px;
}

/* The whole card is the hit area, so the radio is part of the label rather than
   something to aim at. align-items: flex-start keeps the circle level with the heading
   instead of centring it against a note that is several lines tall. */
.mode {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 13px 15px;
  border: 1px solid var(--glass-line);
  border-radius: var(--r-md);
  background: var(--glass-sunken);
  box-shadow: var(--glass-hi-soft);
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.mode:hover { background: var(--glass-hover); }

.mode input[type="radio"] {
  appearance: none;
  -webkit-appearance: none;
  /* The circle is drawn, not inherited, so it stays round in every engine.

     WHY THE PREVIOUS VERSION RENDERED AS AN OVAL

     `appearance: none` removes the platform widget, but the UA stylesheet's other
     declarations survive it. A radio still carries the default
     `padding: 3px 3px 0 5px` -- asymmetric, so the box is 5px wider on one side than
     the other -- and as a flex item it also defaults to `min-width: auto`, which can
     push it past the 15px width it was given. The result was a 15x15 border box
     stretched into a 23x15 ellipse.

     All three have to be reset together. `padding: 0` and `min-width: 0` remove the
     inherited geometry, and `box-sizing: border-box` keeps the 1.5px border inside the
     15px rather than adding to it, so the rendered circle is exactly 15x15. */
  box-sizing: border-box;
  padding: 0;
  min-width: 15px;
  width: 15px;
  height: 15px;
  margin: 2px 0 0;
  flex: 0 0 15px;
  border-radius: 50%;
  border: 1.5px solid var(--glass-line-strong);
  background: transparent;
  position: relative;
  cursor: pointer;
}
.mode input[type="radio"]:checked { border-color: var(--accent); }
.mode input[type="radio"]:checked::after {
  content: "";
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background: var(--accent);
}

/* The text column. min-width: 0 is what lets the long note wrap instead of forcing the
   grid track wider than the card -- the same shrink rule the other flex rows need. */
.mode-body { flex: 1; min-width: 0; }

.mode-head {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 0.9em;
}

/* The "по умолчанию" / "UAC" marker. Small and quiet: it qualifies the heading rather
   than competing with it. */
.mode-tag {
  font-style: normal;
  font-size: 0.68em;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--glass-line);
  background: rgba(255, 255, 255, 0.05);
  color: var(--text-faint);
  white-space: nowrap;
}

.mode-note {
  display: block;
  margin-top: 6px;
  font-size: 0.8em;
  line-height: 1.55;
  color: var(--text-dim);
}

/* The selected install mode.
 *
 * The radio pair shows both outcomes, and this marks which one is in effect. It is a
 * style rather than a rewrite of the text: the note under each heading is the part that
 * says what the mode costs, and changing it on selection would mean the unselected option
 * no longer explains itself. */
.mode.picked {
  border-color: var(--accent);
  background: rgba(255, 255, 255, 0.07);
}
.mode.picked .mode-head b::after {
  content: " ✓";
  color: var(--accent);
}

/* ------------------------------------------------------------------ bridges
 *
 * The relay page. Everything here inherits the panel's glass treatment, so the
 * vocabulary is the existing one: --glass, --glass-line, --blur. Nothing new is
 * introduced that would make this page look like it came from another application.
 *
 * The one thing worth stating: state is carried by THREE signals at once -- the dot,
 * the word, and the border colour. Colour alone would be invisible to anyone who
 * cannot distinguish red from green, and a word alone makes a list of bridges hard to
 * scan.
 */

.bridge-add { margin-top: 12px; }

.bridge-form {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: flex-end;
  padding: 14px;
  border: 1px solid var(--glass-line);
  border-radius: 14px;
  background: var(--glass-flat);
  backdrop-filter: blur(var(--blur-sm));
}

.bridge-field { display: flex; flex-direction: column; gap: 5px; flex: 1 1 180px; }
.bridge-field-label {
  font-size: 11px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.bridge-input {
  width: 100%;
  padding: 9px 11px;
  border: 1px solid var(--glass-line);
  border-radius: 9px;
  background: var(--glass-sunken);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  transition: border-color .15s ease, background .15s ease;
}
.bridge-input:focus {
  outline: none;
  border-color: var(--glass-line-strong);
  background: rgba(255, 255, 255, .06);
}
.bridge-input::placeholder { color: var(--text-faint); }

/* ---------------------------------------------------------------- the grid */

.bridge-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(330px, 1fr));
  gap: 12px;
}

.bridge-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 11px;
  padding: 15px;
  border: 1px solid var(--glass-line);
  border-radius: 16px;
  background: var(--glass);
  backdrop-filter: blur(var(--blur));
  box-shadow: var(--glass-hi-soft);
  transition: border-color .18s ease, transform .18s ease;
}
.bridge-card:hover { transform: translateY(-1px); border-color: var(--glass-line-strong); }

/* A left edge that carries the state. Two pixels, so it reads as a marker rather
   than as decoration. */
.bridge-card::before {
  content: "";
  position: absolute;
  left: 0; top: 14px; bottom: 14px;
  width: 2px;
  border-radius: 2px;
  background: var(--text-faint);
}
.bridge-card.ok::before       { background: #3ddc84; box-shadow: 0 0 12px rgba(61, 220, 132, .55); }
.bridge-card.dead::before     { background: var(--red); box-shadow: 0 0 12px var(--red-glow); }
.bridge-card.pending::before  { background: #f0b429; box-shadow: 0 0 12px rgba(240, 180, 41, .45); }
.bridge-card.disabled::before { background: var(--text-faint); }

.bridge-card.dead { border-color: rgba(255, 74, 74, .28); }

/* ---------------------------------------------------------------- card head */

.bridge-head { display: flex; align-items: center; gap: 10px; }

.bridge-dot {
  width: 9px; height: 9px;
  border-radius: 50%;
  flex: 0 0 auto;
  background: var(--text-faint);
}
.bridge-dot.ok       { background: #3ddc84; box-shadow: 0 0 10px rgba(61, 220, 132, .7); }
.bridge-dot.dead     { background: var(--red); box-shadow: 0 0 10px var(--red-glow); animation: bridge-pulse 2s ease-in-out infinite; }
.bridge-dot.pending  { background: #f0b429; box-shadow: 0 0 10px rgba(240, 180, 41, .6); }
.bridge-dot.disabled { background: var(--text-faint); }

/* A dead bridge pulses. It is the one state that needs to catch the eye from across
   the page, because it means traffic is being moved. */
@keyframes bridge-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: .35; }
}

.bridge-title { flex: 1; min-width: 0; }
.bridge-domain {
  font-size: 13.5px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bridge-label { font-size: 11.5px; color: var(--text-faint); margin-top: 2px; }

.bridge-state {
  font-size: 11px;
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid var(--glass-line);
  color: var(--text-dim);
  white-space: nowrap;
}
.bridge-state.ok       { color: #3ddc84; border-color: rgba(61, 220, 132, .35); background: rgba(61, 220, 132, .09); }
.bridge-state.dead     { color: var(--red); border-color: rgba(255, 74, 74, .4); background: rgba(255, 74, 74, .1); }
.bridge-state.pending  { color: #f0b429; border-color: rgba(240, 180, 41, .35); background: rgba(240, 180, 41, .09); }
.bridge-state.disabled { color: var(--text-faint); }

/* ---------------------------------------------------------------- facts */

.bridge-facts { display: flex; flex-direction: column; gap: 5px; }

.bridge-fact { display: flex; gap: 9px; font-size: 12px; align-items: baseline; }
.bridge-fact-key { color: var(--text-faint); min-width: 104px; flex: 0 0 auto; }
.bridge-fact-val { color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; }
.bridge-fact-val.bad { color: var(--red); }

/* ---------------------------------------------------------------- domains */

.bridge-domains {
  padding: 9px 11px;
  border-radius: 10px;
  background: var(--glass-sunken);
  border: 1px solid var(--glass-line);
}
.bridge-domains-title {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--text-faint);
  margin-bottom: 7px;
}
.bridge-domain-chip {
  display: inline-block;
  font-size: 11.5px;
  padding: 2px 8px;
  margin: 2px 4px 2px 0;
  border-radius: 7px;
  background: rgba(255, 255, 255, .06);
  border: 1px solid var(--glass-line);
  color: var(--text-dim);
}

/* ---------------------------------------------------------------- pending */

.bridge-pending {
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px dashed rgba(240, 180, 41, .45);
  background: rgba(240, 180, 41, .06);
  font-size: 12px;
  color: var(--text-dim);
  display: flex;
  flex-direction: column;
  gap: 7px;
}

.bridge-hint { font-size: 11.5px; color: var(--text-faint); line-height: 1.5; }

.bridge-cmd {
  font-size: 11.5px;
  padding: 9px 11px;
  border-radius: 9px;
  background: rgba(0, 0, 0, .38);
  border: 1px solid var(--glass-line);
  color: var(--text);
  word-break: break-all;
  line-height: 1.55;
  user-select: all;
}

/* ---------------------------------------------------------------- actions */

.bridge-actions {
  display: flex;
  gap: 7px;
  margin-top: auto;
  padding-top: 3px;
}
.bridge-actions button { flex: 0 0 auto; }
.bridge-actions button.danger { color: var(--red); border-color: rgba(255, 74, 74, .3); }
.bridge-actions button.danger:hover { background: rgba(255, 74, 74, .12); }

/* ---------------------------------------------------------------- result */

.bridge-result {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 15px;
  border-radius: 14px;
  border: 1px solid rgba(61, 220, 132, .3);
  background: rgba(61, 220, 132, .05);
}
.bridge-result-title { font-size: 13.5px; font-weight: 600; color: #3ddc84; }
.bridge-result-note { font-size: 12px; color: var(--text-dim); line-height: 1.55; }
.bridge-result button { align-self: flex-start; }

/* ---------------------------------------------------------------- domain row */

.domain-bridge {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-top: 6px;
  flex-wrap: wrap;
}
.domain-bridge-label { font-size: 11.5px; color: var(--text-faint); }

.domain-bridge-select {
  font: inherit;
  font-size: 11.5px;
  padding: 4px 8px;
  border-radius: 8px;
  border: 1px solid var(--glass-line);
  background: var(--glass-sunken);
  color: var(--text-dim);
  cursor: pointer;
  max-width: 260px;
}
.domain-bridge-select:focus { outline: none; border-color: var(--glass-line-strong); }
.domain-bridge-select:disabled { opacity: .5; cursor: wait; }

.domain-bridge-warn {
  font-size: 11px;
  padding: 2px 8px;
  border-radius: 999px;
  color: var(--red);
  border: 1px solid rgba(255, 74, 74, .35);
  background: rgba(255, 74, 74, .09);
}
