/* vokt — the design system, as built.
 *
 * Every value here is lifted from the artboards in the docs repo at
 * vokt/design/*.dc.html, not approximated. Where a number looks oddly specific
 * (1.5px borders, 12.5px help text, 52px fields against 56px buttons) it is
 * because the artboard says so. Do not round them to a grid.
 *
 * Tokens are the canonical set from vokt/brand/tokens.css. If they diverge,
 * that file wins and this one is wrong.
 */

/* ---- tokens ------------------------------------------------------------- */

:root {
  /* ⚠️ LIGHT ONLY, DELIBERATELY. All 19 artboards are light and dark mode is
     on the deferred design list, so there is nothing to build from. A dark
     palette existed here briefly and shipped a theme nobody had designed to
     anyone whose OS was set to dark, which is worse than not offering one.
     color-scheme below tells the browser so, which also keeps form controls
     and scrollbars light instead of the browser darkening them on its own.
     The dark token values are already in vokt/brand/tokens.css for when the
     screens are designed. */
  color-scheme: light;

  --paper: #E4E5DE;
  --panel: #EDEEE8;
  /* Lighter than panel. Used where something should read as a physical object
     rather than a surface: the plate on screen 4, the cards on the home screen. */
  --plate: #F7F8F4;
  --rule:  #C9CBC3;
  --ink:   #15181C;
  --ink-2: #565C64;
  /* ⚠️ --ink-3 clears 4.5:1 on --ink (the inverted panels) but only ~3.1:1 on
     --paper and --panel, so it FAILS WCAG AA for body text on a light ground.
     It is now used only for text on the dark panels, for genuinely inactive
     controls (the "coming soon" cards, the SOON pill), and for hairlines and
     decorative marks. Every readable label, note, caption and timestamp on a
     light surface uses --ink-2. Do not reach for --ink-3 for light-ground
     text again — 2026-09-10 audit. */
  --ink-3: #7B818A;
  --on-accent: #EDEEE8;

  --font: Figtree, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --display-tracking: -0.032em;
  --ease: cubic-bezier(0.4, 0, 0.6, 1);

  /* The artboard is 390 wide with 24 of padding each side. */
  --frame: 390px;
  --gutter: 24px;
  --field-h: 52px;
  --control-h: 56px;
  --radius: 12px;
}


/* ---- frame -------------------------------------------------------------- */

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;   /* dvh so mobile browser chrome does not clip the footer */
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font);
  -webkit-font-smoothing: antialiased;
  display: flex;
  justify-content: center;
}

/* The artboard, as a column. Capped at the design width on a large screen
   rather than stretched: these are phone layouts and a 1400px-wide sign-in
   form is not a wider version of the design, it is a different one. */
.app {
  width: 100%;
  max-width: var(--frame);
  min-height: 100vh;
  min-height: 100dvh;
  padding: 56px var(--gutter) 34px;
  display: flex;
  flex-direction: column;
  gap: 28px;
}

/* The artboards use 26px on the screens that carry a card — Vehicle, Not
   supported, Confirmed, Plan, Tesla — and 28px on the plain form screens.
   Two values, both theirs. */
.app--tight { gap: 26px; }

/* Screen 7 is wider-margined and looser than the rest: 32px sides, 34px gap.
   It has one thing on it and the extra air is what makes the wait feel calm
   rather than empty. */
.app--wide { padding: 56px 32px 34px; gap: 34px; }

/* ⚠️ The tabbed screens are a DIFFERENT frame, not the same one with a bar
   stuck on. 20px gutter rather than 24, 18px between blocks rather than 28,
   and no bottom padding at all because the tab bar supplies it. Built with
   the onboarding values first, which made every tab read as too loose and
   too narrow at once. */
.app--tabbed {
  padding: 56px 20px 0;
  gap: 18px;
}

/* Pushes whatever follows to the bottom of the frame. */
.spacer { flex: 1; min-height: 12px; }

/* ---- top bar ------------------------------------------------------------ */

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex: none;
}
.topbar__logo { display: block; color: var(--ink); }
/* Balances the back chevron so the logo sits optically centred. */
.topbar__spacer { width: 22px; flex: none; }
/* The chevron reads as 22px but the tap area is 44px: a ::before takes the
   slack outward without moving anything in the row, so the layout is still
   built on the 22px glyph. Below the WCAG 24px / iOS 44pt minimum before this. */
.topbar__back {
  display: block; flex: none; color: var(--ink); background: none;
  border: 0; padding: 0; cursor: pointer; position: relative;
}
.topbar__back::before {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: 44px; height: 44px; transform: translate(-50%, -50%);
}

.topbar--centred { justify-content: center; }

/* ---- progress ----------------------------------------------------------- */

.steps { display: flex; gap: 6px; flex: none; }
.steps__step { flex: 1; height: 3px; border-radius: 2px; background: var(--rule); }
.steps__step--done { background: var(--ink); }

/* ---- type --------------------------------------------------------------- */

.block { display: flex; flex-direction: column; gap: 12px; flex: none; }

.display {
  font-weight: 800;
  letter-spacing: var(--display-tracking);
  line-height: 1.03;
  font-size: 32px;
  margin: 0;
  text-wrap: pretty;
}
/* The welcome screen sets its headline much larger and tighter. */
.display--hero { font-size: 48px; line-height: 1.04; }

/* The display tracking collides vo/kt when the word appears in headline text.
   See vokt/brand/README.md. Wrap the word, never the whole line. */
.wordmark { letter-spacing: 0; }

.lede { font-size: 15px; font-weight: 400; line-height: 1.55; color: var(--ink-2); margin: 0; }

/* ---- legal document headings --------------------------------------------- */
/* /privacy and /terms, redesigned 2026-09-23 to read as a contract rather
   than another settings screen — numbered <h2>s, not .field__label's small
   caps kicker style. Lives here rather than scoped under site.css's .legal
   because base.html never loads site.css, and Settings links to these pages
   from inside the app too; the 390px frame needs the same headings, just not
   the desktop-width reading column site.css adds on top for the brochure. */
.legal-h2 {
  font-size: 17px;
  font-weight: 700;
  line-height: 1.3;
  color: var(--ink);
  margin: 0;
  padding-top: 18px;
  border-top: 1px solid var(--rule);
}
/* The first section has nothing above it to separate from. */
.legal-h2--first { padding-top: 0; border-top: none; }

/* The date line under the H1. Plain and small on purpose — every reference
   page checked (Basecamp, Linear, PocketSmith) treats this as metadata, not
   as copy competing with the lede above it. */
.legal-meta { font-size: 13px; color: var(--ink-2); margin: 0; }

/* Jump links to each numbered section, added 2026-09-26 — /privacy and
   /terms are long enough now (12 and 16 sections) that finding one clause
   means reading past everything before it otherwise.

   ⚠️ NO BOX, AND THE HEADING IS A REAL .legal-h2, NOT A SMALLER LABEL OF ITS
   OWN. The first version used .panel-card-style framing (background, border,
   padding) with a small-caps heading, and it — along with the plain-English
   summary above it, styled the same mismatched way — turned out to shrink
   the WHOLE page's reading column on desktop, not just itself, when a
   max-width fix was tried on it the same day. Reverted rather than chased
   further: "In plain English" and "Contents" are now `<h2 class="legal-h2">`
   like every numbered section, first one carrying `.legal-h2--first` so the
   page opens without a divider above it and the numbered sections pick one
   up instead, now that they are no longer first. One heading style for the
   whole document rather than a second, smaller one invented for these two. */
.legal-toc {
  display: flex; flex-direction: column; gap: 6px;
}
.legal-toc a { font-size: 13.5px; color: var(--ink-2); text-decoration: none; }
.legal-toc a:hover, .legal-toc a:focus-visible {
  color: var(--ink); text-decoration: underline;
}

/* A legal document has to work handed to a lawyer as a printout, not only
   read on screen. Nothing else in the product had a print stylesheet before
   this — added for /privacy and /terms specifically, 2026-09-26. */
