/* Refinements layered over the imported design.
 *
 * dash.css and home.css are both regenerated by the porters, so anything
 * hand-written there is lost on the next design import. This file is never
 * generated and is linked LAST on both faces, so its overrides survive.
 *
 * Three jobs: a larger type scale, scrollbars people can actually see and use,
 * and killing the layout flash on load.
 */

/* ── 1. type scale ────────────────────────────────────────────────
   The kit's body text was 0.9375rem (15px) with 13px and 11px steps beneath
   it, which is comfortable for a designer on a 27" display and genuinely hard
   work for everyone else. Everything moves up roughly one step: body to 17px,
   and the two small steps grow proportionally more, since they were the ones
   doing the real damage. The scale stays fluid, so it still tightens on a
   phone rather than wrapping every label onto three lines. */
:root {
  --t-h1:   clamp(1.75rem, 1.7vw + 1.1rem, 2.35rem);
  --t-h2:   clamp(1.25rem, 0.5vw + 1.1rem, 1.4rem);
  --t-h3:   clamp(1.125rem, 0.35vw + 1rem, 1.22rem);
  --t-body: clamp(1.0625rem, 0.25vw + 1rem, 1.125rem);
  --t-sm:   clamp(0.9688rem, 0.2vw + 0.92rem, 1rem);
  --t-xs:   clamp(0.875rem, 0.15vw + 0.84rem, 0.9062rem);
}

/* The root size drives every rem above; 16px was already the browser default
   but the kit re-declared it, so it is restated here to keep one source. */
html { font-size: 100%; }
body { font-size: var(--t-body); line-height: 1.6; }

/* Small-caps eyebrow labels were 11px with heavy letter-spacing — the single
   least readable thing in the console. Give the tracking back some size. */
.stat-l, .rail-h, .badge, .chip, .th, thead th {
  font-size: var(--t-xs);
  letter-spacing: 0.06em;
}

/* Numbers are what customers came for: prices, balances, countdowns. */
.stat-v { font-size: clamp(1.65rem, 1.2vw + 1.2rem, 2.1rem); }
.cell-num, .price, .money { font-size: var(--t-body); font-variant-numeric: tabular-nums; }

/* Tables inherited the small step for density. Density matters less than
   being able to read the row. */
td, th { font-size: var(--t-sm); }

/* Respect a larger OS text size instead of fighting it. */
@media (min-width: 1040px) {
  .wrap { max-inline-size: 1400px; }
}

/* ── 2. visible scrollbars ────────────────────────────────────────
   The kit hid scrollbars on its inner panes and left the page scrollbar to the
   browser's overlay style, so on a long list there was no indication that
   there was more below, and nothing to grab with a mouse. These are styled to
   match the surface palette but are deliberately always visible and wide
   enough to hit. */
* {
  scrollbar-width: auto;
  scrollbar-color: var(--surface-3) transparent;
}

::-webkit-scrollbar {
  inline-size: 12px;
  block-size: 12px;
}

::-webkit-scrollbar-track {
  background: var(--surface-1, transparent);
  border-radius: 999px;
}

::-webkit-scrollbar-thumb {
  background: var(--surface-3);
  border: 3px solid transparent;
  background-clip: padding-box;
  border-radius: 999px;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--text-3);
  background-clip: padding-box;
}

::-webkit-scrollbar-corner { background: transparent; }

/* The option lists set their own 8px bar; widen them to match the rest. */
.optlist::-webkit-scrollbar,
.rail::-webkit-scrollbar,
.table-scroll::-webkit-scrollbar {
  inline-size: 12px;
  block-size: 12px;
}

/* A scrollable pane should look scrollable before you touch it. */
.optlist, .table-scroll { scrollbar-gutter: stable; }

/* ── 3. no flash on load ──────────────────────────────────────────
   shell.js inserts the topbar and rail as the first children of .app. Until it
   runs, .main is the only child, so on a desktop grid it auto-places into the
   narrow rail column — the page renders as a squeezed strip for one frame and
   then jumps. That is the flicker on refresh.

   Both rules below are scoped to `.main:first-child`, which is true ONLY while
   the shell is missing. The moment shell.js prepends the topbar, .main stops
   being the first child and these stop applying on their own — no JS, no
   class toggling, and nothing to un-set. */
.app > .main:first-child { padding-block-start: var(--head); }

@media (min-width: 1040px) {
  .app > .main:first-child { grid-column: 2; }
}

/* ── 4. shared chrome corrections ─────────────────────────────────
   port-chrome.php scopes the homepage's design tokens to .masthead/.foot so
   they cannot restyle pages owned by app.css. That scoping is right, but the
   tokens were previously applied by the homepage setting `color` on <body> —
   which no longer happens off the homepage. The result was chrome that
   inherited app.css's text colour, so headings like "Your next code is two
   seconds away" rendered dark-on-dark and vanished.

   Applying the scoped tokens on the chrome roots is the actual fix, and it
   cures every invisible-text case in one rule rather than per element. */
.masthead,
.foot {
  color: var(--text);
}

.foot a,
.masthead a { color: inherit; }

.foot-top-t { color: var(--text); }
.foot-top-s,
.foot-tag,
.foot-note,
.foot-col a { color: var(--text-3); }

.foot-col a:hover,
.masthead .nav-link:hover { color: var(--accent); }

/* Footer takes the header's background so the page is bookended by the same
   colour instead of the darker --bg-sunk it used to sit on. */
.foot {
  background: var(--masthead-bg);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
  backdrop-filter: blur(14px) saturate(1.4);
}

/* ── header ───────────────────────────────────────────────────────
   The masthead's bottom border resolved to a fully transparent colour, so the
   header had no edge at all and its translucent background blended straight
   into the page — the "everything mixed together" look. A real hairline gives
   it a defined bottom, and a matching one under the footer's top edge keeps
   the two consistent. */
.masthead {
  border-block-end: 1px solid color-mix(in srgb, currentColor 14%, transparent);
}

.foot {
  border-block-start: 1px solid color-mix(in srgb, currentColor 14%, transparent);
}

/* The nav carried `margin-inline-end: auto`, which pushed everything after it
   to the far right and left the links sitting against the logo. Moving the
   auto margin to the start pushes the nav itself right, so it sits with the
   language picker and the call to action instead of crowding the brand. */
@media (min-width: 940px) {
  .masthead-nav {
    margin-inline-start: auto;
    margin-inline-end: 0;
  }

  /* masthead-end previously cancelled its own auto margin because the nav was
     doing the pushing. The nav no longer is, so it must not push either —
     otherwise the two fight and a gap opens between them. */
  .masthead-end { margin-inline-start: 0; }
}

/* ── dashboard overview: the two top panels ───────────────────────
   The row used `align-items: start`, so each column took its natural height
   and the live-window panel stopped well short of "Buy again" beside it.
   Stretching the row and letting the panel grow squares them off.

   Scoped to the overview screen. The buy page reuses the same .buy class for a
   completely different layout — a stack of picker cards beside one panel — and
   stretching there inflated the last card ("Max price") into a tall box that
   was mostly empty space. Two pages, one class name, opposite requirements. */
body[data-page="overview"] .buy { align-items: stretch; }

body[data-page="overview"] .buy > section {
  display: flex;
  flex-direction: column;
  min-block-size: 0;
}

body[data-page="overview"] .buy > section > .flight,
body[data-page="overview"] .buy > section > .card { flex: 1; }

/* ── the Manual / Smart segmented control ─────────────────────────
   The "NEW" badge is violet on a near-transparent wash, which is legible on
   the neutral track but disappears the moment its segment turns accent-green.
   On the selected segment it becomes a solid chip in the accent's ink colour,
   so it reads on either ground. */
.seg-opt.is-on .tag-new {
  background: color-mix(in srgb, var(--accent-ink) 22%, transparent);
  color: var(--accent-ink);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-ink) 38%, transparent);
}

/* The selected segment carries the page's primary action, so give it the
   weight to match and a clear edge against the track behind it. */
.seg-opt.is-on {
  font-weight: 700;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.16);
}

/* The empty state sat at the top of a now-taller panel. Centring it means the
   height change reads as deliberate rather than as a gap. */
.flight-body {
  flex: 1;
  align-content: center;
}

/* "no active window" was set in the monospace face, which read as a code
   fragment rather than a heading. It now matches the card titles beside it —
   same family, weight and tracking, sized to sit in the chrome bar. */
.flight-label {
  font-family: inherit;
  font-size: var(--t-body);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--text-2);
}

/* The step indicator now sits directly in the .buy grid (moved there by the
   porter), so it spans both columns. Both the picker column and the live-order
   panel then begin on the same line, without either being padded to fake it. */
.buy > .steps-bar {
  grid-column: 1 / -1;
  margin-block-end: 4px;
}

/* ── "Two ways to buy" card ───────────────────────────────────────
   The card was a wall of paragraphs, which nobody reads on a page they came
   to act on. Each mode now gets a tinted icon, a tag, and its three steps as a
   flow strip, so the difference between Manual and Smart is legible before a
   single sentence is read. */
.modes { display: grid; gap: 10px; margin-block-start: 16px; }

.mode {
  display: flex;
  gap: 12px;
  padding: 13px;
  border: 1px solid var(--line-soft);
  border-radius: 13px;
  background: var(--surface-2);
}

.mode-ic {
  flex: none;
  inline-size: 36px;
  block-size: 36px;
  display: grid;
  place-items: center;
  border-radius: 10px;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 13%, transparent);
}

.mode-ic svg { inline-size: 19px; block-size: 19px; }

/* Smart buy is the shortcut, so it reads warm against Manual's brand green —
   the two modes should not look interchangeable. */
.mode-ic-alt {
  color: var(--warn);
  background: color-mix(in srgb, var(--warn) 15%, transparent);
}

.mode-body { min-inline-size: 0; }

.mode-t {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: var(--t-body);
  font-weight: 700;
  letter-spacing: -0.015em;
}

.mode-tag {
  padding: 2px 8px;
  border-radius: 999px;
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 13%, transparent);
}

.mode-tag.alt {
  color: var(--warn);
  background: color-mix(in srgb, var(--warn) 15%, transparent);
}

/* The three steps, chevron-separated. A real sequence, so an ordered list. */
.flow {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin: 8px 0 0;
  padding: 0;
  list-style: none;
}

.flow li {
  padding: 3px 9px;
  border-radius: 7px;
  background: var(--surface-3);
  font-size: var(--t-xs);
  font-weight: 650;
  color: var(--text-2);
  white-space: nowrap;
}

.flow li + li::before {
  content: "→";
  margin-inline-end: 7px;
  margin-inline-start: -3px;
  color: var(--text-3);
  font-weight: 400;
}

.mode-p {
  margin-block-start: 8px;
  color: var(--text-3);
  font-size: var(--t-sm);
  line-height: 1.55;
  text-wrap: pretty;
}

.feat-h {
  margin-block: 18px 10px;
  padding-block-start: 15px;
  border-block-start: 1px solid var(--line-soft);
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-3);
}

.feats { display: grid; gap: 11px; margin: 0; padding: 0; list-style: none; }

.feats li {
  display: flex;
  gap: 11px;
  align-items: flex-start;
  color: var(--text-2);
  font-size: var(--t-sm);
  line-height: 1.5;
}

.feats b { color: var(--text); font-weight: 650; }

.feat-ic {
  flex: none;
  inline-size: 26px;
  block-size: 26px;
  display: grid;
  place-items: center;
  border-radius: 8px;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

.feat-ic svg { inline-size: 14px; block-size: 14px; }

/* ── "Code not arriving?" card (order page) ───────────────────────
   Someone reading this is mid-problem and scanning for the line that matches
   their situation, so each case leads with the symptom in a heavier weight and
   the fix follows underneath. The icon carries the tone: neutral, warning, or
   the reassuring one for the case that costs nothing. */
.fixes {
  display: grid;
  gap: 14px;
  margin: 16px 0 0;
  padding: 0;
  list-style: none;
}

.fixes li {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 13px;
  border: 1px solid var(--line-soft);
  border-radius: 13px;
  background: var(--surface-2);
}

.fix-ic {
  flex: none;
  inline-size: 34px;
  block-size: 34px;
  display: grid;
  place-items: center;
  border-radius: 10px;
  color: var(--text-2);
  background: var(--surface-3);
}

.fix-ic svg { inline-size: 18px; block-size: 18px; }

.fix-ic-warn {
  color: var(--warn);
  background: color-mix(in srgb, var(--warn) 15%, transparent);
}

.fix-ic-ok {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 13%, transparent);
}

.fix-t {
  font-size: var(--t-body);
  font-weight: 700;
  letter-spacing: -0.015em;
  text-wrap: balance;
}

.fix-p {
  margin-block-start: 5px;
  color: var(--text-3);
  font-size: var(--t-sm);
  line-height: 1.55;
  text-wrap: pretty;
}

/* The one line worth remembering, so it gets to stand on its own. */
.fix-note {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-block-start: 14px;
  padding: 12px 13px;
  border-radius: 11px;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  font-size: var(--t-sm);
  font-weight: 650;
}

.fix-note svg { inline-size: 17px; block-size: 17px; flex: none; display: block; }

/* ── the two copy buttons (order page) ────────────────────────────
   Each is labelled with what it puts on the clipboard, because two identical
   copy icons side by side is a coin toss. */
/* Two copy buttons: the national number, and the full international one.

   The kit's .copy-btn is a fixed 40px square. Adding a text label inside it
   without releasing that width made the label overflow its own box and the two
   buttons overlap — which is exactly what it looked like. The width is released
   here and the label is a compact chip that sits under the icon, so each button
   still reads as a square target rather than a stretched pill. */
.copy-pair {
  display: inline-flex;
  gap: 6px;
  flex: none;
  align-items: stretch;
}

.copy-pair .copy-btn {
  inline-size: auto;              /* the 40px square is the thing to undo */
  min-inline-size: 44px;
  block-size: auto;
  min-block-size: 44px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 5px 9px;
  line-height: 1;
}

.copy-pair .copy-btn svg { inline-size: 15px; block-size: 15px; }

.copy-tag {
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  opacity: 0.85;
}

/* The labels stay on small screens. Hiding them left two identical copy icons
   side by side with no way to tell which was which — a title attribute is no
   help on a touch device, where there is no hover. Two 44px targets and their
   labels fit fine; it was only ever the desktop assumption that they wouldn't. */
@media (max-width: 560px) {
  .copy-pair { gap: 5px; }
  .copy-pair .copy-btn { padding: 5px 7px; }
  .copy-tag { font-size: 0.58rem; }
}

/* ── starred routes panel ─────────────────────────────────────────
   The row is a <button>, so the remove control sits beside it rather than
   inside it — nesting buttons is invalid and the inner one stops receiving
   clicks. The wrapper keeps them reading as one row. */
.favrow {
  position: relative;
  display: block;
}

.favrow .opt { inline-size: 100%; padding-inline-end: 44px; }

