@import url("tokens.css");

/* Shared styling for the cloud web surfaces: the subscriber dashboard
   (index.html), the ISP portal (portal.html), and every other page that
   links this file — which is how they all receive tokens.css above.

   This file CONSUMES tokens and never redefines one: the pre-tokens copy
   of the palette drifted from the desktop's (the desktop lost --ok and
   nobody noticed), which is exactly what the canonical tokens file and its
   drift test now prevent. Anything that needs a colour uses a semantic
   token (--status-*, --accent, --chart-*), so both themes work without a
   second set of rules here.

   No build step, no framework, no web fonts: these files are served
   straight off @fastify/static from the Coolify container (D22). The few
   translucent borders that need "a status colour at low alpha" use
   color-mix() with the token; where a browser lacks color-mix the base
   rule's --card-border remains — a quieter border, never a wrong colour.

   Colour-blind safety carries over from the desktop rules: state is never
   signalled by hue alone — every pill and banner also carries a word, and
   the status pills add a glyph. */

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

html,
body {
  min-height: 100%;
}

body {
  background:
    radial-gradient(1200px 600px at 80% -10%, var(--bg-glow-1), transparent 60%),
    radial-gradient(900px 500px at -10% 110%, var(--bg-glow-2), transparent 60%),
    var(--bg);
  background-attachment: fixed;
  color: var(--text);
  font-family: var(--font-ui);
  font-size: var(--text-base);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3 {
  line-height: 1.25;
  font-weight: 650;
}

h1 {
  font-size: var(--text-xl);
}

h2 {
  font-size: 15px;
  letter-spacing: 0.2px;
}

h3 {
  font-size: 13px;
}

a {
  color: var(--accent);
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

code,
.mono {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  word-break: break-all;
}

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

.num {
  font-variant-numeric: tabular-nums;
}

.hidden {
  display: none !important;
}

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

.shell {
  max-width: 1180px;
  margin: 0 auto;
  padding: 20px 22px 64px;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  min-width: 0;
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.brand {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  font-size: 18px;
  font-weight: 650;
  letter-spacing: 0.2px;
}

.brand-mark {
  color: var(--accent);
}

/* The SVG mark (ui.js brandMark) sits beside a baseline-aligned wordmark;
   the wrapper self-centres so the glyph does not ride the text baseline. */
.brand-glyph {
  display: inline-flex;
  align-self: center;
  color: var(--accent);
  flex: none;
}

.brand-sub {
  color: var(--muted);
  font-size: var(--text-xs);
  font-weight: 500;
}

.topbar-tools {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  min-width: 0;
}

.topbar-right {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  min-width: 0;
}

.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-3);
}

.section-head p {
  color: var(--muted);
  font-size: var(--text-sm);
  max-width: 70ch;
}

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

.card {
  background: var(--card);
  border: 1px solid var(--card-border);
  border-radius: var(--radius);
  padding: var(--pad-card);
  /* The glass pairing: --card only reads as glass with the blur behind
     it, and the amount is a token so both surfaces soften together. */
  backdrop-filter: blur(var(--glass-blur));
  min-width: 0;
  /* Hover is transform+border only: transform never reflows the SVG chart
     inside, and both durations come from the motion tokens so
     prefers-reduced-motion stills the card for free. */
  transition:
    border-color var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease);
}

.card:hover {
  border-color: color-mix(in srgb, var(--accent) 28%, var(--card-border));
  transform: translateY(-1px);
}

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  min-width: 0;
}

/* --------------------------------------------------------------- chips etc */

.chip {
  background: var(--card);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-pill);
  padding: 4px 12px;
  color: var(--muted);
  font-size: var(--text-xs);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
  min-width: 0;
}

/* Generic label pill (link status, ticket status, speedtest grade). The
   ok/degraded/down trio for *measurement* state uses .status-pill below,
   which adds the glyph; these stay for labels where the word alone is the
   signal. */
