/* ============================================================================
   vetra.css — the design system
   ============================================================================

   ONE stylesheet, imported by every page. It exists because the audit found the
   site was 25 independent one-page websites sharing a logo:

     232 unique hex colours · 156 font sizes · 102 button signatures (81 used
     exactly once) · 60 card selectors · .card defined 7 different ways ·
     52 border-radii · 20 font-family declarations · 757 inline style attributes

   THE RULE THAT MAKES THIS DIFFERENT FROM THE LAST ATTEMPT:
   if a page needs something that is not in here, ADD IT HERE. Never inline it,
   never redefine it in a page <style> block. The moment one page defines its own
   .card, the whole thing is back to 60 selectors.

   Everything below is a token or a component built from tokens. There are no
   raw hex values, font sizes or radii outside the :root block.
   ============================================================================ */

/* ── TOKENS ───────────────────────────────────────────────────────────────── */

:root {
  /* Surfaces + text. */
  --surface:        #ffffff;
  --surface-sub:    #f6f9fc;
  --border:         #e3e8ee;
  --text-primary:   #1a1f36;
  /* CONTRAST — computed, not eyeballed (WCAG 2.1 relative luminance). The 2026-08-25
     homepage audit measured 25 text nodes below AA on this palette: --text-secondary
     was #697386 (4.78:1 on white, 4.52:1 on the sub-surface, 4.27:1 on a wash tint)
     and --text-muted was #8792a2, which reached only 2.98:1 on the sub-surface and
     failed on every surface the site has. Both are darkened here; the hue is unchanged.

       --text-secondary #4e5866  white 7.21:1 · sub 6.83:1 · wash-violet 6.44:1  ✓ AA
       --text-muted     #5c6577  white 5.86:1 · sub 5.55:1 · wash-violet 5.23:1  ✓ AA

     Muted stays a visible step lighter than secondary, which is the only job it has —
     it just no longer buys that step by falling under the threshold. */
  --text-secondary: #4e5866;
  --text-muted:     #5c6577;
  --accent:         #635bff;

  /* Status. Background/foreground pairs — never used apart. */
  --ok-bg:   #cbf4c9;  --ok-fg:   #0e6245;
  --warn-bg: #f8e5b9;  --warn-fg: #983705;
  --bad-bg:  #fce9ec;  --bad-fg:  #a41c4e;

  /* Radii — three. Anything else is a bug. */
  --r-sm: 6px;
  --r-md: 8px;
  --r-lg: 12px;

  /* Type scale — SIX sizes, no more.
     --t-body was 15px and --t-micro 11px until the 2026-08-25 audit. 15px body is
     under every mainstream reading-comfort floor, and 11px microcopy is small enough
     that darkening the colour alone does not rescue it. Raised to 16 and 12; the
     scale still has six distinct steps, which is the rule this block enforces. */
  --t-display: 36px;
  --t-title:   24px;
  --t-lede:    18px;
  --t-body:    16px;
  --t-small:   13px;
  --t-micro:   12px;

  /* Weights — TWO. Never 600, never 700. The old site used six. */
  --w-normal: 400;
  --w-medium: 500;

  /* Wash tints. Card backgrounds behind product art ONLY — never behind text,
     because none of them carries 4.5:1 against --text-secondary. */
  --wash-violet: #f4f0ff;
  --wash-warm:   #fff3ec;
  --wash-cool:   #eef5ff;

  /* Dark band — the inverted sections. Derived from the light tokens rather
     than being a second palette, which is how the old site ended up with 232
     colours. */
  --dark-surface:     #0e1018;
  --dark-surface-sub: #171a24;
  --dark-border:      #262b3a;
  --dark-text:        #ffffff;
  --dark-secondary:   #a4abbd;
  /* #6f7688 measured 4.18:1 on --dark-surface and 3.82:1 on --dark-surface-sub — the
     dark half of the same audit finding, and it matters more now that the demo section
     (a dark band) sits directly under the hero. #949cae is 6.90:1 / 6.30:1 ✓ AA. */
  --dark-muted:       #949cae;

  --space:   16px;
  --measure: 68ch;          /* readable line length */
  --shell:   1120px;        /* page gutter width */

  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  --ease: cubic-bezier(0.2, 0.7, 0.3, 1);
}