.favdel {
  position: absolute;
  inset-inline-end: 8px;
  inset-block-start: 50%;
  transform: translateY(-50%);
  inline-size: 26px;
  block-size: 26px;
  display: grid;
  place-items: center;
  font: inherit;
  font-size: 1.05rem;
  line-height: 1;
  color: var(--text-3);
  background: none;
  border: 0;
  border-radius: 7px;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.15s var(--ease), color 0.15s var(--ease), background 0.15s var(--ease);
}

/* Revealed on hover or focus so it never competes with the row itself, but
   always reachable from the keyboard and always present on touch. */
.favrow:hover .favdel,
.favdel:focus-visible { opacity: 1; }

@media (hover: none) { .favdel { opacity: 1; } }

.favdel:hover {
  color: var(--danger);
  background: color-mix(in srgb, var(--danger) 12%, transparent);
}

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

/* The list keeps `overflow-y: auto` from .optlist even when its height cap is
   lifted, so the final row sat flush against the scroll container's edge and
   its rounded bottom corner was shaved off. A little padding inside the
   scroller — not on the card — keeps the last row clear of the boundary
   whether the list scrolls or not. */
#flight-body .optlist {
  padding-block-end: 8px;
  padding-inline-start: 2px;
}

/* Rows carry a shadow on hover; without this the scroller clips it. */
#flight-body .optlist .favrow { padding-block-end: 1px; }

/* The star on each country row — the first point at which a whole route
   (service + country) exists and can therefore be saved. */
.favstar {
  position: absolute;
  inset-inline-end: 8px;
  inset-block-start: 50%;
  transform: translateY(-50%);
  inline-size: 28px;
  block-size: 28px;
  display: grid;
  place-items: center;
  color: var(--text-3);
  background: none;
  border: 0;
  border-radius: 7px;
  cursor: pointer;
  transition: color 0.15s var(--ease), transform 0.15s var(--ease);
}

.favstar svg { inline-size: 15px; block-size: 15px; }
.favstar:hover { color: var(--warn); transform: translateY(-50%) scale(1.15); }
.favstar.is-on { color: var(--warn); }
.favstar.is-on svg { fill: currentColor; }
.favstar:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Room for the star so it never sits on top of the price. */
.favrow .opt { padding-inline-end: 46px; }

/* ── panel refresh control ────────────────────────────────────────
   Replaces the LIVE badge when the panel is listing starred routes: nothing
   about that list is live, and the customer needs a way to re-pull it. */
/* ── Replace number ───────────────────────────────────────────────
   Disabled while the wait runs, with the remaining time inside the button
   rather than beside it — the control and its condition are one thing, so the
   customer can see the option arriving instead of wondering where it went. */
.btn-replace:disabled {
  background: var(--surface-3);
  color: var(--text-3);
  box-shadow: none;
  cursor: default;
  opacity: 1;                 /* the grey IS the disabled signal */
}

.btn-replace:disabled:hover { transform: none; }

/* Every action in this row now carries a countdown chip, so every one of them
   needs to be told not to break — fixing only Replace just moved the wrap onto
   "Cancel & refund" once it gained its own timer.

   They size to their content rather than splitting the row evenly, because the
   three labels are very different lengths and an even split is what forced the
   longest one onto two lines. The row still wraps, so if they genuinely do not
   fit they stack instead of squeezing. */
/* Three actions on one row, each with its own countdown.

   Side by side they get ~160px each, which is not enough for an icon, a full
   label and a timer on a single line — that is what pushed Replace off the
   edge. So the content stacks INSIDE each button: icon and label on top, the
   countdown beneath. The row stays one row, every label stays whole, and each
   timer belongs unambiguously to the control above it. */
#flight-body .btn,
.flight-body .btn {
  flex: 1 1 0;
  min-inline-size: 0;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding-inline: 10px;
  min-block-size: 48px;
  white-space: nowrap;
}

/* Everything on one line: icon, label, timer. The label is allowed to ellipsis
   rather than wrap, so a narrow panel shortens the text instead of doubling the
   button's height. */
#flight-body .btn > span:first-of-type {
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

#flight-body .btn .gate-count,
#flight-body .btn .rep-clock {
  flex: none;
  margin: 0;
  padding-inline: 5px;
  font-size: 0.68rem;
}

/* On a phone there is no room for three columns, so they stack and the content
   returns to a single line per button. */
@media (max-width: 620px) {
  #flight-body .btn,
  .flight-body .btn {
    flex: 1 0 100%;
    inline-size: 100%;
    flex-direction: row;
    gap: 8px;
    min-block-size: 48px;
  }

  #flight-body .btn .gate-count,
  #flight-body .btn .rep-clock { margin-inline-start: auto; }
}

/* Deliberately no grow on Replace either. Letting it expand pushed it past the
   width the row had left, so it wrapped onto a line of its own and then
   stretched across it. At natural width all three sit on one line. */
.btn-replace { flex: 0 1 auto; }

/* The shared unlock hint, sitting under the three actions rather than
   duplicating the same countdown inside two of them. */
.gate-hint {
  margin-block-start: 9px;
  font-size: var(--t-xs);
  line-height: 1.45;
  text-wrap: pretty;
}

.gate-hint[hidden] { display: none !important; }

/* The unlock countdown, warming as it runs down. Colour is the signal that the
   wait is going somewhere; a static grey number reads as stuck. */
.gate-count {
  display: inline-block;
  min-inline-size: 34px;
  padding-inline: 6px;
  border-radius: 999px;
  font-family: var(--mono);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: center;
  transition: color 0.5s var(--ease), background 0.5s var(--ease);
}

/* Inside a flex button an author `display` beats the UA [hidden] rule, so the
   chip needs the same explicit guard the refresh button does. */
.gate-count[hidden] { display: none !important; }

.gate-count.is-far {
  color: var(--text-3);
  background: color-mix(in srgb, currentColor 12%, transparent);
}

.gate-count.is-near {
  color: var(--warn);
  background: color-mix(in srgb, var(--warn) 15%, transparent);
}

/* Nearly there — green, and a slow pulse so it draws the eye back. */
.gate-count.is-soon {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  animation: gate-pulse 1s ease-in-out infinite;
}

@keyframes gate-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.55; }
}

@media (prefers-reduced-motion: reduce) {
  .gate-count.is-soon { animation: none; }
}

/* Icons sit with their labels rather than crowding them. */
#flight-body .btn svg {
  inline-size: 16px;
  block-size: 16px;
  flex: none;
}

#flight-body .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

.rep-clock {
  margin-inline-start: 8px;
  padding-inline: 7px;
  border-radius: 999px;
  font-family: var(--mono);
  font-size: var(--t-xs);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  background: color-mix(in srgb, currentColor 14%, transparent);
}

.rep-clock:empty { display: none; }

/* ── order page on a phone ────────────────────────────────────────
   Three actions sharing one row is a desktop assumption. At phone width each
   got about a third of the screen, so "Ban number" and "Cancel & refund" broke
   onto two lines and "Replace number" was pushed off the edge with its
   countdown. They stack full width instead — one line each, full-width touch
   targets, and nothing hidden. */
@media (max-width: 620px) {
  #flight-body > div:last-child,
  .flight-body > div:last-child {
    display: grid !important;
    grid-template-columns: 1fr;
    gap: 8px;
  }

  #flight-body .btn,
  #flight-body a.btn {
    flex: 1 1 auto !important;
    inline-size: 100%;
    white-space: nowrap;
  }

  /* Replace keeps its clock on the same line as its label. */
  .btn-replace { justify-content: center; }
}

/* Reading order on a phone: the live number first, then what it is, then what
   has happened to it, and only then the troubleshooting. The help card sits in
   the left column beside the order, so stacking columns naively put it above
   Order details and Activity — advice before the thing it is about.

   `display: contents` dissolves the two columns so all four cards become direct
   grid children and can be ordered individually. */
@media (max-width: 900px) {
  body[data-page="order"] .buy > section { display: contents; }

  body[data-page="order"] .flight { order: 1; }
  body[data-page="order"] #meta-card { order: 2; }
  body[data-page="order"] .picker-col > .card:not(#meta-card) { order: 3; }
  body[data-page="order"] .buy > section:first-child > .card { order: 4; }

  /* The margin that separated the help card from the panel above it is wrong
     once the two are no longer adjacent. */
  body[data-page="order"] .buy > section:first-child > .card { margin-block-start: 0; }
}

/* ── switching between running orders ─────────────────────────────
   Appears only when more than one number is live. Each chip carries the brand,
   the country and — once it has arrived — the code, so the right order can be
   picked without opening each in turn. */
/* "Running now" — its own card below the live panel, one order per line.
   A strip of chips inside the panel could not show the number or the code, and
   those are exactly what someone holding several numbers is scanning for. */
.order-switch { margin-block-start: 14px; }

.order-switch[hidden] { display: none !important; }

.runlist {
  display: grid;
  gap: 6px;
  margin-block-start: 12px;
  max-block-size: 320px;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.runrow {
  display: flex;
  align-items: center;
  gap: 12px;
  inline-size: 100%;
  padding: 10px 12px;
  font: inherit;
  color: inherit;
  text-align: start;
  background: var(--surface-2);
  border: 1px solid transparent;
  border-radius: 12px;
  cursor: pointer;
  transition: background 0.15s var(--ease), border-color 0.15s var(--ease);
}

.runrow:hover { background: var(--surface-3); }

/* The one being viewed is marked, so switching never leaves you guessing
   which order the panel above is showing. */
.runrow.is-on {
  background: var(--accent-wash);
  border-color: color-mix(in srgb, var(--accent) 42%, transparent);
}

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

.run-body { flex: 1; min-inline-size: 0; display: grid; gap: 2px; }

.run-name {
  font-size: var(--t-sm);
  font-weight: 650;
  letter-spacing: -0.01em;
}

.run-meta {
  font-family: var(--mono);
  font-size: var(--t-xs);
  color: var(--text-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.run-code {
  flex: none;
  padding-inline: 8px;
  padding-block: 3px;
  border-radius: 999px;
  font-family: var(--mono);
  font-size: var(--t-sm);
  font-weight: 700;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}

.run-wait {
  flex: none;
  font-size: var(--t-xs);
  font-weight: 650;
  color: var(--text-3);
}

.order-switch[hidden] { display: none !important; }

.oswitch {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-block-size: 34px;
  padding-inline: 11px;
  font: inherit;
  font-size: var(--t-sm);
  font-weight: 650;
  color: var(--text-2);
  background: var(--surface-2);
  border: 1px solid var(--line-soft);
  border-radius: 999px;
  cursor: pointer;
  white-space: nowrap;
  transition: color 0.15s var(--ease), background 0.15s var(--ease),
              border-color 0.15s var(--ease);
}

.oswitch:hover { color: var(--text); background: var(--surface-3); }

.oswitch.is-on {
  color: var(--text);
  background: var(--accent-wash);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}

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

.oswitch .bmark { border-radius: 4px; flex: none; }

.oswitch-geo { color: var(--text-3); font-weight: 500; }

/* A chip whose code has landed says so, so the finished one is obvious
   without opening it. */
.oswitch-code {
  padding-inline: 6px;
  border-radius: 999px;
  font-family: var(--mono);
  font-size: var(--t-xs);
  font-weight: 700;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}

@media (max-width: 560px) {
  .oswitch-geo { display: none; }
}

/* ── panel tabs: Running / Saved ──────────────────────────────────
   The panel used to show one or the other, and a running order always won —
   so a customer with a live number could not reach their saved routes at all,
   nor confirm that starring one had worked. The counts sit on the labels so
   neither tab has to be opened to be understood. */
.flight-tabs {
  display: inline-flex;
  gap: 3px;
  padding: 3px;
  border-radius: 999px;
  background: color-mix(in srgb, currentColor 7%, transparent);
}

.ftab {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-block-size: 28px;
  padding-inline: 11px;
  font: inherit;
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--text-3);
  background: none;
  border: 0;
  border-radius: 999px;
  cursor: pointer;
  white-space: nowrap;
  transition: color 0.15s var(--ease), background 0.15s var(--ease);
}

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

.ftab.is-on {
  color: var(--accent-ink);
  background: var(--accent);
}

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

.ftab-n {
  min-inline-size: 18px;
  padding-inline: 5px;
  border-radius: 999px;
  font-variant-numeric: tabular-nums;
  background: color-mix(in srgb, currentColor 16%, transparent);
}

/* The refresh control loses its label beside the tabs — the icon and its
   accessible name carry it, and the header has no room for both. */
.flight-refresh { padding-inline: 9px; }

@media (max-width: 560px) {
  .ftab { padding-inline: 8px; letter-spacing: 0; }
}

/* An author `display` beats the UA stylesheet's [hidden] rule, so without this
   the button stayed visible whatever the attribute said — LIVE and REFRESH were
   showing at the same time. Any element hiding itself via the attribute needs
   the same guard. */
.flight-refresh[hidden],
.flight-live[hidden] { display: none !important; }

.flight-refresh {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-block-size: 28px;
  padding-inline: 10px;
  font: inherit;
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 11%, transparent);
  border: 0;
  border-radius: 999px;
  cursor: pointer;
}

.flight-refresh:hover { background: color-mix(in srgb, var(--accent) 18%, transparent); }
.flight-refresh:disabled { opacity: 0.6; cursor: default; }
.flight-refresh:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.flight-refresh svg { inline-size: 14px; block-size: 14px; }

.flight-refresh.is-spinning svg { animation: fr-spin 0.7s linear infinite; }

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

@media (prefers-reduced-motion: reduce) {
  .flight-refresh.is-spinning svg { animation: none; }
}

/* ── one voice for panel titles ───────────────────────────────────
   Card headings and the live-panel label were set differently, so sections
   sitting side by side looked like they came from two designs. They now share
   family, size, weight, tracking and colour. */
.card-t,
.flight-label,
.pricing-detail-t {
  font-family: inherit;
  font-size: var(--t-h3);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--text);
}

/* ── running-orders pager (buy page) ──────────────────────────────
   Sits under the list of active orders. Deliberately quiet: the rows are the
   content, the pager is a control, so it borrows the list's spacing rather
   than introducing a second visual language. */
.flight-pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-block-start: 12px;
  padding-block-start: 12px;
  border-block-start: 1px solid var(--line-soft);
}

.flight-pager-btns { display: flex; gap: 5px; }

.pgb {
  min-inline-size: 32px;
  min-block-size: 32px;
  font: inherit;
  font-size: var(--t-xs);
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  color: var(--text-2);
  background: var(--surface-2);
  border: 1px solid var(--line-soft);
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.15s var(--ease), color 0.15s var(--ease);
}

.pgb:hover { color: var(--text); background: var(--surface-3); }

.pgb.is-on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

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

/* The hero demo card's country flag. Sized to sit on the text baseline of the
   chip beside it, with the same soft corner as the sprite it replaced. */
.rp-flag {
  inline-size: 22px;
  block-size: 16px;
  border-radius: 3px;
  object-fit: cover;
  flex: none;
}

