/* ============================================================================
   theme.css — soft pastel-purple restyle, layered on top of app.css
   ────────────────────────────────────────────────────────────────────────────
   Aesthetic shift:
     FROM: acid-lime cyber-zine, editorial Instrument Serif italics, sharp
           rectangles, large §-numbered section rules, scrolling marquee.
     TO:   boba.xyz vibe — soft lavender accent, sans-only typography,
           pill-shaped buttons, generous rounding, organic background blobs,
           hidden editorial chrome (no ticker, no section §-numbers).

   Strategy: override design tokens + a small set of component rules. Most
   of app.css carries over unchanged — that means visual changes propagate
   to every page (panel, billing, admin, chat, etc.) without per-template
   edits. To roll back: just remove the <link> in base.html.
   ============================================================================ */

:root {
  /* ─── Palette ──────────────────────────────────────────────────── */
  --ink:         #0F0F12;
  --ink-soft:    #131317;
  --surface-1:   #1A1A20;
  --surface-2:   #1E1E26;
  --surface-3:   #25252E;
  --border:      #2A2A33;
  --border-soft: #1F1F26;
  --border-hot:  #3A3A47;

  --paper:       #F5F5F7;
  --paper-soft:  #DCDCE0;
  --mute:        #8E8E94;
  --mute-soft:   #6A6A72;
  --faint:       #4A4A54;

  /* Primary accent — lavender. Replaces acid-lime everywhere it was
     referenced via the --acid token (button bg, focus rings, brand dot,
     pill dot, section-rule num, etc.). */
  --acid:        #C4A9FF;
  --acid-dim:    #A88AEF;
  --acid-glow:   rgba(196, 169, 255, 0.22);

  /* Secondary accents — softened to pastel. */
  --magenta:     #F49AC2;
  --magenta-dim: #E07AAB;
  --cyan:        #8ED5DC;
  --amber:       #F4D58D;
  --rose:        #F0A8AC;
  --emerald:     #9DD5B5;

  /* ─── Typography — sans-only ──────────────────────────────────────
     Display heading face is Unbounded (self-hosted, see app.css for the
     four @font-face subsets). Italic fall-back is the Inter italic face
     so we stay sans-only and don't reintroduce Instrument Serif from
     the original theme. Keep --font-mono for code/IDs. */
  --font-display:        "Unbounded", "Inter", "Hanken Grotesk", "Helvetica Neue", system-ui, -apple-system, sans-serif;
  --font-display-italic: "Inter", "Hanken Grotesk", "Helvetica Neue", system-ui, -apple-system, sans-serif;
  --font-body:           "Inter", "Hanken Grotesk", "Helvetica Neue", system-ui, -apple-system, sans-serif;

  /* ─── Generous rounding ─────────────────────────────────────────── */
  --radius-sm:  10px;
  --radius:     14px;
  --radius-lg:  20px;
  --radius-xl:  28px;

  /* ─── Atmospheric softening ─────────────────────────────────────── */
  --mesh-opacity:  0.72;
  --grain-opacity: 0.025;

  /* ─── Mobile / Telegram-WebView safe-area ─────────────────────────
     env(safe-area-inset-*) resolves to 0 on desktop and on screens
     without a notch / home-indicator — so referencing these vars from
     desktop layouts is a no-op. On iPhone (notched) inside iOS Safari
     or Telegram WebView these expand to the system insets, giving us
     a clean padding source for the composer, footer, sticky bars. */
  --safe-top:    env(safe-area-inset-top,    0px);
  --safe-right:  env(safe-area-inset-right,  0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left:   env(safe-area-inset-left,   0px);
}

/* ─── Mobile touch hygiene ───────────────────────────────────────────
   Suppress the grey/blue tap flash WebKit overlays on every tappable
   element. Pure mobile-only effect — desktop browsers ignore the
   property entirely, so the pointer/hover treatment on PC is
   unchanged. `touch-action: manipulation` on interactive controls
   disables the 300 ms click-delay double-tap-to-zoom heuristic in
   mobile Safari without breaking pinch-zoom of the page itself
   (we only attach it to buttons/links, not to scrollers). */
* { -webkit-tap-highlight-color: transparent; }
a, button, [role="button"], input, select, textarea, label {
  touch-action: manipulation;
}

/* ─── Mobile-render safeguards (don't trigger desktop fallback) ───────
   Symptoms we've seen: pages opened in mobile Firefox / Chrome / TG
   WebView render at desktop width because one rogue child stretches
   the box past the viewport. The browser then auto-zooms-out the
   whole page so it fits — same effect as "show desktop site". These
   four rules close every common offender without touching desktop
   layout (max-width:100% caps to PARENT, which is wider on desktop
   than any of these elements, so the cap never bites). */
html {
  /* Disable iOS text auto-scaling in landscape — landscape kicks the
     font up ~20% in WebKit by default which breaks media-query
     thresholds (a 13 px input becomes 16 px → triggers different
     rules than intended). */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  /* `-webkit-overflow-scrolling: touch` — restores momentum scroll on
     iOS for any pinned-height scroll region (the chat feed). */
  -webkit-overflow-scrolling: touch;
}
html, body {
  /* Hard cap on the document width. body already had overflow-x:hidden
     in app.css; this is a belt-and-braces — width can never exceed the
     visual viewport even if some descendant has a negative margin or
     a fixed-width SVG. hidden is the well-supported fallback; clip
     refines on modern engines (creates no scroll context). */
  max-width: 100vw;
  overflow-x: hidden;
  overflow-x: clip;
}
/* Replaced media: the moment one full-bleed <img> or wide <pre> exceeds
   its parent, the browser starts treating the whole document as wider
   than viewport — exactly the trigger for "desktop scaling". Cap them. */
img, svg, video, iframe, canvas, picture {
  max-width: 100%;
  height: auto;
}
/* iOS Safari auto-zooms when the focused input has font-size < 16 px.
   That zoom then refuses to undo — the page stays zoomed. Force 16 px
   on every text-y input on mobile only (the @media gate keeps the
   small mono fields on desktop forms intact). */
@media (max-width: 880px) {
  input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"]),
  select,
  textarea {
    font-size: 16px;
  }
}

/* ─── Body background — soft amorphous blobs ────────────────────────
   Replace the original three-color radial-gradient with four lavender /
   pink / cyan blobs and a slight central wash. Heavier blur, less
   saturation than the cyber-zine atmosphere.

   Animation removed: a 70px filter:blur(...) on a viewport-sized fixed
   pseudo-element was forcing Firefox to re-rasterise the blurred layer
   on every animation tick (Gecko doesn't push large CSS-filter layers
   onto the GPU compositor the way Blink does). That single rule was
   running steady CPU and holding ~30-80 MB of intermediate bitmaps on
   long-lived chat sessions. The drift was barely perceptible anyway
   (4° rotation over 90 s) — a static blob looks identical at any
   given frame, and Firefox can cache the rasterisation once. */
body::before {
  /* Three blobs, not four. The original 4th (34vw 30vw center wash at
     0.04 alpha) sat behind the other three and contributed almost
     nothing visually — but each `radial-gradient()` is one extra
     stops-walk per pixel during rasterise. Profiles in Firefox SWGL
     (software WebRender) showed `commitRadialGradientFromStops`
     accumulating ~50 samples on the renderer thread; cutting the
     count from 4 → 3 trims that work proportionally. The diagonal
     palette (lavender top-left, pink top-right, cyan bottom-right)
     still reads the same. */
  background:
    radial-gradient(58vw 50vw at 12% 14%, rgba(196, 169, 255, 0.10) 0%, transparent 64%),
    radial-gradient(44vw 44vw at 88% 22%, rgba(244, 154, 194, 0.075) 0%, transparent 64%),
    radial-gradient(60vw 50vw at 70% 92%, rgba(142, 213, 220, 0.06) 0%, transparent 64%) !important;
  /* Was blur(70px). The radii (44–60vw) are already so wide that the
     visible blob edges are diffuse from gradient stops alone; the
     extra 20px of kernel was costing rasterise time on every cache
     invalidation (sticky-header backdrop sample, alpine reactivity,
     viewport resize) without adding meaningful softness. 50px keeps
     the visual identity, halves the per-rasterise pixel work. */
  filter: blur(50px) saturate(1.05) !important;
  animation: none !important;
}

/* Grain pseudo (app.css body::after) costs more than it pays back here:
   --grain-opacity is 0.025 on theme, which is essentially invisible —
   but `mix-blend-mode: overlay` still forces Firefox to create an
   isolation group and re-composite the blend on every paint frame.
   When the page scrolls, the content under the grain layer shifts, so
   the blend MUST recompute even though we can't see the result. Drop
   the layer entirely on theme — the lavender mesh below already gives
   the page texture; nothing visually shifts and Gecko stops doing
   per-frame backdrop work. */
body::after { display: none !important; }

/* ─── Hide editorial chrome ─────────────────────────────────────────
   Ticker marquee is gone, section-rule loses its §-number + horizontal
   lines and becomes a simple heading. */
.ticker { display: none !important; }

.section-rule {
  display: flex;
  gap: 12px;
  margin: 56px 0 22px;
  align-items: baseline;
  font-family: var(--font-body);
  font-size: 14px;
  letter-spacing: 0;
  text-transform: none;
}
.section-rule::before,
.section-rule::after { display: none !important; }
.section-rule__num { display: none; }
.section-rule__title {
  color: var(--paper);
  font-weight: 600;
  font-size: 18px;
  letter-spacing: -0.012em;
  text-transform: none;
}

/* ─── Buttons — pill-shaped, soft shadow on hover ──────────────────── */
.btn {
  height: 44px;
  padding: 0 22px;
  border-radius: 999px;
  font-weight: 500;
  letter-spacing: -0.005em;
}
.btn--sm { height: 36px; padding: 0 16px; border-radius: 999px; font-size: 13px; }

.btn--primary {
  background: var(--acid);
  color: #1A1A20;
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.10) inset;
}
.btn--primary:hover {
  background: var(--acid-dim);
  box-shadow: 0 6px 22px rgba(196, 169, 255, 0.28), 0 1px 0 rgba(255, 255, 255, 0.12) inset;
}

.btn--ghost {
  background: rgba(255, 255, 255, 0.04);
  border-color: transparent;
  color: var(--paper);
}
.btn--ghost:hover {
  background: rgba(255, 255, 255, 0.08);
  border-color: transparent;
}

