/* Lilmint — design tokens.
   A cool, blue-accented interface.

   A mid-tone blue cannot be both a legible background for a white label and a
   decorative tint, and it certainly cannot be read as text on white. So the
   accent is split into roles rather than being one value:

     --accent-strong  the FILL UNDER A WHITE LABEL -- 5.2:1 with --accent-text
     --accent         the brand colour, for everything that carries no label
     --accent-ink     the same colour used AS TEXT, darkened until it clears
                      4.5:1 on white.

   Collapsing these is exactly what produces the classic washed-out blue link.
   Every pair below is measured, not chosen by eye. */

:root {
  /* Cool neutrals. Picked from the same family as the blue, so the accent reads
     as a decision rather than a tint dropped onto an unrelated grey. */
  --bg: #f8fafc;
  --surface: #ffffff;
  --surface-2: #f1f5f9;
  --surface-3: #e2e8f0;

  --text: #0f172a;
  /* Not #64748b: that measures 4.34:1 on --surface-2 and 3.86:1 on --surface-3,
     and muted text sits on both (tile labels, tab counts, disabled pills). This
     step clears 4.5:1 on every one of the three light surfaces and still reads
     as secondary against #0f172a. */
  --muted: #59677c;
  --border: #e2e8f0;
  --border-strong: #cbd5e1;

  /* The brand blue in four steps, because one value cannot be both a legible
     background for a white label and a decorative tint:

       --accent         the brand colour: tints, edges, the gradient's light end
       --accent-strong  the FILL UNDER A WHITE LABEL -- 5.2:1 with --accent-text
       --accent-deep    one step deeper, for hover on that fill
       --accent-ink     the accent used AS TEXT -- 6.7:1 on white

     --accent is 3.7:1 against white, which is fine for a mark or a one-pixel
     edge and not for a sentence. Every surface that carries a LABEL therefore
     uses --accent-strong, and --accent is left to the things that carry none. */
  --accent: #3b82f6;
  --accent-strong: #2563eb;
  --accent-deep: #1d4ed8;
  --accent-text: #ffffff; /* the label ON a filled accent */
  --accent-ink: #1d4ed8; /* the accent as text — 6.7:1 on white */
  --accent-soft: #eff6ff;

  --danger: #c0392b;
  --danger-soft: #fdecea;
  /* Success stays green. Nobody reads blue as "it worked". The step is a shade
     darker than the obvious #15803d, which measures 4.49:1 on --ok-soft -- a
     hair under the floor for the 11px badge that actually uses the pair. */
  --ok: #14702f;
  --ok-soft: #e7f6ec;
  /* Warning is pushed orange so it cannot be confused with the brand. A blue
     banner on a blue interface teaches people to ignore banners. */
  --warn: #c2410c;
  --warn-soft: #fff3ec;

  --focus: #1d4ed8;

  --radius-sm: 8px;
  --radius: 12px;
  --radius-lg: 18px;

  --shadow-sm: 0 1px 2px rgba(15, 23, 42, .06);
  --shadow-lg: 0 2px 4px rgba(15, 23, 42, .05), 0 18px 40px rgba(15, 23, 42, .12);
  /* A middle shadow used to sit between these two. It was declared in both
     themes and referenced nowhere -- for years, by the look of it. */

  /* The scales that were missing. Colour, radius and shadow were tokenised from
     the start; spacing, type, stacking and motion were not, so each component
     chose its own numbers -- eighteen distinct paddings and thirteen font sizes
     between them. These are the values the file already reaches for, snapped to
     a rhythm rather than invented. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-7: 32px;

  --text-2xs: 11px;
  --text-xs: 12px;
  --text-sm: 13px;
  --text-base: 14px;
  --text-md: 15px;
  --text-lg: 16px;
  --text-xl: 18px;
  --text-2xl: 22px;
  --text-3xl: 26px;

  /* `font-weight: 650` was used in five places. A weight between 600 and 700
     only exists on a variable font: this machine renders it, and most Android
     phones -- which is where these customers are -- snap it to 700. So the same
     heading was two different weights depending on the handset. */
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;

  --radius-pill: 999px;

  /* The rail's width, needed in three places that must agree: the shell's grid
     column, the gradient that paints it, and the drawer it becomes on a narrow
     screen. The drawer was 272px against a 264px column, which is the kind of
     8px that only shows up when the drawer is open next to the layout it
     replaces. */
  --rail: 264px;

  /* The scrim behind the drawer and the purchase sheet: one literal used in two
     places, and the same value in both themes, which is wrong in one of them --
     half-black over a #0f172a page darkens almost nothing. */
  --overlay: rgba(15, 23, 42, .5);

  /* The stacking order was a comment and a set of literals. Putting the ladder
     here is what stops the next panel guessing at it. */
  --z-chart-tip: 5;
  --z-mobilebar: 20;
  --z-scrim: 30;
  --z-sidebar: 40;
  --z-modal: 50;
  --z-gate: 60;
  --z-toast: 70;

  /* Two durations, because the file has two intentions: .15s for a control
     changing state under the pointer, .22s for a panel crossing the screen.
     One token could not hold both, and the drawer is not a hover. */
  --dur-fast: .15s;
  --dur-base: .22s;

  /* Chart chrome, and again NOT --accent. #3b82f6 sits above the lightness band
     a mark wants on a white surface -- it reads as a highlight rather than as
     data -- so these are the chart-sized counterparts, validated against the
     surface for lightness, chroma and contrast. */
  --chart-series: #2563eb;
  --chart-grid: #e2e8f0;
  --chart-baseline: #cbd5e1;
  --chart-hit: rgba(37, 99, 235, .12);
}

/* Dark values are declared TWICE on purpose -- once for the operating system's
   preference and once for an explicit choice -- and the explicit choice has to
   win in both directions. The :not() guard is what lets an explicit light theme
   beat a dark OS setting; the bare scope is what lets an explicit dark theme
   beat a light one. Declaring them once inside the media query would make the
   toggle work in one direction only. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #0f172a;
    --surface: #1e293b;
    --surface-2: #27354b;
    --surface-3: #334155;

    --text: #f1f5f9;
    /* Lifted from #94a3b8 so muted text also clears 4.5:1 on --surface-3
       (tab counts and the "off" pill), where it measured 4.04:1. */
    --muted: #a2aec0;
    --border: #334155;
    --border-strong: #475569;

    /* The brand is lifted so it still reads on a dark page, and the ink step
       has to move with it -- #1d4ed8 on a #1e293b surface is unreadable, where
       #93c5fd clears 8.1:1. The FILL does not move: white on #2563eb is 5.2:1
       whatever the page behind the button is, and it is the label that has to
       be legible. */
    --accent: #60a5fa;
    --accent-strong: #2563eb;
    --accent-deep: #1d4ed8;
    --accent-text: #ffffff;
    --accent-ink: #93c5fd;
    --accent-soft: #1e3a5f;

    --danger: #ff7b6b;
    --danger-soft: #2e1a19;
    --ok: #4ade80;
    --ok-soft: #14291f;
    --warn: #fb923c;
    --warn-soft: #2c1a10;

    --focus: #93c5fd;

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .45);
    --shadow-lg: 0 2px 4px rgba(0, 0, 0, .5), 0 18px 40px rgba(0, 0, 0, .55);

    /* Denser than the light scrim, and black rather than slate-black: over a
       #0f172a page the light value barely registers at all. */
    --overlay: rgba(0, 0, 0, .62);

    /* Stepped for the dark surface rather than reused from light. The dark band
       is tighter (L 0.48-0.67 against light's 0.43-0.77), so the deeper blues
       are out of range here and only a lighter step fits. */
    --chart-series: #60a5fa;
    --chart-grid: #334155;
    --chart-baseline: #475569;
    --chart-hit: rgba(96, 165, 250, .18);
  }
}

