/* ==========================================================================
   Volar design system — components, built entirely from tokens.css.

   No raw hex appears below this line. Every colour resolves to a token, which
   is what makes a palette change a one-file edit and makes drift detectable
   by test rather than by eye.

   Load order matters:  tokens.css  →  system.css
   ========================================================================== */

/* --------------------------------------------------- reset + base --------- */

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html { -webkit-text-size-adjust: 100%; }

body {
  background: var(--ground);
  color: var(--ink-2);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

::selection { background: var(--signal-wash); color: var(--ink); }

/* One focus treatment for everything, visible on every surface. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: var(--border-thick) solid var(--signal);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.skip-link {
  position: absolute; left: var(--space-2); top: calc(var(--space-2) * -8);
  background: var(--surface); color: var(--ink);
  padding: var(--space-2) var(--space-4);
  border: var(--border) solid var(--rule); border-radius: var(--radius);
  transition: top var(--duration) var(--ease);
  z-index: 100;
}
.skip-link:focus { top: var(--space-2); }

/* --------------------------------------------------- layout --------------- */

.wrap      { width: 100%; max-width: var(--measure);      margin-inline: auto; padding-inline: var(--space-5); }
.wrap-wide { width: 100%; max-width: var(--measure-wide); margin-inline: auto; padding-inline: var(--space-5); }
.prose     { max-width: var(--measure-text); }

.section       { padding-block: var(--space-8); }
.section-tight { padding-block: var(--space-6); }
.stack > * + * { margin-top: var(--space-4); }
.stack-lg > * + * { margin-top: var(--space-6); }

.row { display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap; }
.row-between { display: flex; gap: var(--space-3); align-items: center; justify-content: space-between; flex-wrap: wrap; }

.grid { display: grid; gap: var(--space-4); }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 820px) {
  .grid-2, .grid-3 { grid-template-columns: minmax(0, 1fr); }
  /* Four short stat tiles pair up rather than stacking into a long
     column — halving is legible where a single file is just scrolling. */
  .grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 480px) {
  .grid-4 { grid-template-columns: minmax(0, 1fr); }
}

/* --------------------------------------------------- typography ----------- */

h1, h2, h3, h4 {
  font-family: var(--font-display);
  color: var(--ink);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
}
h1 { font-size: var(--text-4xl); }
h2 { font-size: var(--text-2xl); }
h3 { font-size: var(--text-xl); letter-spacing: var(--tracking-normal); }
h4 { font-size: var(--text-lg); letter-spacing: var(--tracking-normal); }

.display { font-family: var(--font-display); font-size: var(--text-5xl); line-height: var(--leading-tight); letter-spacing: var(--tracking-tight); color: var(--ink); font-weight: var(--weight-bold); }
.lead    { font-size: var(--text-lg); color: var(--ink-2); }
.muted   { color: var(--ink-3); }

/* Eyebrow: mono, wide-tracked, small. Labels a section without pretending
   the sections form a numbered sequence — the "§ I / FIG. 1" affectation on
   the previous site was structure-shaped decoration. */
.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-3);
}

a { color: var(--signal); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--signal-hover); }