/* Integration card — tabbed install snippets. Single-card layout
   replaces the old "Endpoints" block. Tabs are pill-segmented; the
   active one gets the lavender wash, hidden panels use [hidden].

   Geometry notes:
   - inline-flex + nowrap keeps the segmented control as one strip
     even at narrow widths; previous flex-wrap let it bleed to a
     second line for ~80px viewport edge cases.
   - align-items: stretch makes all three buttons the same height
     so the visual baseline doesn't drift between tabs of different
     character lengths.
   - tab radius matches the container minus its padding so the
     active pill sits flush against the inner edge instead of
     leaving a half-moon of background showing.
*/
.integration__tabs {
  display: inline-flex;
  flex-wrap: nowrap;
  align-items: stretch;
  gap: 2px;
  padding: 3px;
  background: rgba(255, 255, 255, 0.025);
  border: 1px solid var(--border-soft);
  border-radius: 999px;
  margin-bottom: 14px;
}
.integration__tab {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--mute);
  padding: 7px 18px;
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.02em;
  line-height: 1.2;
  border-radius: 999px;
  cursor: pointer;
  transition: color 140ms ease, background 140ms ease, box-shadow 140ms ease;
  /* Reserve the 1px ring's worth of width on every tab so the
     active pill doesn't visually nudge its neighbours when it
     gains the inset highlight below. Inset works inside the same
     box — without the reserved ring the active state would still
     be the same width as inactive, just visually thicker by half
     a pixel on each side. */
  box-shadow: inset 0 0 0 1px transparent;
  white-space: nowrap;
}
.integration__tab:hover {
  color: var(--paper);
  background: rgba(255, 255, 255, 0.03);
}
.integration__tab--active {
  color: var(--paper);
  background: rgba(196, 169, 255, 0.18);
  font-weight: 600;
  /* INSET — ring sits inside the button's box, so the pill stays
     the exact same outer dimensions as the inactive tabs next to
     it. The previous outset 1px ring leaked into the 6px gap and
     made the active tab read as "bigger". */
  box-shadow: inset 0 0 0 1px rgba(196, 169, 255, 0.34);
}
.integration__tab:focus-visible {
  outline: 2px solid var(--acid);
  outline-offset: 2px;
}

/* ─── Narrow viewports / Telegram WebView ─────────────────────────────
   On screens ≤ 520 px (the in-bot WebView is ~370 px) the three-pill
   inline-flex bar overflowed the card to the right — the chip
   container kept its natural width and OpenCode got clipped by the
   card's padding. Switching the wrapper to full-width flex with
   equal-share children sizes the pills against the card itself.
   Padding is also trimmed (7→6 px vertical, 18→8 px horizontal) so
   "Claude Code" still fits on one line at 320 px. The same fix is
   applied to the inner OS sub-picker (Windows / macOS / Linux). */
@media (max-width: 520px) {
  .integration__tabs,
  .integration__os {
    display: flex;
    width: 100%;
    flex-wrap: nowrap;
  }
  .integration__tab,
  .integration__os-tab {
    flex: 1 1 0;
    min-width: 0;
    padding-left: 8px;
    padding-right: 8px;
    text-align: center;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .integration__tab { padding-top: 6px; padding-bottom: 6px; font-size: 11.5px; }
  .integration__os-tab { padding-top: 4px; padding-bottom: 4px; font-size: 10.5px; }
}

.integration__hint {
  margin: 0 0 10px;
  font-size: 12.5px;
  color: var(--mute);
  line-height: 1.5;
}

/* OS sub-picker inside an integration panel — small pill-style tabs
   (Windows / macOS / Linux) that pick which install one-liner to show
   below. Sits between the .integration__hint and the .integration__code. */
.integration__os {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  margin: 0 0 10px;
  background: rgba(255, 255, 255, 0.025);
  border: 1px solid var(--border-soft);
  border-radius: 999px;
}
.integration__os-tab {
  appearance: none;
  border: 0;
  background: transparent;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.02em;
  color: var(--mute);
  padding: 5px 12px;
  border-radius: 999px;
  cursor: pointer;
  transition: background 140ms, color 140ms;
}
.integration__os-tab:hover { color: var(--paper-soft); }
.integration__os-tab--active {
  background: rgba(196, 169, 255, 0.16);
  color: var(--acid);
}
/* One-line "then set X = Y" hint that follows .integration__code on
   each integration tab. Inline <code> chips inside use the global
   `.code` styling already defined elsewhere. */
.integration__sub {
  margin: 10px 0 0;
  font-size: 12px;
  line-height: 1.55;
  color: var(--paper-soft);
}
.integration__sub code {
  font-family: var(--font-mono);
  font-size: 11px;
  padding: 1px 6px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border-soft);
  border-radius: 5px;
  color: var(--paper);
}
.integration__code {
  margin: 0;
  padding: 12px 14px;
  background: rgba(0, 0, 0, 0.30);
  border: 1px solid var(--border-soft);
  border-radius: 10px;
  font-family: var(--font-mono);
  font-size: 11.5px;
  line-height: 1.55;
  color: var(--paper-soft);
  white-space: pre-wrap;
  word-break: break-all;
  max-height: 180px;
  overflow-y: auto;
  scrollbar-width: thin;
}
.integration__code code { background: none; color: inherit; padding: 0; }
.integration__copy {
  margin-top: 10px;
  appearance: none;
  border: 1px solid var(--border-soft);
  background: rgba(255, 255, 255, 0.03);
  color: var(--paper);
  border-radius: 8px;
  padding: 7px 14px;
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}
.integration__copy:hover {
  background: rgba(196, 169, 255, 0.10);
  border-color: rgba(196, 169, 255, 0.40);
}
.integration__copy.is-copied {
  color: var(--emerald);
  border-color: rgba(157, 213, 181, 0.50);
  background: rgba(157, 213, 181, 0.10);
}
.integration__more {
  display: inline-block;
  margin-top: 14px;
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--acid);
  text-decoration: none;
  letter-spacing: 0.02em;
}
.integration__more:hover { color: var(--paper); }

/* Setup page — long-form guide. Each tool gets its own section with
   numbered steps + code blocks. Inherits .integration__code styling. */
.setup-section {
  padding: 24px 26px;
  margin-top: 22px;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.012);
}
.setup-section__title {
  font-family: var(--font-body);
  font-size: 18px;
  font-weight: 600;
  color: var(--paper);
  margin: 0 0 6px;
}
.setup-section__sub {
  margin: 0 0 18px;
  font-size: 13px;
  color: var(--mute);
}
.setup-step {
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr);
  gap: 14px;
  align-items: start;
  margin-top: 14px;
}
.setup-step__num {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1px solid rgba(196, 169, 255, 0.40);
  color: var(--paper);
  background: rgba(196, 169, 255, 0.10);
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.setup-step__body { font-size: 13px; line-height: 1.6; color: var(--paper-soft); }
.setup-step__body p { margin: 0 0 8px; }
.setup-step__body code:not(.integration__code code) {
  font-family: var(--font-mono);
  font-size: 11.5px;
  padding: 1px 5px;
  border-radius: 4px;
  background: rgba(196, 169, 255, 0.10);
  color: var(--paper);
}

/* Setup page — anchor TOC chips at the top, links to each per-tool
   article. The guide is a flat document keyed by `#all` / `#cc` /
   `#codex` / `#opencode` / `#sdk` anchors. */
.setup-toc {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 18px;
}
.setup-toc__chip {
  font-family: var(--font-mono);
  font-size: 11.5px;
  letter-spacing: 0.02em;
  color: var(--paper-soft);
  padding: 6px 12px;
  border: 1px solid var(--border-soft);
  border-radius: 999px;
  text-decoration: none;
  background: rgba(255, 255, 255, 0.02);
  transition: color 140ms, background 140ms, border-color 140ms;
}
.setup-toc__chip--active,
.setup-toc__chip--active:hover {
  color: var(--acid);
  background: rgba(196, 169, 255, 0.16);
  border-color: rgba(196, 169, 255, 0.55);
}
.setup-toc__chip:hover {
  color: var(--acid);
  background: rgba(196, 169, 255, 0.10);
  border-color: rgba(196, 169, 255, 0.36);
}

/* Setup page — OS picker (Windows / macOS / Linux) used inside each
   tool's step blocks. Pure CSS: three hidden radios + labels-as-tabs;
   the matching `.setup-os__pane[data-os=…]` is revealed via the
   `:checked ~` general-sibling selector, no JS needed. The id-suffix
   trick (`[id$="-win"]` etc.) lets one rule cover every os-group
   (`os-all-*`, `os-cc-*`, `os-codex-*`, `os-opencode-*`). */
.setup-os { display: block; margin: 8px 0 0; }
.setup-os__radio {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  width: 0;
  height: 0;
}
.setup-os__tabs {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  margin: 0 0 10px;
  background: rgba(255, 255, 255, 0.025);
  border: 1px solid var(--border-soft);
  border-radius: 999px;
}
.setup-os__tab {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.02em;
  color: var(--mute);
  padding: 5px 12px;
  border-radius: 999px;
  cursor: pointer;
  user-select: none;
  transition: background 140ms, color 140ms;
}
.setup-os__tab:hover { color: var(--paper-soft); }
.setup-os__pane { display: none; }
.setup-os__radio[id$="-win"]:checked ~ .setup-os__tabs label[for$="-win"],
.setup-os__radio[id$="-mac"]:checked ~ .setup-os__tabs label[for$="-mac"],
.setup-os__radio[id$="-linux"]:checked ~ .setup-os__tabs label[for$="-linux"] {
  background: rgba(196, 169, 255, 0.16);
  color: var(--acid);
}
.setup-os__radio[id$="-win"]:checked ~ .setup-os__pane[data-os="win"],
.setup-os__radio[id$="-mac"]:checked ~ .setup-os__pane[data-os="mac"],
.setup-os__radio[id$="-linux"]:checked ~ .setup-os__pane[data-os="linux"] {
  display: block;
}
.setup-os__radio:focus-visible ~ .setup-os__tabs {
  box-shadow: 0 0 0 2px rgba(196, 169, 255, 0.30);
  border-color: rgba(196, 169, 255, 0.45);
}
.setup-os__hint {
  margin: 0 0 6px;
  color: var(--mute);
  font-family: var(--font-mono);
  font-size: 11px;
}

/* Setup page — top-level "Automatic / Manual" toggle that swaps the
   entire body of the page. Mirrors the .setup-os radio-tab pattern,
   but with two large pill-tabs sized for primary navigation. Nested
   .setup-os groups inside each pane keep using their own radio names
   (os-cc-auto, os-cc-manual, …) so the suffix-selector matches scope
   to the nearest .setup-os ancestor only. */
.setup-mode { display: block; margin: 22px 0 0; }
.setup-mode__radio {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  width: 0;
  height: 0;
}
.setup-mode__tabs {
  display: inline-flex;
  gap: 4px;
  padding: 5px;
  margin: 0 0 18px;
  background: rgba(255, 255, 255, 0.025);
  border: 1px solid var(--border-soft);
  border-radius: 999px;
}
.setup-mode__tab {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.02em;
  color: var(--mute);
  padding: 9px 18px;
  border-radius: 999px;
  cursor: pointer;
  user-select: none;
  transition: background 140ms, color 140ms;
}
.setup-mode__tab:hover { color: var(--paper-soft); }
.setup-mode__dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.55;
}
.setup-mode__pane { display: none; }
.setup-mode__radio[id$="-auto"]:checked ~ .setup-mode__tabs label[for$="-auto"],
.setup-mode__radio[id$="-manual"]:checked ~ .setup-mode__tabs label[for$="-manual"] {
  background: rgba(196, 169, 255, 0.16);
  color: var(--acid);
}
.setup-mode__radio[id$="-auto"]:checked ~ .setup-mode__pane[data-mode="auto"],
.setup-mode__radio[id$="-manual"]:checked ~ .setup-mode__pane[data-mode="manual"] {
  display: block;
}
.setup-mode__radio:focus-visible ~ .setup-mode__tabs {
  box-shadow: 0 0 0 2px rgba(196, 169, 255, 0.30);
  border-color: rgba(196, 169, 255, 0.45);
}
.setup-mode__lead {
  margin: 0 0 6px;
  font-size: 13.5px;
  line-height: 1.65;
  color: var(--paper-soft);
  max-width: 62ch;
}
.setup-mode__foot {
  margin: 22px 0 0;
  padding: 14px 18px;
  border: 1px dashed var(--border-soft);
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.012);
  color: var(--mute);
  font-family: var(--font-mono);
  font-size: 11.5px;
  line-height: 1.6;
}