.pill {
  display: inline-block;
  border-radius: var(--radius-pill);
  padding: 2px 10px;
  font-size: 11px;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  border: 1px solid var(--card-border);
  background: var(--card);
  color: var(--muted);
  white-space: nowrap;
}

.pill-ok {
  color: var(--status-ok);
  background: var(--status-ok-soft);
  border-color: color-mix(in srgb, var(--status-ok) 45%, transparent);
}

.pill-accent {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}

.pill-warn {
  color: var(--status-degraded);
  background: var(--status-degraded-soft);
  border-color: color-mix(in srgb, var(--status-degraded) 45%, transparent);
}

.pill-err {
  color: var(--status-down);
  background: var(--status-down-soft);
  border-color: color-mix(in srgb, var(--status-down) 45%, transparent);
}

/* A maintenance claim made AFTER the outage began (§4.4). Hatched, not
   solid, so it can never be mistaken for planned work even in a
   screenshot or in greyscale — the visual is the policy. */
.pill-retro {
  color: var(--status-degraded);
  background-image: var(--insufficient-hatch);
  background-color: var(--status-degraded-soft);
  border-color: color-mix(in srgb, var(--status-degraded) 45%, transparent);
  border-style: dashed;
}

/* ------------------------------------------------------------ status pill
   The one visual for ok/degraded/down/unknown/insufficient (ui.js
   statusPill). Colour paired with a glyph and a word so the state survives
   monochrome; insufficient additionally carries the hatch so "not enough
   data" can never be mistaken for a soft OK. Mirrors the desktop's. */

.status-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  font-size: var(--text-sm);
  font-weight: 600;
  white-space: nowrap;
}

.status-pill .status-glyph {
  font-size: 0.85em;
}

.status-pill.status-ok {
  color: var(--status-ok);
  background: var(--status-ok-soft);
}

.status-pill.status-degraded {
  color: var(--status-degraded);
  background: var(--status-degraded-soft);
}

.status-pill.status-down {
  color: var(--status-down);
  background: var(--status-down-soft);
}

.status-pill.status-unknown {
  color: var(--status-unknown);
  background: var(--status-unknown-soft);
}

.status-pill.status-insufficient {
  color: var(--insufficient);
  background-color: var(--status-unknown-soft);
  background-image: var(--insufficient-hatch);
}

/* ---------------------------------------------------------- evidence pair
   supporting[] and contradicting[], side by side, both always visible
   (ui.js evidencePair). The against column is marked with the degraded
   colour, NOT the down/red one: counter-evidence is information, and an
   empty against column is a claim, not an error. Mirrors the desktop's. */

.evidence-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gap);
}

.evidence-col {
  background: var(--card);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-sm);
  padding: var(--space-3) var(--space-4);
  min-width: 0;
}

.evidence-col h4 {
  font-size: var(--text-xs);
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
  margin-bottom: var(--space-2);
}

.evidence-col ul {
  margin: 0;
  padding-left: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: var(--text-sm);
  line-height: 1.5;
}

.evidence-against {
  border-left: 3px solid var(--status-degraded);
}

.evidence-empty {
  color: var(--muted);
  font-size: var(--text-sm);
  line-height: 1.5;
}

/* --------------------------------------------------------------- controls */

.btn {
  background: var(--card);
  background: color-mix(in srgb, var(--text) 6%, transparent);
  border: 1px solid var(--card-border);
  color: var(--text);
  border-radius: var(--radius-sm);
  padding: 7px 14px;
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease);
}

.btn:hover:not(:disabled) {
  background: color-mix(in srgb, var(--text) 12%, transparent);
}

.btn:disabled {
  opacity: 0.45;
  cursor: default;
}

.btn-primary {
  background: var(--accent-soft);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--accent);
}

.btn-primary:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent) 26%, transparent);
}

.btn-danger {
  color: var(--status-down);
  border-color: color-mix(in srgb, var(--status-down) 35%, transparent);
  background: var(--status-down-soft);
}

