/* SOC console.
 *
 * Every value here comes from MASTER_DESIGN_TOKENS. Nothing is chosen locally,
 * because the console and the marketing site are the same company and an
 * operator who has memorised a green must see that exact green in both.
 *
 * Fonts are served from this origin rather than fetched from Google. The
 * console's CSP is `default-src 'none'`, and widening it on the page that
 * authenticates people, to fetch a typeface, would be a poor trade.
 */

@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/fonts/manrope-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/fonts/manrope-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 500 700;
  font-display: swap;
  src: url('/fonts/spacegrotesk-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 500 700;
  font-display: swap;
  src: url('/fonts/spacegrotesk-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  /* Status semantics. Identical in both themes, deliberately: an operator
     memorises the colour, not the label, so a hue that shifts with the theme
     would mean two different things at two times of day. */
  --healthy: #3ddc84;
  --attention: #f4c153;
  --degraded: #ff9f45;
  --down: #ff5d5d;

  /* Reserved. The pipeline emits GREEN, YELLOW and RED only, so --degraded has
     no source yet. It is defined so the palette matches the design system, and
     it must stay unused until a fourth severity actually exists -- a status
     colour with no status behind it is decoration, which is the one thing
     colour is not allowed to be here. */

  --brand: #6f36f6;
  --brand-2: #9b5cff;

  --bg-0: #080a0f;
  --bg-1: #0e1018;
  --bg-2: #151929;
  --surface: rgba(16, 19, 31, 0.84);
  --surface-strong: #121628;
  --line: rgba(255, 255, 255, 0.1);
  --text: #e8eafd;
  --muted: #a3a8c6;
  --header-bg: rgba(8, 10, 15, 0.72);
  --chip-bg: rgba(255, 255, 255, 0.03);
  --chip-text: #ced3f0;

  --radius: 18px;
  --radius-sm: 12px;
  --radius-xs: 10px;
  --gap: 12px;

  /* The height of one row in the windowed monitor list.

     A token rather than a number in the stylesheet, because the JavaScript that
     positions those rows has to read it: a windowed list places rows by
     arithmetic, so the two have to agree, and they change together when the row
     goes from one line to two on a narrow screen. */
  --vrow-h: 40px;

  /* What the shell takes off the viewport before the list gets what is left.
     Stated once because three different rules need the same answer and a
     mobile header is a different height from a desktop one. */
  --chrome-h: 280px;

  /* Console-local aliases.
     The data model speaks GREEN/YELLOW/RED; the design system speaks
     healthy/attention/down. These names let the two vocabularies meet in one
     place instead of at every call site. */
  --bg: var(--bg-0);
  --panel: var(--surface-strong);
  --panel-2: var(--bg-2);
  --green: var(--healthy);
  --yellow: var(--attention);
  --red: var(--down);
  --blue: var(--brand-2);

  /* Tinted fills. Derived from the status hues rather than picked, so they
     cannot drift away from the colour they are meant to support. */
  --green-dim: rgba(61, 220, 132, 0.12);
  --yellow-dim: rgba(244, 193, 83, 0.12);
  --red-dim: rgba(255, 93, 93, 0.13);

  --shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
  --speed: 160ms;
}

/* Light mode. Only the surface and text tokens move; the four status hues above
   are untouched, which is the whole point of keeping them in a separate block. */
:root[data-theme='light'] {
  --bg-0: #f7f8ff;
  --bg-1: #ffffff;
  --bg-2: #eef0fd;
  --surface: rgba(255, 255, 255, 0.92);
  --surface-strong: #ffffff;
  --line: rgba(46, 54, 93, 0.16);
  --text: #171a2d;
  --muted: #5e6686;
  --header-bg: rgba(255, 255, 255, 0.78);
  --chip-bg: rgba(111, 54, 246, 0.08);
  --chip-text: #2e3571;
  --brand-2: #8f52ff;

  --bg: var(--bg-0);
  --panel: var(--bg-1);
  --panel-2: var(--bg-2);
  --blue: var(--brand);

  --green-dim: rgba(61, 220, 132, 0.16);
  --yellow-dim: rgba(244, 193, 83, 0.2);
  --red-dim: rgba(255, 93, 93, 0.14);

  --shadow: 0 18px 40px rgba(46, 54, 93, 0.12);
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  height: 100%;
}

html {
  /* iOS enlarges text when a page is rotated into landscape, on the assumption
     that it was laid out for a desktop. This one is laid out for the screen it
     is on, so the browser inflating it afterwards only breaks the fit. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  background: var(--bg);
  color: var(--text);
  font: 400 14px/1.6 'Manrope', ui-sans-serif, system-ui, -apple-system, 'Segoe UI',
    Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3,
.brand-word,
.counter .n,
.match-code {
  font-family: 'Space Grotesk', 'Manrope', ui-sans-serif, system-ui, sans-serif;
}

/* Motion policy: transitions exist to keep a change comprehensible, never to
   decorate. Anything that would still be moving after a fifth of a second is
   out of scope for an operations screen. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

button,
a.button {
  font: inherit;
  font-weight: 600;
  color: inherit;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 8px 14px;
  cursor: pointer;
  transition: border-color var(--speed) ease, background var(--speed) ease,
    transform var(--speed) ease;
}

a.button {
  display: inline-block;
  text-decoration: none;
}

button:hover:not(:disabled),
a.button:hover {
  border-color: var(--brand-2);
  background: var(--chip-bg);
}

button:disabled {
  opacity: 0.5;
  cursor: default;
}

button.primary,
a.button.primary {
  /* The brand gradient, matching the marketing site's primary button. It is the
     only gradient in the console, and it never appears on anything that carries
     a status meaning. */
  background: linear-gradient(135deg, var(--brand), var(--brand-2));
  border-color: transparent;
  color: #fff;
  font-weight: 700;
}

button.primary:hover:not(:disabled),
a.button.primary:hover {
  background: linear-gradient(135deg, var(--brand-2), var(--brand));
  border-color: transparent;
  transform: translateY(-1px);
}

button.ghost {
  background: none;
}

input,
select,
textarea {
  font: inherit;
  color: inherit;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 9px 12px;
  transition: border-color var(--speed) ease;
}

input:hover,
select:hover {
  border-color: var(--brand-2);
}

/* :focus-visible, so a mouse click does not leave a ring behind, but every
   keyboard path still shows exactly where it is. */
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
button:focus-visible,
a:focus-visible {
  outline: 2px solid var(--brand-2);
  outline-offset: 2px;
}

.hidden {
  display: none !important;
}

/* Available to a screen reader, absent from the screen. Used for the labels a
   sighted operator reads from position and colour. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------- login --- */

.login-wrap {
  display: grid;
  place-items: center;
  /* `dvh`, with `vh` underneath it for browsers that do not have it.

     On a phone `100vh` is the height of the viewport with the browser's
     toolbars hidden, which is not the height the page is given while they are
     showing. The card is centred in a box taller than the screen, so it sits
     low, and the bottom of it -- the recovery link and the theme control -- is
     under the toolbar until the operator scrolls a page that does not look like
     it scrolls. `dvh` is the height that is actually available right now. */
  min-height: 100vh;
  min-height: 100dvh;
  padding: 24px;
  /* Under the notch and the home indicator on a phone, which `viewport-fit=cover`
     has let the page paint into. */
  padding-block-start: max(24px, env(safe-area-inset-top));
  padding-block-end: max(24px, env(safe-area-inset-bottom));
  padding-inline-start: max(24px, env(safe-area-inset-left));
  padding-inline-end: max(24px, env(safe-area-inset-right));
  position: relative;
  overflow: hidden;
}

/* A single soft brand glow behind the card.
   Static, not animated: this is the screen someone opens at 03:00 because
   something is broken, and a moving background is the opposite of what that
   moment needs. It exists to make the page feel like the product rather than a
   default form on a black rectangle. */
.login-wrap::before {
  content: '';
  position: absolute;
  inset: -30%;
  background: radial-gradient(
      42rem 26rem at 50% 18%,
      rgba(111, 54, 246, 0.22),
      transparent 70%
    ),
    radial-gradient(30rem 22rem at 78% 82%, rgba(155, 92, 255, 0.12), transparent 70%);
  pointer-events: none;
}

.login {
  position: relative;
  width: 100%;
  max-width: 420px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 32px;
  box-shadow: var(--shadow);
}

/* A hairline of brand along the top edge of the card. */
.login::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 2px;
  border-radius: var(--radius) var(--radius) 0 0;
  background: linear-gradient(90deg, transparent, var(--brand-2), transparent);
}