/* ── footer polish ────────────────────────────────────────────────
   A hairline of brand colour along the top edge, so the footer reads as a
   deliberate close to the page rather than the point where content stopped. */
.foot { position: relative; }

.foot::before {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-start: 0;
  block-size: 2px;
  background: linear-gradient(
    90deg,
    transparent,
    color-mix(in srgb, var(--accent) 55%, transparent) 22%,
    color-mix(in srgb, var(--accent) 55%, transparent) 78%,
    transparent
  );
}

/* Column headings sat at body weight and read as another link. Small, tracked
   and quiet separates the label from the things under it. */
.foot-h {
  margin: 0 0 12px;
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  opacity: 0.55;
}

.foot-col { display: grid; gap: 2px; align-content: start; }

/* Links get a real hit area and shift slightly on hover, which reads as
   responsive without moving the layout around them. */
.foot-col a {
  padding-block: 6px;
  font-size: var(--t-sm);
  text-decoration: none;
  transition: color 0.18s ease, transform 0.18s ease;
}

.foot-col a:hover { transform: translateX(3px); }

.foot-tag { max-inline-size: 34ch; line-height: 1.6; }

/* The live dot was static; a slow pulse makes the status read as current. */
.foot-status-dot { animation: foot-blip 3.2s ease-in-out infinite; }

@keyframes foot-blip {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: 0.45; transform: scale(0.82); }
}

@media (prefers-reduced-motion: reduce) {
  .foot-status-dot { animation: none; }
  .foot-col a:hover { transform: none; }
}

/* Base row: copyright left, notice centred, contact right. */
.foot-base {
  display: flex;
  align-items: center;
  gap: 14px 26px;
  flex-wrap: wrap;
  padding-block: 18px;
  font-size: var(--t-xs);
}

.foot-mail { margin-inline-start: auto; }

.foot-mail a,
a.mailx {
  color: inherit;
  text-decoration: none;
  border-block-end: 1px solid color-mix(in srgb, currentColor 35%, transparent);
}

a.mailx:hover { color: var(--accent); border-block-end-color: var(--accent); }

/* Until mail.js runs the address shows as "support (at) sms.rip"; the marker
   is spaced so it reads as a word, not punctuation. */
.mailx-at { padding-inline: 2px; opacity: 0.75; }

/* The safety notice wrapped onto two or three lines and pushed the footer
   taller than it needed to be. It stays on one line wherever there is room to
   fit it, and is allowed to wrap again on narrow screens — forcing nowrap
   there would overflow the viewport rather than save space. */
@media (min-width: 900px) {
  .foot-base { flex-wrap: nowrap; gap: 22px; }
  .foot-note { white-space: nowrap; font-size: var(--t-xs); }
}

/* The language menu is part of the header, so it should read as the same
   surface rather than a lighter card floating over it. */
.picker-panel {
  background: var(--masthead-bg);
  -webkit-backdrop-filter: blur(18px) saturate(1.5);
  backdrop-filter: blur(18px) saturate(1.5);
}

/* ── 5. pricing explorer ──────────────────────────────────────────
   The two panes held their own heights, so the shorter one stopped well above
   the taller and the row looked broken. Stretching them to a common height
   and letting each pane scroll internally keeps the pair square. */
.pricing-grid { align-items: stretch; }

.pricing-pane {
  display: flex;
  flex-direction: column;
  min-block-size: 0;
}

/* The scrolling child must be allowed to shrink below its content — without
   min-block-size:0 a flex item floors at its intrinsic height, so the pane grew
   to fit all 200 rows (≈10,000px) instead of scrolling inside a fixed frame. */
.pricing-list,
.pricing-table-wrap {
  flex: 1;
  min-block-size: 0;
  max-block-size: none;   /* the pane owns the height now, not the child */
  overflow-y: auto;
  overscroll-behavior: contain;
}

@media (min-width: 940px) {
  /* One shared height for both panes, sized to the viewport so the whole
     explorer stays on screen instead of running off the bottom. */
  .pricing-grid { block-size: min(72vh, 720px); }
  .pricing-pane { block-size: 100%; }
}

/* The selected service was marked with `currentColor`, which is the text
   colour — a black bar in light mode. It should be the brand accent. */
.svc.is-on {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  box-shadow: inset 3px 0 0 var(--accent);
}

.svc.is-on .svc-name { color: var(--accent); }

.sortb.is-on {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
}

/* Same reasoning for the stock bars, which were also drawn in text colour. */
.stockbar i { background: var(--accent); opacity: 0.75; }

/* The pricing panes sit outside .masthead/.foot, so the scoped accent token is
   not in scope there. Re-declare it on the section in both themes. */
.pricing { --accent: #2fe3a0; }

@media (prefers-color-scheme: light) {
  .pricing { --accent: #0b7f56; }
}

:root[data-theme="light"] .pricing { --accent: #0b7f56; }
:root[data-theme="dark"]  .pricing { --accent: #2fe3a0; }

/* ── countdown ring: continuous, not stuttering ──────────────────
   The clock ticks once a second but the arc transitioned over 0.4s, so
   it animated, sat frozen for 0.6s, then jumped — read as "stuck".
   Matching the transition to the tick makes the sweep continuous.
   The colour keeps its own slower fade so urgent/critical still ease. */
.ring-fill {
  transition: stroke 0.6s linear, stroke-dashoffset 1s linear;
}
/* The final tick and any reset must not animate backwards for a second. */
.ring-wrap.is-settled .ring-fill { transition: stroke 0.3s var(--ease); }

@media (prefers-reduced-motion: reduce) {
  .ring-fill { transition: stroke 0.3s linear; }
}

/* ── service logo: legible at a glance ───────────────────────────
   20px inside a 36px tile left the mark too small to identify, which
   is the one thing the tile exists to do. */
.ic-tile { inline-size: 42px; block-size: 42px; }
.ic-tile .bmark { inline-size: 26px; block-size: 26px; }

/* The chosen-service chip carries the same mark; keep it in step. */
.chosen .ic-tile { inline-size: 44px; block-size: 44px; }
.chosen .ic-tile .bmark { inline-size: 28px; block-size: 28px; }

/* Tags inline with text stay small — enlarging those would break the
   line box they sit in. */
.tag .bmark { inline-size: 16px; block-size: 16px; }

/* ── buy modal: the ring froze while we waited ───────────────────
   .buyfx-ring .f ran `fx-draw 1.5s forwards` — a FINITE animation that
   holds its last frame. The ring drew itself once, then stood perfectly
   still for however long the upstream call took, and only moved again
   when `is-done` swapped in the closing animation. Since a purchase
   always takes longer than 1.5s, the spinner was frozen for essentially
   the entire wait — exactly when it is the only thing telling the
   customer we are still working.

   Replaced with a genuine indeterminate spinner for the waiting state:
   the arc sweeps and the ring rotates continuously, so it can run for
   one second or twenty and always look alive.

   The rotation deliberately stays on `.is-on` rather than
   `:not(.is-done)` — keeping it turning through the 0.5s close avoids
   snapping back to the -90deg origin from whatever angle it stopped at. */
.buyfx.is-on .buyfx-ring svg {
  animation: fx-spin 1.4s linear infinite;
}

.buyfx.is-on:not(.is-done) .buyfx-ring .f {
  animation: fx-sweep 1.6s ease-in-out infinite;
}

@keyframes fx-spin {
  from { transform: rotate(-90deg); }
  to   { transform: rotate(270deg); }
}

@keyframes fx-sweep {
  0%   { stroke-dasharray: 24 290;  stroke-dashoffset: 0; }
  50%  { stroke-dasharray: 190 124; stroke-dashoffset: -48; }
  100% { stroke-dasharray: 24 290;  stroke-dashoffset: -304; }
}

/* A spinner that cannot spin must still show progress, so fall back to a
   steady pulse rather than the frozen ring we just removed. */
/* Declared at top level, NOT inside the media query below.
   Nesting @keyframes inside @media made the parser swallow the media
   block's closing brace, so every rule AFTER it in this file was silently
   dropped — which was the whole star block. Anything appended below would
   have vanished the same way. Balanced braces do not mean valid CSS; the
   only reliable check is counting what the browser actually parses. */
@keyframes fx-fade { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  .buyfx.is-on .buyfx-ring svg { animation: none; transform: rotate(-90deg); }
  .buyfx.is-on:not(.is-done) .buyfx-ring .f {
    animation: fx-fade 1.4s ease-in-out infinite;
    stroke-dasharray: 314; stroke-dashoffset: 90;
  }
}

/* ── cheapest tier: a small star, not a shouted word ─────────────
   "CHEAPEST" in an uppercase pill was louder than the price it was
   pointing at, and with three or four tier rows the eye landed on the
   badge instead of the numbers being compared. A star reads instantly
   without competing — small marks get noticed precisely because they
   do not demand it. */
/* Off-screen but readable — a gold frame is invisible to a screen reader,
   so the cue it carries has to exist as text somewhere. */
.sr-only {
  position: absolute;
  inline-size: 1px; block-size: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The cheapest tier, framed rather than labelled.
   .opcard.is-best already draws its ring with an INSET box-shadow rather
   than a border, so the gold goes the same way — that keeps it inside the
   radius and unaffected by the card's overflow:hidden, which a real border
   plus the gradient-border trick would fight with. */
.opcard.is-best {
  box-shadow:
    inset 0 0 0 1.5px #C9922A,                 /* the lining itself */
    0 0 0 1px rgba(201, 146, 42, .16),         /* faint halo, separates from neighbours */
    0 6px 18px -8px rgba(201, 146, 42, .5),    /* warm lift */
    var(--e2);
  background:
    linear-gradient(180deg, rgba(247, 217, 118, .10), transparent 46%),
    var(--surface-1);
}

/* A brighter filament along the top edge — this is what catches the eye in
   peripheral vision. Fading to transparent at both ends stops it reading as
   a hard rule across the card. */
.opcard.is-best::before {
  content: '';
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: 2px;
  background: linear-gradient(90deg,
    transparent, rgba(247, 217, 118, .35) 18%, #F2CF74 50%, rgba(247, 217, 118, .35) 82%, transparent);
  pointer-events: none;
}

/* Hover must not wipe the gold: the base .opcard:hover rule replaces
   box-shadow wholesale, which would drop the lining on the one row we most
   want to keep marked. */
.opcard.is-best:hover {
  box-shadow:
    inset 0 0 0 1.5px #D9A63A,
    0 0 0 1px rgba(201, 146, 42, .22),
    0 10px 24px -10px rgba(201, 146, 42, .55),
    var(--e3);
}

@media (prefers-color-scheme: dark) {
  .opcard.is-best {
    box-shadow:
      inset 0 0 0 1.5px #D9A63A,
      0 0 0 1px rgba(217, 166, 58, .20),
      0 6px 20px -8px rgba(217, 166, 58, .38),
      var(--e2);
    background:
      linear-gradient(180deg, rgba(247, 217, 118, .07), transparent 46%),
      var(--surface-1);
  }
}

.opcard-star svg {
  inline-size: 100%;
  block-size: 100%;
  display: block;
  fill: currentColor;
  /* A faint outline keeps the gold legible on the selected row's tinted
     background, where a flat fill would otherwise wash out. */
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .18));
}

/* Gold is dark against the dark theme's card; lift it rather than
   letting the one deliberate accent on the row disappear. */
@media (prefers-color-scheme: dark) {
  .opcard-star { color: #F5C518; }
}

/* Below the two-pane breakpoint neither list had a height cap, so each grew
   to its full content. That was survivable when the list stopped at 200
   rows; with lazy paging it is not — the sentinel would sit permanently in
   view, page after page would fire with nothing to scroll, and the phone
   would end up building all 2,200 rows in one go.
   A capped box gives the observer something to work against at every width. */
@media (max-width: 939px) {
  .pricing-list      { max-block-size: 58vh; }
  .pricing-table-wrap { max-block-size: 62vh; }
}

/* ══ developer console (/developers) ══════════════════════════════
   The page was still on the pre-redesign classes (.card/.tbl/.stack) and
   rendered outside the dashboard shell, which is why its header, rail and
   footer looked nothing like the rest of the console. It now renders through
   layouts/dash; these styles cover only what that shell does not. */

.dev-card { margin-block-start: 16px; }

.dev-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}

.dev-h3 {
  margin-block: 22px 8px;
  font-size: var(--t-sm);
  font-weight: 750;
  letter-spacing: -0.01em;
}

/* ── tokens ─────────────────────────────────────────────────── */
.dev-newform {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-block-start: 14px;
}

.dev-newform .input { flex: 1 1 260px; min-inline-size: 0; }

.dev-tokens { margin-block-start: 14px; display: grid; gap: 10px; }

.dev-token {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 14px;
  padding: 12px 14px;
  border-radius: var(--r-lg);
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--line-soft);
}

.dev-token-main { min-inline-size: 0; display: grid; gap: 3px; }
.dev-token-n { font-weight: 700; }

.dev-token-k {
  font-size: 0.82rem;
  color: var(--text-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dev-token-meta {
  display: grid;
  gap: 2px;
  font-size: var(--t-xs);
  color: var(--text-3);
  text-align: end;
  white-space: nowrap;
}

.dev-token-acts { display: flex; gap: 6px; }

.dev-empty { padding: 14px 2px; }

/* The one-time secret. Loud on purpose: it cannot be recovered, so a
   customer who scrolls past it has to rotate to get another. */
.dev-reveal {
  margin-block-start: 14px;
  padding: 14px;
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--accent) 45%, transparent);
}

.dev-reveal-t {
  margin: 0 0 8px;
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--accent);
}

.dev-reveal-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

/* A secret must be selectable in full — truncating it with an ellipsis
   would hand the customer a token they cannot actually copy by hand. */
.dev-reveal-row code {
  flex: 1 1 320px;
  min-inline-size: 0;
  padding: 9px 11px;
  border-radius: var(--r-md);
  background: var(--surface-1);
  font-size: 0.86rem;
  overflow-wrap: anywhere;
  user-select: all;
}

/* ── facts list ─────────────────────────────────────────────── */
.dev-facts { margin: 14px 0 0; display: grid; gap: 1px; }

.dev-facts > div {
  display: grid;
  grid-template-columns: 150px minmax(0, 1fr);
  gap: 14px;
  padding: 9px 0;
  border-block-end: 1px solid var(--line-soft);
}

.dev-facts dt { color: var(--text-3); font-size: var(--t-xs); letter-spacing: 0.04em; }
.dev-facts dd { margin: 0; min-inline-size: 0; overflow-wrap: anywhere; }

/* ── code blocks ────────────────────────────────────────────── */
.dev-pre {
  margin: 12px 0 0;
  padding: 14px 16px;
  border-radius: var(--r-lg);
  background: var(--surface-3);
  box-shadow: inset 0 0 0 1px var(--line-soft);
  overflow-x: auto;
  font-family: var(--mono);
  font-size: 0.83rem;
  line-height: 1.62;
  /* Code must scroll inside its own box; wrapping a curl command or a Go
     struct across lines makes it unreadable and uncopyable. */
  white-space: pre;
  tab-size: 2;
}

.dev-pre code { font: inherit; color: inherit; background: none; padding: 0; }
.dev-pre-tall { max-block-size: 460px; overflow-y: auto; }

.dev-note {
  margin-block-start: 14px;
  padding: 12px 14px;
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--warn) 12%, transparent);
  box-shadow: inset 3px 0 0 var(--warn);
  font-size: var(--t-sm);
}

