/* === fonts === */
/* === Self-hosted typefaces ===
   Inter and JetBrains Mono were loaded from fonts.googleapis.com, which meant
   every visitor's IP and User-Agent went to Google before the page painted.
   That is the one piece of this stack that quietly contacted a third party —
   Ollama is asserted to hold no external sockets, speech runs on our own GPU,
   the indicator reader is local and read-only — and it was doing it on the
   front page.

   It also caused a visible bug. Only the MARKETING pages carried the link, so
   athrive.ai rendered the wordmark in Inter while /chat rendered it in
   system-ui. The two lockups could never match no matter how much CSS they
   shared, because they were different typefaces.

   Variable fonts, one file per family per subset, latin and latin-ext only:
   the site is English and shipping Cyrillic, Greek and Vietnamese would have
   tripled the bytes for glyphs nothing here renders. */

/* Inter — latin-ext */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 900;
  /* swap: text is readable in the fallback immediately and reflows once the
     real face arrives. block would show nothing for up to 3s; optional would
     often never use the font we just shipped. */
  font-display: swap;
  src: url('/assets/fonts/inter.a28eb6d3cc.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Inter — latin */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 900;
  /* swap: text is readable in the fallback immediately and reflows once the
     real face arrives. block would show nothing for up to 3s; optional would
     often never use the font we just shipped. */
  font-display: swap;
  src: url('/assets/fonts/inter.c940764593.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* JetBrains Mono — latin-ext */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 500;
  /* swap: text is readable in the fallback immediately and reflows once the
     real face arrives. block would show nothing for up to 3s; optional would
     often never use the font we just shipped. */
  font-display: swap;
  src: url('/assets/fonts/jetbrainsmono.9c38cb2d0d.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* JetBrains Mono — latin */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 500;
  /* swap: text is readable in the fallback immediately and reflows once the
     real face arrives. block would show nothing for up to 3s; optional would
     often never use the font we just shipped. */
  font-display: swap;
  src: url('/assets/fonts/jetbrainsmono.2c32b9b3ee.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* === tokens === */
/* Light is the default and the brand: warm cream and charcoal, editorial.
   Dark is a genuine second palette rather than an inverted first one — the
   cream becomes a warm near-black rather than pure #000, so the page keeps the
   same temperature and does not read as a different product at night. */
:root {
  color-scheme: light;

  /* Brand: Warm Cream + Charcoal — editorial, premium */
  --paper: #FBF9F4;          /* warm cream */
  --paper-rgb: 251, 249, 244;
  --ink: #0E1422;            /* deep navy charcoal */
  --ink-rgb: 14, 20, 34;
  --ink-2: #1F2937;
  /* Measured against the DARKEST light ground, not the page.
     #6B7280 was 4.59:1 on --paper and passed, but --muted is used throughout
     the sidebar, which sits on --surface-2 (#F4F0E6) — and there it was
     4.25:1, under AA. Group headers, search snippets and the empty state were
     all failing on the one surface nobody thought to check them against.
     #646B77 clears 4.5 on all three light grounds: 5.10 paper, 5.37 surface,
     4.72 surface-2. */
  --muted: #646B77;
  --body: #4B5563;           /* body copy on paper — 7.3:1 on --paper */
  --line: #ECE7DC;           /* warm hairline */
  --line-strong: #CFC7B4;   /* also the sidebar divider — 1.60:1 on --paper */
  --surface: #FFFFFF;
  --surface-2: #F4F0E6;

  /* Accents */
  --accent: #1F3A5F;         /* deep blue */
  --accent-rgb: 31, 58, 95;
  --accent-2: #B7956F;       /* warm bronze */
  --accent-2-rgb: 183, 149, 111;
  --accent-2-ink: #7A5F3E;   /* bronze darkened for text on light — 4.6:1 */

  /* Logo gradient stops. Read by the inline SVG sprite (symbol-mark.html) so
     the mark follows the theme instead of being a fixed image that only suits
     one of them. The crossbar is punched in --paper, not listed here. */
  --logo-1: #1F3A5F;
  --logo-2: #0E1422;
  --logo-3: #B7956F;

  /* Direction. Up and down are the only semantic colours in the product and
     they were hardcoded in dashboard.css, so dark mode never reached them:
     #1B6B3A measured 2.52:1 and #9B1C1C 2.02:1 on #1F1F1F — present in the
     markup, invisible on screen.
     Chosen for MATCHED contrast, 6.25 and 6.21 here, so neither direction
     shouts louder than the other. Colour is never the only signal: every
     direction also carries a glyph or a sign, because red/green is exactly the
     pair ~8% of men cannot separate. */
  --up: #146B3A;
  --down: #B3261E;

  /* Error surfaces. Same story as direction: every error box on the site was
     three literal hexes in a component, so in dark mode a failure painted a
     near-white #FDF2F2 panel across a #1F1F1F page. */
  --danger: #B3261E;
  --danger-bg: #FDF2F2;
  --danger-line: #F5C2C2;

  /* Typography */
  --font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Geometry */
  --radius-sm: 6px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --shadow-lg: 0 30px 80px -20px rgba(14, 20, 34, 0.18);

  --nav-h: 76px;

  /* Fluid metrics — scale continuously with the viewport rather than
     snapping at a single breakpoint. clamp() floors them on phones and
     ceilings them so a 4K monitor doesn't get absurd whitespace. */
  --container: 1280px;
  --gutter: clamp(20px, 4vw, 40px);
  --section-y: clamp(56px, 7vw, 96px);
  --stack-lg: clamp(28px, 4vw, 56px);
  --stack-md: clamp(20px, 2.6vw, 36px);
}

/* Wide monitors: let the measure grow instead of stranding 600px of margin
   on either side. Content stays readable because the inner grids are fluid. */
@media (min-width: 1700px) {
  :root { --container: 1440px; }
}
@media (min-width: 2200px) {
  :root { --container: 1600px; }
}

/* Short landscape viewports (laptops at 1366x768, phones on their side)
   — trim the vertical rhythm so a section still fits on one screen. */
@media (max-height: 720px) and (min-width: 900px) {
  :root { --section-y: clamp(48px, 6vh, 72px); }
}

@media (max-width: 900px) {
  :root { --nav-h: 64px; }
}


/* --- dark --- */
/* Applied two ways on purpose:
     @media  — follows the operating system for anyone who never touches the
               toggle, which is most people
     [data-theme] — the explicit choice, written to localStorage and stamped on
               <html> before first paint by the inline script in each page head
   The attribute rules come last so a deliberate choice always beats the system
   preference, in BOTH directions: someone on a dark OS who picks light must
   get light. */
/* Dark is driven ONLY by [data-theme] on <html>, never by a bare
   @media (prefers-color-scheme: dark).
   This stylesheet is global. A media block here would repaint the marketing
   pages too, and their sections carry their own literal colours — the result
   is a half-dark home page nobody asked for. Instead the app pages run the
   inline boot script (components/theme-boot.html), which resolves the stored
   choice or the system preference and stamps the attribute before first
   paint. System preference is still honoured; it is just honoured where we
   control the outcome. */
:root[data-theme="dark"] {
  color-scheme: dark;

  /* VS Code "Dark Modern". Note the direction: the side bar (#181818) is
     DARKER than the editor (#1F1F1F), which is the reverse of most dark
     themes and the reason the rail reads as chrome rather than as another
     panel. --surface-2 is our side bar, so it takes the darker value. */
  --paper: #1F1F1F;          /* editor background */
  --paper-rgb: 31, 31, 31;
  --ink: #CCCCCC;            /* editor foreground */
  --ink-rgb: 204, 204, 204;
  --ink-2: #BBBBBB;
  --muted: #9D9D9D;          /* description text — 6.08:1 on --paper */
  --body: #CCCCCC;           /* 10.26:1 on --paper */
  --line: #2B2B2B;
  /* Also the sidebar divider, which on a shared ground is the only thing
     separating the rail from the thread — #3C3C3C reads at 1.49:1 there,
     which is the low end of perceptible. Nudged up so the edge is definite
     without becoming a drawn line. */
  --line-strong: #454545;
  --surface: #2A2A2A;        /* inputs, hover states — raised above the page */
  --surface-2: #181818;      /* side bar / activity bar */

  /* VS Code's link blue on dark. The brand #1F3A5F is 2.1:1 here and
     unreadable; #4DAAFC carries at 6.65:1 and is the same hue family.
     All five foreground tokens measured AA or better against both the editor
     ground and the darker side bar — see the ratios beside each. */
  --accent: #4DAAFC;
  --accent-rgb: 77, 170, 252;
  --accent-2: #D6B98F;
  --accent-2-rgb: 214, 185, 143;
  --accent-2-ink: #E0C9A6;

  --shadow-lg: 0 30px 80px -20px rgba(0, 0, 0, 0.7);

  /* The dark variant of the mark, DERIVED rather than hand-picked.
     Hue and saturation are carried over unchanged from the light artwork and
     only lightness is mirrored around 50%. That keeps it recognisably the
     same logo: same hues, same tonal rhythm, inverted.

       #1F3A5F  H215 S51 L25  ->  #A0BBE0  L75   8.38:1 on #1F1F1F
       #0E1422  H222 S42 L 9  ->  #DDE3F1  L91  12.82:1
       #B7956F  H 32 S33 L58  ->  #906E48  L42   3.54:1  (clears 3:1 non-text)

     The first attempt was picked by eye and drifted: the middle stop became
     #E6E6E6, a neutral grey 222 degrees off the brand's blue, so the dark mark
     had no blue in its centre at all and read as a different logo rather than
     the same one at night.

     Reusing the light artwork unchanged is not an option, and not as a matter
     of taste — two of its three stops are DARKER than the dark ground:
     #1F3A5F measures 1.44:1 and #0E1422 measures 1.12:1 on #1F1F1F. The mark
     would lose everything except its bronze tip. */
  /* Matched again on the dark ground: 7.26:1 and 7.26:1. The light values are
     far too dark to survive here — that is the whole reason these are tokens
     now rather than two literals in a component. */
  --up: #34C46B;
  --down: #FF8A8A;

  /* The panel has to read as a panel — #2A1A1A was only 1.01:1 from the page,
     an error box you could not see the edges of. #412826 is 1.22:1 against the
     page and still holds the text at 5.95:1. */
  --danger: #FF8A8A;
  --danger-bg: #412826;
  --danger-line: #5C3835;

  --logo-1: #A0BBE0;
  --logo-2: #DDE3F1;
  --logo-3: #906E48;
}

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

/* === base === */
*, *::before, *::after { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  /* Keep anchor targets clear of the sticky nav. Honoured by Chrome 69+,
     Safari 14.1+, Firefox 68+; main.js offsets manually for the rest. */
  scroll-padding-top: calc(var(--nav-h) + 16px);
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

html, body {
  margin: 0;
  padding: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-sans);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-tap-highlight-color: transparent;
}

/* Safety net against sideways scroll on phones. `clip` rather than `hidden`
   on purpose: `hidden` makes the root a scroll container, which historically
   breaks `position: sticky` on the nav in WebKit. `clip` never does, and
   browsers without it simply go without the net — the real overflow sources
   are fixed at source below. */
@supports (overflow: clip) {
  html { overflow-x: clip; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

a { color: inherit; text-decoration: none; }
img, svg, video { display: block; max-width: 100%; height: auto; }

input, select, textarea, button { font: inherit; color: inherit; }
input, select, textarea { font-size: 16px; } /* < 16px triggers iOS zoom-on-focus */
button { background: none; border: 0; margin: 0; }

h1, h2, h3, h4, p { overflow-wrap: break-word; word-wrap: break-word; }

/* ---------------------------------------------------------------------------
   Focus. Keyboard users get a visible ring everywhere; :focus-visible keeps it
   off mouse clicks. Browsers without :focus-visible fall back to :focus, so the
   ring is never missing — only occasionally over-eager.
   --------------------------------------------------------------------------- */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}
@supports not selector(:focus-visible) {
  a:focus, button:focus, [tabindex]:focus {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
  }
}
.btn-primary:focus-visible { outline-color: var(--accent-2); }
.dark :focus-visible { outline-color: var(--accent-2); }
/* Programmatic focus on a scroll target must not draw a ring around a section */
section:focus, header:focus, main:focus, [tabindex="-1"]:focus { outline: none; }

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: 12px;
  top: -100px;
  z-index: 100;
  padding: 12px 18px;
  background: var(--ink);
  color: var(--paper);
  border-radius: var(--radius-md);
  font-size: 14px;
  font-weight: 500;
  transition: top .15s ease;
}
.skip-link:focus { top: 12px; }

.container {
  width: 100%;
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 var(--gutter);
}
@supports (padding: max(0px)) {
  .container {
    padding-left: max(var(--gutter), env(safe-area-inset-left));
    padding-right: max(var(--gutter), env(safe-area-inset-right));
  }
}

/* ---------------------------------------------------------------------------
   Gradient text.
   `color: transparent` on its own renders the headline INVISIBLE anywhere
   background-clip:text is missing (old WebKit views, Opera Mini, IE, forced
   colours). So: paint a real colour first, and only knock the fill out inside
   an @supports guard — using -webkit-text-fill-color, which no browser honours
   without also supporting the clip.

   The gradient itself is passed in per element as --grad and is applied ONLY
   inside the guard. Two reasons it lives here rather than on the element:

   1. `background: linear-gradient(...)` is a SHORTHAND, so it resets
      background-clip back to border-box. Section CSS is concatenated after
      this file (tokens → base → components → sections, see scripts/build.js),
      so any such shorthand silently un-clipped the text and painted the
      gradient across the whole box with a transparent fill on top — i.e. a
      solid gradient slab where the headline should be.
   2. Applying it under the guard means the no-support fallback gets no
      background at all, just real painted text, instead of dark text sitting
      on a full-bleed gradient block.

   Set --grad on the element; never set `background`/`background-image` on a
   .grad-text* element directly.
   --------------------------------------------------------------------------- */
.grad-text        { color: var(--ink); }
.grad-text-accent { color: var(--accent); }
.grad-text-light  { color: #fff; }

@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .grad-text, .grad-text-accent, .grad-text-light {
    /* Fallback is a flat currentColor "gradient", not `none`: an element that
       gets a .grad-text class but forgets --grad then paints as ordinary
       coloured text instead of vanishing under the transparent fill. */
    background-image: var(--grad, linear-gradient(currentColor, currentColor));
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
  }
}

/* Windows High Contrast / forced-colors strips backgrounds — restore the fill
   or the headline vanishes again. */
@media (forced-colors: active) {
  .grad-text, .grad-text-accent, .grad-text-light {
    background: none !important;
    -webkit-text-fill-color: currentColor;
    color: CanvasText;
  }
}

/* Scroll-reveal: sections fade up as they enter the viewport. The hidden state
   is applied by main.js only after the observer is confirmed working, so a
   no-JS or failed-JS load never leaves the page blank. */
.reveal {
  opacity: 0;
  transform: translate3d(0, 14px, 0);
  transition: opacity .55s cubic-bezier(.2,.6,.2,1), transform .55s cubic-bezier(.2,.6,.2,1);
  will-change: opacity, transform;
}
.reveal.is-revealed {
  opacity: 1;
  transform: translate3d(0, 0, 0);
  will-change: auto; /* release the compositor layer once the animation is done */
}
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
}

.mono { font-family: var(--font-mono); }

.eyebrow {
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--accent);
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  gap: 12px;
}
.eyebrow::before {
  content: "";
  width: 28px;
  height: 1px;
  flex: none;
  background: linear-gradient(to right, var(--accent-2), rgba(var(--accent-2-rgb), 0));
}

/* Section accents — subtle warm tint variation between bands */
.tint-cream { background: linear-gradient(180deg, var(--paper) 0%, var(--surface-2) 100%); }
.tint-bronze {
  background:
    radial-gradient(800px 400px at 90% 0%, rgba(var(--accent-2-rgb), 0.08), transparent 60%),
    var(--paper);
}
.tint-blue {
  background:
    radial-gradient(900px 500px at 10% 100%, rgba(var(--accent-rgb), 0.06), transparent 60%),
    var(--paper);
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 11px 18px;
  border-radius: var(--radius-md);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.2;
  transition: transform .15s ease, background-color .15s ease, border-color .15s ease, box-shadow .2s ease, color .15s ease;
  cursor: pointer;
  border: 1px solid transparent;
  font-family: inherit;
  letter-spacing: 0.005em;
  text-align: center;
}
.btn:active { transform: translateY(1px); }
.btn-primary {
  background: var(--ink);
  color: var(--paper);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.08) inset,
    0 1px 2px rgba(var(--ink-rgb), 0.18),
    0 6px 18px -10px rgba(var(--ink-rgb), 0.45);
}
.btn-primary:hover {
  background: #000;
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.1) inset,
    0 2px 4px rgba(var(--ink-rgb), 0.22),
    0 12px 28px -12px rgba(var(--ink-rgb), 0.55);
  transform: translateY(-1px);
}
.btn-accent {
  background: var(--accent);
  color: #fff;
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.1) inset,
    0 6px 18px -10px rgba(var(--accent-rgb), 0.55);
}
.btn-accent:hover {
  background: #18304d;
  transform: translateY(-1px);
  box-shadow: 0 12px 28px -12px rgba(var(--accent-rgb), 0.65);
}
.btn-ghost { color: var(--ink); }
.btn-ghost:hover { background: rgba(var(--ink-rgb), 0.05); }
.btn-outline {
  border-color: var(--line-strong);
  color: var(--ink);
  background: rgba(255, 255, 255, 0.4);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
}
.btn-outline:hover {
  border-color: var(--ink);
  background: rgba(255, 255, 255, 0.7);
}
/* Touch devices have no hover; give taps their own feedback since
   -webkit-tap-highlight-color is switched off above. */