.login-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 18px;
}

.login-brand img {
  width: 34px;
  height: 34px;
  border-radius: var(--radius-xs);
}

.brand-word {
  font-weight: 700;
  letter-spacing: 0.14em;
  font-size: 13px;
}

.brand-sub {
  display: block;
  font-family: 'Manrope', sans-serif;
  font-weight: 600;
  letter-spacing: 0.02em;
  font-size: 11px;
  color: var(--muted);
}

.login h1 {
  margin: 0 0 6px;
  font-size: 24px;
  line-height: 1.2;
}

.login p.sub {
  margin: 0 0 20px;
  color: var(--muted);
  font-size: 13px;
}

/* The small capsule naming the one way in. Sets the expectation before the
   operator reaches for a password field that is not there. */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  background: var(--chip-bg);
  border: 1px solid var(--line);
  color: var(--chip-text);
  border-radius: 999px;
  padding: 5px 12px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-bottom: 14px;
}

.eyebrow::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--brand-2);
}

.field {
  display: block;
  margin-bottom: 14px;
}

.field span {
  display: block;
  margin-bottom: 6px;
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
}

.field input {
  width: 100%;
}

/* A recovery code is read off paper. Monospaced and spaced out so a mistyped
   character is visible before it is submitted. */
#recovery-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.notice {
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  font-size: 13px;
  margin-bottom: 14px;
  border-inline-start-width: 3px;
  border-inline-start-style: solid;
}

.notice.error {
  background: var(--red-dim);
  border: 1px solid var(--down);
  border-inline-start-width: 3px;
}

.notice.info {
  background: var(--chip-bg);
  border: 1px solid var(--line);
  border-inline-start-color: var(--brand-2);
  color: var(--muted);
}

.login-actions {
  display: flex;
  gap: 8px;
  align-items: center;
}

.login-actions .primary {
  flex: 1;
  text-align: center;
}

/* A class rather than a style attribute.
   The CSP is `style-src 'self'` with no unsafe-inline, which blocks inline
   style attributes as well as <style> blocks -- so a margin written that way
   is silently dropped and the page looks subtly wrong with nothing in the
   console to explain it. */
.login-actions-spaced {
  margin-top: 14px;
}

/* "Leave this tab open."
   Given real weight because the instruction is load-bearing: the tab is what
   collects the session once Telegram says yes, and an operator who closes it
   has to start over without being told why. */
.keep-open {
  margin-top: 12px;
  padding: 10px 12px;
  border-radius: var(--radius-xs);
  border: 1px solid var(--line);
  background: var(--chip-bg);
  color: var(--chip-text);
}

/* The step separator, so the QR reads as an alternative rather than a further
   requirement. */
.or {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 20px 0 14px;
  color: var(--muted);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.or::before,
.or::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--line);
}

/* The code the operator has to match in Telegram. Deliberately the largest
   thing on the screen: it is the one step that must not be skimmed. */
.match-code {
  font-size: 52px;
  font-weight: 700;
  letter-spacing: 10px;
  text-indent: 10px;
  text-align: center;
  margin: 6px 0 4px;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

/* The remaining time, drawn rather than only written.
   A number counting down is read; a shrinking bar is noticed. The request lives
   120 seconds and an operator who misses that restarts for no reason. */
.expiry {
  height: 4px;
  border-radius: 999px;
  background: var(--chip-bg);
  overflow: hidden;
  margin: 14px 0 6px;
}

.expiry-fill {
  height: 100%;
  width: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--brand), var(--brand-2));
  transform-origin: left center;
  transition: transform 1s linear, background 400ms ease;
}