/* ── CAFÉ SKIN ────────────────────────────────────────────────────────────────
   A scoped warm surface for /cafes. Verticals should be recognisable SIBLINGS you
   can tell apart at a glance — /cafes and /bars had drifted to the same violet and
   read as the same page twice.

   Re-points the shared token VALUES rather than forking a stylesheet, exactly like
   `.v-section--dark` below. That is the whole point: every component, every dark
   band, every card keeps working with no café-specific variants, and there is one
   design system rather than two. A forked cafes stylesheet is the thing this
   pattern exists to prevent.

   ⛔ Every colour on /cafes must come from here. No raw hex in cafes.html or
   cafes-tour.css — that rule is asserted by test, because the reason this block
   exists at all is that colours had leaked into the page.

   CONTRAST — computed, not eyeballed (WCAG 2.1 relative luminance):
     body text on page        #756757 on #fdfaf6  5.26:1  ✓ AA
     body text on sub-surface #756757 on #f7efe4  4.80:1  ✓ AA   ← the one that bit
     headings on page         #26201a on #fdfaf6 15.48:1  ✓
     white button text        #ffffff on #c0551d   4.61:1  ✓ AA
     H1 italic accent (36px)  #c0551d on #fdfaf6   4.43:1  ✓ AA-large (needs 3:1)
     eyebrow / micro (≥11px, uppercase, medium) #928470 on #f7efe4 3.20:1 ✓ large-text

   Two values are deliberately a shade darker than the palette as first specified:
   --text-secondary was #7b6d5f, which measured 4.39:1 on the sub-surface and FAILED
   AA on every alternating section; --text-muted was #9c8e80 at 2.79:1 on the same
   surface. Both were fine on the page colour alone, which is exactly how this gets
   missed. The hue is unchanged. */

body.cafe-skin {
  --surface:        #fdfaf6;   /* warm paper */
  --surface-sub:    #f7efe4;   /* alternating band */
  --border:         #e8dcc9;
  --accent:         #c0551d;   /* roasted orange — fills, borders, focus. NEVER body text. */
  --text-primary:   #26201a;
  --text-secondary: #756757;
  --text-muted:     #928470;

  /* Espresso, not the default blue-black — a cool dark band under a cream page
     reads as a different site. */
  --dark-surface:     #1c1511;
  --dark-surface-sub: #271e18;
  --dark-border:      #3a2e25;
  --dark-text:        #fdfaf6;
  --dark-secondary:   #b8a795;
  --dark-muted:       #8c7c6c;

  /* The wash tints are cool by default and turn grey-violet on cream. */
  --wash-warm:   #fdf1e8;
  --wash-violet: #f7efe4;
  --wash-cool:   #f3f0e8;

  background: var(--surface);
  color: var(--text-primary);
}

/* ── RESET ────────────────────────────────────────────────────────────────── */

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

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

body {
  margin: 0;
  font-family: var(--font);
  font-size: var(--t-body);
  font-weight: var(--w-normal);
  line-height: 1.55;
  color: var(--text-primary);
  background: var(--surface);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4, p, figure, ul, ol { margin: 0; }
ul, ol { padding: 0; list-style: none; }
img, svg { max-width: 100%; display: block; }
button, input, select, textarea { font: inherit; color: inherit; }
a { color: inherit; }

/* Visible focus everywhere. Non-negotiable — it is the only affordance a
   keyboard user has, and the demos below are all keyboard-operable. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

/* ── LAYOUT ───────────────────────────────────────────────────────────────── */

.v-shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: calc(var(--space) * 1.5);
}

.v-section { padding-block: calc(var(--space) * 4.5); }

.v-section--sub  { background: var(--surface-sub); }

/* The inverted band. Re-points the text tokens rather than overriding every
   child, so components inside a dark band need no dark-mode variants at all. */
.v-section--dark {
  background: var(--dark-surface);
  --surface:        var(--dark-surface);
  --surface-sub:    var(--dark-surface-sub);
  --border:         var(--dark-border);
  --text-primary:   var(--dark-text);
  --text-secondary: var(--dark-secondary);
  --text-muted:     var(--dark-muted);
  color: var(--text-primary);
}

