/* RebelText user console.
 *
 * One stylesheet, no build step, no framework — and deliberately the operator
 * console's stylesheet: the same tokens, the same chrome down the side, the
 * same slab buttons with their hard offset shadows, the same condensed
 * uppercase headings over monospace body text. A customer who has seen
 * rebeltexting.com should recognise this as the same product, and anyone who
 * has worked on internal/admin/static/admin.css should recognise this as the
 * same stylesheet.
 *
 * The token block below is the console's, copied rather than shared. They are
 * served by different binaries on different ports and neither can link to the
 * other's assets, so the choice is between duplicating ~110 lines of custom
 * properties or inventing a build step to avoid it. The duplication is the
 * cheaper mistake: if the palettes drift, two pages look slightly different,
 * which is visible. A build step that breaks takes both down. It is copied
 * verbatim, so a diff between the two files is the whole answer to "have they
 * drifted".
 *
 * What is not copied is the page structure. The console lays its tables and
 * stat rows straight onto the page ground; this one groups them into cards,
 * because a customer reads one account's pages top to bottom and the headings
 * are what divide them. So a table here sits inside a filled panel, and the
 * nested surface steps up — surface, then surface-2 — which is the rule the
 * console's own dialogs already follow.
 *
 * Dark is the default because it is the site's look. Light swaps the black
 * ground for the site's cream and keeps the red, and it applies when the OS
 * asks for it or the customer picks it from the top bar (portal.js sets
 * data-theme on <html>). Only tokens change between the two; nothing below the
 * token blocks names a colour that assumes one ground or the other.
 *
 * The CSP is default-src 'self', so the site's web fonts cannot be pulled from
 * Google. The stacks name them first (they are used when installed, or once the
 * files are vendored under /static) and fall back to the nearest system faces.
 * It also forbids an inline style attribute, so spacing that would be a one-off
 * style="" in the markup is a rule in here instead. */

:root {
  color-scheme: dark;

  --bg: #000000;
  --surface: #0e0e0e;
  --surface-2: #141414;
  --input: #1b1b1a;
  --border: rgb(255 255 255 / 8%);
  --border-strong: rgb(234 229 205 / 16%);
  --text: #eae5cd;
  --muted: #888880;
  --faint: #525252;

  --primary: #ed462d;
  --primary-hover: #d83c1f;
  --primary-dark: #da3921;
  --rose: #f0b0a6;

  --accent: var(--primary);
  --accent-text: #000000;
  /* Type on a slab filled with --text: primary buttons, the console tag. */
  --on-text: #000000;
  --topbar: rgb(0 0 0 / 92%);
  --sidebar-w: 232px;
  /* One height for everything in the top bar, so the balance, the theme
     control and the account menu line up edge to edge. */
  --topbar-control: 36px;
  --login-card: #000000;
  --login-shadow: rgb(0 0 0 / 25%);

  /* Status tints. Red is the brand, so "wrong" is carried by the rose text on a
     dark red wash rather than by the button red itself. */
  --danger: #ff6b55;
  --danger-bg: rgb(237 70 45 / 12%);
  --ok-bg: rgb(110 231 168 / 9%);
  --ok-text: #8fdcaa;
  --warn-bg: rgb(245 194 107 / 10%);
  --warn-text: #f0c674;
  --info-bg: rgb(234 229 205 / 7%);
  --info-text: #d8d3ba;

  --shadow: 6px 6px 0 0 #262626;
  --shadow-cream: 4px 4px 0 0 rgb(234 229 205 / 16%);
  --radius: 0;

  --font-heading: "Barlow Semi Condensed", "Avenir Next Condensed", "Roboto Condensed",
    "Arial Narrow", "Helvetica Neue", sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
}

/* The light palette, twice: once for an OS that prefers light and has not been
   overridden to dark, once for an explicit choice of light. CSS has no way to
   share one block between a media query and an attribute selector. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    color-scheme: light;

    --bg: #f1eee0;
    --surface: #faf8f0;
    --surface-2: #ebe7d4;
    --input: #ffffff;
    --border: rgb(14 14 14 / 9%);
    --border-strong: rgb(14 14 14 / 18%);
    --text: #111110;
    --muted: #62605a;
    --faint: #a29f93;
    --rose: #b8321c;
    --on-text: #eae5cd;

    --danger: #c2321b;
    --danger-bg: rgb(237 70 45 / 10%);
    --ok-bg: rgb(22 120 70 / 9%);
    --ok-text: #1d6e42;
    --warn-bg: rgb(180 110 0 / 10%);
    --warn-text: #8a5300;
    --info-bg: rgb(14 14 14 / 5%);
    --info-text: #3b3a35;

    --shadow: 6px 6px 0 0 #d6d1b8;
    --topbar: rgb(241 238 224 / 92%);
    --login-card: #faf8f0;
    --login-shadow: rgb(0 0 0 / 18%);
  }
}

:root[data-theme="light"] {
  color-scheme: light;

  --bg: #f1eee0;
  --surface: #faf8f0;
  --surface-2: #ebe7d4;
  --input: #ffffff;
  --border: rgb(14 14 14 / 9%);
  --border-strong: rgb(14 14 14 / 18%);
  --text: #111110;
  --muted: #62605a;
  --faint: #a29f93;
  --rose: #b8321c;
  --on-text: #eae5cd;

  --danger: #c2321b;
  --danger-bg: rgb(237 70 45 / 10%);
  --ok-bg: rgb(22 120 70 / 9%);
  --ok-text: #1d6e42;
  --warn-bg: rgb(180 110 0 / 10%);
  --warn-text: #8a5300;
  --info-bg: rgb(14 14 14 / 5%);
  --info-text: #3b3a35;

  --shadow: 6px 6px 0 0 #d6d1b8;
  --topbar: rgb(241 238 224 / 92%);
  --login-card: #faf8f0;
  --login-shadow: rgb(0 0 0 / 18%);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 13px/1.6 var(--font-mono);
  -webkit-font-smoothing: antialiased;
}

::selection { background: var(--primary); color: #000; }

code {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--muted);
}

a { color: var(--text); }

p a { color: var(--primary); text-decoration: none; }
p a:hover { color: var(--text); }

h1, h2, h3 {
  font-family: var(--font-heading);
  text-transform: uppercase;
  line-height: 1.05;
  color: var(--text);
}

h1 { font-size: 40px; font-weight: 800; letter-spacing: -0.005em; margin: 0 0 24px; }
h2 { font-size: 20px; font-weight: 700; letter-spacing: 0.02em; margin: 0 0 14px; }
h3 { font-size: 16px; font-weight: 700; letter-spacing: 0.02em; margin: 0 0 10px; }

.muted { color: var(--muted); }
.small { font-size: 11px; }

/* A page's opening sentence, sitting under its title rather than inside the
   first card: it says what the whole page is for, so it belongs to none of
   them. The negative margin pulls it up under the heading, whose own bottom
   margin is sized for a card following it. Measured, because prose set the
   full width of the console is prose nobody finishes. */