@media print {
  .topbar, .legal-toc, .site-head, .site-nav, .site-foot { display: none; }
  body { background: #fff; color: #000; }
  .legal-h2 { break-inside: avoid; break-after: avoid; }
  a { color: #000; text-decoration: underline; }
}

/* ⚠️ THE OTHER MISSING RUNG, next to .lede--hero below. .display (32px/800) is
   a page's own h1; .lede (15px/400) is body copy. Neither fits a SECTION
   heading on a long-form page — About was the first page with more than one
   real section, and field__label (11px uppercase, built for a form field's
   caption) got pressed into that job and read SMALLER than the body text
   sitting under it. Found 2026-09-22, reviewing the About rewrite on staging.
   Not .card__name (26px/800): that is a vehicle-name card component
   elsewhere and would carry that association here. */
.section-title {
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.3;
  font-size: 19px;
  margin: 0;
}

/* The About page's own wordmark, at hero scale rather than the 74x23 topbar
   size _logo.html ships at everywhere else it is included. Scoped to this
   wrapper class so no other screen using _logo.html is affected. */
.hero-mark svg { display: block; width: 132px; height: auto; }

/* ⚠️ A dedicated class, not .lede--hero, though the role is the same one that
   class was built for (a subhead paired with a prominent visual above it).
   `.legal .lede { font-size: 16px }` overrides ANY lede-classed element
   inside the marketing frame regardless of its other classes, so an element
   carrying both `lede` and `lede--hero` would render at a different size in
   each frame — wrong for a slogan, which should look the same everywhere.
   A class that is not literally named "lede" sidesteps that collision. */
.tagline {
  font-size: 19px;
  font-weight: 500;
  line-height: 1.35;
  color: var(--ink-2);
  margin: 0;
}

/* ⚠️ The missing rung. The scale went 48px hero straight to 15px lede, so on
   the welcome screen the sentence saying what vokt IS sat three tiers below
   the instruction to connect a car — the reason smaller than the method.
   Sized to read as the hero's other half rather than as fine print. */
.lede--hero {
  font-size: 20px; line-height: 1.4; font-weight: 500;
  color: var(--ink-2); text-wrap: pretty; max-width: 19ch;
}
.note { font-size: 13px; font-weight: 400; color: var(--ink-2); margin: 0; }
.note--centred { text-align: center; }

/* ---- fields ------------------------------------------------------------- */

.fields { display: flex; flex-direction: column; gap: 16px; flex: none; }
.field { display: flex; flex-direction: column; gap: 7px; min-width: 0; }

/* ⚠️ Cloudflare's own class name (app/turnstile.py), not one of ours — the
   widget renders a fixed-width iframe (~300px) inside whatever box holds it.
   .fields stretches its children to the column's full width by default, so
   without this the iframe just sits at the stretched div's left edge rather
   than in the middle of the column the way the button below it reads as
   centred within. */
.cf-turnstile { display: flex; justify-content: center; }

.field__label {
  font-weight: 500;
  font-size: 10px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--ink-2);
}

.field__input {
  height: var(--field-h);
  border-radius: var(--radius);
  background: var(--panel);
  border: 1.5px solid var(--rule);
  padding: 0 16px;
  font-family: inherit;
  font-size: 17px;
  font-weight: 500;
  color: var(--ink);
  width: 100%;
  /* Safari zooms the page on focus for anything under 16px. 17px is the
     artboard value and is above that threshold, so this stays as designed. */
}
.field__input::placeholder { color: var(--ink-2); font-weight: 400; }
.field__input:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; border-color: var(--ink); }

/* ⚠️ A REJECTED FIELD HAS TO LOOK REJECTED, and in a monochrome system nothing
   else will do it. aria-invalid was being set with no rule behind it, so the
   only thing that moved on a failed code was a 12.5px line changing weight.
   After a full page reload that is not a change anybody attributes to their own
   input: it reads as a refresh. Border weight and ink are the two channels
   available here, so both are used. */
.field__input[aria-invalid="true"] { border-width: 2px; border-color: var(--ink); }

.field__help { font-size: 12.5px; font-weight: 400; line-height: 1.5; color: var(--ink-2); }
.field__help--error { color: var(--ink); font-weight: 500; }

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

.btn {
  height: var(--control-h);
  border-radius: var(--radius);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: inherit;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  border: 0;
  width: 100%;
  text-decoration: none;
  transition: opacity 120ms var(--ease);
}
.btn:hover { opacity: 0.88; }
/* Not a cursor change alone: the button has to LOOK unavailable, or the only
   feedback for pressing it is that nothing happens. */
.btn:disabled { opacity: 0.35; cursor: not-allowed; }
.btn:disabled:hover { opacity: 0.35; }
.btn:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

.btn--primary { background: var(--ink); color: var(--on-accent); }
.btn--secondary { background: none; border: 1.5px solid var(--rule); color: var(--ink); }

.actions { display: flex; flex-direction: column; gap: 12px; flex: none; }
/* A deliberate distance before a primary action, for screens whose content
   length changes underneath it. 20px on top of the screen's own gap, so the
   button is clearly apart from the last row without being bottom-anchored to
   whatever space happens to be left. */
.actions--apart { margin-top: 20px; }

/* A control that reads as text, for destructive-adjacent things like sign out. */
.btn-quiet {
  font: inherit; font-size: 15px; padding: 0; color: var(--ink-2);
  background: none; border: 0; cursor: pointer; text-decoration: underline;
}

/* ---- panel -------------------------------------------------------------- */

.panel {
  background: var(--panel);
  border: 1.5px solid var(--rule);
  border-radius: var(--radius);
  padding: 16px;
}
.panel--dashed { border-style: dashed; background: none; }

.rule { border: 0; border-top: 1px solid var(--rule); margin: 0; }

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

/* ---- checkbox ----------------------------------------------------------- */
/* The artboard draws a 22px square, 6px radius, filled ink with a paper tick
   when on and a 1.5px rule outline when off. A native checkbox cannot be
   styled to that, so the input is hidden and the square is drawn beside it —
   still a real checkbox to the keyboard and to a screen reader. */

.consents { display: flex; flex-direction: column; gap: 14px; flex: none; padding: 2px; }

.check { display: flex; align-items: flex-start; gap: 12px; cursor: pointer; }
.check__input {
  position: absolute;
  opacity: 0;
  width: 22px; height: 22px;
  margin: 0;
  cursor: pointer;
}
.check__box {
  width: 22px; height: 22px;
  border-radius: 6px;
  border: 1.5px solid var(--rule);
  background: none;
  flex: none;
  margin-top: 1px;
  display: flex; align-items: center; justify-content: center;
}
.check__box svg { display: block; opacity: 0; }
.check__input:checked ~ .check__box { background: var(--ink); border-color: var(--ink); }
.check__input:checked ~ .check__box svg { opacity: 1; }
.check__input:focus-visible ~ .check__box { outline: 2px solid var(--ink); outline-offset: 2px; }
.check__text { font-size: 14px; font-weight: 500; line-height: 1.45; color: var(--ink); flex: 1; min-width: 0; }
.check__text a { color: var(--ink); font-weight: 600; border-bottom: 1px solid var(--rule); text-decoration: none; }

/* ---- name pair ---------------------------------------------------------- */
.field-row { display: flex; gap: 12px; }
.field-row .field { flex: 1; }

/* ---- mobile, with the country prefix fused to the input ----------------- */
.field__group {
  height: var(--field-h);
  border-radius: var(--radius);
  background: var(--panel);
  border: 1.5px solid var(--rule);
  display: flex; align-items: stretch; overflow: hidden;
}
.field__group:focus-within { outline: 2px solid var(--ink); outline-offset: 2px; border-color: var(--ink); }
.field__prefix {
  display: flex; align-items: center; padding: 0 13px 0 14px; flex: none;
  font-size: 16px; font-weight: 600; letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums; color: var(--ink);
}
.field__divider { width: 1px; background: var(--rule); flex: none; }
.field__group .field__input {
  height: 100%; border: 0; border-radius: 0; background: none; padding: 0 14px;
}
.field__group .field__input:focus-visible { outline: none; }

