/* Wormhole console.
 *
 * Industrial control panel, not SaaS dashboard. The people using this read HMI screens all
 * day: monospace for anything that is data, a cool slate ground, and semantic colour that
 * means exactly one thing each.
 *
 * FOUR DEVICE STATES, FOUR COLOURS, never reused for anything else. The accent is a steel
 * blue precisely so it cannot be mistaken for a status. "Never connected" is slate rather
 * than red on purpose: it is not an outage, it is an install that did not complete, and it
 * leads somewhere different. Destructive ochre is reserved for rotate-key, deregister and
 * revoke.
 *
 * THEMES. Three states, not two. An explicit choice stamps data-theme on <html>; the
 * default "system" setting stamps nothing, where only prefers-color-scheme separates light
 * from dark. So: bare :root defines the complete light palette, the media query redefines
 * only tokens (guarded so an explicit light choice beats a dark OS), and [data-theme=dark]
 * redefines them again so the toggle wins in both directions. Nothing below sets a colour
 * anywhere except through a token.
 */

:root {
  --ground:    #f5f7f9;
  --surface:   #ffffff;
  --sunken:    #edf1f4;
  --ink:       #111a21;
  --muted:     #57646e;
  --faint:     #808d96;
  --line:      #dbe3e8;
  --line-firm: #c3cfd7;

  --accent:      #1f5f86;
  --accent-ink:  #17455f;
  --accent-wash: #e7f0f6;

  --up:         #1b7a5a;
  --up-wash:    #e2f1eb;
  --flap:       #8e6410;
  --flap-wash:  #f7eeda;
  --down:       #a63c2e;
  --down-wash:  #f8e7e3;
  --never:      #62707a;
  --never-wash: #e9edf0;

  --glass:      #a85e14;
  --glass-wash: #faeedf;

  --mono: ui-monospace, "JetBrains Mono", "SFMono-Regular", "SF Mono", Menlo, Consolas,
          "Liberation Mono", monospace;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;

  --rail: 208px;
  --radius: 4px;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ground:    #0d1317;
    --surface:   #141d23;
    --sunken:    #101920;
    --ink:       #e3eaef;
    --muted:     #93a1aa;
    --faint:     #74838d;
    --line:      #222e36;
    --line-firm: #33424c;

    --accent:      #6fb2d8;
    --accent-ink:  #a8d2ea;
    --accent-wash: #152a38;

    --up:         #4cbe93;
    --up-wash:    #10281f;
    --flap:       #d39f3d;
    --flap-wash:  #2b2211;
    --down:       #e07460;
    --down-wash:  #2e1a16;
    --never:      #8b98a1;
    --never-wash: #1b242a;

    --glass:      #e09a42;
    --glass-wash: #2e2113;
  }
}

:root[data-theme="dark"] {
  --ground:    #0d1317;
  --surface:   #141d23;
  --sunken:    #101920;
  --ink:       #e3eaef;
  --muted:     #93a1aa;
  --faint:     #74838d;
  --line:      #222e36;
  --line-firm: #33424c;

  --accent:      #6fb2d8;
  --accent-ink:  #a8d2ea;
  --accent-wash: #152a38;

  --up:         #4cbe93;
  --up-wash:    #10281f;
  --flap:       #d39f3d;
  --flap-wash:  #2b2211;
  --down:       #e07460;
  --down-wash:  #2e1a16;
  --never:      #8b98a1;
  --never-wash: #1b242a;

  --glass:      #e09a42;
  --glass-wash: #2e2113;
}

*, *::before, *::after { box-sizing: border-box; }

html { color-scheme: light dark; }

body {
  margin: 0;
  /* Explicit, from a token: the page composites over a ground painted in the viewer's
   * theme, and a transparent body silently borrows it. */
  background: var(--ground);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
}

/* ---- shell ------------------------------------------------------------------ */

.shell {
  display: grid;
  grid-template-columns: var(--rail) 1fr;
  /* HEIGHT, not min-height, and that one word is the defect.
   *
   * `min-height: 100vh` lets the grid grow to the height of its content, and the rail is a
   * grid item that stretches with it. `.rail-foot` pins sign-out to the bottom of the RAIL,
   * which on a long page is the bottom of the document rather than the bottom of the screen.
   * So on every screen worth scrolling -- which is every screen an operator spends time in --
   * sign-out was below the fold.
   *
   * dvh before vh because this matters most on a phone, where vh is the viewport WITH the
   * browser chrome that is not there, so 100vh overflows by the height of the URL bar and
   * reintroduces the same scroll. vh stays as the fallback for anything that lacks dvh. */
  height: 100vh;
  height: 100dvh;
  /* The main column scrolls, so the shell itself must not. Without this the grid's own
   * overflow puts the scrollbar back on the document and the rail travels with it again. */
  overflow: hidden;
}

.rail {
  border-right: 1px solid var(--line);
  background: var(--surface);
  padding: 20px 0;
  display: flex;
  flex-direction: column;
  gap: 24px;
  /* The rail is exactly the screen and never taller. */
  height: 100%;
  min-height: 0;
  /* AND NEVER WIDER, which needs saying explicitly. A grid item's automatic minimum size is
   * its min-content, so the rail grew to fit its widest unbreakable child and ignored its
   * track: measured 424px wide inside both a 375px and a 208px viewport, identically, because
   * 424 was its content's minimum rather than anything to do with the screen.
   *
   * `overflow: hidden` on .shell then CLIPPED the overflow instead of revealing it, so the
   * page did not scroll sideways and nothing looked wrong -- while sign-out sat outside the
   * viewport at 208px. A defect hidden by the fix for a different one. Same class as the
   * min-height: 0 above, on the other axis. */
  min-width: 0;
  overflow: hidden;
}

