/* APISIX Manager — the whole stylesheet. One file, hand-maintained, no build step.
 *
 * Four rules hold from this first line, and each is enforced by a grep in
 * tests/unit/test_no_io_on_read.py rather than by anyone remembering it:
 *
 *   1. No hex value outside the :root block. Adding a dark theme then stays a
 *      token swap with zero rule changes.
 *   2. There is no success colour and no affirming glyph in the Phase 1
 *      palette. WAF-03's configured-vs-verified split is Phase 2; a reassuring
 *      tick here would assert that attacks are being rejected, which this phase
 *      cannot back, and adding the caveat later is the trust-destroying
 *      sequence this phase exists to avoid.
 *   3. Focus outlines are never suppressed. Every focusable element shows a
 *      2px accent ring.
 *   4. No CSS transitions and no keyframe animations, which also discharges
 *      prefers-reduced-motion by construction.
 *
 * Border widths are the only lengths outside the --sp-* scale, and exactly
 * three are permitted: 1px hairlines and chip borders, 2px focus ring, 3px bar
 * left border. Row min-height is derived (14px x 1.4 + 2 x 8px), not a token.
 */

:root {
  /* Type. System stacks only — no web fonts, no CDN, no @font-face. */
  --font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

  /* Spacing. Multiples of 4 only. */
  --sp-xs: 4px;
  --sp-sm: 8px;
  --sp-md: 16px;
  --sp-lg: 24px;
  --sp-xl: 32px;
  --sp-2xl: 48px;
  --sp-3xl: 64px;

  /* Layout roles, 60 / 30 / 10. */
  --surface: #FFFFFF;
  --surface-2: #F4F5F7;
  --accent: #1B4DB1;

  /* Status. Red = a bad fact the console established. Amber = risk or
   * uncertainty. Those are the only two status colours there are. */
  --danger: #A4231F;
  --danger-bg: #FCEDEC;
  --caution: #7A4B00;
  --caution-bg: #FDF3E0;

  /* Neutrals. */
  --ink: #14161A;
  --ink-muted: #5A6069;
  --hairline: #DDE1E6;
}

/* --- reset ------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-ui);
  /* Table role: 14px / 400 / 1.4. This is a dense operational table; at 16px a
   * 300-row table is ~40% taller for no gain in legibility. Prose that is
   * actually read (banners, empty states) steps up to the 16px Body role. */
  font-size: 14px;
  font-weight: 400;
  line-height: 1.4;
}

/* --- the four type roles ----------------------------------------------- */

.t-micro {
  font-size: 12px;
  font-weight: 600;
  line-height: 1.2;
}

.t-body {
  font-size: 16px;
  font-weight: 400;
  line-height: 1.5;
}

.t-title {
  font-size: 22px;
  font-weight: 600;
  line-height: 1.25;
}

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

/* --- focus, everywhere -------------------------------------------------- */

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

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

.header {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  min-height: 48px;
  padding: 0 var(--sp-lg);
  background: var(--surface-2);
  border-bottom: 1px solid var(--hairline);
}

.header h1 {
  margin: 0;
  font-size: 22px;
  font-weight: 600;
  line-height: 1.25;
}

.header .spacer {
  flex: 1 1 auto;
}

/* The READ-ONLY chip. Neutral on purpose — accent is reserved, and this is a
 * statement of fact, not an interactive element. */
.chip-readonly {
  padding: var(--sp-xs) var(--sp-sm);
  border: 1px solid var(--hairline);
  border-radius: 2px;
  background: var(--surface);
  color: var(--ink-muted);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0.04em;
}

/* The signed-in address. 12px and muted: it answers "which account am I?" and
 * is not something anyone reads twice. */
.identity {
  color: var(--ink-muted);
  font-size: 12px;
  font-weight: 400;
  line-height: 1.2;
}

.signout {
  margin: 0;
}

/* One of the five reserved accent uses: a link-styled control. A real <button>
 * in a real <form method="post"> — never an <a href>, which would make signing
 * out a GET and therefore forgeable. */
.signout-button {
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.2;
  cursor: pointer;
  text-decoration: underline;
}

/* --- the login page ----------------------------------------------------- */

/* The only page in the application with a centred layout, and the only one with
 * a filled button. Both are deliberate: it is the one screen with exactly one
 * thing to do. */
.login-page {
  display: flex;
  justify-content: center;
  padding: var(--sp-3xl) var(--sp-md);
  background: var(--surface-2);
}