@media (hover: none) {
  .btn:active { opacity: 0.85; }
  a:active { opacity: 0.7; }
}

section { padding: var(--section-y) 0; }
.section-head {
  display: grid;
  grid-template-columns: 1.05fr 1fr;
  gap: clamp(24px, 4vw, 56px);
  align-items: end;
  margin-bottom: var(--stack-md);
}
.section-head h2 {
  font-size: clamp(28px, 3.6vw, 46px);
  line-height: 1.08;
  letter-spacing: -0.025em;
  font-weight: 600;
  margin: 12px 0 0;
  text-wrap: balance;
}
.section-head p {
  font-size: clamp(15px, 1.3vw, 17px);
  color: var(--body);
  line-height: 1.55;
  margin: 0;
}

@media (max-width: 900px) {
  .section-head { grid-template-columns: 1fr; gap: 20px; align-items: start; }
}

/* Minimum 44px tap target on any touch-primary device, not just narrow ones —
   a 1024px tablet needs this as much as a phone does. */
@media (hover: none) and (pointer: coarse) {
  .btn, .nav-links a, .foot-col a, .foot-contact {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }
  .foot-col a { display: flex; }
}

/* ---------------------------------------------------------------------------
   Print. Without this the reveal effect prints blank sections, and the dark
   band burns a page of ink.
   --------------------------------------------------------------------------- */