/* THE NAV SCROLLS, THE FOOT DOES NOT.
 *
 * The second half of the same fix. Pinning the foot with margin-top:auto inside a
 * fixed-height rail hides it again the moment the nav itself outgrows the screen, which is a
 * short phone in landscape with five sections. So the nav gets the overflow and the foot is
 * outside it: whatever the nav does, sign-out and the break-glass line stay on screen.
 *
 * min-height: 0 because a flex child's default min-height is its content, which would make
 * it refuse to shrink and expand the rail instead -- the same class of thing as the grid
 * track that outlived its element. */
.nav { min-height: 0; min-width: 0; overflow-y: auto; }

.brand {
  font-family: var(--mono);
  font-size: 13px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
  padding: 0 20px;
}

.nav { display: flex; flex-direction: column; }

.nav a {
  font-family: var(--mono);
  font-size: 13px;
  color: var(--muted);
  text-decoration: none;
  padding: 8px 20px;
  border-left: 2px solid transparent;
}
.nav a:hover { color: var(--ink); background: var(--sunken); }
.nav a[aria-current="page"] {
  color: var(--accent);
  border-left-color: var(--accent);
  background: var(--accent-wash);
}

.rail-foot {
  margin-top: auto;
  padding: 0 20px;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--faint);
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.main { padding: 24px 28px 64px; min-width: 0; }

.page-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 20px;
}

h1 {
  font-family: var(--mono);
  font-weight: 500;
  font-size: 20px;
  letter-spacing: -0.01em;
  margin: 0;
}

h2 {
  font-family: var(--mono);
  font-weight: 500;
  font-size: 14px;
  letter-spacing: 0.02em;
  margin: 28px 0 10px;
  color: var(--muted);
}

.updated {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--faint);
}
/* A stale dashboard that looks live is the failure this whole subsystem exists to prevent,
 * so staleness is loud rather than absent. */
.updated.is-stale { color: var(--down); }

/* ---- tiles ------------------------------------------------------------------- */
/* The counts are the question and the filter is the answer, so they are one control. */

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  gap: 10px;
  margin-bottom: 18px;
}

.tile {
  appearance: none;
  text-align: left;
  font: inherit;
  cursor: pointer;
  background: var(--surface);
  border: 1px solid var(--line);
  border-top: 2px solid var(--line-firm);
  border-radius: var(--radius);
  padding: 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.tile:hover { border-color: var(--line-firm); }
.tile[aria-pressed="true"] { border-top-color: var(--accent); background: var(--accent-wash); }

.tile .n {
  font-family: var(--mono);
  font-size: 24px;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.tile .k {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.tile.t-up    .n { color: var(--up); }
.tile.t-down  .n { color: var(--down); }
.tile.t-flap  .n { color: var(--flap); }
.tile.t-never .n { color: var(--never); }

/* ---- notices ---------------------------------------------------------------- */

.notice {
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  background: var(--surface);
  border-radius: 0 var(--radius) var(--radius) 0;
  padding: 12px 16px;
  margin-top: 14px;
  margin-bottom: 14px;
  font-size: 14px;
}
.notice.is-warn  { border-left-color: var(--flap); }
.notice.is-error { border-left-color: var(--down); }
.notice .t {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  display: block;
  margin-bottom: 4px;
}
.notice a { color: var(--accent); }

/* ---- filters ---------------------------------------------------------------- */

.filters {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 12px;
}

.input {
  font: inherit;
  font-family: var(--mono);
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line-firm);
  border-radius: var(--radius);
  padding: 6px 10px;
  /* `min-width: 220px` alone was a HARD FLOOR, and a floor wider than the viewport pushes the
   * page sideways instead of shrinking. At a 208px window the sign-in inputs rendered 220px
   * wide inside a 165px column and ran off the right edge, and the document scrolled
   * horizontally: scrollWidth 234 against a 208px window. Reported by Stijn, at that width,
   * on the one screen where being unable to read the fields means being unable to get in.
   *
   * min() keeps the comfortable default everywhere there is room and gives it up when there
   * is not, so the floor can never exceed the space the container actually has. 100% rather
   * than 100vw because the container is what constrains it, and the page has padding. */
  min-width: min(220px, 100%);
  max-width: 100%;
}
.input::placeholder { color: var(--faint); }

.chips { display: flex; gap: 6px; flex-wrap: wrap; }

.chip {
  appearance: none;
  font: inherit;
  font-family: var(--mono);
  font-size: 11.5px;
  cursor: pointer;
  background: var(--accent-wash);
  color: var(--accent-ink);
  border: 1px solid var(--accent);
  border-radius: 999px;
  padding: 2px 8px 2px 10px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.chip .x { font-size: 13px; line-height: 1; }

.link-btn {
  appearance: none;
  background: none;
  border: 0;
  font: inherit;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--accent);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
  padding: 2px;
}

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

.table-wrap {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  overflow-x: auto;
}

table { border-collapse: collapse; width: 100%; font-size: 14px; }

th {
  text-align: left;
  font-family: var(--mono);
  font-weight: 500;
  font-size: 11px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--faint);
  background: var(--sunken);
  border-bottom: 1px solid var(--line);
  padding: 9px 14px;
  white-space: nowrap;
  position: sticky;
  /* Not a bare 0: the shell status bar above it is sticky too, and --status-h is that bar's
   * own current height (zero when it is not shown), kept live by wireStatusHeight() in app.js.
   * Without this a table's header parks itself at the same y as an expired-session warning
   * and the two draw on top of each other. */
  top: var(--status-h, 0px);
}

th .sort {
  appearance: none;
  background: none;
  border: 0;
  font: inherit;
  /* Buttons do not inherit text-transform from the UA stylesheet, so a sortable header
   * rendered "Device" beside a plain one rendering "TAGS". */
  text-transform: inherit;
  letter-spacing: inherit;
  color: inherit;
  cursor: pointer;
  padding: 0;
  display: inline-flex;
  gap: 5px;
  align-items: center;
}
th[aria-sort] .sort { color: var(--ink); }

td {
  padding: 8px 14px;
  border-bottom: 1px solid var(--line);
  font-variant-numeric: tabular-nums;
}
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: var(--sunken); }