/* ---- the welcome reel --------------------------------------------------- */
/* Screen 2 cycles the marque under a fixed headline. Height is locked to the
   line box so the line below never moves. */

.reel { display: inline-block; overflow: hidden; height: 50px; vertical-align: bottom; }
.reel__strip { display: block; animation: reel 9s var(--ease) infinite; }
.reel__item { display: block; height: 50px; }

@keyframes reel {
  0%,   18% { transform: translateY(0); }
  25%,  43% { transform: translateY(-50px); }
  50%,  68% { transform: translateY(-100px); }
  75%,  93% { transform: translateY(-150px); }
  100%      { transform: translateY(-200px); }
}

@media (prefers-reduced-motion: reduce) {
  .reel__strip { animation: none; }
}

/* ---- the plate ---------------------------------------------------------- */
/* Screen 4 draws the input as a number plate rather than a text field. The
   real input sits on top of it, transparent, so it is still a plain text box
   to the keyboard, to autofill and to a screen reader. */

.plate-wrap { display: flex; flex-direction: column; align-items: center; gap: 14px; flex: none; padding-top: 8px; }

.plate {
  position: relative;
  display: inline-flex;
  align-items: center;
  background: var(--plate);
  border: 2.5px solid var(--ink);
  border-radius: 10px;
  padding: 12px 26px 14px;
}
.plate__input {
  font-family: inherit;
  font-weight: 800;
  font-size: 38px;
  letter-spacing: 0.08em;
  line-height: 1;
  color: var(--ink);
  background: none;
  border: 0;
  outline: none;
  padding: 0;
  text-align: center;
  text-transform: uppercase;
  /* Six characters at this size, plus the tracking. Sized in ch so it holds
     when the font falls back to the system stack.
     ⚠️ The tracking is per character INCLUDING the last, so six characters need
     6ch + 6 x 0.08em, and 6.6ch was not enough for it: typing the sixth
     character scrolled the first one out of sight, so DDA434 showed as DA434
     with the D lost off the left edge. Nothing truncated — the value was
     always right — which is why it read as a bug in the input rather than in
     its width. */
  width: calc(6.6ch + 0.35em);
}
.plate__input::placeholder { color: var(--rule); }
.plate:focus-within { outline: 2px solid var(--ink); outline-offset: 3px; }

/* The sign-in code, drawn as the same object as the plate. Founder's call, and
   it is the right one: this is the product's signature input treatment, and the
   two screens are consecutive steps in the same funnel. The code field used to
   be .field__input with its tracking hand-applied inline at the call site.

   ⚠️ EIGHT characters, not six, so the width formula is re-derived rather than
   copied. The plate's comment explains the failure this avoids: tracking is
   applied per character INCLUDING the last, so a width that only counts the
   glyphs scrolls the first character out of sight while the value stays
   correct, which reads as a bug in the input rather than in its width.
   8ch + 8 x 0.08em, rounded up, and tabular-nums so the box cannot reflow as
   the digits change.

   ⚠️ NO aria-invalid rule here, deliberately. The plate's resting border is
   already 2.5px of ink, the heaviest in the system, so there is nothing left to
   escalate to. The rejected state is carried by the error summary that takes
   focus, the heading, and the digits staying in the field. */
.plate__input--code {
  font-variant-numeric: tabular-nums;
  text-transform: none;
  width: calc(8.6ch + 0.7em);
}

/* ---- vehicle card ------------------------------------------------------- */
/* Screen 5a. Softer radius than the controls (16 against 12) because it is a
   surface holding things rather than a thing to press. */

.card {
  background: var(--panel);
  border-radius: 16px;
  padding: 18px 20px 20px;
  display: flex; flex-direction: column; gap: 16px;
  flex: none;
}
.card__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.card__title { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.card__name {
  font-weight: 800; letter-spacing: var(--display-tracking);
  line-height: 1.05; font-size: 26px; margin: 0;
}
.card__sub { font-size: 13.5px; font-weight: 500; color: var(--ink-2); }
.card__rule { height: 1px; background: var(--rule); }

/* The plate again, small, as an identifier rather than an input.

   ⚠️ PADDING ISN'T SYMMETRIC, and until 2026-09-24 it was closer to
   symmetric than this and still read wrong: "VOKT" sat visibly high in
   every .plate-chip variant, caught only once vokt-marketing started using
   real screenshots of this exact element and the plate still looked off in
   them. Same root cause as the marketing site's own version of this bug —
   line-height:1 reserves descender space this all-caps text never uses, so
   the glyphs sit high inside their own line box before padding is even
   applied — but fixed here by measurement instead of by eye this time: a
   zero-height marker (vertical-align:baseline) gives the real rendered
   baseline, and Canvas's actualBoundingBoxAscent/Descent (not
   fontBoundingBox, which is the font's reserved space, not the glyphs'
   actual ink) gives the true ink extent either side of it. Comparing that
   against the padding box's own top and bottom edges gives the real gap
   above and below the ink directly — no theory, no guessing, no font-metric
   trap to fall into a second time. Every .plate-chip variant had the same
   defect, sized to its own font-size, and all three are fixed the same way:
   shift half the (gapBelow − gapAbove) difference from bottom padding to
   top padding, preserving the original top+bottom sum. */
.plate-chip {
  background: var(--plate);
  border: 1.6px solid var(--ink);
  border-radius: 5px;
  padding: 4.75px 9px 4.25px;
  flex: none;
  font-weight: 800; font-size: 15px; letter-spacing: 0.07em;
  color: var(--ink); line-height: 1;
}

/* ---- fact list ---------------------------------------------------------- */
/* Status by fill, not hue: a filled disc is true, a hollow ring is not. The
   brand is monochrome by decision, so a red cross is not available and would
   not be wanted. */

.fact { display: flex; align-items: center; gap: 12px; }
.fact__mark {
  width: 21px; height: 21px; border-radius: 11px;
  display: flex; align-items: center; justify-content: center;
  flex: none; margin-top: 1px;
  background: var(--ink);
}
.fact__mark--no { background: none; border: 1.6px solid var(--rule); }
.fact__mark svg { display: block; }
.fact__text {
  font-size: 14.5px; font-weight: 500; letter-spacing: -0.01em;
  line-height: 1.35; color: var(--ink); flex: 1; min-width: 0;
}
.fact--no .fact__text { color: var(--ink-2); }

.attribution { font-size: 10.5px; font-weight: 400; color: var(--ink-2); text-align: center; flex: none; }

/* ---- disclosure list ---------------------------------------------------- */
/* Screen 6. Three groups, same row shape, distinguished by fill and weight:
   what vokt takes is filled and 600; what it does not is a hollow ring and 500
   in ink-2. Status by fill, not hue — the brand has no second colour to spend
   and this reads correctly in a photocopy. */

.disclosure { display: flex; flex-direction: column; gap: 12px; flex: none; }
.disclosure__row { display: flex; align-items: center; gap: 12px; }
.disclosure__mark {
  width: 20px; height: 20px; border-radius: 10px;
  background: var(--ink);
  display: flex; align-items: center; justify-content: center; flex: none;
}
.disclosure__mark--no { background: none; border: 1.5px solid var(--rule); }
/* The cross inside a hollow ring. It was hidden by the rule below, which
   assumed a hollow ring was empty. */
.disclosure__text {
  font-size: 14.5px; font-weight: 600; letter-spacing: -0.01em;
  line-height: 1.35; color: var(--ink); flex: 1; min-width: 0;
}
.disclosure__row--no .disclosure__text { font-weight: 500; color: var(--ink-2); }

/* ---- the peeking group (screen 6, "available, never used") --------------
   Native <details>: no JS, one tap, keyboard- and screen-reader-operable, and
   every row stays in the DOM whether it is open or not.

   ⚠️ THE COLLAPSED STATE SHOWS REAL ROWS. It used to be a 10px uppercase label
   and a chevron, which reads as a caption rather than as a control, so the
   section was effectively invisible. Three rows of the actual contents say what
   is in there without anybody opening it.

   The sample is duplicated in the markup and hidden here when the group opens,
   which is what keeps the rows from moving and keeps the summary's accessible
   name down to the heading and the cue. */
.disclosure-toggle { flex: none; }
.disclosure__summary {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 4px 0;
  cursor: pointer;
  list-style: none;   /* kill the default disclosure triangle */
}
.disclosure__summary::-webkit-details-marker { display: none; }
.disclosure__summary:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
  border-radius: 4px;
}
.disclosure__summary-head { display: flex; align-items: center; gap: 8px; }
/* The count sits right after the heading. */
.disclosure__count {
  font-size: 11px; font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}

