/* ==========================================================================
   Quorel — shared design system.  website/assets/site.css
   No third-party anything. Self-hosted faces, no CDN, no tracker.
   Pairs with website/assets/site.js (optional — the page is complete without it).
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. FACES
   One woff2 genuinely exists in this repo (website/assets/fonts).
   Bricolage Grotesque exists ONLY as base64 inside index.html, so it is not
   linkable from here; headings use Instrument Sans tightened instead.
   Paths are relative so the site works from file:// and from quorel.in alike.
   -------------------------------------------------------------------------- */
@font-face {
  font-family: 'Instrument Sans';
  src: url('fonts/instrument-sans-latin-wght-normal.woff2') format('woff2-variations');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

/* --------------------------------------------------------------------------
   2. TOKENS — light is the default; dark overrides only what changes.
   -------------------------------------------------------------------------- */
:root,
:root[data-theme='light'] {
  /* Brand ramp */
  --azure-50:#eef3fe; --azure-100:#d7e4fd; --azure-300:#8fb6fb;
  --azure-400:#3f77ee; --azure-500:#2f6bed; --azure-600:#2554cf;
  --cyan:#14b8a6; --violet:#6d3aed; --amber:#ffb324;

  /* Semantic accent (swap these, not the ramp, to retheme a page) */
  --accent:var(--azure-500);
  --accent-ink:var(--azure-600);
  --accent-soft:var(--azure-50);
  --accent-line:var(--azure-100);
  --on-accent:#ffffff;

  /* Surfaces */
  --bg:#f3f5fa;
  --surface:#ffffff;
  --surface-2:#eceff7;
  --surface-3:#e3e8f3;
  --scrim:rgb(14 19 34 / .55);

  /* Ink */
  --ink:#0e1322;
  --ink-2:#4b5468;
  --ink-3:#5f6980;   /* darkened: #727c93 was 3.64:1 on --surface-2, under AA */
  --ink-inv:#f4f6fb;

  /* Lines */
  --line:#e0e5ef;
  --line-2:#cfd6e4;

  /* Status */
  --good:#0f9d58; --warn:#b97900; --bad:#d0342c;

  /* Product hues (People live; Books / Monitoring planned) */
  --p-people:var(--azure-500); --p-books:var(--cyan); --p-mon:var(--violet);

  /* Radii — four steps, no others */
  --r-sm:10px; --r-md:14px; --r-lg:18px; --r-xl:24px; --r-pill:999px;

  /* Elevation */
  --shadow-1:0 1px 2px rgb(14 19 34 / .05);
  --shadow-2:0 1px 2px rgb(14 19 34 / .05), 0 6px 16px -8px rgb(14 19 34 / .10);
  --shadow-3:0 1px 2px rgb(14 19 34 / .05), 0 14px 34px -14px rgb(14 19 34 / .16);
  --shadow-4:0 2px 4px rgb(14 19 34 / .06), 0 34px 70px -28px rgb(14 19 34 / .28);
  --shadow-glow:0 10px 26px -10px color-mix(in srgb, var(--accent) 55%, transparent);
  --ring:0 0 0 3px color-mix(in srgb, var(--accent) 34%, transparent);

  /* Gradients */
  /* Starts at azure-500, not -400: white on #3f77ee is 4.13:1, under AA for
     the 14px/600 button label. On #2f6bed it is 4.73:1. */
  --grad-accent:linear-gradient(135deg, var(--azure-500) 0%, var(--azure-600) 100%);
  --grad-ink:linear-gradient(135deg, #232c44 0%, #0e1322 100%);
  --grad-halo:radial-gradient(60% 60% at 50% 0%, color-mix(in srgb, var(--accent) 16%, transparent) 0%, transparent 70%);

  /* Spacing rhythm — 4px base */
  --s-1:.25rem;  --s-2:.5rem;  --s-3:.75rem; --s-4:1rem;   --s-5:1.5rem;
  --s-6:2rem;    --s-7:3rem;   --s-8:4rem;   --s-9:6rem;   --s-10:8rem;

  /* Type scale — fluid, 360px → 1440px */
  --fs-xs:.75rem;
  --fs-sm:.8125rem;
  --fs-md:.875rem;
  --fs-base:clamp(.9375rem, .9rem + .18vw, 1.0625rem);
  --fs-lg:clamp(1.0625rem, 1rem + .3vw, 1.1875rem);
  --fs-xl:clamp(1.25rem, 1.15rem + .5vw, 1.5rem);
  --fs-2xl:clamp(1.5rem, 1.3rem + .9vw, 2rem);
  --fs-3xl:clamp(1.875rem, 1.5rem + 1.6vw, 2.75rem);
  --fs-4xl:clamp(2.25rem, 1.7rem + 2.4vw, 3.5rem);
  --fs-5xl:clamp(2.625rem, 1.8rem + 3.6vw, 4.5rem);

  /* Measure & shell */
  --measure:68ch;
  --measure-tight:52ch;
  --shell:1180px;
  --shell-wide:1340px;
  --gutter:clamp(1.25rem, 4vw, 2.5rem);
  --band:clamp(3.5rem, 8vw, 6.5rem);
  --nav-h:72px;

  /* Motion */
  --dur-fast:140ms;
  --dur:220ms;
  --dur-slow:340ms;
  --ease:cubic-bezier(.16, 1, .3, 1);
  --ease-out:cubic-bezier(.22, .61, .36, 1);

  color-scheme: light;
}

:root[data-theme='dark'] {
  --accent:#5f92f6;
  --accent-ink:var(--azure-300);
  --accent-soft:#17233d;
  --accent-line:#243356;
  /* White, not a dark ink: --grad-accent stays deep azure in dark mode, so a
     dark label on it measured 2.93:1. Solid --accent is only used for hairlines. */
  --on-accent:#ffffff;

  --bg:#0b0f1b;
  --surface:#161d31;
  --surface-2:#1c2438;
  --surface-3:#232c44;
  --scrim:rgb(4 7 14 / .66);

  --ink:#eef1f8;
  --ink-2:#a7afc3;
  --ink-3:#828b9f;
  --ink-inv:#0e1322;

  --line:#232c44;
  --line-2:#2f3a55;

  --good:#2dd482; --warn:#e8b34a; --bad:#f0665c;
  --p-people:#5f92f6; --p-books:#2fc9b6; --p-mon:#9b7bf5;

  --shadow-1:0 1px 2px rgb(0 0 0 / .45);
  --shadow-2:inset 0 1px 0 rgb(255 255 255 / .04), 0 1px 2px rgb(0 0 0 / .45), 0 6px 16px -8px rgb(0 0 0 / .6);
  --shadow-3:inset 0 1px 0 rgb(255 255 255 / .05), 0 14px 34px -14px rgb(0 0 0 / .7);
  --shadow-4:inset 0 1px 0 rgb(255 255 255 / .06), 0 34px 70px -28px rgb(0 0 0 / .8);
  --grad-ink:linear-gradient(135deg, #232c44 0%, #10182a 100%);

  color-scheme: dark;
}

/* OS preference applies only while the reader has made no stored choice.
   NOTE for page agents: the inline <head> boot script must set data-theme ONLY
   when localStorage holds a value, otherwise this branch can never win. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --accent:#5f92f6; --accent-ink:#8fb6fb; --accent-soft:#17233d;
    --accent-line:#243356; --on-accent:#ffffff;
    --bg:#0b0f1b; --surface:#161d31; --surface-2:#1c2438; --surface-3:#232c44;
    --scrim:rgb(4 7 14 / .66);
    --ink:#eef1f8; --ink-2:#a7afc3; --ink-3:#828b9f; --ink-inv:#0e1322;
    --line:#232c44; --line-2:#2f3a55;
    --good:#2dd482; --warn:#e8b34a; --bad:#f0665c;
    --p-people:#5f92f6; --p-books:#2fc9b6; --p-mon:#9b7bf5;
    --shadow-1:0 1px 2px rgb(0 0 0 / .45);
    --shadow-2:inset 0 1px 0 rgb(255 255 255 / .04), 0 1px 2px rgb(0 0 0 / .45), 0 6px 16px -8px rgb(0 0 0 / .6);
    --shadow-3:inset 0 1px 0 rgb(255 255 255 / .05), 0 14px 34px -14px rgb(0 0 0 / .7);
    --shadow-4:inset 0 1px 0 rgb(255 255 255 / .06), 0 34px 70px -28px rgb(0 0 0 / .8);
    --grad-ink:linear-gradient(135deg, #232c44 0%, #10182a 100%);
    color-scheme: dark;
  }
}

/* --------------------------------------------------------------------------
   3. BASE
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

/* Paint the canvas on html, not body: until body has a computed style the UA
   paints white, which is the flash seen mid-navigation in dark mode. */
html {
  background: var(--bg);
  -webkit-text-size-adjust: 100%;
  scrollbar-gutter: stable;
  /* The nav is sticky, so an anchor jump that lands a section at y=0 puts its
     own heading underneath the bar — "Products" scrolled to a headline the
     reader could not read. Padding the scroll container by the nav's height
     (plus a little air) fixes every in-page anchor at once, including the
     browser's landing jump for a cross-page /#fragment. */
  scroll-padding-top: calc(var(--nav-h) + var(--s-4, 1rem));
}

/* Smooth scroll is for in-page clicks only. Applied unconditionally it also
   animates the browser's landing jump for a cross-page /#fragment, whipping the
   reader past every section. site.js sets data-scroll="smooth" after load. */
@media (prefers-reduced-motion: no-preference) {
  html[data-scroll='smooth'] { scroll-behavior: smooth; }
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: 'Instrument Sans', system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;
  font-size: var(--fs-base);
  line-height: 1.6;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* clip, not hidden: hidden on body creates a scroll container and breaks
     position:sticky for the header on some engines. */
  overflow-x: clip;
}

img, svg, video, canvas { max-width: 100%; height: auto; display: block; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; }
h1, h2, h3, h4, h5, h6, p, figure, blockquote, ul, ol, dl { margin: 0; }
ul, ol { padding: 0; list-style: none; }
hr { border: 0; border-top: 1px solid var(--line); margin: var(--s-6) 0; }

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 6px;
}
.btn:focus-visible { outline-offset: 3px; box-shadow: var(--ring); }

::selection { background: color-mix(in srgb, var(--accent) 24%, transparent); }

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 200;
  background: var(--ink); color: var(--ink-inv);
  padding: .75rem 1.25rem; border-radius: 0 0 var(--r-sm) 0; font-weight: 600;
}
.skip-link:focus { left: 0; }

.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;
}