.v-stack   { display: flex; flex-direction: column; gap: var(--space); }
.v-stack-s { display: flex; flex-direction: column; gap: calc(var(--space) / 2); }
.v-row     { display: flex; align-items: center; gap: calc(var(--space) / 2); flex-wrap: wrap; }

.v-grid { display: grid; gap: var(--space); }
@media (min-width: 720px) {
  .v-grid--2 { grid-template-columns: repeat(2, 1fr); }
  .v-grid--3 { grid-template-columns: repeat(3, 1fr); }
  .v-grid--4 { grid-template-columns: repeat(4, 1fr); }
}

/* ── TYPE ─────────────────────────────────────────────────────────────────── */

.v-display { font-size: var(--t-display); font-weight: var(--w-medium); line-height: 1.12; letter-spacing: -0.022em; }
.v-title   { font-size: var(--t-title);   font-weight: var(--w-medium); line-height: 1.25; letter-spacing: -0.014em; }
.v-lede    { font-size: var(--t-lede);    font-weight: var(--w-normal); line-height: 1.5;  color: var(--text-secondary); max-width: var(--measure); }
.v-body    { font-size: var(--t-body);    color: var(--text-secondary); max-width: var(--measure); }
.v-small   { font-size: var(--t-small);   color: var(--text-secondary); }
.v-micro   { font-size: var(--t-micro);   color: var(--text-muted); }

.v-eyebrow {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-weight: var(--w-medium);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.v-strong { color: var(--text-primary); font-weight: var(--w-medium); }
.v-mono   { font-family: var(--mono); font-variant-numeric: tabular-nums; }

@media (min-width: 860px) {
  :root { --t-display: 48px; --t-title: 30px; }
}

/* ── BUTTON — one component, three tones ─────────────────────────────────── */

.v-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: calc(var(--space) / 2);
  padding: 10px 18px;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  font-size: var(--t-body);
  font-weight: var(--w-medium);
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  background: var(--accent);
  color: #fff;
  transition: filter .15s var(--ease), background .15s var(--ease);
}
.v-btn:hover { filter: brightness(1.08); }
.v-btn:active { filter: brightness(0.96); }
.v-btn[disabled] { opacity: .55; cursor: not-allowed; filter: none; }

.v-btn--secondary {
  background: var(--surface);
  color: var(--text-primary);
  border-color: var(--border);
}
.v-btn--secondary:hover { background: var(--surface-sub); filter: none; }

.v-btn--quiet {
  background: transparent;
  color: var(--text-secondary);
  border-color: transparent;
  padding-inline: 8px;
}
.v-btn--quiet:hover { color: var(--text-primary); background: var(--surface-sub); filter: none; }

.v-btn--sm { padding: 6px 12px; font-size: var(--t-small); }

/* ── CARD — one definition. The old site had seven. ──────────────────────── */

.v-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: calc(var(--space) * 1.25);
}
.v-card--flush { padding: 0; overflow: hidden; }
.v-card--wash-violet { background: var(--wash-violet); border-color: transparent; }
.v-card--wash-warm   { background: var(--wash-warm);   border-color: transparent; }
.v-card--wash-cool   { background: var(--wash-cool);   border-color: transparent; }

/* ── PILL — status. bg/fg pairs only, so contrast is always a known quantity */

.v-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 9px;
  border-radius: 999px;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-weight: var(--w-medium);
  letter-spacing: 0.04em;
  white-space: nowrap;
}
.v-pill--ok   { background: var(--ok-bg);   color: var(--ok-fg); }
.v-pill--warn { background: var(--warn-bg); color: var(--warn-fg); }
.v-pill--bad  { background: var(--bad-bg);  color: var(--bad-fg); }
.v-pill--neutral { background: var(--surface-sub); color: var(--text-secondary); box-shadow: inset 0 0 0 1px var(--border); }

.v-dot { width: 6px; height: 6px; border-radius: 2px; background: currentColor; flex: none; }

/* ── FIELD ────────────────────────────────────────────────────────────────── */