.disclosure__sample { display: flex; flex-direction: column; gap: 12px; }
/* ⚠️ The last sampled row fades, which is the whole signal that the list
   continues. Masked rather than given opacity, so the row's own ink and the
   ring both fall away together instead of the text going grey and the ring
   staying put. */
.disclosure__sample .disclosure__row:last-child {
  -webkit-mask-image: linear-gradient(to bottom, var(--ink) 15%, transparent 96%);
  mask-image: linear-gradient(to bottom, var(--ink) 15%, transparent 96%);
}
.disclosure-toggle[open] .disclosure__sample { display: none; }

/* The cue reads "Show all 10" closed and "Show less" open. Text, not just a
   chevron: a chevron alone is what made the old summary look like decoration. */
.disclosure__cue {
  display: flex; align-items: center; gap: 5px;
  font-size: 13px; font-weight: 600; letter-spacing: -0.01em;
  color: var(--ink);
}
.disclosure__cue-less { display: none; }
.disclosure-toggle[open] .disclosure__cue-more { display: none; }
.disclosure-toggle[open] .disclosure__cue-less { display: inline; }
.disclosure__chevron {
  color: var(--ink);
  flex: none;
  transition: transform 180ms var(--ease);
}
.disclosure-toggle[open] .disclosure__chevron { transform: rotate(180deg); }
/* Space the rows off the summary once open; the group's own 12px gap does the rest. */
.disclosure--nested { margin-top: 12px; }

/* The button carries the Tesla mark, so it needs the gap the others do not. */
.btn--with-mark { gap: 10px; }

/* ---- the loading ripple ------------------------------------------------- */
/* Screen 7. Canonical copy lives in the docs repo at vokt/brand/vokt-loading.css.
   Runs ONLY while the app is genuinely fetching from the vehicle: motion that
   means something rather than motion that decorates. */

/* ⚠️ The browser's own [hidden] rule is display: none on the element itself,
   which ANY class setting a display loses to — .loading-screen sets
   display: flex, so <div class="loading-screen" hidden> stayed on screen.
   Caught while adding the plate lookup's wait screen; it would have shipped as
   a loading panel visible under the form it was meant to replace. */
[hidden] { display: none !important; }

.loading-screen {
  flex: 1;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 26px;
  text-align: center;
}

.ripple { display: block; color: var(--ink); overflow: visible; }

/* g.gN, not g[class^="g"]. The attribute selector scores higher and, using the
   animation shorthand, resets the per-glyph delay to 0 — every glyph then
   pulses at once and the wave disappears. */
.ripple g.g0, .ripple g.g1, .ripple g.g2, .ripple g.g3, .ripple g.g4 {
  /* ⚠️ transform-box, because a transform-origin in px on an SVG child
     resolves against whichever reference box this names — and browsers have
     not always agreed on the default. fill-box means "this glyph's own bounding
     box", so `center` is the middle of the letter whatever the viewBox does,
     and the inline px origins in _ripple.html become unnecessary. Without it,
     an origin of 280.5px 440px can resolve against a 128x40 CSS-pixel border
     box instead of user space, which scales each letter about a point far
     outside itself. */
  transform-box: fill-box;
  transform-origin: center;
  animation-name: ripple;
  animation-duration: 1600ms;
  animation-timing-function: var(--ease);
  animation-iteration-count: infinite;
}
.ripple g.g0 { animation-delay:   0ms; }
.ripple g.g1 { animation-delay: 160ms; }
.ripple g.g2 { animation-delay: 320ms; }
.ripple g.g3 { animation-delay: 480ms; }
.ripple g.g4 { animation-delay: 640ms; }

/* Swell and settle in turn, left to right. Scale only, about each glyph's own
   centre. The wave crosses in 0.40 of the cycle and the word rests for the
   final 30%: the rest is what stops it reading as a fidget. */
@keyframes ripple {
    0% { transform: scale(1);    }
   15% { transform: scale(1.11); }
   30% { transform: scale(1);    }
  100% { transform: scale(1);    }
}

/* ⚠️ A LOADING INDICATOR STILL HAS TO INDICATE.
   The global reduced-motion reset near the top of this file is
   `* { animation-duration: 0.01ms !important }`, which is the usual snippet
   and is too blunt for this one case: at 0.01ms an infinite animation is
   visually static, so the wordmark sat there while a Tesla woke for twelve
   seconds and the screen looked broken rather than busy.
   Reduced motion is about MOVEMENT — scaling, parallax, travel — so the scale
   ripple is correctly suppressed. Opacity is not movement and does not cause
   vestibular problems, so the wave still crosses the word, just by fading
   rather than swelling. The per-glyph delays in the base rules still apply;
   only the name and the duration are overridden. */
@keyframes ripple-quiet {
    0% { opacity: 1;    }
   15% { opacity: 0.35; }
   30% { opacity: 1;    }
  100% { opacity: 1;    }
}

@media (prefers-reduced-motion: reduce) {
  .ripple g.g0, .ripple g.g1, .ripple g.g2, .ripple g.g3, .ripple g.g4 {
    animation-name: ripple-quiet !important;
    animation-duration: 1600ms !important;
    animation-iteration-count: infinite !important;
  }
}

/* ---- the figure --------------------------------------------------------- */
/* Screen 8 inverts: ink ground, paper text. The one number that matters gets
   the only inverted surface in the product, and nothing else may claim it. */