/* Setup section — optional header row with a "recommended" badge that
   anchors the first card in the Auto pane. Falls back to the existing
   .setup-section__title flow if no head wrapper is present. */
.setup-section__head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 6px;
  flex-wrap: wrap;
}
.setup-section__head .setup-section__title { margin: 0; }
.setup-section__badge {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--acid);
  padding: 3px 9px;
  border-radius: 999px;
  background: rgba(196, 169, 255, 0.14);
  border: 1px solid rgba(196, 169, 255, 0.40);
}
.setup-section__after {
  margin: 14px 0 0;
  color: var(--mute);
  font-family: var(--font-mono);
  font-size: 11.5px;
}

/* Setup-step inline helpers — small print under a step's code block.
   Keep these tight so a long Manual guide doesn't feel like a wall. */
.setup-step__note {
  margin: 10px 0 0;
  font-size: 12px;
  color: var(--mute-soft);
  line-height: 1.6;
}
.setup-step__path {
  margin: 0 0 8px;
  font-size: 11.5px;
  color: var(--mute-soft);
}
.setup-step__list {
  margin: 8px 0;
  padding-left: 22px;
  color: var(--paper-soft);
  line-height: 1.7;
}

/* Chat-composer "Send" / "Generate image" — premium feel without
   replacing the global .btn--primary (which the rest of the site uses).
   Brand gradient (lavender → magenta-peach) + soft glow on hover so the
   primary action in the conversation surface is unmistakeable. */
.btn--send {
  height: 38px;
  padding: 0 18px 0 16px;
  border-radius: 999px;
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: -0.005em;
  color: #1A1A20;
  background: linear-gradient(135deg, #D8C2FF 0%, #F49AC2 100%);
  border: 0;
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.35) inset,
    0 6px 18px -6px rgba(196, 169, 255, 0.55);
  transition:
    transform 140ms cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 200ms ease,
    filter 200ms ease;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.btn--send:hover {
  transform: translateY(-1px);
  filter: brightness(1.05);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.40) inset,
    0 12px 32px -8px rgba(196, 169, 255, 0.70),
    0 0 0 4px rgba(196, 169, 255, 0.12);
}
.btn--send:active {
  transform: translateY(0) scale(0.985);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.30) inset,
    0 4px 10px -4px rgba(196, 169, 255, 0.55);
}
.btn--send:focus-visible {
  outline: 2px solid rgba(196, 169, 255, 0.65);
  outline-offset: 3px;
}
.btn--send svg {
  flex-shrink: 0;
  transform: translateX(-1px);
  transition: transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
}
.btn--send:hover svg {
  transform: translateX(1px) rotate(-8deg);
}

.btn--magenta {
  background: var(--magenta);
  color: #1A1A20;
}
.btn--magenta:hover { background: var(--magenta-dim); }

.btn--danger-link {
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
}

/* ─── Cards — softer surface, no corner marks ──────────────────────── */
.card {
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  padding: 26px;
}
.card--marked::before,
.card--marked::after { display: none !important; }
.card--hero,
.card--magenta,
.card--emerald {
  background:
    radial-gradient(80% 100% at 0% 0%, rgba(196, 169, 255, 0.08) 0%, transparent 70%),
    var(--surface-1);
  border: 1px solid var(--border-soft);
}

/* ─── Brand — drop the italic serif, cat mascot mark ───────────────── */
.brand__name {
  font-family: var(--font-body);
  font-weight: 700;
  font-style: normal;
  font-size: 18px;
  letter-spacing: -0.012em;
  line-height: 1;
}
.brand__name .dot { color: var(--acid); }
.brand__name .ext {
  font-family: var(--font-body);
  font-weight: 500;
  font-style: normal;
  font-size: 14px;
  color: var(--mute);
  letter-spacing: 0;
}

/* Brand mark — rounded lavender plate holding the mascot. The plate
   shape is the constant identity; the mascot inside fills most of it. */
.brand__mark {
  width: 40px;
  height: 40px;
  background: var(--acid);
  border-color: transparent;
  border-radius: 11px;
  color: #FFFFFF;
}
.brand__mark svg,
.brand__mark img {
  width: 36px;
  height: 36px;
  object-fit: contain;
  display: block;
}
.brand__mark svg path,
.brand__mark svg ellipse {
  stroke: none;
}

/* ─── Site header — softer translucent ─────────────────────────────── */
.site-header {
  /* Alpha walked up across two perf passes:
     - 0.78 → 0.88 when we cut backdrop-filter from blur(14px) → blur(6px).
     - 0.88 → 0.94 with backdrop-filter killed entirely.
     The frosted-glass effect was per-scroll-frame: Firefox had to
     re-sample the pixels under the sticky bar every frame, and those
     pixels included the body::before blur(50px) mesh — nested blur
     work that bottlenecked smooth scrolling on the landing. At 0.94
     alpha the bar is already nearly opaque; what little colour the
     blur was leaking through is invisible at a glance. -webkit prefix
     dropped too (no sampler runs => no need for the prefix at all). */
  background: rgba(15, 15, 18, 0.94);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-bottom: 1px solid var(--border-soft);
}
.site-header__inner { height: 64px; }

/* ─── Hamburger drawer (mobile only) ─────────────────────────────────
   Pure-CSS off-canvas navigation. Three elements live together on
   desktop, all set to display: none:
     • #mobile-nav-toggle   — hidden checkbox carrying open-state
     • .mobile-nav-btn      — the burger button (a <label>)
     • .mobile-nav-overlay  — click-to-close backdrop (another <label>)
   When the viewport drops to ≤880px the button + overlay surface and
   .nav itself becomes a fixed-position drawer that slides in from
   the right via translateX. The trick is that the checkbox sits as
   a sibling of .nav in DOM order, so `:checked ~ .nav` flips its
   transform without any JS. CSP-safe because no event handlers. */
.mobile-nav-toggle  { position: absolute; opacity: 0; pointer-events: none; }
.mobile-nav-btn     { display: none; }
.mobile-nav-overlay { display: none; }

@media (max-width: 880px) {
  .site-header__inner {
    position: relative;
    height: 56px;
    gap: 8px;
    padding: 0 16px;
  }

  /* Burger button — three crisp paper-soft bars in a 44×44 hit area,
     positioned absolute so it sits on the row's right edge regardless
     of how many nav items the page rendered. */
  .mobile-nav-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    margin-left: auto;
    border-radius: 12px;
    cursor: pointer;
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid var(--border-soft);
    transition: background 160ms ease, border-color 160ms ease;
    z-index: 110;
  }
  .mobile-nav-btn:hover,
  .mobile-nav-btn:focus-within { background: rgba(255, 255, 255, 0.08); border-color: var(--border-hot); }

  .mobile-nav-btn__bars {
    display: inline-flex;
    flex-direction: column;
    gap: 4px;
    width: 18px;
  }
  .mobile-nav-btn__bars > span {
    display: block;
    width: 100%;
    height: 2px;
    background: var(--paper);
    border-radius: 2px;
    transition: transform 200ms ease, opacity 160ms ease;
    transform-origin: center;
  }
  /* Animate burger → ✕ when drawer opens. */
  .mobile-nav-toggle:checked ~ .mobile-nav-btn .mobile-nav-btn__bars > span:nth-child(1) {
    transform: translateY(6px) rotate(45deg);
  }
  .mobile-nav-toggle:checked ~ .mobile-nav-btn .mobile-nav-btn__bars > span:nth-child(2) {
    opacity: 0;
  }
  .mobile-nav-toggle:checked ~ .mobile-nav-btn .mobile-nav-btn__bars > span:nth-child(3) {
    transform: translateY(-6px) rotate(-45deg);
  }

  /* Click-to-close overlay behind the drawer. Stays in DOM but with
     opacity 0 + pointer-events none so it doesn't catch taps when
     closed. */
  .mobile-nav-overlay {
    display: block;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.55);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
    opacity: 0;
    pointer-events: none;
    transition: opacity 220ms ease;
    z-index: 99;
  }
  .mobile-nav-toggle:checked ~ .mobile-nav-overlay {
    opacity: 1;
    pointer-events: auto;
  }

  /* The drawer itself. Off-canvas right; slides in. .nav was a row
     before — flip to column, generous spacing, full-height. */
  .nav {
    position: fixed;
    top: 0;
    right: 0;
    width: min(82vw, 320px);
    height: 100dvh;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    gap: 6px;
    padding: 76px 20px max(24px, var(--safe-bottom));
    background: var(--ink-soft);
    border-left: 1px solid var(--border-soft);
    box-shadow: -16px 0 40px rgba(0, 0, 0, 0.4);
    transform: translateX(100%);
    transition: transform 260ms cubic-bezier(0.4, 0, 0.2, 1);
    z-index: 100;
    overflow-y: auto;
  }
  .mobile-nav-toggle:checked ~ .nav {
    transform: translateX(0);
  }

  /* Inside the drawer nav links become full-width rows with bigger
     touch targets. Active state keeps its lavender wash. */
  .nav__link {
    display: flex;
    align-items: center;
    width: 100%;
    height: 44px;
    padding: 0 14px;
    font-size: 15px;
    border-radius: 10px;
  }
  .nav__sep {
    display: block;
    height: 1px;
    width: 100%;
    margin: 8px 0;
    background: var(--border-soft);
  }
  /* CTA "Get a key" sits at the bottom of the drawer with breathing room. */
  .nav__link--cta {
    margin-top: 8px;
    justify-content: center;
    font-weight: 600;
  }

  /* Lang switch — keep it compact at the bottom of the drawer. */
  .lang-switch {
    margin-top: auto;
    align-self: center;
  }

  /* Stop the body from scrolling when the drawer is open. */
  .mobile-nav-toggle:checked ~ .nav,
  .mobile-nav-toggle:checked ~ .mobile-nav-overlay { /* nothing extra; here for selector clarity */ }
}
html:has(.mobile-nav-toggle:checked) { overflow: hidden; }