/* Numbers are data: tabular, mono, aligned. */
.num, .tabular { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* --------------------------------------------------- buttons -------------- */

.btn {
  /* justify-content matters once a button is stretched (width:100% on the
     auth forms): without it the label hugs the left edge of a full-width
     control instead of sitting in the middle of it. */
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: 1;
  padding: var(--space-3) var(--space-4);
  border: var(--border) solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
  text-decoration: none;
  transition: background var(--duration) var(--ease), border-color var(--duration) var(--ease), color var(--duration) var(--ease);
}
/* Filled actions read --signal-solid / --signal-on, not --signal. On light
   the two are identical and this is pixel-identical to what it replaced. On
   dark they cannot be equal: the accent that carries link text at 9.4:1 is far
   too pale to sit under white button text. This is the seam between the
   working system and the dark theme. */
.btn-primary   { background: var(--signal-solid); color: var(--signal-on); border-color: var(--signal-solid); }
.btn-primary:hover { background: var(--signal-hover); border-color: var(--signal-hover); color: var(--signal-on); }
[data-theme="dark"] .btn-primary:hover { color: var(--ground); }
.btn-secondary { background: var(--surface); color: var(--ink); border-color: var(--rule); }
.btn-secondary:hover { border-color: var(--ink-3); color: var(--ink); }
.btn-ghost     { background: transparent; color: var(--signal); padding-inline: var(--space-2); }
.btn-ghost:hover { background: var(--signal-wash); }
/* Destructive actions (revoke a key, delete an account). Outlined rather
   than filled: these sit next to ordinary controls, and a solid red block
   pulls the eye toward the one action we least want mis-clicked. It fills
   in on hover, once the pointer is already committed to it. */
.btn-danger    { background: var(--surface); color: var(--status-critical-ink); border-color: var(--status-critical); }
.btn-danger:hover { background: var(--status-critical); color: var(--surface); border-color: var(--status-critical); }
.btn-sm { font-size: var(--text-xs); padding: var(--space-2) var(--space-3); }
.btn[disabled], .btn[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }

/* --------------------------------------------------- card ----------------- */

.card {
  background: var(--surface);
  border: var(--border) solid var(--rule);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
}
.card-tight { padding: var(--space-4); }
.card-title { font-family: var(--font-display); font-size: var(--text-lg); color: var(--ink); margin-bottom: var(--space-2); }

/* --------------------------------------------------- table ---------------- */
/* Built for schema listings and coverage tables: dense, tabular figures,
   header that survives a long scroll. */

.table-wrap { overflow-x: auto; border: var(--border) solid var(--rule); border-radius: var(--radius-lg); background: var(--surface); }

table.data { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
table.data th, table.data td { padding: var(--space-2) var(--space-4); text-align: left; border-bottom: var(--border) solid var(--rule-soft); }
table.data thead th {
  position: sticky; top: 0; z-index: 1;
  background: var(--surface);
  border-bottom: var(--border) solid var(--rule);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-3);
  white-space: nowrap;
}
table.data tbody tr:last-child td { border-bottom: none; }
table.data tbody tr:hover { background: var(--signal-wash); }
table.data td.num, table.data th.num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; text-align: right; }
table.data td.name { font-family: var(--font-mono); color: var(--ink); }
/* Monospace but left-aligned: dtypes, identifiers — things you scan down,
   not quantities you compare magnitudes of. */
table.data td.mono { font-family: var(--font-mono); color: var(--ink-2); }

/* --------------------------------------------------- key / value ---------- */
/* Spec lists — schema fields, file facts. */

.kv { display: grid; grid-template-columns: max-content 1fr; gap: var(--space-2) var(--space-5); font-size: var(--text-sm); }
.kv dt { font-family: var(--font-mono); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--ink-3); }
.kv dd { color: var(--ink); font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* --------------------------------------------------- status badge --------- */
/* A status never travels on colour alone: dot + text label, always. */

.badge {
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  padding: var(--space-1) var(--space-2);
  border: var(--border) solid var(--rule);
  border-radius: var(--radius-full);
  background: var(--surface);
  color: var(--ink-2);
  white-space: nowrap;
}
.badge::before { content: ""; width: 7px; height: 7px; border-radius: var(--radius-full); background: var(--ink-3); flex: none; }
.badge-good::before     { background: var(--status-good); }
.badge-warning::before  { background: var(--status-warning); }
.badge-serious::before  { background: var(--status-serious); }
.badge-critical::before { background: var(--status-critical); }
.badge-good     { color: var(--status-good-ink); }
.badge-warning  { color: var(--status-warning-ink); }
.badge-serious  { color: var(--status-serious-ink); }
.badge-critical { color: var(--status-critical-ink); }

/* --------------------------------------------------- code ----------------- */

/* 0.2em, not 0.35em, because the horizontal padding has to stay narrower
   than a word space. At 0.35em the chip padded 4.09px each side against a
   ~3.3px space in the surrounding prose, so "Base URL `…volardata.com`."
   rendered with the period detached and the preceding word double-spaced —
   spacing the markup never asked for. Measured: the gap from the box edge to
   the "." was 0px, i.e. all of it was this padding. Eleven places read that
   way. Vertical is untouched; it feeds line box height. */
code { font-family: var(--font-mono); font-size: 0.9em; background: var(--surface-sunk); padding: 0.1em 0.2em; border-radius: var(--radius-sm); color: var(--ink); }

/* A code block that must match the height of its neighbours in a grid row.
   The grid cell stretches; the <pre> inside it does not, so a short example
   beside a long one leaves a hole under the short one (69px of curl against
   192px of Python on /docs left 123px of nothing). */
.code-cell { display: flex; flex-direction: column; }
.code-cell > .code-block { flex: 1; }

.code-block {
  background: var(--surface-sunk);
  border: var(--border) solid var(--rule);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  overflow-x: auto;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  color: var(--ink);
}
.code-block code { background: none; padding: 0; }

.code-tabs { display: flex; gap: var(--space-1); margin-bottom: calc(var(--space-2) * -1); }
.code-tab {
  font-family: var(--font-mono); font-size: var(--text-xs);
  padding: var(--space-2) var(--space-3);
  border: var(--border) solid transparent; border-bottom: none;
  border-radius: var(--radius) var(--radius) 0 0;
  background: transparent; color: var(--ink-3); cursor: pointer;
}
.code-tab[aria-selected="true"] { background: var(--surface-sunk); border-color: var(--rule); color: var(--ink); }

/* --------------------------------------------------- form ----------------- */

.field { display: block; margin-bottom: var(--space-4); }
.field-label { display: block; font-size: var(--text-xs); font-weight: var(--weight-medium); color: var(--ink-2); margin-bottom: var(--space-2); }
.field-input {
  width: 100%;
  font-family: var(--font-body); font-size: var(--text-base);
  padding: var(--space-3);
  background: var(--surface);
  border: var(--border) solid var(--rule);
  border-radius: var(--radius);
  color: var(--ink);
}
.field-input:focus { border-color: var(--signal); }
.field-input[data-mono] { font-family: var(--font-mono); }
.field-help  { font-size: var(--text-xs); color: var(--ink-3); margin-top: var(--space-2); }
.field-error { font-size: var(--text-xs); color: var(--status-critical-ink); margin-top: var(--space-2); }

/* --------------------------------------------------- notice --------------- */

.notice {
  border: var(--border) solid var(--rule);
  border-left: var(--border-thick) solid var(--ink-3);
  border-radius: var(--radius);
  background: var(--surface);
  padding: var(--space-4);
  font-size: var(--text-sm);
}
.notice-good     { border-left-color: var(--status-good); }
.notice-warning  { border-left-color: var(--status-warning); }
.notice-critical { border-left-color: var(--status-critical); }

/* --------------------------------------------------- stat ----------------- */

/* Bottom-aligned: these sit in a stretched grid row, so a shorter value
   (.stat-value-sm) would otherwise pull its label up out of line with
   its neighbours — 15px on /schema/chains, where one value is a phrase
   set smaller on purpose. Aligning to the bottom keeps the labels on one
   line whatever size the values are. */
.stat { display: flex; flex-direction: column; justify-content: flex-end; gap: var(--space-1); }
.stat-value { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: var(--text-3xl); color: var(--ink); line-height: 1; }
.stat-value-sm { font-size: var(--text-lg); line-height: 1.2; }
.stat-label { font-family: var(--font-mono); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--ink-3); }