.v-field { display: flex; flex-direction: column; gap: 5px; }
.v-label { font-size: var(--t-small); font-weight: var(--w-medium); color: var(--text-primary); }

.v-input {
  width: 100%;
  padding: 9px 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  font-size: var(--t-body);
  color: var(--text-primary);
  transition: border-color .15s var(--ease), box-shadow .15s var(--ease);
}
.v-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
.v-input::placeholder { color: var(--text-muted); }

.v-hint  { font-size: var(--t-small); color: var(--text-muted); }
.v-error { font-size: var(--t-small); color: var(--bad-fg); }

/* ── CAPTURE — the inline two-field lead form ─────────────────────────────────
   Added 2026-08-25. The homepage audit's second critical finding was that the page
   had zero forms and zero inputs: a visitor who cannot phone a demo line at 11pm
   left no trace at all. This is the ONE component both of its capture points reuse,
   which is also why it lives here rather than in a page block.

   It sits inside a .v-section--dark band on the homepage, so every colour resolves
   through the token indirection that band re-points — no dark variant needed. */

/* [hidden] must beat the display below, exactly as it does for .v-modal. The form
   ships hidden and assets/index-cta.js reveals it once bound — without this rule
   the class selector wins, the attribute does nothing, and a form that cannot
   submit is shown to every visitor whose JS never ran. */
.v-capture[hidden] { display: none; }
.v-capture { display: flex; flex-direction: column; gap: calc(var(--space) / 2); max-width: 34rem; }

.v-capture__row { display: flex; flex-wrap: wrap; gap: calc(var(--space) / 2); }
.v-capture__row .v-field { flex: 1 1 12rem; }

/* Off-screen rather than display:none — a hidden input is skipped by some bots,
   and being skipped is the one thing a honeypot must not be. Not tabbable, not
   announced, not visible. */
.v-capture__trap {
  position: absolute; left: -9999px;
  width: 1px; height: 1px; overflow: hidden;
}

/* ── MODAL ────────────────────────────────────────────────────────────────────
   Added 2026-08-25 to carry the demo scheduler the homepage never had. auto.html
   already ran this pattern in Tailwind (#scheduler-modal / #scheduler-iframe); this
   is the same behaviour expressed in tokens so the homepage does not have to load a
   second design system to open one dialog.

   THE LAZINESS IS LOAD-BEARING: the iframe ships as src="about:blank" and is only
   pointed at Calendly on open. Calendly's embed is ~1.7MB; eager-loading it would
   breach the page-weight and Lighthouse budgets that run on this file. */

.v-modal[hidden] { display: none; }
.v-modal {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space);
}
.v-modal__scrim { position: absolute; inset: 0; background: rgba(10, 12, 20, 0.72); border: 0; padding: 0; }
.v-modal__panel {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 900px;
  height: min(80vh, 760px);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.v-modal__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space);
  padding: 11px calc(var(--space) * 0.875);
  border-bottom: 1px solid var(--border);
}
.v-modal__frame { flex: 1; width: 100%; border: 0; background: var(--surface); }
/* The dialog's own way out when the third-party embed does not render. flex-shrink: 0 so it
   survives the frame above it claiming the space, and a top border so it reads as a footer
   rather than as part of the embed. Both copies of the markup — index.html's inline modal and
   the one assets/index-cta.js builds — use this class. */
.v-modal__fallback {
  flex: 0 0 auto;
  margin: 0;
  padding: 10px calc(var(--space) * 0.875);
  border-top: 1px solid var(--border);
  background: var(--surface);
}

/* ── TABLE ────────────────────────────────────────────────────────────────── */

.v-table { width: 100%; border-collapse: collapse; font-size: var(--t-small); }
.v-table th {
  text-align: left;
  padding: 9px 12px;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-weight: var(--w-medium);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border);
}
.v-table td { padding: 11px 12px; border-bottom: 1px solid var(--border); vertical-align: middle; }
.v-table tr:last-child td { border-bottom: none; }

/* Wide content must scroll inside its own box — the page body never scrolls
   sideways. */
.v-scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* ── HONESTY BLOCK — required on every vertical page ─────────────────────── */