@media print {
  .reveal { opacity: 1 !important; transform: none !important; }
  .nav, .nav-toggle, .hero-bg, .skip-link, .preview-frame { display: none !important; }
  .dark {
    background: none !important;
    color: #000 !important;
  }
  .dark .lab, .dark .author-line { color: #333 !important; }
  .grad-text, .grad-text-light {
    background: none !important;
    -webkit-text-fill-color: #000;
    color: #000 !important;
  }
  body { background: #fff; }
  section, .hero { padding: 24px 0 !important; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 11px; color: #555; }
}

/* === components/chat.css === */
/* === QuantX chat ===
   Layout follows the familiar assistant shape — a centred prompt that becomes a
   thread — but stays on the site's warm-cream palette rather than the dark
   chrome those tools use, so it reads as part of Athrive and not as an embedded
   third-party widget. All colour comes from tokens.css. */

/* Every surface now uses the one .brand lockup from nav.css — marketing nav,
   app bar, login card. There is no second brand rule left to keep in step.
   .brand carries a bottom margin from the nav context that the bar does not
   want. */
.dash-bar .brand { margin-bottom: 0; text-decoration: none; }

.chat-body {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.chat {
  flex: 1;
  width: 100%;
  /* Was a flat 760px, which stranded most of a wide monitor as margin. Now
     continuous rather than stepped: min() means it tracks the window at every
     size instead of snapping at breakpoints, so a 13" laptop, a 27" display
     and a half-width window each get a sensible measure without anything to
     configure. The 1400px ceiling exists because past it even a table stops
     being scannable — the eye loses the row on the way across. */
  max-width: min(1400px, 92vw);
  margin: 0 auto;
  padding: 24px 20px 0;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* --- empty state --- */
.chat-hero {
  margin: auto 0;
  padding: 32px 0 24px;
  text-align: center;
}
.chat-hero h1 {
  font-size: clamp(28px, 4.2vw, 42px);
  letter-spacing: -0.02em;
  margin: 6px 0 12px;
}

.chat-suggest {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: center;
  margin-top: 22px;
}
.chat-chip {
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--ink-2);
  border-radius: 999px;
  padding: 8px 14px;
  font: inherit;
  font-size: 13.5px;
  cursor: pointer;
  transition: border-color .15s, background .15s, transform .15s;
}
/* Hover LIFTS. --surface-2 is a step below the page in both themes, so the
   chip used to sink under the cursor while its border brightened — two
   signals pointing opposite ways. */
.chat-chip:hover {
  border-color: var(--accent);
  background: var(--surface);
  transform: translateY(-1px);
}

/* --- thread --- */
.chat-thread {
  flex: 1;
  overflow-y: auto;
  padding: 8px 0 16px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
/* Label beside the message, not stacked above it.
   Stacked, every turn cost a line of vertical space to say "YOU" — and on a
   wide screen that line was 3 characters followed by a great deal of nothing.
   In a gutter it reads as a transcript and roughly a third more of the
   conversation fits on screen.

   The label is right-aligned so YOU and QUANTX end at the same x and the
   content starts on one axis regardless of which is speaking.

   EVERY child except the label is forced into column 2. renderData() appends
   the result table to .chat-msg directly, and without this it would land in
   the next implicit grid row — underneath the label, in the gutter. Same for
   the error box and the stopped note. */
.chat-msg {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 6px;
}
.chat-msg > *:not(.chat-role) { min-width: 0; }

@media (min-width: 760px) {
  .chat-msg {
    grid-template-columns: 76px minmax(0, 1fr);
    column-gap: 18px;
    row-gap: 8px;
    align-items: start;
  }
  .chat-role { text-align: right; padding-top: 3px; }
  .chat-msg > *:not(.chat-role) { grid-column: 2; }
}

.chat-role {
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--muted);
  font-weight: 600;
}
/* Your own message is RAISED, so it takes --surface, not --surface-2.
   It was --surface-2, which in dark maps to #181818 — darker than the #1F1F1F
   page — so the bubble read as a hole punched into the thread rather than as
   something sitting on it. In a dark interface a raised surface is lighter:
   light falls from above, and elevation is more of it landing on the surface.
   --surface-2 is right for the sidebar, which is chrome recessed at the edge,
   and backwards for a message. One token was doing two jobs with opposite
   elevation semantics.

   Now it is above the page in both themes — #FFFFFF on cream, #2A2A2A on
   #1F1F1F — and the text clears AA on both: 18.39:1 and 8.94:1.

   No border. --line measures 1.01:1 against the new dark fill and 1.23:1
   against white, so it was drawing nothing in either theme; the fill alone
   carries the shape. A border that cannot be seen is weight in the stylesheet
   and a hairline the browser still has to paint. */
.chat-msg-user .chat-text {
  background: var(--surface);
  border-radius: var(--radius-lg);
  padding: 11px 15px;
  /* justify-self, not align-self: in a grid the latter moves it on the block
     axis and the bubble would stretch the full column. */
  justify-self: start;
  max-width: min(100%, 68ch);
}
.chat-text {
  color: var(--ink);
  font-size: 16px;
  line-height: 1.65;
  white-space: pre-wrap;
  word-break: break-word;
  /* Measure, not container width. At 760px and 15.5px this ran to ~97
     characters per line; comfortable reading is 45-75 and past ~85 it
     measurably degrades. Expressed in ch so it tracks the font size instead
     of silently drifting if that changes.
     The result table is a SIBLING of .chat-text, not a child, so it takes the
     full container width — prose is narrowed, data is not. That split is what
     lets the container widen for tables without running the prose out to a
     100-character line.
     80ch rather than 68: with the container fluid there is room, and 80 is
     still inside the comfortable band where 100+ is not. */
  max-width: 80ch;
}
.chat-text table {
  border-collapse: collapse;
  margin: 10px 0;
  font-size: 13.5px;
  width: 100%;
  display: block;
  overflow-x: auto;        /* wide result tables scroll, the page never does */
}
.chat-text th, .chat-text td {
  border-bottom: 1px solid var(--line);
  padding: 6px 10px;
  text-align: left;
  white-space: nowrap;
}
.chat-text th { color: var(--muted); font-weight: 600; }
/* Inline code needs to read as a distinct run of text, and --surface-2 sits
   1.08:1 from the page — technically a different colour, visually the same
   one. --surface is 1.35:1 and actually shows. */
.chat-text code {
  font-family: var(--font-mono);
  font-size: .9em;
  background: var(--surface);
  padding: 1px 5px;
  border-radius: var(--radius-sm);
}

.chat-cursor::after {
  content: '▍';
  color: var(--accent);
  animation: chat-blink 1s steps(2, start) infinite;
}
@keyframes chat-blink { to { visibility: hidden; } }

.chat-error {
  color: var(--danger);
  background: var(--danger-bg);
  border: 1px solid var(--danger-line);
  border-radius: var(--radius-md);
  padding: 10px 13px;
  font-size: 14px;
}

/* --- composer --- */
/* The composer is the bottom-most thing on screen, so on a notched iPhone in
   standalone mode it lands under the home indicator — the send button sits
   beneath the gesture bar and taps go to the system, not the page.
   env() resolves to 0 everywhere else, so this costs nothing on desktop. */
.chat-composer {
  position: sticky;
  bottom: 0;
  padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(to bottom, rgba(var(--paper-rgb), 0), var(--paper) 22%);
  padding-top: 14px;
}
/* The thread widens for tables; the composer does not follow it all the way.
   A single-line input 1400px across makes the caret hard to find and the text
   run away from the eye. Capped and centred — it is one line of typing, not a
   data view. */
.chat-box {
  max-width: 1080px;
  margin: 0 auto;
  border: 1px solid var(--line-strong);
  background: var(--surface);
  border-radius: var(--radius-lg);
  padding: 12px 14px 10px;
  box-shadow: 0 1px 2px rgba(var(--ink-rgb), .04);
  transition: border-color .15s, box-shadow .15s;
}
.chat-box:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .10);
}
.chat-box textarea {
  width: 100%;
  border: 0;
  outline: 0;
  resize: none;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 15.5px;
  line-height: 1.55;
  max-height: 200px;
}
.chat-box-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 8px;
}
.chat-hint { font-size: 12px; color: var(--muted); }

@media (max-width: 640px) {
  /* Landscape on a notched phone puts the rounded corner over the left edge. */
  .chat {
    padding: 16px 14px 0;
    padding-left: max(14px, env(safe-area-inset-left, 0px));
    padding-right: max(14px, env(safe-area-inset-right, 0px));
  }
  .chat-hero h1 { font-size: 26px; }
  .chat-hint { display: none; }
}

/* Result rows, rendered from the query output rather than from model text —
   see renderData() in chat.js. Kept visually quieter than the prose: the
   sentence is the answer, this is the evidence behind it. */
/* The block owns the vertical rhythm; the inner wrapper owns the horizontal
   scroll. Splitting them lets the expander sit outside the scroll area so it
   holds its position on a wide table. */
.chat-data-block { margin-top: 10px; }
.chat-data-wrap { overflow-x: auto; }
table.chat-data {
  border-collapse: collapse;
  /* Tracks the prose: both came down together, so the sentence and the
     evidence under it keep their relationship. */
  font-size: 13.5px;
  font-variant-numeric: tabular-nums;
  min-width: 100%;
}
table.chat-data th {
  text-align: left;
  font-weight: 600;
  font-size: 11.5px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 4px 14px 6px 0;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
/* One row of data is one line on screen, and it is never clipped.
   Two earlier attempts were both wrong. nowrap + text-overflow: ellipsis cut
   every headline at 340px — and a truncated headline is not a shorter
   headline, it is a different one, since the clipped half usually carries the
   direction of the story. Replacing that with overflow-wrap: anywhere was
   worse: `anywhere` lowers a cell's MIN-CONTENT width to a single character,
   so the table happily crushed TITLE to one word per line and broke words
   mid-syllable ("Forec / asters").
   So: no wrapping, no clipping, no max-width. The full sentence sits on its
   own line and the table scrolls sideways in .chat-data-wrap if it must —
   which is why prose columns are ordered last (proseLast() in chat.js), where
   their length displaces nothing. */
table.chat-data td {
  padding: 6px 14px 6px 0;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
  vertical-align: top;
}
/* Numbers and timestamps stay on one line — they are short, and a wrapped
   price is harder to read than a wide column. */
table.chat-data td.num, table.chat-data th.num { white-space: nowrap; }
table.chat-data tr:last-child td { border-bottom: 0; }
/* Scores and prices are compared down the column, so right-align them and use
   lining figures — 7.51 above 6.55 only reads as bigger when the decimal
   points line up. Tagged in renderData(); text columns are unaffected. */
table.chat-data td.num, table.chat-data th.num { text-align: right; }
/* Now a real control, so it has to look and behave like one — a link that
   reads as static text is worse than no link, because nobody clicks it. */
.chat-data-more {
  /* Right-aligned and always in the same spot, directly under the table's
     right edge — a control that moves between renders is one the eye has to
     hunt for each time. */
  display: block;
  margin-left: auto;
  text-align: right;
  font-size: 11.5px;
  color: var(--accent);
  padding: 6px 0 2px;
  background: none;
  border: 0;
  cursor: pointer;
  text-decoration: underline;
  font-family: inherit;
}
.chat-data-more:hover { color: var(--ink); }
.chat-data-more:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Stop button while a turn is streaming. Deliberately the same near-black as
   the primary button: red read as an error state, when stopping is a normal
   thing to do. Only the label changes, so the control never moves or resizes
   mid-answer — a click target that shifts under the cursor at the exact moment
   it is wanted is worse than no button. */
.btn-stop {
  background: var(--ink);
  border-color: var(--ink);
  color: #fff;
}
.btn-stop:hover { background: #000; border-color: #000; }

/* A cancel is not a failure, so it is muted rather than red like .chat-error. */
.chat-stopped {
  font-size: 12.5px;
  color: var(--muted);
  font-style: italic;
  margin-top: 8px;
}

/* --- dictation --- */
/* The mic and Ask sit together so the two ways of asking a question are in
   one place, and so adding the mic did not move the Ask button people have
   already learned the position of. */
.chat-actions { display: flex; align-items: center; gap: 8px; }

.chat-mic {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-2);
  cursor: pointer;
  transition: border-color .15s, background .15s, color .15s;
}
.chat-mic:hover { border-color: var(--accent); color: var(--ink); }
.chat-mic:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.chat-mic[disabled] { opacity: .55; cursor: default; }

/* Recording is the one state that must be unmistakable — a mic you cannot
   tell is live is a privacy problem, not a styling one. Red is correct here
   (unlike on the stop button, where it wrongly implied an error) because it
   is the universal recording signal, and it pulses so it reads as live rather
   than merely selected. */
.chat-mic.is-recording {
  /* Deliberately NOT var(--danger): this is a filled button with white text,
     and the dark --danger is a light red that white cannot sit on (1.6:1). A
     recording indicator should also be the same red in both themes — it is a
     universal signal, not a themed accent. */
  background: #B3261E;
  border-color: #B3261E;
  color: #fff;
  animation: chat-mic-pulse 1.4s ease-in-out infinite;
}
@keyframes chat-mic-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(179, 38, 30, .45); }
  50%      { box-shadow: 0 0 0 6px rgba(179, 38, 30, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .chat-mic.is-recording { animation: none; }
}

/* === components/dashboard.css === */
/* ---------------------------------------------------------------------------
   Sign-in + QuantX dashboard.
   Uses the existing token palette so the authed surface reads as the same
   product as the marketing site.
   --------------------------------------------------------------------------- */

/* ---- Sign in ---- */
.auth {
  min-height: 100vh;
  min-height: 100dvh; /* avoids the mobile URL-bar jump where supported */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(24px, 6vw, 64px) var(--gutter);
  background: var(--surface-2);
}
.auth-card {
  width: 100%;
  max-width: 440px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: clamp(28px, 5vw, 44px);
  box-shadow: var(--shadow-lg);
}
/* The login card carries the full brand lockup (.brand / .brand-word from
   nav.css) rather than a smaller variant of its own. It is the first screen
   anyone sees, so it should state the name at full strength — and a second
   set of brand rules is a second thing to keep in step, which the old
   .auth-brand had already failed to do: weight 600 against 700, tracking
   -0.02em against -0.045em, and a size that just inherited the body. */
.auth-card .brand { margin-bottom: 28px; text-decoration: none; }

/* Informational, not an error: being signed out by your own second sign-in is
   normal behaviour, and a red alarm box would suggest something went wrong. */
.auth-notice {
  margin: 0 0 18px;
  padding: 11px 14px;
  border: 1px solid var(--line-strong);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-md);
  background: var(--surface-2);
  color: var(--ink-2);
  font-size: 14px;
  line-height: 1.5;
}
.auth-card h1 {
  font-size: clamp(21px, 2.6vw, 26px);
  line-height: 1.25;
  letter-spacing: -0.02em;
  margin: 0 0 10px;
}
.auth-sub { color: var(--body); font-size: 14px; line-height: 1.6; margin: 0 0 24px; }
.auth-card label {
  display: block;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 8px;
}
.auth-card input {
  width: 100%;
  font-family: var(--font-mono);
  font-size: 14px;
  padding: 12px 14px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  background: var(--paper);
  color: var(--ink);
  margin-bottom: 16px;
}
.auth-card input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}
.auth-card button[type="submit"] { width: 100%; justify-content: center; }
.auth-error {
  color: var(--danger);
  background: var(--danger-bg);
  border: 1px solid var(--danger-line);
  border-radius: var(--radius-sm);
  font-size: 13px;
  padding: 10px 12px;
  margin: 14px 0 0;
}
.auth-foot { color: var(--muted); font-size: 12.5px; line-height: 1.6; margin: 24px 0 0; }