/* Under fifteen seconds it stops being brand-coloured and starts being a
   status, because by then it is one. */
.expiry-fill.urgent {
  background: var(--attention);
}

/* A white plate behind the QR, so it still scans on a dark console. */
.login-qr {
  display: block;
  width: 168px;
  height: 168px;
  margin: 6px auto;
  background: #fff;
  padding: 10px;
  border-radius: var(--radius-sm);
}

.link-button {
  background: none;
  border: none;
  color: var(--brand-2);
  padding: 0;
  text-decoration: underline;
  font-size: 13px;
  font-weight: 600;
}

.link-button:hover:not(:disabled) {
  background: none;
  border-color: transparent;
  color: var(--brand);
}

/* Footer of the login card: back to the public site, and the theme control. */
.login-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 22px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
  font-size: 12px;
}

.login-foot a {
  color: var(--muted);
  text-decoration: none;
  font-weight: 600;
}

.login-foot a:hover {
  color: var(--text);
}

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

.app {
  display: grid;
  grid-template-rows: auto auto 1fr;
  /* One column, and explicitly allowed to be narrower than its contents.

     An implicit `auto` column is sized to the largest thing in it, so a single
     panel that insists on 326 pixels does not overflow its own box -- it widens
     the column, which stretches the header and the banner with it and gives the
     whole console a horizontal scrollbar. The page then slides sideways under
     a thumb while it is being read, and the reason is nowhere near the symptom.
     `minmax(0, 1fr)` pins the column to the viewport and leaves anything too
     wide to deal with that itself, where it can be seen and fixed. */
  grid-template-columns: minmax(0, 1fr);
  height: 100vh;
  height: 100dvh;
}

.topbar {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 0 18px;
  padding-inline-start: max(18px, env(safe-area-inset-left));
  padding-inline-end: max(18px, env(safe-area-inset-right));
  height: 60px;
  background: var(--header-bg);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line);
}

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-decoration: none;
  color: inherit;
}

.brand img {
  width: 28px;
  height: 28px;
  border-radius: 8px;
}

.tabs {
  display: flex;
  gap: 4px;
  margin-inline-start: 10px;
}

.tabs button {
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius-xs);
  color: var(--muted);
  font-weight: 600;
}

.tabs button:hover:not(:disabled) {
  color: var(--text);
  background: var(--chip-bg);
  border-color: transparent;
}

.tabs button[aria-current='page'] {
  background: var(--chip-bg);
  border-color: var(--line);
  color: var(--text);
}

.topbar .spacer {
  flex: 1;
}

.who {
  color: var(--muted);
  font-size: 12px;
  font-weight: 600;
}

/* The connection light.
 *
 * The pulse is not decoration. A static green dot proves nothing -- a frozen
 * page shows one too. Something that is still moving is the only evidence, on
 * a screen nobody is touching, that this tab is alive and connected. It is
 * therefore the one animation that runs continuously, and reduced-motion stops
 * it like everything else, which is why the colour still carries the state on
 * its own. */
.live {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
  background: var(--chip-bg);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 5px 12px;
}

.live-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--muted);
  flex: none;
  position: relative;
}

.live-dot.on {
  background: var(--healthy);
}

.live-dot.on::after {
  content: '';
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  border: 1px solid var(--healthy);
  animation: ping 2s ease-out infinite;
}

.live-dot.off {
  background: var(--down);
}

@keyframes ping {
  0% {
    transform: scale(0.6);
    opacity: 0.9;
  }
  70%,
  100% {
    transform: scale(1.5);
    opacity: 0;
  }
}

/* ---------------------------------------------------------- trust banner --- */

.banner {
  padding: 11px 18px;
  font-weight: 600;
  font-size: 13px;
  background: var(--red-dim);
  border-bottom: 1px solid var(--down);
  color: var(--text);
}

.banner.warn {
  background: var(--yellow-dim);
  border-bottom-color: var(--attention);
}

.banner-lead {
  margin-bottom: 4px;
}

/* The reasons, one per line. Lighter than the lead so the eye reaches the
   warning first and the diagnosis second. */
.banner-detail {
  margin: 0;
  padding-inline-start: 18px;
  font-weight: 500;
  opacity: 0.85;
}

.banner-detail:empty {
  display: none;
}

.view {
  overflow: auto;
  /* A list that has reached its end must not hand the gesture to the page
     behind it. On a phone that gesture is pull-to-refresh, and reloading the
     console mid-incident because a scroll overshot is not a behaviour anyone
     asked for. */
  overscroll-behavior-y: contain;
  -webkit-overflow-scrolling: touch;
  padding: 16px;
  padding-inline-start: max(16px, env(safe-area-inset-left));
  padding-inline-end: max(16px, env(safe-area-inset-right));
  padding-block-end: calc(16px + env(safe-area-inset-bottom));
}

h2.section {
  margin: 0 0 14px;
  font-size: 17px;
}

.muted {
  color: var(--muted);
}

.crumb {
  background: none;
  border: none;
  color: var(--brand-2);
  padding: 0;
  margin-bottom: 12px;
  font-size: 13px;
  font-weight: 600;
}

.crumb:hover:not(:disabled) {
  background: none;
  border-color: transparent;
  text-decoration: underline;
}

/* -------------------------------------------------------------- counters --- */

.counters {
  display: flex;
  gap: var(--gap);
  margin-bottom: 20px;
  flex-wrap: wrap;
}

.counter {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 14px 20px;
  min-width: 120px;
  overflow: hidden;
}

/* The status hue as a bar down the left edge rather than a coloured border on
   all four sides: it reads as a label on the number, not as an alarm around
   the whole tile. */
.counter::before {
  content: '';
  position: absolute;
  inset-inline-start: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background: var(--muted);
}

.counter .n {
  font-size: 30px;
  font-weight: 700;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}

.counter .k {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}

.counter.green::before {
  background: var(--healthy);
}
.counter.yellow::before {
  background: var(--attention);
}
.counter.red::before {
  background: var(--down);
}