/* --------------------------------------------------------------------------
   4. TYPOGRAPHY
   -------------------------------------------------------------------------- */
h1, h2, h3, h4, .display {
  font-weight: 700;
  letter-spacing: -.028em;
  line-height: 1.08;
  text-wrap: balance;
}
h1, .display-1 { font-size: var(--fs-5xl); letter-spacing: -.036em; }
h2, .display-2 { font-size: var(--fs-3xl); letter-spacing: -.03em; }
h3, .display-3 { font-size: var(--fs-xl);  letter-spacing: -.022em; line-height: 1.25; }
h4 { font-size: var(--fs-lg); letter-spacing: -.018em; line-height: 1.3; }

.lede {
  font-size: var(--fs-lg);
  line-height: 1.65;
  color: var(--ink-2);
  max-width: var(--measure-tight);
}
.prose { max-width: var(--measure); color: var(--ink-2); }
.prose > * + * { margin-top: var(--s-4); }
.prose strong { color: var(--ink); font-weight: 600; }
.prose a { color: var(--accent-ink); text-decoration: underline; text-underline-offset: 3px; }

.kicker {
  display: block;
  font-size: var(--fs-xs); font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--ink-3);
}
.muted { color: var(--ink-2); }
.dim   { color: var(--ink-3); }
.mono  { font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace; font-size: .92em; }
.num   { font-variant-numeric: tabular-nums; font-feature-settings: 'tnum' 1; }
.grad-text {
  background: var(--grad-accent);
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}

/* --------------------------------------------------------------------------
   5. LAYOUT
   -------------------------------------------------------------------------- */
.wrap { width: 100%; max-width: var(--shell); margin-inline: auto; padding-inline: var(--gutter); }
.wrap-wide { max-width: var(--shell-wide); }
.wrap-narrow { max-width: 780px; }

.section { padding-block: var(--band); }
.section-tight { padding-block: calc(var(--band) * .6); }
.section-flush { padding-block: 0; }

/* Tinted band — use to separate adjacent sections without a hard rule. */
.band-tint { background: var(--surface-2); }
.band-ink  { background: var(--grad-ink); color: var(--ink-inv); }
.band-ink .kicker, .band-ink .muted { color: color-mix(in srgb, var(--ink-inv) 66%, transparent); }
.band-ink .card { background: rgb(255 255 255 / .05); border-color: rgb(255 255 255 / .10); }

.section-head { max-width: 720px; margin-bottom: var(--s-7); }
.section-head > * + * { margin-top: var(--s-3); }
.section-head-center { margin-inline: auto; text-align: center; }
.section-head-center .lede { margin-inline: auto; }