/* ── tables ─────────────────────────────────────────────────── */
.dev-table-wrap { margin-block-start: 12px; overflow-x: auto; }

.dev-table { inline-size: 100%; border-collapse: collapse; min-inline-size: 520px; }

.dev-table th {
  padding: 9px 12px;
  text-align: start;
  font-size: var(--t-xs);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-3);
  border-block-end: 1px solid var(--line);
}

.dev-table td {
  padding: 10px 12px;
  font-size: var(--t-sm);
  border-block-end: 1px solid var(--line-soft);
  vertical-align: top;
}

.dev-table tr:last-child td { border-block-end: 0; }

.dev-col-m { inline-size: 92px; }
.dev-col-p { inline-size: 38%; }

.dev-verb {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.05em;
}

.dev-verb-get  { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); }
.dev-verb-post { background: color-mix(in srgb, var(--warn) 20%, transparent);   color: var(--warn); }

/* ── language tabs ──────────────────────────────────────────── */
.dev-tabs {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-block-start: 14px;
  padding-block-end: 10px;
  border-block-end: 1px solid var(--line-soft);
}

.dev-tab {
  padding: 7px 13px;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-3);
  font-size: var(--t-xs);
  font-weight: 700;
  transition: background 0.18s var(--ease), color 0.18s var(--ease);
}

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

.dev-tab.is-on {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent);
}

/* ── small screens ──────────────────────────────────────────── */
@media (max-width: 720px) {
  /* Three columns cannot hold their meaning at phone width; stack them so
     the name and the actions each get the full row. */
  .dev-token { grid-template-columns: minmax(0, 1fr); gap: 9px; }
  .dev-token-meta { text-align: start; }
  .dev-token-acts { justify-content: flex-start; }

  .dev-facts > div { grid-template-columns: minmax(0, 1fr); gap: 2px; }

  .dev-pre { font-size: 0.78rem; padding: 12px; }

  .dev-head { flex-direction: column; align-items: stretch; }
  .dev-head .btn { inline-size: 100%; }
}


/* ══ overview: usage chart, favourites panel, sortable table ══════ */

.ovc { display: flex; flex-direction: column; gap: 14px; block-size: 100%; }

.ovc-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 14px; flex-wrap: wrap;
}

.ovc-tabs { display: flex; gap: 5px; }

.ovc-tab {
  padding: 5px 11px; border-radius: 999px;
  background: var(--surface-2); color: var(--text-3);
  font-size: var(--t-xs); font-weight: 700;
  transition: background 0.18s var(--ease), color 0.18s var(--ease);
}

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

.ovc-tab.is-on {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 38%, transparent);
}

/* ── the plot ─────────────────────────────────────────────────
   The SVG carries a viewBox of 0..100 in both axes and is stretched to the
   box with preserveAspectRatio="none". Bars are rectangles, so non-uniform
   scaling is harmless — and unlike percentage heights on flex children, an
   SVG coordinate always resolves. That was the bug: bars collapsed to a
   row of dashes because their parent had no definite height. */
.ovc-chart {
  flex: 1;
  min-block-size: 170px;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 10px;
}

/* Y axis labels, positioned against the same 0..100 space as the SVG. */
.ovc-ys { position: relative; inline-size: 2ch; }

.ovc-y {
  position: absolute;
  inset-inline-end: 0;
  inset-block-start: var(--y);
  translate: 0 -50%;
  font-size: 0.68rem;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}

.ovc-plotwrap { position: relative; display: flex; flex-direction: column; min-inline-size: 0; }

/* An SVG with a viewBox carries an intrinsic 1:1 aspect ratio, so `width:100%`
   alone made it as TALL as it was wide — an 820px-wide panel produced an
   820px-tall chart. An explicit block-size overrides that and lets the
   wrapper decide the height. */
.ovc-svg {
  inline-size: 100%;
  block-size: 100%;
  flex: 1 1 0;
  min-block-size: 0;
  display: block;
  overflow: visible;
}

.ovc-grid {
  stroke: var(--line-soft);
  stroke-width: 0.4;
  vector-effect: non-scaling-stroke;
}

.ovc-b { fill: color-mix(in srgb, var(--accent) 24%, transparent); }
.ovc-d { fill: var(--accent); }

/* Grow from the baseline. transform-box:fill-box makes the origin relative
   to the rect itself, so every bar rises from where it sits rather than
   from the SVG's origin. One composited transform per bar — no reflow. */
.ovc-b, .ovc-d {
  transform-box: fill-box;
  transform-origin: bottom;
  animation: ovc-rise 0.46s var(--spring) backwards;
  animation-delay: calc(var(--i) * 26ms);
}

.ovc-hit { fill: transparent; cursor: default; }
.ovc-hit:hover { fill: color-mix(in srgb, currentColor 5%, transparent); }

/* X labels sit under the plot in the same proportional space. */
.ovc-xs { position: relative; block-size: 15px; margin-block-start: 6px; }

.ovc-x {
  position: absolute;
  inset-inline-start: var(--x);
  translate: -50% 0;
  font-size: 0.68rem;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.ovc-x.is-hidden { display: none; }

/* ── sortable headers ───────────────────────────────────────── */

.th-sort { cursor: pointer; user-select: none; }
.th-sort-l { display: inline-flex; align-items: center; gap: 5px; }
.th-sort:hover { color: var(--text); }

.th-arrow {
  inline-size: 0; block-size: 0;
  border-inline: 4px solid transparent;
  border-block-end: 5px solid currentColor;
  opacity: 0.25;
  transition: opacity 0.16s var(--ease), rotate 0.16s var(--ease);
}

.th-sort.is-asc  .th-arrow { opacity: 0.9; }
.th-sort.is-desc .th-arrow { opacity: 0.9; rotate: 180deg; }

.table thead th.t-end .th-sort-l { justify-content: flex-end; }
.th-sort:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 4px; }

/* ── phones ─────────────────────────────────────────────────── */
@media (max-width: 720px) {
  .ovc-chart { min-block-size: 150px; }
  .ovc-head { flex-direction: column; }
  .ovc-tabs { inline-size: 100%; }
  .ovc-tab { flex: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .ovc-b, .ovc-d { animation: none; }
}

@keyframes ovc-rise {
  from { transform: scaleY(0); opacity: 0; }
  to   { transform: scaleY(1); opacity: 1; }
}


/* ══ overview: routes panel ═══════════════════════════════════════
   Three sections behind one header. The tabs sit on the header line beside
   the title — stacked under it they read as a second heading and pushed the
   list out of the card. Rows are a TABLE, not a stack of cards: aligned
   columns scan far faster than repeated boxes, which is what made the
   previous version look cluttered. */

.picker-col .card-hd,
body[data-page="overview"] .picker-col .card > .card-t {
  margin-block-end: 0;
}

/* Title and tabs share one line, wrapping only when there is no room. */
body[data-page="overview"] .picker-col .card {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

body[data-page="overview"] .picker-col .card > .card-t {
  display: inline-block;
}

.rt-tabs {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  border-radius: 10px;
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--line-soft);
}

.rt-tab {
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 6px 10px;
  /* A modest radius. 999px on a short, wide box draws the ellipse the last
     version showed instead of a tab. */
  border-radius: 7px;
  color: var(--text-3);
  font-size: var(--t-xs);
  font-weight: 700;
  white-space: nowrap;
  text-decoration: none;
  transition: background 0.16s var(--ease), color 0.16s var(--ease);
}

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

.rt-tab.is-on {
  background: var(--surface-1);
  color: var(--text);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .08);
}

.rt-n {
  min-inline-size: 17px;
  padding: 0 5px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent);
  font-size: 0.66rem;
  font-variant-numeric: tabular-nums;
}

.rt-tab-link { color: var(--accent); flex: 0 0 auto; }

/* ── the table ──────────────────────────────────────────────── */

.rt-body { flex: 1; min-block-size: 0; }

.rt-table { inline-size: 100%; border-collapse: collapse; table-layout: fixed; }

.rt-w-svc { inline-size: 46%; }
.rt-w-cty { inline-size: 30%; }
.rt-w-end { inline-size: 24%; }

.rt-table td {
  padding: 11px 4px;
  border-block-end: 1px solid var(--line-soft);
  vertical-align: middle;
  font-size: var(--t-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rt-table tr:last-child td { border-block-end: 0; }
.rt-table tr:hover td { background: color-mix(in srgb, currentColor 3%, transparent); }

.rt-cell {
  display: flex;
  align-items: center;
  gap: 9px;
  min-inline-size: 0;
  color: inherit;
  text-decoration: none;
}

.rt-cell .bmark { inline-size: 22px; block-size: 22px; flex: none; border-radius: 5px; }

.rt-t { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rt-c-cty { color: var(--text-3); }

.rt-c-end { text-align: end; position: relative; padding-inline-end: 22px; }

.rt-price { font-family: var(--mono); font-weight: 700; }
.rt-out { color: var(--text-3); font-size: var(--t-xs); }

.rt-timer {
  font-family: var(--mono);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

.rt-timer.is-urgent { color: var(--warn); }

.rt-code {
  padding: 3px 9px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 15%, transparent);
  color: var(--accent);
  font-family: var(--mono);
  font-weight: 700;
}

/* Remove sits in the row's own padding, so it never displaces the price. */
.rt-del {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-end: 0;
  translate: 0 -50%;
  inline-size: 20px;
  block-size: 20px;
  border-radius: 50%;
  color: var(--text-3);
  font-size: 0.95rem;
  line-height: 1;
  opacity: 0;
  transition: opacity 0.16s var(--ease), background 0.16s var(--ease);
}

.rt-table tr:hover .rt-del, .rt-del:focus-visible { opacity: 1; }
.rt-del:hover { background: color-mix(in srgb, var(--danger) 16%, transparent); color: var(--danger); }

@media (hover: none) { .rt-del { opacity: 1; } }

.rt-empty { display: grid; gap: 6px; place-content: center; place-items: start; padding: 22px 4px; block-size: 100%; }
.rt-empty-t { margin: 0; font-weight: 700; }

/* ── footer ─────────────────────────────────────────────────── */

.rt-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-block-start: auto;
  padding-block-start: 10px;
  border-block-start: 1px solid var(--line-soft);
}

.rt-count { font-size: var(--t-xs); color: var(--text-3); }
.rt-pages { display: flex; align-items: center; gap: 8px; }
.rt-page-n { font-size: var(--t-xs); color: var(--text-3); font-variant-numeric: tabular-nums; }

.rt-pg {
  inline-size: 26px; block-size: 26px;
  border-radius: 7px;
  background: var(--surface-2);
  font-size: 0.95rem; line-height: 1;
  transition: background 0.16s var(--ease);
}

.rt-pg:hover:not(:disabled) { background: var(--surface-3); }
.rt-pg:disabled { opacity: 0.3; cursor: default; }

body[data-page="overview"] #quick {
  display: flex; flex-direction: column; flex: 1; min-block-size: 0;
}

/* ── chart: range switch on the chrome bar, styled tooltip ──── */

.ovc-tabs { display: flex; gap: 3px; }

.ovc-tab {
  padding: 4px 10px;
  border-radius: 7px;
  background: transparent;
  color: var(--text-3);
  font-size: var(--t-xs);
  font-weight: 700;
  transition: background 0.16s var(--ease), color 0.16s var(--ease);
}

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

.ovc-tab.is-on {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--accent);
}

/* The plot takes the whole panel and follows its height, so the card can be
   any size and the chart still fills it. */
.ovc { display: flex; flex-direction: column; block-size: 100%; min-block-size: 0; }
.ovc-chart { flex: 1; min-block-size: 120px; }

/* Our own tooltip. The SVG <title> fallback gave the browser's delayed grey
   box — unstyleable, and about a second too late to feel connected to the
   bar it describes. */
.ovc-tip {
  position: fixed;
  z-index: 60;
  display: grid;
  gap: 3px;
  padding: 9px 11px;
  border-radius: var(--r-md);
  background: var(--surface-1);
  box-shadow: var(--e3), inset 0 0 0 1px var(--line-soft);
  font-size: var(--t-xs);
  line-height: 1.35;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  translate: 0 4px;
  transition: opacity 0.14s var(--ease), translate 0.14s var(--ease);
}

.ovc-tip.is-on { opacity: 1; translate: 0 0; }
.ovc-tip-d { font-weight: 750; color: var(--text); }
.ovc-tip-r { display: flex; align-items: center; gap: 7px; color: var(--text-3); }

.ovc-dot { inline-size: 8px; block-size: 8px; border-radius: 2px; flex: none; }
.ovc-dot-a { background: color-mix(in srgb, var(--accent) 24%, transparent); }
.ovc-dot-b { background: var(--accent); }

.ovc-hit { fill: transparent; }
.ovc-hit:hover { fill: color-mix(in srgb, currentColor 4%, transparent); }

@media (max-width: 720px) {
  .rt-w-cty { inline-size: 26%; }
  .rt-table td { padding: 10px 3px; font-size: var(--t-xs); }
  .ovc-chart { min-block-size: 140px; }
}

/* ══ overview: chart fills its panel, tables read as tables ═══════ */

/* The plot was sitting in the top third with dead space beneath it. The
   panel is a flex column and the chart is its flexible child; without
   min-block-size:0 the SVG's own intrinsic size wins and the chart stops
   growing, which is what left the gap. */
body[data-page="overview"] .flight {
  display: flex;
  flex-direction: column;
  min-block-size: 0;
}

body[data-page="overview"] .flight-body {
  flex: 1;
  min-block-size: 0;
  display: flex;
  flex-direction: column;
}

body[data-page="overview"] .flight-body > .ovc { flex: 1; min-block-size: 0; }
body[data-page="overview"] .ovc-chart { flex: 1; min-block-size: 0; }

/* ── real tables ────────────────────────────────────────────── */

.rt-table thead th {
  padding: 0 4px 8px;
  text-align: start;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-3);
  border-block-end: 1px solid var(--line);
  white-space: nowrap;
}

.rt-table thead th.rt-c-end { text-align: end; padding-inline-end: 22px; }

/* Zebra striping does the work that borders alone could not: it makes the
   rows read as one table rather than four separate strips. */
.rt-table tbody tr:nth-child(even) td {
  background: color-mix(in srgb, currentColor 2.5%, transparent);
}

.rt-table tbody tr:hover td {
  background: color-mix(in srgb, var(--accent) 7%, transparent);
}

.rt-table tbody td { border-block-end: 1px solid var(--line-soft); }
.rt-table tbody tr:last-child td { border-block-end: 0; }