.subtitle { margin: -14px 0 28px; max-width: 760px; }
.danger-text { color: var(--danger); }

/* Long values — endpoint URLs, error text from somebody else's server — break
   rather than stretching their column past the viewport. */
.wrap { overflow-wrap: anywhere; }
/* A value that reads as one thing — a timestamp — kept on one line. */
.nowrap { white-space: nowrap; }

/* A message's text in a log table: wide enough to read a line of it, no wider
   than a column should be, and cut after three lines so one long message does
   not make its row a page tall. Line breaks the sender typed are kept. */
.msg-body { min-width: 14rem; max-width: 28rem; overflow-wrap: anywhere; }
.msg-body .clamp {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
  white-space: pre-line;
}
.msg-body .attach { display: block; }
/* The ID under a message's text; a sent message's opens its page. */
.msg-body .msg-id { display: block; width: fit-content; margin-top: 4px; }
.msg-body a.msg-id { text-decoration: underline; text-underline-offset: 3px; }
.msg-body a.msg-id:hover code { color: var(--primary); }

/* ── One message ─────────────────────────────────────────────────────────── */

/* The page title with its status beside it, and the way back to the log. */
.back { margin: 0 0 8px; }
.section-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.section-head h1 { margin-bottom: 0; }
.section-head + .subtitle { margin-top: 8px; overflow-wrap: anywhere; }

/* The text as sent: every line break kept, wrapped rather than scrolled, since
   it is prose and not JSON. */
pre.message-text { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 14px; }
.card h3 { margin-top: 20px; font-size: 12px; }
.media-list { margin: 0; padding-left: 20px; }
.media-list li + li { margin-top: 4px; }

/* A message's life, one ruled step at a time, the marker coloured by how the
   step went. */
.timeline { list-style: none; margin: 0 0 12px; padding: 0; }
.timeline .tl {
  position: relative;
  padding: 10px 0 10px 22px;
  border-bottom: 1px solid var(--border);
}
.timeline .tl:last-child { border-bottom: none; }
.timeline .tl::before {
  content: "";
  position: absolute;
  left: 2px;
  top: 16px;
  width: 8px;
  height: 8px;
  border: 1px solid var(--muted);
  background: var(--surface);
}
.timeline .tl-ok::before { border-color: var(--ok-text); background: var(--ok-text); }
.timeline .tl-bad::before { border-color: var(--danger); background: var(--danger); }
.timeline .tl-pending::before { border-style: dashed; }
.tl-head { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; }
.tl-at { white-space: nowrap; font-variant-numeric: tabular-nums; }

/* Screen-reader-only text. Used where a link's visible label is short enough to
   be ambiguous out of context — "View", repeated down a column of invoices. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── Shell ───────────────────────────────────────────────────────────────── */

/* The console's sidebar for the sections, and a bar along the top of the page
   for what belongs to the account as a whole: its balance, the theme, and who
   is signed in. A customer moving between the two consoles should not have to
   relearn where the navigation lives, and the identity block no longer
   competes with the section list for the same column.

   A grid rather than a fixed-position aside, so the main column is laid out
   next to the sidebar instead of underneath it with a margin standing in. The
   sidebar spans both rows; the top bar and the page share the second column.

   No JavaScript collapses it. The CSP forbids inline script and a sidebar that
   needs a toggle to be usable is a sidebar that breaks with it blocked, so the
   narrow layout is a media query over the same markup. */
.shell {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  grid-template-rows: auto 1fr;
  grid-template-areas:
    "side top"
    "side main";
  min-height: 100vh;
  /* Without this the leftover space under a short page is shared out between
     the rows, which at narrow widths pushes the content halfway down the
     screen. */
  align-content: start;
}

.shell > main { grid-area: main; }

.sidebar {
  grid-area: side;
  position: sticky;
  top: 0;
  align-self: start;
  z-index: 10;
  display: flex;
  flex-direction: column;
  gap: 28px;
  height: 100vh;
  padding: 24px 16px;
  background: var(--topbar);
  border-right: 1px solid var(--border);
  /* The red rule the top bar used to carry, stood on its end. */
  box-shadow: inset 3px 0 0 var(--primary);
  overflow-y: auto;
}

/* Keyboard users would otherwise tab the whole nav on every page. */
.skip {
  position: absolute;
  left: -9999px;
  z-index: 20;
  padding: 10px 16px;
  background: var(--text);
  color: var(--on-text);
  font-family: var(--font-heading);
  text-transform: uppercase;
}
.skip:focus { left: 8px; top: 8px; }

.brand, .login-brand {
  display: flex;
  align-items: center;
  gap: 4px;
  color: var(--text);
  text-decoration: none;
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: 20px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  white-space: nowrap;
}

.brand { padding: 0 8px; }
.brand img { width: 32px; height: 32px; display: block; }

/* "console", set as the site's small tag rather than as quiet type beside the
   wordmark: it is what tells the two consoles apart, and the operator one
   stamps "admin" the same way. */
.brand span, .login-brand span {
  padding: 2px 6px;
  margin-left: 2px;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.1em;
  color: var(--on-text);
  background: var(--text);
}