.login-card {
  width: 100%;
  max-width: 400px;
  height: max-content;
  padding: var(--sp-xl);
  border: 1px solid var(--hairline);
  border-radius: 2px;
  background: var(--surface);
}

.login-heading {
  margin: 0 0 var(--sp-md);
}

.login-body {
  margin: 0 0 var(--sp-lg);
  color: var(--ink-muted);
}

/* The three conditional messages reuse the bar tokens, so the login page cannot
 * drift into its own idea of what red and amber mean. */
.login-message {
  flex-direction: column;
  gap: var(--sp-xs);
  margin-bottom: var(--sp-lg);
  padding: var(--sp-md);
  font-size: 14px;
  line-height: 1.4;
}

/* THE only filled button in the application (UI-SPEC accent reserve list item
 * 1). If a second one ever appears, one of them is wrong. */
.button-primary {
  display: inline-block;
  padding: var(--sp-sm) var(--sp-md);
  border: 1px solid var(--accent);
  border-radius: 2px;
  background: var(--accent);
  color: var(--surface);
  font-family: var(--font-ui);
  font-size: 16px;
  font-weight: 600;
  line-height: 1.5;
  cursor: pointer;
  text-align: center;
  text-decoration: none;
}

/* --- page regions ------------------------------------------------------- */

.page {
  padding: var(--sp-lg);
}

#freshness,
#filters,
#explain {
  margin-bottom: var(--sp-lg);
}

/* --- the filter bar ----------------------------------------------------- */

.filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--sp-md);
}

.filter-search {
  min-width: 280px;
  padding: var(--sp-sm) var(--sp-md);
  border: 1px solid var(--hairline);
  border-radius: 2px;
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 14px;
  line-height: 1.4;
}

.filter-form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-md);
  margin: 0;
}

.filter-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-xs);
  margin: 0;
  padding: 0 0 0 var(--sp-sm);
  border: 0;
  border-left: 1px solid var(--hairline);
}

.filter-group legend {
  padding: 0;
  color: var(--ink-muted);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* Visually hidden but STILL FOCUSABLE. `display: none` here would remove the
 * checkbox from the tab order and make the whole filter bar unreachable by
 * keyboard — the label alone is not focusable. */
.chip-input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  margin: 0;
  pointer-events: none;
}

/* The pill. Neutral when unselected: accent is reserved, and "this chip exists"
 * does not qualify. */
.chip-toggle {
  display: inline-block;
  padding: var(--sp-xs) var(--sp-sm);
  border: 1px solid var(--hairline);
  border-radius: 2px;
  background: var(--surface);
  color: var(--ink);
  font-size: 12px;
  font-weight: 400;
  line-height: 1.2;
  cursor: pointer;
  white-space: nowrap;
}

/* Three simultaneous signals, of which colour is only one: the leading tick is
 * in the markup, the weight steps to 600, and the fill goes accent. A reader
 * with no colour at all still sees two of the three. */
.chip-input:checked + .chip-toggle {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--surface);
  font-weight: 600;
}

/* The focus ring has to be driven off the hidden input, because the input is
 * what actually receives focus and the label is what is actually visible. */