.figure-panel {
  background: var(--ink);
  color: var(--paper);
  border-radius: 16px;
  padding: 22px 20px 20px;
  display: flex; flex-direction: column; gap: 13px;
  flex: none;
}
.figure-panel .field__label { color: var(--ink-3); }
.figure {
  display: flex; align-items: baseline; gap: 8px;
}
.figure__value {
  font-weight: 600;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  font-size: 52px;
  line-height: 1;
  color: var(--paper);
}
.figure__unit { font-weight: 600; font-size: 20px; color: var(--ink-3); }
.figure-panel__rule { height: 1px; background: #2E333A; }
.figure-panel__meta {
  display: flex; justify-content: space-between; align-items: center; gap: 12px;
  font-size: 13px; font-weight: 500; color: var(--ink-3);
}

/* The name field, which is filled rather than empty: it has a suggestion in it. */
.field__input--named {
  border-color: var(--ink);
  font-weight: 600;
}

/* ---- the home screen ---------------------------------------------------- */
/* Screen 11. The app frame gains a tab bar, so it stops being a single column
   with padding and becomes a page with a fixed footer. */

.app--tabbed { padding-bottom: 0; }

.tabbar {
  border-top: 1px solid var(--rule);
  background: var(--panel);
  display: flex;
  padding: 12px 4px 26px;   /* the bottom padding clears the home indicator */
  flex: none;
  /* Full-bleed: the bar spans the frame while the content is inset by 20px. */
  margin: 0 -20px;
  /* ⚠️ Sticky, because it was in the document flow and scrolled away with the
     page. On a home screen taller than the viewport you had to scroll to the
     BOTTOM to reach the tabs, which is the one thing navigation must never
     ask. .app is min-height:100dvh, so on a short screen the bar already sat
     at the bottom — which is why this only shows up once there is enough on
     the page to scroll. */
  position: sticky;
  bottom: 0;
  z-index: 10;
}
.tab {
  flex: 1;
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  color: var(--ink-2);
  text-decoration: none;
  /* 10px, weight 500, sentence case. An earlier version added uppercase and
     letter-spacing, which the artboard does not have — it made the labels
     read as section headings rather than as tabs. */
  font-size: 10px; font-weight: 500;
}
.tab--on { color: var(--ink); font-weight: 600; }
.tab svg { display: block; flex: none; }

/* The gauge. A bar, not a dial: it reads at a glance and it prints. */
.gauge { display: flex; flex-direction: column; gap: 9px; }
.gauge__track {
  height: 10px; border-radius: 5px;
  background: var(--rule);
  overflow: hidden; display: flex;
}
.gauge__fill { background: var(--ink); border-radius: 5px 0 0 5px; }
.gauge__ends {
  display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
  font-size: 11.5px; font-weight: 500; color: var(--ink-2);
}

.headline-figure { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.headline-figure__value {
  font-weight: 600; letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  font-size: 54px; line-height: 0.92;
}
.headline-figure__unit { font-weight: 600; font-size: 21px; color: var(--ink-2); }
.headline-figure__aside { font-size: 13px; font-weight: 600; color: var(--ink); }

/* Two facts side by side, split by a hairline. */
/* ⚠️ Both halves CENTRED, and symmetrically padded. They were left-aligned
   with padding on the second half only, which put the two values at different
   distances from the rule between them and made a symmetrical pair look like a
   mistake. Centring is only true if the padding is equal on both sides. */
.pair { display: flex; align-items: stretch; gap: 0; }
.pair__half {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 0 12px;
  text-align: center;
}
.pair__label { display: flex; align-items: center; justify-content: center; gap: 7px; }
.pair__rule { width: 1px; background: var(--rule); }
.pair__value {
  font-weight: 600; letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums; font-size: 16px;
}
.pair__when { font-size: 11.5px; font-weight: 500; color: var(--ink-2); }

.statement { font-size: 14.5px; font-weight: 600; letter-spacing: -0.01em; color: var(--ink); }
.aside-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }

/* ---- detail rows -------------------------------------------------------- */
/* The Vehicles and Settings tabs are lists of label/value pairs inside a
   panel, split by hairlines. One pattern, used by both. */

.panel-card {
  background: var(--panel);
  border-radius: 18px;
  padding: 18px 20px 20px;
  display: flex; flex-direction: column; gap: 0;
  flex: none;
}
/* Heading row: icon, then the label. The 14px is the artboard's spacer div
   between a card's heading and its first row. */
.panel-card__heading {
  display: flex; align-items: center; gap: 8px;
  margin-bottom: 14px;
  color: var(--ink-2);
}

.row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 0;
  border-top: 1px solid var(--rule);
}
/* The first row sits under the heading, not under a rule. */
.row:first-of-type { border-top: 0; padding-top: 0; }
.row__body { flex: 1; min-width: 0; }
.row__label { font-size: 11.5px; font-weight: 400; color: var(--ink-2); margin-top: 2px; }
.row__value { font-size: 14.5px; font-weight: 500; color: var(--ink); }
.row__value--mono { font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
.row__aside { display: flex; align-items: center; gap: 8px; flex: none; }

/* --- the Syncs card --------------------------------------------------------
   A name and two dated columns. The columns are fixed and tabular so the dates
   line up down the card rather than ragging against four names of different
   lengths — the point of the card is comparing them.

   ⚠️ 74px holds "14 Sep 2026" at 13px. At 400px that leaves ~176px for the
   name, and the longest is "Warrant of fitness" at ~120px, so it fits without
   a narrow tier. Shrink the column and that stops being true. */
/* Sits between the heading and the column labels. The heading already carries
   14px beneath it, so this only needs to clear the labels. */
.sync__note {
  margin: 0 0 12px;
  font-size: 12px; line-height: 1.5; color: var(--ink-2);
}
.sync { gap: 8px; }
.sync__name { flex: 1; min-width: 0; }
.sync__when {
  flex: none; width: 74px; text-align: right;
  font-size: 13px; color: var(--ink);
  font-variant-numeric: tabular-nums; letter-spacing: -0.01em;
}
.sync__when--none { color: var(--ink-3); }
/* The column headings, which are labels rather than data.

   ⚠️ No rule above them, and it has to be said explicitly. `.row:first-of-type`
   zeroes the border for the first element OF ITS TYPE, and the card's heading
   is a div too — so the heading wins that match and the first actual row keeps
   a border it should not have. The only separator in this card belongs between
   these labels and the first row of data. */
.sync--head { padding-top: 0; padding-bottom: 6px; border-top: 0; }
.sync--head .sync__when { font-size: 11px; font-weight: 500; color: var(--ink-2); }

/* A row that is a link out of the panel. */
.row--action { cursor: pointer; }
.row--action .row__value { font-weight: 600; }

/* Dimmed, for something that exists in the design and not yet in the app. */
.row--soon .row__value { color: var(--ink-3); }
.soon {
  font-size: 10px; font-weight: 500; letter-spacing: 0.15em;
  text-transform: uppercase; color: var(--ink-3);
  border: 1px solid var(--rule); border-radius: 999px;
  padding: 3px 8px; flex: none;
}

/* Retained at 0 and doing nothing: panels are separated by the frame's own
   18px gap in the artboards, not by a spacer element. Kept as a no-op so the
   templates read the same as the design's structure without adding space the
   design does not have. */
.section-gap { display: none; }

/* A destructive action reads as text, never as a filled button: nothing here
   should be the easiest thing on the screen to press. */
.btn--quiet-danger {
  background: none;
  border: 1.5px solid var(--rule);
  color: var(--ink);
}

/* ---- notification grid -------------------------------------------------- */
/* Screen 16: the event on the left, one checkbox column per channel. A grid
   rather than a flex row, so the columns line up down the whole list however
   long the labels get. */

/* ⚠️ stretch, NOT center. Each cell draws its own border-top, and with
   align-items: center every cell is only as tall as its own content and sits
   at a different height inside the row — so the three border-tops in one row
   land at three different y positions and the separator becomes a staircase.
   Stretching makes every cell the full row height, so the tops line up and
   read as one continuous rule. The content is centred inside each cell
   instead, which is what was wanted in the first place. */
.channels { display: grid; grid-template-columns: 1fr 46px; align-items: stretch; }
.channels__head {
  display: flex; align-items: flex-end; justify-content: center;
  padding-bottom: 12px; color: var(--ink-2);
}
.channels__label {
  display: flex; align-items: center;
  padding: 14px 0;
  font-size: 14.5px; font-weight: 500; color: var(--ink);
  border-top: 1px solid var(--rule);
}
.channels__cell {
  display: flex; align-items: center; justify-content: center;
  padding: 14px 0;
  border-top: 1px solid var(--rule);
}
/* The first data row sits under the channel icons, not under a rule. */
.channels__label--first, .channels__cell--first { border-top: 0; padding-top: 0; }

/* The box reuses the signup checkbox, which is built to sit beside a paragraph
   that may wrap: it has margin-top 1px so it lines up with the FIRST LINE of
   the text, and its label is a flex row sized by that text. Neither is right in
   a grid cell, where the box is the only thing in it and should be centred.
   Both were inherited, and the column came out a pixel high and off-centre. */
.channels__cell .check {
  position: relative;
  display: inline-flex;
  gap: 0;
  margin: 0;
}
.channels__cell .check__box { margin-top: 0; }
.channels__cell .check__input {
  /* Over the box, not over the whole row, so the tap target is the box. */
  top: 0; left: 0; margin: 0;
}

/* ---- banners ------------------------------------------------------------ */
/* Two weights, and the weight IS the meaning. A car briefly out of coverage
   is a quiet note on the panel ground; a connection Tesla has stopped
   accepting inverts, because it is the one state where the customer must act.
   Status by fill, not hue — there is no second colour to spend. */

.banner {
  background: var(--panel);
  border: 1.5px solid var(--rule);
  border-radius: 16px;
  padding: 15px 18px;
  display: flex; flex-direction: column; gap: 5px;
  flex: none;
}
.banner__heading {
  font-size: 14.5px; font-weight: 600; letter-spacing: -0.015em;
  color: var(--ink); margin: 0;
}
/* A heading that leads with a glyph. Same shape as .panel-card__heading, which
   is where every other icon-and-label pair in the product is laid out. */
.banner__heading--iconed { display: flex; align-items: center; gap: 8px; }
.banner__body {
  font-size: 12.5px; font-weight: 400; line-height: 1.5;
  color: var(--ink-2); margin: 0;
}

.banner--loud {
  background: var(--ink);
  border: 0;
  color: var(--paper);
  padding: 18px 18px 16px;
  gap: 8px;
}
.banner--loud .banner__heading { font-size: 16px; line-height: 1.35; color: var(--paper); }
.banner--loud .banner__body { font-size: 13px; color: var(--ink-3); }

/* The button inside an inverted panel has to invert too, or it disappears. */
.banner--loud .btn--primary {
  background: var(--paper);
  color: var(--ink);
  margin-top: 4px;
}

/* ---- plan cards --------------------------------------------------------- */
/* Screen 9. A radio group that does not look like radio buttons: the whole
   card is the target, and the chosen one is marked by its border and a tick
   rather than by a dot. */

/* ⚠️ TWO TILES SIDE BY SIDE since 2026-09-13, matching /pricing. They were
   full-width rows stacked vertically: name and price on the left, a tick on the
   right. The brochure shows the same two plans as near-square cards, and a
   customer who compared them there and then met a different shape here had to
   re-read the same decision.

   ⚠️ What did NOT change is the mechanism. This is still a radio group — the
   whole tile is the target and the chosen one is marked by its border and a
   tick. /pricing's cards are two CTAs with a button each; these are one choice
   with a submit below. Matching the LOOK is the point; matching the markup
   would break the form. */
.plan-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; flex: none; }
.plan {
  display: flex; flex-direction: column; align-items: flex-start; gap: 8px;
  background: var(--panel);
  /* 2px, not 1.5px, so selecting a tile does not shift its contents half a
     pixel — the same fix /pricing's cards needed. */
  border: 2px solid var(--rule);
  border-radius: 16px;
  padding: 16px 14px 18px;
  cursor: pointer;
  position: relative;
}
/* ⚠️ Keyed on the RADIO, not on a class the server rendered.
   The tick and the border used to come from .plan--on, written into the markup
   from whichever plan was already chosen — so tapping the other card changed
   the radio and moved nothing on screen. It read as "the monthly plan is not
   selectable". The checkboxes twenty lines up have always done this correctly;
   these cards were the odd ones out.
   :has() for the border, a sibling selector for the tick. Where :has() is not
   supported the tick still moves, which is the part that says "this one". */