/* --------------------------------------------------- coverage strip ------- */
/* THE SIGNATURE ELEMENT.
   One bar per day. Height encodes snapshots captured that day; fill uses the
   sequential ramp (one hue — colour-vision-safe by construction). Gaps are
   marked separately below the strip with a labelled tick, so a gap is never
   communicated by colour alone.

   Redundant encoding is the point: a thin day is both SHORTER and LIGHTER. */

.coverage {
  /* No gap between bars. At 115 days across a wide container each bar is a
     few px, and a 1px gap is 15-20% of that width: the strip strobes into a
     barcode while the daily heights are near-uniform. Contiguous, it reads as
     one silhouette and the notches where capture actually dropped become the
     subject — which is the honest reading of the data anyway. */
  display: flex; align-items: flex-end; gap: 0;
  height: 56px;
  /* Bottom padding clears the gap tick, which hangs 9px below a bar. Without
     it, the scroll container introduced below clips the ticks — and the tick
     is the redundant encoding that stops a gap being colour-only. */
  padding: var(--space-2) var(--space-2) var(--space-4);
  background: var(--surface);
  border: var(--border) solid var(--rule);
  border-radius: var(--radius);
  /* One bar per day, and the strip gains a day every day: 115 now, 365 in a
     year. At 390px the bars hit their 2px floor and pushed the page 6px wide.
     Scrolling holds however long the record gets, where a breakpoint would
     only move the day it breaks. */
  overflow-x: auto;
  overscroll-behavior-x: contain;
}
.coverage-day {
  flex: 1 1 0; min-width: 2px;
  background: var(--seq-600);
  border-radius: 0;
  position: relative;
}
/* Completeness bands — darker means more complete. */
.coverage-day[data-band="full"]    { background: var(--seq-700); }
.coverage-day[data-band="high"]    { background: var(--seq-600); }
.coverage-day[data-band="partial"] { background: var(--seq-500); }
.coverage-day[data-band="low"]     { background: var(--seq-400); }
.coverage-day[data-gap]::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -6px; height: 3px;
  background: var(--status-critical); border-radius: var(--radius-sm);
}
.coverage-legend { display: flex; gap: var(--space-4); margin-top: var(--space-3); font-family: var(--font-mono); font-size: var(--text-2xs); color: var(--ink-3); }

/* Inline sparkline variant for tables and nav. */
/* gap: 0 for the same reason as .coverage above — a 1px gap on a 3px bar is
   a third of its width, and the strip reads as a barcode rather than a
   silhouette. This rule is later than .coverage and equal in specificity, so
   it has to restate it. */