/* Grids. --min sets the smallest acceptable column; they collapse on their own,
   so no breakpoint list to maintain. */
.grid { display: grid; gap: var(--s-5); }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--min, 320px)), 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--min, 260px)), 1fr)); }
.grid-4 { grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--min, 200px)), 1fr)); }

/* Bento — 6-col on desktop, 2-col tablet, 1-col phone. Children opt into span. */
.bento { display: grid; gap: var(--s-4); grid-template-columns: repeat(6, 1fr); }
.bento > * { grid-column: span 6; }
@media (min-width: 640px)  { .bento > * { grid-column: span 3; } .bento .span-2 { grid-column: span 6; } }
@media (min-width: 1024px) {
  .bento > * { grid-column: span 2; }
  .bento .span-2 { grid-column: span 2; }
  .bento .span-3 { grid-column: span 3; }
  .bento .span-4 { grid-column: span 4; }
  .bento .span-6 { grid-column: span 6; }
  .bento .row-2  { grid-row: span 2; }
}

.cluster { display: flex; flex-wrap: wrap; gap: var(--s-3); align-items: center; }
.cluster-center { justify-content: center; }
.stack > * + * { margin-top: var(--s-4); }
.stack-lg > * + * { margin-top: var(--s-6); }

/* Two-up feature row that reverses cleanly. */
.split { display: grid; gap: clamp(2rem, 5vw, 4.5rem); align-items: center; grid-template-columns: 1fr; }
@media (min-width: 900px) { .split { grid-template-columns: 1fr 1fr; } .split-reverse > :first-child { order: 2; } }

/* --------------------------------------------------------------------------
   6. COMPONENTS
   -------------------------------------------------------------------------- */

/* --- Nav bar ------------------------------------------------------------ */
/* The blur lives on a ::before, never on the header itself: backdrop-filter on
   the header establishes a containing block and would trap the mobile panel's
   position:fixed inside the 72px bar. */
.site-header, header.site {
  position: sticky; top: 0; z-index: 60;
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur) var(--ease);
}
.site-header::before, header.site::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}
/* site.js flips data-scrolled once the page has moved. */
.site-header[data-scrolled='true'], header.site[data-scrolled='true'] { border-bottom-color: var(--line); }
.site-header[data-scrolled='true']::before, header.site[data-scrolled='true']::before { opacity: 1; }

.nav-row { display: flex; align-items: center; gap: var(--s-5); min-height: var(--nav-h); }
.nav-links { display: flex; align-items: center; gap: var(--s-5); flex: 1; }
.nav-links a { font-size: var(--fs-md); font-weight: 500; color: var(--ink-2); transition: color var(--dur-fast); }
.nav-links a:hover { color: var(--ink); }
.nav-links a[aria-current='page'] { color: var(--ink); font-weight: 600; }
.nav-end { display: flex; align-items: center; gap: var(--s-2); margin-left: auto; }

.lockup { display: flex; align-items: center; }
/* 40px read small against a 72px bar — the mark is the one thing on the page
   that has to be recognisable at a glance, so it takes a larger share of the
   bar and steps down only where the bar itself gets shorter. */
.lockup img { height: 48px; width: auto; }
@media (max-width: 600px) { .lockup img { height: 40px; } }
.lockup .logo-dark { display: none; }
:root[data-theme='dark'] .lockup .logo-light { display: none; }
:root[data-theme='dark'] .lockup .logo-dark  { display: block; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .lockup .logo-light { display: none; }
  :root:not([data-theme]) .lockup .logo-dark  { display: block; }
}

/* --- Product lockup ------------------------------------------------------
   Parent mark + "Quorel" wordmark, a hairline rule, then the product name in a
   pill. Two devices rather than one, because a bare second word beside the
   wordmark reads as a tagline: the rule says "these belong together", the pill
   says "this half is the product, not the company". Corporate pages (/,
   /pricing, /security, /contact) stay on the bare parent lockup, so the pill is
   the thing that makes the Quorel Technologies -> Quorel People tier visible.
   Everything except the raster wordmark is CSS on the shared accent tokens, so
   it retints with the theme instead of needing a second set of PNGs. */
.lockup-product { gap: 10px; }
.lockup-rule {
  width: 1px; align-self: stretch; margin-block: 10px;
  background: var(--line-2); border-radius: 1px;
}
.lockup-sub {
  font-size: 11px; font-weight: 600; line-height: 1; white-space: nowrap;
  text-transform: uppercase; letter-spacing: .14em;
  padding: 5px 9px 4px; border-radius: var(--r-pill);
  background: var(--accent-soft); color: var(--accent-ink);
  border: 1px solid var(--accent-line);
}
@media (max-width: 600px) {
  .lockup-product { gap: 8px; }
  .lockup-sub { font-size: 10px; letter-spacing: .12em; padding: 4px 7px 3px; }
}
/* The sub-label adds ~90px to the lockup — enough to push the eight-item product
   nav onto a second line anywhere between the 900px collapse point and ~1360px.
   In that band the lockup steps down a size and the nav tightens its gap, both
   of which are cheaper than dropping the product name: the name is the entire
   reason this lockup exists. Measured at 1280px, the tightest common width. */
@media (min-width: 901px) and (max-width: 1360px) {
  .nav-row .lockup-product { gap: 8px; }
  .nav-row .lockup-product img { height: 40px; }
  .nav-row .lockup-sub { font-size: 10px; letter-spacing: .12em; padding: 4px 7px 3px; }
  .lockup-product ~ .nav-links { gap: var(--s-4); }
}
/* Below ~380px the lockup, theme toggle and menu button stop sharing the bar.
   The pill is what goes: a squeezed sub-label reads as damage, a parent lockup
   on its own still reads as the brand. */
@media (max-width: 380px) {
  .lockup-product .lockup-rule, .lockup-product .lockup-sub { display: none; }
}

.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; border-radius: var(--r-sm);
  border: 1px solid var(--line-2); background: var(--surface); color: var(--ink-2);
  cursor: pointer; transition: color var(--dur-fast), border-color var(--dur-fast);
}
.icon-btn:hover { color: var(--ink); border-color: var(--ink-3); }
.icon-btn svg { width: 17px; height: 17px; }
:root[data-theme='light'] .icon-sun { display: none; }
:root[data-theme='dark']  .icon-moon { display: none; }
@media (prefers-color-scheme: dark) { :root:not([data-theme]) .icon-moon { display: none; } }

.menu-toggle { display: none; background: none; border: 0; color: var(--ink); cursor: pointer; padding: 8px; }
.menu-toggle svg { width: 22px; height: 22px; }