.sidebar nav {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.sidebar nav a {
  padding: 8px 12px;
  color: var(--muted);
  text-decoration: none;
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 15px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  /* "API keys" is the one item here that is two words, and wrapped it stands
     the whole row up two lines tall once the sidebar is laid along the top. */
  white-space: nowrap;
  /* The marker is on the leading edge, where a vertical list reads it. */
  border-left: 2px solid transparent;
}

.sidebar nav a:hover { color: var(--text); background: var(--surface-2); }
.sidebar nav a.active { color: var(--text); border-left-color: var(--primary); }

/* A group's name: small, quiet, and not a link, so it reads as the heading of
   the rows under it rather than as one more of them. */
.sidebar .nav-label {
  margin: 18px 12px 4px;
  color: var(--faint);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* The one item that leaves the portal. A size down, so it reads as the
   reference kept beside the developer pages rather than one of them. It never
   takes the active marker: nothing here is ever the page you are on. */
.sidebar nav a.external { font-size: 13px; }

/* ── Top bar ─────────────────────────────────────────────────────────────── */

/* Sticky, so the balance is in view however far down a long table somebody
   has scrolled — it is the number that decides whether the next send goes
   out. Everything sits at the far end: the start of the bar is the page's own
   heading, just below it, and nothing up here should compete with that. */
.topbar {
  grid-area: top;
  position: sticky;
  top: 0;
  z-index: 9;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
  min-height: 64px;
  padding: 12px 32px;
  background: var(--topbar);
  border-bottom: 1px solid var(--border);
  backdrop-filter: blur(8px);
}

/* The balance, set like the figures on the overview but on one line: a small
   label, the number, and the way to add more fastened to its end. */
.balance {
  display: flex;
  align-items: stretch;
  height: var(--topbar-control);
  border: 1px solid var(--border-strong);
  background: var(--surface);
}

.balance-label, .balance-value {
  display: flex;
  align-items: center;
  padding: 0 0 0 12px;
}

.balance-label {
  color: var(--muted);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.balance-value {
  padding-right: 12px;
  padding-left: 8px;
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 17px;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
}

.balance.low .balance-value { color: var(--danger); }

/* The primary slab, square, because it is the one action up here that spends
   money and it should look like the others that do. */
.balance-add {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  background: var(--text);
  color: var(--on-text);
  text-decoration: none;
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: 20px;
  line-height: 1;
}
.balance-add:hover, .balance-add.active { background: var(--primary); color: #000; }

/* Who is signed in, and the menu behind their name. */
.account-menu { position: relative; }

.account-menu summary {
  display: flex;
  align-items: center;
  gap: 8px;
  height: var(--topbar-control);
  max-width: 240px;
  padding: 0 12px;
  border: 1px solid var(--border-strong);
  color: var(--text);
  cursor: pointer;
  list-style: none;
}
.account-menu summary::-webkit-details-marker { display: none; }
.account-menu summary:hover,
.account-menu[open] summary { border-color: var(--text); }
.account-menu summary.active { border-bottom-color: var(--primary); }
.account-menu summary:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

.caret { color: var(--muted); font-size: 10px; }

.account-menu .menu {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  z-index: 30;
  display: flex;
  flex-direction: column;
  min-width: 240px;
  max-width: calc(100vw - 32px);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow);
}

.menu-who {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
}

.account-menu .menu > a {
  padding: 10px 14px;
  color: var(--text);
  text-decoration: none;
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 14px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border-left: 2px solid transparent;
}
.account-menu .menu > a:hover { background: var(--surface-2); }
.account-menu .menu > a[aria-current="page"] { border-left-color: var(--primary); }

.signout {
  margin: 0;
  padding: 12px 14px;
  border-top: 1px solid var(--border);
}
.signout button { width: 100%; padding: 7px 12px; }

.profile-name {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 15px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.who {
  color: var(--muted);
  font-size: 12px;
  overflow-wrap: anywhere;
}

/* Narrow: the brand and the top bar share the first row, and the sections run
   along the second, scrolling sideways. The sidebar's box steps aside
   (display: contents) so its two children can each take a cell of the shell's
   grid. No toggle, because the CSP forbids inline script and a nav that needs
   one is a nav that breaks without it. */
@media (max-width: 860px) {
  .shell {
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-rows: auto auto 1fr;
    grid-template-areas:
      "brand top"
      "nav nav"
      "main main";
  }

  .sidebar { display: contents; }

  .brand {
    grid-area: brand;
    padding: 12px 0 12px 16px;
    background: var(--topbar);
    border-bottom: 1px solid var(--border);
    box-shadow: inset 0 3px 0 var(--primary);
  }
  /* The wordmark gives way to the balance on a phone: the mark alone still
     says whose console it is. */
  .brand { font-size: 0; gap: 0; }
  .brand span { display: none; }

  .topbar {
    position: static;
    gap: 8px;
    min-height: 0;
    padding: 12px 16px;
    box-shadow: inset 0 3px 0 var(--primary);
    backdrop-filter: none;
  }

  .sidebar nav {
    grid-area: nav;
    /* The containing block for the screen-reader text inside the API
       reference link, which is absolutely positioned. With the sidebar's box
       stepped aside nothing else above it is positioned, so without this that
       text is placed against the page instead — at the far end of this row's
       overflow — and drags the whole page sideways with it. */
    position: relative;
    flex-direction: row;
    align-items: center;
    padding: 4px 16px;
    overflow-x: auto;
    background: var(--topbar);
    border-bottom: 1px solid var(--border);
  }

  .sidebar nav a { border-left: 0; border-bottom: 2px solid transparent; }
  .sidebar nav a.active { border-bottom-color: var(--primary); }
  /* Laid along the top the groups run together; a divider stands in for the
     name, which there is no room to print. */
  .sidebar .nav-label {
    flex: none;
    width: 1px;
    height: 18px;
    margin: 0 6px;
    padding: 0;
    overflow: hidden;
    background: var(--border-strong);
    color: transparent;
    font-size: 0;
  }

  .balance-label { display: none; }
  .balance-value { padding-left: 10px; font-size: 15px; }
  .account-menu summary { max-width: 12ch; padding: 0 10px; }
  .account-menu summary .profile-name { font-size: 13px; }
}

@media (max-width: 420px) {
  /* The name is in the menu it opens; the caret alone is enough to open it. */
  .account-menu summary .profile-name { display: none; }
}

/* ── Layout ──────────────────────────────────────────────────────────────── */

main {
  /* width rather than the auto a block would normally take. As a grid item an
     auto width resolves against the content, so one table wider than the phone
     stretches the whole column and drags the page sideways with it. Resolving
     against the track instead keeps main the width of the screen and leaves
     the overflow to the table's own scroller. */
  width: 100%;
  min-width: 0;
  max-width: 1160px;
  margin: 0 auto;
  padding: 40px 32px 80px;
}

/* The signed-out shell: the site's red hero with its faint diagonal grid, and
   the sign-in card sitting on it like the site's black call-to-action. The
   operator console's sign-in page is the same page. */
body:has(main.centered) {
  background:
    repeating-linear-gradient(45deg, rgb(0 0 0 / 5%) 0 1px, transparent 1px 64px),
    repeating-linear-gradient(-45deg, rgb(0 0 0 / 5%) 0 1px, transparent 1px 64px),
    var(--primary);
}

/* min-height rather than height, and centring only from a viewport tall enough
   to spare it: on a short screen a centred card is one whose top is scrolled
   off and unreachable. */
main.centered {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
  max-width: 540px;
  padding: 24px;
}

@media (min-height: 560px) {
  main.centered { justify-content: center; padding-bottom: 72px; }
}

/* The wordmark above the card. Signed out there is no sidebar, so without this
   the page never says whose console it is. It is the console's lockup — the
   mark, the name, and the tag saying which of the two this is — sitting on the
   card's left edge. */
.login-brand {
  margin: 0 0 20px;
  font-size: 26px;
}

.login-brand img { width: 40px; height: 40px; display: block; }

/* ── Cards ───────────────────────────────────────────────────────────────── */

/* The console's panel: a filled slab with a hard offset shadow, square like
   everything else here. A page is a stack of them, each under its own
   heading. */
.card {
  padding: 24px 26px;
  margin-bottom: 24px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow);
}

.card > h2:first-child, .card > h1:first-child { margin-top: 0; }

/* Spacing that the markup would otherwise carry as a style attribute, which the
   CSP drops on the floor. A paragraph or a run of controls after the facts of a
   card needs air; the last thing in a card does not need a trailing margin. */
.card > p:last-child, .card > .facts + p:last-child { margin-bottom: 0; }
.card .facts + p, .card > .notice, .card > .banner { margin-top: 16px; }
.card > .notice:first-child, .card > .banner:first-child { margin-top: 0; }
.card > .row-actions { margin-top: 20px; }

/* Signed out there is one card and no chrome around it, so it is the site's
   black call-to-action standing on the red ground rather than a panel on the
   page: no border, more padding, and a shadow thrown far enough to read as
   depth against the hero. */
main.centered .card {
  padding: 36px;
  margin-bottom: 0;
  background: var(--login-card);
  border: 0;
  box-shadow: 12px 12px 0 0 var(--login-shadow);
}

main.centered h1 { font-size: 44px; margin-bottom: 8px; }
main.centered .card > p:first-of-type { margin-top: 0; }

/* The one action on a signed-out page, given the width of the card and a taller
   box to press — the console's sign-in button. */
main.centered form button {
  width: 100%;
  margin-top: 10px;
  padding: 13px;
  font-size: 13px;
}

/* ── Messages ────────────────────────────────────────────────────────────── */

/* Tinted, with the loud left edge the console uses: at a glance the colour says
   which of the three this is, and the weight of the edge says it is about the
   page rather than part of it. */
.error, .notice, .banner {
  padding: 12px 16px;
  margin: 0 0 20px;
  border: 1px solid currentcolor;
  border-left-width: 4px;
  font-size: 12px;
}

.error { background: var(--danger-bg); color: var(--danger); }
.notice { background: var(--ok-bg); color: var(--ok-text); }
.banner { background: var(--warn-bg); color: var(--warn-text); }
.banner strong, .notice strong, .error strong { display: block; margin-bottom: 4px; }

/* An operator signed in as the customer. The words on the left, the way out on
   the right, and the way out drops under the words when there is no room. */
.banner.impersonation {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 16px;
}
.banner.impersonation p { margin: 0; flex: 1 1 32ch; }
.impersonation-end { margin: 0; }

/* Standing on the red ground rather than in the card — the layout puts the
   page-level error and notice above it — a tinted alert disappears, so out here
   it is solid. */
main.centered > .error { background: var(--login-card); color: var(--rose); border-color: var(--login-card); }
main.centered > .notice { background: var(--login-card); color: var(--text); border-color: var(--login-card); }

/* Inside the card the card is already the alert's container, so the alert keeps
   only its colour. Drawing the box in the card's own background is the same
   thing as drawing nothing, except that it still spends its padding and left
   border — indenting the text out of line with the heading above it and the
   field labels below. */
main.centered .card > .error,
main.centered .card > .notice,
main.centered .card > .banner {
  padding: 0;
  border: 0;
  background: none;
}

main.centered .card > .error { color: var(--rose); }
main.centered .card > .notice { color: var(--text); }
main.centered .card > .banner { color: var(--warn-text); }

/* A credential shown exactly once. It is loud on purpose: the page that renders
   it is the only place it will ever exist, and somebody who scrolls past it has
   lost it. The red edge is the same marker the console puts on a message body
   it wants read. */
.card.secret-once {
  border-left: 4px solid var(--primary);
}

code.secret {
  display: block;
  margin-top: 12px;
  padding: 14px 16px;
  background: var(--input);
  border: 1px solid var(--border-strong);
  color: var(--text);
  font-size: 13px;
  /* A key and a signing secret are long, unbroken and case-sensitive. Wrapping
     anywhere beats a horizontal scrollbar over the one string on the page that
     has to be copied whole. */
  word-break: break-all;
  user-select: all;
}

/* ── Forms ───────────────────────────────────────────────────────────────── */

/* Field names, in the console's key: small, tracked out, uppercase, muted. The
   field itself carries the gap below, so the label only owns the gap above. */
label {
  display: block;
  margin-bottom: 6px;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}

input, select, button {
  font: inherit;
  padding: 9px 12px;
  border: 1px solid var(--border-strong);
  border-radius: 0;
  background: var(--input);
  color: var(--text);
}

input[type="email"], input[type="password"], input[type="text"],
input[type="number"], select {
  width: 100%;
  margin-bottom: 16px;
}

/* A password box with the show/hide eye portal.js adds inside its right edge.
   The wrapper takes the box's bottom margin so the button, pinned top to
   bottom, spans the input alone. */
.pw-field {
  position: relative;
  display: block;
  margin-bottom: 16px;
}

.pw-field > input { margin-bottom: 0; padding-right: 42px; }

/* Edge draws its own reveal eye; two would be one too many. */
.pw-field > input::-ms-reveal { display: none; }

.pw-field > .pw-toggle {
  position: absolute;
  /* A card's button rule aligns it to flex-start, which an absolute box
     obeys too and so stops stretching between top and bottom. */
  align-self: stretch;
  top: 1px;
  right: 1px;
  bottom: 1px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  padding: 0;
  margin: 0;
  border: 0;
  background: transparent;
  color: var(--muted);
  box-shadow: none;
  transform: none;
}

.pw-field > .pw-toggle:hover { color: var(--text); }
.pw-field > .pw-toggle:focus-visible { outline-offset: -2px; }

.fields .pw-field { margin-bottom: 0; }

/* Short fields and the button that submits them on one line, the way the
   operator console lays out its own profile: three password boxes are three
   dozen characters between them, and stacking them full width makes a form
   that is mostly empty box.

   Each field is elastic down to a readable minimum and wraps onto its own line
   below that, rather than shrinking into something nothing fits in. Bottoms
   align, so the button sits on the same line as the inputs even though its
   smaller type makes it the shorter box. */
.fields {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 12px;
  margin-top: 16px;
}

/* A column rather than a wrapping <label>: an input inherits its font, and
   inside the label it would inherit the caption's 10px. */
.fields .field {
  display: flex;
  flex-direction: column;
  flex: 1 1 190px;
  min-width: 0;
}

.fields .field label { margin-bottom: 6px; }
.fields .field input { margin-bottom: 0; }
/* Grows with the fields, so a line is always filled — including the one the
   button lands on alone once the row wraps. */
.fields button { flex: 1 0 auto; }

/* A native select sizes itself, ignoring the padding and line-height that set
   an input's height, so the two never line up. Without native appearance it
   takes the same box as an input. The arrow is two gradient triangles: the
   CSP allows no data: image to draw it with. */
select {
  appearance: none;
  padding-right: 32px;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: calc(100% - 17px) 55%, calc(100% - 12px) 55%;
  background-size: 5px 5px;
  background-repeat: no-repeat;
  cursor: pointer;
}
select:hover { background-image:
    linear-gradient(45deg, transparent 50%, var(--text) 50%),
    linear-gradient(135deg, var(--text) 50%, transparent 50%); }

input::placeholder { color: var(--faint); }

input:hover, select:hover { border-color: rgb(234 229 205 / 30%); }

input[type="checkbox"], input[type="radio"] { accent-color: var(--primary); }

input:focus-visible, select:focus-visible {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 1px var(--primary);
}

/* A select marked data-searchable, as portal.js turns it: a text box dressed as
   the select it replaces — same arrow — over a list that narrows as you type.
   The list floats over whatever is below rather than pushing it down. */
.combo {
  position: relative;
  display: block;
  min-width: 0;
}

.combo > .combo-input {
  width: 100%;
  margin: 0;
  padding-right: 32px;
  text-overflow: ellipsis;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: calc(100% - 17px) 55%, calc(100% - 12px) 55%;
  background-size: 5px 5px;
  background-repeat: no-repeat;
}

.combo-list {
  position: absolute;
  z-index: 20;
  top: calc(100% + 4px);
  left: 0;
  min-width: 100%;
  max-width: min(420px, calc(100vw - 32px));
  max-height: 280px;
  overflow-y: auto;
  margin: 0;
  padding: 4px 0;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow);
}

.combo-list > li {
  padding: 7px 12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
}

.combo-list > li[hidden] { display: none; }
.combo-list > li[aria-selected="true"] { color: var(--primary); }
.combo-list > li.active { background: var(--info-bg); color: var(--text); }
.combo-list > li.combo-empty { color: var(--muted); cursor: default; }

button:focus-visible, a:focus-visible, .plans:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

/* A checkbox and its words on one line, which the block-level label rule above
   would otherwise stack. The label is a sentence here rather than a field name,
   so it drops the uppercase treatment with it. */
label.check {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 8px;
  font-size: 12px;
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  color: var(--text);
}
label.check input { margin: 0; flex: none; }

fieldset {
  border: 1px solid var(--border-strong);
  padding: 14px 16px;
  margin: 0 0 16px;
}

legend {
  padding: 0 6px;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ── Buttons ─────────────────────────────────────────────────────────────── */

/* Three weights, the console's, and each says what it is for.
 *
 * The plain button is the quiet one: an outline, no fill, for the action that
 * is a way out or a refinement — Cancel, Close, Filter, Send test. The console
 * dresses sign-out and its theme control the same way.
 *
 * .primary is the site's call-to-action: a solid slab with a hard offset shadow
 * that the button presses into on hover. It is cream rather than red so the
 * brand red stays free to mean "this one is destructive", which is .danger. */
button {
  background: transparent;
  color: var(--muted);
  border-color: var(--border-strong);
  font-weight: 500;
  font-size: 11px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  cursor: pointer;
}

button:hover { color: var(--text); border-color: var(--text); }

button.primary {
  background: var(--text);
  color: var(--on-text);
  border-color: transparent;
  font-weight: 700;
  font-size: 12px;
  box-shadow: 4px 4px 0 0 var(--primary);
  transition: transform 0.12s ease, box-shadow 0.12s ease, background-color 0.12s ease;
}

button.primary:hover {
  color: var(--on-text);
  border-color: transparent;
  transform: translate(2px, 2px);
  box-shadow: 2px 2px 0 0 var(--primary);
}
button.primary:active { transform: translate(4px, 4px); box-shadow: none; }

button.danger {
  background: var(--primary);
  color: #000;
  border-color: transparent;
  font-weight: 700;
  font-size: 12px;
  box-shadow: 4px 4px 0 0 var(--text);
  transition: transform 0.12s ease, box-shadow 0.12s ease, background-color 0.12s ease;
}

button.danger:hover {
  background: var(--primary-hover);
  color: #000;
  border-color: transparent;
  transform: translate(2px, 2px);
  box-shadow: 2px 2px 0 0 var(--text);
}
button.danger:active { transform: translate(4px, 4px); box-shadow: none; }

/* A button that sits in a table row or a dialog's head rather than at the foot
   of a form. Same button, smaller: a column of full-size ones shouts down the
   data beside it. */
button.small { padding: 5px 10px; font-size: 11px; }
button.small.primary, button.small.danger { box-shadow: 3px 3px 0 0 var(--primary); }
button.small.danger { box-shadow: 3px 3px 0 0 var(--text); }
button.small.primary:hover { box-shadow: 1px 1px 0 0 var(--primary); }
button.small.danger:hover { box-shadow: 1px 1px 0 0 var(--text); }

/* A button mid-submit. portal.js swaps its label for the busy one and holds its
   width, so the row does not jump; here it is left looking pressed — the slab
   down against its shadow — rather than faded, which reads as "not allowed"
   instead of "working on it". */
button[aria-busy="true"] {
  cursor: progress;
  pointer-events: none;
}

button.primary[aria-busy="true"] { transform: translate(2px, 2px); box-shadow: 2px 2px 0 0 var(--primary); }
button.danger[aria-busy="true"] { transform: translate(2px, 2px); box-shadow: 2px 2px 0 0 var(--text); }
.signout button[aria-busy="true"] { color: var(--muted); }

/* The theme control. Quiet, because it is a preference rather than an action —
   it sits beside the balance and must not compete with it. A square holding
   the glyph alone, the same height as its neighbours in the top bar. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: var(--topbar-control);
  height: var(--topbar-control);
  padding: 0;
  font-size: 16px;
  line-height: 1;
}
.theme-toggle[hidden] { display: none; }

/* A link that does what a quiet button would — go to another page — dressed as
   one, so an action does not read as a sentence. Navigation stays a link: it
   works without script, opens in a new tab, and says where it goes. */
a.button {
  display: inline-block;
  padding: 9px 12px;
  border: 1px solid var(--border-strong);
  color: var(--muted);
  font-weight: 500;
  font-size: 11px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
}
a.button:hover { color: var(--text); border-color: var(--text); }

/* Dressed as the quiet button it sits beside, border and all: it is the other
   way to act on the filter row, and bare text there read as a caption. */
.clear {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 9px 12px;
  border: 1px solid var(--border-strong);
  color: var(--muted);
  text-decoration: none;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

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

/* ── Figures ─────────────────────────────────────────────────────────────── */

/* The console's stat row: tiles sharing their borders like the site's feature
   grid rather than floating as separate cards, on the page ground rather than
   inside a panel. */
.figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 0;
  margin-bottom: 24px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow);
}

.figure {
  padding: 20px 22px;
  border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  margin: 0 -1px -1px 0;
}

.figure:first-child { box-shadow: inset 3px 0 0 var(--primary); }

.figure .label {
  margin-bottom: 6px;
  color: var(--muted);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.figure .value {
  font-family: var(--font-heading);
  font-size: 34px;
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

.figure .value.low { color: var(--danger); }

/* The unit beside a figure — "/ $1", "credits". Small and muted so the number
   stays the thing being read, rather than competing with its own label. */
.figure .value .unit {
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 400;
  color: var(--muted);
}

/* An offer under a figure — "Buy credits" under the balance. */
.figure p { margin: 8px 0 0; }

/* ── Facts ───────────────────────────────────────────────────────────────── */

/* Label and value in one row, ruled off from the next. Read down the labels or
   down the values; either way the pair is on one line, which a two-column grid
   only manages until one value runs long. */
dl.facts { margin: 0; }
dl.facts div {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
}
dl.facts div:last-child { border-bottom: none; }
dl.facts dt { color: var(--muted); }
dl.facts dd { margin: 0; text-align: right; overflow-wrap: anywhere; }

/* ── Tables ──────────────────────────────────────────────────────────────── */

/* Inside a card, so the fill is the card's and the header row is the step up
   from it — the same surface-then-surface-2 nesting the console's dialogs use.
   A table on the page ground would want the fill itself. */
table {
  width: 100%;
  border-collapse: collapse;
  border: 1px solid var(--border-strong);
}

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

th {
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  background: var(--surface-2);
  border-bottom-color: var(--border-strong);
}

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

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

td a { color: var(--text); text-decoration: none; }
td a:hover { color: var(--primary); }

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

/* A table whose cells hold URLs and payload metadata rather than short words.
 *
 * Without the floor, a narrow viewport is resolved by squeezing the columns
 * instead of scrolling: .wrap then breaks a URL at every character and the
 * column ends up one glyph wide, which is unreadable in a way a sideways
 * scrollbar is not. The width is chosen to be just past a phone, so it only
 * ever engages where the alternative is that collapse. */
.table-wrap table.wide { min-width: 700px; }
.details table.wide { min-width: 520px; }

/* The attempt the row was opened from, among its siblings. */
tr.current td { background: var(--info-bg); }

/* ── Pills ───────────────────────────────────────────────────────────────── */

/* The site's small tag: square, mono, uppercase, tracked out, outlined in
   whatever colour its state carries. */
.pill {
  display: inline-block;
  padding: 2px 8px;
  border: 1px solid currentcolor;
  font-size: 10px;
  font-weight: 500;
  line-height: 1.6;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--muted);
}

.pill.ok { background: var(--ok-bg); color: var(--ok-text); }
.pill.warn { background: var(--warn-bg); color: var(--warn-text); }
.pill.bad { background: var(--danger-bg); color: var(--danger); }

/* ── Rows, filters and pager ─────────────────────────────────────────────── */

.row-actions {
  display: flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
}

td .row-actions { gap: 8px; justify-content: flex-end; }
td .row-actions form { margin: 0; }
/* The cell is sized by its buttons rather than by the longest word in them, so
   a narrow column does not hyphenate "Send test" down the middle. */
td .row-actions button { white-space: nowrap; }

/* A flex grid: every control grows, so each line the row wraps onto is filled
   edge to edge and there is never a strip of empty space after the last one.
   Stretched, so the button and the Clear link are the same height as the
   fields beside them.

   Each control's width comes from its basis, not its content — min-width: 0
   is what lets it — or the endpoint picker sizes itself to its longest URL
   and takes the whole row with it. The option text is truncated by the
   browser rather than by us, so the full URL is still there when the menu is
   open. The visually hidden labels are absolutely positioned and the hidden
   inputs not displayed, so neither takes a cell — and the labels are left out
   of these rules, whose width would otherwise undo the 1px that hides them. */
.filters {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 8px;
  margin-bottom: 20px;
}
.filters > :not(.visually-hidden) { flex: 1 1 180px; min-width: 0; margin-bottom: 0; }
.filters > button, .filters > .clear { flex: 1 0 90px; }
.filters select { width: auto; margin-bottom: 0; }
.filters label.check {
  align-items: center;
  margin-bottom: 0;
  padding: 9px 12px;
  border: 1px solid var(--border-strong);
}

.pager {
  display: flex;
  align-items: center;
  gap: 24px;
  margin-top: 20px;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.pager a { color: var(--text); text-decoration: none; border-bottom: 1px solid var(--primary); }
.pager a:hover { color: var(--primary); }

/* The sent / received switch above the message log: two links, since the CSP
   forbids the script a client-side tab would need. */
.view-tabs {
  display: flex;
  gap: 24px;
  margin-bottom: 20px;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.view-tabs a { color: var(--muted); text-decoration: none; padding-bottom: 4px; border-bottom: 1px solid transparent; }
.view-tabs a:hover { color: var(--text); }
.view-tabs a.active { color: var(--text); border-bottom-color: var(--primary); }

/* The analytics page's filters: labelled fields rather than the log's
   placeholder-only row, because a date field has no placeholder to name it. */
.analytics-filters { margin: 0; }
.analytics-filters .field { flex-basis: 150px; }
/* Room for a key's name and prefix, which the picker otherwise clips. */
.analytics-filters .field.wide { flex-basis: 240px; }
.analytics-filters .field select { width: 100%; margin-bottom: 0; }
.analytics-filters .clear { align-self: center; margin-top: 16px; }

/* One tile per message status on the analytics page: the figure row's
   shared-border grid, wider, each tile ruled across the top in its status's
   colour so the row reads as delivered-to-failed at a glance. */
/* The five totals above them, narrow enough to share one row on a laptop. */
.figures.totals { grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); }

.status-tiles { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.status-tiles .figure,
.status-tiles .figure:first-child { box-shadow: inset 0 3px 0 var(--border-strong); }
.status-tiles .figure.ok { box-shadow: inset 0 3px 0 var(--ok-text); }
.status-tiles .figure.warn { box-shadow: inset 0 3px 0 var(--warn-text); }
.status-tiles .figure.bad { box-shadow: inset 0 3px 0 var(--danger); }

.status-tiles .tile-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 14px;
}
.status-tiles dl.facts { margin-top: 12px; font-size: 12px; }
.status-tiles dl.facts div { padding: 6px 0; }

/* The status's share of all messages. A progress element because the CSP
   refuses the style attribute a sized div would need. */
.status-tiles progress {
  display: block;
  width: 100%;
  height: 4px;
  margin-top: 14px;
  border: none;
  appearance: none;
  background: var(--border);
  color: var(--muted);
}
.status-tiles progress::-webkit-progress-bar { background: var(--border); }
.status-tiles progress::-webkit-progress-value { background: var(--muted); }
.status-tiles progress::-moz-progress-bar { background: var(--muted); }
.status-tiles .ok progress::-webkit-progress-value { background: var(--ok-text); }
.status-tiles .ok progress::-moz-progress-bar { background: var(--ok-text); }
.status-tiles .warn progress::-webkit-progress-value { background: var(--warn-text); }
.status-tiles .warn progress::-moz-progress-bar { background: var(--warn-text); }
.status-tiles .bad progress::-webkit-progress-value { background: var(--danger); }
.status-tiles .bad progress::-moz-progress-bar { background: var(--danger); }

/* ── Plans ───────────────────────────────────────────────────────────────── */

/* One row, scrolled sideways, rather than a grid that wraps.
 *
 * A wrapping grid put the cheapest three plans on one line and the most
 * expensive on its own below, which reads as a separate thing rather than the
 * top of a ladder. A single row keeps the price order intact and stays intact
 * as plans are added — the catalogue is operator-editable, so "how many fit"
 * is not something this stylesheet gets to assume.
 *
 * The cards do not shrink. Squeezing four plans into a phone's width is how
 * you get four unreadable columns; scrolling past them is how every pricing
 * page on a phone already behaves. */
.plans {
  display: flex;
  gap: 20px;
  margin: 24px 0;
  overflow-x: auto;
  /* Snap to the start of a card so a flick never leaves one half off-screen. */
  scroll-snap-type: x proximity;
  /* Room for the focus ring and for each card's offset shadow, which would
     otherwise be clipped by the scroll container. */
  padding: 4px 4px 12px;
  margin-inline: -4px;
}

/* The scrollbar is the affordance. Hiding it is the common instinct and the
   wrong one: a horizontal scroller with no visible track is a row of cards that
   most people never discover the rest of. Styled down, not away. */
.plans::-webkit-scrollbar { height: 8px; }
.plans::-webkit-scrollbar-track { background: var(--surface-2); }
.plans::-webkit-scrollbar-thumb { background: var(--border-strong); }
.plans::-webkit-scrollbar-thumb:hover { background: var(--muted); }
.plans { scrollbar-width: thin; scrollbar-color: var(--border-strong) var(--surface-2); }

.plan {
  background: var(--surface);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow);
  padding: 22px;
  display: flex;
  flex-direction: column;
  /* Fixed width, never shrinking: this is what makes the row scroll instead of
     compressing. 260px fits the longest line each card carries — the overage
     sentence — on two lines rather than four. */
  flex: 0 0 260px;
  scroll-snap-align: start;
}

/* The plan being paid for, marked with the red edge the console puts on the
   thing you are looking at. */
.plan.current { box-shadow: inset 3px 0 0 var(--primary), var(--shadow); }

.plan h2 { margin-bottom: 4px; }

.plan .amount {
  font-family: var(--font-heading);
  font-size: 32px;
  font-weight: 800;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  margin: 8px 0 2px;
}
.plan .per { color: var(--muted); font-size: 11px; margin-bottom: 16px; }

.plan ul {
  list-style: none;
  padding: 0;
  margin: 0 0 20px;
  font-size: 12px;
}
.plan li { padding: 5px 0; border-bottom: 1px solid var(--border); }
.plan li:last-child { border-bottom: none; }

.plan form { margin-top: auto; }
.plan form button, .plan > button { width: 100%; }

/* What pressing the button above will actually do. It sits under the button
   rather than above it because it only matters once somebody is looking at
   that specific card's action. */
.plan .change-note { margin: 12px 0 0; line-height: 1.45; }

/* align-self, because the card is a flex column and a stretched tag is a red
   band across the top of it rather than a label on the plan. */
.plan .badge {
  align-self: flex-start;
  padding: 2px 7px;
  margin-bottom: 8px;
  background: var(--primary);
  color: #000;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* ── Buying credits ──────────────────────────────────────────────────────── */

/* A row of one-press amounts, as a flex grid: each amount grows, so however
   many fit on a line, every line — the last one included — is filled edge to
   edge, where a grid would leave empty cells after an uneven count. The basis
   is what sets how many share a line: six across a wide card, three and three
   on a narrow one. */
.amounts {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin: 20px 0 24px;
}
.amounts .amount { flex: 1 1 140px; min-width: 0; }
.amounts .amount { display: flex; flex-direction: column; gap: 8px; }
.amounts button { width: 100%; padding: 12px; font-size: 14px; }

/* What the amount above buys. Under the button rather than on it: the dollars
   are the choice being made, and the credits are the consequence — the same
   arrangement the plan cards use for their price and their allowance. */
.amount-credits {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--muted);
  text-align: center;
}

/* Which card to charge. A fieldset rather than a styled div, so a screen
   reader reads the legend before either option and the pair is announced as
   one question. */
.card-choice { margin: 0 0 4px; }

.card-choice label {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin: 0;
  padding: 8px 0;
  /* The label's own casing, not the uppercase every other label here takes:
     this one contains a card's name and a sentence, not a field title. */
  font-size: 13px;
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  color: var(--text);
  cursor: pointer;
}
.card-choice input[type="radio"] { margin: 3px 0 0; flex: none; }
.card-choice .choice { display: flex; flex-direction: column; gap: 2px; }
.card-choice .choice-title { display: flex; align-items: center; gap: 8px; }

/* The field for anything the row above does not offer. The margin its input
   carries by default would push the button out of line with it, so it is
   dropped here and the gap comes from the row instead. */
.custom-amount { border-top: 1px solid var(--border); padding-top: 20px; }
.custom-amount .row-actions { align-items: stretch; gap: 8px; }
.custom-amount input { flex: 1 1 200px; min-width: 0; margin-bottom: 0; }
.custom-amount button { flex: 1 0 auto; }
.custom-amount > p:last-child { margin-bottom: 0; }

/* ── Dialogs ─────────────────────────────────────────────────────────────── */

/* Native <dialog>, opened by the command attributes rather than by script: the
   CSP forbids inline script, and a confirmation that needs JavaScript to appear
   is one that silently stops confirming the day the script is blocked. */
.confirm {
  width: min(460px, calc(100vw - 32px));
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow);
}
.confirm::backdrop, .details::backdrop { background: rgb(0 0 0 / 60%); }

/* A confirmation the server rendered arrives with open already on it, and a
   dialog opened that way is laid out in the flow of the page rather than over
   it — no backdrop, no centring. portal.js upgrades it to a real modal, but the
   markup has to read correctly before that runs and where it never does, so the
   modal's own geometry is written out here. A dialog showModal() opened matches
   these values already, so the rule costs the others nothing. */
.confirm[open] {
  position: fixed;
  inset: 0;
  margin: auto;
  z-index: 20;
  /* The rest of that geometry, which the browser gives a dialog showModal()
     opened and withholds from one merely carrying open. Without them a panel
     taller than the viewport — a phone held sideways, or one with the text
     scaled up — is clipped at both ends, and neither the heading nor the two
     buttons can be scrolled to, because a fixed box does not move with the
     page and an overflowing one gets no scrollbar of its own. */
  max-height: calc(100% - 32px);
  overflow: auto;
}

/* The way out of that same dialog is a link rather than a button, because it
   has to work with no script at all: it goes back to the page with nothing
   charged. Dressed as the quiet button it sits beside, so the two ways out of a
   confirmation do not look like two different kinds of thing. */
.confirm-actions a {
  padding: 9px 12px;
  border: 1px solid var(--border-strong);
  color: var(--muted);
  font-weight: 500;
  font-size: 11px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  text-decoration: none;
}
.confirm-actions a:hover { color: var(--text); border-color: var(--text); }

.confirm form { padding: 24px; margin: 0; }
.confirm h2 { margin: 0 0 12px; }
.confirm p { margin: 0 0 12px; font-size: 12px; }

.confirm-actions {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
  align-items: center;
  margin-top: 20px;
  flex-wrap: wrap;
}
.confirm-actions button { width: auto; }

/* Wider than a confirmation, and scrollable: it holds two JSON bodies and a
   table of retries, which no fixed height is going to fit. */
.details {
  width: min(760px, calc(100vw - 32px));
  max-height: calc(100vh - 64px);
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow);
}

.details-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 20px;
  border-bottom: 1px solid var(--border);
  /* Kept in view while the bodies below it scroll, so the Close button does not
     wander off the top of a long payload. */
  position: sticky;
  top: 0;
  background: var(--surface);
}
.details-head h2 { margin: 0; font-size: 18px; overflow-wrap: anywhere; }
.details-body { padding: 16px 20px 24px; overflow-y: auto; }
.details-body h3 { margin: 24px 0 8px; font-size: 12px; }