/* A red count that is not zero is the reason someone opened this page. */
.counter.red .n {
  color: var(--down);
}

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

.cards {
  display: grid;
  /* `min(260px, 100%)` rather than a bare 260px. A grid track with a fixed
     minimum does not shrink below it -- it overflows the container instead --
     so on a 320px screen the cards pushed the whole page sideways and every
     screen behind them inherited a horizontal scrollbar. */
  grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr));
  gap: var(--gap);
}

.card {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 16px;
  text-align: start;
  cursor: pointer;
  width: 100%;
  overflow: hidden;
  transition: border-color var(--speed) ease, transform var(--speed) ease,
    box-shadow var(--speed) ease;
}

.card:hover {
  border-color: var(--brand-2);
  transform: translateY(-2px);
  box-shadow: var(--shadow);
}

.card::before {
  content: '';
  position: absolute;
  inset-inline-start: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background: transparent;
}

.card.red::before {
  background: var(--down);
}
.card.yellow::before {
  background: var(--attention);
}
.card.green::before {
  background: var(--healthy);
}

.card h3 {
  margin: 0 0 10px;
  font-size: 15px;
}

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

.pill {
  font-size: 11px;
  font-weight: 700;
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid var(--line);
  font-variant-numeric: tabular-nums;
}

.pill.green {
  background: var(--green-dim);
  border-color: var(--healthy);
  color: var(--healthy);
}
.pill.yellow {
  background: var(--yellow-dim);
  border-color: var(--attention);
  color: var(--attention);
}
.pill.red {
  background: var(--red-dim);
  border-color: var(--down);
  color: var(--down);
}

.card .outage {
  margin-top: 10px;
  color: var(--down);
  font-weight: 700;
  font-size: 12px;
}

/* ------------------------------------------------------ virtualised list --- */

/* Only the rows in view exist in the DOM. Five thousand monitors is a
 * requirement, and five thousand nodes is a page that scrolls badly on the
 * hardware an operations room actually has. */
.vlist {
  position: relative;
  overflow: auto;
  overscroll-behavior-y: contain;
  -webkit-overflow-scrolling: touch;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel);
  height: calc(100vh - var(--chrome-h));
  height: calc(100dvh - var(--chrome-h));
  min-height: 240px;
}

.vlist-sizer {
  position: relative;
  width: 100%;
}

.vrow {
  position: absolute;
  left: 0;
  right: 0;
  display: grid;
  grid-template-columns: 4px minmax(0, 1fr) auto 168px 110px 96px;
  align-items: center;
  gap: 10px;
  padding-inline-end: 12px;
  /* The token, not a repeat of it.

     This was `40px` while `--vrow-h` was also 40px, which is the same number
     written twice and therefore two numbers. The windowed list reads the token
     to decide where to put each row; had the two ever drifted, rows would have
     been positioned by one value and drawn at another. They could not even be
     caught drifting, because the list also writes an inline height, so whatever
     this rule said was overridden and the result looked consistent while being
     wrong. */
  height: var(--vrow-h);
  border-bottom: 1px solid var(--line);
  background: none;
  border-left: none;
  border-right: none;
  border-top: none;
  border-radius: 0;
  text-align: start;
  cursor: pointer;
}

.vrow:hover {
  background: var(--chip-bg);
}

.vrow .bar {
  height: 100%;
}
.vrow .bar.green {
  background: var(--healthy);
}
.vrow .bar.yellow {
  background: var(--attention);
}
.vrow .bar.red {
  background: var(--down);
}

.vrow .name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.vrow .col {
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* The severity chip and, only when it is not `valid`, the data state that
   explains it. Two facts in one column because they are read together: RED
   because it broke and RED because it went quiet need different responses. */
.vrow .sev-cell {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.vrow .sev-cell .data-state {
  font-size: 11px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Integration faults, not incidents. Deliberately quiet: they must be findable
   at a glance down the column without competing with the severity bar. */
.vrow .flags {
  display: flex;
  gap: 6px;
  align-items: center;
}

.flag {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  padding: 2px 7px;
  border-radius: 999px;
  white-space: nowrap;
  border: 1px solid var(--line);
  color: var(--muted);
  background: var(--chip-bg);
}

.flag.never {
  color: var(--attention);
  border-color: color-mix(in srgb, var(--attention) 45%, transparent);
}

.section-head {
  position: absolute;
  left: 0;
  right: 0;
  /* The same token as a row, for the same reason: the windowed list gives every
     element it positions -- headings included -- one height, so a different
     value here would be a number that never takes effect and quietly disagrees
     with the layout. */
  height: var(--vrow-h);
  display: flex;
  align-items: center;
  padding: 0 12px;
  background: var(--panel-2);
  border-bottom: 1px solid var(--line);
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}

/* ----------------------------------------------------------------- queue --- */

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

table.queue {
  width: 100%;
  border-collapse: collapse;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

table.queue th,
table.queue td {
  text-align: start;
  padding: 9px 12px;
  border-bottom: 1px solid var(--line);
  font-size: 13px;
}

table.queue th {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  background: var(--panel-2);
}

table.queue tbody tr {
  transition: background var(--speed) ease;
}

table.queue tbody tr:hover {
  background: var(--chip-bg);
}

table.queue tr:last-child td {
  border-bottom: none;
}

.sev {
  font-weight: 700;
  font-size: 11px;
  padding: 3px 9px;
  border-radius: 999px;
  letter-spacing: 0.04em;
}
.sev.RED {
  background: var(--red-dim);
  color: var(--down);
  border: 1px solid var(--down);
}
.sev.YELLOW {
  background: var(--yellow-dim);
  color: var(--attention);
  border: 1px solid var(--attention);
}
.sev.GREEN {
  background: var(--green-dim);
  color: var(--healthy);
  border: 1px solid var(--healthy);
}

.row-actions {
  display: flex;
  gap: 6px;
}

.pager {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-top: 12px;
}

/* ----------------------------------------------------------------- sweep --- */

/* Deliberately quiet when healthy. This panel is on screen at all times, and a
   green box shouting for attention beside the customer cards would compete with
   the colours that actually mean something. It has to be readable when somebody
   goes looking, not loud when nothing is wrong. */
.sweep {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 0 0 var(--gap);
  padding: 10px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel);
  font-size: 12px;
}

.sweep-head {
  display: flex;
  align-items: center;
  gap: 8px;
}

.sweep-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: none;
}

.sweep-ok .sweep-dot {
  background: var(--healthy);
}

.sweep-bad .sweep-dot {
  background: var(--down);
}

.sweep-bad {
  border-color: color-mix(in srgb, var(--down) 45%, transparent);
  background: var(--red-dim);
}

.sweep-verdict {
  color: var(--muted);
}

.sweep-bad .sweep-verdict {
  color: var(--down);
  font-weight: 700;
}

.sweep-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.sweep-late {
  color: var(--down);
}

.sweep-affected {
  color: var(--muted);
}

/* ---------------------------------------------------------------- detail --- */

/* `align-items: start` so a short panel stops stretching to match the tallest
   one in its row. Without it the status panel grew to the height of the reason
   list beside it and left a large empty field under its five rows, which read
   as a screen that had failed to load rather than one that was simply brief. */
.detail-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}

.detail-head .section {
  margin: 0;
}

.link-copy {
  font-size: 12px;
  padding: 4px 12px;
  white-space: nowrap;
}

/* The identifier people type into the contract, and quote to each other. Kept
   LTR and monospaced because it is a key, not prose. */
.monitor-id {
  margin: 2px 0 14px;
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 12px;
  color: var(--muted);
}

.detail-grid {
  display: grid;
  /* See `.cards`: a fixed track minimum overflows rather than shrinks. */
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
  align-items: start;
  gap: var(--gap);
}

/* Metrics carry three columns of their own and are the densest thing on the
   screen, so they get the full width rather than a third of it. */
.panel.wide {
  grid-column: 1 / -1;
}

.panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 16px;
}