.plan:has(.plan__input:checked) { border-color: var(--ink); }

.plan__input { position: absolute; opacity: 0; width: 0; height: 0; }
.plan__body { display: flex; flex-direction: column; gap: 3px; flex: 1; min-width: 0; }
/* ⚠️ Sentence case, matching /pricing's .plan__label. It was 10px uppercase
   with 0.15em tracking — a field label, from when this was a form row. Beside
   the brochure's "Yearly" it read as a different product. Quieter than the
   figure, which is the point of having it at all. */
.plan__name {
  font-weight: 600; font-size: 12.5px; letter-spacing: 0.01em;
  color: var(--ink-2);
}
/* nowrap and sized for a ~150px tile — see /pricing for the same sum. */
.plan__price { display: flex; align-items: baseline; gap: 5px; white-space: nowrap; }
.plan__amount {
  font-weight: 600; letter-spacing: -0.02em; font-variant-numeric: tabular-nums;
  /* ⚠️ 26px, down from 30px. The app column is ~390px, so two tiles are
     ~169px and the inner width is ~137px. "$49.99" at 30px plus " a year" came
     to about the same, and .plan__price is nowrap — so it would have
     overflowed rather than wrapped. Same sum as /pricing's cards. */
  font-size: 26px; line-height: 1; color: var(--ink);
}
.plan__period { font-size: 12.5px; font-weight: 500; color: var(--ink-2); }
.plan__note { font-size: 11.5px; font-weight: 400; color: var(--ink-2); margin-top: 2px; }
/* The corner badge, out of flow, exactly as on /pricing — so the yearly tile
   never grows a row the monthly one does not have. */
.plan__tag {
  position: absolute; top: -9px; right: 12px;
  font-size: 9px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
  background: var(--ink); color: var(--on-accent); border-radius: 999px; padding: 3px 8px;
}

/* ⚠️ Absolutely positioned since the tiles became a column. It used to be a
   flex sibling sitting to the right of .plan__body; in a column it stacked
   UNDER the note, which reads as a bullet rather than as a selection state.
   Bottom-right, opposite the badge. */
.plan__mark {
  position: absolute; right: 12px; bottom: 12px;
  width: 20px; height: 20px; border-radius: 10px;
  border: 1.5px solid var(--rule);
  display: flex; align-items: center; justify-content: center;
  flex: none;
}
.plan__mark svg { opacity: 0; }
.plan__input:checked ~ .plan__mark { background: var(--ink); border-color: var(--ink); }
.plan__input:checked ~ .plan__mark svg { opacity: 1; }
.plan__input:focus-visible ~ .plan__mark { outline: 2px solid var(--ink); outline-offset: 2px; }

/* ---- setup steps -------------------------------------------------------- */

.steps-list { display: flex; flex-direction: column; gap: 0; flex: none; }
.step-row {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 14px 0;
  border-top: 1px solid var(--rule);
}
.step-row:first-child { border-top: 0; padding-top: 0; }
.step-row__mark {
  width: 21px; height: 21px; border-radius: 11px;
  background: var(--ink);
  display: flex; align-items: center; justify-content: center;
  flex: none; margin-top: 1px;
}
.step-row__mark--waiting { background: none; border: 1.5px solid var(--rule); }

/* The tick arriving. A step goes from hollow ring to filled disc when the
   answer lands, and the transition is what makes the list read as something
   happening rather than a page that changed while you blinked.
   ⚠️ Transitions, not an animation, so the ring is never mid-flight when the
   screen is first painted — the waiting state has to be honest from frame one. */
.step-row__mark { transition: background 260ms var(--ease), border-color 260ms var(--ease); }
.step-row__mark svg { transition: opacity 200ms var(--ease) 120ms; opacity: 1; }
.step-row__mark--waiting svg { opacity: 0; }
.step-row__text { transition: color 260ms var(--ease); }

/* The steps still to happen breathe gently, so a wait of a second or two does
   not look like a screen that has stopped. Opacity only — see the ripple, and
   the note there about reduced motion suppressing MOVEMENT rather than change. */
@keyframes step-waiting {
    0% { opacity: 1;    }
   50% { opacity: 0.55; }
  100% { opacity: 1;    }
}
.step-row--waiting .step-row__mark { animation: step-waiting 1500ms var(--ease) infinite; }

/* A waiting step really is empty — nothing has happened yet, which is not the
   same as "no". This is the one hollow ring in the product with nothing in it.
   ⚠️ opacity, not display:none, which is set above. display cannot transition,
   so the tick would have snapped in rather than arriving. */
.step-row__body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.step-row__text { font-size: 14.5px; font-weight: 500; color: var(--ink); }
.step-row--waiting .step-row__text { color: var(--ink-2); }
/* Always in the markup now, so the script can fill it in when a step lands.
   [hidden] takes it out of the flow entirely — see the !important rule near
   the top — so an empty one does not open a gap under the text. */
.step-row__value {
  font-size: 13px; font-weight: 600; letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums; color: var(--ink-2);
}