/* First and last cells get the rounding, so a hovered row reads as one
   continuous band instead of three abutting rectangles. */
.rt-table tbody td:first-child {
  border-start-start-radius: 7px; border-end-start-radius: 7px;
}
.rt-table tbody td:last-child {
  border-start-end-radius: 7px; border-end-end-radius: 7px;
}

/* ── count badge ────────────────────────────────────────────── */

/* Sized in em so two and three digits both fit without the tab growing
   enough to wrap the row. */
.rt-n {
  min-inline-size: 1.45em;
  padding: 1px 0.42em;
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent);
  font-size: 0.66rem;
  font-variant-numeric: tabular-nums;
  text-align: center;
  line-height: 1.45;
}

/* Running is always clickable — an empty tab shows an empty state rather
   than silently refusing, which reads as a broken control. */
.rt-tab { cursor: pointer; }

.rt-empty {
  display: grid;
  gap: 7px;
  place-content: center;
  justify-items: center;
  text-align: center;
  padding: 24px 12px;
  block-size: 100%;
  color: var(--text-3);
}

.rt-empty svg { inline-size: 26px; block-size: 26px; opacity: 0.45; }
.rt-empty-t { margin: 0; font-weight: 700; color: var(--text); }
.rt-empty .cell-sub { max-inline-size: 34ch; }

/* The footer keeps its shape on a single page, carrying just the count, so
   the card does not change height when paging appears or disappears. */
.rt-foot { min-block-size: 38px; }

@media (max-width: 720px) {
  .rt-table thead th { font-size: 0.64rem; }
}

/* Three tabs plus two count badges must stay on one line — wrapped, they
   read as two rows of controls and push the table down. Nowrap plus a
   shrinkable link tab keeps them together at any card width. */
.rt-tabs { flex-wrap: nowrap; min-inline-size: 0; }

.rt-tab {
  min-inline-size: 0;
  padding-inline: 9px;
  gap: 5px;
}

.rt-tab-link {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Below the two-column breakpoint the card is full width, so the tabs have
   room to breathe again. */
@media (max-width: 999px) {
  .rt-tab { padding-inline: 12px; }
}

/* The link tab is an <a> and the others are <button>s, which resolve their
   line box slightly differently — a 1px vertical drift between them. One
   line-height for all three removes it. */
.rt-tab { line-height: 1.45; }

/* The chart needs a floor, not just a flex share.
   Above 940px the .buy grid gives both panels a shared min(72vh,720px), so
   `flex: 1` had something to divide. Below that the grid stacks and has NO
   height, so a flex child with min-block-size:0 collapsed to nothing — the
   plot vanished on mobile and the y-axis labels piled on top of each other
   at the same coordinate.

   A real minimum makes the chart independent of whether a parent height
   exists; flex:1 still lets it grow into the panel on desktop. */
body[data-page="overview"] .ovc-chart {
  flex: 1;
  min-block-size: 190px;
}

/* The .ovc / .flight-body chain keeps min-block-size:0 on purpose — those
   must be allowed to shrink so the chart, not the wrapper, owns the floor. */

@media (max-width: 939px) {
  /* Stacked layout: give the panel a sensible height of its own rather than
     letting it hug the content it no longer constrains. */
  body[data-page="overview"] .flight-body { min-block-size: 240px; }
  body[data-page="overview"] .ovc-chart { min-block-size: 200px; }
}

@media (max-width: 480px) {
  body[data-page="overview"] .flight-body { min-block-size: 210px; }
  body[data-page="overview"] .ovc-chart { min-block-size: 175px; }

  /* Tabs sit beside the panel title on a narrow chrome bar; let them keep
     their own line if the title needs the room. */
  .flight-chrome { flex-wrap: wrap; row-gap: 8px; }
  .ovc-tabs { margin-inline-start: auto; }
}

/* ══ history & prices tables ══════════════════════════════════════
   Fixed columns so the headings stay over their own values. Both tables sit
   in .table-scroll, so a min-inline-size lets them scroll horizontally on a
   phone rather than crushing seven columns into 375px. */

.tbl-history, .tbl-prices { table-layout: fixed; min-inline-size: 860px; }

.c-id     { inline-size: 9%; }
.c-date   { inline-size: 13%; }
.c-svc    { inline-size: 16%; }
.c-cty    { inline-size: 15%; }
.c-price  { inline-size: 10%; }
.c-num    { inline-size: 22%; }
.c-status { inline-size: 15%; }

.p-svc   { inline-size: 20%; }
.p-cty   { inline-size: 18%; }
.p-rate  { inline-size: 22%; }
.p-stock { inline-size: 13%; }
.p-price { inline-size: 13%; }
.p-buy   { inline-size: 14%; }

/* Truncate rather than wrap: one height per row keeps the columns reading as
   columns down a long page. */
.tbl-history td, .tbl-prices td {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: middle;
}

/* The number/code cell is the one that legitimately carries two lines. */
.tbl-history .c-num, .tbl-history td:nth-child(6) { white-space: normal; }

.tbl-history .cell-flex, .tbl-prices .cell-flex { min-inline-size: 0; }
.tbl-history .cell-flex > span, .tbl-prices .cell-flex > span {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Figures compared down a column need tabular digits, or the decimal points
   wander as the values change. */
.tbl-history .num-cell, .tbl-prices .num-cell {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

/* The sort caret was a bare ▲ glyph sitting after the label at full weight. */
.sortable { cursor: pointer; user-select: none; white-space: nowrap; }
.sortable .caret { margin-inline-start: 5px; font-size: 0.6rem; opacity: 0.25; transition: opacity 0.16s var(--ease), rotate 0.16s var(--ease); }
.sortable:hover .caret { opacity: 0.55; }
.sortable.is-sorted .caret { opacity: 0.9; color: var(--accent); }
.sortable.is-sorted.desc .caret { rotate: 180deg; display: inline-block; }
.sortable.t-end { text-align: end; }

/* Scrollable tables should look scrollable before you touch them. */
.table-scroll { scrollbar-gutter: stable; }

@media (max-width: 720px) {
  /* The filter row and the toolbar stack rather than squeezing four controls
     onto one line. */
  .card-hd { flex-wrap: wrap; }
  .card-hd .search { flex: 1 1 100%; }
  .card-hd .select { flex: 1 1 auto; }

  .tbl-history, .tbl-prices { min-inline-size: 720px; }
  .tbl-history td, .tbl-prices td,
  .tbl-history th, .tbl-prices th { padding-inline: 10px; }
}

/* ══ balance & account ════════════════════════════════════════════ */

.bal-grid, .prof-grid {
  display: grid;
  gap: 16px;
  grid-template-columns: 1fr;
  align-items: start;
}

@media (min-width: 900px) {
  .bal-grid  { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .prof-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  /* The ledger is the long one; give it the full width beneath the pair. */
  .bal-ledger { grid-column: 1 / -1; }
}

.bal-hero { display: grid; gap: 6px; align-content: start; }

.bal-n {
  margin: 4px 0 2px;
  font-family: var(--mono);
  font-size: clamp(2.1rem, 4vw, 2.9rem);
  font-weight: 780;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}

/* ── top-up ─────────────────────────────────────────────────── */

.bal-off {
  display: grid; gap: 8px; justify-items: start;
  padding: 16px;
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--warn) 10%, transparent);
  box-shadow: inset 3px 0 0 var(--warn);
}

.bal-off-t { margin: 0; font-weight: 750; }

.bal-form { display: grid; gap: 12px; margin-block-start: 14px; }
.bal-chips { display: flex; gap: 8px; flex-wrap: wrap; }

.bal-chip { position: relative; }
.bal-chip input { position: absolute; opacity: 0; inline-size: 100%; block-size: 100%; margin: 0; cursor: pointer; }

.bal-chip span {
  display: inline-flex; align-items: center; justify-content: center;
  min-inline-size: 68px;
  padding: 9px 14px;
  border-radius: var(--r-md);
  background: var(--surface-2);
  font-family: var(--mono);
  font-weight: 700;
  font-size: var(--t-sm);
  transition: background 0.16s var(--ease), box-shadow 0.16s var(--ease), color 0.16s var(--ease);
}

.bal-chip input:checked + span {
  background: color-mix(in srgb, var(--accent) 15%, transparent);
  color: var(--accent);
  box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--accent) 45%, transparent);
}

.bal-chip input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }

.bal-custom { display: grid; gap: 5px; }
.bal-min { margin: 0; text-align: center; }

/* ── ledger ─────────────────────────────────────────────────── */

.tbl-ledger { table-layout: fixed; min-inline-size: 680px; }
.l-date { inline-size: 15%; }
.l-type { inline-size: 15%; }
.l-note { inline-size: 34%; }
.l-amt  { inline-size: 18%; }
.l-bal  { inline-size: 18%; }

.tbl-ledger td { vertical-align: middle; }
.tbl-ledger .l-note, .tbl-ledger td:nth-child(3) {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Money in and money out must be distinguishable at a glance, and the sign
   is what carries it — not the label, which can be an unfamiliar type. */
.bal-amt { font-family: var(--mono); font-weight: 700; font-variant-numeric: tabular-nums; }
.bal-amt.is-in  { color: var(--accent); }
.bal-amt.is-out { color: var(--text); }

/* ── account page ───────────────────────────────────────────── */

.prof-facts { margin: 14px 0 0; display: grid; gap: 1px; }

.prof-facts > div {
  display: grid;
  grid-template-columns: 130px minmax(0, 1fr);
  gap: 14px;
  padding: 10px 0;
  border-block-end: 1px solid var(--line-soft);
}

.prof-facts > div:last-child { border-block-end: 0; }
.prof-facts dt { color: var(--text-3); font-size: var(--t-xs); letter-spacing: 0.03em; }
.prof-facts dd { margin: 0; min-inline-size: 0; overflow-wrap: anywhere; }
.prof-chip { margin-inline-start: 6px; }

.prof-form { display: grid; gap: 12px; margin-block-start: 14px; max-inline-size: 380px; }
.prof-field { display: grid; gap: 5px; }
.prof-field > span { font-size: var(--t-xs); color: var(--text-3); letter-spacing: 0.03em; }

.prof-stats { display: flex; gap: 26px; flex-wrap: wrap; margin-block-start: 14px; }
.prof-stats > div { display: grid; gap: 2px; }
.prof-n { font-size: 1.45rem; font-weight: 780; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.prof-k { font-size: var(--t-xs); color: var(--text-3); }

.prof-links { display: flex; gap: 8px; flex-wrap: wrap; margin-block-start: 16px; }

@media (max-width: 720px) {
  .prof-facts > div { grid-template-columns: minmax(0, 1fr); gap: 2px; }
  .prof-stats { gap: 18px; }
  .bal-chip span { min-inline-size: 60px; padding: 9px 11px; }
  .tbl-ledger { min-inline-size: 620px; }
}

/* ── right-aligned table headings ────────────────────────────────
   `.t-end` on a <th> was losing to `.table th`: the class alone is (0,1,0)
   while the base rule is (0,1,1), so every right-aligned heading sat over
   the LEFT edge of a column whose values were right-aligned — Status above
   its badges, "In stock" above its figures. The cells were unaffected,
   because `.table td` sets no text-align for `.t-end` to fight.

   Scoped through .table so it beats the base rule without becoming a
   site-wide !important. */
.table th.t-end,
.table thead th.t-end { text-align: end; }

/* The sort control inside a right-aligned heading has to follow it, or the
   label detaches from its caret as the column widens. */
.table th.t-end .th-sort-l { justify-content: flex-end; }

/* ══ prices: service picker, sleek scrollbars, pager ══════════════ */

.svc-wrap { position: relative; }

.svc-btn {
  display: inline-flex; align-items: center; gap: 9px;
  min-inline-size: 200px; max-inline-size: 260px;
  padding: 9px 12px;
  border-radius: var(--r-md);
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--line-soft);
  font-size: var(--t-sm); font-weight: 650;
  text-align: start;
  transition: background 0.16s var(--ease);
}

.svc-btn:hover { background: var(--surface-3); }
.svc-cur { display: inline-flex; align-items: center; gap: 9px; flex: 1; min-inline-size: 0; }
.svc-cur span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.svc-caret { inline-size: 16px; block-size: 16px; flex: none; opacity: 0.5; }

.svc-panel {
  position: absolute; z-index: 40;
  inset-block-start: calc(100% + 6px);
  inset-inline-start: 0;
  inline-size: min(340px, 86vw);
  padding: 10px;
  border-radius: var(--r-lg);
  background: var(--surface-1);
  box-shadow: var(--e4), inset 0 0 0 1px var(--line-soft);
}

.svc-search { inline-size: 100%; margin-block-end: 8px; }

/* Capped so the panel never runs off the screen; the list pages in as it
   is scrolled rather than rendering 2,204 rows up front. */
.svc-list {
  max-block-size: min(320px, 52vh);
  overflow-y: auto;
  overscroll-behavior: contain;
  display: grid;
  gap: 2px;
}

.svcopt {
  display: flex; align-items: center; gap: 10px;
  inline-size: 100%;
  padding: 8px 9px;
  border-radius: var(--r-md);
  text-align: start;
  font-size: var(--t-sm);
  transition: background 0.14s var(--ease);
}

.svcopt:hover { background: var(--surface-2); }

.svcopt.is-on {
  background: color-mix(in srgb, var(--accent) 13%, transparent);
  color: var(--accent);
  font-weight: 700;
}

.svcopt-n { flex: 1; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.svcopt-c { flex: none; font-size: var(--t-xs); color: var(--text-3); font-variant-numeric: tabular-nums; }
.svc-more { margin: 0; padding: 9px; text-align: center; font-size: var(--t-xs); color: var(--text-3); }

/* ── logo fallback ──────────────────────────────────────────────
   Most of the catalogue has no public brand mark, and a broken <img> reads
   as a fault rather than an absence. A lettered tile is the same size and
   shape as a real logo, so rows stay aligned either way. */
.ico-fallback {
  display: inline-grid; place-items: center;
  inline-size: 22px; block-size: 22px;
  flex: none;
  border-radius: 6px;
  background: color-mix(in srgb, currentColor 10%, transparent);
  color: var(--text-3);
  font-size: 0.68rem; font-weight: 800; line-height: 1;
}

.tbl-prices .bmark, .svcopt .bmark { inline-size: 22px; block-size: 22px; border-radius: 6px; flex: none; }
.tbl-prices .flag { inline-size: 20px; block-size: 15px; border-radius: 3px; flex: none; }

/* ── sleek scrollbars ───────────────────────────────────────────
   The global bars are deliberately wide and obvious on page-level scrollers.
   Inside a small picker that reads as heavy, so these get a slim, quiet
   variant that still has a visible thumb to grab. */
.scroll-sleek { scrollbar-width: thin; scrollbar-color: var(--surface-3) transparent; }
.scroll-sleek::-webkit-scrollbar { inline-size: 8px; block-size: 8px; }
.scroll-sleek::-webkit-scrollbar-track { background: transparent; }

.scroll-sleek::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, currentColor 22%, transparent);
  border: 2px solid transparent;
  background-clip: padding-box;
  border-radius: 999px;
}