.panel h3 {
  margin: 0 0 12px;
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}

.kv {
  display: grid;
  grid-template-columns: minmax(120px, auto) 1fr;
  gap: 6px 14px;
  font-size: 13px;
}

.kv dt {
  color: var(--muted);
}

.kv dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
}

ul.reasons {
  margin: 0;
  padding-inline-start: 18px;
}

/* --------------------------------------------------------------- metrics --- */

table.metrics {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

table.metrics th {
  text-align: start;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  padding: 0 10px 6px;
  border-bottom: 1px solid var(--line);
}

table.metrics td {
  padding: 8px 10px;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}

table.metrics tr:last-child td {
  border-bottom: none;
}

table.metrics td.k {
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 12px;
}

/* The human name under the key, when the customer supplied one. */
table.metrics td.k .label {
  display: block;
  font-family: inherit;
  font-size: 12px;
  color: var(--muted);
}

table.metrics td.v {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

table.metrics td.th {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
}

/* Coloured to match the severity each threshold would produce, so the eye can
   pair "> 90" with the yellow it causes without reading the number twice. */
.thr {
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--line);
  white-space: nowrap;
}

.thr.YELLOW {
  color: var(--attention);
  border-color: color-mix(in srgb, var(--attention) 45%, transparent);
  background: var(--yellow-dim);
}

.thr.RED {
  color: var(--down);
  border-color: color-mix(in srgb, var(--down) 45%, transparent);
  background: var(--red-dim);
}

/* A metric that cannot change colour is still worth showing -- it is the one
   somebody believed was being watched -- but it must not read as live. */
table.metrics tr.inert-row td.k,
table.metrics tr.inert-row td.v {
  opacity: 0.62;
}

/* ------------------------------------------------------------ provenance --- */

/* Permanent, on every monitor, and not conditional on anything. Every number
   on this screen was measured and sent by the customer's own agent from inside
   their own network; BotFlow makes no request to any customer service. An
   operator who assumes otherwise will read a green edge check as proof the
   site is reachable from the outside, which it is not, and that misreading is
   only possible because the screen used to stay silent about where its data
   came from. */
.provenance {
  margin: 18px 0 0;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--chip-bg);
  font-size: 12px;
  line-height: 1.6;
  color: var(--muted);
}

.provenance strong {
  color: var(--text);
}

ul.reasons li {
  margin-bottom: 6px;
}

/* The one-line answer to "why is this red", above the numbers that justify it. */
.summary {
  margin: 0 0 12px;
  color: var(--text);
  font-size: 14px;
  line-height: 1.5;
}

/* What the customer said to do about it. Indented under the reason it belongs
   to, so a monitor with several reasons does not turn into one flat list of
   instructions with no way to tell which applies to what. */
ul.actions {
  margin: 6px 0 0;
  padding-inline-start: 18px;
  color: var(--muted);
  font-size: 13px;
}

.toast {
  position: fixed;
  inset-inline-end: 18px;
  bottom: calc(18px + env(safe-area-inset-bottom));
  background: var(--panel);
  border: 1px solid var(--line);
  border-inline-start: 3px solid var(--brand-2);
  border-radius: var(--radius-sm);
  padding: 12px 16px;
  /* Bounded by the screen as well as by taste. A fixed 380px is wider than a
     small phone, and a toast that overflows the viewport drags the page
     sideways to show a message about something that already happened. */
  max-width: min(380px, calc(100vw - 36px));
  box-shadow: var(--shadow);
  animation: toast-in 180ms ease-out;
}

.toast.error {
  border-inline-start-color: var(--down);
}

/* Enters from where it will sit, so the eye is led to it rather than startled
   by something that was suddenly there. */
@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* ------------------------------------------------------------- skeleton --- */

/* Shown while the first snapshot is in flight.
   An empty console and a loading console look identical otherwise, and on this
   product the difference between "nothing is wrong" and "nothing has arrived"
   is the entire point. */
.skeleton {
  border-radius: var(--radius-sm);
  background: linear-gradient(
    90deg,
    var(--panel) 0%,
    var(--panel-2) 50%,
    var(--panel) 100%
  );
  background-size: 200% 100%;
  animation: shimmer 1.4s ease-in-out infinite;
}