:root[data-theme="dark"] {
  --bg: #0f172a;
  --surface: #1e293b;
  --surface-2: #27354b;
  --surface-3: #334155;

  --text: #f1f5f9;
  --muted: #a2aec0;
  --border: #334155;
  --border-strong: #475569;

  --accent: #60a5fa;
  --accent-strong: #2563eb;
  --accent-deep: #1d4ed8;
  --accent-text: #ffffff;
  --accent-ink: #93c5fd;
  --accent-soft: #1e3a5f;

  --danger: #ff7b6b;
  --danger-soft: #2e1a19;
  --ok: #4ade80;
  --ok-soft: #14291f;
  --warn: #fb923c;
  --warn-soft: #2c1a10;

  --focus: #93c5fd;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .45);
  --shadow-lg: 0 2px 4px rgba(0, 0, 0, .5), 0 18px 40px rgba(0, 0, 0, .55);
  --overlay: rgba(0, 0, 0, .62);

  --chart-series: #60a5fa;
  --chart-grid: #334155;
  --chart-baseline: #475569;
  --chart-hit: rgba(96, 165, 250, .18);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  background: var(--bg);
  color: var(--text);
  font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
  /* So form controls, scrollbars and the like follow the theme rather than
     staying stubbornly light inside a dark page. */
  color-scheme: light;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) body { color-scheme: dark; }
}
:root[data-theme="dark"] body { color-scheme: dark; }

body.is-auth {
  display: grid;
  place-items: center;
  padding: 24px;
}

h1, h2, h3 {
  letter-spacing: -.02em;
  /* Tight for headings, loose for reading. The default 1.5 is right for body
     copy and much too airy above 20px -- it is most of the difference between
     type that looks set and type that looks typed. */
  line-height: 1.2;
  /* Keeps a heading from breaking with one word stranded on its own line.
     Silently ignored where unsupported, which is a perfectly good outcome. */
  text-wrap: balance;
}

p { text-wrap: pretty; }

/* Numbers that sit above one another should line up. Only in columns and
   figures -- tabular digits on a large standalone number make 121 look loose. */
.num, .kv__val, .chart__tick, table td, table th { font-variant-numeric: tabular-nums; }

a { color: var(--accent-ink); }

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

[hidden] { display: none !important; }

/* Small caps label used above figures and form fields. */
.eyebrow {
  margin: 0;
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
}

.muted { color: var(--muted); }
.small { font-size: 13px; }
.spaced { margin-top: 16px; }

/* A label that is read but not shown. The inline number check needs a real
   <label> for the field, and a visible one would be a line the block does not
   need -- the hint underneath already says what the field is for. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ============================================================== auth page */

.card {
  width: 100%;
  max-width: 440px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: var(--space-7);
}

.card__head { margin-bottom: var(--space-6); }

/* The one lockup, used by every door: the rail, the shop's top bar, the auth
   cards and the account gate. A mark plus the name, so the storefront and the
   dashboard read as one business rather than two products. */
.brand { display: inline-flex; align-items: center; gap: var(--space-3); }

/* Block-level inside a card, where it sits on its own line above the heading
   and has to push it down. */
.card .brand { display: flex; margin-bottom: var(--space-5); }

.card h1 {
  margin: 0 0 var(--space-1);
  font-size: var(--text-3xl);
}

.sub {
  margin: 0;
  color: var(--muted);
  font-size: 14px;
}

/* The same construction as the shop's `.networks` control: one recessed track,
   the checked item lifted onto a raised pill. The two read as one system. */
.tabs {
  display: flex;
  gap: var(--space-1);
  padding: var(--space-1);
  margin-bottom: var(--space-6);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
}

.tabs label {
  flex: 1;
  padding: 9px 12px;
  text-align: center;
  font-size: 14px;
  font-weight: 600;
  color: var(--muted);
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: color var(--dur-fast), background var(--dur-fast);
}

.tabs input { position: absolute; opacity: 0; pointer-events: none; }

.tabs input:checked + label {
  color: var(--accent-ink);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
}

.tabs input:focus-visible + label {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.field { margin-bottom: 16px; }

.field label {
  display: block;
  margin-bottom: 6px;
  font-size: 14px;
  font-weight: 600;
}

.field__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.hint {
  margin: 6px 0 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--muted);
}

/* A checkbox and its sentence on one row, for a confirmation the customer has
   to read rather than a setting they scan. */
.consent {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  margin: 0 0 16px;
  font-size: 13px;
  line-height: 1.45;
  cursor: pointer;
}

.consent[hidden] { display: none; }

.consent input {
  flex: none;
  width: 17px;
  height: 17px;
  margin: 1px 0 0;
  accent-color: var(--accent);
  cursor: pointer;
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="tel"],
input[type="search"] {
  width: 100%;
  padding: 11px 13px;
  font: inherit;
  font-size: 15px;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  transition: border-color var(--dur-fast), box-shadow var(--dur-fast);
}

input::placeholder { color: var(--muted); opacity: .65; }

input:focus {
  border-color: var(--accent-ink);
  box-shadow: 0 0 0 3px var(--accent-soft);
  outline: none;
}

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

.input-group { position: relative; }
.input-group input { padding-right: 68px; }

.toggle {
  position: absolute;
  top: 50%;
  right: 8px;
  transform: translateY(-50%);
  padding: 5px 9px;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.toggle:hover { color: var(--accent-ink); }

.error {
  margin: 6px 0 0;
  font-size: 13px;
  color: var(--danger);
}

.alert {
  margin: 0 0 16px;
  padding: 11px 13px;
  font-size: 14px;
  line-height: 1.45;
  border-radius: var(--radius);
  border: 1px solid transparent;
}
.alert--error { color: var(--danger); background: var(--danger-soft); border-color: var(--danger); }
.alert--ok { color: var(--ok); background: var(--ok-soft); border-color: var(--ok); }
.alert--warn { color: var(--warn); background: var(--warn-soft); border-color: var(--warn); }
.alert--warn strong { font-weight: 700; }
/* Neither a success nor a failure: "we could not check" is an answer, and
   colouring it red would teach the reader to read it as one. */
.alert--note { color: var(--muted); background: var(--surface-2); border-color: var(--border-strong); }

/* =============================================================== buttons */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  font: inherit;
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  color: var(--accent-text);
  background: var(--accent-strong);
  border: 1px solid transparent;
  /* Pill, to match the chips and the segmented controls. Every action on the
     page is the same shape, which is most of what makes the set read as one. */
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: background var(--dur-fast), box-shadow var(--dur-fast);
}
.btn:hover { background: var(--accent-deep); }
/* One pixel of travel is the whole press feedback, and it is enough. */
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn:disabled:active { transform: none; }

.btn--block { width: 100%; }

.btn--ghost {
  color: var(--text);
  background: var(--surface);
  border-color: var(--border-strong);
}
.btn--ghost:hover { background: var(--surface-2); border-color: var(--muted); }

.btn--small { padding: var(--space-2) var(--space-3); font-size: var(--text-sm); }

/* ============================================================ app shell */

.app { display: grid; grid-template-columns: minmax(0, 1fr); min-height: 100vh; }

.sidebar {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-5) var(--space-4);
  background: var(--surface);
  border-right: 1px solid var(--border);
}

.sidebar__brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-1) var(--space-2) var(--space-5);
}

.sidebar__mark {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: var(--radius-sm);
  background: var(--accent-strong);
  color: var(--accent-text);
  font-size: var(--text-md);
  /* The one true 800 in the file, on the logotype glyph and nowhere else -- a
     deliberate one-off, so it stays a literal rather than widening the scale. */
  font-weight: 800;
  letter-spacing: -.02em;
}