/* ─── Nav — pill links, lavender active ────────────────────────────── */
.nav__link {
  border-radius: 999px;
  padding: 0 14px;
  font-weight: 500;
}
.nav__link:hover { background: rgba(255, 255, 255, 0.05); }
.nav__link--active {
  background: rgba(196, 169, 255, 0.12);
  color: var(--acid);
}
.nav__link--cta {
  background: var(--acid);
  color: #1A1A20;
  border-radius: 999px;
  padding: 0 16px;
}
.nav__link--cta:hover { background: var(--acid-dim); color: #1A1A20; }

/* Telegram-support link — same lavender palette as the rest of nav,
   inline paper-plane icon so it still reads as an offsite action. */
.nav__link--support {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.nav__link--support svg {
  flex-shrink: 0;
  margin-top: -1px;
  opacity: 0.75;
  transition: opacity 160ms ease;
}
.nav__link--support:hover svg { opacity: 1; }

.nav__user {
  font-family: var(--font-body);
  font-weight: 500;
}

/* Lang switch — pill segmented control to match the surrounding pill
   nav links. Overrides the sharp 6px rectangle from app.css and the
   high-contrast solid-acid active state, both of which made the toggle
   read louder than every other element in the header. */
.lang-switch {
  border-color: var(--border-soft);
  border-radius: 999px;
  height: 32px;
  padding: 2px;
  margin-left: 6px;
  align-items: stretch;
}
.lang-switch__btn {
  display: inline-flex;
  align-items: center;
  border-radius: 999px;
  padding: 0 10px;
  cursor: pointer;
  transition: background 160ms, color 160ms;
}
.lang-switch__btn--active {
  background: rgba(196, 169, 255, 0.12);
  color: var(--acid);
}
.lang-switch__btn:not(.lang-switch__btn--active):hover {
  background: rgba(255, 255, 255, 0.04);
  color: var(--paper);
}

/* ─── Models block — collapsible model list inside a provider card.
   Pure <details>/<summary>, no JS. Closed by default — each provider
   has 5-15 model bindings and we don't want every card to stretch
   the page. The chevron flips on [open] via a sibling rule. */
.models-block > summary {
  list-style: none;
  cursor: pointer;
  user-select: none;
  padding: 4px 0;
}
.models-block > summary::-webkit-details-marker,
.models-block > summary::marker { display: none; }
.models-block__chev {
  display: inline-block;
  margin-right: 6px;
  color: var(--mute);
  transition: transform 120ms ease;
  font-family: var(--font-mono);
}
.models-block[open] > summary .models-block__chev { transform: rotate(90deg); }
.models-block > summary:hover .eyebrow { color: var(--paper); }

/* ─── Action menu — per-row dropdown for admin tables ──────────────
   Uses native <details>/<summary> so we get open/close without JS,
   keeping CSP happy. Trigger looks like a regular .btn--ghost button
   (we strip the default disclosure triangle); the floating panel sits
   below-right and rides above the table on z-index. */
.actions-menu {
  display: inline-block;
  position: relative;
}
.actions-menu > summary {
  list-style: none;
  cursor: pointer;
  user-select: none;
}
.actions-menu > summary::-webkit-details-marker,
.actions-menu > summary::marker { display: none; }
.actions-menu[open] > summary {
  background: rgba(196, 169, 255, 0.10);
  color: var(--acid);
}
.actions-menu__panel {
  /* `position: fixed` — JS in base.html computes `top` / `left` against
     the trigger's `getBoundingClientRect()` every time the menu opens
     (and on scroll / resize while open). Fixed positioning sidesteps
     every clip we used to hit: data-wrap rounded corners, table cell
     overflow, sticky page header, container z-index battles. The two
     usages of `.actions-menu` today (admin_users / admin_models) sit
     in plain `<section>`s outside any `.reveal` wrapper, so the
     transform-containing-block hazard the absolute variant worried
     about doesn't apply here. `z-index: 100` still keeps the panel
     above the sticky `.site-header` (z-index: 30). */
  position: fixed;
  min-width: 200px;
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  padding: 4px;
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.5);
  z-index: 100;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.actions-menu__item {
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  background: transparent;
  text-align: left;
  width: 100%;
  padding: 9px 12px;
  font-family: var(--font-mono);
  font-size: 12.5px;
  color: var(--paper-soft);
  cursor: pointer;
  border-radius: 6px;
  transition: background 140ms, color 140ms;
}
.actions-menu__item:hover {
  background: rgba(255, 255, 255, 0.04);
  color: var(--paper);
}
.actions-menu__item--danger { color: var(--rose); }
.actions-menu__item--danger:hover {
  background: rgba(248, 113, 113, 0.08);
  color: var(--rose);
}
.actions-menu form.inline { margin: 0; }

/* ─── Profile menu — header avatar dropdown ───────────────────────────
   Mounts the existing actions-menu portal logic onto a different
   trigger shape: pill button with circular avatar + login + chevron.
   csp-safe-init.js portals .actions-menu__panel into <body> while
   open, so the dropdown rides above the sticky header regardless
   of containing-block restrictions. Mobile (≤720px) collapses the
   trigger to the avatar bubble — saves nav real estate without
   sacrificing affordance. */
.profile-menu > summary.profile-menu__btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px 4px 4px;
  border-radius: 999px;
  border: 1px solid var(--border-soft);
  color: var(--paper-soft);
  background: transparent;
  font-family: var(--font-mono);
  font-size: 12.5px;
  line-height: 1;
  transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}
.profile-menu > summary.profile-menu__btn:hover {
  background: rgba(255, 255, 255, 0.04);
  border-color: var(--border);
  color: var(--paper);
}
/* The .actions-menu[open] > summary base rule adds a lavender bg —
   that styling is meant for square-ish admin-row buttons and looks
   wrong on a pill. Override with higher specificity. */
.profile-menu[open] > summary.profile-menu__btn {
  background: rgba(196, 169, 255, 0.10);
  border-color: rgba(196, 169, 255, 0.32);
  color: var(--acid);
}

.profile-menu__avatar {
  width: 26px;
  height: 26px;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: rgba(196, 169, 255, 0.16);
  color: var(--acid);
  font-weight: 600;
  font-size: 12px;
  flex-shrink: 0;
  text-transform: uppercase;
  letter-spacing: 0;
}

.profile-menu__login {
  max-width: 140px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.profile-menu__caret {
  flex-shrink: 0;
  color: var(--mute);
  transition: transform 160ms ease, color 160ms ease;
}
.profile-menu[open] > summary.profile-menu__btn .profile-menu__caret {
  transform: rotate(-180deg);
  color: var(--acid);
}

/* Dropdown body. The base .actions-menu__panel pads itself by 4px;
   we drop that here so the identity header card can bleed to the
   panel edges and the bottom-border separator looks clean. */
.profile-menu__panel {
  min-width: 244px;
  padding: 0;
  overflow: hidden;
  border-radius: 12px;
}

/* Identity header — Stripe-style account card pinned at the top of
   the dropdown. Resolves the operator's "menu looks chimeric" gripe:
   without it, the trigger pill said "admin" and the dropdown
   immediately dropped into action links with no continuity. The
   gradient strip below mirrors the trigger's [open]-state tint so
   the eye reads them as the same surface. */
.profile-menu__header {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 14px;
  background: linear-gradient(180deg, rgba(196, 169, 255, 0.08), transparent 90%);
  border-bottom: 1px solid var(--border-soft);
}
.profile-menu__avatar--lg {
  width: 36px;
  height: 36px;
  font-size: 14px;
}
.profile-menu__identity {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow: hidden;
}
.profile-menu__identity-name {
  font-family: var(--font-mono);
  font-size: 13.5px;
  color: var(--paper);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.profile-menu__identity-role {
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--mute);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* Item groups inside the panel. The base `.actions-menu__panel`
   sets `display: flex; flex-direction: column`, but
   csp-safe-init.js *portals the panel into <body>* on open, which
   strips it of any `.profile-menu` ancestor and would otherwise let
   our anchor items lay out side-by-side (UA-default `<a>` is
   inline). Pin every selector below to a class that travels with
   the portaled element (`.profile-menu__panel` / `.profile-menu__items`)
   so the styles survive the move. */
.profile-menu__items {
  display: flex;
  flex-direction: column;
  padding: 4px;
}

/* Anchor reset. UA-stylesheet `a { text-decoration: underline; … }`
   wins over the base `.actions-menu__item` rule (we never set
   `text-decoration` there). Force every link state to the panel's
   design tokens. */
.profile-menu__panel a.actions-menu__item,
.profile-menu__panel a.actions-menu__item:link,
.profile-menu__panel a.actions-menu__item:visited {
  text-decoration: none;
  color: var(--paper-soft);
  font-family: var(--font-mono);
  display: block;
}
.profile-menu__panel a.actions-menu__item:hover,
.profile-menu__panel a.actions-menu__item:focus,
.profile-menu__panel a.actions-menu__item:focus-visible {
  text-decoration: none;
  color: var(--paper);
  outline: none;
}

/* Items breathe a bit more inside this panel than they do in admin
   tables — the dropdown sits above the chrome and doesn't have to
   match cell heights. Selector pinned to the portalled root. */
.profile-menu__panel .actions-menu__item {
  padding: 10px 14px;
  font-size: 13px;
  border-radius: 8px;
}

.profile-menu__sep {
  border: 0;
  border-top: 1px solid var(--border-soft);
  margin: 0;
  height: 0;
}

@media (max-width: 720px) {
  .profile-menu__login { display: none; }
  .profile-menu > summary.profile-menu__btn { padding: 3px; }
}

/* ─── Auth tabs — CSS-only segmented control on /login ──────────────
   Two hidden radios drive `:checked ~ panel` selectors to flip which
   form is visible. Pure CSS, no JS, CSP-safe. The labels look like a
   matched pair of pill nav-buttons; active state borrows the lavender
   soft-tint pattern used elsewhere (.nav__link--active, .actions-menu). */
.auth-tabs__radio {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.auth-tabs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px;
  margin-top: 22px;
  padding: 4px;
  background: rgba(255, 255, 255, 0.02);
  border: 1px solid var(--border-soft);
  border-radius: 999px;
}
.auth-tabs__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 10px 16px;
  font-family: var(--font-body);
  font-size: 13.5px;
  font-weight: 500;
  color: var(--mute);
  background: transparent;
  border-radius: 999px;
  cursor: pointer;
  user-select: none;
  transition: background 160ms, color 160ms;
}
.auth-tabs__btn:hover {
  color: var(--paper-soft);
}
.auth-tabs__btn svg {
  flex-shrink: 0;
  opacity: 0.7;
  transition: opacity 160ms;
}
#auth-tab-email:checked ~ .auth-tabs .auth-tabs__btn--email,
#auth-tab-password:checked ~ .auth-tabs .auth-tabs__btn--password {
  background: rgba(196, 169, 255, 0.14);
  color: var(--acid);
}
#auth-tab-email:checked ~ .auth-tabs .auth-tabs__btn--email svg,
#auth-tab-password:checked ~ .auth-tabs .auth-tabs__btn--password svg {
  opacity: 1;
}