.btn-danger:hover:not(:disabled) {
  background: color-mix(in srgb, var(--status-down) 18%, transparent);
}

.btn-sm {
  padding: 4px 10px;
  font-size: var(--text-xs);
}

.btn-link {
  background: none;
  border: none;
  color: var(--accent);
  padding: 0;
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
}

.btn-link:hover {
  text-decoration: underline;
}

/* A request in flight (ui.js btnBusy): an indeterminate sweep INSIDE the
   button, because the fetch has no real fraction to draw — a bar that
   crept toward 100% here would be a fabricated percentage. Duration is
   derived from a motion token, so prefers-reduced-motion (which zeroes
   the tokens) stops the sweep without this rule knowing why. */
.btn-busy {
  position: relative;
  overflow: hidden;
}

.btn-busy::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, var(--accent-soft), transparent);
  transform: translateX(-100%);
  animation: btn-sweep calc(var(--dur-slow) * 4) var(--ease) infinite;
  pointer-events: none;
}

@keyframes btn-sweep {
  to {
    transform: translateX(100%);
  }
}

/* Live numbers change width as digits change unless figures are tabular —
   the label is "Theme: auto/light/dark", so keep it from wobbling too. */
.theme-btn {
  font-variant-numeric: tabular-nums;
  flex: none;
}

.btn-row {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
  align-items: center;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}

.field label,
.field-label {
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--muted);
}

.field-hint {
  font-size: var(--text-xs);
  color: var(--muted);
  line-height: 1.45;
}

input[type="text"],
input[type="email"],
input[type="password"],
select,
textarea {
  background: var(--bg-soft);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  padding: 8px 11px;
  width: 100%;
  min-width: 0;
}

textarea {
  resize: vertical;
  min-height: 92px;
  line-height: 1.5;
}

/* The border+glow pair is the visible focus for form fields; the global
   :focus-visible ring (tokens.css) still covers everything else. */
input:focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-soft);
}

select option {
  background: var(--bg-soft);
  color: var(--text);
}

form.stack-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
  gap: var(--space-3);
}

/* Consent switch — the single most important control in the product, so it
   gets a real affordance rather than a checkbox that reads as a form field. */
.switch {
  appearance: none;
  -webkit-appearance: none;
  width: 38px;
  height: 21px;
  border-radius: var(--radius-pill);
  background: var(--card);
  background: color-mix(in srgb, var(--text) 12%, transparent);
  border: 1px solid var(--card-border);
  position: relative;
  cursor: pointer;
  flex: none;
  transition: background var(--dur-fast) var(--ease);
}

.switch::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  background: var(--muted);
  transition: transform var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}

.switch:checked {
  background: var(--accent-soft);
  background: color-mix(in srgb, var(--accent) 28%, transparent);
  border-color: color-mix(in srgb, var(--accent) 50%, transparent);
}

.switch:checked::after {
  transform: translateX(17px);
  background: var(--accent);
}

.switch:disabled {
  opacity: 0.5;
  cursor: default;
}

.switch-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
}

.switch-row .switch {
  margin-top: 2px;
}

/* ----------------------------------------------------------------- banners */

.banner {
  border: 1px solid var(--card-border);
  border-radius: var(--radius);
  padding: 10px 14px;
  font-size: var(--text-sm);
  font-weight: 550;
  line-height: 1.45;
}

.banner-err {
  background: var(--status-down-soft);
  border-color: color-mix(in srgb, var(--status-down) 40%, transparent);
  color: var(--status-down);
}

.banner-warn {
  background: var(--status-degraded-soft);
  border-color: color-mix(in srgb, var(--status-degraded) 40%, transparent);
  color: var(--status-degraded);
}

.banner-ok {
  background: var(--status-ok-soft);
  border-color: color-mix(in srgb, var(--status-ok) 40%, transparent);
  color: var(--status-ok);
}

.banner-info {
  background: var(--accent-soft);
  border-color: color-mix(in srgb, var(--accent) 30%, transparent);
  color: var(--accent);
}