.sidebar__name {
  font-size: var(--text-xl);
  font-weight: var(--weight-bold);
  letter-spacing: -.02em;
}

.nav { display: grid; gap: var(--space-1); }

.nav__item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-3);
  font: inherit;
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  color: var(--muted);
  background: none;
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
  text-align: left;
  transition: background var(--dur-fast), color var(--dur-fast);
}
.nav__item:hover { background: var(--surface-2); color: var(--text); }
a.nav__item { text-decoration: none; }

.nav__item[aria-current="page"] {
  color: var(--accent-ink);
  background: var(--accent-soft);
  font-weight: var(--weight-semibold);
}

.nav__icon { flex: none; width: 18px; height: 18px; }
.nav__item[aria-current="page"] .nav__icon { color: var(--accent-ink); }

/* The support links. `overflow-wrap: anywhere` because an email address is one
   unbreakable word, and the rail is a fixed 264px -- without it a longer address
   than this one would push the sidebar wider than the column it lives in. */
.sidebar__contact {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  padding: 0 var(--space-2);
  overflow-wrap: anywhere;
}
.sidebar__contact a {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--accent-ink);
  text-decoration: none;
}
.sidebar__contact a:hover { text-decoration: underline; }

.sidebar__foot {
  margin-top: auto;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

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

.avatar {
  display: grid;
  place-items: center;
  flex: none;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-size: 13px;
  font-weight: 700;
  text-transform: uppercase;
}

.sidebar__meta { min-width: 0; }
.sidebar__meta strong {
  display: block;
  font-size: 14px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sidebar__meta span {
  display: block;
  font-size: 12px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.mobilebar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: var(--z-mobilebar);
}

.iconbtn {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
}

.mobilebar__brand { font-size: 16px; font-weight: 700; letter-spacing: -.02em; }

.scrim {
  position: fixed;
  inset: 0;
  background: var(--overlay);
  z-index: var(--z-scrim);
  border: 0;
  padding: 0;
}

/* ----------------------------------------------------------- purchase popup */

/*
  Above the drawer's scrim (30) and the drawer itself (40), below the account
  gate (60) and the toasts (70) -- so a suspended account still covers it, and
  feedback still shows over it.

  Anchored to the TOP and scrolling itself, rather than centred and clipped.

  100dvh is deliberately not used: dvh tracks the URL bar, not the on-screen
  keyboard. The layout viewport that a fixed overlay is laid out against does
  not shrink when the keyboard opens, so a panel clamped to the viewport height
  is still taller than the part anyone can see -- and clamping it only adds a
  nested scrollport that traps the field under the keyboard. What works is a
  short panel on an overlay that can scroll, so the browser's scroll-into-view
  can bring the focused field up where it can be seen.
*/
.modal {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: grid;
  align-content: start;
  justify-items: center;
  padding: 16px;
  background: var(--overlay);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.modal__panel {
  width: 100%;
  max-width: 420px;
  padding: 20px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}

.modal__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 16px;
}

.modal__title { margin: 0; font-size: var(--text-xl); font-weight: var(--weight-semibold); }

/* The one control a thumb has to find without looking, so it is bigger than
   the 38px the toolbar's icon buttons use. */
.modal__head .iconbtn { width: 44px; height: 44px; }

.modal__panel .summary { margin: 0 0 16px; }

/* Every action in the sheet is a full-width thumb target. */
.modal__panel .btn { min-height: 48px; }

@media (max-width: 559px) {
  /* iOS zooms the page in when a field under 16px takes focus, and inside a
     fixed sheet that zoom shifts everything the customer is looking at. The
     house input size is 15px, so the field is nudged up just here. */
  .modal__panel input[type="tel"] { font-size: 16px; }
}

@media (min-width: 560px) {
  /* `safe` keeps a panel taller than the viewport from losing its top edge off
     the scrollport, which is the classic centred-dialog bug. A browser that
     does not know the value drops the line and keeps the `start` above. */
  .modal { align-content: safe center; padding: 24px; }
}

/* ------------------------------------------------------------------ content */

.content {
  padding: 24px;
  max-width: 1080px;
  width: 100%;
  margin: 0 auto;
  display: grid;
  gap: 20px;
  align-content: start;
  min-width: 0;
}

.view { display: grid; gap: 20px; min-width: 0; }

.view__head { display: grid; gap: 2px; }

/* Fluid rather than stepped: a section heading has no business being the same
   size on a phone as on a 1440px screen. */
.view__title { margin: 0; font-size: clamp(20px, 1.2rem + 0.6vw, 25px); }

/* -------------------------------------------------------------- hero card */

/* The gradient's first two stops are the ones that can carry a white label. The
   brand blue is the LAST stop only, at the far corner: white on it is 3.7:1,
   which is fine for a mark and for an edge but not for the label sitting in the
   top-left. Giving it the last 45% keeps that label on a fill that clears 4.5:1
   across its whole width while the brand still shows on the card. */
.balance-card {
  position: relative;
  overflow: hidden;
  padding: var(--space-6);
  border-radius: var(--radius-lg);
  background: linear-gradient(135deg, var(--accent-deep) 0%, var(--accent-strong) 55%, var(--accent) 100%);
  color: var(--accent-text);
  box-shadow: var(--shadow-lg);
}

.balance-card__label {
  margin: 0 0 var(--space-2);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  /* This pair -- white on the brand blue -- measures 3.7:1. That clears the 3:1
     floor the large amount lives on, but not the 4.5:1 an ordinary label wants,
     so there is NO headroom to soften it to secondary here. It stays full
     strength; opacity was dropped when the fill stopped being a bright yellow
     that had 9.1:1 to spend. */
}

.balance-card__amount {
  margin: 0;
  font-size: clamp(32px, 6vw, 44px);
  font-weight: 700;
  /* The one number the whole page leads with, so it gets more room and a
     tighter fit than anything else. Proportional figures, never tabular --
     equal-width digits make a large number look loose. */
  letter-spacing: -.035em;
  line-height: 1.05;
}

.balance-card__foot {
  margin: var(--space-5) 0 0;
  font-size: var(--text-sm);
}

.balance-card__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-5);
}

.balance-card .btn--onaccent {
  color: var(--accent-ink);
  background: var(--surface);
  border-color: transparent;
}
.balance-card .btn--onaccent:hover { background: var(--surface-2); }

/* ------------------------------------------------------------------- tiles */

.tiles {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}

/* A well, not a card. These are a stat strip inside a panel, and while they
   carried their own border AND shadow they were three more cards competing with
   the panel holding them -- which is how every surface on the page ended up
   looking exactly as important as every other one. Recessed fill, no shadow, and
   the panel becomes the only raised thing. /admin shares this, and its 4-up tile
   row sits on the same background, so it gains the same reading.

   The border stays. In dark mode --surface-2 against --bg is a clear step, but in
   light these two stone tones are within a couple of percent of each other, and
   a well with no edge simply disappears. */
.tile {
  padding: var(--space-4);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  min-width: 0;
}

.tile__label {
  margin: 0 0 6px;
  font-size: 13px;
  color: var(--muted);
}

.tile__value {
  margin: 0;
  font-size: var(--text-2xl);
  font-weight: 700;
  letter-spacing: -.02em;
}

/* ------------------------------------------------------------------ panels */

.panel {
  min-width: 0;
  padding: var(--space-5);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}

/* The shop's catalogue drops the panel chrome so its cards sit directly on the
   page. That is the whole difference between a wall of tiles and a box of
   cards: with a panel behind them the cards are inside a container, and the
   container is what the eye sees first. */