.auth-tabs__panel {
  display: none;
  margin-top: 22px;
}
#auth-tab-email:checked ~ .auth-tabs__panel--email,
#auth-tab-password:checked ~ .auth-tabs__panel--password {
  display: block;
}

/* "Sent to {email}" header that replaces the email-entry form after
   the magic-link POST. Subtle dashed-rule envelope, lavender accent
   on the address itself (rendered inline as <code>). */
.auth-card__sent {
  padding: 14px 16px;
  background: rgba(196, 169, 255, 0.06);
  border: 1px solid rgba(196, 169, 255, 0.18);
  border-radius: var(--radius);
}
/* Inline hint below an input — small, mute, mono. */
.auth-card__hint {
  margin: 8px 0 0;
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--mute);
  line-height: 1.5;
}

/* Big-and-clear 6-digit code input. Centered, wide letter spacing,
   monospace — feels like a one-time-code field. Browsers with
   `autocomplete="one-time-code"` will auto-fill from SMS/email
   integrations (iOS, Android) without us doing anything special. */
.code-input {
  text-align: center;
  font-size: 22px;
  font-weight: 600;
  letter-spacing: 0.28em;
  padding: 14px 12px;
}
.code-input::placeholder {
  color: var(--mute-soft);
  letter-spacing: 0.28em;
}

/* ─── Footer — minimal, single-row ─────────────────────────────────── */
.site-footer {
  /* `auto` keeps the sticky-footer behaviour set up in app.css
     (body flex column + main flex:1) — on short pages the footer
     hugs the viewport bottom; on long pages this collapses to 0
     and the regular flex spacing kicks in. Visual gap above the
     footer is owned by `padding-top` so it survives either case. */
  margin-top: auto;
  /* Bottom padding folds in the iOS home-indicator inset on devices
     that have one; on desktop var(--safe-bottom) is 0px and the
     footer keeps its original 28px. */
  padding: 80px 28px calc(28px + var(--safe-bottom));
  border-top: 1px solid var(--border-soft);
  font-family: var(--font-body);
  font-size: 13px;
  color: var(--mute);
}

/* ─── In Telegram WebView ─────────────────────────────────────────────
   Hide the public footer + the in-page header inside a Telegram
   WebApp sheet — TG already provides its own close/back chrome and
   the sheet height is precious. Also widen .shell so panels reach
   the safe edges (Telegram already overlays its bezel). The rules
   only fire when tg-webapp.js has set body.in-tg, so PC + standalone
   mobile rendering stays untouched. */
.in-tg .site-footer { display: none !important; }
.in-tg .site-header {
  /* Keep the header but tighten — TG users already see the page
     title in the bot's sheet header. Loses ~36px of vertical real
     estate, which matters on short Android sheets. */
  padding-top: max(8px, var(--safe-top)) !important;
  padding-bottom: 8px !important;
}
.in-tg main > .shell {
  padding-left:  max(16px, var(--safe-left));
  padding-right: max(16px, var(--safe-right));
}
.in-tg .ticker { display: none !important; }
.site-footer__inner {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
}
.site-footer__cols,
.site-footer__heading,
.site-footer__list { display: none !important; }
.site-footer__inner > div:first-child > div:last-child {
  color: var(--mute);
  font-family: var(--font-body);
}
.site-footer__legal {
  display: inline-flex;
  align-items: center;
  gap: 18px;
  font-family: var(--font-body);
  font-size: 13px;
  color: var(--mute);
}
.site-footer__legal a {
  color: var(--mute);
  text-decoration: none;
  transition: color 160ms ease;
}
.site-footer__legal a:hover { color: var(--paper); }

/* ─── Inputs — softer ──────────────────────────────────────────────── */
.input {
  height: 46px;
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.025);
  border: 1px solid var(--border-soft);
  font-family: var(--font-body);
  font-size: 14.5px;
}
.input:focus {
  border-color: var(--acid);
  background: rgba(196, 169, 255, 0.04);
  box-shadow: 0 0 0 4px var(--acid-glow);
}

.field-label {
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--mute);
}
.field-label::before { display: none; }

/* ─── Eyebrow — less aggressive ────────────────────────────────────── */
.eyebrow {
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.02em;
  text-transform: none;
  color: var(--mute);
}

/* ─── Pills / chips / tags ─────────────────────────────────────────── */
.pill {
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  padding: 6px 12px;
}
.chip {
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
}
.tag {
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  padding: 4px 10px;
}
.tag--image { background: rgba(244, 154, 194, 0.10); color: var(--magenta); border-color: rgba(244, 154, 194, 0.30); }
.tag--ok    { background: rgba(157, 213, 181, 0.12); color: var(--emerald); border-color: rgba(157, 213, 181, 0.30); }

/* ─── Hero title — drop the giant serif italic ─────────────────────── */
.hero__title {
  font-family: var(--font-body);
  font-weight: 700;
  font-style: normal;
  font-size: clamp(42px, 8vw, 88px);
  line-height: 1.05;
  letter-spacing: -0.028em;
}
.hero__title .it { font-style: normal; }
.hero__title .em {
  color: var(--acid);
  position: relative;
}
.hero__title .em::after { display: none; }
.hero__lede {
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
}

.hero__num { display: none; }   /* hide editorial "§ 01 / GATEWAY" label */
.hero__stat-label { font-family: var(--font-body); font-weight: 500; font-size: 12px; letter-spacing: 0; text-transform: none; }

/* Hero stat numbers — drop from 700 to 500 so they feel reportive,
   not brutalist. Boba uses a similar light-display weight for stats. */
.hero__stat-value {
  font-family: var(--font-body);
  font-weight: 500;
  font-style: normal;
  font-size: 32px;
  letter-spacing: -0.022em;
  color: var(--paper);
}
.hero__stat-value .small {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 13px;
  color: var(--mute);
}

/* Stats strip — softer card shell instead of edge-only borders. */
.hero__stats {
  margin-top: 48px;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  background: var(--surface-1);
  overflow: hidden;
}
.hero__stat {
  border-right: 1px solid var(--border-soft);
}
.hero__stat:last-child { border-right: none; }

/* The leftmost stat (live latency) gets a pulsing dot in front of the
   label so visitors register that the number is being measured live,
   not stamped at build time. Updates every 5 minutes via live_stats. */
.hero__stat-label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.hero__stat-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--emerald);
  box-shadow: 0 0 0 0 rgba(157, 213, 181, 0.55);
  animation: hero-stat-dot-pulse 2.2s ease-out infinite;
}
@keyframes hero-stat-dot-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(157, 213, 181, 0.55); }
  70%  { box-shadow: 0 0 0 7px rgba(157, 213, 181, 0); }
  100% { box-shadow: 0 0 0 0 rgba(157, 213, 181, 0); }
}
.hero__stat-pending {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 18px;
  color: var(--mute);
  letter-spacing: -0.005em;
}

/* ─── Landing hero — boba-mode centering + cat mascot card ──────────
   When the index page sets `.hero--boba`, the section becomes a
   flex-column centered layout with the mascot card above the title.
   Inner pages (panel, billing, etc.) don't get this — they use the
   default left-aligned hero treatment. */
.hero--boba {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding-top: 56px;
}
.hero--boba .hero__top {
  justify-content: center;
  margin-bottom: 22px;
}
.hero--boba .hero__title {
  text-align: center;
  max-width: 14ch;
}
.hero--boba .hero__lede {
  text-align: center;
  margin-left: auto;
  margin-right: auto;
}
.hero--boba .hero__cta {
  justify-content: center;
}
.hero--boba .hero__stats {
  width: 100%;
}

/* Referral teaser under the CTA. Brand acid palette (same lavender as
   the primary CTA, nav__link--cta, hero__title em) — outline + tinted
   bg instead of solid fill so it visually steps below the CTA without
   leaving the brand colour. */
.hero__bonus-wrap {
  margin-top: 18px;
  text-align: center;
}
.hero__bonus {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 8px 16px;
  border-radius: 999px;
  background: rgba(196, 169, 255, 0.08);
  border: 1px solid rgba(196, 169, 255, 0.28);
  color: var(--acid);
  font-size: 13.5px;
  font-weight: 500;
  text-decoration: none;
  transition: background 160ms ease, border-color 160ms ease, transform 160ms ease;
}
.hero__bonus:hover {
  background: rgba(196, 169, 255, 0.14);
  border-color: rgba(196, 169, 255, 0.45);
  transform: translateY(-1px);
}
.hero__bonus-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--acid);
  flex-shrink: 0;
}