td.id { font-family: var(--mono); font-size: 13px; white-space: nowrap; }
td.id a { color: var(--ink); text-decoration: none; }
td.id a:hover { color: var(--accent); text-decoration: underline; }
td.site { color: var(--muted); }
td.actions { text-align: right; width: 1%; white-space: nowrap; }

/* ---- fleet table: device name, tags, last seen (P19) ------------------------- */

td.id { position: relative; }
tbody tr { cursor: pointer; }
/* Real controls inside a clickable row keep their own cursor rather than inheriting the
 * row's, so a link or a chip does not look like plain text pretending to be clickable. */
tbody tr a, tbody tr button { cursor: pointer; }

.device-name { color: var(--ink); text-decoration: none; display: flex; flex-direction: column; gap: 1px; }
.device-name:hover { color: var(--accent); text-decoration: underline; }
.row-sub { font-family: var(--mono); font-size: 11.5px; color: var(--faint); text-decoration: none; }

td.last-seen { color: var(--muted); font-size: 13px; }

.tag-chip-row { display: inline-flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.chip-static {
  font-family: var(--mono);
  font-size: 11px;
  background: var(--sunken);
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 1px 8px;
}
.chip-more {
  appearance: none;
  font: inherit;
  font-family: var(--mono);
  font-size: 11px;
  cursor: pointer;
  background: var(--surface);
  color: var(--muted);
  border: 1px dashed var(--line-firm);
  border-radius: 999px;
  padding: 1px 7px;
}
.chip-more:hover { border-color: var(--accent); color: var(--accent); }

/* The +N popover: the overflow tags only, since the first three are already visible inline
 * in the row and repeating them would be the row's own noise problem moved one click deep.
 *
 * display IS GATED ON :popover-open, and that is not a style preference. The UA stylesheet
 * hides a popover with `[popover]:not(:popover-open) { display: none }`, and author rules
 * outrank UA rules by ORIGIN before specificity is even considered -- an unconditional
 * `display: flex` here, at any specificity, wins the cascade and the popover renders open
 * all the time, unconditionally, from the moment it exists. Found by looking at the fleet
 * table right after writing this: every row's popover and the header's tag filter were
 * showing at once, because that is exactly the rule this comment describes. */
.tag-popover {
  padding: 8px;
  border: 1px solid var(--line-firm);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  box-shadow: 0 6px 20px rgb(0 0 0 / 0.16);
  max-width: 16rem;
  /* Reset the UA's centring so anchor positioning below is what places it, same as .menu-panel. */
  margin: 0;
  inset: auto;
}
.tag-popover:popover-open { display: flex; flex-direction: column; gap: 4px; }
.tag-popover .chip-static { align-self: flex-start; }

/* The Tags column header's own filter, a checklist rather than a menu: checking a box must
 * not close the panel the way choosing a menu item does.
 *
 * display is gated on :popover-open for the same reason .tag-popover's is -- see the comment
 * there. This one omitting it is what actually surfaced the bug on screen. */
.tag-filter-panel {
  padding: 6px;
  border: 1px solid var(--line-firm);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  box-shadow: 0 6px 20px rgb(0 0 0 / 0.16);
  max-height: 60vh;
  overflow-y: auto;
  min-width: 14rem;
  /* Reset the UA's centring so anchor positioning below is what places it, same as .menu-panel. */
  margin: 0;
  inset: auto;
}
.tag-filter-panel:popover-open { display: flex; flex-direction: column; gap: 2px; }
.tag-filter-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 6px;
  border-radius: 4px;
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  color: var(--ink);
  cursor: pointer;
}
.tag-filter-row:hover { background: var(--sunken); }
.tag-filter-name { font-family: var(--mono); font-size: 12.5px; flex: 1; }
.tag-filter-count {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--faint);
  font-variant-numeric: tabular-nums;
}

.empty {
  padding: 40px 16px;
  text-align: center;
  color: var(--muted);
  font-size: 14px;
}

/* ---- pills ------------------------------------------------------------------ */
/* Never colour alone: a dot, a colour and a word. Some of these engineers are colour-blind
 * and all of them are reading a laptop screen in a plant. */

.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: 3px;
  white-space: nowrap;
  border: 1px solid transparent;
}
.pill::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }

.pill.s-up    { color: var(--up);    background: var(--up-wash);    border-color: var(--up); }
.pill.s-flap  { color: var(--flap);  background: var(--flap-wash);  border-color: var(--flap); }
.pill.s-down  { color: var(--down);  background: var(--down-wash);  border-color: var(--down); }
.pill.s-never { color: var(--never); background: var(--never-wash); border-color: var(--never); }

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

.btn {
  appearance: none;
  font: inherit;
  font-family: var(--mono);
  font-size: 12.5px;
  cursor: pointer;
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line-firm);
  border-radius: var(--radius);
  padding: 6px 12px;
}
a.btn { text-decoration: none; display: inline-block; }
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn:disabled { cursor: not-allowed; opacity: 0.5; }
.btn:disabled:hover { border-color: var(--line-firm); color: var(--ink); }

.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--surface); }
.btn-primary:hover { color: var(--surface); filter: brightness(1.1); }

.btn-destructive { background: var(--glass); border-color: var(--glass); color: var(--surface); }
.btn-destructive:hover { color: var(--surface); filter: brightness(1.08); }