.panel--flush {
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

.panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.panel__title { margin: 0; font-size: var(--text-lg); font-weight: var(--weight-semibold); }

.linkish {
  font: inherit;
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--accent-ink);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}
.linkish:hover { text-decoration: underline; }

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

.chips { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-4); }

.chip {
  padding: var(--space-2) var(--space-3);
  font: inherit;
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: border-color var(--dur-fast), color var(--dur-fast), background var(--dur-fast);
}
.chip:hover { border-color: var(--accent-ink); color: var(--accent-ink); background: var(--accent-soft); }

/* ---------------------------------------------------------------- networks */

/* A segmented control, in the same construction as the auth page's `.tabs`: one
   recessed track, the checked item lifted onto a raised pill. `.tabs` itself is
   untouched -- it is shared with the sign-in page, which is not in scope -- but
   the two now read as one system rather than two.

   The DOM is generated by renderNetworks() and is not up for negotiation: the
   radio must stay a sibling BEFORE its label, because the checked and
   focus-visible states are drawn on the label through `+`. */
.networks {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  padding: var(--space-1);
  margin-bottom: var(--space-4);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
}
.network { position: relative; }
.network input { position: absolute; opacity: 0; pointer-events: none; }

.network label {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  color: var(--muted);
  border: 0;
  border-radius: var(--radius-pill);
  cursor: pointer;
  background: transparent;
  transition: color var(--dur-fast), background var(--dur-fast), box-shadow var(--dur-fast);
}

.network input:checked + label {
  color: var(--accent-ink);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
}