/* The audit's strongest finding was that /pilot's "What it doesn't do yet" is
   the best writing on the site. This makes that a component rather than a
   thing one page happened to do well. */
.v-honesty {
  border-left: 3px solid var(--warn-fg);
  background: var(--surface-sub);
  border-radius: 0 var(--r-md) var(--r-md) 0;
  padding: var(--space) calc(var(--space) * 1.25);
}
.v-honesty > * + * { margin-top: 8px; }

/* ── EXPAND CARD — the three-level pattern ───────────────────────────────── */

/* CARD (collapsed) → PANEL (subhead + benefits + live mini-UI) → DETAIL (an
   interaction the visitor performs). Levels are driven by a data-level attribute
   on the root so the state is inspectable and testable, rather than living in
   a pile of toggled classes. */
.v-expand {
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  overflow: hidden;
  transition: border-color .2s var(--ease);
}
.v-expand[data-level="1"]:hover { border-color: var(--text-muted); }
.v-expand[data-level="2"], .v-expand[data-level="3"] { border-color: var(--accent); }

.v-expand__head {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--space);
  width: 100%;
  padding: calc(var(--space) * 1.1) calc(var(--space) * 1.25);
  background: none;
  border: 0;
  text-align: left;
  cursor: pointer;
}
.v-expand__num {
  font-family: var(--mono);
  font-size: var(--t-micro);
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 3px 7px;
}
.v-expand__title { font-size: var(--t-lede); font-weight: var(--w-medium); letter-spacing: -0.012em; }
.v-expand__sub   { font-size: var(--t-small); color: var(--text-secondary); margin-top: 3px; }

.v-expand__chev {
  width: 20px; height: 20px;
  color: var(--text-muted);
  transition: transform .2s var(--ease);
}
.v-expand[data-level="2"] .v-expand__chev,
.v-expand[data-level="3"] .v-expand__chev { transform: rotate(180deg); color: var(--accent); }

.v-expand__panel { display: none; padding: 0 calc(var(--space) * 1.25) calc(var(--space) * 1.25); }
.v-expand[data-level="2"] .v-expand__panel,
.v-expand[data-level="3"] .v-expand__panel { display: block; }

.v-expand__detail { display: none; margin-top: var(--space); }
.v-expand[data-level="3"] .v-expand__detail { display: block; }

/* Benefit list — four checkmarked lines, per the spec. */
.v-benefits { display: grid; gap: 7px; }
@media (min-width: 640px) { .v-benefits { grid-template-columns: 1fr 1fr; } }
.v-benefit {
  display: grid;
  grid-template-columns: 16px 1fr;
  gap: 9px;
  font-size: var(--t-small);
  color: var(--text-secondary);
}
.v-benefit__tick { color: var(--ok-fg); width: 16px; height: 16px; margin-top: 1px; }
.v-section--dark .v-benefit__tick { color: #5ad19a; }

/* ── FAQ — the answer family (/faq and its /faq-<vertical> siblings) ─────── */

/* Built ON TOP of .v-expand rather than beside it: /bars and /cafes already
   ship a correctly wired accordion (a real <button>, aria-expanded,
   aria-controls) and a second one would be the 60-card-selectors problem
   starting over. Everything here is spacing, the scope badge and answer prose. */

.faq-nav { display: flex; flex-wrap: wrap; gap: 8px 20px; }
.faq-nav a { font-size: var(--t-small); color: var(--text-secondary); text-decoration: none; }
.faq-nav a:hover { color: var(--text-primary); }

.faq-list { display: grid; gap: 10px; }

/* Title and badge stack, so the badge reads as a label ON the question rather
   than as part of it. .v-expand__title is inline by default. */
.faq-item .v-expand__title { display: block; }

/* THE SCOPE BADGE. Every answer whose terms belong to ONE trade carries one,
   so a bar manager arriving from the shared nav never reads an auto-repair
   number as their own. It is markup, so it does NOT survive being scraped or
   read aloud — that is why the answer text names the trade in words too, and
   why the test asserts both. */
.faq-scope {
  display: inline-block;
  margin-top: 6px;
  padding: 2px 9px;
  border-radius: 999px;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-weight: var(--w-medium);
  letter-spacing: 0.04em;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 28%, transparent);
}