/* 900px, not 768: six labels stop clearing the lockup + sign-in button below it. */
@media (max-width: 900px) {
  .menu-toggle { display: block; }
  .nav-links {
    /* Tracks --nav-h so the panel can never drift out of step with the bar. */
    position: fixed; inset: var(--nav-h) 0 0 0; z-index: 55;
    flex-direction: column; align-items: stretch; justify-content: flex-start;
    gap: 0; padding: var(--s-4) var(--gutter);
    background: var(--bg); border-top: 1px solid var(--line);
    overflow-y: auto;
    transform: translateY(-8px); opacity: 0; visibility: hidden;
    transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease), visibility var(--dur);
  }
  .nav-links.open { transform: none; opacity: 1; visibility: visible; }
  .nav-links a { padding: var(--s-3) 0; font-size: var(--fs-lg); border-bottom: 1px solid var(--line); }
  .nav-cta-mobile { margin-top: var(--s-4); }
}

/* --- Buttons ------------------------------------------------------------ */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
  font-weight: 600; font-size: var(--fs-md); line-height: 1;
  padding: .8rem 1.35rem; border-radius: var(--r-sm);
  border: 1px solid transparent; cursor: pointer; white-space: nowrap;
  transition: background var(--dur-fast), border-color var(--dur-fast),
              color var(--dur-fast), transform var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
}
.btn svg { width: 16px; height: 16px; flex: none; }
.btn:active { transform: translateY(1px); }

.btn-primary {
  background: var(--grad-accent); color: var(--on-accent);
  box-shadow: var(--shadow-2);
}
.btn-primary:hover { box-shadow: var(--shadow-glow); transform: translateY(-1px); }

.btn-ghost {
  background: var(--surface); color: var(--ink); border-color: var(--line-2);
}
.btn-ghost:hover { border-color: var(--ink-3); background: var(--surface-2); }

.btn-ink { background: var(--ink); color: var(--ink-inv); }
.btn-ink:hover { transform: translateY(-1px); box-shadow: var(--shadow-3); }

.btn-quiet { color: var(--ink-2); padding: .6rem .8rem; background: none; }
.btn-quiet:hover { color: var(--ink); }

.btn-sm { padding: .55rem 1rem; font-size: var(--fs-sm); border-radius: 9px; }
.btn-lg { padding: 1rem 1.75rem; font-size: var(--fs-base); border-radius: var(--r-md); }
.btn-block { width: 100%; }

/* --- Pill / eyebrow ----------------------------------------------------- */
.pill {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .35rem .8rem; border-radius: var(--r-pill);
  background: var(--accent-soft); color: var(--accent-ink);
  border: 1px solid var(--accent-line);
  font-size: var(--fs-sm); font-weight: 600; line-height: 1.4;
}
.pill-neutral { background: var(--surface-2); color: var(--ink-2); border-color: var(--line); }
.pill-soon { background: var(--surface-2); color: var(--ink-3); border-color: var(--line); font-weight: 500; }
.pill-live::before {
  content: ''; width: 7px; height: 7px; border-radius: 50%;
  background: var(--good); flex: none;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--good) 22%, transparent);
}

/* --- Cards -------------------------------------------------------------- */
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--s-5);
  box-shadow: var(--shadow-1);
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease),
              border-color var(--dur-fast);
}
.card > * + * { margin-top: var(--s-3); }
.card-lift:hover { transform: translateY(-3px); box-shadow: var(--shadow-3); border-color: var(--line-2); }
.card-flat { box-shadow: none; }
.card-pad-lg { padding: var(--s-6); }
.card-link { display: block; }
.card-link:hover .card-title { color: var(--accent-ink); }
.card-title { font-size: var(--fs-lg); font-weight: 650; letter-spacing: -.018em; transition: color var(--dur-fast); }
.card-body { color: var(--ink-2); font-size: var(--fs-md); line-height: 1.65; }

/* Square icon chip for card headers */
.card-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 42px; height: 42px; border-radius: var(--r-sm);
  background: var(--accent-soft); color: var(--accent-ink);
  border: 1px solid var(--accent-line);
}
.card-icon svg { width: 20px; height: 20px; }

/* --- Stat block --------------------------------------------------------- */
.stat { display: flex; flex-direction: column; gap: .3rem; }
.stat-value {
  font-size: var(--fs-3xl); font-weight: 700; letter-spacing: -.035em;
  line-height: 1; font-variant-numeric: tabular-nums;
}
.stat-label { font-size: var(--fs-sm); color: var(--ink-3); letter-spacing: .01em; }
.stat-row {
  display: grid; gap: var(--s-5);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr));
}
.stat-row-bordered .stat { padding-left: var(--s-4); border-left: 2px solid var(--accent-line); }

/* --- Browser / device frame (pure CSS, no screenshots) ------------------- */
.frame {
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  background: var(--surface);
  box-shadow: var(--shadow-4);
  overflow: hidden;
}
.frame-bar {
  display: flex; align-items: center; gap: var(--s-3);
  padding: .7rem .9rem;
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
}
/* Three dots from one element — no extra markup. */
.frame-dots { width: 10px; height: 10px; border-radius: 50%; flex: none; background: var(--line-2);
  box-shadow: 16px 0 0 var(--line-2), 32px 0 0 var(--line-2); margin-right: 32px; }
.frame-url {
  flex: 1; min-width: 0;
  font-size: var(--fs-xs); color: var(--ink-3);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-pill); padding: .28rem .75rem;
  text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.frame-body { padding: var(--s-5); }
.frame-body-flush { padding: 0; }

.frame-phone {
  width: min(300px, 100%);
  border-radius: 38px;
  border: 9px solid var(--surface-3);
  padding: 0;
  background: var(--surface);
  box-shadow: var(--shadow-4);
  overflow: hidden;
  position: relative;
}
.frame-phone::before {
  content: ''; position: absolute; top: 8px; left: 50%; transform: translateX(-50%);
  width: 92px; height: 20px; border-radius: var(--r-pill);
  background: var(--surface-3); z-index: 2;
}
.frame-phone .frame-body { padding: var(--s-6) var(--s-4) var(--s-4); }

/* --- Footer ------------------------------------------------------------- */
.site-footer {
  border-top: 1px solid var(--line);
  background: var(--surface-2);
  padding-block: var(--s-8) var(--s-6);
  font-size: var(--fs-md);
}
.footer-grid {
  display: grid; gap: var(--s-6);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr));
}
.footer-col h4 { font-size: var(--fs-sm); text-transform: uppercase; letter-spacing: .12em; color: var(--ink-3); font-weight: 600; margin-bottom: var(--s-3); }
/* The lockup opts out of the column's blanket link styling: it is a flex object
   that happens to live in a footer column, and display:block would drop its
   sub-label pill onto its own line and repaint it as link text. */