.hero__mascot {
  width: 96px;
  height: 96px;
  margin-bottom: 28px;
  border-radius: 22px;
  background: var(--acid);
  display: grid;
  place-items: center;
  box-shadow:
    0 18px 60px -16px rgba(196, 169, 255, 0.45),
    0 4px 14px -6px rgba(196, 169, 255, 0.30);
  transition: transform 0.3s var(--ease-out);
}
.hero__mascot:hover {
  transform: rotate(-3deg) translateY(-2px);
}
.hero__mascot svg,
.hero__mascot img {
  width: 80px;
  height: 80px;
  object-fit: contain;
  display: block;
}

/* ─── Invites grid — two columns on desktop, stack on mobile ──────── */
.invites-grid {
  display: grid;
  grid-template-columns: 1.5fr 1fr;
  gap: 28px;
}
@media (max-width: 720px) {
  .invites-grid {
    grid-template-columns: 1fr;
    gap: 18px;
  }
  /* Inline-style grid for the stat sidebar (two stat cards) was set up
     for the right column on desktop; on mobile it can stay 1-col. */
  .invites-grid > div:last-child {
    grid-template-columns: 1fr 1fr;
    display: grid;
    gap: 12px !important;
  }
}
@media (max-width: 480px) {
  .invites-grid > div:last-child {
    grid-template-columns: 1fr;
  }
}

/* Mobile copybox — referral link box was wrapping by single letter
   into a thin column on phones because the parent grid + the inner
   long URL kept fighting for width. Stack vertically: full-width
   link box on top, full-width copy button below. */
@media (max-width: 480px) {
  .copybox {
    flex-direction: column;
    align-items: stretch;
  }
  .copybox .text {
    word-break: break-all;
    overflow-wrap: anywhere;
    font-size: 12px;
  }
  .copybox .btn { width: 100%; }
}

/* ─── Page headings — keep big but lose the editorial italic ──────── */
.page-h1 {
  font-family: var(--font-body) !important;
  font-style: normal !important;
  font-weight: 700;
  letter-spacing: -0.025em;
  /* Russian compound words ("Двухфакторная", "Кабина оператора") wider
     than the viewport at 56px caused right-edge clipping. clamp lets
     the heading breathe down to 28 px on the narrowest phones, and
     hyphens lets browsers split the word where a clip would be worse
     than a hyphen. */
  font-size: clamp(28px, 9vw, 56px);
  line-height: 1.04;
  word-break: break-word;
  overflow-wrap: anywhere;
  hyphens: auto;
}
.page-sub {
  font-family: var(--font-body);
  color: var(--paper-soft);
  /* Same overflow safety for sub-headlines (the "open auth app…" copy
     was getting clipped at the same width). */
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* `<span class="it">` is used across hero / page / numcard headings as
   an "emphasis" mark — originally rendered in italic Instrument Serif.
   Globally drop the italic and tint it lavender so it still reads as
   accent without the editorial slant. */
.it {
  font-style: normal !important;
  color: var(--acid);
}

/* ─── Pull-quote — restrained sans, lavender bar ───────────────────── */
.pullquote {
  font-family: var(--font-body);
  font-style: normal;
  font-weight: 500;
  font-size: clamp(22px, 3vw, 32px);
  line-height: 1.3;
  letter-spacing: -0.012em;
  border-left: 3px solid var(--acid);
  padding-left: 22px;
}
.pullquote .quotemark { display: none; }

/* ─── Numcard — drop italic ────────────────────────────────────────── */
.numcard {
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
}
.numcard__no {
  font-family: var(--font-body);
  font-weight: 800;
  font-style: normal;
  font-size: 28px;
  color: var(--faint);
}
.numcard__title {
  font-family: var(--font-body);
  font-weight: 600;
  font-style: normal;
  font-size: 18px;
  letter-spacing: -0.012em;
}
.numcard__body { font-size: 14px; }

/* ─── Data table ───────────────────────────────────────────────────── */
.data-wrap {
  border-radius: var(--radius-lg);
  background: var(--surface-1);
}
.data-table thead th {
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: none;
  color: var(--mute);
}
.data-table tbody tr:hover { background: rgba(196, 169, 255, 0.04); }

.data-table__id   { font-family: var(--font-mono); font-size: 12.5px; }
.data-table__meta { font-family: var(--font-body); font-size: 12.5px; color: var(--mute); }

/* Compact variant for dense admin lists (e.g. /admin/models) — tighter
   vertical rhythm and a narrow action column that doesn't push buttons
   onto multiple lines. */
.data-table--compact thead th,
.data-table--compact tbody td {
  padding-top: 8px;
  padding-bottom: 8px;
}
.data-table--compact tbody tr + tr td { border-top: 1px solid var(--border-soft); }

/* ─── Stat / hero numbers ──────────────────────────────────────────── */
.stat-big {
  font-family: var(--font-body) !important;
  font-style: normal !important;
  font-weight: 700;
  letter-spacing: -0.02em;
}

/* ─── Code blocks — kept mono, softer borders ──────────────────────── */
pre.code-block, .code-card {
  border-radius: var(--radius);
  border-color: var(--border-soft);
}

/* ─── Selection ────────────────────────────────────────────────────── */
::selection { background: var(--acid); color: #1A1A20; }

/* ─── Plan cards (billing) — pill-shaped buttons inside, soft borders */
.plan-card {
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
}
.plan-card--featured {
  border-color: rgba(196, 169, 255, 0.40);
}

/* ─── Chat sidebar / main — already soft, just unify radii ─────────── */
.chat-side,
.chat-main {
  background: var(--surface-1);
  border-color: var(--border-soft);
  border-radius: var(--radius-lg);
}
.chat-msg--user {
  background: rgba(196, 169, 255, 0.10);
  border-color: rgba(196, 169, 255, 0.22);
}
.chat-msg--asst {
  background: rgba(255, 255, 255, 0.025);
}
.chat-toolbar select {
  border-radius: 8px;
}

/* ─── Provider picker (billing) — softer ──────────────────────────── */
.provider-pick {
  border-radius: var(--radius);
}
.provider-pick__name {
  font-family: var(--font-body);
  font-weight: 600;
}

/* ─── Misc cleanups: places using --font-display directly ─────────── */
[style*="--font-display"] {
  font-family: var(--font-display) !important;
  font-style: normal !important;
}

/* ─── Alerts ────────────────────────────────────────────────────── */
.alert-ok,
.alert-error {
  border-radius: var(--radius);
}

/* Editorial § markers were noise in production — hidden globally.
   Section titles still render via `.section-rule__title`. */
.section-rule__num { display: none; }

/* ─── Legal pages (Terms / Privacy) ─────────────────────────────────
   Read-only documents — narrow column, generous line-height, muted
   color hierarchy. No grids, no cards. */
.legal {
  max-width: 720px;
  margin: 48px auto 80px;
  padding: 0 4px;
}
.legal__head {
  padding-bottom: 22px;
  margin-bottom: 8px;
  border-bottom: 1px solid var(--border-soft);
}
.legal__head .page-h1 {
  margin: 12px 0 8px;
}
.legal__head .page-sub {
  color: var(--mute);
  font-size: 13.5px;
}
.legal__section {
  padding: 18px 0;
  border-bottom: 1px dashed var(--border-soft);
}
.legal__section:last-child {
  border-bottom: none;
}
.legal__h2 {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 17px;
  letter-spacing: -0.01em;
  color: var(--paper);
  margin: 0 0 12px;
}
.legal p {
  margin: 0 0 12px;
  color: var(--paper-soft);
  font-size: 14.5px;
  line-height: 1.7;
}
.legal p:last-child {
  margin-bottom: 0;
}
.legal a {
  color: var(--acid);
  text-decoration: underline;
  text-decoration-color: rgba(196, 169, 255, 0.4);
  text-underline-offset: 2px;
}
.legal a:hover {
  text-decoration-color: var(--acid);
}
.legal strong {
  color: var(--paper);
  font-weight: 600;
}
.legal__list {
  margin: 0 0 12px;
  padding: 0;
  list-style: none;
}
.legal__list li {
  position: relative;
  padding: 6px 0 6px 22px;
  color: var(--paper-soft);
  font-size: 14.5px;
  line-height: 1.65;
}
.legal__list li::before {
  content: "";
  position: absolute;
  left: 4px;
  top: 14px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--acid);
  opacity: 0.6;
}

/* ─── Auth pages (login / register) — Boba-style centered card ────
   The page itself drops the site header/footer/ticker (see the {% block %}
   overrides in login.html / register.html). What's left is a single
   floating card in the middle of the viewport plus a "Back" pill in the
   top-left, matching the boba.xyz / Boba DeFi auth flow. */
body.page-auth main {
  padding: 0;
}
body.page-auth main .shell {
  max-width: none;
  margin: 0;
  padding: 0;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.auth-back {
  position: fixed;
  top: 22px;
  left: 22px;
  z-index: 10;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 9px 16px 9px 12px;
  border-radius: 999px;
  background: rgba(30, 30, 38, 0.7);
  border: 1px solid var(--border-soft);
  color: var(--paper-soft);
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 500;
  text-decoration: none;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  transition: background 180ms ease, border-color 180ms ease, color 180ms ease;
}
.auth-back:hover {
  background: rgba(40, 40, 50, 0.85);
  border-color: var(--border-hot);
  color: var(--paper);
}
.auth-back svg {
  flex: 0 0 auto;
}

.auth-card {
  width: 100%;
  max-width: 420px;
  margin: 48px auto;
  padding: 36px 32px 28px;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-xl);
  background: var(--surface-1);
  text-align: center;
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.02) inset,
    0 20px 60px -20px rgba(0, 0, 0, 0.55);
}

.auth-card__logo {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 72px;
  height: 72px;
  margin: 0 auto 18px;
  border-radius: 22px;
  background: var(--acid);
  box-shadow: 0 8px 32px -8px var(--acid-glow);
}
.auth-card__logo svg,
.auth-card__logo img {
  width: 56px;
  height: 56px;
  object-fit: contain;
  display: block;
}

/* Resend block on the email code-step. Lives below the main code-entry
   form, hosts its own (small) Turnstile widget + a single ghost button
   whose label flips between "Resend in 1:43" and "Resend the code"
   when the JS countdown elapses. Visually quiet so the primary
   "Sign in" CTA above stays the focal point. */