/* ---- the ... menu ----------------------------------------------------------- */

.menu-trigger {
  appearance: none;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius);
  color: var(--muted);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  padding: 2px 8px;
}
.menu-trigger:hover { color: var(--ink); border-color: var(--line-firm); background: var(--surface); }

.menu-panel {
  /* The popover is in the top layer, so it needs its own painted surface -- it does not
   * inherit the page's. */
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line-firm);
  border-radius: var(--radius);
  box-shadow: 0 6px 20px rgb(0 0 0 / 0.16);
  padding: 4px;
  min-width: 190px;
  /* Reset the UA's centring so anchor positioning below is what places it. */
  margin: 0;
  inset: auto;
}

/* Attached to its trigger, flipping above when there is no room below. position-area does
 * the flip on its own -- no measurement, no scroll listener. */
@supports (anchor-name: --probe) {
  .menu-panel {
    position-area: bottom span-left;
    position-try-fallbacks: top span-left, bottom span-right, top span-right;
    margin-top: 4px;
  }
  /* Same attachment as .menu-panel, for the fleet table's own popovers: the +N tag overflow
   * and the Tags header's filter checklist. anchoredPopover() (lib/menu.js) wires the
   * anchor-name/position-anchor pair in JS; this is the declarative half that actually moves
   * the panel once that pair exists, and without it the panel would sit centred by the UA
   * default with the anchor silently doing nothing. */
  .tag-popover, .tag-filter-panel {
    position-area: bottom span-left;
    position-try-fallbacks: top span-left, bottom span-right, top span-right;
    margin-top: 4px;
  }
}

.menu-item {
  appearance: none;
  display: block;
  width: 100%;
  text-align: left;
  text-decoration: none;
  font: inherit;
  font-size: 13px;
  background: none;
  border: 0;
  border-radius: 3px;
  color: var(--ink);
  cursor: pointer;
  padding: 6px 10px;
}
.menu-item:hover { background: var(--accent-wash); color: var(--accent-ink); }
.menu-item.is-destructive { color: var(--glass); }
.menu-item.is-destructive:hover { background: var(--glass-wash); }

/* Disabled, not hidden. Hiding an action you may not take produces an engineer who believes
 * the console is broken; at 2am that costs more than the tidier menu saves. */
.menu-item[aria-disabled="true"] { color: var(--faint); cursor: not-allowed; }
.menu-item[aria-disabled="true"]:hover { background: none; color: var(--faint); }

.menu-why {
  display: block;
  font-family: var(--sans);
  font-size: 11.5px;
  line-height: 1.35;
  color: var(--faint);
  margin-top: 2px;
}

/* ---- dialog ----------------------------------------------------------------- */

.dialog {
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line-firm);
  border-radius: var(--radius);
  padding: 22px 24px;
  max-width: 30rem;
  width: calc(100vw - 32px);
  box-shadow: 0 12px 40px rgb(0 0 0 / 0.24);
}
.dialog::backdrop { background: rgb(0 0 0 / 0.45); }

.dialog-title { font-family: var(--mono); font-weight: 500; font-size: 16px; margin: 0 0 10px; }
.dialog-body { margin: 0 0 16px; color: var(--muted); font-size: 14px; }
.dialog-label { display: block; font-size: 13px; margin-bottom: 6px; }
.dialog-label code { font-family: var(--mono); background: var(--sunken); padding: 1px 5px; border-radius: 3px; }
.dialog .input { width: 100%; }
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }

/* ---- toasts ----------------------------------------------------------------- */

.toasts {
  position: fixed;
  right: 16px;
  bottom: 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 50;
  max-width: min(30rem, calc(100vw - 32px));
}

.toast {
  background: var(--surface);
  border: 1px solid var(--line-firm);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  padding: 10px 12px;
  font-size: 13.5px;
  box-shadow: 0 4px 14px rgb(0 0 0 / 0.14);
  display: flex;
  gap: 10px;
  align-items: flex-start;
}
.toast-success { border-left-color: var(--up); }
.toast-error   { border-left-color: var(--down); }
.toast-text { flex: 1; }
.toast-close {
  appearance: none; background: none; border: 0; cursor: pointer;
  color: var(--muted); font-size: 16px; line-height: 1; padding: 0 2px;
}

/* ---- sparkline -------------------------------------------------------------- */

/* Scales to its column. preserveAspectRatio="none" is set on the element: this is a
 * proportion strip, not a shape, so stretching it horizontally is exactly right. */
.sparkline { display: block; width: 100%; max-width: 560px; height: 22px; border-radius: 2px; overflow: hidden; }
.spark-ground { fill: var(--sunken); }
.spark-up   { fill: var(--up); }
.spark-down { fill: var(--down); }
/* Blue rather than --never's neutral grey: Stijn's word was that this signals NEW, which
 * this console treats as worth noticing, not as a colour with nothing to say. --accent
 * rather than a fifth token, so "new" reads as the same blue as a link or a primary button
 * rather than introducing a fourth semantic colour to a strip that only needed a third. */
.spark-new  { fill: var(--accent); }

/* ---- facts (device detail) -------------------------------------------------- */

/* Cell borders rather than a 1px grid gap over a coloured container: with auto-fit, the
 * last row is usually short, and the gap trick paints the leftover track in the line
 * colour -- a grey slab hanging off the end of the grid. Borders leave it as surface. */
.facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.fact {
  padding: 12px 14px;
  border-right: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.fact .k {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
  margin-bottom: 4px;
}
.fact .v { font-family: var(--mono); font-size: 14px; font-variant-numeric: tabular-nums; }
.fact .v.is-warn { color: var(--down); }
.fact .v.is-ok { color: var(--up); }

.cmd {
  display: flex;
  gap: 8px;
  align-items: center;
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 8px 10px;
  font-family: var(--mono);
  font-size: 12.5px;
  overflow-x: auto;
}
.cmd code { white-space: nowrap; }

/* THE WHOLE LINE IS THE CONTROL, on the enrolment page: button.cmd rather than a div with a
 * button beside it. appearance: none and the rest undo the UA's own button skin, which .cmd's
 * background/border override in most browsers already but not reliably everywhere. */
button.cmd {
  appearance: none;
  /* N12: sized to its content rather than stretched to the page. Capped at 100% of whatever
   * contains it, so a long one-liner does not re-widen the page the way width: 100% did --
   * .cmd's own overflow-x: auto (and white-space: nowrap on the code inside) still lets it
   * scroll internally past that cap rather than wrapping mid-command. */
  width: fit-content;
  max-width: 100%;
  text-align: left;
  color: inherit;
  cursor: pointer;
}
button.cmd:hover, button.cmd:focus-visible { border-color: var(--accent); }
button.cmd code { flex: 1; overflow-x: auto; }
/* Subtle until it matters: full opacity only on hover or keyboard focus, so the icon reads as
 * an affordance rather than another piece of the command to parse. */
.copy-icon {
  margin-left: auto;
  flex: none;
  display: inline-flex;
  color: var(--faint);
  opacity: 0.55;
}
button.cmd:hover .copy-icon, button.cmd:focus-visible .copy-icon {
  opacity: 1;
  color: var(--accent);
}
button.cmd.is-primary { border-color: var(--accent); background: var(--accent-wash); }
button.cmd.is-primary .copy-icon { color: var(--accent-ink); }

/* ---- responsive ------------------------------------------------------------- */
/* Down to a phone, read-only: a field engineer standing next to a dead IPC, checking
 * whether it is the tunnel or the box, is a real scenario. */

@media (max-width: 780px) {
  /* Two rows: the bar, then the scrolling column. `auto 1fr` rather than one track, so the
   * bar takes what it needs and the main column takes the rest and scrolls inside it. */
  .shell { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
  .rail {
    border-right: 0;
    border-bottom: 1px solid var(--line);
    flex-direction: row;
    /* WRAPS, so the foot becomes a second line in the bar instead of being dropped. */
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 12px;
    padding: 10px 14px;
    /* Not overflow-x: auto any more. A horizontally scrolling bar hid the foot by putting it
     * off the right edge, which is the same defect as hiding it, one axis over. */
    overflow: visible;
    height: auto;
    min-width: 0;
  }
  .nav { flex-direction: row; overflow-x: auto; overflow-y: visible; }
  .nav a { border-left: 0; border-bottom: 2px solid transparent; padding: 6px 10px; }
  .nav a[aria-current="page"] { border-left-color: transparent; border-bottom-color: var(--accent); }

  /* `.rail-foot { display: none }` WAS HERE, and it did not merely push sign-out below the
   * fold -- it removed it. With it went the operator's own name and the break-glass line,
   * whose comment in index.html says it is "permanent, not buried… an engineer who has never
   * seen this line before will not think of it at 2am". Hidden on precisely the device that
   * engineer is holding in the field, and on the one screen where the console being useless
   * is why they are reading it.
   *
   * It is now a full-width second row in the bar: nothing is dropped, nothing is off the
   * right edge, and sign-out is reachable without scrolling the page. */
  .rail-foot {
    display: flex;
    min-width: 0;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 12px;
    margin-top: 0;
    padding: 0;
    flex-basis: 100%;
  }
  /* The break-glass command is the longest thing here and the least droppable. It wraps
   * inside its own line rather than widening the bar, which would put the page back on a
   * horizontal scroll -- the 208px defect again. */
  .rail-foot code { white-space: normal; word-break: break-all; }
  /* Every child of the foot, not only the code. The span wrapping "break-glass: <code>" is
   * itself a flex item, and its automatic minimum would put the floor back. */
  .rail-foot > * { min-width: 0; }
  .main { padding: 16px 14px 48px; }

  /* Columns that stop earning their width first. Flaps is gone from the table entirely
   * (P19: the state pill already carries "flapping · N/24h", so a separate column repeated
   * the same number), and last-seen joins tags here for the same reason it did before --
   * the state pill already says roughly how long, and the exact figure is one tap away on
   * the device page. */
  .col-tags, td.tags, td.last-seen { display: none; }
}

/* ---- activity ---------------------------------------------------------------- */

td.when { font-family: var(--mono); font-size: 12.5px; color: var(--muted); white-space: nowrap; }

/* The console's own reads, shown but subordinate. They are part of the record -- reading
 * the audit trail is itself audited -- so hiding them by default would make this a log of
 * what someone wanted to see rather than of what happened. Dimming them lets a real
 * session stand out without pretending the reads did not occur. */
tr.is-read td { color: var(--faint); }
tr.is-read td.id { color: var(--muted); }

tr.is-refused td.id { color: var(--ink); }
tr.is-unparsed td { background: var(--flap-wash); }

.repeats {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted);
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 0 5px;
  margin-left: 6px;
  cursor: help;
}

.notice ul { font-size: 13.5px; color: var(--muted); line-height: 1.5; }

/* ---- forms ------------------------------------------------------------------- */

.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 14px;
  margin-bottom: 20px;
}

/* One column (N4/N8/N14/D5): the Operators page used to split LEFT, what this browser session
 * can do directly, from RIGHT, a composed command for the bastion. N8 and N14 moved both
 * remaining root-only actions onto buttons, so nothing draws that split any more, and the
 * independent-scroll question N4 asked only made sense with two columns of substantial
 * content -- collapsed instead, per D5's own fallback for exactly this case. */