.faq-answer { max-width: var(--measure); color: var(--text-secondary); }
.faq-answer > * + * { margin-top: 10px; }
.faq-answer strong { color: var(--text-primary); font-weight: var(--w-medium); }
.faq-answer a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.faq-answer ul { display: grid; gap: 7px; }
.faq-answer li { position: relative; padding-left: 15px; }
.faq-answer li::before { content: "—"; position: absolute; left: 0; color: var(--text-muted); }

/* ANSWERS RENDER OPEN UNTIL JS CONFIRMS IT CAN COLLAPSE THEM. Same reasoning as
   .v-reveal below: a page whose script fails shows all of its content rather
   than a column of headings with every answer invisible. assets/faq.js stamps
   data-faq-ready on <html> before it binds anything.

   Keyed to .faq-item, the same hook assets/faq.js binds — NOT to the .faq-list
   wrapper. A sibling page that groups its items differently still gets both the
   fail-open default and the collapse, and there is exactly one class to get
   right rather than two that must agree. */
.faq-item .v-expand__panel { display: block; }
[data-faq-ready] .faq-item[data-level="1"] .v-expand__panel { display: none; }

/* The pilot router on /faq — four destinations, no second capture form. */
.faq-routes { display: grid; gap: 10px; }
@media (min-width: 720px) { .faq-routes { grid-template-columns: repeat(2, 1fr); } }
.faq-route { display: grid; gap: 5px; text-decoration: none; transition: border-color .15s var(--ease); }
.faq-route:hover { border-color: var(--accent); }
.faq-route__ico { display: inline-block; width: 15px; height: 15px; vertical-align: -2px; color: var(--accent); }

/* ── PRODUCT SURFACE — where the real UI renders ─────────────────────────── */

/* Everything inside .v-ui is product chrome, not marketing chrome. It stays on
   the light tokens even inside a dark band, because that is what the dashboard
   actually looks like and the whole point is that this IS the dashboard.

   ⚠️ THESE ARE PINNED COPIES OF THE :root LIGHT TOKENS. When a value up there
   changes, change it here too — this block does not inherit it. Caught in a
   browser on PR #995: the AA contrast fix updated :root and missed this, so the
   hero's product card — the most-seen element on the site, above the fold on
   every visit — kept rendering .v-micro at the old #8792a2, which is 3.15:1 on
   white and the exact failure the fix existed to remove. A grep-level test
   cannot see this; only a computed style can. */
.v-ui {
  --surface:        #ffffff;
  --surface-sub:    #f6f9fc;
  --border:         #e3e8ee;
  --text-primary:   #1a1f36;
  --text-secondary: #4e5866;   /* 7.21:1 on #ffffff · 6.83:1 on #f6f9fc ✓ AA */
  --text-muted:     #5c6577;   /* 5.86:1 on #ffffff · 5.55:1 on #f6f9fc ✓ AA */
  background: var(--surface);
  color: var(--text-primary);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.v-ui__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space);
  padding: 9px 12px;
  background: var(--surface-sub);
  border-bottom: 1px solid var(--border);
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.v-ui__body { padding: calc(var(--space) * 0.9); }
.v-ui__body--flush { padding: 0; }

/* A row in a product table that can change state live. */
.v-ui-row {
  display: grid;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  font-size: var(--t-small);
  transition: background .35s var(--ease);
}
.v-ui-row:last-child { border-bottom: none; }
.v-ui-row[data-state="warn"] { background: color-mix(in srgb, var(--warn-bg) 45%, var(--surface)); }
.v-ui-row[data-state="ok"]   { background: color-mix(in srgb, var(--ok-bg) 40%, var(--surface)); }

/* Fires when a row changes because ANOTHER demo did something to it. That
   cross-demo write-back is the platform argument, so it gets a deliberate
   visual beat rather than a silent swap. */
@keyframes v-flash {
  0%   { box-shadow: inset 0 0 0 2px transparent; }
  35%  { box-shadow: inset 0 0 0 2px var(--accent); }
  100% { box-shadow: inset 0 0 0 2px transparent; }
}
.v-flash { animation: v-flash 1.1s var(--ease); }