/* ---- Dashboard chrome ---- */
.dash-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 14px var(--gutter);
  border-bottom: 1px solid var(--line);
  /* --paper, not --surface. On white the bar read as a separate panel laid
     over a cream page — a hard edge across the top that belonged to no
     component. It is chrome for the page, so it takes the page's ground and
     the hairline below does the separating.
     Still fully opaque, which is what a sticky bar needs: content scrolls
     underneath it, and a translucent ground would let the thread show
     through. In dark this lands on the editor ground while the rail stays a
     step darker, which is the VS Code relationship. */
  background: var(--paper);
  position: sticky;
  top: 0;
  z-index: 20;
}

.dash-bar-right { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.dash-fresh, .dash-account { font-size: 12.5px; color: var(--muted); }
.dash-account { font-weight: 600; color: var(--ink); }
.btn-sm { padding: 7px 13px; font-size: 13px; }

.dash { padding: clamp(24px, 4vw, 40px) var(--gutter) 80px; }
.dash-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
  margin-bottom: var(--stack-md);
}
.dash-head h1 {
  font-size: clamp(26px, 3.4vw, 38px);
  letter-spacing: -0.03em;
  margin: 6px 0 0;
}
.dash-controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.dash-controls label { font-size: 12.5px; color: var(--muted); }
.dash-controls select {
  font: inherit;
  font-size: 13px;
  padding: 7px 10px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
}

.dash-error {
  color: var(--danger);
  background: var(--danger-bg);
  border: 1px solid var(--danger-line);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  font-size: 13.5px;
  margin-bottom: 20px;
}

/* ---- Panels ---- */
.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  margin-bottom: 22px;
  overflow: hidden;
}
.panel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  padding: 16px 20px;
  border-bottom: 1px solid var(--line);
}
.panel-head h2 { font-size: 15px; letter-spacing: -0.01em; margin: 0; }
.panel-note { font-size: 12px; color: var(--muted); font-family: var(--font-mono); }
.panel-body { padding: 4px 0 0; }
.dash-loading, .dash-empty { color: var(--muted); font-size: 13.5px; padding: 20px; margin: 0; }

/* Wide tables scroll inside the panel — the page body must never scroll
   sideways on a phone. */
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
/* 13px was below the answer text in the chat and below the composer — small
   for the densest data on the site. Raised, while the row padding comes DOWN:
   bigger glyphs and tighter rows put more readable data on one screen, which
   is the opposite trade from bigger glyphs and looser rows. */
.dash-table { width: 100%; border-collapse: collapse; font-size: 14.5px; table-layout: auto; }
/* A tall row must not centre its short cells: the timestamp belongs beside
   the FIRST line of the post it timestamps, not halfway down it. */
.dash-table td { vertical-align: top; }
.dash-table th {
  text-align: left;
  font-size: 11.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 600;
  padding: 8px 14px;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
.dash-table td {
  padding: 8px 14px;
  border-bottom: 1px solid var(--line);
  color: var(--body);
  vertical-align: top;
}
.dash-table tbody tr:last-child td { border-bottom: 0; }
.dash-table tbody tr:hover { background: var(--paper); }
.dash-table strong { color: var(--ink); font-family: var(--font-mono); }
.headline-link { color: var(--accent); text-decoration: none; }
.headline-link:hover { text-decoration: underline; }

.dir { font-family: var(--font-mono); font-weight: 600; }
.dir-bull { color: var(--up); }
.dir-bear { color: var(--down); }
.dir-flat { color: var(--muted); }

@media (max-width: 700px) {
  .dash-table th, .dash-table td { padding: 7px 10px; }
  .dash-head { align-items: flex-start; }
}

/* ===== report tables ===== */

/* The header row stays put while the rows scroll under it.
   A 200-row table whose header has scrolled away is a grid of unlabelled
   numbers — you cannot tell IMPACT from SPY without scrolling back up, which
   means you scroll back up. position: sticky needs the scroll container to be
   the ancestor that scrolls, which is .table-wrap, and a background or the
   rows show through as they pass beneath. */
.table-wrap {
  max-height: 70vh;
  overflow: auto;
}
.dash-table thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--paper);
  /* box-shadow, not border-bottom: borders on a sticky cell scroll away with
     the cell's own box in several engines, leaving the header floating with no
     edge against the rows sliding under it. */
  box-shadow: inset 0 -1px 0 var(--line-strong);
}