.op-col { min-width: 0; max-width: 62rem; }
/* The signed-in operator's own roster row (N9), so a glance down the table finds it without
 * reading every name. */
.op-table tr.is-you > td { background: var(--accent-wash); }
.you-badge {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent-ink);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  border-radius: 3px;
  padding: 0 5px;
  margin-left: 6px;
}
.field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.field .input { width: 100%; }

.field-label {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--faint);
}

.field-hint { font-size: 12.5px; color: var(--faint); line-height: 1.4; }
/* Reserves nothing, same reasoning as .notice-slot: an empty hint that still took a line's
 * height pushed everything below it down for no reason, which mattered once the name and tag
 * sections were compressed to two lines and a blank third line undid the compression. */
.field-hint:empty { display: none; }

/* Input and its buttons on one row (P11): the name field used to be a label, an input, and a
 * button row as three separate lines. wrap rather than nowrap, so a narrow viewport drops the
 * buttons to a second line instead of clipping the input -- the same 208px-width lesson this
 * console already learned once on the login form. */
.name-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 4px; }
.name-row .input { flex: 1 1 14rem; min-width: 0; max-width: 26rem; }
/* Validity is a sentence naming the rule, not a red border. "Invalid" tells an operator
 * nothing; "uppercase is not allowed" tells them what to change. */
.field-hint.is-ok  { color: var(--up); }
.field-hint.is-bad { color: var(--down); }

.input[aria-invalid="true"] { border-color: var(--down); }

textarea.input { line-height: 1.5; resize: vertical; }

.cmd { margin-bottom: 4px; }

/* A pressed toggle has to look pressed. aria-pressed was set and nothing rendered it, so
 * the enrolment method switch was announced correctly to a screen reader and invisible to
 * everyone else -- the inverse of the usual failure, and just as broken. */
.btn[aria-pressed="true"] {
  background: var(--accent-wash);
  border-color: var(--accent);
  color: var(--accent-ink);
}

/* A permission verb, as reported by the bastion. Monospace because it is an identifier the
 * operator may need to quote back verbatim, not a label. */
.verb {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--accent-ink);
  background: var(--accent-wash);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  border-radius: 3px;
  padding: 1px 7px;
}

select.input { cursor: pointer; }
.field-hint.is-warn { color: var(--flap); }

/* --- sign in ------------------------------------------------------------------------
 *
 * Narrow and centred, and deliberately not the shell layout: somebody who is not signed in
 * has no fleet, so a rail listing screens they cannot open would be four dead links and a
 * question about whether the console is broken.
 */
.login-page { max-width: 34rem; margin: 0 auto; padding: 3rem 0 4rem; }
.login-page h1 { margin: 0 0 0.75rem; }
.login-stand { color: var(--muted); margin: 0 0 2rem; max-width: 60ch; }
.login-form { display: flex; flex-direction: column; gap: 1.25rem; }
.login-actions { display: flex; gap: 0.75rem; align-items: center; }
/* Reserves nothing. An empty slot that occupied height would push the form up when a
 * refusal arrived, moving the button out from under the pointer of somebody retrying. */
.notice-slot:empty { display: none; }

.login-fallback {
  margin-top: 3rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line);
  color: var(--muted);
}
.login-fallback .t {
  display: block;
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--faint);
  margin-bottom: 0.5rem;
}
.login-fallback p { margin: 0 0 0.75rem; max-width: 62ch; }
.login-fallback .cmd {
  font-family: var(--mono);
  font-size: 0.85rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 0.6rem 0.8rem;
  margin: 0;
  color: var(--ink);
  white-space: pre;
  overflow-x: auto;
}
/* The rail is hidden rather than absent, so the shell is still one static tree. See the
 * comment at the toggle in app.js for why that trade is worth making for one screen. */
.shell.no-rail .rail { display: none; }

/* AND THE COLUMN GOES WITH IT. `display: none` takes the rail out of the grid but leaves the
 * track it sat in, so .main-col became the first item and was laid out in the 208px rail
 * column -- measured at 1600px wide: `208px 1377px`, main-col 208, with 1377px of empty space
 * beside it. On the sign-in screen only, which is the one screen that sets no-rail.
 *
 * Which is the screen somebody is on when they cannot get in. Reported by Stijn while he was
 * looking at it, in the same hour he spent unable to log in against a form squeezed into a
 * quarter of a narrow phone's width on a 1600px display. Hiding an element and removing its
 * track are two changes and only one of them was made.
 *
 * Not caught by any check here, and the reason is worth recording: every assertion this
 * console has about the sign-in screen is about what it SAYS. `first-visit-check` drove this
 * exact screen eight ways tonight and passed, because a 208px column still contains the form,
 * the caret, the CLI line and the right sentence. Text-shaped questions cannot see a layout,
 * so this one needed a number. */
.shell.no-rail { grid-template-columns: 1fr; }

/* N10: the terminal's own tab keeps the rail column (unlike no-rail above, which removes it
 * entirely) but drops the section links -- clicking Fleet from inside a live session would
 * abandon it with no warning, and this tab has nowhere else useful to go anyway. Sign-out and
 * the break-glass line (.rail-foot) stay: an operator with only this tab open still needs
 * both (Q16), and neither is a navigation hazard the way the section links are. */
.shell.terminal-only .nav { display: none; }

/* --- terminal ------------------------------------------------------------------------ */
.term-banner {
  border: 1px solid var(--line);
  border-left: 3px solid var(--faint);
  border-radius: 4px;
  background: var(--surface);
  padding: 0.7rem 0.9rem;
  margin: 0 0 0.75rem;
}
/* Four states, four left borders and four words. Never colour alone: the word in
 * .term-status carries the state on its own, per convention 7. */