.network input:focus-visible + label {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* Operator brand colours, used only on the dot -- never as text, where they
   would fail contrast.

   The base colour matters: the dots are generated from whatever networks the
   API returns, so a network with no brand colour rule renders a dot that is
   simply invisible. Grey is at least a dot. */
.dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; flex: none; background: var(--muted); }
.dot--MTN { background: #ffcc00; }
.dot--Telecel { background: #e60000; }
.dot--AirtelTigo { background: #0033a0; }

/* ----------------------------------------------------------------- bundles */

/* The catalogue is the product, so this is the one thing on the page that lifts
   under the pointer. Everything else stays flat and lets it. */
.bundles {
  display: grid;
  gap: var(--space-3);
  /* Stepped rather than `auto-fill`. Inside the dashboard the grid lives in a
     column that is the viewport minus a 264px rail, so auto-fill has no idea
     how much room it really has and lands six or seven across on a desktop
     where five is as dense as a price list stays legible. Two at 360px, where
     the content box is about 328px: one per row reads as a list, not a shop. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

@media (min-width: 600px) { .bundles { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 880px) { .bundles { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (min-width: 1120px) { .bundles { grid-template-columns: repeat(5, minmax(0, 1fr)); } }

/* A wall of uniform tiles: white on the page, a hairline edge, the softest
   shadow in the file. Column direction with the call to action on `margin-top:
   auto` is what squares the buttons up along the bottom of a row, which is most
   of what makes a grid of cards look like a shop rather than a list. */
.bundle {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  font: inherit;
  color: inherit;
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  transition: border-color var(--dur-fast), box-shadow var(--dur-fast), transform var(--dur-fast);
}
.bundle:hover {
  border-color: var(--accent);
  box-shadow: var(--shadow-lg);
  /* One pixel of travel, the same feedback `.btn:active` gives. */
  transform: translateY(-1px);
}

.bundle[aria-pressed="true"] {
  border-color: var(--accent-ink);
  background: var(--accent-soft);
  /* The ink, not the fill: a #3b82f6 ring on a #eff6ff background is a 3.4:1
     edge, which is the opposite of what a selected state is for. */
  box-shadow: inset 0 0 0 1px var(--accent-ink);
}

/* A dot, not a tick and not a class: a pseudo-element adds no child node, and
   renderBundles() walks the container's children by identity to deselect the
   rest -- a wrapper or an extra element per card would break that. */
.bundle[aria-pressed="true"]::after {
  content: '';
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent-ink);
}

.bundle__size { display: block; font-size: var(--text-lg); font-weight: var(--weight-semibold); }
.bundle__meta { display: block; font-size: var(--text-xs); color: var(--muted); }
.bundle__price {
  display: block;
  font-size: var(--text-xl);
  font-weight: var(--weight-bold);
  color: var(--accent-ink);
  letter-spacing: -.02em;
}

/* The card's own button, drawn rather than nested: the card IS a <button>, and
   a button inside a button is not markup a browser will accept. `margin-top:
   auto` is what squares the buttons along the bottom of a row -- the flex gap
   above is the floor, and the leftover height goes here. */
.bundle__cta {
  margin-top: auto;
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  text-align: center;
  color: var(--accent-text);
  background: var(--accent-strong);
  border-radius: var(--radius-pill);
}

/* ------------------------------------------------------------------- steps */

.steps {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: minmax(0, 1fr);
  margin: 0;
  padding: 0;
  list-style: none;
}

.step { display: grid; gap: var(--space-2); align-content: start; }

.step__num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  /* The fill carrying the label, which is the pair the whole accent system is
     built around -- the same one the buttons use. */
  color: var(--accent-text);
  background: var(--accent-strong);
  border-radius: 50%;
}

.step__name { margin: 0; font-size: var(--text-md); font-weight: var(--weight-semibold); }
.step__text { margin: 0; font-size: var(--text-base); color: var(--muted); }

/* Three across only once there is room for three. `repeat(3, 1fr)` with no
   guard is the classic way to make a phone scroll sideways, and minmax(0, 1fr)
   stops a long word widening its own column even here. */
@media (min-width: 640px) {
  .steps { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-5); }
}

/* --------------------------------------------------------------------- faq */

/* Full-width rows divided by hairlines, not a stack of cards. Every question is
   a band in one list, which is what an accordion of unrelated answers should
   look like -- cards would hand each one the weight of a headline. */
.faq {
  display: grid;
  gap: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.faq__item { background: none; border: 0; border-radius: 0; }
.faq__item + .faq__item { border-top: 1px solid var(--border); }

.faq__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  cursor: pointer;
  list-style: none;
}
.faq__q::-webkit-details-marker { display: none; }

/* A chevron from two borders rather than an icon: no asset, no extra markup, and
   it turns with the open state. Its transition is covered by the single
   reduced-motion block at the end of the file -- which is the reason that block
   was consolidated rather than left in two pieces. */
.faq__q::after {
  content: '';
  flex: none;
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  transform: rotate(45deg) translateY(-2px);
  transition: transform var(--dur-fast);
}
.faq__item[open] .faq__q::after { transform: rotate(225deg) translateY(-2px); }

.faq__a {
  margin: 0;
  padding: 0 var(--space-4) var(--space-4);
  font-size: var(--text-base);
  color: var(--muted);
  line-height: 1.45;
}

/* ---------------------------------------------------------------- recipient */

.recipient { display: grid; gap: 12px; grid-template-columns: minmax(0, 1fr); align-items: end; }

@media (min-width: 560px) {
  .recipient { grid-template-columns: minmax(0, 1fr) auto; }
}

.saved { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 14px 0 0; }

.summary {
  margin-top: 18px;
  padding: 15px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 14px;
}

.summary__row { display: flex; justify-content: space-between; gap: 12px; padding: 4px 0; }

.summary__row--total {
  margin-top: 8px;
  padding-top: 11px;
  border-top: 1px solid var(--border-strong);
  font-weight: 700;
}

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

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

.table-wrap { overflow-x: auto; max-width: 100%; }
.table-wrap td.wrap { white-space: normal; }

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

th, td {
  padding: 11px 12px;
  text-align: left;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

th {
  font-size: var(--text-2xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
}

tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: var(--surface-2); }

.num { text-align: right; font-variant-numeric: tabular-nums; }
.amount--out { font-weight: 600; }
.amount--in { color: var(--ok); font-weight: 600; }

.badge {
  display: inline-block;
  margin-left: 8px;
  padding: 2px 8px;
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: .02em;
  text-transform: uppercase;
  border-radius: var(--radius-pill);
  background: var(--ok-soft);
  color: var(--ok);
}

/* The same three colours on the dashboard and the guest pages. Green is still
   only "delivered": orange is in flight, red is the one that needs money back. */
.badge--ok { background: var(--ok-soft); color: var(--ok); }
.badge--pending { background: var(--warn-soft); color: var(--warn); }
.badge--failed { background: var(--danger-soft); color: var(--danger); }

/* ------------------------------------------------------------ orders page */

.order-tabs {
  display: flex;
  gap: var(--space-1);
  padding: var(--space-1);
  margin-bottom: var(--space-4);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  /* Four tabs on a phone: scroll within the strip rather than the page. */
  overflow-x: auto;
}

.order-tab {
  flex: 1 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 9px 12px;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--dur-fast), background var(--dur-fast);
}

.order-tab[aria-selected="true"] {
  color: var(--accent-ink);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
}

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

.order-tab__count {
  min-width: 20px;
  padding: 1px 6px;
  border-radius: var(--radius-pill);
  background: var(--surface-3);
  color: var(--muted);
  font-size: var(--text-2xs);
  font-variant-numeric: tabular-nums;
}

.order-tab[aria-selected="true"] .order-tab__count {
  background: var(--accent-soft);
  color: var(--accent-ink);
}

/* The table keeps its columns and scrolls sideways inside this wrapper when the
   screen is too narrow. Squeezing seven or eight columns into a phone is what
   put Date on top of Bundle. */
.orders-table-wrap {
  max-width: 100%;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.orders-table {
  table-layout: fixed;
  width: 100%;
  /* Narrower than this and the fixed Date/Amount columns cannot hold their
     text, so the wrapper scrolls instead of letting cells overlap. */
  min-width: 880px;
}

.orders-table--admin { min-width: 1020px; }

.orders-table th,
.orders-table td {
  padding: 11px 12px;
  /* The global table rule is nowrap, which is right for a ledger and wrong
     here: a long reference or a status sentence must wrap, not widen the page. */
  white-space: normal;
  vertical-align: top;
  overflow-wrap: anywhere;
}

.orders-table th {
  border-bottom: 1px solid var(--border);
  /* Column headings are identifiers, not prose. "NETWORK" must not break into
     "NETWOR" and "K" in a column sized for the value. */
  white-space: nowrap;
}

/* Fixed proportions rather than auto widths. Date is wide enough for the full
   date, Bundle is wide enough to wrap a long name, Network fits its heading,
   and Amount is wide enough for one line of money. */
.orders-table th:nth-child(1) { width: 16%; } /* Order */
.orders-table th:nth-child(2) { width: 14%; } /* Date */
.orders-table th:nth-child(3) { width: 17%; } /* Bundle */
.orders-table th:nth-child(4) { width: 13%; } /* Recipient */
.orders-table th:nth-child(5) { width: 11%; } /* Network */
.orders-table th:nth-child(6) { width: 14%; } /* Amount */
.orders-table th:nth-child(7) { width: 15%; } /* Status */

/* The administrator list has one more column than the customer list. These come
   after the seven above so the same-specificity selectors resolve in this
   table's favour. */
.orders-table--admin th:nth-child(1) { width: 15%; } /* Order */
.orders-table--admin th:nth-child(2) { width: 12%; } /* Date */
.orders-table--admin th:nth-child(3) { width: 13%; } /* Customer */
.orders-table--admin th:nth-child(4) { width: 14%; } /* Bundle */
.orders-table--admin th:nth-child(5) { width: 11%; } /* Recipient */
.orders-table--admin th:nth-child(6) { width: 10%; } /* Network */
.orders-table--admin th:nth-child(7) { width: 13%; } /* Amount */
.orders-table--admin th:nth-child(8) { width: 12%; } /* Status */

.orders-table td { border-bottom: 1px solid var(--border); }

.order-row:last-child td { border-bottom: 0; }
.order-row:hover td { background: var(--surface-2); }

/* The order just placed. A tint and a left edge rather than a flash, so it is
   still identifiable after the customer has scrolled back to it. */
.order-row--highlight td { background: var(--accent-soft); }
.order-row--highlight td:first-child { box-shadow: inset 3px 0 0 var(--accent); }

.order-cell { display: block; min-width: 0; }
.order-cell__strong { display: block; font-size: var(--text-sm); font-weight: 600; }
.order-cell__strong--nowrap { white-space: nowrap; }
.order-cell__sub { display: block; margin-top: 2px; color: var(--muted); font-size: var(--text-xs); }

.order-cell__note {
  display: block;
  margin-top: 4px;
  color: var(--muted);
  font-size: var(--text-xs);
  line-height: 1.45;
}

.orders-table .badge { margin-left: 0; }

/* The single-order guest pages: the badge and its sentence sit together, with
   the badge outside the prose so it reads as a state, not a sentence opener. */
.order-state {
  display: flex;
  align-items: flex-start;
  gap: 10px;
}

.order-state .badge { margin-left: 0; flex: none; }

.order-state__sentence {
  margin: 0;
  font-size: var(--text-sm);
  line-height: 1.45;
  color: var(--text);
}

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

.activity { display: grid; gap: 2px; }

.activity__row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 10px;
  border-radius: var(--radius);
}
.activity__row:hover { background: var(--surface-2); }

.activity__icon {
  display: grid;
  place-items: center;
  flex: none;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  font-size: 15px;
  font-weight: 700;
  background: var(--surface-3);
  color: var(--muted);
}
.activity__icon--in { background: var(--accent-soft); color: var(--accent-ink); }

.activity__main { min-width: 0; flex: 1; }
.activity__title {
  font-size: 14px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.activity__sub { font-size: 12px; color: var(--muted); }

.activity__amount { font-size: var(--text-base); font-weight: var(--weight-bold); font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------ beneficiaries */

.bene-list { display: grid; gap: 8px; margin-top: 14px; }

.bene {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.bene__name {
  font: inherit;
  font-weight: 600;
  color: var(--text);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-align: left;
}
.bene__name:hover { color: var(--accent-ink); }

.bene__phone { font-size: 13px; color: var(--muted); }

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

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

select {
  padding: 8px 11px;
  font: inherit;
  font-size: 14px;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  cursor: pointer;
}

/* -------------------------------------------------------------------- misc */

.kv { display: grid; gap: 0; margin: 0; font-size: 14px; }

/* Label left, value right, hairline between -- the receipt shape, which is what
   an order's details are. The first and last rows drop their outer padding so
   the block sits flush in whatever holds it. */
.kv__row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) 0;
}
.kv__row + .kv__row { border-top: 1px solid var(--border); }
.kv__row:first-child { padding-top: 0; }
.kv__row:last-child { padding-bottom: 0; }

.kv__key { color: var(--muted); }
.kv__val { text-align: right; font-weight: 500; }

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

/* ============================================================ admin panel */

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 14px 24px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.topbar__left { display: flex; align-items: center; gap: 10px; }
.topbar__right { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

.topbar__brand {
  font-size: 16px;
  font-weight: 700;
  letter-spacing: -.02em;
}

.topbar__user { font-size: 14px; color: var(--muted); }
.topbar__user strong { color: var(--text); }

.pill {
  display: inline-block;
  padding: 2px 9px;
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  border-radius: var(--radius-pill);
  border: 1px solid transparent;
  white-space: nowrap;
}
.pill--admin { color: var(--accent-ink); background: var(--accent-soft); border-color: var(--accent-ink); }
.pill--on { color: var(--ok); background: var(--ok-soft); border-color: var(--ok); }
.pill--off { color: var(--muted); background: var(--surface-3); border-color: var(--border-strong); }
.pill--warn { color: var(--warn); background: var(--warn-soft); border-color: var(--warn); }

/* Form beside the table on a wide screen, stacked on a narrow one. */
.admin-grid {
  display: grid;
  gap: 20px;
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
  min-width: 0;
}

@media (min-width: 1000px) {
  .admin-grid { grid-template-columns: 340px minmax(0, 1fr); }
  .admin .content { max-width: 1220px; }
}

.rowactions { display: flex; gap: 6px; justify-content: flex-end; }

/* A disabled bundle is still listed -- it has to be, so it can be brought
   back -- so it is dimmed rather than hidden. */
tr.row--off td { opacity: .55; }
tr.row--off .price-now { text-decoration: line-through; }

.conflict-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }

/* A two-up split for panels that are peers (best sellers beside networks). */
.admin-grid--split { grid-template-columns: minmax(0, 1fr); }
@media (min-width: 1000px) {
  .admin-grid--split { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}

/* A section heading inside a panel, where an h2 would be too loud. */
.subhead {
  margin: var(--space-5) 0 var(--space-1);
  padding-top: var(--space-4);
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  border-top: 1px solid var(--border);
}

.balance--sm { font-size: 30px; margin-bottom: 6px; }

/* Covers the whole app for an account that cannot use it -- suspended, or
   holding a temporary password. Not dismissable, because there is nothing
   behind it the server would answer. */
.gate {
  position: fixed;
  inset: 0;
  z-index: var(--z-gate);
  display: grid;
  place-items: center;
  padding: 24px;
  background: var(--bg);
  overflow-y: auto;
}

.rowactions--start { justify-content: flex-start; flex-wrap: wrap; }
.rowactions--start .btn { flex: none; }

.filters input[type="search"] { width: auto; min-width: 200px; flex: 1 1 200px; }

/* ================================================================= charts */

.chart { position: relative; width: 100%; }
.chart svg { display: block; width: 100%; height: auto; }

.chart__bar { fill: var(--chart-series); }
.chart__grid { stroke: var(--chart-grid); stroke-width: 1; }
.chart__baseline { stroke: var(--chart-baseline); stroke-width: 1; }

/* Axis text wears an ink token, never the series colour. Identity comes from
   the mark beside it. */
.chart__tick {
  fill: var(--muted);
  font-size: var(--text-2xs);
  font-variant-numeric: tabular-nums;
}

.chart__peak { fill: var(--text); font-size: 11px; font-weight: 600; }

/* One hit area per slot, full height and never narrower than a comfortable
   touch. Landing on a 6px bar should not be a test of aim. */
.chart__hit { fill: transparent; outline: none; }
.chart__hit:hover { fill: var(--chart-hit); }
.chart__hit:focus-visible { fill: var(--chart-hit); stroke: var(--focus); stroke-width: 2; }

.chart__tip {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  display: grid;
  gap: 1px;
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  background: var(--text);
  color: var(--bg);
  font-size: 12px;
  white-space: nowrap;
  pointer-events: none;
  z-index: var(--z-chart-tip);
}
.chart__tip strong { font-size: 13px; }
.chart__tip span { opacity: .78; }

/* ============================================================== skeletons */

/* Shown only on a first load, never on a refetch -- replacing a render that is
   already on screen with grey blocks is worse than a moment of stale numbers. */
.skeleton {
  border-radius: var(--radius-sm);
  background: linear-gradient(90deg, var(--surface-2) 25%, var(--surface-3) 37%, var(--surface-2) 63%);
  background-size: 400% 100%;
  animation: skeleton 1.4s ease infinite;
}

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

.skeleton--line { height: 12px; }
.skeleton--value { height: 26px; width: 60%; }
.skeleton--chart { height: 172px; width: 100%; }

/* ================================================================== toasts */

.toasts {
  position: fixed;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  z-index: var(--z-toast);
  display: grid;
  gap: 8px;
  width: min(420px, calc(100vw - 32px));
}

.toast {
  padding: 11px 14px;
  font-size: 14px;
  line-height: 1.45;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-left-width: 3px;
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  animation: toast-in .18s ease;
}
.toast--ok { border-left-color: var(--ok); }
.toast--error { border-left-color: var(--danger); }
.toast--info { border-left-color: var(--accent-ink); }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(8px); }
}

/* Anchors dressed as buttons and links need the underline taken off: `.btn` and
   `.linkish` were written for <button>, where the browser supplies none, and the
   storefront is the first page to use them on an <a>. */
a.btn, a.linkish { text-decoration: none; }

/* ------------------------------------------------------------- storefront */

/* The public front door: no rail, no session, no drawer. It borrows the content
   column and the panels -- the catalogue itself is the dashboard's, class for
   class -- and adds what a front door needs: a header strip, a hero, and a
   footer carrying the ways to reach a person. */

/* A thin strip rather than a chunky header, and it stays for the whole scroll,
   so the way in is never more than a glance away on a long price list. */
.sitebar {
  position: sticky;
  top: 0;
  z-index: var(--z-mobilebar);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

/* The same measure and gutter as `.content`, so the brand lines up with the
   first panel instead of floating at the window edge. */
.sitebar__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  max-width: 1080px;
  margin: 0 auto;
  padding: var(--space-3) var(--space-6);
}

.shop-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }

/* The bar's right-hand cluster. The number check sits beside the way in rather
   than inside `#storeActions`, whose children the storefront script replaces. */
.sitebar__right { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }
.sitebar__link {
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  color: var(--text);
  text-decoration: none;
}
.sitebar__link:hover { color: var(--accent-ink); }

/* The brand is a link to the shop on the pages that have one, and a link must
   not arrive underlined simply because it is one. */
a.brand { color: inherit; text-decoration: none; }

/* One field, one answer. A full-width panel on a desktop is a page of emptiness
   around a text box. */
.verify-panel { max-width: 520px; margin: 0 auto; }
.verify-note { text-align: center; }

/* The inline number check above the catalogue, on the shop and on the
   dashboard's Buy data view. Field and button side by side on a wide screen,
   stacked on a phone; the hint sits under the row, and the result under that.
   Kept compact on purpose -- the cards are the subject of the page. */
.verify-cta {
  display: grid;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.verify-cta__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  gap: var(--space-2);
}

/* One column on a phone: the field takes the row and the button goes under it,
   which is the easiest pair to use with a thumb. */
@media (max-width: 559px) {
  .verify-cta__row { grid-template-columns: minmax(0, 1fr); }
}

.verify-cta__note { margin: 0; }

/* The alert's own bottom margin is for a banner in the flow; the grid's gap
   already separates this one. */
.verify-cta__result { margin: 0; }

/* Three things do not fit across a phone's bar. The LINK is the one that goes:
   the same journey has a full-width button directly above the catalogue, and
   the footer keeps the link as well. Leaving it in wraps the bar to two rows
   with the brand alone on the first, which pushes everything down for a
   shortcut that is already a few centimetres below. */
@media (max-width: 480px) {
  .sitebar__link { display: none; }
}

/* The theme control is a full-width rail button on the dashboard. On a plain
   page it would be a bar right across the content column, so it keeps its own
   width and sits under the panel. */
.content > #themeHost {
  display: flex;
  justify-content: center;
  max-width: 240px;
  margin: 0 auto;
}

/* A sticky header sits over whatever an in-page link lands on, so an anchor
   target has to reserve the bar's height or "Browse bundles" scrolls the first
   row of cards underneath it. */