.scroll-sleek::-webkit-scrollbar-thumb:hover {
  background: color-mix(in srgb, currentColor 38%, transparent);
  background-clip: padding-box;
}

/* ── footer: count beside the pager ─────────────────────────── */
.price-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
  padding: 12px 18px;
  border-block-start: 1px solid var(--line-soft);
}

.price-foot .pager { margin: 0; padding: 0; border: 0; }
.pg-gap { padding-inline: 4px; color: var(--text-3); }
.pgb:disabled { opacity: 0.3; cursor: default; }

@media (max-width: 720px) {
  .svc-btn { min-inline-size: 0; inline-size: 100%; max-inline-size: none; }
  .svc-wrap { flex: 1 1 100%; }
  .svc-panel { inline-size: 100%; }
  .price-foot { justify-content: center; }
}

/* ══ selects: match the service picker ════════════════════════════
   The kit drew its caret as two linear-gradients forming a filled triangle,
   which read as a different control sitting beside the new picker button.
   These now share the picker's surface, radius, padding and chevron, so the
   filter bar is one set of controls rather than two generations of them.

   They stay native <select> elements on purpose: 205 countries is exactly
   the case where a phone's own picker beats anything reimplemented in a
   div, and it keeps keyboard and screen-reader behaviour for free. */
:root {
  --caret: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
}

@media (prefers-color-scheme: dark) {
  :root {
    --caret: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  }
}

.select {
  padding: 9px 38px 9px 12px;
  border-radius: var(--r-md);
  background-color: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--line-soft);
  font-size: var(--t-sm);
  font-weight: 650;
  line-height: 1.45;

  /* One chevron, matching the picker button — not the two gradients that
     drew a solid triangle. */
  background-image: var(--caret);
  background-position: right 12px center;
  background-size: 16px 16px;
  background-repeat: no-repeat;

  transition: background-color 0.16s var(--ease);
}

.select:hover { background-color: var(--surface-3); }

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

/* The picker button and the selects must agree on height, or the filter bar
   looks stepped. */
.svc-btn { padding: 9px 12px; line-height: 1.45; }

/* ── picker list: keep the count clear of the scrollbar ─────────
   The route count sat hard against the scrollbar thumb, which read as one
   crowded element. A stable gutter reserves the track's width whether or
   not the list currently scrolls, so rows do not shift when it appears. */
.svc-list {
  scrollbar-gutter: stable;
  padding-inline-end: 4px;
}

.svcopt { padding-inline-end: 10px; }
.svcopt-c { margin-inline-start: 4px; }

/* A <select> and a <button> resolve their inner line box differently — 44px
   against 41px with identical padding — which left the filter bar stepped.
   Pinning one height and centring the content makes them agree regardless of
   how each control measures its own text. */
.select, .svc-btn {
  min-block-size: 42px;
  display: inline-flex;
  align-items: center;
}

/* A select cannot be a flex container in every engine, so it keeps its own
   box and just matches the height. */
.select {
  display: inline-block;
  block-size: 42px;
  padding-block: 0;
}

/* ══ statistics: chart frame, breakdown tables, phone layout ══════════

   The kit drew both charts as a 560×200 SVG stretched with
   preserveAspectRatio="none". That distorts everything inside it: on a phone
   560 viewBox units are squeezed into ~340px while the height is stretched to
   220px, so axis text was compressed horizontally and pulled vertically, and
   strokes rendered at different weights on each axis.

   So the SVG holds shapes only. Every label is HTML laid out around the plot,
   and lines carry vector-effect="non-scaling-stroke". The chart is then free
   to be any width without a single glyph deforming. */

.sc {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) auto;
  gap: 6px 10px;
  block-size: 210px;
}

.sc-y {
  grid-column: 1; grid-row: 1;
  display: flex; flex-direction: column; justify-content: space-between;
  color: var(--text-3); font-family: var(--mono); font-size: 10px;
  text-align: end; font-variant-numeric: tabular-nums;
}
/* Half a line, so each label centres on its own gridline rather than
   sitting below it. */
.sc-y span { transform: translateY(-0.5em); white-space: nowrap; }
.sc-y span:first-child { transform: none; }

.sc-plot {
  grid-column: 2; grid-row: 1;
  position: relative;
  border-block-end: 1px solid var(--line-soft);
  /* Four gridlines from a gradient — no elements, no reflow cost. */
  background-image: repeating-linear-gradient(
    to bottom, var(--line-soft) 0 1px, transparent 1px 25%);
}

.sc-x {
  grid-column: 2; grid-row: 2;
  display: flex; justify-content: space-between;
  color: var(--text-3); font-family: var(--mono); font-size: 10px;
  white-space: nowrap;
}

/* ── issued/received bars ── */

.sc-bars {
  position: absolute; inset: 0;
  display: flex; align-items: flex-end; gap: 2px;
}
.sc-b {
  flex: 1 1 0; min-inline-size: 0;
  block-size: var(--h, 0%);
  display: flex; align-items: flex-end;
  background: var(--line-soft);
  border-radius: 3px 3px 0 0;
  transition: filter 0.16s var(--ease, ease);
  /* Deliberately no fill-mode, and a transform rather than a height.
     The first version grew block-size from 0 with fill-mode:backwards, which
     pins the bar at its from-state for as long as the animation has not
     started — and it does not start while the tab is backgrounded or frames
     are otherwise not being composited. Coming back to such a tab showed an
     empty chart. With no fill-mode the resting state is the real bar, so the
     chart is correct whether or not the animation ever runs; scaleY is also
     compositor-only, which matters at ninety bars. */
  transform-origin: bottom;
  animation: sc-rise 0.5s var(--ease, ease);
}
.sc-b:hover { filter: brightness(1.45); }
.sc-b i {
  display: block; inline-size: 100%;
  block-size: var(--k, 0%);
  background: var(--accent);
  border-radius: 3px 3px 0 0;
}

@keyframes sc-rise { from { transform: scaleY(0); } }

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

/* Ninety bars in a phone's width leaves each one sub-pixel; dropping the gap
   keeps them as a readable block rather than a grey smear. */
.sc-bars:has(> :nth-child(50)) { gap: 1px; }

/* ── spend area ── */

.sc-svg { position: absolute; inset: 0; inline-size: 100%; block-size: 100%; }
/* Transparent per-day strips, purely so each point can carry a tooltip. */
.sc-hits { position: absolute; inset: 0; display: flex; }
.sc-hits > div { flex: 1 1 0; min-inline-size: 0; }

/* ── empty states ── */

.sc-empty {
  block-size: 210px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; text-align: center; padding: 16px;
}
.sc-empty-t { color: var(--text-2, var(--text-3)); font-weight: 650; }
.sc-empty-s { color: var(--text-3); font-size: var(--t-sm); }
.sc-empty-row { block-size: auto; padding-block: 30px; }

/* ── breakdown tables ── */

/* dash.css puts a 640px floor under every .table so the wide ones (history,
   prices) scroll inside .table-scroll rather than crushing their columns.
   These two have four columns and fit a phone comfortably, so the floor only
   forced a sideways scroll onto a table that never needed one. */
.tbl-stats { inline-size: 100%; min-inline-size: 0; table-layout: fixed; }

/* .table-scroll reserves a scrollbar gutter so a scrolling table does not
   shift its content when the bar appears. A table that fits needs neither,
   and the reserved 7px was itself what pushed these two past their content
   box — the table sized to 100% of the card, then had 7px less to live in.
   Opting out is a class rather than :has(), so it does not depend on
   selector support. */
.table-fit { overflow-x: visible; scrollbar-gutter: auto; }
.tbl-stats .s-name { inline-size: auto; }
.tbl-stats .s-n { inline-size: 84px; }
.tbl-stats .cell-strong {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tbl-stats .flag { inline-size: 20px; block-size: 15px; flex: none; border-radius: 3px; }
.tbl-stats .bmark { inline-size: 22px; block-size: 22px; flex: none; border-radius: 6px; }
.tbl-stats .cell-flex { display: flex; align-items: center; gap: 10px; min-inline-size: 0; }

/* Both cards sit in the same auto-fit row, so their charts must agree on
   height or the row grows to the taller one and one card floats. */
.sc-card .card-body { display: flex; flex-direction: column; }

/* Same reasoning for the breakdown pair. Both tables now show five rows, so
   the only thing that can differ between the cards is whether a pager is
   present — reserving its height when it is empty keeps them identical
   instead of leaving one with a block of dead space under its last row.
   Scoped by id: an empty pager elsewhere should still collapse. */
#cty-pager:empty, #svc-pager:empty { min-block-size: 68px; }

/* Four tiles across a mid-width viewport auto-fit to three, leaving one
   stranded on its own row. Two-up is the only balanced arrangement for four
   of anything at this width. */
@media (min-width: 561px) and (max-width: 900px) {
  body[data-page="stats"] .grid.g-4 { grid-template-columns: repeat(2, 1fr); }
}

/* The numeric columns have to clear their own headings, not just their
   values: "Issued" is 42px of text, so at the kit's 14px side padding a
   column narrower than 70px overflows its cell — which is what pushed the
   table 7px past the card and put a sideways scroll on a table that fits. */
@media (max-width: 900px) {
  .sc, .sc-empty { block-size: 190px; }
  /* Matched to `.table thead th` in dash.css — a bare `.tbl-stats th` is one
     element short of its specificity and silently loses. */
  .tbl-stats thead th, .tbl-stats tbody td { padding-inline: 8px; }
  .tbl-stats .s-n { inline-size: 66px; }
}

@media (max-width: 560px) {
  .sc, .sc-empty { block-size: 168px; }
  .sc { gap: 5px 7px; }
  .sc-y, .sc-x { font-size: 9px; }
  /* Five dates cannot fit across a phone without colliding. */
  .sc-x-mid { display: none; }

  /* Four KPIs in one column ran 526px down an 812px phone — the whole first
     screen was headline numbers and nothing else. Two up puts the charts back
     above the fold. Scoped to this screen: the same grid carries other
     pages' tiles, which are not measured here. */
  body[data-page="stats"] .grid.g-4 { grid-template-columns: 1fr 1fr; }
  body[data-page="stats"] .grid.g-4 .stat { padding: 13px 13px; }
  body[data-page="stats"] .grid.g-4 .stat-v { font-size: 1.32rem; }

  .tbl-stats thead th, .tbl-stats tbody td { padding-inline: 7px; }
  .tbl-stats .s-n { inline-size: 64px; }
  .tbl-stats .bmark { inline-size: 20px; block-size: 20px; }
  .tbl-stats .cell-flex { gap: 8px; }
}

/* ══ account: two stacked columns, 2FA, recovery codes ════════════════

   The page was four cards in a two-column grid, each sized by its own
   content. Grid rows stretch to their tallest cell, so "This account" and
   "Password" — both narrow — were pinned to the height of whatever sat
   beside them, leaving the empty right-hand side the whole card acquired.

   Two columns of stacked cards instead. Each card is now only as tall as it
   needs to be, and the columns absorb the difference between them. */

.prof-grid { align-items: start; }
.prof-col { display: grid; gap: 16px; align-content: start; min-inline-size: 0; }

/* A heading with a status chip pushed to the end of the line. */
.prof-hd {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
}

/* ── the fact lists ── */

.prof-facts-tight { margin-block-start: 12px; }
.prof-facts-tight > div { padding-block: 9px; }

/* ── password: fill the card instead of a 380px column ── */

.prof-form {
  display: grid;
  gap: 12px 14px;
  margin-block-start: 14px;
  max-inline-size: none;                      /* was 380px */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
.prof-field-wide { grid-column: 1 / -1; }

.prof-form-foot {
  grid-column: 1 / -1;
  display: flex; align-items: center; justify-content: space-between;
  gap: 14px; flex-wrap: wrap;
  margin-block-start: 2px;
}

/* ── usage: a grid, so four tiles cannot straggle across one line ── */

.prof-stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px 18px;
  margin-block-start: 14px;
}
.prof-stat {
  display: grid; gap: 2px; align-content: start;
  padding: 12px 14px;
  border-radius: var(--r-sm);
  background: var(--surface-2);
}
.prof-stats .prof-n { font-size: 1.3rem; }

/* ── shared bits ── */

.prof-bullets {
  margin: 12px 0 16px; padding-inline-start: 18px;
  display: grid; gap: 7px;
  color: var(--text-2); font-size: var(--t-sm);
}
.prof-bullets li::marker { color: var(--accent); }

.prof-2fa-forms { display: grid; gap: 10px; margin-block-start: 16px; }
.prof-inline { display: flex; gap: 8px; flex-wrap: wrap; }
.prof-inline .input { flex: 1 1 190px; min-inline-size: 0; }
.prof-inline .btn { flex: none; }

/* ── two-factor enrolment ── */

.wrap-narrow { max-inline-size: 760px; }

.tfa-steps { margin: 0; padding: 0; list-style: none; display: grid; gap: 26px; }
.tfa-step { display: flex; gap: 14px; align-items: flex-start; }
.tfa-num {
  flex: none;
  inline-size: 28px; block-size: 28px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--accent); color: var(--accent-ink);
  font-weight: 700; font-size: var(--t-sm);
}
.tfa-body { flex: 1; min-inline-size: 0; }
.tfa-h { margin: 2px 0 4px; font-size: 1.02rem; font-weight: 700; }

.tfa-scan {
  display: flex; gap: 20px; flex-wrap: wrap;
  align-items: flex-start; margin-block-start: 14px;
}
/* White plate under the code: a QR needs light quiet zones to scan, and in
   dark mode the card behind it is not light. */
.tfa-qr {
  flex: none;
  inline-size: 188px;
  padding: 10px;
  border-radius: var(--r-sm);
  background: #fff;
  box-shadow: var(--e1);
}
.tfa-qr svg { display: block; inline-size: 100%; block-size: auto; }

.tfa-manual { flex: 1 1 240px; min-inline-size: 0; }
.tfa-manual-t { font-weight: 650; margin: 0 0 2px; }

.tfa-key {
  display: block;
  margin-block: 10px 6px;
  padding: 11px 13px;
  border-radius: var(--r-sm);
  background: var(--surface-2);
  font-family: var(--mono);
  font-size: 0.95rem;
  letter-spacing: 0.06em;
  overflow-wrap: anywhere;
  user-select: all;                 /* one click selects the whole key */
}

.tfa-confirm { display: flex; gap: 10px; flex-wrap: wrap; margin-block-start: 14px; }
.tfa-note {
  margin-block-start: 24px; padding-block-start: 16px;
  border-block-start: 1px solid var(--line-soft);
  color: var(--text-3); font-size: var(--t-sm);
}

/* A code field: monospaced and spaced out, so a mistyped digit is visible. */
.input-code {
  font-family: var(--mono);
  font-size: 1.15rem;
  letter-spacing: 0.34em;
  text-align: center;
  max-inline-size: 220px;
}
.tfa-confirm .input-code { flex: none; }