/* Scores line up on the decimal point so a column can be compared by eye. */
.dash-table td.num { text-align: right; font-variant-numeric: tabular-nums; }

/* The range pair, fixed to two decimals and hung off a centred ellipsis, so
   the two ends align down the column instead of drifting with each value's
   own precision. */
.range { display: inline-flex; align-items: baseline; gap: 4px; font-variant-numeric: tabular-nums; }
.range-min, .range-max { min-width: 4.2ch; }
.range-min { text-align: right; }
.range-max { text-align: left; }
.range-sep { color: var(--muted); }

/* One cell answering both questions: which way, and how much.
   Hue is direction — the ONLY thing on this page that means up or down — and
   the number is magnitude. The arrow is not decoration: red/green is exactly
   the pair ~8% of men cannot separate, so direction is never carried by colour
   alone. */
.impact {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  justify-content: flex-end;
  font-variant-numeric: tabular-nums;
  font-weight: 650;
}
.impact-arrow { font-size: 11px; }
.impact-value { min-width: 4.2ch; text-align: right; }

/* The post, in full. It was clamped to two lines with the remainder in a
   title tooltip — and the tooltip covered the rows around it, so reading one
   post hid three others. A post you must hover to read, in a table you are
   scanning, is a post you do not read.
   The column takes the width instead: it is the widest cell because it carries
   the most meaning, and everything beside it is short by nature. */
.dash-table td.post {
  white-space: normal;
  width: 46%;
  min-width: 34ch;
}
.post-text { color: var(--body); line-height: 1.5; }

/* ===== per-ticker calls =====
   Each chip is one call: direction, symbol, conviction. They wrap and are
   ordered by score, so the strongest reads first and the row can be scanned
   left to right rather than parsed. */
.dash-table td.calls { white-space: normal; min-width: 20ch; max-width: 30ch; }
.calls { display: flex; flex-wrap: wrap; gap: 4px 6px; }
.call {
  display: inline-flex;
  align-items: baseline;
  gap: 3px;
  padding: 1px 6px 2px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.call-arrow { font-size: 9.5px; }
.call-sym { font-weight: 650; }
.call-score { color: var(--muted); font-size: 11.5px; }
.call-more { align-self: center; font-size: 11.5px; color: var(--muted); }

/* ===== event flags =====
   A walkback reverses an earlier post, so it is the one flag that changes the
   DIRECTION of a trade rather than its size — it earns the louder treatment.
   Threat is context and stays quiet. */
.topic-cell { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.topic-name { }
.flag {
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .05em;
  padding: 2px 5px;
  border-radius: var(--radius-sm);
  white-space: nowrap;
}
.flag-walkback { background: var(--danger-bg); color: var(--danger); border: 1px solid var(--danger-line); }
.flag-threat { background: var(--surface-2); color: var(--muted); }

/* Tickers WRAP instead of stacking.
   The cell was as narrow as its longest ticker, so eight symbols became eight
   lines and a two-line post sat in an eight-line row — the row height was
   being set by the least important column on the page. Given room to flow they
   take one or two lines and the row is as tall as the post. */
.dash-table td.tickers {
  white-space: normal;
  min-width: 15ch;
  max-width: 22ch;
  font-variant-numeric: tabular-nums;
  line-height: 1.5;
}

/* Short by nature — these should never be the reason a row wraps. */
.dash-table td.nowrap { white-space: nowrap; }

/* ===== the NSS scale in the header ===== */
.scale { display: inline-flex; align-items: center; gap: 7px; }
.scale-end { font-size: 11.5px; font-weight: 650; font-variant-numeric: tabular-nums; }
.scale-bear { color: var(--down); }
.scale-bull { color: var(--up); }
.scale-bar {
  width: 76px;
  height: 5px;
  border-radius: 3px;
  background: linear-gradient(90deg, var(--down) 0%, var(--line-strong) 50%, var(--up) 100%);
}

.dash-updated {
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* The panels carried the vertical rhythm of a marketing section, which on a
   dense report is a screen of empty cream above the first row. */
/* ===== vertical rhythm =====
   No single rule was reserving the empty band above the first table — five
   were, each individually reasonable and cumulatively a third of a screen
   before any data:

     .dash      padding-top   up to 40px
     .dash-head h1 margin           6px
     .dash-head margin-bottom        6px
     .panel     margin-top          18px
     .panel-head padding-top        14px
                                 ------
                                   ~84px

   These are the numbers of a marketing page, where whitespace is the product.
   On a report the data is the product, and every pixel of chrome is a row of
   signals pushed below the fold. Roughly halved. */
.dash { padding-top: clamp(14px, 2vw, 22px); padding-bottom: 48px; }
.dash-head { margin-bottom: 2px; }
.dash-head h1 { margin-top: 2px; font-size: clamp(24px, 2.6vw, 30px); }
.panel { margin-top: 10px; margin-bottom: 14px; }
.panel-head { padding: 9px 16px 8px; }
.panel-body { padding: 0 0 2px; }
/* The eyebrow repeats what the logo two inches away already says, on its own
   line, above every load. */
.dash .eyebrow { display: none; }

/* The pager. Full width and inside the panel so it reads as the end of the
   table rather than a control floating after it. */
.load-more {
  display: block;
  width: 100%;
  padding: 11px 16px;
  border: 0;
  border-top: 1px solid var(--line);
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
  transition: background .14s;
}
.load-more:hover:not([disabled]) { background: var(--surface-2); }
.load-more[disabled] { color: var(--muted); cursor: default; }
.load-more:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* === components/footer.css === */
.site-footer {
  border-top: 1px solid var(--line);
  padding: clamp(44px, 5vw, 64px) 0 clamp(28px, 3vw, 40px);
  padding-bottom: calc(clamp(28px, 3vw, 40px) + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(180deg, var(--paper) 0%, var(--surface-2) 100%);
  position: relative;
}
/* Footer brand uses larger size, matching the new nav lockup */
.site-footer .brand svg { width: 36px; height: 36px; }
.site-footer .brand-word { font-size: 22px; }
.site-footer::before {
  content: "";
  position: absolute;
  top: -1px;
  left: 0; right: 0;
  height: 1px;
  background: linear-gradient(
    90deg,
    transparent,
    rgba(var(--accent-2-rgb), 0.4) 30%,
    rgba(var(--accent-rgb), 0.4) 70%,
    transparent
  );
}
.foot {
  display: grid;
  grid-template-columns: 1.5fr repeat(4, 1fr);
  gap: clamp(28px, 3.5vw, 48px);
}
.foot > * { min-width: 0; }
.foot-brand .lede {
  font-size: 16px;
  font-weight: 500;
  letter-spacing: -0.025em;
  color: var(--ink-2);
  max-width: 280px;
  margin-top: 12px;
  line-height: 1.3;
  font-feature-settings: "cv11";
}
.foot-contact {
  display: inline-block;
  margin-top: 14px;
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--accent);
  letter-spacing: 0.01em;
  border-bottom: 1px solid rgba(var(--accent-rgb), 0.25);
  padding-bottom: 1px;
  overflow-wrap: anywhere;
  transition: color .15s ease, border-color .15s ease;
}
.foot-contact:hover {
  color: var(--ink);
  border-color: var(--ink);
}
.foot-col h4 {
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  font-weight: 600;
  margin: 0 0 16px;
}
.foot-col a {
  display: block;
  font-size: 14px;
  color: var(--body);
  padding: 6px 0;
  /* transform rather than padding — padding reflows the whole column on hover */
  transition: color .15s ease, transform .15s ease;
}
.foot-col a:hover {
  color: var(--ink);
  transform: translateX(4px);
}
.foot-bottom {
  margin-top: clamp(32px, 4vw, 56px);
  padding-top: 24px;
  border-top: 1px solid var(--line);
  display: flex;
  flex-wrap: wrap;
  gap: 8px 24px;
  justify-content: space-between;
  font-size: 12px;
  color: var(--muted);
}

@media (max-width: 900px) {
  .foot { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .foot-brand { grid-column: 1 / -1; }
}

@media (max-width: 420px) {
  .foot { grid-template-columns: minmax(0, 1fr); gap: 24px; }
  .foot-bottom { justify-content: flex-start; }
}

/* === components/nav.css === */
.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  background: rgba(var(--paper-rgb), 0.82);
  -webkit-backdrop-filter: saturate(180%) blur(14px);
  backdrop-filter: saturate(180%) blur(14px);
  border-bottom: 1px solid var(--line);
}
/* Firefox < 103 and older WebKit views get no blur — a translucent bar would
   let body text show through and become unreadable, so go near-opaque. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .nav { background: rgba(var(--paper-rgb), 0.98); }
}
.nav::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 1px;
  background: linear-gradient(
    90deg,
    transparent 0%,
    rgba(var(--accent-2-rgb), 0.45) 50%,
    transparent 100%
  );
  opacity: 0.6;
  pointer-events: none;
}
.nav-inner {
  height: var(--nav-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}
.brand {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  line-height: 1;
  flex: none;
}
.brand svg {
  display: block;
  flex: none;
  width: clamp(28px, 2.6vw, 33px);
  height: clamp(28px, 2.6vw, 33px);
}
/* The wordmark. The app bar uses this same class on the same markup rather
   than a lookalike of its own — an earlier copy had already drifted to
   -0.02em with neither font-feature setting, which spelled the brand in a
   visibly different cut of the same typeface. */
.brand-word {
  font-family: var(--font-sans);
  font-weight: 700;
  letter-spacing: -0.045em;
  font-size: clamp(24px, 2.4vw, 29px);
  line-height: 1;
  color: var(--ink);
  font-feature-settings: "cv11";
  font-variation-settings: "wght" 700;
}

/* Wrapper so the links and the CTAs collapse into one panel together on
   mobile — no duplicated markup, nothing hidden-but-focusable. */
.nav-menu {
  display: flex;
  flex: 1;
  align-items: center;
  justify-content: space-between;
  gap: clamp(16px, 2.5vw, 32px);
  margin-left: clamp(20px, 3.5vw, 56px);
  min-width: 0;
}
.nav-links {
  display: flex;
  align-items: center;
  gap: clamp(16px, 2.2vw, 32px);
  font-size: 14px;
  color: #374151;
}
.nav-links a {
  position: relative;
  padding: 4px 2px;
  white-space: nowrap;
  transition: color .15s ease;
}
.nav-links a::after {
  content: "";
  position: absolute;
  left: 2px; right: 2px; bottom: -2px;
  height: 1px;
  background: var(--accent-2);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .2s ease;
}
.nav-links a:hover { color: var(--ink); }
.nav-links a:hover::after { transform: scaleX(1); }
.nav-cta { display: flex; gap: 8px; align-items: center; flex: none; }

/* ---------------------------------------------------------------------------
   Mobile menu toggle
   --------------------------------------------------------------------------- */
.nav-toggle {
  display: none;
  width: 44px;
  height: 44px;
  margin-right: -10px;
  align-items: center;
  justify-content: center;
  flex: none;
  cursor: pointer;
  color: var(--ink);
  border-radius: var(--radius-sm);
  -webkit-appearance: none;
  appearance: none;
}
.nav-toggle-box {
  position: relative;
  display: block;
  width: 22px;
  height: 16px;
}
.nav-toggle-bar,
.nav-toggle-bar::before,
.nav-toggle-bar::after {
  position: absolute;
  left: 0;
  width: 22px;
  height: 2px;
  background-color: currentColor;
  border-radius: 2px;
  transition: transform .2s ease, background-color .15s ease;
}
.nav-toggle-bar { top: 7px; }
.nav-toggle-bar::before { content: ""; top: -7px; }
.nav-toggle-bar::after  { content: ""; top: 7px; }
.nav.is-open .nav-toggle-bar { background-color: transparent; }
.nav.is-open .nav-toggle-bar::before { transform: translateY(7px) rotate(45deg); }
.nav.is-open .nav-toggle-bar::after  { transform: translateY(-7px) rotate(-45deg); }

@media (max-width: 900px) {
  .nav-toggle { display: inline-flex; }

  .nav-menu {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    margin: 0;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    gap: 0;
    padding: 4px var(--gutter) 20px;
    background: var(--paper);
    border-bottom: 1px solid var(--line);
    box-shadow: 0 24px 40px -24px rgba(var(--ink-rgb), 0.35);
    /* Long menus on short landscape phones must scroll, not run off-screen. */
    max-height: calc(100vh - var(--nav-h));
    max-height: calc(100dvh - var(--nav-h));
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }
  .nav.is-open .nav-menu { display: flex; }

  .nav-links {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    font-size: 16px;
  }
  .nav-links a {
    display: flex;
    align-items: center;
    min-height: 52px;
    padding: 8px 2px;
    border-bottom: 1px solid var(--line);
  }
  .nav-links a::after { display: none; }

  .nav-cta {
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
    margin-top: 18px;
  }
  .nav-cta .btn {
    justify-content: center;
    min-height: 48px;
    font-size: 15px;
  }
}

/* === components/sidebar.css === */
/* === Chat sidebar ===
   Conversation list, new chat, search and the theme switch.

   Every colour is a token, which is the only reason dark mode was a palette
   change rather than a rewrite. Nothing here hardcodes a hex. */

/* The collapsed rail is sized by the largest thing IN it, which is a 32px
   icon button — 11px either side gives 54px.

   It was 68px, derived as calc(var(--mark) + 30px) back when the rail held the
   logo. The logo moved to the bar with the rest of the lockup and the
   derivation stayed, so the rail went on reserving width for something that
   was no longer there: 14px of empty column down the whole height of the
   window, on every page, in both themes. --mark had no other reader left and
   is gone.

   32px clears the 24px minimum target size (WCAG 2.5.8) with room, and is the
   same button size the composer uses. */
:root {
  --rail: 54px;
}

.side {
  position: fixed;
  inset: 0 auto 0 0;
  z-index: 40;
  width: 268px;
  display: flex;
  flex-direction: column;
  /* Same ground as the page, in both themes. It was --surface-2, a step
     darker, which put a second background beside the thread and made the rail
     read as a different surface rather than part of the same window.

     That step was doing the separating, though — the two grounds differ by
     only 1.08:1, which is enough to see as a large edge but nothing you could
     call a line. With one shared ground the border has to carry it alone, and
     --line does not: 1.16:1 in dark, 1.17:1 in light, invisible either way.
     Hence --line-strong here, the only place on the site where a border is
     the sole boundary between two regions. */
  background: var(--paper);
  border-right: 1px solid var(--line-strong);
  /* Off-canvas by default; the desktop rule below parks it open. Phones get a
     hamburger, and the thread keeps the full width it needs for result
     tables. */
  transform: translateX(-100%);
  transition: transform .22s ease;
}
.side.is-open { transform: none; }

.side-scrim {
  position: fixed;
  inset: 0;
  z-index: 39;
  background: rgba(var(--ink-rgb), .45);
  border: 0;
}

/* black-translucent means the page paints behind the status bar, so the panel
   head needs the inset or the toggle sits under the clock. */
.side-head {
  padding-top: calc(14px + env(safe-area-inset-top, 0px));
  position: relative;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  padding: 14px 12px 10px 14px;
}
.side-toggle, .side-open {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}
.side-toggle:hover, .side-open:hover { background: var(--surface); color: var(--ink); }

.side-actions { padding: 4px 8px 8px; }

.side-item {
  display: flex;
  align-items: center;
  min-width: 0;
  width: 100%;
  padding: 9px 10px;
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--ink-2);
  font: inherit;
  font-size: 14px;
  text-align: left;
  cursor: pointer;
  transition: background .14s, color .14s;
}
.side-item:hover { background: var(--surface); color: var(--ink); }
.side-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.side-item svg { flex: none; }

.side-search { padding: 6px 2px 2px; }
.side-search input {
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 13.5px;
}
.side-search input:focus {
  outline: 0;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .12);
}

/* --- conversation list --- */
.side-list {
  flex: 1;
  overflow-y: auto;
  padding: 4px 8px 12px;
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-height: 0;
}
.side-group {
  padding: 12px 10px 5px;
  font-size: 11px;
  font-weight: 650;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}
.side-convo {
  display: flex;
  align-items: center;
  gap: 6px;
  border-radius: var(--radius-md);
}
.side-convo:hover { background: var(--surface); }
.side-convo.is-active { background: var(--surface); }
.side-convo.is-active .side-convo-title { color: var(--ink); font-weight: 600; }

.side-convo-title {
  flex: 1;
  min-width: 0;
  padding: 8px 4px 8px 10px;
  border: 0;
  background: none;
  color: var(--ink-2);
  font: inherit;
  font-size: 13.5px;
  text-align: left;
  cursor: pointer;
  /* One line, ellipsised. Unlike the result table — where a clipped headline
     changes the meaning — a clipped thread title is only a label, and the
     full text is one click away. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The rename field replaces the title button in place, so it has to occupy
   the same box — a field that resizes the row makes the list jump while you
   are reading which one you clicked. */
.side-convo-edit {
  flex: 1;
  min-width: 0;
  margin: 2px 4px 2px 6px;
  padding: 6px 8px;
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 13.5px;
}
.side-convo-edit:focus { outline: none; box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .15); }

.side-convo-del {
  flex: none;
  width: 26px;
  height: 26px;
  margin-right: 5px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--muted);
  cursor: pointer;
  /* Revealed on hover/focus rather than always shown: a delete button beside
     every row invites the accident it causes. */
  opacity: 0;
  transition: opacity .12s, color .12s;
}
.side-convo:hover .side-convo-del,
.side-convo-del:focus-visible { opacity: 1; }
.side-convo-del:hover { color: var(--danger); }