.anchor { scroll-margin-top: 88px; }

/* The headline block. Deliberately NOT a card: a hero in a box, on a page made
   of boxes, is one container too many. */
.hero { display: grid; gap: var(--space-3); padding: var(--space-4) 0 var(--space-2); }

.hero__title {
  margin: 0;
  max-width: 24ch;
  font-size: clamp(28px, 1.5rem + 2.2vw, 42px);
  font-weight: var(--weight-bold);
  letter-spacing: -.03em;
  line-height: 1.12;
}

.hero__sub { margin: 0; max-width: 62ch; font-size: var(--text-lg); color: var(--muted); }

/* Three short claims that are true of this deployment, NOT a statistics strip.
   A figure nobody measured is a lie with a font on it. */
.hero__points {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-1) 0 0;
  padding: 0;
  list-style: none;
}

.hero__point {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 6px var(--space-3) 6px 10px;
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--accent-ink);
  background: var(--accent-soft);
  border-radius: var(--radius-pill);
}

/* The tick is two borders turned 45 degrees -- no asset, no extra markup, and
   the same trick the FAQ chevron uses. */
.hero__point::before {
  content: '';
  flex: none;
  width: 5px;
  height: 9px;
  margin-top: -2px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
}

.hero__cta { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-2); }

/* ----------------------------------------------------------- site footer */

.sitefoot {
  display: grid;
  gap: var(--space-3);
  margin-top: var(--space-5);
  padding-top: var(--space-6);
  border-top: 1px solid var(--border);
}

.sitefoot__text { margin: 0; max-width: 62ch; font-size: var(--text-base); color: var(--muted); }

.sitefoot__links { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); }
.sitefoot__links a { font-size: var(--text-base); font-weight: var(--weight-semibold); }

.sitefoot__cta { display: flex; flex-wrap: wrap; gap: var(--space-3); }

/* ============================================================== shop home */

.sitebar__nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

/* The old storefront hid its one link on a narrow bar. The new layout needs
   Verify, Track order and Login as well, so the navigation moves to its own row
   on a phone rather than dropping them. */
@media (max-width: 480px) {
  .sitebar__inner { align-items: flex-start; }
  .sitebar__nav { width: 100%; gap: var(--space-2) var(--space-3); }
  .sitebar__link { display: inline; }
}

.shop-content { max-width: 1080px; }

/* Three tappable network cards. The logos are branded tiles rather than image
   assets, so there is nothing extra to load and the colours come from the same
   palette as the dots used elsewhere. */
.network-cards {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}

.network-card {
  display: grid;
  justify-items: center;
  gap: 6px;
  min-width: 0;
  padding: var(--space-4) var(--space-2);
  font: inherit;
  color: var(--text);
  text-align: center;
  background: var(--surface);
  border: 2px solid var(--border);
  border-radius: var(--radius-lg);
  cursor: pointer;
  transition: border-color var(--dur-fast), background var(--dur-fast), box-shadow var(--dur-fast);
}
.network-card:hover { border-color: var(--border-strong); box-shadow: var(--shadow-sm); }
.network-card.is-selected,
.network-card[aria-checked="true"] {
  border-color: var(--accent-ink);
  background: var(--accent-soft);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.network-logo {
  display: grid;
  place-items: center;
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: 12px;
  color: #fff;
  font-size: var(--text-2xs);
  font-weight: 800;
  letter-spacing: -.02em;
}
.network-logo--MTN { color: #0f172a; background: #ffcc00; }
.network-logo--Telecel { background: #e60000; }
.network-logo--AirtelTigo { background: #0033a0; }

.network-card__name {
  min-width: 0;
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  overflow-wrap: anywhere;
}
.network-card__hint { font-size: var(--text-2xs); color: var(--muted); }
.network-hint { margin: 0 0 var(--space-4); }

@media (max-width: 420px) {
  .network-card { padding: var(--space-3) 4px; }
  .network-card__name { font-size: var(--text-xs); }
  .network-card__hint { display: none; }
}

/* The guest-only invitation. It sits between the hero and the catalogue, where
   the saving it advertises is about to be shown. */
.promo-card {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-5);
  background: linear-gradient(135deg, var(--accent-soft), var(--surface));
  border: 1px solid var(--accent);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}
.promo-card__copy { flex: 1 1 280px; min-width: 0; }
.promo-card__title { margin: 0 0 4px; font-size: var(--text-xl); }
.promo-card__text { margin: 0; max-width: 62ch; font-size: var(--text-base); color: var(--muted); }

/* The shop's package card. The size and price are the two loudest things; the
   Instant tag and the Save badge are quiet enough to scan past. */
.bundle--shop { gap: 9px; }

.bundle__head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}
.bundle__head .network-logo {
  width: 26px;
  height: 26px;
  border-radius: 8px;
  font-size: 9px;
}
.bundle__head .bundle__size {
  min-width: 0;
  font-size: clamp(18px, 1rem + .4vw, 21px);
  overflow-wrap: anywhere;
}

.bundle__instant {
  align-self: flex-start;
  padding: 2px 8px;
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--accent-ink);
  background: var(--accent-soft);
  border-radius: var(--radius-pill);
}

.price-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  min-width: 0;
}
.price-row__label { font-size: var(--text-xs); color: var(--muted); }
.price-row__value {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.price-row--member .price-row__value { color: var(--accent-ink); }
.price-row--main .price-row__value {
  font-size: var(--text-xl);
  font-weight: var(--weight-bold);
  color: var(--accent-ink);
  letter-spacing: -.02em;
}
.price-row--strike .price-row__value {
  color: var(--muted);
  font-weight: var(--weight-medium);
  text-decoration: line-through;
}

.save-badge {
  align-self: flex-start;
  padding: 2px 8px;
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  color: var(--ok);
  background: var(--ok-soft);
  border-radius: var(--radius-pill);
}

/* The admin filter that reveals hidden bundles. Off by default, so the list is
   the shop window's list until somebody asks for the rest. */
.filter-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  cursor: pointer;
  white-space: nowrap;
}

/* The dashboard's card carries the members price and, beside it, the guest
   price it beat. */
.bundle__prices { display: grid; gap: 2px; }
.bundle__guest-price {
  font-size: var(--text-xs);
  color: var(--muted);
  text-decoration: line-through;
  font-variant-numeric: tabular-nums;
}

.checkout-member-line {
  margin: 0 0 var(--space-4);
  padding: var(--space-3);
  font-size: var(--text-sm);
  line-height: 1.45;
  color: var(--text);
  background: var(--accent-soft);
  border-radius: var(--radius);
}
.checkout-member-line a { font-weight: var(--weight-semibold); }

/* The inline track form: two fields and a button, stacking on a phone. */
.track-form {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
  align-items: end;
  gap: var(--space-3);
}
.track-form .field { margin-bottom: 0; }

.track-result {
  display: grid;
  gap: var(--space-3);
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}

@media (max-width: 719px) {
  .track-form { grid-template-columns: minmax(0, 1fr); }
}

/* ======================================================== theme toggle */
/* One sun/moon control, in the rail's brand row and in the mobile bar on the
   customer and admin pages, and in the shop's header. Both headers exist
   because the rail is off screen on a phone; shared.js mounts a button into
   every host and keeps them showing the same state. */

.theme-host { display: flex; align-items: center; }
.sidebar__brand .theme-host { margin-left: auto; }
.mobilebar .theme-host { margin-left: auto; }
.sitebar__right .theme-host { flex: none; }

.theme-toggle {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  padding: 0;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  transition: background var(--dur-fast), color var(--dur-fast), border-color var(--dur-fast);
}
.theme-toggle:hover {
  color: var(--accent-ink);
  background: var(--surface-3);
  border-color: var(--border-strong);
}
.theme-toggle:active { transform: translateY(1px); }
.theme-toggle svg { width: 19px; height: 19px; }