.term-banner.is-connecting  { border-left-color: var(--accent); }
.term-banner.is-live        { border-left-color: var(--up); }
.term-banner.is-closed-you  { border-left-color: var(--faint); }
.term-banner.is-closed-them { border-left-color: var(--flap); }
.term-banner.is-failed      { border-left-color: var(--down); }
.term-status {
  display: block;
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--faint);
  margin-bottom: 0.3rem;
}
.term-detail { margin: 0; color: var(--muted); max-width: 90ch; }

.term-pane {
  /* A HOST FOR THE EMULATOR, not a text box. This carried font, colour and white-space rules
   * when it was a <pre> appending text nodes; xterm owns all of that now, and leaving them
   * would have two things deciding how a cell is drawn. What stays is the box: a border so it
   * reads as a region, and a height, because a terminal is only useful at a usable size. */
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--term-ground, #0d1317);
  padding: 0.5rem;
  height: 26rem;
  overflow: hidden;
}
/* The emulator fills its host. Without this it lays out at its own idea of a size and the
 * border sits somewhere unrelated to the text. */
.term-pane .xterm { height: 100%; }
/* A focus ring that is actually visible on a dark pane, because this element takes
 * keystrokes and "am I typing into the device or the page" must never be a guess. */
.term-pane:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.term-actions { display: flex; gap: 0.6rem; margin: 0.75rem 0 0; flex-wrap: wrap; }

/* --- shell status -------------------------------------------------------------------
 *
 * Two conditions, two banners, and they must never be confused: an expired session and an
 * unreachable console look identical from a stale screen, and telling them apart is the
 * whole reason this exists. See showStatus() in app.js.
 */
/* The column that scrolls. min-height: 0 for the same reason as the nav: without it this
 * flex child refuses to shrink below its content and pushes the shell taller than the
 * viewport, which is the state the change above exists to end. */
.main-col {
  display: flex; flex-direction: column; min-width: 0; flex: 1;
  min-height: 0; overflow-y: auto;
}
.shell-status:empty { display: none; }
/* STICKY, AND WITH THE PAGE'S OWN BACKGROUND. Found by reproducing the report: the bar lived
 * inside .main-col, the same element that scrolls the page under it, so anyone already a
 * screenful down a device page when their session expired never saw it -- it rendered above
 * the fold they had scrolled past. Pinned to the top of the scrolling container it came from,
 * and opaque, so it occludes rather than lets a row of the table scroll visibly through it. */
.shell-status {
  padding: 1rem 2rem 0;
  position: sticky;
  top: 0;
  z-index: 6;
  background: var(--ground);
}
.shell-status .bar {
  border: 1px solid var(--line);
  border-left: 4px solid var(--faint);
  border-radius: 4px;
  background: var(--surface);
  padding: 0.85rem 1rem;
}
/* A TINT, NOT JUST THE ACCENT BAR. The same report: the bar was there and read as decoration,
 * the same weight as a quiet aside elsewhere on the page. A wash across the whole card is what
 * a warning looks like everywhere else the fleet uses one (files-check's refusals, the device
 * notices); this had been styled lighter than either, which is backwards for the one banner
 * that means "what you are looking at may be stale". */
.shell-status .bar.is-session { border-left-color: var(--flap); background: var(--flap-wash); }
.shell-status .bar.is-unreachable { border-left-color: var(--down); background: var(--down-wash); }
.shell-status .t {
  display: block;
  font-family: var(--mono);
  font-size: 0.85rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink);
  margin-bottom: 0.35rem;
}
.shell-status p { margin: 0 0 0.4rem; color: var(--muted); max-width: 92ch; }
.shell-status p:last-child { margin-bottom: 0; }
.shell-status code {
  font-family: var(--mono);
  font-size: 0.85em;
  background: var(--ground);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 0.1rem 0.3rem;
}
/* The sentence under the code field. Explains that the code is checked at the moment of
 * action rather than reused from login, which is why this dialog asks for one at all. */
.dialog-hint { color: var(--muted); font-size: 13px; margin: 0.4rem 0 0; max-width: 52ch; }
.mint-row { display: flex; align-items: center; gap: 0.75rem; margin-top: 0.75rem; flex-wrap: wrap; }
/* The minted token sits below the composed command rather than replacing it: somebody on
 * the bastion still needs the line, and this screen has always composed rather than hidden. */
.mint-out:empty { display: none; }
.mint-out { margin-top: 0.75rem; }
/* Sits under the Reach-it buttons. Empty until something is worth saying, because a strip
 * that is always there is a strip nobody reads, and these three things are only true
 * sometimes. */
.web-note:empty { display: none; }
.web-note { margin-top: 0.75rem; display: flex; flex-direction: column; gap: 0.6rem; }

/* --- files ---------------------------------------------------------------------------- */
.files-stand { color: var(--muted); max-width: 78ch; margin: 0 0 1.25rem; }
.files-quick { display: flex; flex-direction: column; gap: 0.5rem; margin: 0 0 1.75rem; }
/* One click each, and the reason is on the button. Five places worth looking beats a
 * directory tree for an audience that does not know the filesystem. */
.files-pick {
  display: flex; flex-direction: column; gap: 0.2rem; align-items: flex-start;
  text-align: left; width: 100%; cursor: pointer;
  background: var(--surface); border: 1px solid var(--line); border-radius: 4px;
  padding: 0.65rem 0.8rem; color: var(--ink); font: inherit;
}
.files-pick:hover { border-color: var(--accent); }
.files-pick code { font-family: var(--mono); font-size: 0.85rem; }
.files-pick span { color: var(--muted); font-size: 13.5px; }
.files-row { display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; }
.files-row .input { flex: 1 1 18rem; min-width: 0; }
.files-out:empty { display: none; }
.files-out { margin-top: 1.25rem; display: flex; flex-direction: column; gap: 0.6rem; }
/* A third condition, and a third border: the console could not find out what you may do.
 * Distinct from an expired session and from an unreachable bastion because it is neither. */
