*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
/* The `hidden` attribute must always win. The UA rule [hidden]{display:none}
   has the same specificity as a class selector, so any component that sets a
   `display` on its class (e.g. .il-hero{display:grid}, .il-rev-head{display:flex})
   silently defeats `hidden`, and toggling el.hidden does nothing. This makes
   the attribute authoritative app-wide. Show an element by removing the
   attribute (el.hidden = false), never by leaving it on and overriding display. */
[hidden] { display: none !important; }
:root {
  --bg: #080B1A; --bg-center: #0A1628; --bg-edge: #020408;
  --bg-glass: rgba(10,22,40,0.6); --bg-card: rgba(255,255,255,0.03);
  /* Real-device Safari sometimes fails to composite backdrop-filter on a
     position:fixed full-screen overlay, leaving the raw --bg-glass alpha
     with no blur — a legible double-exposure over whatever's behind it.
     Mobile full-screen overlays (Otto Dock) fall back to fully opaque so
     they read solid regardless of whether blur composites — nothing is
     lost visually since a fully opaque layer has no "behind it" to blur
     in the first place. */
  --bg-glass-mobile-solid: rgba(10,22,40,1);
  --teal: #00D4FF; --teal-dim: rgba(0,212,255,0.15); --teal-glow: rgba(0,212,255,0.25);
  --on-teal: #080B1A;
  --purple: #9D6FE0; --purple-dim: rgba(157,111,224,0.15);
  /* 2026-04-30 — Blue Chips Mono. Coral tokens retained for backwards
     compat (hundreds of consumers across pages); values flipped to
     pure cyan #00FFFF. Hover uses a brighter cyan tint, glow uses
     the same alpha at cyan. --text-action also flipped — it was an
     orange CTA-text token in the same family. */
  --cyan: #00FFFF; --cyan-hover: #7CFFFF; --cyan-glow: rgba(0,255,255,0.35);
  /* Cyan tier palette (added 2026-05-19 with THEME_RULES.md). Every
     hard-coded rgba(0,255,255,*) in component CSS routes through one of
     these tokens — they have light-mode counterparts below so every
     surface adapts. Lower numbers = fainter alpha. */
  --cyan-bg-faint: rgba(0,255,255,0.06);
  --cyan-bg-soft: rgba(0,255,255,0.10);
  --cyan-bg-hover: rgba(0,255,255,0.18);
  --cyan-border-faint: rgba(0,255,255,0.22);
  --cyan-border: rgba(0,255,255,0.32);
  --cyan-border-strong: rgba(0,255,255,0.55);
  --cyan-glow-soft: rgba(0,255,255,0.25);
  --cyan-glow-strong: rgba(0,255,255,0.50);
  /* Softened from pure rgba(0,255,255,0.85), 2026-07-21. Pure saturated
     cyan set as small TEXT (not a border/background/button fill) against
     a near-black page is a documented eye-strain pattern — the eye's
     lens focuses different wavelengths at different depths, so a fully
     saturated color at high contrast against black makes small text
     glyphs genuinely harder to focus on, felt as text that "vibrates"
     or is uncomfortable to read, not just an aesthetic complaint.
     rgba(124,255,255,...) is the same hue, already used elsewhere in
     this system as `--cyan-hover` (#7CFFFF) — still unmistakably cyan,
     still the one accent color per Blue Chips Mono, just the variant
     already designated for contexts that need the color to sit
     comfortably rather than glow at full saturation. Borders, button
     fills, and focus rings keep pure --cyan; only text color changes. */
  --cyan-text-action: rgba(124,255,255,0.92);
  /* Text color to use ON a cyan-filled surface (button bg = var(--cyan),
     text on it = var(--on-cyan)). Light mode flips to white because
     light-mode cyan resolves to dark teal. */
  --on-cyan: #0a0a0f;
  /* Error palette — used by inline error states (form errors, chat
     errors, dock error messages). Both modes pass AA. */
  --error-bg: rgba(255, 80, 80, 0.08);
  --error-border: rgba(255, 80, 80, 0.32);
  --error-text: rgba(255, 200, 200, 0.92);
  --error-dot: #FF6B6B;
  /* Warn (amber) palette — added 2026-05-28 for dirty-draft, big-rewrite,
     "Watch" quality band, and general non-error caution states. Replaces
     hand-rolled per-page amber palettes (#FFB84D, #a85d00, #c4760a, etc).
     Both modes pass AA. */
  --warn-bg: rgba(255, 184, 77, 0.10);
  --warn-border: rgba(255, 184, 77, 0.32);
  --warn-text: rgba(255, 210, 140, 0.92);
  --warn-dot: #FFB84D;
  /* Success (mint) palette — added 2026-05-28 for "Gold" quality band,
     publish-confirmed, and general positive feedback. Replaces hand-rolled
     #00FFB3, #00B894, #00997A literals. Both modes pass AA. */
  --success-bg: rgba(0, 255, 179, 0.08);
  --success-border: rgba(0, 255, 179, 0.30);
  --success-text: rgba(160, 255, 210, 0.92);
  --success-dot: #00FFB3;
  /* Shadow scale — added 2026-05-28 per UX_STANDARD §2. Stops inline
     `box-shadow: 0 0 24px var(--cyan-glow)` from being scattered across
     pages. Bioluminescent glow per UX_STANDARD §1.5 (color as life, not
     decoration). */
  --shadow-active-glow: 0 0 24px var(--cyan-glow);
  --shadow-card-hover: 0 4px 24px rgba(0, 0, 0, 0.40), 0 0 0 1px var(--cyan-border-faint);
  --shadow-focus-ring: 0 0 0 3px var(--cyan-bg-soft);
  /* Motion scale — added 2026-05-28 per UX_STANDARD §2 + §1.5. House easing
     `cubic-bezier(.2, .7, .2, 1)` is the de-facto curve already used in
     `sidebar.css` / `dashboard.css` — promoted to a token. Pages stop
     inlining `0.15s` / `0.2s` / `0.18s` etc. and reach for these. */
  --motion-fast: 120ms;
  --motion-base: 200ms;
  --motion-slow: 320ms;
  --motion-ease: cubic-bezier(.2, .7, .2, 1);
  /* Neutral muted surface tokens — for non-cyan tints (chat bubbles,
     input bg, dividers). */
  --surface-soft: rgba(255, 255, 255, 0.04);
  --surface-border: rgba(255, 255, 255, 0.10);
  --surface-border-faint: rgba(255, 255, 255, 0.06);
  --icon-muted: rgba(255, 255, 255, 0.32);
  /* Tooltips read as "system message" and stay dark in both themes —
     the bg/fg pair is identical across :root and [data-theme="light"]. */
  --tooltip-bg: rgba(20, 20, 28, 0.94);
  --tooltip-fg: rgba(255, 255, 255, 0.92);
  --mint: #00FFB3; --mint-dim: rgba(0,255,179,0.1);
  --white: #FFFFFF; --body: #B8C9D9; --muted: #6E8398;
  /* Legacy .app-nav bar (shared.js renderer). Was a hardcoded
     rgba(2,4,8,0.9) with NO light value, so in light mode dark text sat on a
     near-black bar at 1.95:1. Matches the newer topnav component's
     --tp-nav-bg so the two navs stay consistent. */
  --app-nav-bg: rgba(2, 4, 8, 0.9);
  /* Feature-story overlay. The card must be OPAQUE: it sits on a deliberately
     dark scrim, and a translucent card composites over that scrim in BOTH
     themes — which is why light mode put dark text on a near-black panel at
     1.06:1. Scrim stays dark in both themes on purpose (a scrim dims the page;
     it is not a surface). Tokenised rather than left as a literal per rule 1. */
  --fs-scrim: rgba(0, 0, 0, 0.85);
  --fs-card-bg: #141921;
  --border: rgba(0,212,255,0.12); --border-hover: rgba(0,212,255,0.35);
  --text-primary: rgba(255,255,255,0.95);
  --text-secondary: rgba(255,255,255,0.72);
  /* 0.45 → 0.65, 2026-07-21. Real-phone report: description-length
     copy (a full sentence, not a short label/caption) read as too
     dim on mobile. 0.45 sat at the bottom of the 40-60% range dark
     UIs like Linear use for this tier — appropriate for small
     labels/icons, not for sentence-length body copy, which is most
     of what actually uses this token across the app. First pass
     landed at 0.60 but a real before/after screenshot comparison
     showed it read as only marginally brighter — 0.65 is the value
     that actually reads clearly on a real screen. Still leaves a
     visible step below --text-secondary (0.72) so the primary/
     secondary/tertiary hierarchy stays intact. Computed contrast
     against the real page background (~#020408-#0A1628): comfortably
     clears WCAG AA's 4.5:1 for normal text with room to spare. */
  --text-tertiary: rgba(255,255,255,0.65);
  /* Same softening as --cyan-text-action above (see that comment for the
     eye-strain rationale) — pure #00FFFF as small readable text is a
     documented legibility/comfort issue on a near-black background. */
  --text-action: rgba(124,255,255,0.92);
  --card-bg: #141921;
  --card-bg-hover: #1a2230;
  --card-bg-featured: #1e2a3a;
  --tag-bg: rgba(255,255,255,0.08);
  --tag-border: rgba(255,255,255,0.12);
  /* Locked design invariant 2026-05-01 — see CLAUDE.md.
     Inter for everything; JetBrains Mono is loaded alongside on key
     surfaces for technical/numeric overlays when a monospace cue is
     warranted. Tabular-nums + lining-nums declared globally so
     big-number stats (rankings, citations, position) align in
     columns even when individual cards forget to opt in. */
  --font: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
/* Light mode token overrides (Blue Chips Mono, light variant).
   Pure #00FFFF cyan washes out on white — light mode swaps the accent
   token to a darker teal (#007A8E) so every consumer of var(--cyan)
   / var(--teal) / var(--text-action) automatically reads as a real
   color on white. --white stays a literal #FFFFFF (used by surfaces
   that are intentionally white, e.g. modal fills); body color is set
   directly in the light-mode body rule below. */
[data-theme="light"] {
  --bg: #F5F8FA; --bg-center: #FFFFFF; --bg-edge: #E2EAEF;
  --bg-glass: rgba(255,255,255,0.78); --bg-card: rgba(14,31,38,0.03);
  --bg-glass-mobile-solid: rgba(255,255,255,1);
  --teal: #007A8E; --teal-dim: rgba(0,122,142,0.10); --teal-glow: rgba(0,122,142,0.20);
  --on-teal: #FFFFFF;
  /* This legacy accent is retired (not part of Blue Chips Mono) but still
     consumed by a handful of dashboard-legacy.html spots. Rule 2 requires
     a light-mode pair regardless of legacy status — the original dark-mode
     hex already reads well against light backgrounds (~7:1), so it carries
     over unchanged; only the dark-mode value moved (brightened) to clear
     3:1 against the near-black default background. */
  --purple: #7B2FBE; --purple-dim: rgba(123,47,190,0.12);
  /* #007A8E -> #006D7E (2026-08-07): same hue, 11% darker. Cyan text on a cyan
     tint failed AA in light mode across the WHOLE family — --cyan-bg-faint
     4.36, --cyan-bg-soft 4.13, --cyan-bg-hover 3.80, all against a 4.5
     minimum. Dark mode was never affected (10-14:1). It is a class, not an
     instance: every `color: var(--cyan)` on a `background: var(--cyan-bg-*)`
     inherited it, including the active tab on the LOGIN page, which every
     merchant sees before they see anything else.
     THEME_RULES rule 8: "If a new token can't hit AA in one mode, the token is
     wrong — adjust the value, don't ship the contrast hole."
     Darkening the text rather than lightening the tints was chosen because it
     only ever INCREASES contrast: on the tints 5.21 / 4.94 / 4.55, on the page
     background 5.64 (was 4.90), and white-on-cyan buttons 6.02 (was 5.24).
     Nothing gets worse anywhere. Locked by scripts/check-contrast.js so the
     pairing can never regress. */
  --cyan: #006D7E; --cyan-hover: #005F70; --cyan-glow: rgba(0,122,142,0.28);
  --cyan-bg-faint: rgba(0,122,142,0.06);
  --cyan-bg-soft: rgba(0,122,142,0.10);
  --cyan-bg-hover: rgba(0,122,142,0.16);
  --cyan-border-faint: rgba(0,122,142,0.25);
  --cyan-border: rgba(0,122,142,0.45);
  --cyan-border-strong: rgba(0,122,142,0.65);
  --cyan-glow-soft: rgba(0,122,142,0.18);
  --cyan-glow-strong: rgba(0,122,142,0.32);
  --cyan-text-action: var(--cyan);
  --on-cyan: #FFFFFF;
  --error-bg: rgba(200, 30, 30, 0.06);
  --error-border: rgba(200, 30, 30, 0.40);
  --error-text: #B33;
  --error-dot: #C44;
  /* Warn (amber) light-mode — darker amber for AA contrast on pale bg. */
  --warn-bg: rgba(166, 90, 0, 0.06);
  --warn-border: rgba(166, 90, 0, 0.30);
  --warn-text: #8F4D00;
  --warn-dot: #C4760A;
  /* Success (mint) light-mode — darker mint for AA contrast on pale bg. */
  --success-bg: rgba(0, 128, 90, 0.06);
  --success-border: rgba(0, 128, 90, 0.30);
  --success-text: #007961;
  --success-dot: #007961;
  /* Shadow + motion scale (light-mode — softer alphas, same curve). */
  --shadow-active-glow: 0 0 24px rgba(0, 122, 142, 0.22);
  --shadow-card-hover: 0 4px 24px rgba(14, 31, 38, 0.10), 0 0 0 1px rgba(0, 122, 142, 0.18);
  --shadow-focus-ring: 0 0 0 3px rgba(0, 122, 142, 0.18);
  --surface-soft: rgba(14, 31, 38, 0.04);
  --surface-border: rgba(14, 31, 38, 0.12);
  --surface-border-faint: rgba(14, 31, 38, 0.08);
  --icon-muted: rgba(14, 31, 38, 0.40);
  /* Tooltip pair stays identical to dark mode — see :root comment. */
  --tooltip-bg: rgba(20, 20, 28, 0.94);
  --tooltip-fg: rgba(255, 255, 255, 0.92);
  --mint: #007961; --mint-dim: rgba(0,153,122,0.10);
  --body: #3F4D55; --muted: #606E76;
  --app-nav-bg: rgba(235, 243, 240, 0.92);
  --fs-scrim: rgba(0, 0, 0, 0.65);
  --fs-card-bg: #FFFFFF;
  --border: rgba(14,31,38,0.12); --border-hover: rgba(0,122,142,0.40);
  --text-primary: #0E1F26;
  --text-secondary: rgba(14,31,38,0.72);
  --text-tertiary: rgba(14,31,38,0.66);
  --text-action: #007A8E;
  --card-bg: #F5FAFC; --card-bg-hover: #ECF3F7; --card-bg-featured: #E0EBF0;
  --tag-bg: rgba(14,31,38,0.06); --tag-border: rgba(14,31,38,0.12);
}
html { scroll-behavior: smooth; }
body {
  font-family: var(--font); background: radial-gradient(ellipse at 50% 30%, var(--bg-center), var(--bg-edge) 70%);
  background-attachment: fixed; color: var(--white); line-height: 1.6;
  -webkit-font-smoothing: antialiased; min-height: 100vh; overflow-x: hidden;
  font-feature-settings: "tnum" on, "lnum" on;
}
[data-theme="light"] body { color: var(--text-primary); }
/* Helpers for surfaces that want explicit tabular-nums affordance
   (e.g. big-number stat tiles, ranking columns). The body-level rule
   already turns it on globally; these are class hooks for inline-style
   surfaces that re-declare font-variant-numeric and need to opt back
   in without touching the body rule. */
.tabular-nums,
.ro-stat-num {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" on, "lnum" on;
}
a { color: var(--teal); text-decoration: none; }
a:hover { color: var(--mint); }

/* Nav */
.app-nav {
  position: sticky; top: 0; z-index: 100; min-height: 60px;
  background: var(--app-nav-bg); backdrop-filter: blur(20px);
  border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 24px;
}
.app-nav-inner { max-width: 1080px; margin: 0 auto; width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.nav-left { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.nav-logo { font-size: 20px; font-weight: 800; color: var(--text-primary); text-decoration: none; }
.nav-otto { width: 28px; height: 28px; }
.nav-links { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.nav-links a { font-size: 13px; font-weight: 600; color: var(--body); transition: color 0.2s; text-decoration: none; white-space: nowrap; }
.nav-links a:hover, .nav-links a.active { color: var(--text-primary); }
.nav-links button {
  background: none; border: none; font-size: 13px; font-weight: 600; color: var(--muted);
  cursor: pointer; font-family: var(--font); transition: color 0.2s; white-space: nowrap;
}
.nav-links button:hover { color: var(--cyan); }
@media (max-width: 720px) {
  .app-nav { padding: 8px 16px; }
  .nav-links { gap: 12px; width: 100%; justify-content: flex-start; padding-top: 6px; border-top: 1px solid rgba(0,212,255,0.06); margin-top: 4px; overflow-x: auto; flex-wrap: nowrap; -webkit-overflow-scrolling: touch; }
  .nav-links::-webkit-scrollbar { display: none; }
}

/* Containers */
.container { max-width: 960px; margin: 0 auto; padding: 0 24px; }
.page { padding: 48px 0; }

/* Cards */
.card {
  position: relative; border-radius: 18px; padding: 2px;
  background: linear-gradient(135deg, var(--teal), var(--purple));
}
.card-inner {
  background: var(--bg-glass); backdrop-filter: blur(16px); border-radius: 16px; padding: 32px 28px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
}
.card-flat {
  background: var(--bg-glass); backdrop-filter: blur(12px);
  border: 1px solid var(--border); border-radius: 16px; padding: 28px 24px;
  transition: border-color 0.3s, box-shadow 0.3s;
}
.card-flat:hover { border-color: var(--border-hover); box-shadow: 0 0 24px var(--teal-glow); }

/* Buttons — "Ask Otto" pattern (2026-05-15 contrast pass).
   Both buttons share the cyan-border identity of the sidebar's Ask
   Otto pill. .btn-cyan ships filled by default (primary CTA); .btn-outline
   ships transparent with cyan text and fills on hover. Borders are 2px
   so cyan reads as a real outline (1px washed out on light bg).
   Both modes use var(--cyan), which resolves to #00FFFF in dark and
   #007A8E in light, so contrast is real on white backgrounds without
   per-page overrides. */
.btn-cyan {
  display: inline-block; font-size: 15px; font-weight: 700; padding: 12px 30px;
  background: var(--cyan); color: var(--on-cyan); border: 2px solid var(--cyan); border-radius: 10px;
  cursor: pointer; font-family: var(--font); transition: all 0.2s ease;
  box-shadow: 0 4px 20px var(--cyan-glow); text-decoration: none; text-align: center;
  letter-spacing: 0.01em;
}
.btn-cyan:hover { background: var(--cyan-hover); border-color: var(--cyan-hover); transform: translateY(-1px); box-shadow: 0 6px 30px var(--cyan-glow); color: var(--on-cyan); }
.btn-cyan:disabled { opacity: 0.5; cursor: not-allowed; box-shadow: none; transform: none; }

.btn-outline {
  display: inline-block; font-size: 14px; font-weight: 600; padding: 10px 22px;
  background: transparent; color: var(--cyan); border: 2px solid var(--cyan); border-radius: 10px;
  cursor: pointer; font-family: var(--font); transition: all 0.2s ease; text-decoration: none;
  letter-spacing: 0.01em;
}
.btn-outline:hover {
  background: var(--cyan); color: var(--on-cyan); box-shadow: 0 0 24px var(--cyan-glow);
}

/* Focus rings — accessibility floor for the two shared button
   primitives. Without this, keyboard users get no visible indicator
   when these CTAs receive focus. The 2px cyan outline at 3px offset
   stands off the cyan fill of .btn-cyan via the transparent gap, and
   sits cleanly outside .btn-outline. Pages on light backgrounds where
   cyan-on-light contrast feels weak can strengthen with a local
   :focus-visible rule (see content.html:140 — uses --tp-focus-ring). */
.btn-cyan:focus-visible,
.btn-outline:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 3px;
}

/* Form elements */
.input {
  width: 100%; padding: 16px 20px; font-size: 16px; font-family: var(--font);
  background: var(--bg-glass); border: 2px solid var(--border); border-radius: 12px;
  color: var(--white); outline: none; backdrop-filter: blur(12px); transition: border-color 0.3s, box-shadow 0.3s;
}
.input:focus { border-color: var(--teal); box-shadow: 0 0 20px rgba(0,212,255,0.15); }
.input::placeholder { color: var(--muted); }
[data-theme="light"] .input { color: var(--text-primary); }
[data-theme="light"] .input:focus { box-shadow: 0 0 20px var(--cyan-glow); }

/* Labels */
.label { font-size: 12px; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; color: var(--teal); margin-bottom: 8px; display: block; }

/* Typography */
/* Heading weights — Blue Chips spec (CLAUDE.md, locked).
   h1/h2 at 600, h3 at 500. Operator-tool aesthetic — Linear / Vercel /
   Stripe — calmer than marketing-bold weights. Pages with explicit
   weight overrides have been removed in this same PR; new pages should
   inherit these defaults. Per-surface narrowing is allowed (see
   .rch-page-title--radar in reach.html for the canonical example). */
h1 { font-size: clamp(28px, 4vw, 42px); font-weight: 600; letter-spacing: -1px; line-height: 1.1; margin-bottom: 12px; }
h2 { font-size: 22px; font-weight: 600; letter-spacing: -0.5px; margin-bottom: 8px; }
h3 { font-size: 17px; font-weight: 500; margin-bottom: 6px; }
.text-muted { color: var(--muted); }
.text-body { color: var(--body); }
.text-teal { color: var(--teal); }
.text-cyan { color: var(--cyan); }
.text-mint { color: var(--mint); }

/* Status badges */
.badge { display: inline-block; font-size: 11px; font-weight: 700; padding: 4px 12px; border-radius: 6px; text-transform: uppercase; letter-spacing: 1px; }
.badge-building { background: var(--teal-dim); color: var(--teal); }
.badge-ready { background: var(--mint-dim); color: var(--mint); }
/* .badge-trial — Blue Chips cyan/glass variant. */
.badge-trial { background: var(--cyan-bg-soft); color: var(--cyan); border: 1px solid var(--cyan-border-faint); }

/* Progress bar */
.progress { height: 6px; background: var(--surface-border-faint); border-radius: 3px; overflow: hidden; margin-top: 8px; }
.progress-bar { height: 100%; border-radius: 3px; background: linear-gradient(90deg, var(--teal), var(--mint)); transition: width 0.5s; }

/* Grid */
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; }
.grid-4 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
@media (min-width: 768px) { .grid-4 { grid-template-columns: repeat(4, 1fr); } }
@media (max-width: 640px) { .grid-2 { grid-template-columns: 1fr; } }

/* Modal */
.modal-bg { display: none; position: fixed; inset: 0; z-index: 200; background: rgba(2,4,8,0.8); backdrop-filter: blur(6px); align-items: center; justify-content: center; }
.modal-bg.open { display: flex; }
.modal-box { background: rgba(10,22,40,0.95); border: 1px solid var(--border); border-radius: 20px; padding: 36px 32px; max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; }
.modal-box h3 { margin-bottom: 16px; }
.modal-close { position: absolute; top: 14px; right: 14px; background: none; border: none; color: var(--muted); font-size: 22px; cursor: pointer; }
[data-theme="light"] .modal-box { background: rgba(255,255,255,0.98); color: var(--text-primary); }

/* Utility */
.hidden { display: none !important; }
.center { text-align: center; }
.mt-4 { margin-top: 16px; }
.mt-6 { margin-top: 24px; }
.mt-8 { margin-top: 32px; }
.mb-4 { margin-bottom: 16px; }
.gap-3 { gap: 12px; }

/* Animation */
.fade-in { animation: fadeIn 0.5s ease; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }

/* Loading spinner — pairs with "Loading…" text so a slow fetch reads as
   in-progress, not frozen. Usage: <span class="ro-spinner"></span> next to
   the loading message; add data-ro-spinner-lg for a bigger standalone one. */
.ro-spinner {
  display: inline-block;
  width: 14px; height: 14px;
  border: 2px solid var(--cyan-border-faint);
  border-top-color: var(--cyan);
  border-radius: 50%;
  animation: roSpin 0.7s linear infinite;
  vertical-align: -2px;
}
.ro-spinner[data-ro-spinner-lg] { width: 22px; height: 22px; border-width: 3px; vertical-align: -5px; }
@keyframes roSpin { to { transform: rotate(360deg); } }

/* Otto arm animation */
.otto-svg .otto-arm { transform-origin: center top; }
.otto-svg .otto-arm:nth-child(1) { animation: armWave 4s ease-in-out 0s infinite; }
.otto-svg .otto-arm:nth-child(2) { animation: armWave 4s ease-in-out 0.3s infinite; }
.otto-svg .otto-arm:nth-child(3) { animation: armWave 4s ease-in-out 0.6s infinite; }
.otto-svg .otto-arm:nth-child(4) { animation: armWave 4s ease-in-out 0.9s infinite; }
.otto-svg .otto-arm:nth-child(5) { animation: armWave 4s ease-in-out 0.15s infinite; }
.otto-svg .otto-arm:nth-child(6) { animation: armWave 4s ease-in-out 0.45s infinite; }
.otto-svg .otto-arm:nth-child(7) { animation: armWave 4s ease-in-out 0.75s infinite; }
.otto-svg .otto-arm:nth-child(8) { animation: armWave 4s ease-in-out 1.05s infinite; }
@keyframes armWave { 0%,100%{transform:rotate(0)} 25%{transform:rotate(8deg)} 75%{transform:rotate(-8deg)} }
@keyframes ottoBob { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-8px)} }