.skeleton-counter {
  height: 74px;
  min-width: 120px;
  flex: 1;
}

.skeleton-card {
  height: 116px;
}

@keyframes shimmer {
  from {
    background-position: 200% 0;
  }
  to {
    background-position: -200% 0;
  }
}

/* An empty state that says which of the two empties this is. */
.empty {
  text-align: center;
  padding: 40px 20px;
  color: var(--muted);
  border: 1px dashed var(--line);
  border-radius: var(--radius-sm);
}

/* The card grid would otherwise squeeze this into one column's width, so a
   message about the whole estate would read as a comment on one card. */
.cards > .empty {
  grid-column: 1 / -1;
}

/* --------------------------------------------------------------- bidi ---
   The console is Hebrew and right-to-left, but the things it is reporting on
   are not. Addresses, ports, monitor ids, durations and version strings are
   left-to-right runs made largely of digits and punctuation, and the Unicode
   bidirectional algorithm treats punctuation as neutral: dropped into an RTL
   paragraph, "10.0.0.5:8080" is entitled to come out as "8080:10.0.0.5", and a
   trailing colon or minus sign can migrate to the other end of the value.

   That is not a cosmetic defect on a screen whose only job is to tell an
   operator which machine is broken. It is a wrong answer presented with the
   same confidence as a right one, at the moment it matters most.

   `isolate` is the important half. `direction: ltr` alone fixes the order
   inside the span but still lets the value participate in the surrounding
   paragraph's ordering; isolation makes it a single opaque unit that the
   Hebrew around it cannot reach into. */
.ltr {
  direction: ltr;
  unicode-bidi: isolate;
}

/* Customer-authored text -- monitor names, section keys, incident reasons --
   is marked `dir="auto"` in the DOM so the browser picks direction per value
   from its first strong character. A Hebrew estate can still contain an
   English monitor name, and neither should be forced to inherit the other's
   direction. Isolation keeps that decision from leaking into the row. */
[dir='auto'] {
  unicode-bidi: isolate;
}

/* The sign-in card is served to anyone who reaches the address, so it stays
   English and left-to-right even while the document is in Hebrew for the
   console behind it. */
#login {
  direction: ltr;
  text-align: start;
}

/* ---------------------------------------------------------------- admin --- */

/* The rejection detail, hung under the customer it belongs to.
   A separate panel would force an operator to match a reason back to a row by
   name, which is exactly the step that gets done wrong at 3am. */
tr.rejection td {
  background: var(--panel-2);
  border-top: 0;
  padding-top: 0;
  font-size: 12px;
}

tr.rejection ul.reasons {
  margin: 6px 0 0;
  color: var(--muted);
}

.field small {
  display: block;
  margin-top: 4px;
  font-size: 11px;
  font-weight: 400;
}

.guide-dialog {
  width: min(860px, 92vw);
  max-height: 86vh;
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 20px;
  box-shadow: var(--shadow);
}

.guide-dialog::backdrop {
  background: rgb(0 0 0 / 55%);
}

.guide-dialog h3 {
  margin: 0 0 12px;
  font-size: 15px;
}

/* The deletion dialog.

   Red on the border and the heading, not on the whole panel. A dialog with a
   red background is one whose text is harder to read, and the text is the part
   that has to be read -- it is the list of what will stop existing. The colour
   is here to stop the window being mistaken for any other dialog in the
   console, which is a job the frame does as well as a fill would. */
.danger-dialog {
  border-color: var(--down);
  border-width: 2px;
}

.danger-title {
  color: var(--down);
}

.danger-heading {
  margin: 14px 0 6px;
  font-weight: 700;
  font-size: 13px;
}

.danger-list {
  margin: 0 0 6px;
  padding-inline-start: 20px;
  line-height: 1.7;
}

.danger-list li {
  margin-bottom: 2px;
}

/* The one irreversible control in the console.

   Filled rather than outlined, and it is the only filled red in the interface.
   `.primary` is the brand gradient and appears on every screen; a destructive
   button that borrowed it would be the same shape and weight as "create". */
button.destroy {
  background: var(--down);
  border-color: var(--down);
  color: #fff;
}

button.destroy:hover:not(:disabled) {
  filter: brightness(1.08);
}

/* Disabled until the operator has typed the customer id. `button:disabled`
   above already fades it; what this adds is that it stays red while faded, so
   it reads as a control being withheld rather than one that is not there. */
button.destroy:disabled {
  background: var(--down);
  border-color: var(--down);
  color: #fff;
}

/* The guide is Markdown meant to be pasted elsewhere as Markdown, so it is
   shown as text. `pre-wrap` rather than `pre`: the prose is Hebrew and would
   otherwise run off the edge of a dialog nobody can scroll sideways. */
pre.guide {
  max-height: 58vh;
  overflow: auto;
  margin: 0 0 14px;
  padding: 14px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  line-height: 1.6;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  text-align: start;
}

/* The credential block, above the guide in the same dialog.

   Boxed and outlined so it does not read as another paragraph of the guide.
   The operator sees the password once -- the server never stores it -- and a
   one-time value that looks like body text is one that gets scrolled past. */
.access {
  margin: 0 0 16px;
  padding: 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--bg);
}

.access > p {
  margin: 0 0 10px;
}

.access .field {
  margin: 0 0 10px;
}

/* Selectable and wrapping. A connection string is long, and a value the
   operator must copy by hand when the clipboard is unavailable has to be
   reachable with a mouse -- `pre` alone would push it off the edge. */
pre.credential {
  margin: 4px 0 0;
  padding: 10px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  user-select: all;
  text-align: start;
}

/* A checkbox is a control, not a text field: it must not be stretched to the
   full width the `.field` label gives an <input type="text">. */
.field.checkbox input {
  width: auto;
  margin-inline-end: 6px;
  vertical-align: middle;
}

/* The document checker. Shares `pre.guide`'s type so a document reads the same
   whether it is being pasted in or copied out; `textarea` needs the box
   properties `pre` gets from the browser. */