.shell-status .bar.is-unknown { border-left-color: var(--accent); }
.files-listing-head { display: flex; align-items: baseline; gap: 0.75rem; margin: 0 0 0.5rem; }
.files-listing-head code { font-family: var(--mono); font-size: 0.85rem; }
.files-listing { display: flex; flex-direction: column; gap: 2px; }
/* One level, on request. Directories first, sizes on the right, and a click on a file fills
 * the path box rather than starting a transfer over the device's own link. */
.files-entry {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  text-align: left; width: 100%; cursor: pointer; font: inherit; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line); border-radius: 3px;
  padding: 0.4rem 0.65rem;
}
.files-entry:hover { border-color: var(--accent); }
.files-entry code { font-family: var(--mono); font-size: 0.82rem; word-break: break-all; }
.files-entry span { color: var(--muted); font-size: 12.5px; white-space: nowrap; }

/* --- operator roster ------------------------------------------------------------------ */
/* Five states, and the table has to make them distinguishable at a glance rather than by
 * reading. The two that must never look alike are "CLI only", which is a complete and
 * deliberate state, and "cannot sign in", which is somebody stuck half-enrolled. */
.op-table { width: 100%; border-collapse: collapse; margin-bottom: 1.5rem; }
.op-table th {
  text-align: left; font-weight: 600; font-size: 0.85rem; color: var(--muted);
  border-bottom: 1px solid var(--line); padding: 0.4rem 0.75rem 0.4rem 0;
}
.op-table td { padding: 0.55rem 0.75rem 0.55rem 0; vertical-align: top; }
.op-table tr.op-detail td {
  padding-top: 0; padding-bottom: 0.9rem; color: var(--muted);
  border-bottom: 1px solid var(--line); max-width: 92ch;
}
.op-access { font-weight: 600; }
.op-access.is-ok { color: var(--up, inherit); }
.op-access.is-warn { color: var(--flap, inherit); }
.op-access.is-muted { color: var(--muted); }
.op-factor.is-no, .op-factor.is-unknown { color: var(--muted); }
/* Not truncated: a fingerprint exists to be read back over a second channel, and half of one
 * compares equal to nothing. It gets its own wrap rather than an ellipsis. */
.fp { font-size: 0.78rem; word-break: break-all; }

/* The bastion's own output, shown verbatim. Pre rather than a paragraph because it carries a
 * path and a filename, and re-wrapping either makes it unreadable at the moment somebody is
 * copying it. Scrolls in its own box so a long path cannot widen the page. */
.notice pre.said {
  margin: 0.4rem 0; padding: 0.5rem 0.6rem; overflow-x: auto;
  font-family: var(--mono); font-size: 0.78rem;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  white-space: pre-wrap; word-break: break-all;
}

/* --- enrolment: the fallback and the bulk log ------------------------------------------- */
/* A disclosure rather than a heading. The commands inside are what an engineer needs when this
 * console cannot reach the bastion, which is exactly when they need them most -- so they are
 * kept and reachable, and not the first thing on the page. */
.fallback {
  margin: 1.5rem 0; padding: 0.75rem 1rem;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface);
}
.fallback > summary { cursor: pointer; font-weight: 600; }
.fallback > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.fallback .field-label { margin-top: 0.75rem; }

/* One row per device while a batch runs. Scrolls in its own box: three hundred rows must not
 * make the page the thing that scrolls, and the download button has to stay reachable. */
.bulk-log { max-height: 20rem; overflow-y: auto; margin-top: 0.75rem; }
.bulk-row { display: flex; gap: 0.75rem; align-items: center; padding: 0.25rem 0; min-width: 0; }
.bulk-row > code { flex: 0 0 12rem; font-size: 0.78rem; word-break: break-all; }
.bulk-row > .cmd { flex: 1; min-width: 0; }
.bulk-row.is-bad > code { color: var(--down, inherit); }

/* --- operator creation: the QR beside its own text ------------------------------------- */
/* Side by side, and the text is not a fallback tucked underneath. There is no independent QR
 * decoder available to verify our encoder against, so a phone that will not scan must never be
 * a dead end -- the secret in readable groups is the guarantee and the QR is the convenience. */
.qr-box { display: flex; gap: 1.25rem; flex-wrap: wrap; align-items: flex-start; margin: 0.75rem 0; }
.qr { background: #fff; padding: 0.5rem; border: 1px solid var(--line); border-radius: var(--radius); }
.qr svg { display: block; width: 220px; height: 220px; }
.qr-text { min-width: 0; flex: 1; max-width: 44ch; }
.secret {
  display: block; font-family: var(--mono); font-size: 0.95rem; letter-spacing: 0.05em;
  padding: 0.5rem 0.6rem; margin: 0.35rem 0; word-break: break-all;
  background: var(--surface); border: 1px solid var(--line-firm); border-radius: var(--radius);
}

/* --- tag editing ------------------------------------------------------------------------ */
.tag-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.5rem; min-height: 1.6rem; }

/* The id beneath a name. Not decoration: it is what the bastion, the audit trail and
 * `wormhole ssh` use, so it stays on screen where somebody reads a name and then opens a
 * terminal. */
.page-sub { font-family: var(--mono); font-size: 0.8rem; color: var(--muted); margin-top: 0.15rem; }

/* The proxied device UI, in a sandboxed frame. Given real height because a device interface in
 * a 200px box is unusable in a different way from one that is isolated. */
.device-ui {
  display: block; width: 100%; height: 32rem; margin-top: 0.75rem;
  border: 1px solid var(--line-firm); border-radius: var(--radius); background: #fff;
}