/* ── NAV + FOOTER ─────────────────────────────────────────────────────────
   The shared chrome. These live HERE, not in a <style> block inside
   partials/nav.html and partials/footer.html, because those partials are
   expanded into every page — inline styles there would put a duplicate copy of
   this CSS on every page of the site and reintroduce exactly the drift this
   file exists to end. The rule at the top of this file applies to partials too. */

  .v-nav {
    position: sticky; top: 0; z-index: 50;
    background: color-mix(in srgb, var(--surface) 88%, transparent);
    backdrop-filter: saturate(1.6) blur(12px);
    border-bottom: 1px solid var(--border);
  }
  .v-nav__inner { display: flex; align-items: center; gap: var(--space); height: 60px; }
  .v-nav__brand {
    display: inline-flex; align-items: center; gap: 8px;
    font-weight: var(--w-medium); letter-spacing: 0.13em;
    font-size: var(--t-small); text-decoration: none; color: var(--text-primary);
  }
  .v-nav__mark { width: 9px; height: 9px; border-radius: 2px; background: var(--accent); }
  .v-nav__links { display: none; gap: 22px; margin-inline: auto; }
  .v-nav__links a {
    font-size: var(--t-small); color: var(--text-secondary);
    text-decoration: none; transition: color .15s var(--ease);
  }
  .v-nav__links a:hover { color: var(--text-primary); }
  .v-nav__actions { display: none; gap: 8px; align-items: center; }
  .v-nav__toggle {
    margin-left: auto; display: grid; gap: 4px; padding: 8px;
    background: none; border: 0; cursor: pointer;
  }
  .v-nav__toggle span { display: block; width: 18px; height: 1.5px; background: var(--text-primary); }
  .v-nav__menu { border-top: 1px solid var(--border); padding-block: var(--space); background: var(--surface); }
  .v-nav__menu a { font-size: var(--t-body); color: var(--text-secondary); text-decoration: none; padding-block: 6px; }
  .v-nav__menu a.v-btn { color: #fff; margin-top: 6px; }

  @media (min-width: 860px) {
    .v-nav__links, .v-nav__actions { display: flex; }
    .v-nav__toggle { display: none; }
    .v-nav__menu { display: none !important; }
  }

  .v-footer { border-top: 1px solid var(--border); padding-block: calc(var(--space) * 2); background: var(--surface); }
  .v-footer__top { display: flex; flex-wrap: wrap; gap: var(--space); align-items: center; justify-content: space-between; margin-bottom: var(--space); }
  .v-footer__brand {
    display: inline-flex; align-items: center; gap: 8px;
    font-weight: var(--w-medium); letter-spacing: 0.13em;
    font-size: var(--t-small); text-decoration: none; color: var(--text-primary);
  }
  .v-footer__mark { width: 9px; height: 9px; border-radius: 2px; background: var(--accent); }
  .v-footer__links { display: flex; flex-wrap: wrap; gap: 6px 18px; }
  .v-footer__links a { font-size: var(--t-small); color: var(--text-secondary); text-decoration: none; }
  .v-footer__links a:hover { color: var(--text-primary); }
  .v-footer__legal { margin-top: var(--space); font-size: var(--t-micro); color: var(--text-muted); max-width: var(--measure); }

/* ── UTILITIES ────────────────────────────────────────────────────────────── */

.v-hidden { display: none !important; }
.v-sr {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.v-divider { height: 1px; background: var(--border); border: 0; margin: 0; }

/* Reveal-on-scroll. Defined once, here, so no page ships its own.
   Starts VISIBLE and is only hidden once JS confirms it can animate — a page
   that fails to run JS shows all its content rather than a blank column, which
   is how scroll-reveal usually breaks. */
.v-reveal { opacity: 1; }
[data-reveal-ready] .v-reveal {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity .5s var(--ease), transform .5s var(--ease);
}
[data-reveal-ready] .v-reveal.is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  [data-reveal-ready] .v-reveal { opacity: 1; transform: none; }
}