/* ------------------------------------------------------- one-shot secrets
   A value the server cannot show twice — today only the device token
   (devices.js). It gets its own block rather than a .banner because it has
   to hold a heading, the value, actions and the steps that follow it, and
   because "shown once" must not look like the informational banners the
   page uses for advice a subscriber can ignore.

   It reads as accented and deliberate rather than alarming: nothing has
   gone wrong, but the subscriber has exactly one chance to act. The full
   accent border plus the heavier left edge is the same construction the
   error state uses for its own emphasis, so the page has one grammar for
   "this block is not like its neighbours" instead of two. */

.secret {
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  border-left: 3px solid var(--accent);
  /* Opaque, not the translucent --accent-soft the banners use: this block
     nests its own surfaces (the value, the buttons) and a translucent
     parent would let the card's glass show through all of them at once,
     flattening the very hierarchy the block exists to create. */
  background: color-mix(in srgb, var(--accent) 12%, var(--bg-soft));
  border-radius: var(--radius);
  padding: var(--space-4);
  margin-bottom: var(--gap);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* Focused programmatically when it appears (devices.js) so a screen reader
   lands on it. That focus is not a keyboard tab stop, so it gets no ring —
   :focus-visible in tokens.css still rings it if it is ever reached by
   keyboard. */
.secret:focus {
  outline: none;
}

.secret h3 {
  color: var(--accent);
}

.secret h4 {
  font-size: var(--text-sm);
  font-weight: 650;
  line-height: 1.25;
}

/* The warning is body text, not a colour flourish: it carries the whole
   consequence of losing the value, so it stays at readable weight and
   contrast rather than shrinking into a hint. */
.secret-warn {
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--text);
}

/* user-select: all makes one click take the whole value — a token broken
   across lines is easy to select short, and a truncated secret fails with
   an authentication error that says nothing about the real cause. The copy
   button remains the primary path; this is the manual fallback. */
.secret-value {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: 1.55;
  word-break: break-all;
  user-select: all;
  background: var(--bg);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  color: var(--text);
}

/* overflow-wrap, not the break-all the token block uses: a host name is
   short enough to fit on all but the narrowest screens, and break-all
   would split it mid-word even when it did fit. */
.secret-inline {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
  user-select: all;
  background: var(--bg);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-sm);
  padding: 1px 6px;
}

/* The one solid-fill button in the product. Everywhere else --accent-soft
   is enough separation, but here the button sits on an accent-tinted field
   and soft-on-soft would make the single action this block exists for the
   least visible thing in it. */
.secret .btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg-soft);
}

.secret .btn-primary:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent) 88%, var(--text));
}

/* A plain block list, not a flex column: a flex container blockifies its
   children and the browsers disagree about whether a list marker survives
   that. Numbered steps that silently lose their numbers would be a poor
   trade for one gap declaration. */
.secret-steps {
  margin: 0;
  padding-left: 20px;
  font-size: var(--text-sm);
  line-height: 1.65;
}

.secret-steps li + li {
  margin-top: var(--space-2);
}

.secret-steps strong {
  font-weight: 650;
}

/* ----------------------------------------------------------- panel states
   Four distinct states and they must not look alike (ui.js):
   empty (dashed, neutral), error (solid down-coloured edge + pill),
   loading (skeleton), insufficient (hatch). */

/* Empty states are load-bearing: a pilot ISP meets them on day one, before
   a single subscriber has consented. They must read as "nothing here yet",
   not as "something broke". Dashed neutral border, muted text, no red. */
.empty {
  border: 1px dashed var(--card-border);
  border-radius: var(--radius-sm);
  padding: 18px 16px;
  color: var(--muted);
  font-size: var(--text-sm);
  line-height: 1.55;
  text-align: left;
}

.empty strong {
  display: block;
  color: var(--text);
  font-size: 13px;
  font-weight: 600;
  margin-bottom: 4px;
}

.empty p + p {
  margin-top: 6px;
}