.auth-resend {
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px dashed var(--border-soft);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.auth-resend__form {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  width: 100%;
}
.auth-resend__captcha {
  display: flex;
  justify-content: center;
}
.auth-resend__btn {
  height: 34px;
  padding: 0 16px;
  border-radius: 999px;
  font-size: 12.5px;
  font-weight: 500;
  letter-spacing: -0.005em;
  color: var(--paper-soft);
  background: transparent;
  border: 1px solid var(--border-soft);
  cursor: pointer;
  transition: color 140ms, border-color 140ms, background 140ms;
}
.auth-resend__btn:hover:not(:disabled) {
  color: var(--acid);
  border-color: rgba(196, 169, 255, 0.36);
  background: rgba(196, 169, 255, 0.06);
}
.auth-resend__btn:disabled {
  cursor: not-allowed;
  opacity: 0.55;
  font-family: var(--font-mono);
  font-size: 11.5px;
  letter-spacing: 0.02em;
}

.auth-card__title {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 22px;
  line-height: 1.2;
  letter-spacing: -0.02em;
  color: var(--paper);
  margin: 0;
}

.auth-card__form {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin-top: 24px;
  text-align: left;
}

.auth-card__field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.auth-card__field .input {
  width: 100%;
}

.auth-card__hint {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--mute-soft);
  letter-spacing: 0.02em;
}

.auth-card__submit {
  width: 100%;
  height: 48px;
  justify-content: center;
  font-size: 15px;
  font-weight: 600;
  margin-top: 4px;
}

.auth-card__alt {
  margin: 18px 0 0;
  font-size: 13px;
  color: var(--mute);
  text-align: center;
}
.auth-card__alt a {
  color: var(--acid);
  font-weight: 600;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color 160ms ease;
}
.auth-card__alt a:hover {
  border-bottom-color: var(--acid);
}

.auth-card__tos {
  margin: 18px -4px 0;
  font-size: 11px;
  line-height: 1.55;
  color: var(--mute-soft);
  text-align: center;
}
.auth-card__tos a {
  color: var(--mute);
  text-decoration: underline;
  text-decoration-color: rgba(196, 169, 255, 0.35);
  text-underline-offset: 2px;
}
.auth-card__tos a:hover {
  color: var(--paper-soft);
}

.auth-card__referral {
  margin-top: 18px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  border-radius: 999px;
  background: rgba(142, 213, 220, 0.08);
  border: 1px solid rgba(142, 213, 220, 0.28);
  color: var(--cyan);
  font-size: 12px;
  font-weight: 500;
}
.auth-card__referral .pill__dot {
  background: var(--cyan);
}

.auth-foot {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 22px;
  text-align: center;
  font-size: 12.5px;
  color: var(--mute-soft);
  pointer-events: none;
}
.auth-foot a,
.auth-foot span {
  display: inline-block;
  margin: 0 12px;
  pointer-events: auto;
}
.auth-foot a {
  color: var(--mute);
  text-decoration: none;
}
.auth-foot a:hover {
  color: var(--paper-soft);
}

@media (max-width: 560px) {
  .auth-back {
    top: 14px;
    left: 14px;
    padding: 7px 12px 7px 10px;
    font-size: 12px;
  }
  .auth-card {
    margin: 80px 16px 80px;
    padding: 28px 22px 22px;
  }
  .auth-card__title {
    font-size: 20px;
  }
  .auth-foot {
    bottom: 14px;
  }
}

/* ─── Language swap transition ─────────────────────────────────────
   While lang-switch.js is fetching the new locale's HTML and swapping
   <main>/<nav>/<footer> in place, fade main+footer briefly so the swap
   doesn't feel jarring. The lang switcher itself stays at full opacity
   so you can still see which button you pressed. */
body.lang-swapping main,
body.lang-swapping .site-footer {
  transition: opacity 140ms ease;
  opacity: 0.55;
  pointer-events: none;
}

/* ─── Provider viewer (landing) ────────────────────────────────────
   Replaces the dense "models & pricing" table on /. Three tabs across
   the top (logos + name), and a panel beneath that lists the available
   model IDs for the selected provider. No prices, no type tags —
   pricing lives behind auth in /panel. */
[x-cloak] { display: none !important; }

.provider-viewer {
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  background: var(--surface-1);
  overflow: hidden;
}

.provider-tabs {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  border-bottom: 1px solid var(--border-soft);
  background: rgba(255, 255, 255, 0.012);
}

.provider-tab {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 22px 18px;
  background: transparent;
  border: 0;
  border-right: 1px solid var(--border-soft);
  color: var(--mute);
  font-family: var(--font-body);
  font-size: 15px;
  font-weight: 500;
  cursor: pointer;
  transition: color 160ms ease, background 160ms ease;
  position: relative;
}
.provider-tab:last-child { border-right: 0; }
.provider-tab:hover { color: var(--paper-soft); background: rgba(255, 255, 255, 0.02); }
.provider-tab:focus-visible {
  outline: 2px solid var(--acid-glow);
  outline-offset: -2px;
}

.provider-tab--active {
  color: var(--paper);
  background: rgba(196, 169, 255, 0.06);
}
.provider-tab--active::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  background: var(--acid);
}

.provider-tab__logo {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  color: var(--mute);
  transition: color 160ms ease;
}
.provider-tab__logo svg {
  width: 100%;
  height: 100%;
}
.provider-tab--active .provider-tab__logo--openai    { color: #FFFFFF; }
.provider-tab--active .provider-tab__logo--anthropic { color: #D97757; }
.provider-tab--active .provider-tab__logo--gemini    { color: #8BB8FF; }

.provider-tab__name {
  letter-spacing: -0.01em;
}

.provider-panel {
  padding: 22px 22px 26px;
}

/* Alpine x-transition: enter classes for the panel as a whole. The
   surrounding <template x-if> un/remounts the panel each time `tab`
   changes — that's what restarts the per-card staggered fade-in. */
.provider-panel--enter {
  transition: opacity 240ms ease, transform 240ms ease;
}
.provider-panel--enter-start {
  opacity: 0;
  transform: translateY(6px);
}
.provider-panel--enter-end {
  opacity: 1;
  transform: translateY(0);
}

.model-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 12px;
}

/* Each model card animates in with a small fade-up, and uses :nth-child
   to stagger so the rows arrive like a wave rather than all at once. */
.model-card {
  position: relative;
  padding: 16px 18px 14px;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.012);
  transition: transform 200ms ease, border-color 200ms ease, background 200ms ease;
  animation: model-card-in 360ms cubic-bezier(0.22, 1, 0.36, 1) both;
  will-change: transform, opacity;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.model-card:hover {
  transform: translateY(-2px);
  border-color: rgba(196, 169, 255, 0.32);
  background: rgba(196, 169, 255, 0.04);
}

/* Head row holds the model name; right-padding reserves space for the
   pulsing online dot in the corner. The ×N chip moved into the meta
   block below so the name has room to breathe. */
.model-card__head-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding-right: 18px;
  min-width: 0;
}
.model-card__name {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 14.5px;
  line-height: 1.2;
  color: var(--paper);
  letter-spacing: -0.01em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* Meta block — two stacked rows, one per fact (rate, vs-API). Sits
   between the name and the id/copy row. Each row is a "label: value"
   pair where the value is the bright chip (coloured mult or emerald
   discount), so the eye picks them out without scanning. */
.model-card__meta {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.model-card__meta-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  font-family: var(--font-mono);
  font-size: 11.5px;
  letter-spacing: 0.02em;
}
.model-card__meta-label {
  color: var(--mute);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 10.5px;
}

/* Token-rate multiplier chip. Tells the user how aggressively tokens
   are debited for THIS model relative to the cheapest text model
   (baseline ×1). Colour-ramped by appetite so the eye picks out
   premium burners instantly:
     cheap   (×<1)        green
     base    (×1..2)      neutral (paper on tinted bg)
     mid     (×2..5)      amber
     high    (×5..10)     peach
     premium (×10+)       magenta
     image                muted lavender (no per-token rate) */
.model-card__mult {
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: 0.01em;
  color: var(--paper);
  padding: 3px 9px;
  border: 1px solid var(--border-soft);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.04);
  white-space: nowrap;
  line-height: 1.2;
}

.model-card__mult--cheap {
  color: var(--emerald);
  border-color: rgba(157, 213, 181, 0.45);
  background: rgba(157, 213, 181, 0.12);
}
.model-card__mult--base {
  color: var(--paper);
  border-color: rgba(196, 169, 255, 0.35);
  background: rgba(196, 169, 255, 0.08);
}
.model-card__mult--mid {
  color: #F4D58D;
  border-color: rgba(244, 213, 141, 0.42);
  background: rgba(244, 213, 141, 0.10);
}
.model-card__mult--high {
  color: #F49AC2;
  border-color: rgba(244, 154, 194, 0.45);
  background: rgba(244, 154, 194, 0.12);
}
.model-card__mult--premium {
  color: #FF7DAA;
  border-color: rgba(255, 125, 170, 0.55);
  background: rgba(255, 125, 170, 0.16);
  text-shadow: 0 0 8px rgba(255, 125, 170, 0.25);
}
.model-card__mult--image {
  color: var(--mute);
  font-style: italic;
  letter-spacing: 0.04em;
}

/* Emerald percentage shown in the "vs API" meta row. Standalone class
   so we can reuse it without dragging in the old savings flexbox. */
.model-card__savings-value {
  font-weight: 700;
  color: var(--emerald);
}

.model-card__id-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-top: 8px;
  border-top: 1px dashed var(--border-soft);
}
.model-card__id {
  flex: 1;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--mute);
  letter-spacing: -0.005em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  user-select: all;
}

/* Copy button — square mono affordance. Two SVG icons stacked, swap
   via .is-copied class set briefly by the inline click handler. */
.model-card__copy {
  position: relative;
  width: 26px;
  height: 26px;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid var(--border-soft);
  border-radius: 7px;
  background: rgba(255, 255, 255, 0.02);
  color: var(--mute);
  cursor: pointer;
  transition: color 160ms ease, border-color 160ms ease, background 160ms ease;
}
.model-card__copy:hover {
  color: var(--paper);
  border-color: rgba(196, 169, 255, 0.45);
  background: rgba(196, 169, 255, 0.08);
}
.model-card__copy:focus-visible {
  outline: 2px solid rgba(196, 169, 255, 0.55);
  outline-offset: 2px;
}
.model-card__copy-icon {
  position: absolute;
  width: 14px;
  height: 14px;
  transition: opacity 160ms ease, transform 160ms ease;
}
.model-card__copy-icon--done {
  opacity: 0;
  transform: scale(0.7);
  color: var(--emerald);
}
.model-card__copy.is-copied {
  color: var(--emerald);
  border-color: rgba(157, 213, 181, 0.45);
  background: rgba(157, 213, 181, 0.08);
}
.model-card__copy.is-copied .model-card__copy-icon--idle {
  opacity: 0;
  transform: scale(0.7);
}
.model-card__copy.is-copied .model-card__copy-icon--done {
  opacity: 1;
  transform: scale(1);
}