pre {
  margin: 0;
  padding: 14px 16px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.5;
  /* Long JSON scrolls sideways inside its own box rather than widening the
     dialog. */
  overflow-x: auto;
}

/* ── Narrow ──────────────────────────────────────────────────────────────── */

@media (max-width: 700px) {
  main { padding: 28px 16px 64px; }
  h1 { font-size: 32px; }
  .card { padding: 20px 18px; }
}

@media (max-width: 600px) {
  /* The filter row stacks rather than squeezing three controls onto a phone,
     every control the full width of the column. flex: none because in a
     column the row's basis would be read as a height. */
  .filters { flex-direction: column; }
  .filters > :not(.visually-hidden),
  .filters > button,
  .filters > .clear { flex: none; width: 100%; }

  /* Let the row run to both screen edges. A card cut off by the viewport rather
     than by a margin is the clearest possible signal that it scrolls — and the
     padding keeps the first card aligned with the text above it. */
  .plans {
    margin-inline: -16px;
    padding-inline: 16px;
    scroll-padding-inline-start: 16px;
  }
}

/* Snapping fights anyone who scrolls in steps rather than flicks, and a reduced
   -motion preference is the best signal we get for that. The buttons' press is
   decoration too. */
@media (prefers-reduced-motion: reduce) {
  .plans { scroll-snap-type: none; }
  button, button:hover, button:active { transition: none; transform: none; }
}