.side-empty {
  padding: 14px 12px;
  font-size: 13px;
  color: var(--muted);
  line-height: 1.5;
}
/* The matched message text under a search hit — the reason the thread is in
   the results, which the title alone often does not show. */
.side-snippet {
  padding: 0 10px 8px;
  font-size: 12px;
  color: var(--muted);
  line-height: 1.45;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.side-foot {
  border-top: 1px solid var(--line);
  padding: 8px;
  padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
}
/* Only the icon for the theme you would switch TO is shown — a control
   labelled "Dark mode" showing a moon is unambiguous; showing both is not. */
.side-icon-moon, .side-icon-sun { display: none; }
:root[data-theme="dark"] .side-icon-sun { display: inline-flex; }
:root:not([data-theme="dark"]) .side-icon-moon { display: inline-flex; }

/* --- the page beside it --- */
.chat-shell {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  transition: padding-left .22s ease;
}

/* Pushes the sign-out / report buttons to the right of the lockup. */
.dash-spacer { flex: 1; min-width: 0; }

/* --- desktop: an explicit toggle, not a hover trap ---
   It used to open on hover. That is fewer clicks and worse: the panel opened
   whenever the pointer crossed the left edge on its way somewhere else, and
   there was no way to keep it open while reading. State you cannot set is not
   state — it is a surprise.

   Now it is a toggle that stays where it is put, remembered in localStorage
   and stamped on <html> before first paint so it never flashes open on load.
   Because the choice is deliberate, the panel PUSHES the thread rather than
   overlaying it: reflow is acceptable in response to a click, and was not
   acceptable in response to the cursor drifting.

   Default open — a first-time visitor should meet the conversation list, not
   a column of unlabelled icons. */
@media (min-width: 900px) {
  .side {
    transform: none;
    width: 268px;
    overflow: hidden;
    transition: width .2s ease;
  }
  .chat-shell { padding-left: 268px; }

  :root[data-side="closed"] .side { width: var(--rail); }
  :root[data-side="closed"] .chat-shell { padding-left: var(--rail); }

  /* Collapsed: icons only, centred on the rail. display:none rather than a
     zero width, because there is no animation to keep smooth here and a
     hidden label must not be reachable by Tab. */
  :root[data-side="closed"] .side-item span,
  :root[data-side="closed"] .side-list,
  :root[data-side="closed"] .side-search { display: none; }
  :root[data-side="closed"] .side-actions { padding-left: 11px; padding-right: 11px; }
  :root[data-side="closed"] .side-item { justify-content: center; padding-left: 0; padding-right: 0; }

  /* The toggle lives in the PANEL, never in the bar — that is the difference
     between reaching for the sidebar and reaching across the header for it.
     The bar button stays for phones only, where the panel is off-canvas and
     therefore not reachable at all. */
  .side-open { display: none; }
  .side-toggle { display: inline-flex; }

  /* Collapsed the head holds only the toggle, so it is simply centred — and
     permanently visible rather than revealed on hover. With no mark to swap
     with, hiding it would leave an empty rail with no way out of it. */
  :root[data-side="closed"] .side-head { justify-content: center; }

  .side-scrim { display: none !important; }
}

@media (prefers-reduced-motion: reduce) {
  .side, .chat-shell { transition: none; }
}
@media (max-width: 899px) {
  /* The bar becomes the only way back to the sidebar, so it needs the room. */
  .dash-bar { gap: 8px; }
}

/* === sections/cta.css === */
.cta-band {
  padding: clamp(64px, 8vw, 110px) 0;
  text-align: center;
  border-top: 1px solid var(--line);
  background:
    radial-gradient(900px 500px at 50% 0%, rgba(var(--accent-2-rgb), 0.10), transparent 60%),
    radial-gradient(1100px 600px at 50% 100%, rgba(var(--accent-rgb), 0.06), transparent 65%),
    var(--paper);
  position: relative;
  overflow: hidden;
}
.cta-band h2 {
  font-size: clamp(28px, 4.6vw, 64px);
  line-height: 1.08;
  letter-spacing: -0.03em;
  font-weight: 600;
  margin: 0 0 24px;
  text-wrap: balance;
  --grad: linear-gradient(180deg, var(--ink) 0%, var(--accent) 100%);
}
@media (min-width: 900px) {
  .cta-band h2 { line-height: 1; letter-spacing: -0.035em; }
}
.cta-band p {
  font-size: clamp(16px, 1.5vw, 18px);
  color: var(--body);
  max-width: 560px;
  margin: 0 auto clamp(24px, 3vw, 36px);
  line-height: 1.6;
}
.cta-band .ctas {
  display: flex;
  gap: 12px;
  justify-content: center;
  flex-wrap: wrap;
}

@media (max-width: 480px) {
  .cta-band .ctas .btn { flex: 1 1 100%; }
}

/* === sections/features.css === */
.features {
  display: grid;
  /* Fluid: 3 up on desktop, 2 up on tablets, 1 up on phones — without needing
     a breakpoint for every step in between. */
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
  gap: 1px;
  background: var(--line);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.feature {
  background: var(--paper);
  padding: clamp(24px, 2.6vw, 32px) clamp(20px, 2.2vw, 28px);
  display: flex;
  flex-direction: column;
  gap: 14px;
  position: relative;
  min-width: 0;
  transition: background-color .25s ease, transform .25s ease;
}
.feature::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 2px;
  background: linear-gradient(90deg, var(--accent) 0%, var(--accent-2) 100%);
  opacity: 0;
  transition: opacity .25s ease;
}
.feature:hover {
  background: var(--surface);
}
.feature:hover::before { opacity: 1; }

.feature-num {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--accent-2-ink);
  text-transform: uppercase;
}
.feature h3 {
  font-size: clamp(20px, 1.9vw, 24px);
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: 1.2;
  margin: 0;
  color: var(--ink);
}
.feature p {
  font-size: clamp(14px, 1.2vw, 15px);
  color: var(--body);
  line-height: 1.6;
  margin: 0;
}
.feature-stat {
  margin-top: 4px;
  padding: 14px 16px;
  border-left: 2px solid var(--accent);
  background: rgba(var(--accent-rgb), 0.04);
  border-radius: 0 6px 6px 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  position: relative;
}
.feature-stat .num {
  font-family: var(--font-mono);
  font-size: clamp(22px, 2.2vw, 26px);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--ink);
  line-height: 1.1;
}
.feature-stat .caption {
  font-size: 12px;
  color: var(--muted);
  line-height: 1.45;
}