/* ---- the vehicle on screen 8 -------------------------------------------- */
/* Between the reading and the box that asks you to name it. Quiet on purpose:
   the figure above is what this screen is for, and this is the answer to
   "which car?" rather than a second thing competing with it. */

.named-vehicle {
  display: flex; flex-direction: column; gap: 3px;
  align-items: center; text-align: center;
  flex: none;
}
.named-vehicle__name {
  font-size: 17px; font-weight: 600; letter-spacing: -0.01em; color: var(--ink);
}
.named-vehicle__meta { font-size: 13px; font-weight: 500; color: var(--ink-2); }
/* When the reading rail carries only the timestamp, it has nothing to sit
   opposite, so it centres rather than pushing to one edge. */
.figure-panel__meta--single { justify-content: center; }

/* ---- editable rows ------------------------------------------------------ */
/* A row the customer can change carries a pencil. Without one there is nothing
   to say the value is not simply a fact about the account, and the Settings
   artboard puts one on all three account rows. */

a.row, button.row {
  text-decoration: none;
  background: none;
  border-top: 1px solid var(--rule);
  width: 100%;
  text-align: left;
  font: inherit;
  cursor: pointer;
  color: inherit;
}
a.row:first-of-type, button.row:first-of-type { border-top: 0; padding-top: 0; }
.row__pencil { color: var(--ink-2); flex: none; display: flex; }
a.row:hover .row__pencil, button.row:hover .row__pencil { color: var(--ink); }
a.row:focus-visible, button.row:focus-visible {
  outline: 2px solid var(--ink); outline-offset: 2px; border-radius: 6px;
}

/* ---- the vehicle strip -------------------------------------------------- */
/* Screen 11 opens with the vehicle itself, beside a placeholder for the second
   one. Not a list: two cards side by side, equal width. */

.strip { display: flex; gap: 12px; }
/* Centred, like screen 8, which this card is meant to match. The plate is
   already centred under the car, so left-aligned words beneath it read as two
   different layouts sharing one card. Covers the customer's own name for the
   vehicle too, when they have set one. */
.strip__card {
  flex: 1; min-width: 0;
  background: var(--panel);
  border-radius: 18px;
  padding: 12px;
  display: flex; flex-direction: column; gap: 8px;
  text-align: center;
}
.strip__name { font-size: 15px; font-weight: 600; line-height: 1.2; }
/* ⚠️ align-items: center, or THE PLATE STRETCHES THE WHOLE CARD WIDTH. A flex
   column stretches its children across the cross axis by default, and a chip
   as wide as the card reads as a text field rather than a number plate. The
   render is width:100% so it still fills; only the chip shrinks to its text.
   
   ⚠️ gap 0, and the plate's own negative margin does the spacing — the same
   arrangement as screen 8, at card size. A gap here would fight it. */
.strip__middle {
  flex: 1; display: flex; flex-direction: column; justify-content: center;
  align-items: center;
  gap: 0; min-height: 0; color: var(--ink-3);
}
/* The plate laps the bottom of the car, as it does on screen 8. Smaller
   overlap because the chip and the render are both smaller. */
.strip__middle .plate-chip { margin-top: -9px; }
/* Tesla's render, served from vokt's own disk and already cropped to the car
   by the time it gets here — see crop_to_car in app/vehicle_image.py. That
   alone roughly doubled the car in this box without the box changing: it used
   to be a 720x405 canvas letterboxed into 56px, so the car itself was about
   26px of it.
   The height stays fixed so the two strip cards are the same size whether
   there is a picture or the fallback icon. */
.strip__render { display: block; width: 100%; height: 66px; object-fit: contain; }
/* ⚠️ NO CROPPING NUMBERS HERE, AND THAT IS DELIBERATE.

   This used to carry a hand-measured aspect-ratio and object-position for each
   model. It was wrong twice — a band measured on the Model 3 clipped the Model
   Y's roof, and a margin that suited the Model 3 left the Model S jammed into
   its frame — because four cars Tesla places differently on one canvas cannot
   share constants derived from one of them.

   Each render is trimmed to its own car before being committed, measuring the
   image rather than trusting a number in a stylesheet. See crop_to_car in
   app/vehicle_image.py, run by dev/fetch_renders.py at build time since
   2026-09-11. Every render arrives already tight, so all this has to do is fit
   it in a consistent box.

   ⚠️ contain, NEVER cover. cover crops, and cropping is what kept going wrong;
   contain cannot clip a car whatever shape it arrives in. The fixed height is
   what makes four differently-framed renders sit at the same size — the Model
   X and S are shot closer and taller than the 3 and Y, so they come out
   narrower here rather than towering. */
.named-vehicle__render {
  display: block;
  width: calc(100% + var(--gutter) * 2);
  margin-inline: calc(var(--gutter) * -1);
  height: 160px;
  object-fit: contain;
}

/* The plate LAPS THE BOTTOM OF THE CAR — about halfway between sitting on it
   and sitting clear of it.

   ⚠️ It was absolutely positioned at one point, to buy back its line of
   height. That was the wrong trade over a render reaching the frame edge: it
   covered the sills and wheels. But fully below left it looking detached from
   the car it names.

   A negative margin instead of position:absolute, so it still takes part of a
   line in the flow — nothing to recompute when the render's height changes,
   and the words below move with it. The chip is later in the markup, so it
   paints over the picture without needing a z-index.

   ⚠️ Add the flex gap when reading this: .named-vehicle has gap 3px, so the
   chip's top lands about 17px above the bottom of the render's box.

   Slightly larger than the base chip because it is the one identifier somebody
   checks letter by letter, and here it does that under a large picture.

   ⚠️ padding is 5.94px / 5.06px, not the round 5px / 6px it looks like it
   should be — see the measured fix on .plate-chip above. */
.named-vehicle .plate-chip { margin: -20px 0 2px; font-size: 17px; padding: 5.94px 11px 5.06px; }

/* ⚠️ THIS PULL IS NOW ZERO, and the history is the point.

   It was -22px, written when the renders still carried Tesla's own transparent
   padding: the screen's 26px gap landed on top of that padding and read as a
   hole, so the block was pulled up to close it.

   `dev/fetch_renders.py` started cropping at BUILD time on 2026-09-11 and the
   padding went with it. Measured since: the Model 3 renders have 0px of
   transparent pixels above the car, and the Model S has 19px in a 304px image
   that scales to about 10px in a 160px box. So the compensation had nothing
   left to compensate for and was simply eating the gap, which is what made this
   screen read as cramped once the step bar arrived above it.

   Kept as a named rule rather than deleted: the screen that leads with the
   picture is the one place this would be re-derived from scratch, and a zero
   with the reasoning attached is worth more than a missing line.

   ⚠️ Then it went POSITIVE. Removing the pull left the screen's standard 26px,
   and that is still tight here, because the render bleeds to both edges: with
   no side margin to breathe there is nothing between a full-width picture and
   the step bar but that one gap. 16px on top of it. */
.named-vehicle--top { margin-top: 16px; }

/* Make and model, then the year under it — screen 8's two lines, at card
   size. The make line carries the weight because it is what the card is for. */
.strip__make { font-size: 13px; font-weight: 600; line-height: 1.25; color: var(--ink); }
.strip__sub { font-size: 11.5px; font-weight: 500; line-height: 1.3; color: var(--ink-2); }
.strip__card--empty {
  background: none;
  border: 1.5px dashed var(--rule);
  align-items: center; justify-content: center;
  color: var(--ink-3);
  text-align: center;
}
.strip__empty-title { font-size: 14.5px; font-weight: 600; color: var(--ink-3); }
.strip__empty-note { font-size: 10.5px; font-weight: 500; color: var(--ink-3); }
/* ⚠️ padding is 3.55px / 3.45px, not 3px / 4px — see the measured fix on
   .plate-chip above. */
.plate-chip--small { font-size: 13px; padding: 3.55px 8px 3.45px; }