/* Light shows the moon (the switch's destination); dark shows the sun. */
.theme-toggle__icon--sun { display: none; }
.theme-toggle__icon--moon { display: block; }
:root[data-theme="dark"] .theme-toggle__icon--sun { display: block; }
:root[data-theme="dark"] .theme-toggle__icon--moon { display: none; }

/* No-JavaScript fallback. theme.js normally writes data-theme before paint, so
   this only covers a page whose script failed to load. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle__icon--sun { display: block; }
  :root:not([data-theme="light"]) .theme-toggle__icon--moon { display: none; }
}

/* ==================================================== customer overview */

/* A greeting and its action share a line when there is room. */
.view__head--row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.view__sub { margin: 4px 0 0; font-size: var(--text-base); color: var(--muted); }

/* Three figures above the fold: total, delivered, still on the way. These are
   cards rather than the recessed wells used lower down, because they are the
   first thing the page says after the balance. */
.stats {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(142px, 1fr));
}

.stat-card {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
  padding: var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}

.stat-card__icon {
  display: grid;
  place-items: center;
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: 12px;
  background: var(--accent-soft);
  color: var(--accent-ink);
}
.stat-card__icon svg { width: 20px; height: 20px; }
.stat-card--delivered .stat-card__icon { background: var(--ok-soft); color: var(--ok); }
.stat-card--pending .stat-card__icon { background: var(--warn-soft); color: var(--warn); }
.stat-card--failed .stat-card__icon { background: var(--danger-soft); color: var(--danger); }

.stat-card__body { min-width: 0; }
.stat-card__label { margin: 0; font-size: var(--text-xs); color: var(--muted); }
.stat-card__value {
  margin: 2px 0 0;
  font-size: var(--text-2xl);
  font-weight: var(--weight-bold);
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}

/* The five newest orders, as rows in one panel. A list rather than a table:
   four facts and a badge do not need column headers on a phone. */
.recent-orders { display: grid; }

.recent-order {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) 0;
}
.recent-order:first-child { padding-top: 0; }
.recent-order:last-child { padding-bottom: 0; }
.recent-order + .recent-order { border-top: 1px solid var(--border); }

.recent-order__main { display: grid; gap: 2px; min-width: 0; }
.recent-order__title {
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.recent-order__sub {
  font-size: var(--text-xs);
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.recent-order__side { display: flex; align-items: center; gap: var(--space-2); flex: none; }
.recent-order__amount {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.recent-order .badge { margin-left: 0; }

@media (max-width: 479px) {
  .recent-order__side { flex-direction: column; align-items: flex-end; gap: 4px; }
}

/* Packages grouped by network on the overview. The group heading carries the
   operator colour on its dot and the name in words, so the label is never
   colour alone. */
.bundle-groups { display: grid; gap: var(--space-5); }
.bundle-group { display: grid; gap: var(--space-3); }
.bundle-group + .bundle-group {
  padding-top: var(--space-5);
  border-top: 1px solid var(--border);
}

.bundle-group__head { display: flex; align-items: center; gap: var(--space-2); }
.bundle-group__title { margin: 0; font-size: var(--text-md); font-weight: var(--weight-bold); }
.bundle-group__count { margin-left: auto; font-size: var(--text-xs); color: var(--muted); }

.bundle__network {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  align-self: flex-start;
  padding: 2px 8px;
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--muted);
  background: var(--surface-2);
  border-radius: var(--radius-pill);
}

/* The size and the price are what a package IS. Both are set large enough to
   be read from arm's length, and the price keeps the accent ink so it separates
   from the two grey lines around it. */
.bundle__size {
  font-size: clamp(19px, 1rem + .55vw, 22px);
  font-weight: var(--weight-bold);
  letter-spacing: -.02em;
  line-height: 1.2;
}
.bundle__price { font-size: clamp(18px, 1rem + .35vw, 20px); }
.bundle__cta { transition: background var(--dur-fast); }
.bundle:hover .bundle__cta { background: var(--accent-deep); }
.bundle:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.bundle:active { transform: translateY(0); }

/* A button doing work in the background says so twice: the label changes, and a
   small spinner appears beside it. Both are cheap, and together they stop a
   slow request from looking like a tap that never registered. */
.btn[aria-busy="true"] { cursor: progress; }
.btn[aria-busy="true"]::before {
  content: '';
  flex: none;
  width: 14px;
  height: 14px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: btn-spin .7s linear infinite;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }

.chip:disabled { opacity: .55; cursor: not-allowed; }

.iconbtn:hover { background: var(--surface-3); color: var(--accent-ink); }
.iconbtn:active { transform: translateY(1px); }

/* ----------------------------------------------------------------- layout */

@media (min-width: 960px) {
  .app {
    grid-template-columns: var(--rail) minmax(0, 1fr);
    /* Paints the rail for the full page height. The sidebar itself is only
       100vh (it has to be, to stick), so on a page taller than the viewport
       this stops the column showing a different colour below it. */
    background: linear-gradient(90deg, var(--surface) 0 var(--rail), var(--bg) var(--rail));
  }

  .sidebar {
    position: sticky;
    top: 0;
    height: 100vh;
    overflow-y: auto;
  }

  .mobilebar { display: none; }
  .content { padding: var(--space-7); }
}

@media (max-width: 959px) {
  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    width: var(--rail);
    z-index: var(--z-sidebar);
    transform: translateX(-100%);
    transition: transform var(--dur-base) ease;
    box-shadow: var(--shadow-lg);
    overflow-y: auto;
  }
  .sidebar.is-open { transform: none; }
}

/* One block rather than two, and it lives at the end of the file on purpose:
   `.sidebar`'s transition is declared in the layout section just above, and a
   reduced-motion rule of the same specificity only beats it by coming after it.
   Consolidating this anywhere earlier would quietly stop the drawer obeying it,
   which is the failure mode of a guard that looks present and does nothing. */
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }
  .skeleton { animation: none; }
  .sidebar { transition: none; }
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

@media (max-width: 420px) {
  .card { padding: 24px; }
  .content { padding: 16px; }
  .panel { padding: 16px; }
  /* The header strip's gutter tracks `.content`'s, or the brand sits 8px in
     from the panel below it for no reason anyone could name. */
  .sitebar__inner { padding-left: 16px; padding-right: 16px; }
}

/* ---------------------------------------------------- number verification */

/* Two actions of equal weight on one line, stacked on a phone. Deliberately
   layout-only: the colours are the existing button and token colours. */
.verify-actions {
  display: flex;
  gap: var(--space-2);
}

.verify-actions .btn {
  flex: 1 1 0;
  min-width: 0;
}

@media (max-width: 560px) {
  .verify-actions .btn {
    padding-left: 10px;
    padding-right: 10px;
    font-size: var(--text-base);
  }
}

@media (max-width: 360px) {
  .verify-actions { flex-direction: column; }
  .verify-actions .btn { width: 100%; }
}

.verify-numbers textarea {
  width: 100%;
  padding: 11px 13px;
  font: inherit;
  font-size: 15px;
  line-height: 1.5;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  resize: vertical;
}

.verify-numbers textarea::placeholder { color: var(--muted); opacity: .65; }

.verify-numbers textarea:focus {
  border-color: var(--accent-ink);
  box-shadow: 0 0 0 3px var(--accent-soft);
  outline: none;
}

.verify-numbers input[type="file"] {
  width: 100%;
  padding: 10px 12px;
  font: inherit;
  font-size: var(--text-sm);
  color: var(--text);
  background: var(--surface);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
}