/* "Couldn't load" must not look like "nothing to show": solid border where
   empty is dashed, a down-coloured edge, and the pill above the title. */
.empty.state-error {
  border-style: solid;
  border-color: color-mix(in srgb, var(--status-down) 35%, transparent);
  border-left: 3px solid var(--status-down);
}

.empty.state-error .status-pill {
  margin-bottom: var(--space-2);
}

/* Insufficient data (coarse_loss): the hatch token, never a softer OK. */
.state-insufficient {
  background-image: var(--insufficient-hatch);
}

.loading {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-2) 0;
}

/* Loading skeleton. Duration derives from the motion token so
   prefers-reduced-motion (which zeroes --dur) stops the shimmer without
   this rule knowing why. */
.skeleton {
  border-radius: var(--radius-sm);
  background-color: var(--card);
  background-image: linear-gradient(90deg, transparent, var(--accent-soft), transparent);
  background-size: 200% 100%;
  animation: skeleton-sheen calc(var(--dur) * 7) var(--ease) infinite;
  min-height: 12px;
}

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

.spinner {
  color: var(--muted);
  font-size: var(--text-sm);
  padding: 6px 0;
}

/* State illustrations (ui.js stateArt): block-level above the wording,
   scaled down where a phone (or compact density) cannot spare the rows. */
.empty .state-art,
.fig .state-art {
  display: block;
  margin-bottom: var(--space-2);
}

:root[data-density="compact"] .state-art {
  width: 84px;
  height: 50px;
}

/* ------------------------------------------------------------ proportion
   The determinate bar (ui.js proportionBar): glass track, pill radius,
   fill colour supplied by the caller from a semantic token. The width
   transition covers a re-render with new counts; the insufficient variant
   is the hatch and nothing else — no fill exists to animate, because a
   partial fill below 300 probes would itself be a rate claim (§4.2). */
.bar-track {
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--status-unknown-soft);
  border: 1px solid var(--card-border);
  overflow: hidden;
  margin-top: 6px;
}

.bar-fill {
  height: 100%;
  border-radius: var(--radius-pill);
  transition: width var(--dur) var(--ease);
}

.bar-track.bar-insufficient {
  background-image: var(--insufficient-hatch);
}

/* ------------------------------------------------------------------ tables */

.scroll {
  overflow-x: auto;
  max-width: 100%;
}

/* Caps the subscriber list so the QoE panel below it stays reachable without
   scrolling past a long roster. */
.scroll-y {
  max-height: 340px;
  overflow-y: auto;
}

table {
  border-collapse: collapse;
  width: 100%;
  font-size: var(--text-sm);
}

th,
td {
  text-align: left;
  padding: 8px 10px;
  border-bottom: 1px solid var(--card-border);
  vertical-align: top;
}

thead th {
  color: var(--muted);
  font-weight: 600;
  white-space: nowrap;
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.4px;
}

tbody tr:last-child td {
  border-bottom: none;
}

td.num,
th.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

td.nowrap {
  white-space: nowrap;
}

tr.row-click {
  cursor: pointer;
}

tr.row-click td {
  transition: background-color var(--dur-fast) var(--ease);
}

tr.row-click:hover td {
  background: color-mix(in srgb, var(--text) 5%, transparent);
}

tr.row-active td {
  background: var(--accent-soft);
}

/* ------------------------------------------------------------- key/value */

.meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
  gap: 2px 20px;
  font-size: var(--text-sm);
}

.meta-row {
  display: flex;
  gap: var(--space-2);
  justify-content: space-between;
  border-bottom: 1px dotted var(--card-border);
  padding: 4px 0;
  min-width: 0;
}

.meta-row dt {
  color: var(--muted);
  white-space: nowrap;
}

.meta-row dd {
  font-variant-numeric: tabular-nums;
  text-align: right;
  word-break: break-all;
  min-width: 0;
}

.figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 130px), 1fr));
  gap: var(--space-2);
}