.footer-col a:not(.lockup), .footer-col span:not(.lockup):not(.lockup-rule):not(.lockup-sub) {
  display: block; padding-block: .3rem; color: var(--ink-2);
}
.footer-col a:not(.lockup):hover { color: var(--accent-ink); }
.footer-bottom {
  display: flex; flex-wrap: wrap; gap: var(--s-3); justify-content: space-between;
  margin-top: var(--s-7); padding-top: var(--s-5);
  border-top: 1px solid var(--line); color: var(--ink-3); font-size: var(--fs-sm);
}

/* --- Scroll progress ---------------------------------------------------- */
.scroll-progress {
  position: fixed; top: 0; left: 0; right: 0; height: 2px; z-index: 100;
  transform-origin: 0 50%;
  transform: scaleX(var(--progress, 0));
  background: var(--grad-accent);
  pointer-events: none;
}

/* --------------------------------------------------------------------------
   7. MOTION VOCABULARY
   Rule: nothing rests at opacity 0 unless JS is present to restore it.
   .no-js sits on <html> in the markup and is removed by an inline head script,
   so a JS-off browser never sees hidden content.
   -------------------------------------------------------------------------- */
.reveal {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease);
  /* No blur filter: compositing it per-frame is the single most expensive thing
     the old page did on a mid-range Android. */
}
.reveal.in { opacity: 1; transform: none; }
/* Applied to anything already above the fold so it settles without animating. */
.reveal.no-anim { transition: none; }

/* Children cascade. site.js writes --i (0..7); the CSS holds the step. */
.reveal-stagger > * { transition-delay: calc(var(--i, 0) * 55ms); }

.parallax {
  transform: translate3d(0, calc(var(--py, 0) * 1px), 0);
  will-change: transform;
}

/* --scrub runs 0 → 1 across the element's pass through the viewport.
   Consume it directly: opacity: var(--scrub); transform: scale(calc(.9 + var(--scrub) * .1)) */
.scrub { --scrub: 0; }
.scrub-pin { position: sticky; top: 0; }

.hover-lift { transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease); }
.hover-lift:hover { transform: translateY(-3px); box-shadow: var(--shadow-3); }

/* Decorative aurora wash — safe behind hero content, costs one paint. */
.halo { position: relative; isolation: isolate; }
/* Zero horizontal bleed on purpose: a negative inline inset here grows
   documentElement.scrollWidth even though body clips it, which is a sideways
   scrollbar on mobile. The gradient fades out well inside the edges anyway. */
.halo::before {
  content: ''; position: absolute; inset: -20% 0 auto 0; height: 120%;
  background: var(--grad-halo); z-index: -1; pointer-events: none;
}

/* --- Guards ------------------------------------------------------------- */
/* No JS: every animated element is simply present. */
.no-js .reveal,
.no-js .parallax { opacity: 1; transform: none; transition: none; }
.no-js .scroll-progress { display: none; }

/* Reduced motion: content appears, nothing moves. */
@media (prefers-reduced-motion: reduce) {
  .reveal, .parallax { opacity: 1 !important; transform: none !important; transition: none !important; }
  .reveal-stagger > * { transition-delay: 0s !important; }
  .card-lift:hover, .hover-lift:hover, .btn:hover, .btn:active { transform: none; }
  .scroll-progress { display: none; }
  *, *::before, *::after {
    animation-duration: .01ms !important; animation-iteration-count: 1 !important;
    transition-duration: .01ms !important; scroll-behavior: auto !important;
  }
}

/* --------------------------------------------------------------------------
   8. UTILITIES (small, closed set — add nothing speculative)
   -------------------------------------------------------------------------- */
.text-center { text-align: center; }
.mx-auto { margin-inline: auto; }
.mt-3 { margin-top: var(--s-3); } .mt-4 { margin-top: var(--s-4); }
.mt-5 { margin-top: var(--s-5); } .mt-6 { margin-top: var(--s-6); } .mt-7 { margin-top: var(--s-7); }
.full-bleed { width: 100vw; margin-inline: calc(50% - 50vw); }
.no-wrap { white-space: nowrap; }
.hide-sm { display: none; }
@media (min-width: 640px) { .hide-sm { display: revert; } .only-sm { display: none; } }

/* --------------------------------------------------------------------------
   9. JOURNEY — scroll-scrubbed cinematic hero.
   site.js eases ONE 0..1 progress custom property (--jp) onto .journey-stage;
   everything below derives from it in CSS: band opacity from the band's
   --a/--b window (copied from data-a/data-b by JS), the per-band entrance
   ramp --k, per-word entrances from --k, and the camera push.
   Page contract (head, before any stylesheet paint matters):
     <html class="no-js"> + inline boot script that swaps it to class="js"
     (see site.js header comment for the exact snippet). html.js gates the
     body fade; html:not(.js) shows the complete static hero instead.
   The GATE media list here MUST stay character-identical to GATES in site.js.
   -------------------------------------------------------------------------- */

/* Page-ready fade gate — only pages whose boot script adds html.js opt in. */
html.js body { opacity: 0; }
html.js body.ready { opacity: 1; transition: opacity .5s var(--ease-out); }

.journey { position: relative; height: calc(var(--journey-vh, 420) * 1vh); }

/* The journey always sits on deep ink with aurora light, both themes — that
   contrast against the light page below is the drama. Fixed colours here are
   deliberate; do not swap them for theme tokens. */
.journey-stage {
  position: sticky; top: 0;
  height: 100vh; height: 100svh;
  overflow: clip;
  background: #0b0f1b;
  color: #f4f6fb;
}
.journey-stage::before {
  content: ''; position: absolute; inset: -12%; z-index: 0; pointer-events: none;
  background:
    radial-gradient(52% 44% at 22% 16%, rgb(47 107 237 / .34), transparent 70%),
    radial-gradient(46% 40% at 78% 28%, rgb(109 58 237 / .20), transparent 70%),
    radial-gradient(42% 36% at 55% 88%, rgb(20 184 166 / .14), transparent 70%);
}
/* Bottom anchor so the settle band and CTAs always read over the scene. */
.journey-stage::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 32%;
  z-index: 1; pointer-events: none;
  background: linear-gradient(to top, rgb(11 15 27 / .88), transparent);
}