/* Tiny online dot in the top-right corner of every card — pulsing
   emerald, matches the hero stat dot. Signals "this model is callable
   right now" without spelling it out. */
.model-card__online {
  position: absolute;
  top: 16px;
  right: 16px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--emerald);
  box-shadow: 0 0 0 0 rgba(157, 213, 181, 0.55);
  animation: model-card-online-pulse 2.4s ease-out infinite;
}
@keyframes model-card-online-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(157, 213, 181, 0.55); }
  70%  { box-shadow: 0 0 0 6px rgba(157, 213, 181, 0); }
  100% { box-shadow: 0 0 0 0 rgba(157, 213, 181, 0); }
}

@keyframes model-card-in {
  0% {
    opacity: 0;
    transform: translateY(10px) scale(0.985);
  }
  100% {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

/* Staggered delays — fixed schedule rather than var(--i) inline so the
   template stays clean. After the 10th card we cap the delay so larger
   model rosters don't trail in for an awkwardly long time. */
.model-card:nth-child(1)  { animation-delay:  20ms; }
.model-card:nth-child(2)  { animation-delay:  60ms; }
.model-card:nth-child(3)  { animation-delay: 100ms; }
.model-card:nth-child(4)  { animation-delay: 140ms; }
.model-card:nth-child(5)  { animation-delay: 180ms; }
.model-card:nth-child(6)  { animation-delay: 220ms; }
.model-card:nth-child(7)  { animation-delay: 260ms; }
.model-card:nth-child(8)  { animation-delay: 300ms; }
.model-card:nth-child(9)  { animation-delay: 340ms; }
.model-card:nth-child(n+10) { animation-delay: 380ms; }

/* Respect reduced-motion users — no slide, no stagger, just instant. */
@media (prefers-reduced-motion: reduce) {
  .model-card,
  .provider-panel--enter {
    animation: none !important;
    transition: none !important;
  }
  .model-card__online {
    animation: none;
  }
}

@media (max-width: 640px) {
  .provider-tab {
    padding: 16px 10px;
    font-size: 13px;
    gap: 8px;
  }
  .provider-tab__logo {
    width: 18px;
    height: 18px;
  }
  .provider-panel {
    padding: 16px 14px 20px;
  }
  .model-grid {
    grid-template-columns: 1fr;
    gap: 10px;
  }
}

/* ──────────────────────────────────────────────────────────────────
   Mobile responsive — landing page focus.

   Adds two breakpoints on top of the existing ones (560/640/760/880):

   • 720px and below — tablet portrait + the larger half of phones.
     Tightens shell + header + footer padding, drops hero top
     padding so the boba mascot sits in the visible viewport on
     first paint, and trims the code-card chrome.

   • 480px and below — phone-only (320–414). The narrow ones.
     Collapses the Support nav label to icon-only so the top bar
     fits, drops the noisy `·moe` half of the brand mark, shrinks
     the mascot, tightens code-card paddings, and lets
     the footer legal row wrap to two rows when needed.

   Both breakpoints live here (theme layer) instead of app.css so
   they override the editorial defaults consistently. ────────── */

@media (max-width: 720px) {
  .shell { padding: 0 20px; }

  .site-header__inner { padding: 0 16px; height: 56px; gap: 12px; }
  .site-footer { padding: 32px 20px 48px; }

  .hero--boba { padding-top: 32px; }
  .hero { padding: 32px 0 16px; }
  .hero__lede { margin-top: 22px; }
  .hero__cta { margin-top: 26px; }

  .code-card__head { padding: 10px 14px; }
  pre.code-block { padding: 14px 16px; font-size: 12.5px; }

  .pullquote { padding-left: 18px; font-size: clamp(20px, 4.4vw, 28px); }

  .section-rule { margin: 36px 0 18px; }
  .section-rule__title { font-size: 16px; }
}

@media (max-width: 480px) {
  .shell { padding: 0 16px; }

  .site-header__inner { padding: 0 12px; gap: 6px; }
  /* Nav rules are no longer relevant here — at ≤880px the entire nav
     is an off-canvas drawer with its own full-width pill links (see
     "Hamburger drawer" block near .site-header). Keep brand-related
     rules only, because brand stays in the header row. */

  /* Brand mark already signals the site — drop the trailing ".moe"
     text on the narrowest viewports so the nav has room to breathe. */
  .brand { gap: 8px; }
  .brand__mark { width: 32px; height: 32px; border-radius: 9px; }
  .brand__mark img,
  .brand__mark svg { width: 28px; height: 28px; }
  .brand__name { font-size: 15px; }
  .brand__name .ext { display: none; }
  .brand__name .dot { display: none; }

  .hero--boba { padding-top: 20px; }
  .hero__mascot { width: 80px; height: 80px; margin-bottom: 18px; border-radius: 18px; }
  .hero__mascot img,
  .hero__mascot svg { width: 64px; height: 64px; }
  .hero__lede { font-size: 15px; line-height: 1.55; margin-top: 18px; }
  .hero__cta { margin-top: 22px; }
  .hero__cta .btn { width: 100%; justify-content: center; }

  .code-card__head { padding: 9px 12px; }
  .code-card__title { font-size: 10px; letter-spacing: 0.14em; }
  pre.code-block { padding: 12px 14px; font-size: 12px; }

  .chip { padding: 6px 10px; font-size: 11px; letter-spacing: 0.06em; }
  .chip-grid { gap: 6px; margin-top: 18px; }

  .pullquote { padding-left: 14px; border-left-width: 2px; }

  .site-footer { padding: 24px 16px calc(36px + var(--safe-bottom)); }
  .site-footer__inner { flex-wrap: wrap; gap: 12px; }
  .site-footer__legal { flex-wrap: wrap; gap: 10px 14px; font-size: 10.5px; }
}

/* ─── Ultra-narrow viewports (iPhone SE / folded Galaxy / 320px) ──────
   Pull padding down once more and force long monospace tokens (API
   keys, model IDs) to wrap so they can't push the column wider than
   the device. word-break:break-word here is scoped to <pre>/<code>
   inside the panel — body copy keeps its normal wrapping. */
@media (max-width: 360px) {
  .shell { padding: 0 12px; }

  pre, code {
    word-break: break-word;
    overflow-wrap: anywhere;
  }

  /* Tables go from "fits the column" to "scroll horizontally inside
     a card" — losing some text is better than blowing out the page. */
  .keys-panel table,
  .usage-panel table,
  .tx-panel table { display: block; overflow-x: auto; }
}

/* ─── Terminal demo (landing) ───────────────────────────────────────
   Replaces the synthetic RPS live-feed. A single macOS-style
   terminal card with a typewriter cycling through 6 SDK snippets,
   one language at a time. JS lives in /static/js/terminal-demo.js.

   Body height is fixed (320 / 240 px) so the layout doesn't shift
   between languages with different line counts. White-space:pre
   keeps the typed indentation visible without forcing word-wrap. */
.terminal-demo {
  margin: 22px 0 0;
}

.term {
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  background: var(--surface-1);
  overflow: hidden;
}

.term__chrome {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 18px;
  border-bottom: 1px solid var(--border-soft);
  background: var(--surface-2);
}
.term__dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  flex-shrink: 0;
}
.term__dot--r { background: #FF6058; }
.term__dot--y { background: #FFBD2E; }
.term__dot--g { background: #28C940; }
.term__label {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.18em;
  color: var(--mute);
  text-transform: uppercase;
}

.term__body {
  position: relative;
  margin: 0;
  padding: 22px 24px;
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.6;
  color: var(--paper-soft);
  background: var(--ink-soft);
  height: 380px;
  overflow: hidden;
  white-space: pre;
  word-break: normal;
}
.term__body code {
  display: block;
  white-space: pre;
  font: inherit;
  color: inherit;
}

.term__cursor {
  display: inline-block;
  width: 8px;
  height: 14px;
  margin-left: 2px;
  vertical-align: text-bottom;
  background: var(--paper);
  animation: term-cursor-blink 1s steps(2) infinite;
}
@keyframes term-cursor-blink {
  0%, 49%   { opacity: 1; }
  50%, 100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .term__cursor { animation: none; opacity: 1; }
}

/* Line-level syntax tokens — span wrappers injected by the
   typewriter. Each line is one span with one type. Vocabulary
   mirrors the JS array's `type` field (see terminal-demo.js). */
.tok-cmd     { color: var(--paper); }
.tok-user    { color: var(--paper); font-weight: 500; }
.tok-think   { color: var(--paper-soft); }
.tok-tool    { color: var(--acid); }
.tok-code    { color: var(--paper-soft); }
.tok-comment { color: var(--mute); font-style: italic; }
.tok-spin    { color: var(--magenta); }
.tok-fail    { color: var(--rose); }
.tok-ok      { color: var(--emerald); }

/* ─── Value pair (landing) ──────────────────────────────────────────
   Two equal cards directly under the terminal. Stays in the boba
   palette: tokens card uses --acid (lavender) icon, pay card uses
   --magenta. Stacks on narrow viewports. */
.pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 18px;
  margin-top: 18px;
}
.pair-card {
  padding: 24px 26px;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  background: var(--surface-1);
}
.pair-card__icon {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: 12px;
  background: var(--surface-2);
  margin-bottom: 14px;
}
.pair-card__icon svg {
  width: 22px;
  height: 22px;
}
.pair-card--tokens .pair-card__icon svg { color: var(--acid); }
.pair-card--pay    .pair-card__icon svg { color: var(--magenta); }

.pair-card__title {
  font-family: var(--font-body);
  font-size: 16px;
  font-weight: 600;
  color: var(--paper);
  margin: 0;
  letter-spacing: -0.005em;
}
.pair-card__body {
  margin: 6px 0 0;
  font-family: var(--font-body);
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--mute-soft);
}

@media (max-width: 760px) {
  .term__chrome { padding: 10px 14px; }
  .term__dot { width: 10px; height: 10px; }
  .term__label { font-size: 10px; }
  .term__body {
    padding: 16px 18px;
    font-size: 11.5px;
    height: 300px;
  }
  .pair { grid-template-columns: 1fr; gap: 14px; }
  .pair-card { padding: 20px 22px; }
}