.coverage-spark { height: 18px; padding: 0; border: none; background: none; gap: 0; }

/* --------------------------------------------------- nav / footer --------- */

.site-nav {
  border-bottom: var(--border) solid var(--rule);
  background: var(--surface);
  position: sticky; top: 0; z-index: 10;
}
.site-nav-inner { display: flex; align-items: center; justify-content: space-between; gap: var(--space-5); height: 56px; }
/* :not(.btn) matters. `.site-nav a` outranks `.btn-primary` on specificity
   (0,1,1 vs 0,1,0), so without this exclusion a button placed in the nav
   silently loses its own text colour — dark ink on the indigo fill. */
.site-nav a:not(.btn) { color: var(--ink-2); text-decoration: none; font-size: var(--text-sm); }
.site-nav a:not(.btn):hover { color: var(--ink); }
.site-nav .brand { font-family: var(--font-display); font-weight: var(--weight-bold); font-size: var(--text-lg); color: var(--ink); letter-spacing: var(--tracking-tight); }
.site-nav-links { display: flex; gap: var(--space-5); align-items: center; }

.site-footer {
  border-top: var(--border) solid var(--rule);
  margin-top: var(--space-9);
  padding-block: var(--space-7);
  font-size: var(--text-sm);
  color: var(--ink-3);
}

/* The links wrap to a second row here; they are never hidden.
   `.site-nav-links` wraps a Jinja block, so the shell cannot know which link
   in it matters — hiding it took "Get an API key" off every public page and
   "Sign out" off every signed-in one, leaving the wordmark alone in the bar.
   Three declarations have to travel together for wrapping to work:
     - the bar's 56px is a fixed height, so it would clip a second row rather
       than grow for it;
     - the group takes a full row of its own, so it wraps predictably instead
       of being squeezed into a narrow column beside the brand;
     - a header free to reach three rows must not also be pinned to the top of
       a phone screen, so it stops being sticky exactly where it can grow. */
@media (max-width: 700px) {
  .site-nav { position: static; }
  .site-nav-inner {
    height: auto;
    flex-wrap: wrap;
    padding-block: var(--space-3);
    gap: var(--space-3);
  }
  .site-nav-links {
    width: 100%;
    flex-wrap: wrap;
    gap: var(--space-3) var(--space-4);
  }
  .display { font-size: var(--text-3xl); }
  h1 { font-size: var(--text-2xl); }
}

/* --------------------------------------------------- disclosure ----------- */
/* Native <details> so the content stays readable, linkable and findable by
   in-page search with scripting off — an FAQ that only exists after JS runs
   is an FAQ search engines and cautious buyers never see. */

.faq details { border-bottom: var(--border) solid var(--rule); }
.faq summary {
  display: flex; justify-content: space-between; align-items: center; gap: var(--space-4);
  padding: var(--space-4) 0;
  cursor: pointer;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  color: var(--ink);
  list-style: none;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: '+'; font-family: var(--font-mono); color: var(--ink-3); flex-shrink: 0; }
.faq details[open] summary::after { content: '\2212'; }
.faq details > p {
  padding-bottom: var(--space-5);
  color: var(--ink-2);
  font-size: var(--text-sm);
  max-width: var(--measure-text);
}

/* --------------------------------------------------- consent ------------- */
/* Sits above the footer, does not cover content, and does not trap the page.
   Accept and Decline are the same size deliberately: refusing has to be as
   easy as agreeing, and a site selling published evidence should not be
   running a dark pattern at the door. */

.consent {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 90;
  background: var(--surface);
  border-top: var(--border) solid var(--rule);
  box-shadow: 0 -2px 12px rgba(13, 21, 25, 0.06);
}
.consent-inner {
  max-width: var(--measure-wide);
  margin-inline: auto;
  padding: var(--space-4) var(--space-5);
  display: flex;
  gap: var(--space-5);
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
}
.consent-text {
  font-size: var(--text-sm);
  color: var(--ink-2);
  max-width: var(--measure-text);
  margin: 0;
}
.consent-actions { display: flex; gap: var(--space-3); flex-shrink: 0; }

@media (max-width: 720px) {
  .consent-inner { align-items: stretch; }
  .consent-actions > .btn { flex: 1; }
}

/* --------------------------------------------------- theme toggle -------- */
/* Sits at the end of the nav. Mono and small: it is a preference control, not
   an action, and should not compete with the CTA beside it. */

.theme-toggle {
  background: none;
  border: var(--border) solid var(--rule);
  border-radius: var(--radius);
  color: var(--ink-3);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
  transition: color var(--duration) var(--ease), border-color var(--duration) var(--ease);
}
.theme-toggle:hover { color: var(--ink); border-color: var(--ink-3); }