/* Camera — pushes into the scene as the journey progresses. Pure CSS from --jp. */
.journey-cam {
  position: absolute; inset: 0; z-index: 0;
  display: grid; place-items: center;
  will-change: transform;
  transform: translate3d(0, calc(var(--jp, 0) * -4%), 0) scale(calc(1.04 + var(--jp, 0) * .22));
}

/* Narrative bands. JS copies data-a/data-b to --a/--b once and toggles .on;
   opacity and the entrance ramp are computed here. First band: data-a="-0.06"
   so the load-time ease from -0.08 → 0 plays its entrance; last band:
   data-b="1.2" so it never fades out. */
.journey-band {
  position: absolute; inset: 0; z-index: 2;
  display: flex; flex-direction: column; justify-content: center; align-items: flex-start;
  padding: 0 clamp(1.5rem, 7vw, 6.5rem);
  visibility: hidden; pointer-events: none;
  --k: clamp(0, (var(--jp, 0) - var(--a, 0)) / var(--ramp, .035), 1);
  opacity: min(clamp(0, (var(--jp, 0) - var(--a, 0)) * 50, 1),
               clamp(0, (var(--b, 1) - var(--jp, 0)) * 50, 1));
}
.journey-band.on { visibility: visible; }
.journey-band a, .journey-band button { pointer-events: none; }
.journey-band.on a, .journey-band.on button { pointer-events: auto; }
/* Ink scrim behind the copy so it reads over light scene cards. */
.journey-band::before {
  content: ''; position: absolute; inset: -4%; z-index: -1; pointer-events: none;
  opacity: calc(.3 + .7 * var(--k, 1));
  background: radial-gradient(ellipse 72% 60% at 34% 50%,
    rgb(11 15 27 / .82), rgb(11 15 27 / .52) 48%, transparent 76%);
}
.journey-band > * { max-width: 660px; }
.journey-band-end { align-items: flex-end; text-align: right; }
.journey-band-end::before {
  background: radial-gradient(ellipse 72% 60% at 66% 50%,
    rgb(11 15 27 / .82), rgb(11 15 27 / .52) 48%, transparent 76%);
}
.journey-band-center { align-items: center; text-align: center; }
.journey-band-center > * { max-width: 780px; }
.journey-band-center::before {
  background: radial-gradient(ellipse 64% 56% at 50% 52%,
    rgb(11 15 27 / .84), rgb(11 15 27 / .56) 48%, transparent 76%);
}

.band-title {
  font-size: clamp(2rem, 4.6vw, 3.6rem);
  font-weight: 700; letter-spacing: -.03em; line-height: 1.08;
  text-wrap: balance; margin-bottom: .35em;
}
.band-sub {
  --ks: clamp(0, (var(--k, 1) - .4) * 2.8, 1);
  opacity: var(--ks);
  transform: translateY(calc((1 - var(--ks)) * 12px));
  font-size: var(--fs-lg); line-height: 1.55;
  color: rgb(244 246 251 / .82);
  max-width: 34em;
}
.band-cta {
  --kb: clamp(0, (var(--k, 1) - .6) * 3.5, 1);
  opacity: var(--kb);
  transform: translateY(calc((1 - var(--kb)) * 12px));
  display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-5);
}
.journey-band-center .band-cta { justify-content: center; }

/* Split text — JS wraps words of [data-split] in .w spans (sr-only original
   kept, fragments aria-hidden) and adds e-<mode> from data-entrance. */
[data-split] .w { display: inline-block; white-space: nowrap; }
.e-rise .w, .e-settle .w, .e-scatter .w, .e-grid .w {
  --kc: clamp(0, (var(--k, 1) - var(--th, 0)) * 2.8, 1);
  opacity: var(--kc);
}
.e-rise .w    { transform: translateY(calc((1 - var(--kc)) * 26px)); }
.e-settle .w  { transform: translateY(calc((1 - var(--kc)) * 10px)) scale(calc(.96 + var(--kc) * .04)); }
.e-scatter .w {
  transform: translate(calc((1 - var(--kc)) * var(--jx, 0px)), calc((1 - var(--kc)) * var(--jy, 0px)))
             rotate(calc((1 - var(--kc)) * var(--jr, 0deg)));
}
.e-grid .w    { transform: translateX(calc((1 - var(--kc)) * var(--jx, 0px))); }
.e-blur { position: relative; }
.e-blur .soft {
  position: absolute; inset: 0; pointer-events: none;
  filter: blur(9px);
  opacity: calc(1 - var(--k, 1));
}
.e-blur .sharp { opacity: var(--k, 1); }

/* Step dots — one <span> per band; JS lights the live one and those before. */
.journey-steps {
  position: absolute; right: clamp(.875rem, 2.5vw, 2rem); top: 50%;
  transform: translateY(-50%);
  display: flex; flex-direction: column; gap: 10px; z-index: 3;
}
.journey-steps span {
  width: 8px; height: 8px; border-radius: 50%;
  background: rgb(244 246 251 / .22);
  transition: background var(--dur) var(--ease);
}
.journey-steps span.lit { background: var(--azure-300); }

/* Scroll cue — fades once the reader moves (JS adds .hide). */
.journey-cue {
  position: absolute; bottom: 22px; left: 50%; transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  z-index: 3; pointer-events: none;
  color: rgb(244 246 251 / .72);
  transition: opacity var(--dur-slow) var(--ease);
}
.journey-cue.hide { opacity: 0; }
.journey-cue-label {
  font-size: var(--fs-xs); font-weight: 600;
  letter-spacing: .22em; text-transform: uppercase;
}
.journey-cue-line {
  width: 1px; height: 42px; position: relative; overflow: hidden;
  background: rgb(244 246 251 / .18);
}
.journey-cue-line::after {
  content: ''; position: absolute; left: 0; top: -50%; width: 100%; height: 50%;
  background: var(--azure-300);
  animation: journey-cue-drop 2.2s var(--ease-out) infinite;
}
@keyframes journey-cue-drop { from { top: -50%; } to { top: 110%; } }

/* Static hero — the complete no-scrub rendition. Hidden while the scrub runs;
   shown by the gates below and with JS off. Put full hero content in it. */
.journey-static {
  position: absolute; inset: 0; z-index: 4;
  display: none; flex-direction: column; justify-content: flex-end;
  padding: 0 clamp(1.5rem, 6vw, 5rem) 10svh;
  background: linear-gradient(to top,
    rgb(11 15 27 / .92), rgb(11 15 27 / .4) 55%, rgb(11 15 27 / .12));
}

/* Nav over the journey: page adds .on-ink to the header; site.js adds .solid
   near journey end (and past 60% viewport when the scrub is gated). */