.fig {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 12px;
  border: 1px solid var(--card-border);
  border-radius: var(--radius-sm);
  min-width: 0;
}

.fig-num {
  font-size: 19px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.fig-label {
  color: var(--muted);
  font-size: var(--text-xs);
}

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

.tabs {
  display: inline-flex;
  gap: var(--space-1);
  background: var(--card);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-pill);
  padding: 3px;
}

.tab {
  background: transparent;
  border: none;
  color: var(--muted);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  padding: 6px 16px;
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}

.tab:hover {
  color: var(--text);
}

.tab.active {
  background: var(--accent-soft);
  color: var(--accent);
}

/* ----------------------------------------------------------- mode switch
   Same segmented shape as the tabs so it reads as "also navigation", but
   smaller: it changes framing, not place. Selection is carried by
   aria-pressed, so the styling can never drift from what a screen reader
   is told. Mirrors the desktop's. */

.mode-switch {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  background: var(--card);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-pill);
  padding: 3px;
}

.mode-switch:empty {
  display: none;
}

.mode-seg {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: transparent;
  border: none;
  color: var(--muted);
  font: inherit;
  font-size: var(--text-xs);
  font-weight: 600;
  padding: 5px 12px;
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}

/* The icon rides currentColor, so it goes muted→accent with the label and
   never needs its own state rules. */
.mode-seg svg {
  flex: none;
}

.mode-seg:hover {
  color: var(--text);
}

.mode-seg[aria-pressed="true"] {
  background: var(--accent-soft);
  color: var(--accent);
}

/* ------------------------------------------------------------ mode picker
   First-run overlay, skippable, honest one-liners. Mirrors the desktop. */

.picker-overlay {
  position: fixed;
  inset: 0;
  /* Scrim, not a themed surface: a translucent darkening reads correctly
     over both themes and there is no token for "behind a dialog". */
  background: rgba(11, 14, 20, 0.55);
  backdrop-filter: blur(6px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-5);
  z-index: 60;
}

.picker {
  background: var(--bg-soft);
  border: 1px solid var(--card-border);
  border-radius: var(--radius);
  padding: var(--space-6);
  max-width: 540px;
  width: 100%;
  max-height: 90vh;
  overflow-y: auto;
}

.picker h2 {
  font-size: var(--text-xl);
  margin-bottom: var(--space-2);
}

.picker-sub {
  color: var(--muted);
  font-size: var(--text-sm);
  line-height: 1.5;
  margin-bottom: var(--space-4);
}

.picker-opts {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.picker-opt {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  text-align: left;
  background: var(--card);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-sm);
  padding: var(--space-3) var(--space-4);
  color: var(--text);
  font: inherit;
  cursor: pointer;
  /* Same rise as the cards; transform+border only, on the motion tokens,
     so reduced-motion stills the picker along with everything else. */
  transition: border-color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}

.picker-opt:hover {
  border-color: var(--accent);
  transform: translateY(-1px);
}

.picker-opt .opt-icon {
  display: inline-flex;
  flex: none;
  color: var(--accent);
  margin-top: 2px;
}

.picker-opt .opt-text {
  min-width: 0;
}

.picker-opt .opt-label {
  display: block;
  font-weight: 650;
}

.picker-opt .opt-tagline {
  display: block;
  color: var(--muted);
  font-size: var(--text-sm);
  margin-top: 2px;
}

.picker-skip {
  margin-top: var(--space-4);
  background: none;
  border: none;
  color: var(--muted);
  text-decoration: underline;
  cursor: pointer;
  font: inherit;
  font-size: var(--text-sm);
}

/* ------------------------------------------------------------------- auth */

.auth-wrap {
  max-width: 420px;
  margin: 6vh auto 0;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
}

.auth-wrap .card {
  padding: 22px;
}

.auth-note {
  color: var(--muted);
  font-size: var(--text-xs);
  line-height: 1.5;
  text-align: center;
}