/* ---- the two-fact card -------------------------------------------------- */
/* Warrant and registration. Its own shape: 16px padding and a 16px gap either
   side of the rule, where the odometer pair inside the RUC card uses 12px and
   an inset second column. Two patterns, both the artboards'. */

.duo {
  background: var(--panel);
  border-radius: 18px;
  padding: 16px 20px;
  display: flex; align-items: stretch; gap: 16px;
  flex: none;
}
.duo__half {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  text-align: center;
}
.duo__label { display: flex; align-items: center; justify-content: center; gap: 7px; }
.duo__value { font-weight: 600; font-size: 14px; }
.duo__rule { width: 1px; background: var(--rule); flex: none; }

/* ---- the RUC card ------------------------------------------------------- */

.ruc-card {
  background: var(--panel);
  border-radius: 18px;
  padding: 20px;
  display: flex; flex-direction: column; gap: 16px;
  flex: none;
}
.ruc-card__head { display: flex; align-items: center; gap: 8px; color: var(--ink-2); }
.ruc-card__rule { height: 1px; background: var(--rule); }

/* ---- set aside ---------------------------------------------------------- */

.aside-card {
  background: var(--panel);
  border-radius: 18px;
  padding: 18px 20px 20px;
  display: flex; flex-direction: column; gap: 11px;
  flex: none;
}
/* ⚠️ A <details> since 2026-09-12, so the summary carries the whole of what
   stays visible when it is shut: the title, the chevron AND the figure. A card
   that hides its own number when closed is a card you have to open to use. */
.aside-card { padding: 18px 20px 20px; }
.aside-card > summary { list-style: none; cursor: pointer; }
.aside-card > summary::-webkit-details-marker { display: none; }
.aside-card > summary:focus-visible {
  outline: 2px solid var(--ink); outline-offset: 3px; border-radius: 10px;
}
.aside-card__head { display: flex; flex-direction: column; gap: 11px; }
.aside-card__headrow {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  color: var(--ink-2);
}
.aside-card__body { display: flex; flex-direction: column; gap: 12px; margin-top: 12px; }

/* How often they put money by. A preference, so it reads as a set of choices
   rather than as actions: the chosen one is filled, the others are outlines.
   Status by fill, because the brand has no second colour to spend. */
.cadence { display: flex; gap: 8px; }
.cadence__pill {
  flex: 1;
  font-family: inherit; font-size: 12.5px; font-weight: 600;
  letter-spacing: -0.01em;
  padding: 8px 6px;
  border-radius: 999px;
  border: 1.5px solid var(--rule);
  background: none;
  color: var(--ink-2);
  cursor: pointer;
  transition: background 120ms var(--ease), color 120ms var(--ease),
              border-color 120ms var(--ease);
}
.cadence__pill[aria-pressed="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--on-accent);
}
.cadence__pill:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* What the household can actually find, as opposed to what the driving costs.
   A real input, because it is the one number on this screen that is theirs. */
.setaside__label { display: block; }
.setaside {
  display: flex; align-items: center; gap: 2px;
  background: var(--panel);
  border: 1.5px solid var(--rule);
  border-radius: var(--radius);
  padding: 0 14px;
  height: var(--field-h);
}
.setaside:focus-within { outline: 2px solid var(--ink); outline-offset: 2px; border-color: var(--ink); }
.setaside__prefix {
  font-size: 17px; font-weight: 600; color: var(--ink-2); flex: none;
}
.setaside__input {
  flex: 1; min-width: 0;
  font-family: inherit; font-size: 17px; font-weight: 600;
  font-variant-numeric: tabular-nums; letter-spacing: -0.02em;
  color: var(--ink);
  background: none; border: 0; outline: none; padding: 0;
}
.setaside__cadence { font-size: 13.5px; font-weight: 500; color: var(--ink-2); flex: none; }

/* Licence sizes. A grid rather than a row: six of them will not fit across a
   390px frame without becoming unreadable, and the weekly cost under each is
   the whole point of showing them together. */
.blocks { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.blocks__option {
  display: flex; flex-direction: column; gap: 2px;
  font-family: inherit; text-align: left;
  padding: 9px 10px;
  border: 1.5px solid var(--rule);
  border-radius: var(--radius);
  background: none;
  cursor: pointer;
}
.blocks__option--on { border-color: var(--ink); border-width: 2px; padding: 8.5px 9.5px; }
.blocks__option:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.blocks__km {
  font-size: 13px; font-weight: 600; letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums; color: var(--ink);
}
.blocks__cost {
  font-size: 12px; font-weight: 500; font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}
.blocks__rate { font-size: 11px; font-weight: 500; color: var(--ink-3); }
.aside-card .chevron, .aside-card__headrow > svg {
  transition: transform 180ms var(--ease);
}
.aside-card[open] .aside-card__headrow > svg { transform: rotate(180deg); }
.aside-card__title { display: flex; align-items: center; gap: 8px; }
.aside-card__figure { display: flex; align-items: baseline; gap: 7px; }
.aside-card__amount {
  font-weight: 600; letter-spacing: -0.02em; font-variant-numeric: tabular-nums;
  font-size: 32px; line-height: 1;
}
.aside-card__cadence { font-size: 15px; font-weight: 500; color: var(--ink-2); }
.aside-card__note { font-size: 13.5px; font-weight: 400; line-height: 1.5; color: var(--ink-2); }

/* What the weekly figure rests on. Ink rather than ink-2, because it is a
   statement of fact about their driving and not a footnote to the amount. */
.aside-card__basis {
  display: flex; align-items: flex-start; gap: 8px;
  font-size: 13.5px; line-height: 1.45; color: var(--ink);
}
.aside-card__basis svg { margin-top: 1px; color: var(--ink-2); }
.aside-card__basis strong { font-weight: 600; font-variant-numeric: tabular-nums; }

/* ---- accessibility utilities ------------------------------------------- */

/* Visible to a screen reader, gone for everyone else. Replaces the inline
   clip-rect that screen 4's plate label was carrying on its own. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---- form error summary ---------------------------------------------------
   Screen 3. Inline field errors say what is wrong beside each input; this
   gathers them into one place at the top, is given focus after a failed
   submit, and links to each field. Both halves ship — WCAG wants the summary
   AND the inline errors, not one or the other. */
.error-summary {
  border: 1.5px solid var(--ink);
  border-radius: var(--radius);
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: none;
}
.error-summary:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.error-summary__title {
  font-size: 13.5px; font-weight: 600; letter-spacing: -0.01em;
  color: var(--ink); margin: 0;
}
.error-summary ul { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 3px; }
.error-summary li { font-size: 13px; line-height: 1.45; color: var(--ink); }
.error-summary a { color: var(--ink); font-weight: 500; }

/* ---- the estimate caveat on the home screen -----------------------------
   When the run-out date is projected from CarJam's figure rather than
   measured, VOICE.md rule 6 wants the limit stated in words, not parked in a
   tooltip that never opens on a phone. */
.ruc-card__caveat {
  font-size: 12px; font-weight: 400; line-height: 1.5;
  color: var(--ink-2); margin: 0;
}

/* ---- the history chart, and reading a figure off it --------------------- */

/* ⚠️ pan-y, never none. The chart is 84px tall in the middle of a scrolling
   page: taking every gesture would mean a thumb landing on it cannot scroll
   past. Vertical belongs to the page, horizontal to the chart. */
.chart {
  touch-action: pan-y;
  cursor: crosshair;
  -webkit-user-select: none;
  user-select: none;
}
.chart__marker, .chart__halo { transition: opacity 120ms var(--ease); pointer-events: none; }

/* The readout carries today's figure at rest and follows a finger while one is
   on the chart, so the slot is never empty. */
.chart-readout {
  display: flex; align-items: baseline; gap: 8px;
  margin-bottom: 12px; min-height: 22px;
}
.chart-readout__amount {
  font-size: 17px; font-weight: 600; letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums; color: var(--ink);
}
.chart-readout__when { font-size: 12.5px; font-weight: 500; color: var(--ink-2); }