/* === sections/hero.css === */
.hero {
  padding: clamp(56px, 8vw, 120px) 0 clamp(56px, 6.5vw, 96px);
  position: relative;
  overflow: hidden;
  isolation: isolate;
}
.hero .container { position: relative; z-index: 2; }

.hero-bg {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0; /* longhands for Safari < 14.1 */
  inset: 0;
  z-index: 0;
  pointer-events: none;
}
.hero-bg::before {
  content: "";
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  inset: 0;
  background:
    /* warm bronze glow, top-right */
    radial-gradient(900px 500px at 82% 14%, rgba(var(--accent-2-rgb), 0.18) 0%, transparent 62%),
    /* deep blue glow, bottom-left */
    radial-gradient(1100px 600px at 8% 92%, rgba(var(--accent-rgb), 0.14) 0%, transparent 65%),
    /* very subtle ink wash so text contrasts */
    radial-gradient(1200px 600px at 50% 30%, rgba(var(--ink-rgb), 0.04) 0%, transparent 60%),
    /* fade to paper at the bottom */
    linear-gradient(180deg, transparent 0%, rgba(var(--paper-rgb), 0.6) 70%, var(--paper) 100%);
}
.hero-bg svg {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  inset: 0;
  width: 100%;
  height: 100%;
}
.hero-bg .grid-overlay {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  inset: 0;
  background-image:
    linear-gradient(rgba(var(--ink-rgb), 0.05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(var(--ink-rgb), 0.05) 1px, transparent 1px);
  background-size: 64px 64px;
  -webkit-mask-image: radial-gradient(ellipse 80% 70% at 50% 30%, #000 30%, transparent 80%);
  mask-image: radial-gradient(ellipse 80% 70% at 50% 30%, #000 30%, transparent 80%);
}
/* No mask support → the grid would run edge to edge as a hard rectangle.
   Fade it back instead so it still reads as texture. */
@supports not ((mask-image: radial-gradient(#000, transparent)) or (-webkit-mask-image: radial-gradient(#000, transparent))) {
  .hero-bg .grid-overlay { opacity: 0.35; }
}

.hero-bg .node { fill: var(--accent); }
.hero-bg .node-glow { fill: var(--accent); opacity: 0.12; }
.hero-bg .edge { stroke: rgba(var(--ink-rgb), 0.18); stroke-width: 1; fill: none; }
.hero-bg .edge-strong { stroke: rgba(var(--accent-rgb), 0.45); stroke-width: 1.2; fill: none; }

/* Travelling pulses.
   `offset-path` landed in Firefox 72 and Safari 16. Without it the circles
   have no cx/cy and pile up as three bronze dots in the top-left corner of
   the hero — so they stay hidden unless the engine can actually move them. */
.hero-bg .pulse { fill: var(--accent-2); opacity: 0; }

@supports (offset-path: path('M 0 0 L 1 1')) {
  @keyframes pulse-travel-1 {
    0%   { offset-distance: 0%;   opacity: 0; }
    8%   { opacity: 1; }
    92%  { opacity: 1; }
    100% { offset-distance: 100%; opacity: 0; }
  }
  .hero-bg .pulse-1 {
    offset-path: path('M 220 180 L 480 240 L 720 180 L 980 320 L 1240 220');
    animation: pulse-travel-1 6s linear infinite;
  }
  .hero-bg .pulse-2 {
    offset-path: path('M 100 420 L 360 360 L 620 480 L 880 420 L 1180 540');
    animation: pulse-travel-1 8s linear infinite;
    animation-delay: 1.5s;
  }
  .hero-bg .pulse-3 {
    offset-path: path('M 1320 80 L 1080 220 L 820 140 L 560 280 L 280 220');
    animation: pulse-travel-1 9s linear infinite;
    animation-delay: 3s;
  }
}
@media (prefers-reduced-motion: reduce) {
  .hero-bg .pulse { animation: none; opacity: 0; }
}

.h1 {
  font-size: clamp(32px, 7vw, 96px);
  line-height: 1.02;
  letter-spacing: -0.035em;
  font-weight: 600;
  margin: 20px 0 20px;
  max-width: 18ch;
  text-wrap: balance;
  --grad: linear-gradient(180deg, var(--ink) 0%, #1a2236 100%);
}
.h1 .accent {
  --grad: linear-gradient(90deg, var(--accent) 0%, #305680 70%, var(--accent-2) 100%);
}
@media (min-width: 900px) {
  .h1 { line-height: 0.98; letter-spacing: -0.04em; max-width: 1100px; }
}

.lede {
  font-size: clamp(16px, 1.5vw, 19px);
  color: var(--body);
  line-height: 1.6;
  max-width: 620px;
  margin: 0 0 clamp(24px, 3vw, 36px);
}
.hero-ctas {
  display: flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
}
.hero-meta {
  margin-top: clamp(32px, 4.5vw, 56px);
  display: flex;
  gap: clamp(24px, 4vw, 48px);
  flex-wrap: wrap;
}
.hero-meta div { display: flex; flex-direction: column; gap: 4px; }
.hero-meta .label {
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 600;
}
.hero-meta .value { font-size: 15px; color: var(--ink); font-weight: 500; }

@media (max-width: 640px) {
  /* Let the headline find its own break points instead of forcing one that
     overflows a 360px viewport. */
  .h1 br { display: none; }
  .h1 { max-width: none; }
  .hero-ctas .btn { flex: 1 1 auto; min-width: 148px; }
  .hero-meta { gap: 20px 28px; }
}

/* === sections/logos.css === */
.logos {
  padding: 56px 0 40px;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
  position: relative;
}
.logos::before,
.logos::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  height: 1px;
}
.logos::before {
  top: -1px;
  background: linear-gradient(90deg, transparent, rgba(var(--accent-2-rgb), 0.35), transparent);
}
.logos::after {
  bottom: -1px;
  background: linear-gradient(90deg, transparent, rgba(var(--accent-2-rgb), 0.35), transparent);
}
.logos-label {
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--accent);
  font-weight: 600;
  text-align: center;
  margin-bottom: 28px;
}
.logos-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 56px;
}
.logos-row span {
  font-family: var(--font-sans);
  font-weight: 500;
  color: #9CA3AF;
  font-size: 18px;
  transition: color .2s, transform .2s;
}
.logos-row span:hover {
  color: var(--ink);
  transform: translateY(-1px);
}

/* === sections/preview.css === */
.preview {
  padding: clamp(56px, 8vw, 120px) 0 0;
  background:
    radial-gradient(800px 400px at 50% 100%, rgba(var(--accent-rgb), 0.05), transparent 60%),
    var(--paper);
}
.preview-frame {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 8px;
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.6) inset,
    0 30px 80px -20px rgba(var(--ink-rgb), 0.18),
    0 8px 24px -12px rgba(var(--accent-rgb), 0.20);
}
.preview-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--line);
}
.preview-bar .dot {
  width: 10px;
  height: 10px;
  flex: none;
  border-radius: 50%;
  background: var(--line-strong);
}
.preview-bar .dot:nth-child(1) { background: #E89A9A; }
.preview-bar .dot:nth-child(2) { background: var(--accent-2); opacity: 0.65; }
.preview-bar .dot:nth-child(3) { background: #9CC3A0; }
.preview-bar .url {
  margin-left: 12px;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.preview-body {
  padding: 0;
  /* min-height, not height: at narrow widths the rows wrap to two lines and a
     fixed 520px box spills its content out through the frame. */
  min-height: 520px;
  display: grid;
  grid-template-columns: 240px minmax(0, 1fr);
}
.preview-side {
  background: linear-gradient(180deg, var(--paper), var(--surface-2));
  border-right: 1px solid var(--line);
  padding: 20px 16px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.preview-side-label {
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  font-weight: 600;
  padding: 8px;
}
.preview-side-item {
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  font-size: 13px;
  color: #374151;
  display: flex;
  align-items: center;
  gap: 10px;
}
.preview-side-item.active {
  background: rgba(var(--accent-rgb), 0.10);
  color: var(--accent);
  font-weight: 600;
}
.preview-side-item::before {
  content: "";
  width: 14px;
  height: 14px;
  flex: none;
  border: 1.5px solid currentColor;
  border-radius: 3px;
  opacity: 0.5;
}
.preview-main {
  padding: clamp(18px, 3vw, 32px);
  display: flex;
  flex-direction: column;
  gap: 20px;
  min-width: 0; /* let the grid track shrink instead of forcing overflow */
}
.preview-h {
  font-size: clamp(18px, 2vw, 22px);
  font-weight: 600;
  letter-spacing: -0.015em;
  margin: 0;
  color: var(--ink);
}
.preview-meta {
  font-size: 12px;
  color: var(--muted);
  font-family: var(--font-mono);
}
.agent-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface);
  transition: border-color .2s ease, box-shadow .2s ease;
}
.agent-row:hover {
  border-color: rgba(var(--accent-rgb), 0.4);
  box-shadow: 0 6px 20px -10px rgba(var(--accent-rgb), 0.25);
}
.agent-row .left { display: flex; align-items: center; gap: 12px; min-width: 0; }
.agent-row .left > svg { flex: none; }
.agent-row .name { font-size: 14px; font-weight: 500; color: var(--ink); }
.agent-row .desc { font-size: 12px; color: var(--muted); margin-top: 2px; }
.agent-row .pill {
  flex: none;
  font-size: 11px;
  padding: 4px 10px;
  border-radius: 100px;
  background: rgba(var(--accent-2-rgb), 0.12);
  color: var(--accent-2-ink);
  font-weight: 600;
  font-family: var(--font-mono);
  letter-spacing: 0.04em;
  white-space: nowrap;
}
.agent-row .pill.live {
  background: var(--accent);
  color: #fff;
}

@media (max-width: 900px) {
  .preview-body { grid-template-columns: minmax(0, 1fr); min-height: 0; }
  .preview-side { display: none; }
}

@media (max-width: 560px) {
  .preview-frame { padding: 6px; }
  .preview-bar .url { display: none; }
  .agent-row {
    align-items: flex-start;
    flex-direction: column;
    gap: 10px;
  }
  .agent-row .pill { align-self: flex-start; }
  /* The "+ New agent" chip and the heading stop fitting side by side */
  .preview-main > div:first-child { flex-wrap: wrap; gap: 12px; }
}

/* === sections/quote.css === */
.dark {
  background:
    radial-gradient(900px 500px at 85% 10%, rgba(var(--accent-2-rgb), 0.12), transparent 60%),
    radial-gradient(1200px 700px at 5% 90%, rgba(var(--accent-rgb), 0.22), transparent 65%),
    var(--ink);
  color: #fff;
  padding: clamp(64px, 10vw, 140px) 0;
  position: relative;
  overflow: hidden;
}
.dark::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(var(--accent-2-rgb), 0.55), transparent);
}
.dark::after {
  content: "";
  position: absolute;
  bottom: 0; left: 0; right: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(var(--accent-rgb), 0.6), transparent);
}
.dark .eyebrow { color: var(--accent-2); }
.dark .eyebrow::before {
  background: linear-gradient(to right, var(--accent-2), rgba(var(--accent-2-rgb), 0));
}
.dark h2 {
  font-size: clamp(26px, 5.5vw, 72px);
  line-height: 1.12;
  letter-spacing: -0.025em;
  font-weight: 500;
  margin: 24px 0 0;
  max-width: 1080px;
  text-wrap: balance;
}
@media (min-width: 900px) {
  .dark h2 { line-height: 1.05; letter-spacing: -0.03em; }
}
.dark .author-line {
  margin-top: clamp(24px, 3vw, 36px);
  font-size: 13px;
  color: rgba(255, 255, 255, 0.7);
  letter-spacing: 0.01em;
}

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
  gap: clamp(28px, 3.5vw, 48px);
  padding-top: clamp(40px, 6vw, 80px);
  border-top: 1px solid rgba(255, 255, 255, 0.14);
  margin-top: clamp(40px, 6vw, 80px);
}
.stat { min-width: 0; }
.stat .num {
  font-size: clamp(38px, 4.4vw, 56px);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.05;
  --grad: linear-gradient(135deg, #ffffff 0%, var(--accent-2) 100%);
}
.stat .lab {
  font-size: 13px;
  color: rgba(255, 255, 255, 0.68);
  margin-top: 12px;
  max-width: 220px;
  line-height: 1.45;
}

/* === sections/security.css === */
.security {
  padding: var(--section-y) 0;
  background:
    radial-gradient(900px 500px at 100% 0%, rgba(var(--accent-rgb), 0.05), transparent 60%),
    radial-gradient(700px 400px at 0% 100%, rgba(var(--accent-2-rgb), 0.06), transparent 60%),
    var(--paper);
  border-top: 1px solid var(--line);
  position: relative;
}
.security-grid {
  display: grid;
  grid-template-columns: 1.1fr 1fr;
  gap: clamp(32px, 5vw, 80px);
  align-items: start;
}
.security-grid > div { min-width: 0; }
.security-grid h2 {
  font-size: clamp(30px, 5vw, 56px);
  line-height: 1.06;
  letter-spacing: -0.03em;
  font-weight: 600;
  margin: 16px 0 24px;
  text-wrap: balance;
}
.security-grid > div > p {
  font-size: clamp(15px, 1.4vw, 17px);
  color: var(--body);
  line-height: 1.6;
  max-width: 520px;
}
.compliance-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: 0 12px 36px -20px rgba(var(--ink-rgb), 0.18);
}
.compliance-cell {
  background: var(--surface);
  padding: clamp(22px, 2.4vw, 30px) clamp(20px, 2.1vw, 26px);
  display: flex;
  flex-direction: column;
  gap: 10px;
  position: relative;
  min-width: 0;
  transition: background-color .2s ease;
}
.compliance-cell:hover { background: linear-gradient(180deg, var(--surface), var(--surface-2)); }
.compliance-cell .badge {
  width: 40px;
  height: 40px;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1.5px solid var(--accent);
  color: var(--accent);
  background: rgba(var(--accent-rgb), 0.04);
  border-radius: var(--radius-md);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  font-family: var(--font-mono);
  margin-bottom: 8px;
}
.compliance-cell:nth-child(2n) .badge {
  border-color: var(--accent-2);
  color: var(--accent-2-ink);
  background: rgba(var(--accent-2-rgb), 0.06);
}
.compliance-cell .title { font-size: 14px; font-weight: 600; color: var(--ink); }
.compliance-cell .desc { font-size: 13px; color: var(--muted); line-height: 1.55; }

.security-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 32px;
}
.security-list .row {
  display: flex;
  gap: 14px;
  padding: 16px 0;
  border-top: 1px solid var(--line);
  font-size: 14px;
}
.security-list .row:first-child { border-top: none; }
.security-list .row .k {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--accent);
  width: 140px;
  flex: none;
  padding-top: 3px;
}
.security-list .row .v { color: var(--ink); line-height: 1.55; min-width: 0; }

@media (max-width: 900px) {
  .security-grid { grid-template-columns: minmax(0, 1fr); gap: 32px; }
}

@media (max-width: 520px) {
  /* A 140px key column leaves too little room for the value on a phone. */
  .security-list .row { flex-direction: column; gap: 6px; padding: 14px 0; }
  .security-list .row .k { width: auto; padding-top: 0; }
}