textarea.guide {
  display: block;
  width: 100%;
  box-sizing: border-box;
  resize: vertical;
  direction: ltr;
}

.check-result {
  margin-top: 14px;
  max-height: 40vh;
  overflow: auto;
}

.check-result > p {
  margin: 0 0 10px;
}

/* "· ללא סף", trailing a metric key that cannot change colour.

   Deliberately quiet rather than a warning colour. It is a statement about the
   contract the customer wrote, not about the health of the thing being
   measured, and colouring it would put a second, competing severity on a panel
   whose whole job is to explain one. */
.inert {
  margin-inline-start: 6px;
  font-size: 11px;
}

/* "מעולם לא נצפה תקין", under the status panel.

   Marked, but not with a severity colour. The monitor already carries one, and
   a second colour beside it would read as a second, worse verdict when it is
   actually a note about how to read the first. */
.never-healthy {
  margin: 12px 0 0;
  padding: 8px 10px;
  border-inline-start: 3px solid var(--warn, #c99a2e);
  background: rgba(201, 154, 46, 0.08);
  border-radius: 3px;
  font-size: 12px;
  line-height: 1.6;
}

.never-healthy .hint {
  display: block;
  margin-top: 4px;
  opacity: 0.75;
}

/* Field paths, always LTR even on a Hebrew page: `metrics[0].treshold` is
   unreadable when the brackets and dots get bidi-reordered. */
ul.unknown-fields {
  margin: 8px 0 0;
  padding-inline-start: 18px;
  font-family: var(--mono, ui-monospace, monospace);
  font-size: 12px;
  line-height: 1.8;
}

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

   Everything above this line describes a workstation. Everything below it is
   what changes when the console is opened on a phone, which it is: alerts
   arrive in Telegram, and the operator who reads one at 03:00 is holding the
   device they read it on. Sending them to a desk before they can see which
   monitor it was is the whole reason this section exists.

   The queries are split by what they are actually asking about, not by device
   name. `pointer: coarse` is about the size of a fingertip and is true on a
   tablet plugged into a monitor. `max-width` is about how many columns fit and
   is true on a narrow desktop window. Conflating them produces a stylesheet
   that makes a 1400px touchscreen look like a phone, and a 400px browser
   window on a laptop unreachably small. */

/* -- fingertips ----------------------------------------------------------- */

@media (pointer: coarse) {
  /* 44px is the smallest target that can be hit reliably without looking, and
     several of the controls here -- acknowledge, assign, deactivate -- are ones
     an operator presses while their attention is on something else.

     `.vrow` and `.section-head` are excluded, and that exclusion is load-
     bearing rather than cosmetic. They are buttons, but they are also rows in a
     windowed list whose position is computed from a fixed height and applied as
     an inline style. A `min-height` larger than that height wins over the
     inline value, so every row would be drawn taller than the step between
     them and the list would render as overlapping text. Their touch size is
     handled by `--vrow-h` below, which the list is told about. */
  button:not(.vrow):not(.section-head),
  a.button {
    min-height: 44px;
  }

  .link-button {
    min-height: 44px;
  }

  input:not([type='checkbox']),
  select,
  textarea {
    min-height: 44px;
    /* 16px, and not for appearance. Safari zooms the page in whenever a field
       with text smaller than 16px takes focus, and it does not zoom back out
       afterwards -- so one tap on a filter leaves the operator on a console
       that no longer fits the screen, with no obvious way back. */
    font-size: 16px;
  }

  /* A row is a target as well as a line of text. */
  :root {
    --vrow-h: 48px;
  }
}

/* -- a narrow window ------------------------------------------------------ */

@media (max-width: 900px) {
  :root {
    --chrome-h: 300px;
  }

  .topbar {
    gap: 10px;
    padding-inline-start: max(12px, env(safe-area-inset-left));
    padding-inline-end: max(12px, env(safe-area-inset-right));
  }

  .view {
    padding: 12px;
    padding-inline-start: max(12px, env(safe-area-inset-left));
    padding-inline-end: max(12px, env(safe-area-inset-right));
    padding-block-end: calc(12px + env(safe-area-inset-bottom));
  }
}

/* -- one hand ------------------------------------------------------------- */

@media (max-width: 720px) {
  :root {
    /* The header is taller here because it wraps, and the counters take two
       rows instead of one. The list gets what is left, floored by its own
       `min-height` so it never collapses to nothing on a short screen. */
    --chrome-h: 340px;
  }

  /* The header wraps rather than scrolls or hides.

     Hiding controls on a small screen is the usual answer and the wrong one
     here: every item in this bar is either an identity, a connection state or
     a way out, and an operator who cannot see which account they are acting as
     is being asked to trust a screen that will not say. So it reflows into
     rows, in an order that puts the two facts they need first -- who is signed
     in, and whether this page is still live. */
  .topbar {
    flex-wrap: wrap;
    height: auto;
    min-height: 52px;
    padding-block: 8px;
    row-gap: 8px;
  }

  .topbar .brand {
    order: 1;
  }
  .topbar .spacer {
    order: 2;
  }
  .topbar .live {
    order: 3;
  }
  .topbar [data-theme-toggle] {
    order: 4;
  }
  .topbar #logout {
    order: 5;
  }

  /* Its own row, and scrollable sideways within it. The tab strip is the one
     thing here that genuinely can grow without limit -- a section added later
     lands in it -- so it is given somewhere to grow that is not the height of
     the header.

     `overflow-x: auto` is what makes the rest of this rule do anything, and
     leaving it out is why the console scrolled sideways at 300 points: three
     Hebrew tab labels were wider than the row, spilled out of the header and
     pushed the whole page wider than the window. The symptom appeared on
     every screen and the cause was in the header, which is the kind of defect
     that gets chased in the wrong file.

     It is also what allows the strip to shrink at all. A flex item defaults to
     `min-width: auto` -- "never shrink below my contents" -- but a box with
     overflow other than `visible` has an automatic minimum size of zero, so
     the declaration below is already implied. Writing `min-width: 0` here as
     well reads like a second safeguard and is not one: remove the overflow and
     it does nothing, keep the overflow and it was never needed. A mutation of
     it changes no pixel on any screen, which is how it was found out. */
  .tabs {
    order: 6;
    flex: 1 0 100%;
    margin-inline-start: 0;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }

  .tabs::-webkit-scrollbar {
    display: none;
  }

  .tabs button {
    flex: none;
    white-space: nowrap;
  }

  /* Its own row, and allowed to break.

     The same defect as the tab strip above, one element further along, and it
     hid behind it: with `flex: 1 0 auto` this is a flex item that refuses to
     shrink below its contents, and its contents are a 36-character user id
     with nothing in it a browser may break at. So it pushed the page sideways
     -- by four points, or eight, or eleven, depending on the digits the id
     happened to be made of, which is why the suite failed on some runs and not
     others and looked like a flake.

     Truncating it was the alternative and is refused for the reason the rest
     of this block exists: the header wraps rather than hides, and the account
     an operator is acting as is the last thing to cut off mid-way. It gets a
     full row and breaks across lines instead. */
  .topbar > #who {
    order: 7;
    flex: 1 0 100%;
    min-width: 0;
    overflow-wrap: anywhere;
  }

  .topbar .button.ghost {
    order: 8;
  }

  .banner {
    padding-inline-start: max(12px, env(safe-area-inset-left));
    padding-inline-end: max(12px, env(safe-area-inset-right));
  }

  /* Two up rather than four across, and smaller with it. Four counters at
     desktop size do not fit a phone, and a fifth of a screen spent on numbers
     that are mostly zero is a fifth not spent on the list that says what is
     wrong. */
  .counters {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(130px, 45%), 1fr));
    margin-bottom: 14px;
  }

  .counter {
    min-width: 0;
    padding: 10px 14px;
  }

  .counter .n {
    font-size: 24px;
  }

  .skeleton-counter {
    height: 58px;
    min-width: 0;
  }

  .filters select {
    flex: 1 1 140px;
    min-width: 0;
  }

  .pager {
    flex-wrap: wrap;
  }

  .panel {
    padding: 14px;
  }

  .kv {
    grid-template-columns: minmax(90px, auto) 1fr;
  }

  .guide-dialog {
    width: min(860px, 94vw);
    max-height: 86dvh;
    padding: 14px;
  }

  pre.guide {
    max-height: 52dvh;
  }

  .check-result {
    max-height: 40dvh;
  }

  /* Tables become one card per row.

     A table is a grid of values whose meaning comes from the column they are
     under, and there is no width at which seven of those columns fit across a
     phone. The usual fallback is a sideways scrollbar, which is worse than it
     looks: the column headings scroll away, so the operator is reading
     unlabelled values and has to remember what the fourth one meant.

     So the heading travels with the value instead, out of `data-label` on each
     cell. The action cell deliberately has no label -- the buttons say what
     they do, and a caption over a control is noise. */
  table.queue,
  table.metrics {
    display: block;
    border-collapse: separate;
  }

  table.queue thead,
  table.metrics thead {
    display: none;
  }

  table.queue tbody,
  table.metrics tbody {
    display: block;
  }

  table.queue tbody tr,
  table.metrics tbody tr {
    display: block;
    padding: 10px 12px;
    border-bottom: 1px solid var(--line);
  }

  table.queue tbody tr:last-child,
  table.metrics tbody tr:last-child {
    border-bottom: none;
  }

  table.queue td,
  table.metrics td {
    display: block;
    padding: 3px 0;
    border-bottom: none;
    /* A value that was safe on one line of a wide table is not safe in a
       narrow cell. Connection strings and monitor ids have no spaces to break
       at, so without this they push the card wider than the screen. */
    overflow-wrap: anywhere;
  }

  /* The heading, carried inline in front of its value.

     Inline rather than in a column of its own, and that is the version that
     survives contact with the data. Several of these cells hold more than one
     child -- two threshold chips, a name above an id -- and a two-column grid
     gives every child a cell of its own, so the second chip lands underneath
     the heading instead of beside the first. Inline text has no such opinion
     about how many things follow it. */
  table.queue td[data-label]::before,
  table.metrics td[data-label]::before {
    content: attr(data-label);
    margin-inline-end: 8px;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--muted);
  }

  /* The value column must not be forced onto one line once it shares a narrow
     card with its own heading. */
  table.metrics td.v {
    white-space: normal;
  }

  table.queue td:not([data-label]) {
    padding-top: 8px;
  }

  .row-actions {
    flex-wrap: wrap;
  }

  tr.rejection td {
    padding-top: 8px;
  }
}