.chip-input:focus-visible + .chip-toggle {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The out-of-band swap target. Muted so the count reads as secondary to the
 * label it qualifies — and it inherits the label's colour when selected, so it
 * stays legible on the accent fill. */
.chip-count {
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

.chip-input:checked + .chip-toggle .chip-count {
  color: var(--surface);
}

.filter-reset {
  align-self: center;
  margin-left: auto;
  color: var(--accent);
  font-size: 12px;
  font-weight: 600;
}

/* --- the Explain panel -------------------------------------------------- */

/* Sits between the filter bar and the table, and outside #rows — so a timed
 * swap can never close an explanation an operator is reading. */
.explain {
  padding: var(--sp-md) var(--sp-lg);
  border-left: 3px solid var(--hairline);
  background: var(--surface-2);
  font-size: 14px;
  line-height: 1.4;
}

.explain-domain {
  margin: 0 0 var(--sp-md);
  font-size: 14px;
  font-weight: 600;
}

.explain-ids {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--sp-xs) var(--sp-md);
  margin: 0 0 var(--sp-md);
}

.explain-ids dt {
  color: var(--ink-muted);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.explain-ids dd {
  margin: 0;
}

/* The whole reason the panel exists: one sentence saying where the mode came
 * from. Body type, because it is prose that is actually read rather than a
 * cell that is scanned. */
.explain-sentence {
  margin: 0 0 var(--sp-md);
  font-size: 16px;
  font-weight: 400;
  line-height: 1.5;
}

.explain-heading {
  margin: 0 0 var(--sp-sm);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* Scrolls rather than wrapping: a curl command broken across lines is a curl
 * command that does not survive a copy and paste. */
.explain-curl {
  margin: 0 0 var(--sp-md);
  padding: var(--sp-sm) var(--sp-md);
  overflow-x: auto;
  border: 1px solid var(--hairline);
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.4;
}

.explain-close {
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  text-decoration: underline;
}

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

/* What keeps the console usable at 200% zoom on a 1280px viewport, and below
 * the 768px functional floor: it scrolls horizontally rather than reflowing. */
.table-wrap {
  overflow-x: auto;
}

table.domains {
  width: 100%;
  border-collapse: collapse;
  /* Hairlines separate rows. No zebra striping: it fights the status
   * backgrounds that arrive in plan 01-05, and hairlines are enough. */
}

table.domains thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: var(--sp-sm) var(--sp-md);
  background: var(--surface-2);
  border-bottom: 1px solid var(--hairline);
  color: var(--ink-muted);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0.04em;
  text-align: left;
  text-transform: uppercase;
  white-space: nowrap;
}

table.domains td {
  padding: var(--sp-sm) var(--sp-md);
  border-bottom: 1px solid var(--hairline);
  vertical-align: top;
}

table.domains tbody tr:hover {
  background: var(--surface-2);
}

.col-domain {
  min-width: 280px;
}

.col-waf {
  width: 200px;
}

.col-cache {
  width: 150px;
}

.col-backends {
  width: 300px;
}

.col-changed {
  width: 130px;
}

/* The domain name: a link-styled button, because it performs an action (opens
 * the Explain panel) rather than navigating. Native <button>, never a div. */
.domain-link {
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 14px;
  cursor: pointer;
  text-align: left;
  text-decoration: underline;
}

/* Shown only when the service_id deviates from the dashed-domain convention —
 * D-19's convention filter, visible at a glance and costing one line. */
.service-id {
  margin-top: var(--sp-xs);
  color: var(--ink-muted);
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.2;
}

/* --- status chips ------------------------------------------------------- */

/* Used by the WAF mode cell, the cache cell and the two WAF warning chips.
 *
 * Every chip carries its meaning in TEXT. Colour and glyph are secondary
 * channels and never the only one, so a chip is still completely correct if the
 * glyph renders as tofu or the reader sees no colour at all. */
.chip {
  display: inline-block;
  padding: var(--sp-xs) var(--sp-sm);
  border: 1px solid var(--hairline);
  border-radius: 2px;
  background: var(--surface);
  color: var(--ink);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0.04em;
  white-space: nowrap;
}

/* BLOCK and MONITOR are neutral ink on purpose. The cluster is deliberately
 * parked in monitor while false positives are observed, so colouring monitor
 * amber would colour nearly every row amber and amber would then mean nothing.
 * Colour only carries information while most rows are uncoloured. */
.chip--neutral {
  border-color: var(--hairline);
  background: var(--surface);
  color: var(--ink);
}

/* An established bad fact: OFF, NOT ATTACHED. Rare, therefore actually seen. */
.chip--danger {
  border-color: var(--danger);
  background: var(--danger-bg);
  color: var(--danger);
}

/* Risk or uncertainty: cache OFF, NO STATIC ROUTE, CONDITIONAL, SHADOWED. */
.chip--caution {
  border-color: var(--caution);
  background: var(--caution-bg);
  color: var(--caution);
}

/* Border STYLE separates cache OFF from NO STATIC ROUTE, which share a colour.
 * Stated explicitly on both so the distinction is legible in the template and
 * assertable in a test, rather than resting on an unwritten default. */
.chip--solid {
  border-style: solid;
}

.chip--dashed {
  border-style: dashed;
}

/* --- origin badge ------------------------------------------------------- */

/* SET HERE / INHERITED / APISIX DEFAULT.
 *
 * Border STYLE is the non-colour channel that carries origin, so the
 * distinction survives in monochrome. The border is therefore
 * information-bearing and uses --ink-muted rather than --hairline: it has to
 * clear the 3:1 non-text contrast floor, and a hairline does not. */
.badge {
  display: inline-block;
  margin-left: var(--sp-xs);
  padding: var(--sp-xs) var(--sp-sm);
  border: 1px solid var(--ink-muted);
  border-radius: 2px;
  background: var(--surface);
  color: var(--ink-muted);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0.04em;
  white-space: nowrap;
}

.badge--solid {
  border-style: solid;
}

.badge--dotted {
  border-style: dotted;
}

/* Rare and worth flagging: the DOCUMENTED APISIX default is block, the ACTUAL
 * hardcoded fallback is monitor, so anyone reasoning from the docs has it
 * backwards. */
.badge--caution {
  border-color: var(--caution);
  color: var(--caution);
}

/* --- bars: the freshness bar and the cluster banners --------------------- */

/* The 3px left border is the status-colour carrier. Neutral by default, so a
 * bar that is merely present says nothing alarming — and the freshness bar is
 * ALWAYS present, because its absence must never be a signal. */
.bar {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--sp-md);
  width: 100%;
  margin-bottom: var(--sp-sm);
  padding: var(--sp-sm) var(--sp-lg);
  border-left: 3px solid var(--hairline);
  background: var(--surface-2);
  color: var(--ink);
  font-size: 16px;
  font-weight: 400;
  line-height: 1.5;
}

/* Established facts about the cluster: missing plugin_metadata, a lost poller
 * lock, a dead browser connection. Staleness is NEVER red — the console being
 * unable to reach APISIX is not a fact about a domain. */
.bar--danger {
  border-left-color: var(--danger);
  background: var(--danger-bg);
  color: var(--danger);
}

.bar--caution {
  border-left-color: var(--caution);
  background: var(--caution-bg);
  color: var(--caution);
}

/* The cluster WAF default (WAF-02), right-aligned in the freshness bar. */
.bar-end {
  margin-left: auto;
}

/* The lost-connection banner is rendered on every page and hidden, so revealing
 * it needs no request — which matters, because the condition that reveals it is
 * "no request will succeed". Without this a dead console renders "Updated 12s
 * ago" forever: a page that lies quietly. */
.bar--disconnected {
  display: none;
}

body.is-disconnected .bar--disconnected {
  display: flex;
}

/* Dimming by ink-muted rather than by opacity: opacity would drag the table
 * under the 4.5:1 contrast floor this phase commits to, and the point is to
 * make the data look untrustworthy, not to make it unreadable. */
body.is-disconnected table.domains {
  color: var(--ink-muted);
}

/* --- the backends cell -------------------------------------------------- */

.nodes {
  margin: 0;
  padding: 0;
  font-family: var(--font-mono);
  font-size: 14px;
  line-height: 1.4;
  list-style: none;
}

.nodes li + li {
  margin-top: var(--sp-sm);
}

/* A node at weight 0 carries THREE simultaneous signals: the literal word
 * DISABLED in the markup, line-through on the address, and muted colour. The
 * word is the primary channel; these two are support. */
.node--disabled {
  color: var(--ink-muted);
}

.node--disabled .node-addr {
  text-decoration: line-through;
}

.node-footer {
  margin: var(--sp-sm) 0 0;
  font-size: 12px;
  font-weight: 600;
  line-height: 1.2;
}

/* NO BACKENDS, and the two TRAFFIC STILL FLOWS findings. */
.node-footer--danger {
  color: var(--danger);
}

/* SINGLE BACKEND, SHARED UPSTREAM. */
.node-footer--caution {
  color: var(--caution);
}

/* Bounds row height at 4 lines plus footers regardless of how pathological an
 * upstream is. Accent is permitted here: this is a link-styled control, which
 * is one of the five reserved uses. */
.cell--truncate-more {
  display: inline-block;
  margin-top: var(--sp-sm);
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  text-decoration: underline;
}

/* --- the state block ---------------------------------------------------- */

/* never connected / connected-but-empty / no match. All three render INSIDE
 * #rows as one <tr><td colspan="5">, so there is exactly one swap target and
 * one branch point. The generous top padding stands in for the --sp-2xl margin
 * a table cell cannot carry. */
.state td {
  padding: var(--sp-2xl) var(--sp-xl) var(--sp-xl);
  border-bottom: 0;
}

.state td .t-title {
  margin: 0 0 var(--sp-md);
}

.state td .t-body {
  margin: 0;
  color: var(--ink-muted);
}

.state td a {
  color: var(--accent);
}

/* Hover highlighting a state block would imply it is a row that can be acted
 * upon. It is a message. */
table.domains tbody tr.state:hover {
  background: var(--surface);
}

/* --- visually hidden, still announced ----------------------------------- */

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  border: 0;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --- phone layout (max-width: 720px) ------------------------------------
 *
 * Phase 1's viewport contract stopped at a 768px floor with table-wrap
 * horizontal scroll and deferred phone cards. This block supersedes that
 * deferral for narrow handsets only: desktop rules above stay authoritative
 * at ≥721px (sticky thead, five columns, table-wrap overflow-x).
 *
 * Goal: header, filters, freshness/status bars and domain inventory usable
 * without horizontal PAGE overflow. Intentionally scrollable regions
 * (.explain-curl) keep their own overflow-x.
 */

@media (max-width: 720px) {
  html {
    overflow-x: clip;
  }

  /* --- chrome ---------------------------------------------------------- */

  .header {
    flex-wrap: wrap;
    min-height: 0;
    padding: var(--sp-sm) var(--sp-md);
    gap: var(--sp-sm) var(--sp-md);
  }

  .header h1 {
    /* Body role size on phone so the title fits beside READ-ONLY without
     * inventing a fifth type size outside the design system. */
    font-size: 16px;
    line-height: 1.5;
  }

  /* Force identity + sign-out onto a full-width second row so a long email
   * cannot shove Sign out past the right edge. */
  .header .spacer {
    display: none;
  }

  .header .identity {
    flex: 1 1 auto;
    min-width: 0;
    overflow-wrap: anywhere;
  }

  .page {
    padding: var(--sp-md);
  }

  .bar {
    padding: var(--sp-sm) var(--sp-md);
  }

  .bar-end {
    margin-left: 0;
  }

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

  .filters {
    flex-direction: column;
    align-items: stretch;
  }

  .filter-search {
    min-width: 0;
    width: 100%;
  }

  .filter-form {
    flex-direction: column;
    gap: var(--sp-sm);
  }

  .filter-group {
    padding: var(--sp-sm) 0 0;
    border-left: 0;
    border-top: 1px solid var(--hairline);
  }

  .filter-reset {
    margin-left: 0;
    align-self: flex-start;
  }

  /* --- domain inventory as cards --------------------------------------- */

  .table-wrap {
    overflow-x: visible;
  }

  table.domains,
  table.domains thead,
  table.domains tbody,
  table.domains th,
  table.domains td,
  table.domains tr {
    display: block;
    width: 100%;
  }

  /* Column headers move into each cell via data-label; keep thead in the a11y
   * tree but out of layout (same technique as .visually-hidden). */
  table.domains thead {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    border: 0;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  table.domains tbody tr {
    margin-bottom: var(--sp-md);
    padding: var(--sp-xs) 0;
    border: 1px solid var(--hairline);
    border-radius: 2px;
    background: var(--surface);
  }

  table.domains tbody tr:hover {
    background: var(--surface);
  }

  table.domains tbody tr.state {
    border-color: var(--hairline);
  }

  table.domains tbody tr.state:hover {
    background: var(--surface);
  }

  table.domains td {
    display: grid;
    grid-template-columns: minmax(6.5rem, 34%) 1fr;
    gap: var(--sp-sm) var(--sp-md);
    align-items: start;
    min-width: 0;
    width: auto;
    padding: var(--sp-sm) var(--sp-md);
    border-bottom: 0;
  }

  table.domains td::before {
    content: attr(data-label);
    color: var(--ink-muted);
    font-size: 12px;
    font-weight: 600;
    line-height: 1.2;
    letter-spacing: 0.04em;
    text-transform: uppercase;
  }

  /* State messages are prose, not field rows — no fake column label. */
  table.domains tr.state td {
    display: block;
    padding: var(--sp-xl) var(--sp-md) var(--sp-md);
  }

  table.domains tr.state td::before {
    content: none;
  }

  .col-domain,
  .col-waf,
  .col-cache,
  .col-backends,
  .col-changed {
    width: auto;
    min-width: 0;
  }

  .domain-link {
    overflow-wrap: anywhere;
    word-break: break-word;
  }

  /* Long chip labels (NO STATIC ROUTE) wrap inside the card value column. */
  .chip,
  .badge {
    white-space: normal;
  }

  .nodes {
    overflow-wrap: anywhere;
  }

  /* --- explain + login ------------------------------------------------- */

  .explain {
    padding: var(--sp-md);
  }

  .explain-ids {
    grid-template-columns: 1fr;
  }

  .explain-ids dd {
    overflow-wrap: anywhere;
    margin-bottom: var(--sp-sm);
  }

  .login-page {
    padding: var(--sp-xl) var(--sp-md);
  }

  .login-card {
    padding: var(--sp-lg);
  }

  .button-primary {
    display: block;
    width: 100%;
  }
}