.site-header.solid { border-bottom-color: var(--line); }
.site-header.solid::before { opacity: 1; }
.site-header.on-ink:not(.solid) { border-bottom-color: transparent; }
.site-header.on-ink:not(.solid)::before { opacity: 0; }
.site-header.on-ink:not(.solid) .menu-toggle { color: #f4f6fb; }
.site-header.on-ink:not(.solid) .icon-btn {
  background: rgb(255 255 255 / .08);
  border-color: rgb(255 255 255 / .18);
  color: rgb(244 246 251 / .82);
}
.site-header.on-ink:not(.solid) .lockup .logo-light { display: none; }
.site-header.on-ink:not(.solid) .lockup .logo-dark { display: block; }
/* Same reason as the wordmark swap above: in LIGHT theme the transparent header
   sits on the journey's near-black ground, where the light-theme pill (pale
   azure fill, deep azure text) would be a bright chip of the wrong mode. */
.site-header.on-ink:not(.solid) .lockup-rule { background: rgb(255 255 255 / .22); }
.site-header.on-ink:not(.solid) .lockup-sub {
  background: rgb(255 255 255 / .10);
  border-color: rgb(255 255 255 / .18);
  color: var(--azure-300);
}
/* A scrim under the transparent header. Without it the links depend entirely on
   whatever the hero happens to be painting behind them at that scroll position,
   and the top of the page — the one view every visitor sees — was rendering
   near-black nav text on the near-black journey ground. The gradient is soft
   enough to keep the header reading as transparent. */
.site-header.on-ink:not(.solid)::after {
  content: '';
  position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: linear-gradient(to bottom, rgb(8 11 20 / .58), rgb(8 11 20 / 0));
}

/* Light links whenever the header sits on ink. Previously gated on a 901px
   media query, which meant the ONE state that needs them — the top of the page
   on any screen — fell back to the dark page ink and vanished. The real
   exception is the open mobile panel, which paints its own light sheet, so it
   is scoped to that state rather than to a width. */
.site-header.on-ink:not(.solid) .nav-links:not(.open) a { color: rgb(244 246 251 / .78); }
.site-header.on-ink:not(.solid) .nav-links:not(.open) a:hover,
.site-header.on-ink:not(.solid) .nav-links:not(.open) a[aria-current='page'] { color: #ffffff; }
.site-header.on-ink:not(.solid) .btn-ghost {
  background: rgb(255 255 255 / .10); border-color: rgb(255 255 255 / .26); color: #f4f6fb;
}

/* THE GATES — scrub off on phones, coarse portrait, short landscape, and
   reduced motion. Must stay character-identical to GATES in site.js. */
@media (max-width: 768px),
       (orientation: portrait) and (pointer: coarse),
       (pointer: coarse) and (max-height: 560px),
       (prefers-reduced-motion: reduce) {
  .journey { height: 100vh; height: 100svh; }
  .journey-band, .journey-cue, .journey-steps { display: none; }
  .journey-static { display: flex; }
  .journey-cam { transform: none !important; }
  .scene-card { transform: none !important; }
}

/* No JS: same still rendition, and the on-ink header falls back to solid so
   light links can never sit over the light page body. */
html:not(.js) .journey { height: 100vh; height: 100svh; }
html:not(.js) .journey-band,
html:not(.js) .journey-cue,
html:not(.js) .journey-steps { display: none; }
html:not(.js) .journey-static { display: flex; }
html:not(.js) .journey-cam { transform: none; }
html:not(.js) .scene-card { transform: none; }
html:not(.js) .site-header.on-ink { border-bottom-color: var(--line); }
html:not(.js) .site-header.on-ink::before { opacity: 1; }

/* --------------------------------------------------------------------------
   10. DEVICE SCENE — the illustrated dashboard composition the camera pushes
   into. Pages place it inside .journey-cam: a .scene-ground aurora panel plus
   absolutely-positioned .scene-card children. Each card may set --dx/--dy
   (unitless px factors) for soft parallax drift driven by the inherited --jp.
   -------------------------------------------------------------------------- */
.device-scene {
  position: relative;
  width: min(940px, 90vw);
  aspect-ratio: 16 / 10;
  margin-inline: auto;
}
.scene-ground {
  position: absolute; inset: 6% 4%;
  border-radius: var(--r-xl);
  border: 1px solid rgb(255 255 255 / .10);
  background:
    radial-gradient(60% 55% at 30% 18%, rgb(47 107 237 / .42), transparent 70%),
    radial-gradient(50% 45% at 76% 72%, rgb(109 58 237 / .28), transparent 70%),
    #10182a;
}
.scene-card {
  position: absolute;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-4);
  padding: var(--s-4);
  transform: translate3d(calc(var(--jp, 0) * var(--dx, 0) * 1px),
                         calc(var(--jp, 0) * var(--dy, 0) * 1px), 0);
}

/* --------------------------------------------------------------------------
   11. MARQUEE — slow auto-scrolling strip (CSS only). Contract: two identical
   .marquee-track children, the second aria-hidden="true". Pauses on hover and
   keyboard focus; reduced motion shows the first track wrapped and static.
   -------------------------------------------------------------------------- */
.marquee-band {
  display: flex; overflow: hidden;
  --marquee-dur: 36s;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.marquee-track {
  display: flex; flex: none; align-items: center;
  gap: var(--s-7); padding-right: var(--s-7);
  width: max-content;
  animation: marquee var(--marquee-dur) linear infinite;
}
.marquee-band:hover .marquee-track,
.marquee-band:focus-within .marquee-track { animation-play-state: paused; }
@keyframes marquee { to { transform: translateX(-100%); } }
@media (prefers-reduced-motion: reduce) {
  .marquee-track { animation: none; flex-wrap: wrap; width: auto; }
  .marquee-track[aria-hidden] { display: none; }
}

/* Reduced motion — journey additions (the gates above already hide the scrub;
   these settle what remains). */
@media (prefers-reduced-motion: reduce) {
  html.js body { opacity: 1; transition: none; }
  .journey-cue-line::after { animation: none; }
  [data-split] .w { opacity: 1 !important; transform: none !important; }
}

/* --------------------------------------------------------------------------
   12. CHARTS — declarative inline SVG, drawn from data-* attributes.
   site.js reads the attributes, writes the geometry ONCE, adds .chart (built)
   and then .drawn when the chart scrolls into view. Everything that moves is a
   CSS transition on a dash offset, so a chart costs zero main-thread frames.
   Sizing is CSS: the svg is width:100% inside the figure, so a chart is as big
   as the column you drop it in. Recolour a single chart by overriding
   --chart-a / --chart-b on the element.
   With JS off (or on malformed data) the data-label sentence is rendered
   instead, so the page still carries the information.
   -------------------------------------------------------------------------- */
.chart-line, .chart-bars, .chart-donut, .chart-gauge, .chart-spark {
  --chart-a: var(--accent);                                   /* series colour */
  --chart-b: var(--cyan);                                     /* ramp far end  */
  --chart-grid: color-mix(in srgb, var(--ink-3) 26%, transparent);
  --chart-track: color-mix(in srgb, var(--ink-3) 20%, transparent);
  --chart-draw: 1000ms;
  display: block; position: relative; margin: 0;
  font-variant-numeric: tabular-nums; font-feature-settings: 'tnum' 1;
}
.chart-line svg, .chart-bars svg, .chart-donut svg,
.chart-gauge svg, .chart-spark svg { width: 100%; height: auto; display: block; }

/* Default footprints — override with a plain width/max-width on the element. */
.chart-donut { width: var(--chart-w, 168px); max-width: 100%; }
.chart-gauge { width: var(--chart-w, 200px); max-width: 100%; }
.chart-spark { width: var(--chart-w, 88px); display: inline-block; vertical-align: middle; }

/* Strokes and fills */
.c-line  { fill: none; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
.c-grid  { stroke: var(--chart-grid); stroke-width: .7; }
.c-track { fill: none; stroke: var(--chart-track); stroke-linecap: round; }
.c-sweep { fill: none; stroke-linecap: round; }
.c-dot   { fill: var(--surface); stroke: var(--chart-a); stroke-width: 2.4; }
.c-txt   { fill: var(--ink-3); font-size: 9px; font-weight: 500; }
.c-val   { fill: var(--ink); font-size: 11px; font-weight: 700; }

/* Centred figure for the ring types — HTML, not SVG text, so it inherits the
   page face and the count-up already in site.js can drive it. */
/* Overlays the svg ONLY, never the caption below it — hence the aspect-ratio
   rather than inset:0, which would centre the number against the whole figure. */
.chart-figure {
  position: absolute; inset: 0 0 auto 0; aspect-ratio: 1; pointer-events: none;
  display: grid; place-content: center; text-align: center;
}
.chart-gauge .chart-figure { aspect-ratio: 120 / 76; align-content: end; padding-bottom: 6%; }
.chart-donut .chart-cap, .chart-gauge .chart-cap { text-align: center; }
.chart-num {
  font-size: var(--fs-2xl); font-weight: 700; letter-spacing: -.03em;
  line-height: 1; color: var(--ink);
}
/* display:block matters: on a non-<figure> chart the caption is a <span>. */
.chart-cap { display: block; margin-top: var(--s-2); font-size: var(--fs-sm); color: var(--ink-3); }

/* Columns — plain elements, so a rounded cap stays round while it grows.
   The single hairline is the track's own background, not an extra node. */
.c-cols {
  display: flex; gap: clamp(4px, 1.6%, 14px);
  height: var(--chart-h, 150px);
  background: linear-gradient(var(--chart-grid), var(--chart-grid)) 0 100% / 100% 1px no-repeat;
}
.chart-bars[data-values] .c-cols { padding-top: 1.5em; }
/* height:100% is load-bearing: the bar's own height is a percentage, and a
   flex item stretched by the container is not a definite height to resolve
   against — the bars come out at zero. */
.c-col {
  flex: 1; min-width: 0; height: 100%;
  display: flex; flex-direction: column; justify-content: flex-end; align-items: center;
}
.c-num { flex: none; font-size: var(--fs-xs); font-weight: 700; color: var(--ink); margin-bottom: 5px; }
.c-bar {
  flex: none; width: min(26px, 62%); height: 0; min-height: 3px;
  border-radius: var(--r-pill);
  background: linear-gradient(to top, var(--chart-a), var(--chart-b));
}
.c-axis { display: flex; gap: clamp(4px, 1.6%, 14px); margin-top: .5rem; }
.c-axis span {
  flex: 1; min-width: 0; text-align: center;
  font-size: var(--fs-xs); color: var(--ink-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Draw-in. .drawn lands on the chart root when it first enters the viewport. */
.c-line, .c-sweep { stroke-dasharray: 1; stroke-dashoffset: 1; }
.c-line  { transition: stroke-dashoffset var(--chart-draw) var(--ease-out); }
.c-sweep { transition: stroke-dashoffset 900ms var(--ease-out); }  /* 900ms = count-up */
.c-bar   { transition: height 640ms var(--ease-out) calc(var(--i, 0) * 70ms); }
.c-num   { opacity: 0; transition: opacity 300ms var(--ease-out) calc(260ms + var(--i, 0) * 70ms); }
.drawn .c-line { stroke-dashoffset: 0; }
.drawn .c-sweep { stroke-dashoffset: calc(1 - var(--v, 0)); }
.drawn .c-bar { height: calc(var(--v, 0) * 100%); }
.drawn .c-num { opacity: 1; }

.c-area { opacity: 0; transition: opacity 460ms var(--ease-out) 220ms; }
.c-txt  { opacity: 0; transition: opacity 380ms var(--ease-out) 300ms; }
.c-val  { opacity: 0; transition: opacity 380ms var(--ease-out) 720ms; }
.drawn .c-area, .drawn .c-txt, .drawn .c-val { opacity: 1; }

.c-dot {
  transform: scale(0); transform-box: fill-box; transform-origin: center;
  transition: transform 420ms var(--ease) 680ms;
}
.drawn .c-dot { transform: scale(1); }

/* Blind viewport or reduced motion: land finished, with nothing to animate. */
.no-anim :is(.c-line, .c-bar, .c-num, .c-sweep, .c-area, .c-txt, .c-val, .c-dot) {
  transition: none !important;
}

/* Trend tint for sparklines in stat tiles. */
.chart-spark[data-trend='up']   { --chart-a: var(--good); --chart-b: var(--good); }
.chart-spark[data-trend='down'] { --chart-a: var(--bad);  --chart-b: var(--bad); }

/* No JS, or data site.js could not parse: show the text alternative itself.
   Never an empty box — the sentence in data-label is the content. */
html:not(.js) :is(.chart-line, .chart-bars, .chart-donut, .chart-gauge, .chart-spark)[data-label],
.chart-fallback {
  width: auto; display: block;
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface-2); color: var(--ink-2);
  padding: var(--s-3) var(--s-4);
  font-size: var(--fs-sm); line-height: 1.55;
}
html:not(.js) :is(.chart-line, .chart-bars, .chart-donut, .chart-gauge, .chart-spark)[data-label]::after {
  content: attr(data-label);
}