/* -- a phone -------------------------------------------------------------- */

@media (max-width: 560px) {
  :root {
    /* Two lines per row, so the six columns of a monitor row become four cells
       on two lines instead of six squeezed across 360 pixels. The list is told
       this number rather than measuring it; see the note on the token. */
    --vrow-h: 60px;
  }

  /* The monitor row, folded.

     Name and integration flags on top, then severity and the two times
     underneath. What is on the first line is what the operator is scanning for;
     what is on the second is what they read once they have found it. */
  .vrow {
    grid-template-columns: 4px minmax(0, 1fr) auto auto;
    grid-template-rows: auto auto;
    grid-template-areas:
      'bar name name flags'
      'bar sev  seen next';
    align-items: center;
    row-gap: 2px;
    column-gap: 8px;
    padding-block: 5px;
  }

  .vrow .bar {
    grid-area: bar;
    align-self: stretch;
  }
  .vrow .name {
    grid-area: name;
  }
  .vrow .flags {
    grid-area: flags;
  }
  .vrow .sev-cell {
    grid-area: sev;
  }
  .vrow .seen {
    grid-area: seen;
  }
  .vrow .next {
    grid-area: next;
  }

  .vrow .col {
    font-size: 11px;
  }

  .login {
    padding: 22px;
  }

  .match-code {
    font-size: 44px;
    letter-spacing: 8px;
    text-indent: 8px;
  }

  .login-foot {
    flex-wrap: wrap;
  }

  .detail-head {
    align-items: flex-start;
  }
}