/* Divider between the password form and the federated button. The rule is
   drawn either side of the word rather than behind it, so it reads as a
   separator rather than as strikethrough text. */
.auth-or {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 16px 0 12px;
  color: var(--muted);
  font-size: var(--text-xs);
}

.auth-or::before,
.auth-or::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--card-border);
}

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

.chart {
  width: 100%;
  overflow-x: auto;
}

.chart svg {
  display: block;
  width: 100%;
  height: auto;
  max-width: 100%;
}

.legend {
  display: flex;
  gap: var(--space-4);
  flex-wrap: wrap;
  font-size: var(--text-xs);
  color: var(--muted);
  margin-top: var(--space-2);
}

.legend span {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.swatch {
  width: 14px;
  height: 3px;
  border-radius: 2px;
  display: inline-block;
}

/* ----------------------------------------------------------------- thread */

.msg {
  border: 1px solid var(--card-border);
  border-radius: var(--radius-sm);
  padding: 10px 13px;
  background: var(--card);
}

.msg + .msg {
  margin-top: 10px;
}

.msg-head {
  display: flex;
  justify-content: space-between;
  gap: var(--space-2);
  flex-wrap: wrap;
  font-size: var(--text-xs);
  color: var(--muted);
  margin-bottom: 6px;
}

.msg-author {
  font-weight: 650;
  color: var(--text);
}

.msg-body {
  font-size: 13px;
  white-space: pre-wrap;
  word-break: break-word;
}

.msg-isp {
  border-color: color-mix(in srgb, var(--accent) 30%, transparent);
  background: var(--accent-soft);
}

/* ------------------------------------------------------------ transitions
   View-level motion, opacity+transform only (the QoE chart is SVG inside
   these containers and layout animation would smear any redraw). The
   animation runs each time a .view-fade element goes display:none→shown,
   because a freshly-displayed element starts its animations — no JS hook
   needed for the auth→app swap. */

.view-fade {
  animation: view-enter var(--dur-slow) var(--ease);
}

@keyframes view-enter {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
}

/* One fade over the whole card column when a mode reorders it (mode-ui.js
   re-triggers the class). Opacity only: the cards move by flex order, and
   animating their positions would imply continuity the reorder does not
   have. Overrides .view-fade's animation while present — by then the
   entry animation has long finished. */
.mode-fade {
  animation: mode-fade var(--dur) var(--ease);
}

@keyframes mode-fade {
  from {
    opacity: 0.3;
  }
}

/* ------------------------------------------------------------------ toast */

.toast {
  position: fixed;
  left: 50%;
  bottom: 22px;
  transform: translateX(-50%);
  background: var(--bg-soft);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-pill);
  padding: 8px 18px;
  font-size: var(--text-sm);
  font-weight: 600;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
  z-index: 50;
  max-width: min(92vw, 520px);
  text-align: center;
}

.toast-err {
  color: var(--status-down);
  border-color: color-mix(in srgb, var(--status-down) 45%, transparent);
}

.toast-ok {
  color: var(--status-ok);
  border-color: color-mix(in srgb, var(--status-ok) 40%, transparent);
}

/* ------------------------------------------------------------- responsive
   The dashboard must work from a 360px phone up — an ISP support agent
   will open it mid-call. Grids already collapse via minmax(min(100%,…));
   what remains is trimming the shell and letting the topbar stack. */

/* Same 720px threshold as the desktop, so the two surfaces stack the
   evidence columns at the same width and screenshots stay comparable. */
@media (max-width: 720px) {
  .evidence-pair {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 480px) {
  .shell {
    padding: 14px 12px 48px;
  }

  /* The strapline is decoration; the controls are not. */
  .brand-sub {
    display: none;
  }

  .auth-wrap {
    margin-top: 3vh;
  }

  .picker {
    padding: var(--space-4);
  }
}

/* On a 360px phone the illustrations give their rows back to the words. */
@media (max-width: 400px) {
  .state-art {
    width: 76px;
    height: 46px;
  }
}