/* ── recovery codes ── */

.rec-warn {
  padding: 13px 15px;
  border-radius: var(--r-sm);
  background: var(--warn-wash, color-mix(in oklab, var(--warn, #d29922) 12%, transparent));
  color: var(--text-2);
  font-size: var(--t-sm);
  line-height: 1.55;
}
.rec-warn strong { color: var(--text); }

.rec-list {
  margin: 16px 0; padding: 0; list-style: none;
  display: grid; gap: 8px;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.rec-list code {
  display: block;
  padding: 11px 12px;
  border-radius: var(--r-sm);
  background: var(--surface-2);
  font-family: var(--mono);
  font-size: 1rem;
  letter-spacing: 0.06em;
  text-align: center;
  user-select: all;
}

.rec-actions { display: flex; gap: 10px; flex-wrap: wrap; }

@media (max-width: 560px) {
  .prof-form { grid-template-columns: minmax(0, 1fr); }
  .prof-stats { gap: 10px; }
  .prof-stat { padding: 10px 11px; }
  .prof-stats .prof-n { font-size: 1.15rem; }
  .tfa-qr { inline-size: 100%; max-inline-size: 240px; margin-inline: auto; }
  .tfa-scan { gap: 16px; }
  .input-code { max-inline-size: none; inline-size: 100%; }
  .tfa-confirm .input-code { flex: 1 1 auto; }
}

/* ══ affiliate ════════════════════════════════════════════════════════ */

.aff-link-card { margin-block-end: 16px; }

.aff-link {
  display: flex; gap: 10px; flex-wrap: wrap; align-items: center;
  margin-block-start: 14px;
}
.aff-link code {
  flex: 1 1 280px; min-inline-size: 0;
  padding: 12px 14px;
  border-radius: var(--r-sm);
  background: var(--surface-2);
  font-family: var(--mono);
  font-size: 0.95rem;
  overflow-wrap: anywhere;
  user-select: all;
}
.aff-link .btn { flex: none; }

/* Progress to the payout minimum. The single most useful thing on the page:
   it turns "when can I withdraw" from a question into a picture. */
.aff-bar {
  block-size: 8px;
  border-radius: 999px;
  background: var(--surface-2);
  overflow: hidden;
  margin-block-start: 14px;
}
.aff-bar > span {
  display: block; block-size: 100%;
  border-radius: 999px;
  background: var(--accent);
  transition: inline-size 0.4s var(--ease, ease);
}
.aff-bar-k { margin-block-start: 7px; font-family: var(--mono); }

.aff-form { display: grid; gap: 12px; margin-block-start: 14px; }
.aff-form .btn { justify-self: start; }

.aff-alt {
  margin-block-start: 18px; padding-block-start: 14px;
  border-block-start: 1px solid var(--line-soft);
  display: grid; gap: 8px; justify-items: start;
}

.aff-steps {
  margin: 14px 0 16px; padding-inline-start: 20px;
  display: grid; gap: 9px;
  color: var(--text-2); font-size: var(--t-sm); line-height: 1.55;
}
.aff-steps li::marker { color: var(--accent); font-weight: 700; }

.tbl-aff { inline-size: 100%; min-inline-size: 0; }
.tbl-aff .nowrap { white-space: nowrap; }

/* Addresses and hashes are long and nobody reads them in full — they are
   there to be recognised and copied, so they truncate rather than wrap a row
   to three lines. */
.aff-addr {
  display: block;
  font-family: var(--mono);
  font-size: var(--t-xs);
  color: var(--text-3);
}
.aff-tx {
  display: inline-block;
  max-inline-size: 220px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  vertical-align: bottom;
  font-family: var(--mono);
  font-size: var(--t-xs);
  color: var(--accent);
  user-select: all;
}

/* A reversed commission is history, not income. */
.is-void { opacity: 0.55; }
.is-void .cell-strong { text-decoration: line-through; }

@media (max-width: 560px) {
  .aff-link code { flex-basis: 100%; font-size: 0.85rem; }
  .aff-link .btn { inline-size: 100%; }
  .aff-tx { max-inline-size: 120px; }
  .tbl-aff thead th, .tbl-aff tbody td { padding-inline: 8px; }
}

/* ══ support ══════════════════════════════════════════════════════════

   These screens rendered in the old public layout until now, so none of the
   dashboard's own classes reached them. They use the same card, tag and field
   vocabulary as everything else here. */

.sup-bot { margin-block-end: 14px; }

.sup-ask { display: flex; gap: 9px; flex-wrap: wrap; margin-block-start: 12px; }
.sup-ask .input { flex: 1 1 240px; min-inline-size: 0; }
.sup-ask .btn { flex: none; }

.sup-chips { display: flex; gap: 6px; flex-wrap: wrap; margin-block-start: 10px; }
.sup-chip {
  padding: 6px 11px;
  border: 0; border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-2);
  font: inherit; font-size: var(--t-xs);
  cursor: pointer;
  transition: color 0.16s var(--ease, ease), background-color 0.16s var(--ease, ease);
}
.sup-chip:hover { color: var(--text); background: var(--surface-3, var(--line-soft)); }

/* ── the avatars ──────────────────────────────────────────────────────

   One mark per speaker, replacing the words "You" and "sms.rip support".
   A name repeated down every message is noise; a mark is read at a glance
   and costs no line of text.

   Both are <button> rather than <span>: they respond to a click, so they
   have to be focusable and announce themselves as pressable. */

.ava {
  flex: none;
  inline-size: 30px; block-size: 30px;
  display: grid; place-items: center;
  padding: 0; border: 0;
  border-radius: 50%;
  font: inherit; font-size: 0.7rem; font-weight: 700; letter-spacing: 0.02em;
  cursor: pointer;
  /* The spin. shell.js counts turns and writes the transform, so every click
     is another full rotation rather than a one-shot that never replays. */
  transition: transform 0.6s cubic-bezier(0.34, 1.24, 0.5, 1),
              box-shadow 0.2s var(--ease, ease);
}
.ava:hover { box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 22%, transparent); }
.ava:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.ava-you {
  background: linear-gradient(140deg,
              color-mix(in oklab, var(--accent) 78%, #4d7cff),
              color-mix(in oklab, var(--accent) 30%, #7a4dff));
  color: #fff;
}
.ava-team { background: var(--accent); color: var(--accent-ink); }
.ava-team svg { inline-size: 17px; block-size: 17px; display: block; }

@media (prefers-reduced-motion: reduce) {
  .ava { transition: box-shadow 0.2s ease; }
}

/* ── ticket list ── */

.sup-list { margin: 0; padding: 0; list-style: none; }
.sup-list > li + li { border-block-start: 1px solid var(--line-soft); }

.sup-item {
  display: flex; align-items: center; gap: 12px;
  padding: 13px 18px;
  color: inherit; text-decoration: none;
  transition: background-color 0.16s var(--ease, ease);
}
.sup-item:hover { background: var(--surface-2); }
.sup-item-main { flex: 1; min-inline-size: 0; display: grid; gap: 2px; }
.sup-item-main .cell-strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── messages: the thread and the assistant share this ────────────────

   The card around each message is gone. A card per message stacked a border,
   a shadow and 18px of padding around a single sentence, which is what made a
   three-line conversation fill a screen. The bubble carries the surface now,
   and the avatar sits outside it. */

.sup-thread { display: grid; gap: 8px; margin-block-end: 14px; }

.chat-log {
  display: grid; gap: 8px;
  margin-block-start: 12px;
  max-block-size: 340px;
  overflow-y: auto;
  scrollbar-gutter: stable;
}

.msg { display: flex; align-items: flex-start; gap: 9px; max-inline-size: 84%; }

/* The customer reads out from their own edge, so their mark sits on the
   outside of the row rather than in the middle of the thread. */
.msg.is-you { justify-self: end; flex-direction: row-reverse; }
.msg.is-staff, .msg.is-team { justify-self: start; }
.msg.is-pending { opacity: 0.6; }

.msg-main {
  min-inline-size: 0;
  padding: 9px 12px;
  border-radius: 13px;
  background: var(--surface-2);
}
.msg.is-you .msg-main {
  background: var(--accent-dim, var(--surface-3, var(--line-soft)));
  border-start-end-radius: 4px;
}
.msg.is-staff .msg-main, .msg.is-team .msg-main {
  border-start-start-radius: 4px;
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent) 18%, transparent);
}

.msg-hd { display: flex; align-items: baseline; gap: 10px; margin-block-end: 2px; }
.msg-who { font-size: var(--t-xs); font-weight: 700; color: var(--accent); }
.msg-at { color: var(--text-3); font-size: var(--t-xs); font-family: var(--mono); }
.msg.is-you .msg-hd { justify-content: flex-end; }

/* Escaped in PHP, wrapped here — so a pasted log keeps its line breaks
   without nl2br ever touching raw input. */
.msg-body {
  font-size: var(--t-sm); line-height: 1.55;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.msg-acts { display: flex; gap: 6px; flex-wrap: wrap; margin-block-start: 8px; }

/* ── forms ── */

.sup-textarea { resize: vertical; min-block-size: 76px; line-height: 1.5; }
.sup-reply { padding: 14px; }
.sup-reply-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin-block-start: 10px;
}
.sup-closed { text-align: center; }

@media (max-width: 560px) {
  .sup-ask .input { flex-basis: 100%; }
  .sup-ask .btn { inline-size: 100%; }
  .msg { max-inline-size: 100%; }
  .sup-item { padding: 12px 14px; }
  .ava { inline-size: 27px; block-size: 27px; font-size: 0.64rem; }
  .ava-team svg { inline-size: 15px; block-size: 15px; }
}

/* ══ topbar: fit a phone ══════════════════════════════════════════════

   The header row is flex/nowrap and its parts add up to 416px: a 40px rail
   toggle, a 98px brand with the "Console" sub-label, then a 234px cluster of
   balance chip, two icon buttons and the avatar. On a 375px phone that is
   41px of horizontal overflow on EVERY dashboard screen — the whole page
   scrolls sideways, and the avatar sits off the edge.

   Nothing here is load-bearing at that width: the word "Console" is decoration
   beside a logo that already says sms.rip, and 40px controls can be 36 without
   dropping below the 24px minimum tap target. */

@media (max-width: 560px) {
  .topbar-row { gap: 6px; padding-inline: 10px; }

  /* Decoration next to a wordmark that already names the product. */
  .brand-sub { display: none; }
  .brand { gap: 7px; min-inline-size: 0; }
  .brand-word { font-size: 1.05rem; }

  .topbar-end { gap: 4px; }
  .balance { padding-inline: 8px; gap: 5px; }
  .balance svg { display: none; }          /* the figure is the information */
  .icon-btn { inline-size: 36px; block-size: 36px; }
  .avatar { inline-size: 34px; block-size: 34px; font-size: 0.72rem; }
}

/* Below ~360px even that is tight, so the balance chip goes: it is one tap
   away on the Wallet item in the rail, and an overflowing header is worse
   than a hidden shortcut. */
@media (max-width: 360px) {
  .topbar-end > .balance { display: none; }
}

/* ══ balance: hero figures, one-line stamps, a ledger that fits ═══════ */

/* Three figures under the balance. The card carried one number and a line of
   reassurance, so it ran short beside the taller card next to it and left a
   block of empty white — while the two questions a balance page exists to
   answer, what went out and what came back, went unanswered. */
.bal-facts {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
  margin: 16px 0 0;
  padding-block-start: 14px;
  border-block-start: 1px solid var(--line-soft);
}
.bal-facts > div { display: grid; gap: 1px; align-content: start; min-inline-size: 0; }
.bal-facts dt {
  color: var(--text-3); font-size: var(--t-xs);
  letter-spacing: 0.05em; text-transform: uppercase;
}
.bal-facts dd {
  margin: 0;
  font-family: var(--mono); font-weight: 700; font-size: 1.02rem;
  font-variant-numeric: tabular-nums;
}
/* The caption under each figure is prose, not a number — matched by two
   classes so it beats the rule above without reaching for !important. */
.bal-facts dd.bal-facts-k {
  font-family: inherit; font-weight: 400;
  font-size: var(--t-xs); color: var(--text-3);
}

.bal-note { margin-block-start: 2px; }

/* One line, not two. A date stacked over its time doubled the height of every
   row in the ledger for information that reads perfectly well side by side. */
.bal-when { white-space: nowrap; }
.bal-time { color: var(--text-3); margin-inline-start: 7px; }

/* Rows the pager has hidden. A bare [hidden] on a <tr> loses to the table
   display rules in some engines, so it is stated explicitly. */
.tbl-ledger tr[hidden] { display: none; }

#tx-pager:empty { display: none; }

@media (max-width: 640px) {
  /* Detail and running balance are the two columns a phone cannot afford:
     one is free text, the other is derivable from the amounts above it. The
     table then fits without a sideways scroll instead of forcing one. */
  .tbl-ledger { min-inline-size: 0; table-layout: auto; }
  .tbl-ledger th:nth-child(3), .tbl-ledger td:nth-child(3),
  .tbl-ledger th:nth-child(5), .tbl-ledger td:nth-child(5) { display: none; }

  .tbl-ledger thead th, .tbl-ledger tbody td { padding-inline: 10px; }
  /* Still one line on a phone: with Detail and the running balance gone
     there is room for it, and stacking it again would undo the fix. */
  .bal-when { font-size: var(--t-xs); }
  .bal-time { margin-inline-start: 5px; }

  .bal-facts { gap: 8px; }
  .bal-facts dd { font-size: 0.92rem; }
  .bal-facts dt { letter-spacing: 0.02em; }
}

@media (max-width: 400px) {
  /* Below this the three captions wrap to three lines each and stop helping. */
  .bal-facts dd.bal-facts-k { display: none; }
}

/* ══ buy: price tiers, not operators ═════════════════════════════════

   A route can be sold at several prices at once — the UK on Facebook has
   eight. The kit titled every row with the operator name; we have none, so it
   fell back to the country, and the customer saw "United Kingdom" eight times
   with the only thing that differed set in small type on the right.

   Rows are numbered now, and read name / stock / price / buy — the same shape
   as every other row in the console.

   Badges were tried here and removed. "Cheapest" only restated the sort order,
   and both it and "Most available" fought the price for the same strip of
   space, worst on a phone where they crowded the buy button. The gold lining
   on the first row says the same thing without adding an object to read. */

.opcard.is-tier .opcard-n { letter-spacing: -0.01em; }

/* The figure is the point of the line, so it is not greyed to the same weight
   as the label beside it. */
.opcard.is-tier .opcard-m .num { color: var(--text-2); }

@media (max-width: 560px) {
  .opcard.is-tier { gap: 10px; padding: 12px 13px; }
  /* Price stays beside the cart rather than wrapping under the name: it is
     half the decision being made on this row. */
  .opcard.is-tier .opcard-p { font-size: 1rem; }
  .opcard.is-tier .opcard-cart { inline-size: 38px; block-size: 38px; }
}

/* ══ admin: the console's design language ════════════════════════════

   The admin area used to be its own product visually — forced dark, its own
   palette variables, its own .kpi/.tbl/.pill vocabulary out of app.css. Two
   design languages in one system means every fix gets made twice, and it was
   the only screen that ignored the theme the user had picked.

   It loads the console stylesheets now. The classes below are the ones its
   markup uses that live only in app.css, restyled onto the console's surfaces
   rather than rewriting nine views that are covered by end-to-end tests.

   Everything is scoped to the admin body so none of it can reach a customer
   page, where several of these names mean something else. */

body[data-page="admin"] .stack { display: grid; gap: 14px; align-content: start; }
body[data-page="admin"] .spread { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
body[data-page="admin"] .row-tight { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }

body[data-page="admin"] .tiny { font-size: var(--t-xs); }
body[data-page="admin"] .muted { color: var(--text-3); }

body[data-page="admin"] .card-pad-sm { padding: 14px; }
body[data-page="admin"] .scroll-x { overflow-x: auto; overscroll-behavior-x: contain; scrollbar-gutter: stable; }

/* ── tables ── */

body[data-page="admin"] .tbl { inline-size: 100%; border-collapse: collapse; font-size: var(--t-sm); }
body[data-page="admin"] .tbl thead th {
  padding: 10px 12px;
  border-block-end: 1px solid var(--line-soft);
  color: var(--text-3);
  font-size: var(--t-xs); font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase;
  text-align: start; white-space: nowrap;
}
body[data-page="admin"] .tbl tbody td { padding: 11px 12px; border-block-end: 1px solid var(--line-soft); vertical-align: middle; }
body[data-page="admin"] .tbl tbody tr:last-child td { border-block-end: 0; }
body[data-page="admin"] .tbl tbody tr { transition: background-color 0.14s var(--ease, ease); }
body[data-page="admin"] .tbl tbody tr:hover { background: var(--surface-2); }

/* ── the little status chip ── */

body[data-page="admin"] .pill {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px; border-radius: 999px;
  background: var(--surface-3, var(--surface-2));
  color: var(--text-2);
  font-size: var(--t-xs); font-weight: 650;
  white-space: nowrap;
}
body[data-page="admin"] .pill-ok     { background: var(--accent-dim, var(--surface-2)); color: var(--accent); }
body[data-page="admin"] .pill-warn   { background: color-mix(in oklab, var(--warn, #d29922) 16%, transparent); color: var(--warn, #d29922); }
body[data-page="admin"] .pill-danger { background: color-mix(in oklab, var(--danger) 15%, transparent); color: var(--danger); }
body[data-page="admin"] .pill-info   { background: var(--surface-3, var(--surface-2)); color: var(--text-3); }

/* ── KPI tiles: the console's .stat, under the old name ── */

body[data-page="admin"] .kpi {
  display: grid; gap: 3px;
  padding: 15px 16px;
  border-radius: var(--r-lg);
  background: var(--surface-1);
  box-shadow: var(--e1);
  transition: transform 0.26s var(--spring, ease), box-shadow 0.26s var(--ease, ease);
}
body[data-page="admin"] .kpi:hover { transform: translateY(-2px); box-shadow: var(--e3); }
body[data-page="admin"] .kpi > span:first-child {
  color: var(--text-3); font-size: var(--t-xs); font-weight: 700;
  letter-spacing: 0.09em; text-transform: uppercase;
}
body[data-page="admin"] .kpi b {
  font-family: var(--mono);
  font-size: clamp(1.25rem, 1.1vw + 0.95rem, 1.5rem);
  font-weight: 700; letter-spacing: -0.03em; line-height: 1.1;
}

/* ── forms ── */

body[data-page="admin"] .field { display: grid; gap: 5px; }
body[data-page="admin"] .label { font-size: var(--t-xs); color: var(--text-3); letter-spacing: 0.03em; }
body[data-page="admin"] .search { display: flex; align-items: center; gap: 8px; max-inline-size: 420px; }
body[data-page="admin"] .search .input { flex: 1; min-inline-size: 0; }
body[data-page="admin"] .search svg { inline-size: 17px; block-size: 17px; color: var(--text-3); flex: none; }

/* ── filter tabs ── */

body[data-page="admin"] .tabs { display: flex; gap: 4px; flex-wrap: wrap; }
body[data-page="admin"] .tab,
body[data-page="admin"] .admin-nav a {
  padding: 7px 13px; border-radius: 999px;
  color: var(--text-3); font-size: var(--t-sm); font-weight: 600;
  white-space: nowrap;
  transition: color 0.16s var(--ease, ease), background-color 0.16s var(--ease, ease);
}
body[data-page="admin"] .tab:hover,
body[data-page="admin"] .admin-nav a:hover { color: var(--text); background: var(--surface-2); }
body[data-page="admin"] .tab.is-on,
body[data-page="admin"] .admin-nav a.on { color: var(--accent-ink); background: var(--accent); }
body[data-page="admin"] .admin-nav { display: flex; gap: 4px; flex-wrap: wrap; }

/* ── notices ── */

body[data-page="admin"] .flash {
  padding: 12px 15px; border-radius: var(--r-sm);
  background: var(--surface-2); color: var(--text-2); font-size: var(--t-sm);
}
body[data-page="admin"] .flash-error {
  background: color-mix(in oklab, var(--danger) 12%, transparent);
  color: var(--text);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--danger) 34%, transparent);
}
body[data-page="admin"] .flash-success {
  background: var(--accent-dim, var(--surface-2));
  color: var(--text);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent) 30%, transparent);
}

/* The switch between the two sides stays on every page at every width —
   it is how you get back, so hiding it on a phone strands you there.
   Compact rather than absent below 640px. */
@media (max-width: 640px) {
  .adm-tosite, .dash-admin { padding-inline: 9px; font-size: var(--t-xs); }
}

/* ── admin: the queue block ──────────────────────────────────────────
   These lived in the old layout's inline <style>, which went with the dark
   scheme. Restated here in the console's surfaces. */

body[data-page="admin"] .adm-queue {
  box-shadow: inset 0 0 0 1.5px color-mix(in oklab, var(--accent) 30%, transparent), var(--e1);
}
body[data-page="admin"] .adm-queue-hd {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; margin-block-end: 12px;
}
body[data-page="admin"] .adm-queue-items {
  display: grid; gap: 9px;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
body[data-page="admin"] .adm-q {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 12px 14px;
  border-radius: var(--r-sm);
  /* --surface-3, not --surface-2: in light mode surface-2 IS white,
     so a tile using it disappears into the card it sits on. */
  background: var(--surface-3, var(--surface-2));
  color: var(--text);
  font-size: var(--t-sm);
  text-decoration: none;
  transition: background-color 0.16s var(--ease, ease), transform 0.2s var(--spring, ease);
}
body[data-page="admin"] .adm-q:hover {
  background: var(--line-soft);
  transform: translateY(-1px);
}
body[data-page="admin"] .adm-q b {
  font-family: var(--mono);
  font-size: 1.3rem; font-weight: 700;
  color: var(--accent);
  line-height: 1;
}

@media (max-width: 560px) {
  body[data-page="admin"] .adm-queue-items { grid-template-columns: 1fr; }
  body[data-page="admin"] .kpi { padding: 12px 13px; }
}

/* ── admin: pagers, settings, KPI rhythm ─────────────────────────── */

body[data-page="admin"] .adm-pager {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
  padding-block-start: 2px;
}
body[data-page="admin"] .adm-pager .pager { padding: 0; }
body[data-page="admin"] .pgb.is-off { opacity: 0.35; pointer-events: none; }

/* The KPI row. Nine tiles in an auto-fit grid put eight on one line and left
   the ninth alone on a second, which is the empty space that stood out. Five
   columns with the float tile spanning two makes ten cells — two even rows. */
body[data-page="admin"] .adm-kpis {
  display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}
@media (min-width: 1180px) {
  body[data-page="admin"] .adm-kpis { grid-template-columns: repeat(5, 1fr); }
  body[data-page="admin"] .adm-kpis > .kpi:first-child { grid-column: span 2; }
}
/* Labels wrap rather than truncate. Clipping "Customer balances" to
   "Customer bala..." is worse than a second line, and the grid equalises tile
   heights anyway so a two-line label costs nothing in the row. */
body[data-page="admin"] .kpi > span:first-child {
  letter-spacing: 0.06em;
  min-block-size: 2.1em;
}
body[data-page="admin"] .kpi { gap: 2px; }

/* Settings: two columns of cards rather than one 620px ribbon down a wide
   screen. The save button spans both so it cannot end up beside a group. */
body[data-page="admin"] .adm-settings {
  display: grid; gap: 14px; align-items: start;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
}
body[data-page="admin"] .adm-settings > .card { margin: 0; }
body[data-page="admin"] .adm-settings > button { grid-column: 1 / -1; justify-self: start; min-inline-size: 220px; }

body[data-page="admin"] .adm-settings .card { display: grid; gap: 11px; align-content: start; }
body[data-page="admin"] .adm-settings h2 { font-size: 1.02rem; font-weight: 700; }

@media (max-width: 560px) {
  body[data-page="admin"] .adm-pager { justify-content: center; }
  body[data-page="admin"] .adm-settings > button { inline-size: 100%; }
}

/* The switch between the customer console and the admin. Present on every
   page for staff, and never rendered at all for anyone else -- the payload
   simply does not carry the flag. */
.dash-admin, .adm-tosite { flex: none; white-space: nowrap; }

/* ── admin pager ─────────────────────────────────────────────────────

   The shared .pgb rule was written for the console, where a pager button is
   a <button>. These are anchors, and an anchor is an inline box: it ignores
   min-inline-size and min-block-size entirely, so the buttons took their size
   from line-height instead. That produced tall uneven boxes that grew with
   the digit count — "101" visibly wider than "1" — with the label sitting off
   centre.

   Sized as a grid box here, so every one is the same square whatever is in
   it, and only genuinely long numbers stretch. */

body[data-page="admin"] .pager {
  display: flex; align-items: center; justify-content: flex-end;
  gap: 4px; padding: 0; flex-wrap: wrap;
}

body[data-page="admin"] .pgb {
  display: inline-grid; place-items: center;
  min-inline-size: 32px; block-size: 32px;
  padding-inline: 9px;
  border: 1px solid var(--line-soft);
  border-radius: 8px;
  background: var(--surface-1);
  color: var(--text-2);
  font-size: var(--t-sm); font-weight: 650;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  text-decoration: none;
  box-shadow: none;
  transition: color 0.14s var(--ease, ease), background-color 0.14s var(--ease, ease),
              border-color 0.14s var(--ease, ease);
}
body[data-page="admin"] .pgb:hover {
  color: var(--text);
  background: var(--surface-2);
  border-color: color-mix(in oklab, var(--accent) 34%, var(--line-soft));
}
body[data-page="admin"] .pgb.is-on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}
/* Ends of the range: present so the row does not reflow as you page, but
   plainly not pressable. */
body[data-page="admin"] .pgb.is-off {
  opacity: 0.4;
  pointer-events: none;
  background: transparent;
}

body[data-page="admin"] .pg-gap {
  padding-inline: 2px;
  color: var(--text-3);
  font-size: var(--t-sm);
  user-select: none;
}

/* The row count and the buttons, one line, pushed apart. */
body[data-page="admin"] .adm-pager {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
  padding-block: 2px;
}

@media (max-width: 560px) {
  body[data-page="admin"] .adm-pager { justify-content: center; }
  body[data-page="admin"] .pager { justify-content: center; }
  body[data-page="admin"] .pgb { min-inline-size: 34px; block-size: 34px; }
}

/* ── the view switch, and paying for it in the header ────────────────

   Adding "Admin view" to the console header pushed the topbar to 441px on a
   375px phone. The switch itself is not negotiable — it is how a staff member
   gets back — so the space comes from what is next to it.

   The language globe goes first: it is href="#" and does nothing yet, so it
   is the only control in that row costing space without doing work. */
@media (max-width: 560px) {
  .topbar-end > a.icon-btn[aria-label="Language"] { display: none; }

  .dash-admin, .adm-tosite {
    padding-inline: 8px;
    font-size: var(--t-xs);
    letter-spacing: -0.01em;
  }
}

/* Narrower still and the balance chip goes too, as it already did — the
   figure is one tap away on Wallet, and an overflowing header is worse. */
@media (max-width: 420px) {
  .topbar-end > .balance { display: none; }
}

/* ── admin: the accounts table ───────────────────────────────────────

   The balance control carries the page. It was an input placeholdered "+/-"
   next to a link called Apply — no way to tell whether typing 5 added or
   removed, on a control that moves real money. The amount is positive now and
   the two buttons carry the direction. */

body[data-page="admin"] .tbl-users td { vertical-align: middle; }

body[data-page="admin"] .usr-id { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
body[data-page="admin"] .usr-mail { color: var(--text); font-weight: 600; text-decoration: none; }
body[data-page="admin"] .usr-mail:hover { color: var(--accent); text-decoration: underline; }

body[data-page="admin"] .usr-bal { font-weight: 700; font-variant-numeric: tabular-nums; }

body[data-page="admin"] .usr-role {
  min-block-size: 32px;
  padding: 2px 26px 2px 9px;
  font-size: var(--t-xs);
  inline-size: auto;
}

/* amount + Add + Take, on one line */
body[data-page="admin"] .usr-adjust {
  display: flex; align-items: center; gap: 6px; flex-wrap: nowrap;
}

/* The currency sits inside the field, so the number is unambiguous without a
   separate label eating a column. */
body[data-page="admin"] .usr-amt { position: relative; flex: none; }
body[data-page="admin"] .usr-cur {
  position: absolute; inset-inline-start: 9px; inset-block-start: 50%;
  transform: translateY(-50%);
  color: var(--text-3); font-size: var(--t-sm);
  pointer-events: none;
}
body[data-page="admin"] .usr-amt .input {
  inline-size: 96px;
  min-block-size: 32px;
  padding: 2px 8px 2px 20px;
  font-size: var(--t-sm);
  text-align: end;
}

body[data-page="admin"] .usr-adjust .btn { flex: none; min-block-size: 32px; padding-inline: 12px; }

/* Take is destructive, so it reads as the quieter of the two — and greys out
   entirely on an account with nothing to take. */
body[data-page="admin"] .usr-take {
  color: var(--danger);
  box-shadow: inset 0 0 0 1.5px color-mix(in oklab, var(--danger) 32%, transparent);
}
body[data-page="admin"] .usr-take:hover:not([disabled]) {
  background: color-mix(in oklab, var(--danger) 12%, transparent);
}
body[data-page="admin"] .usr-take[disabled] {
  opacity: 0.34; cursor: not-allowed; box-shadow: inset 0 0 0 1.5px var(--line-soft); color: var(--text-3);
}

@media (max-width: 900px) {
  /* Below this the row is scrolling anyway; keep the control intact rather
     than letting it wrap into an unreadable stack. */
  body[data-page="admin"] .tbl-users { min-inline-size: 860px; }
}
