/* Vanderhall Design Tokens V1. This file is the only source of raw design values.
   V9 is dark only. Light mode is removed rather than defaulted away: the first five colors below
   each used to carry a light value and a dark one in a two-value function, and now carry the dark
   value alone. There is no second value left to keep in step, and no branch a stale stored
   preference could reach. The check suite bans that function by name across this tree, which is
   why it is described here rather than written out. */
:root {
  color-scheme: dark;

  --paper: #0E0E10;
  --ink: #F4F4F5;
  /* V22. True white, and the one place on this site that uses it. --ink is #F4F4F5, which is the right
     reading colour and the wrong brand colour: the six footer social marks ship under terms that name
     white as an approved rendering, and an off-white is a very light grey. Grey is approved by one of
     those six brands and by none of the other five, so this token exists to keep that row honest. Do
     not use it for type. */
  --brand-white: #FFFFFF;
  --accent: #E08A55;
  --danger: #FF9A8F;

  --ink-70: color-mix(in srgb, var(--ink) 70%, transparent);
  --ink-60: color-mix(in srgb, var(--ink) 60%, transparent);
  --ink-50: color-mix(in srgb, var(--ink) 50%, transparent);
  --ink-24: color-mix(in srgb, var(--ink) 24%, transparent);
  --ink-12: color-mix(in srgb, var(--ink) 12%, transparent);
  --ink-06: color-mix(in srgb, var(--ink) 6%, transparent);
  --ink-03: color-mix(in srgb, var(--ink) 3%, transparent);

  --surface-1: var(--ink-03);
  --surface-2: var(--ink-06);
  --surface-hover: var(--ink-06);
  --line: var(--ink-12);
  --line-strong: var(--ink-50);
  --line-faint: var(--ink-06);
  --hairline: 1px;

  --text-primary: var(--ink);
  --text-secondary: var(--ink-70);
  --text-tertiary: var(--ink-60);

  /* Fixed values for surfaces that are dark because of what sits behind them rather than because
     of a theme: type over hero photography, the inverse buttons on it, and the footer. They match
     --paper and --ink now, but they are not aliases of them: their meaning is "the value that
     belongs on top of a dark photograph", which no longer moves and never should. */
  --dark-paper: #0E0E10;
  --dark-ink: #F4F4F5;
  --pure-white: #FFFFFF;
  --pure-black: #000000;
  --scrim-strong: rgb(0 0 0 / 0.70);
  --scrim-medium: rgb(0 0 0 / 0.34);
  --scrim-clear: rgb(0 0 0 / 0);

  --font-sans: "Archivo", "Archivo Fallback", -apple-system, BlinkMacSystemFont,
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  --wt-regular: 400;
  --wt-medium: 500;
  --wt-bold: 700;

  --fs-display-xl: clamp(2.5rem, 1.444rem + 4.51vw, 5.5rem);
  --fs-display-l: clamp(2rem, 1.296rem + 3vw, 4rem);
  --fs-display-m: clamp(1.625rem, 1.317rem + 1.31vw, 2.5rem);
  --fs-display-s: clamp(1.3125rem, 1.158rem + 0.66vw, 1.75rem);
  --fs-title: clamp(1.125rem, 1.059rem + 0.28vw, 1.3125rem);
  --fs-body-lg: 1.125rem;
  --fs-body: 1rem;
  --fs-body-sm: 0.9375rem;
  --fs-caption: 0.8125rem;
  --fs-nav: 0.875rem;
  --fs-eyebrow: 0.75rem;

  --lh-display: 1.04;
  --lh-heading: 1.16;
  --lh-body: 1.55;
  --lh-caps: 1.20;

  --ls-display-xl: -0.022em;
  --ls-display: -0.015em;
  --ls-title: -0.008em;
  --ls-caps: 0.10em;
  --ls-caps-tight: 0.14em;

  --s-1: 0.25rem;
  --s-2: 0.5rem;
  --s-3: 0.75rem;
  --s-4: 1rem;
  --s-5: 1.5rem;
  --s-6: 2rem;
  --s-7: 2.5rem;
  --s-8: 3rem;
  --s-9: 4rem;
  --s-10: 5rem;

  --sf-xs: clamp(0.75rem, 0.662rem + 0.38vw, 1rem);
  --sf-s: clamp(1rem, 0.824rem + 0.75vw, 1.5rem);
  --sf-m: clamp(1.5rem, 1.148rem + 1.5vw, 2.5rem);
  --sf-l: clamp(2.5rem, 1.62rem + 3.76vw, 5rem);
  --sf-xl: clamp(3.5rem, 1.916rem + 6.76vw, 8rem);
  --sf-2xl: clamp(5rem, 2.711rem + 9.77vw, 11.5rem);

  /* V12-E: --w-page is gone. Owen on 2026-08-06: the site filled his MacBook Pro but not a larger
     monitor. A 1440px cap on .page meant that above that width every full-bleed element stopped too,
     so a 2560 monitor showed the heroes, the concept band and the lifestyle photographs as a centred
     1440 block with paper down both sides. The page grid's own named lines already hold the reading
     column at --w-content; the outer cap was only ever holding the photography back. Anything that
     needs to agree with the content column now says so with --w-content, which is the measure it
     actually meant. */
  --w-content: 1200px;
  --w-narrow: 800px;
  --w-text: 65ch;
  --w-form: 40rem;
  --gutter: clamp(1.25rem, 4vw, 4rem);
  --header-h: 3.75rem;

  --r-1: 2px;
  --r-2: 4px;
  --r-pill: 999px;

  --shadow-2: 0 2px 4px rgb(0 0 0 / 0.08), 0 24px 48px rgb(0 0 0 / 0.14);

  --frost-bg: color-mix(in srgb, var(--paper) 82%, transparent);
  --frost-blur: blur(20px) saturate(140%);
  --frost-bg-solid: var(--paper);

  --dur-1: 120ms;
  --dur-2: 220ms;
  --dur-3: 380ms;
  --dur-4: 680ms;
  /* V9 amendment 1. The first long duration on the site, and the reason is that a continuous drift
     needs a period and the 120 to 680 ms scale cannot express one. At 55 s one full loop of the
     concept band travels at roughly 58 px/s on a desktop width, which is drift rather than scroll. */
  --dur-marquee: 55s;
  --ease-std: cubic-bezier(0.40, 0, 0.20, 1);
  --ease-out: cubic-bezier(0.16, 1, 0.30, 1);

  --focus-w: 2px;
  --focus-offset: 2px;
  --focus-color: var(--accent);

  /* V12-C: --rise-word is gone with the word cascade it existed for. The reveals are time-based
     transitions now and their distance is --rise, declared on [data-reveal] in site.css. */
  --ar-feature: 3 / 2;
  --ar-cutout: 16 / 10;

  --z-sticky: 10;
  --z-header: 100;
  --z-panel: 200;
  --z-modal: 300;
  --z-veil: 400;

  --hit-target: 2.75rem;
  --icon-size: 1.25rem;

  /* Sized so the concept band renders each tile at most 328 CSS px, which the existing 656w hub
     file covers exactly at 2x. No new rung, and the media audit's ceiling is respected. */
  --marquee-item: clamp(15rem, 22vw, 20.5rem);
  --card-pad: clamp(1.25rem, 0.986rem + 1.13vw, 2rem);
  --card-gap: clamp(1.5rem, 1.324rem + 0.75vw, 2rem);
  --form-gap: clamp(1.75rem, 1.574rem + 0.75vw, 2.25rem);
  --field-h: 3rem;
  --hero-min: min(76svh, max(30rem, 62svh));
  --hero-min-mobile: min(80svh, max(26rem, 66svh));
}

/* V11-E, extended by V12-D. The concepts hub, the nine concept detail pages, and the Brawley GTS
   purchase page render on a white studio field. The class sits on <main>, so it is full width at any
   viewport and the sticky header and the footer, which are outside main, stay dark exactly as they are
   everywhere else.

   V12-D added the purchase page for the same reason V11-E moved the concepts, and it is worth stating
   as a rule rather than as two decisions: this scope belongs on a page whose imagery was shot in a
   white studio. Owen on 2026-08-06 asked for the page to be white so the images look cleaner, and
   what made them unclean was the keying that the dark page required. The walkaround frames are
   delivered unkeyed now, so the vehicle stands on the same white it was photographed on.

   Why the pages moved: the renders were authored on a white studio floor with soft shadows and
   reflections, and V9 keyed that floor onto the dark paper. No threshold key removes a gradient
   cleanly, so the hub cards kept grey ghosting where the contact shadow had been and the detail
   heroes showed vertical banding through the field around the vehicle. Owen, on 2026-08-05:
   "Originally, we were on a white background, and it looked really good. Now, when we edited it, it
   has these gray marks." Putting the pages back on white removes the cause rather than chasing it
   with a better key, and the image pipeline stops keying these files at all.

   THIS IS NOT LIGHT MODE, and the distinction is mechanical rather than a matter of opinion. What V9
   removed was a stored preference, a visible control, and colours defined as two-value functions that
   had to be kept in step with each other. None of those comes back here: this is one class carrying
   fixed values on eleven routes, with no branch to take, nothing to store, and nothing to switch.
   The check suite's bans on the two-value function and on the stored preference stay in force and
   still pass.

   Every value in the ramp is restated rather than inherited. A custom property's var() references
   resolve where the property is declared, not where it is used, so redeclaring --ink alone would
   leave every --ink-NN below still mixing the dark value. The whole ramp is written out for that
   reason, and verify-browser measures the computed result rather than trusting this note. */
.page--studio {
  color-scheme: light;

  --paper: #FFFFFF;
  --ink: #101014;
  /* Derived, not chosen. #E08A55 measures 7.29:1 on the dark paper and 2.65:1 on white, and
     --focus-color is the accent, so on white it would ship a focus indicator below the 3:1 floor.
     This value is the same hue scaled toward black until it measured a target of 3.5:1, which is the
     3:1 requirement with room to spare rather than a value sitting exactly on the line. Measured:
     3.50:1 on white, 5.51:1 on the dark paper. The accent marks themselves are decorative and exempt;
     the focus ring is not, which is what set the target. */
  --accent: #C17749;

  --ink-70: color-mix(in srgb, var(--ink) 70%, transparent);
  --ink-60: color-mix(in srgb, var(--ink) 60%, transparent);
  --ink-50: color-mix(in srgb, var(--ink) 50%, transparent);
  --ink-24: color-mix(in srgb, var(--ink) 24%, transparent);
  --ink-12: color-mix(in srgb, var(--ink) 12%, transparent);
  --ink-06: color-mix(in srgb, var(--ink) 6%, transparent);
  --ink-03: color-mix(in srgb, var(--ink) 3%, transparent);

  --surface-1: var(--ink-03);
  --surface-2: var(--ink-06);
  --surface-hover: var(--ink-06);
  --line: var(--ink-12);
  --line-strong: var(--ink-50);
  --line-faint: var(--ink-06);

  --text-primary: var(--ink);
  --text-secondary: var(--ink-70);
  --text-tertiary: var(--ink-60);

  --focus-color: var(--accent);

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

@media (min-width: 1024px) {
  :root { --header-h: 4.75rem; }
}

@media (prefers-reduced-motion: reduce) {
  :root { --dur-1: 1ms; --dur-2: 1ms; --dur-3: 1ms; --dur-4: 1ms; }
}

@supports not (backdrop-filter: blur(1px)) {
  :root { --frost-bg: var(--frost-bg-solid); }
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  min-width: 20rem;
  margin: 0;
  background: var(--paper);
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
}

.page {
  display: grid;
  grid-template-columns:
    [full-start] minmax(var(--gutter), 1fr)
    [content-start] minmax(0, var(--w-content))
    [content-end] minmax(var(--gutter), 1fr)
    [full-end];
}
/* V12-E: no max-width and no auto margins. The grid does both jobs already: the middle track holds
   the reading column at --w-content, and the two 1fr tracks either side centre it at any width, so
   the cap was doing nothing for the content and everything to the photography. A .bleed child spans
   full-start to full-end, which is the viewport now rather than 1440px, and the heroes, the concept
   band and the lifestyle frames run edge to edge on a large monitor as they always did on a laptop. */

.page > * { grid-column: content; }
.page > .bleed { grid-column: full; }

/* A definite width rather than a max-width, because auto margins on a grid item turn off stretch
   and the box collapses to its own max-content. That is what shrank the centred form to 409px. */
.narrow { width: min(100%, var(--w-narrow)); }

/* Centring is applied deliberately rather than to every narrow block. A max-width alone left an
   800px band flush left in a 1200px column, which is what made the specification figures read as
   pushed to one side. But a model page's opening line belongs on the same left edge as the
   headings and photographs around it: centred, it floats. So the tables, the order block, and the
   forms centre, and running copy stays where its neighbours are. Text inside stays left aligned. */
.centered { margin-inline: auto; }

.section { padding-block: var(--sf-l); }
.section--tight { padding-block: var(--sf-m); }

.cluster { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3); }

@font-face {
  font-family: "Archivo";
  src: url("/assets/fonts/archivo-latin-variable.woff2") format("woff2-variations");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Archivo Fallback";
  src: local("Helvetica Neue");
  size-adjust: 97%;
  ascent-override: 92%;
}

html { background: var(--paper); }
body { overflow-x: hidden; }
body.has-open-sheet { overflow: hidden; }

img { display: block; max-width: 100%; }
a { color: inherit; text-underline-offset: var(--s-1); }
button, input, select, textarea { font: inherit; }
button { color: inherit; }

h1, h2, h3, p { margin-block: 0; }
h1, h2, h3 { text-wrap: balance; }
h1 { font-size: var(--fs-display-l); line-height: var(--lh-display); letter-spacing: var(--ls-display); }
h2 { font-size: var(--fs-display-m); line-height: var(--lh-heading); letter-spacing: var(--ls-display); }
h3 { font-size: var(--fs-display-s); line-height: var(--lh-heading); letter-spacing: var(--ls-title); }
p { color: var(--text-secondary); }
.lede { max-width: var(--w-text); font-size: var(--fs-body-lg); }

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: var(--focus-offset);
  border-radius: var(--r-1);
}

.sr-only {
  position: absolute;
  width: var(--hairline);
  height: var(--hairline);
  padding: 0;
  margin: calc(var(--hairline) * -1);
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: fixed;
  z-index: var(--z-modal);
  top: var(--s-3);
  left: var(--s-3);
  padding: var(--s-3) var(--s-4);
  background: var(--ink);
  color: var(--paper);
  transform: translateY(calc((var(--s-10) + var(--s-5)) * -1));
  transition: transform var(--dur-1) var(--ease-out);
}

.skip-link:focus { transform: translateY(0); }

/* Caps register */
.eyebrow,
.site-footer h2 {
  font-size: var(--fs-eyebrow);
  font-weight: var(--wt-medium);
  letter-spacing: var(--ls-caps-tight);
  line-height: var(--lh-caps);
  text-transform: uppercase;
}

.eyebrow { color: var(--text-tertiary); }
.eyebrow::before {
  content: "";
  display: inline-block;
  width: var(--s-3);
  height: var(--hairline);
  margin-right: var(--s-3);
  background: var(--accent);
  vertical-align: 0.35em;
}

/* Buttons and links */
.button {
  display: inline-flex;
  min-height: var(--hit-target);
  align-items: center;
  justify-content: center;
  border: var(--hairline) solid transparent;
  border-radius: var(--r-1);
  padding: var(--s-3) var(--s-5);
  font-size: var(--fs-nav);
  font-weight: var(--wt-medium);
  letter-spacing: var(--ls-caps);
  line-height: var(--lh-caps);
  text-decoration: none;
  text-transform: uppercase;
  cursor: pointer;
  transition: background var(--dur-1) var(--ease-std), border-color var(--dur-1) var(--ease-std);
}

.button--primary { background: var(--ink); color: var(--paper); }
.button--primary:hover { background: var(--ink-70); }
.button--secondary { border-color: var(--line-strong); background: transparent; color: var(--text-primary); }
.button--secondary:hover { border-color: var(--ink); background: var(--surface-hover); }
.button--inverse { background: var(--dark-ink); color: var(--dark-paper); }
.button--ghost-inverse { border-color: color-mix(in srgb, var(--dark-ink) 60%, transparent); background: transparent; color: var(--dark-ink); }
.button--ghost-inverse:hover { border-color: var(--dark-ink); }

.icon-button {
  display: inline-grid;
  width: var(--hit-target);
  height: var(--hit-target);
  place-items: center;
  border: 0;
  border-radius: var(--r-1);
  background: transparent;
  font-size: var(--fs-title);
  cursor: pointer;
}

.icon-button:hover { background: var(--surface-hover); }

.text-link {
  display: inline-block;
  padding: var(--s-2) 0;
  color: var(--text-primary);
  font-size: var(--fs-body-sm);
  font-weight: var(--wt-medium);
  text-decoration: underline;
  text-decoration-color: var(--line-strong);
}

.text-link span { display: inline-block; transition: transform var(--dur-2) var(--ease-out); }
.text-link:hover span { transform: translateX(var(--s-1)); }

/* Header */
.site-header {
  position: sticky;
  z-index: var(--z-header);
  top: 0;
  height: var(--header-h);
  background: var(--frost-bg);
  backdrop-filter: var(--frost-blur);
}

.site-header.is-scrolled { border-bottom: var(--hairline) solid var(--line); }

.site-header__inner {
  display: flex;
  /* V12-E: was --w-page. The header row's job is to land the lockup on the same optical left edge
     as the content column below it, which is --w-content plus the gutter it is padded by. */
  width: min(100%, calc(var(--w-content) + 2 * var(--gutter)));
  height: 100%;
  align-items: center;
  gap: var(--s-6);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* The lockup no longer swaps: the page is dark, so the reverse artwork is the src the markup ships
   rather than a content: url() override on top of a file it never wants. */
.brand { display: inline-flex; flex: 0 0 auto; align-items: center; outline-offset: var(--s-1); text-decoration: none; }
.brand img { display: block; width: auto; height: 1.25rem; }
.brand--sheet { width: auto; height: 1.375rem; }
.desktop-nav { display: none; align-self: stretch; align-items: center; gap: var(--s-6); }
.nav-link { display: inline-flex; height: 100%; align-items: center; color: var(--text-secondary); font-size: var(--fs-nav); font-weight: var(--wt-medium); letter-spacing: var(--ls-caps); text-decoration: none; text-transform: uppercase; }
.nav-link:hover { color: var(--text-primary); }
.nav-link.is-current { color: var(--text-primary); text-decoration: underline; text-decoration-color: var(--accent); text-decoration-thickness: var(--focus-w); text-underline-offset: var(--s-2); }
.site-header__actions { display: flex; align-items: center; gap: var(--s-2); margin-left: auto; }
.header-request { display: none; }
.menu-button { display: inline-grid; }

.sheet {
  position: fixed;
  z-index: var(--z-panel);
  inset: 0;
  overflow: auto;
  padding: var(--s-5) var(--gutter) var(--sf-l);
  background: var(--paper);
  box-shadow: var(--shadow-2);
}

.sheet__top { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s-5); margin-bottom: var(--sf-m); }
.mobile-nav { display: flex; flex-direction: column; gap: var(--s-5); }
.mobile-nav > a:not(.button) { font-size: var(--fs-display-s); font-weight: var(--wt-bold); text-decoration: none; }
.sheet-backdrop { position: fixed; z-index: calc(var(--z-header) + var(--z-sticky)); inset: 0; background: color-mix(in srgb, var(--pure-black) 52%, transparent); }

/* Page furniture */
.page-header { display: flex; flex-direction: column; gap: var(--s-4); padding-block: var(--sf-l) var(--sf-m); }
.page-header h1 { max-width: var(--w-narrow); }
.page-header > p { max-width: var(--w-text); font-size: var(--fs-body-lg); }
.page-header.form-shell { padding-bottom: var(--sf-s); }
/* V16-A: on /contact/ the subheading is gone and the form reads as part of the header's own
   block, so the header keeps almost none of its usual clearance and the section below gives
   up most of its top padding. Scoped to the modifier rather than to .form-shell, because the
   two dealer form pages keep their intros and their ordinary spacing. */
.page-header--tight { padding-bottom: var(--s-2); }
.page-header--tight + .section--tight { padding-top: var(--s-3); }

/* V10. One title per page, with the accent mark the eyebrow used to carry standing on its own above
   it. A ::before on the heading rather than an element of its own, because an empty span in the
   markup is a label with nothing to say; and above the title rather than inline before it, because an
   inline mark would indent the first line and break the left edge every heading, photograph and
   paragraph on the page is aligned to. Two pixels, not the eyebrow's hairline: a hairline reads as a
   rendering artefact under a title this size. */
.page-header--marked h1::before,
.section-heading--marked h2::before {
  content: "";
  display: block;
  width: var(--s-6);
  height: 2px;
  margin-bottom: var(--s-4);
  background: var(--accent);
}
.section-heading--marked h2::before { margin-bottom: var(--s-3); }
.section-heading { display: flex; flex-direction: column; gap: var(--s-3); margin-bottom: var(--sf-m); }
.section-heading p { max-width: var(--w-text); }
/* V18. The brand-family label above each lineup group: a real heading (h3 on the homepage, h2 on
   /vehicles/) held to the caps register with a hairline rule filling the line, so the model names
   below it keep the display type. It rides the .section-heading base class so both reveal lists
   already cover it. */
.section-heading--group :is(h2, h3) {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  margin-block: 0;
  color: var(--text-secondary);
  font-size: var(--fs-nav);
  font-weight: var(--wt-medium);
  letter-spacing: var(--ls-caps);
  line-height: var(--lh-caps);
  text-transform: uppercase;
}
.section-heading--group :is(h2, h3)::after { content: ""; flex: 1; border-top: var(--hairline) solid var(--line); }
/* Groups stack inside the lineup section with the same breath the vehicle sections keep between
   themselves, so the grouping adds headings without changing the page's rhythm. */
.lineup-group + .lineup-group { margin-top: var(--sf-l); }

/* Cards */
.card-grid { display: grid; grid-template-columns: 1fr; gap: var(--card-gap); }
.card { position: relative; display: flex; min-width: 0; flex-direction: column; overflow: hidden; border: var(--hairline) solid var(--line); border-radius: var(--r-2); background: var(--paper); }
/* Hub cards are delivered at exactly this ratio, so the slot never cover-crops them. */
.card__media { position: relative; aspect-ratio: var(--ar-cutout); overflow: hidden; background: var(--surface-1); }
.card__media img { width: 100%; height: 100%; object-fit: cover; object-position: var(--media-focal, 50% 50%); transition: transform var(--dur-4) var(--ease-out); }
.card__body { display: flex; flex: 1; flex-direction: column; gap: var(--s-2); padding: var(--card-pad); }
.card__title { font-size: var(--fs-display-s); }
.card__link { text-decoration: none; }
.card__link::after { content: ""; position: absolute; inset: 0; }
.card__descriptor { font-size: var(--fs-body-sm); }
.card:hover { border-color: var(--ink-24); }
.card:hover .card__media img { transform: scale(1.03); }
.card:hover .card__title { text-decoration: underline; text-underline-offset: var(--s-2); }

/* Vehicle sections: one per vehicle, media and content alternating sides down the page. The
   homepage renders the lead photograph only, the vehicles page adds two supporting frames. */
.vehicle-scroll { display: flex; flex-direction: column; gap: var(--sf-l); }
.vehicle-section { display: grid; grid-template-columns: 1fr; gap: var(--sf-m); align-items: center; }
/* The name carries the section, so it takes the display-m step rather than the smaller
   heading size its level would otherwise imply. V18 adds h4: the grouped lineup drops the names
   one level under each brand-family heading, and h4 sits outside the global heading resets. */
.vehicle-section__body :is(h2, h3, h4) { margin-block: 0; font-size: var(--fs-display-m); line-height: var(--lh-heading); letter-spacing: var(--ls-display); text-wrap: balance; }
.vehicle-section__media { display: flex; flex-direction: column; gap: var(--s-3); min-width: 0; }
/* Both frames are links to the model page, so they are blocks rather than inline anchors. */
.vehicle-section__lead, .vehicle-section__support { display: block; overflow: hidden; border-radius: var(--r-2); background: var(--surface-1); }
.vehicle-section__lead { aspect-ratio: var(--ar-feature); }
.vehicle-section__row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-3); }
.vehicle-section__support { aspect-ratio: var(--ar-feature); }
.vehicle-section__media img { width: 100%; height: 100%; object-fit: cover; }
.vehicle-section__body { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-4); min-width: 0; }
.vehicle-section__body p { max-width: var(--w-text); }

/* Photo modules: one photograph, a label in the caps register, and the figures that photograph
   shows. The rhythm is one step tighter than the V5 plan proposed, because at the planned
   --sf-xl the captions read as stranded rather than paced. */
.photo-scroll { display: flex; flex-direction: column; gap: var(--sf-l); }
.photo-module { display: grid; grid-template-columns: 1fr; gap: var(--sf-s); margin: 0; align-items: center; }
.photo-module__media { aspect-ratio: var(--ar-feature); overflow: hidden; border-radius: var(--r-2); background: var(--surface-1); }
.photo-module__media img { width: 100%; height: 100%; object-fit: cover; }
.photo-module__body { display: flex; flex-direction: column; gap: var(--s-3); min-width: 0; }
/* Constrained the way the prose it replaced was: below 1024px this column spans the full content
   width, and unconstrained rows would strand a label and its value at opposite ends. */
.photo-module__specs { display: flex; flex-direction: column; width: 100%; max-width: var(--w-text); }

/* Warranty, the manufacturer's estimate sentence, and the model-year qualifier on the past
   models. Quiet by intent: it is the fine print under the figures, not a section of its own. */
.spec-note { display: flex; flex-direction: column; gap: var(--s-3); border-top: var(--hairline) solid var(--line); padding-top: var(--sf-s); color: var(--text-tertiary); font-size: var(--fs-caption); }

/* One way back, in the same place on every page below the homepage. */
/* The gap is not cosmetic: an inline-flex line box discards the whitespace between the arrow span
   and the label, so without it the two run together as "←All vehicles". */
.back-nav a { display: inline-flex; min-height: var(--hit-target); align-items: center; gap: var(--s-1); color: var(--text-secondary); font-size: var(--fs-body-sm); font-weight: var(--wt-medium); text-decoration: none; }
.back-nav a:hover { color: var(--text-primary); text-decoration: underline; text-underline-offset: var(--s-2); }
.back-nav a span { transition: transform var(--dur-2) var(--ease-out); }
.back-nav a:hover span { transform: translateX(calc(-1 * var(--s-1))); }
.hero__content .back-nav a { color: inherit; }
/* V12-A: the way back opens the hero content on the model pages, so it needs its own gap rather
   than the column's --s-4 between it and the eyebrow. It reads as a label above the block, not as
   a first line of it. */
.hero__content .back-nav { margin-bottom: calc(-1 * var(--s-2)); }

/* Past model, on Venice and Carmel. A hairline pill in the caps register, quiet enough to be a
   fact rather than a warning.
   V11 amendment: beside the name rather than under it. On its own line the pill read as a second
   heading and cost the block a whole row of vertical space on four surfaces. Wrapping is allowed and
   the gap is asymmetric because of it: when the pill does drop to its own line it should sit close
   under the name it qualifies, not a full row away. */
.model-headline { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2) var(--s-4); }
.model-tag { align-self: center; border: var(--hairline) solid var(--line-strong); border-radius: var(--r-pill); padding: var(--s-1) var(--s-3); color: var(--text-tertiary); font-size: var(--fs-eyebrow); font-weight: var(--wt-medium); letter-spacing: var(--ls-caps-tight); line-height: var(--lh-caps); text-transform: uppercase; white-space: nowrap; }
.hero__content .model-tag { border-color: color-mix(in srgb, var(--pure-white) 55%, transparent); }

/* V12-A: the sticky model bar and all five of its rules are gone. Owen asked for the bar under the
   hero photograph to go, and its slots were repetitions of the photograph above it: the name was
   the h1, the action was the hero button. The way back moved into .hero__content, above, and on the
   purchase page it sits above the h1. The class is banned in check-content.mjs so it cannot return
   by accident with no markup behind it. */

/* Hero */
.hero { position: relative; display: block; overflow: clip; background: var(--dark-paper); color: var(--dark-ink); }
.hero__media, .hero__scrim { position: absolute; inset: 0; }
.hero__media picture { display: block; width: 100%; height: 100%; }
.hero__image { width: 100%; height: 100%; object-fit: cover; object-position: var(--hero-focal, 50% 45%); }
.hero__scrim { z-index: 1; background: linear-gradient(to top, var(--scrim-strong), var(--scrim-medium) 48%, var(--scrim-clear) 78%); }
.hero__content { position: relative; z-index: 2; display: flex; width: min(100%, var(--w-content)); min-height: var(--hero-min-mobile); flex-direction: column; justify-content: flex-end; gap: var(--s-4); margin-inline: auto; padding: var(--sf-l) var(--gutter); }
.hero__content h1 { max-width: var(--w-narrow); color: var(--pure-white); font-size: var(--fs-display-xl); letter-spacing: var(--ls-display-xl); }
.hero__content p, .hero__content .eyebrow { color: var(--pure-white); }
.hero__descriptor { max-width: var(--w-text); font-size: var(--fs-body-lg); }
.hero__actions { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-2); }

/* Split and row-link blocks */
.split { display: grid; grid-template-columns: 1fr; gap: var(--sf-m); align-items: center; }
.split__body { display: flex; flex-direction: column; gap: var(--s-4); }
.split__body .section-heading { margin-bottom: 0; }
.split__media { display: block; aspect-ratio: var(--ar-cutout); overflow: hidden; border-radius: var(--r-2); }
.split__media img { width: 100%; height: 100%; object-fit: cover; }
/* V11-J: the pathway card rules are retired with the component. V11-D and V11-H removed its last two
   callers, and .pathway joins the retired-components list in check-content.mjs. */

/* V19. The purchase page's long reference block is a grid of native disclosures. The first group
   opens with the page; every other group stays one click away without depending on JavaScript. */
.spec-table { display: grid; grid-template-columns: 1fr; align-items: start; gap: var(--s-4); }
.spec-group { overflow: hidden; border: var(--hairline) solid var(--line); border-radius: var(--r-2); }
.spec-group summary { display: flex; min-height: var(--hit-target); align-items: center; justify-content: space-between; gap: var(--s-4); padding: var(--s-3) var(--s-4); background: var(--surface-1); color: var(--text-secondary); font-size: var(--fs-eyebrow); font-weight: var(--wt-medium); letter-spacing: var(--ls-caps-tight); text-transform: uppercase; cursor: pointer; transition: background var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out); }
.spec-group summary::-webkit-details-marker { display: none; }
.spec-group summary::marker { content: ""; }
.spec-group summary::after { content: "View details  +"; flex: 0 0 auto; color: var(--accent); font-size: var(--fs-caption); letter-spacing: var(--ls-caps-tight); }
.spec-group summary:hover { background: var(--surface-2); color: var(--text-primary); }
.spec-group summary:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: calc(var(--focus-w) * -1); }
.spec-group[open] summary { color: var(--text-primary); }
.spec-group[open] summary::after { content: "Hide details  −"; }
.spec-group .spec-rows { padding: 0 var(--s-4) var(--s-4); }
.spec-row { display: grid; grid-template-columns: 1fr; gap: var(--s-1); border-bottom: var(--hairline) solid var(--line-faint); padding: var(--s-3) 0; font-size: var(--fs-body-sm); }
.spec-row > span { color: var(--text-secondary); }
.spec-row strong { font-weight: var(--wt-medium); font-variant-numeric: tabular-nums; }

/* V11-C. Under a photograph a specification should not read as a data table. The label goes above
   the value in the caps register, the value takes a larger step, there is more air per row, and the
   hairline sits between rows rather than under every one, so the group ends on a value rather than
   on a rule. This is the same move .price already makes on the purchase page, which is the most
   premium block on the site: an existing pattern applied, not a new one invented.

   Scoped to .photo-module__specs deliberately. .spec-row is shared with the GTS specification
   table, which is a reference block a visitor scans for one number, and a table is the right shape
   for that. The two-column rule above and its 768px grid stay in force there and are simply
   overridden here by the extra class in the selector.

   The label-over-value shape is also what the V11-C row splitting is for: a short label over a
   short value reads as a specification, a short label over a 20-word sentence reads as a paragraph
   with a heading, and this treatment would have made the second worse rather than better. */
.photo-module__specs .spec-row {
  display: block;
  border-bottom: 0;
  border-top: var(--hairline) solid var(--line);
  padding: var(--s-4) 0;
}
.photo-module__specs .spec-row:first-child { border-top: 0; padding-top: 0; }
.photo-module__specs .spec-row:last-child { padding-bottom: 0; }
.photo-module__specs .spec-row > span {
  display: block;
  margin-bottom: var(--s-2);
  color: var(--text-tertiary);
  font-size: var(--fs-eyebrow);
  font-weight: var(--wt-medium);
  letter-spacing: var(--ls-caps-tight);
  line-height: var(--lh-caps);
  text-transform: uppercase;
}
.photo-module__specs .spec-row strong {
  display: block;
  color: var(--text-primary);
  font-size: var(--fs-body-lg);
  font-weight: var(--wt-medium);
  line-height: var(--lh-heading);
}

/* Forms. Centred in their column for the same reason the specification band is. */
.form-shell { width: min(100%, var(--w-form)); margin-inline: auto; }
.lead-form { display: flex; width: min(100%, var(--w-form)); flex-direction: column; gap: var(--form-gap); margin-inline: auto; }
.form-grid { display: grid; grid-template-columns: 1fr; gap: var(--form-gap) var(--s-4); }
.field, .field-group { display: flex; min-width: 0; flex-direction: column; gap: var(--s-2); }
.field label, .field-group legend { color: var(--text-primary); font-size: var(--fs-body-sm); font-weight: var(--wt-medium); }
input, select, textarea { width: 100%; min-height: var(--field-h); border: var(--hairline) solid var(--line-strong); border-radius: var(--r-1); padding: var(--s-3); background: transparent; color: var(--text-primary); }
textarea { min-height: calc(var(--hit-target) * 2); resize: vertical; }
input:hover, select:hover, textarea:hover { border-color: var(--ink); }
[aria-invalid="true"] { border-color: var(--danger); border-width: var(--focus-w); }
.field__error { color: var(--danger); font-size: var(--fs-caption); }
.field-group { margin: 0; border: 0; padding: 0; }
.checkbox-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-2); }
.check { display: flex; min-height: var(--hit-target); align-items: flex-start; gap: var(--s-2); }
.check > input { width: var(--icon-size); min-height: var(--icon-size); margin-top: var(--s-1); }
.honeypot { position: absolute; left: -200vw; }
.form-error-summary { border-left: var(--focus-w) solid var(--danger); padding: var(--s-4); color: var(--danger); }
.form-error-summary a { color: inherit; }
.form-status { font-size: var(--fs-body-sm); }
.form-key, .field__help, .form-note { color: var(--text-tertiary); font-size: var(--fs-caption); }
/* Quiet footnote under the submit row rather than a bordered note, which is the difference
   between looking unfinished and looking careful. */
/* The negative pull belongs to the note INSIDE the form, where it exists to tuck the line up under the submit
   row. Declaring it on every .form-note printed the ones outside the form on top of it: on /contact/ and on the
   Launch Edition page a second note follows the form, and the two paragraphs overlapped. A sibling selector was
   not enough, because those notes are not siblings of the form's own note; they are siblings of the form. */
.form-note { max-width: var(--w-text); }
.lead-form .form-note { margin-top: calc(var(--form-gap) * -0.5); }
/* A column with real gaps, for a section that holds a form plus the paragraphs around it. Not .stack: that one
   aligns to the start, which would collapse the form to its own content width instead of its measure. */
.form-stack { display: flex; flex-direction: column; gap: var(--s-5); }
.form-heading { margin-bottom: var(--sf-s); font-size: var(--fs-display-s); }
.form-fieldset { display: flex; min-width: 0; flex-direction: column; gap: var(--form-gap); margin: 0; border: 0; padding: 0; }
/* The margin, not the flex gap, is what separates a legend from the first field: a rendered
   legend sits in the fieldset's border area, outside the flex layout, so the gap never applies
   to it. V16-A widens it after Owen read step one with the legend crowding First name. */
.form-fieldset > legend { padding: 0; margin-bottom: var(--s-5); color: var(--text-primary); font-size: var(--fs-title); font-weight: var(--wt-medium); }
.form-section { border-top: var(--hairline) solid var(--line); padding-top: var(--sf-m); }
.radio-row { display: flex; flex-wrap: wrap; gap: var(--s-5); }
.conditional-field { margin-left: var(--s-6); }
.input-suffix { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
.input-suffix input { grid-area: 1 / 1 / 2 / 3; padding-right: var(--s-10); }
.input-suffix span { z-index: 1; grid-area: 1 / 2; padding-right: var(--s-3); color: var(--text-tertiary); font-size: var(--fs-caption); }
.form-submit-row { display: flex; flex-direction: column; align-items: flex-start; }
/* V16-B. The rule that makes the Contact stepper's submit gating real. site.js has set the row's
   hidden attribute outside the last step since V13, but display:flex on the class beat the UA
   sheet's [hidden] rule, so the button showed on every step anyway. An author rule for the same
   attribute is what the UA sheet could not be. */
.form-submit-row[hidden] { display: none; }

/* Brawley GTS purchase page */
/* V12-D: a hairline under the opening block. The page is a sequence of ruled bands: the figure band
   rules top and bottom, the disclosures rule above. Without one here the title, the price and the
   walkaround ran together as one tall column of white with no structure in it. */
.gts-open { display: flex; flex-direction: column; gap: var(--sf-s); border-bottom: var(--hairline) solid var(--line); padding-bottom: var(--sf-m); }
.gts-open__row { display: flex; flex-direction: column; gap: var(--sf-s); }
.gts-open__intro { display: flex; flex-direction: column; gap: var(--s-4); }
.gts-open__descriptor { max-width: var(--w-text); font-size: var(--fs-body-lg); }
.gts-section > .section-heading { align-items: center; text-align: center; }
.gts-section > .section-heading p { margin-inline: auto; }
.gts-section > .cluster { justify-content: center; }

/* One component, one string, always visible: never a tooltip, never behind a hover. */
.price { display: flex; flex-direction: column; gap: var(--s-1); }
.price__label { color: var(--text-tertiary); font-size: var(--fs-eyebrow); font-weight: var(--wt-medium); letter-spacing: var(--ls-caps-tight); line-height: var(--lh-caps); text-transform: uppercase; }
.price__value { color: var(--text-primary); font-size: var(--fs-display-s); font-weight: var(--wt-medium); font-variant-numeric: tabular-nums; line-height: var(--lh-heading); }
.price__mark { color: var(--text-tertiary); font-size: var(--fs-caption); }
.price__delivery { color: var(--text-secondary); font-size: var(--fs-body-sm); }
.price__disclaimer { max-width: 48ch; color: var(--text-tertiary); font-size: var(--fs-caption); }

/* The walkaround. V12-D: no border and no radius on the stage. The page is white now and the frames
   are delivered on the white they were shot on, so the stage is the same field as the paper around it
   and a hairline rectangle would draw a box around a photograph rather than containing one. The
   vehicle appears to stand on the page. Nothing is keyed and nothing is hidden; the background stays
   var(--paper) so the studio white and the page white are the same declaration. */
.walkaround { display: flex; flex-direction: column; gap: var(--s-5); }
.walkaround__stage { position: relative; overflow: hidden; background: var(--paper); touch-action: pan-y; }
.walkaround__stage[data-ready] { cursor: grab; }
.walkaround__stage.is-dragging { cursor: grabbing; }
.walkaround__frame { width: 100%; height: auto; }
/* The first frame stays in flow and sizes the stage. The rest overlay it from the top left rather
   than stretching to inset: 0, so none of them can be distorted by the stage box. */
.walkaround__frame:not(:first-of-type) { position: absolute; top: 0; left: 0; opacity: 0; transition: opacity var(--dur-2) var(--ease-std); }
.walkaround__frame:not(:first-of-type).is-active { opacity: 1; }
.walkaround__frame:first-of-type { transition: opacity var(--dur-2) var(--ease-std); }
.walkaround__frame:first-of-type:not(.is-active) { opacity: 0; }
/* Below the stage rather than on it. Inside the frame it landed over the wheels on a phone, where
   the studio white leaves almost no clear ground. */
.walkaround__hint { color: var(--text-tertiary); font-size: var(--fs-eyebrow); font-weight: var(--wt-medium); letter-spacing: var(--ls-caps-tight); text-align: center; text-transform: uppercase; transition: opacity var(--dur-3) var(--ease-std); }
.walkaround__hint.is-used { opacity: 0; }
.walkaround__controls { display: flex; align-items: center; justify-content: center; gap: var(--s-5); }
.walkaround__dots { display: flex; align-items: center; gap: var(--s-2); }
.walkaround__dots span { width: var(--s-2); height: var(--s-2); border-radius: var(--r-pill); background: var(--ink-24); transition: background var(--dur-1) var(--ease-std); }
.walkaround__dots span.is-active { background: var(--accent); }
.walkaround__caption { display: flex; flex-direction: column; gap: var(--s-1); text-align: center; }
.walkaround__caption strong { color: var(--text-primary); font-size: var(--fs-title); font-weight: var(--wt-medium); }
.walkaround__caption span { color: var(--text-tertiary); font-size: var(--fs-caption); font-variant-numeric: tabular-nums; }
/* The partial-set note is only true of one colour, so once the island is running it appears when
   that colour is chosen rather than standing on the page apologising in advance. Without
   JavaScript there is no selection to react to, so it stays visible and stays honest. */
.walkaround__note { color: var(--text-tertiary); font-size: var(--fs-caption); text-align: center; }
.walkaround[data-ready] .walkaround__note { display: none; }
.walkaround[data-ready][data-paint="jean-grey"] .walkaround__note { display: block; }

.swatch-tiers { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: center; gap: var(--s-5) var(--sf-m); }
.swatch-tier { display: flex; flex-direction: column; align-items: center; gap: var(--s-3); }
.swatch-tier__label { color: var(--text-tertiary); font-size: var(--fs-eyebrow); font-weight: var(--wt-medium); letter-spacing: var(--ls-caps-tight); line-height: var(--lh-caps); text-transform: uppercase; font-variant-numeric: tabular-nums; }
.swatches { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: var(--s-2); }
.swatch { display: grid; width: var(--hit-target); height: var(--hit-target); place-items: center; border: 0; border-radius: var(--r-pill); padding: 0; background: transparent; }
.swatch:not([disabled]) { cursor: pointer; }
/* V12-D: the ring is --line-strong rather than --ink-24. On the dark page a 24% ring was enough to
   separate every paint from the paper. On white, Ivory White and Jean Grey are within a few percent of
   the page, and at 24% the tile had no edge at all: two of the nine colours were invisible circles. */
.swatch > span { width: var(--s-6); height: var(--s-6); border-radius: var(--r-pill); background: var(--swatch-color); box-shadow: inset 0 0 0 var(--hairline) var(--line-strong); transition: transform var(--dur-2) var(--ease-out); }
.swatch.is-selected > span { outline: var(--focus-w) solid var(--ink); outline-offset: var(--s-1); }

.gts-figures { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sf-s); border-block: var(--hairline) solid var(--line); padding-block: var(--sf-s); }
.gts-figure { display: flex; min-width: 0; flex-direction: column; gap: var(--s-1); }
.gts-figure__value { font-size: var(--fs-display-s); font-weight: var(--wt-medium); font-variant-numeric: tabular-nums; line-height: var(--lh-heading); }
.gts-figure__label { color: var(--text-tertiary); font-size: var(--fs-eyebrow); font-weight: var(--wt-medium); letter-spacing: var(--ls-caps-tight); line-height: var(--lh-caps); text-transform: uppercase; }
/* V12-D: .gts-note is retired with its only caller. The sentence it carried is printed under
   DISCLOSURES at the foot of the page, verbatim, alongside the other two. */

.gts-scene { margin: 0; }
.gts-scene img { width: 100%; height: auto; border-radius: var(--r-2); }
.gts-scene figcaption { margin-top: var(--s-3); color: var(--text-tertiary); font-size: var(--fs-eyebrow); font-weight: var(--wt-medium); letter-spacing: var(--ls-caps-tight); text-transform: uppercase; }

.disclosures { display: flex; flex-direction: column; gap: var(--s-4); border-top: var(--hairline) solid var(--line); padding-top: var(--sf-s); }
.disclosures p { max-width: var(--w-text); color: var(--text-tertiary); font-size: var(--fs-caption); }
.disclosures--centered { align-items: center; text-align: center; }
.disclosures--centered .footnotes { align-items: center; }

/* Concept detail. The wordmark is the page title, so it renders at heading scale and the duplicate
   sans h1 is gone. The artwork is drawn dark on transparency.
   V9 inverted it, because V9 put a dark page behind it: brightness(0) then invert(1), not invert(1)
   alone, because some marks are two-tone (Yuma Defense sets DEFENSE in mid grey) and mid grey
   inverts to mid grey, which all but disappears on a dark background.
   V11-E puts the studio field back, so the artwork renders as authored and the filter is removed
   inside that scope rather than deleted: the rule is still what any dark page would need, and the
   ten routes that are white now say so for themselves. */
.concept-title { margin: 0; }
.concept-title img { width: auto; max-width: 100%; height: auto; max-height: clamp(2.75rem, 6vw, 4.25rem); filter: brightness(0) invert(1); }
.page--studio .concept-title img { filter: none; }
.concept-status { color: var(--text-tertiary); font-size: var(--fs-caption); }
/* The slides carry their own white studio canvas, and the page is white again, so the canvas is the
   page. The figure's background is only there to cover a rounding gap at the edges. */
.concept-figure { overflow: hidden; background: var(--paper); }
.concept-figure picture, .concept-figure img { display: block; width: 100%; height: auto; }
.concept-gallery { display: flex; flex-direction: column; gap: var(--card-gap); }
.concept-back { border-top: var(--hairline) solid var(--line); }
.concept-back a { display: inline-flex; min-height: var(--hit-target); align-items: center; font-size: var(--fs-body-sm); font-weight: var(--wt-medium); }

/* Owner resources */
/* Owner resources. V8 gave the groups the vehicle they are about and turned nineteen hairline rows
   into cards: the page was the only one on the site with no photography, and a list of filenames
   read as a document index rather than as something to act on. */
.resource-groups { display: flex; flex-direction: column; gap: var(--sf-l); }
/* V16-I: the media column's rules are retired with the owner photographs. */
.resource-group { display: grid; grid-template-columns: 1fr; gap: var(--sf-s); }
.resource-group__body { min-width: 0; }
.resource-group h2 { margin-bottom: var(--s-5); font-size: var(--fs-display-s); }
.resource-cards { display: flex; flex-direction: column; gap: var(--s-3); }
.resource-card { display: grid; min-height: var(--hit-target); grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--s-2) var(--s-4); border: var(--hairline) solid var(--line); border-radius: var(--r-2); padding: var(--s-4); text-decoration: none; transition: border-color var(--dur-2) var(--ease-out); }
.resource-card:hover { border-color: var(--line-strong); }
.resource-card:hover .resource-card__title { text-decoration: underline; text-underline-offset: var(--s-2); }
.resource-card__title { font-weight: var(--wt-medium); }
.resource-card__meta { display: inline-flex; align-items: center; gap: var(--s-3); color: var(--text-tertiary); font-size: var(--fs-caption); white-space: nowrap; }
.resource-card__cue { display: inline-block; transition: transform var(--dur-2) var(--ease-out); }

/* 404 */
.not-found { display: flex; min-height: 50vh; flex-direction: column; justify-content: center; gap: var(--s-5); }

/* Footer. Its local re-theme is gone: it existed to force a dark block onto a light page, and on a
   dark page every one of those declarations resolved to the value it was overriding. Without help
   the footer then merges invisibly into the page, so it takes the quietest surface in the set plus
   the top border it already had. That is a distinction, not a panel. */
.site-footer { display: grid; gap: var(--sf-m); padding: var(--sf-l) var(--gutter) var(--sf-m); border-top: var(--hairline) solid var(--line); background: var(--surface-1); }
.footer-links { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sf-m); width: min(100%, var(--w-content)); margin-inline: auto; }
/* V22, D-V22-5. Left, not centred. With Follow gone these are three columns of different lengths, and
   centred ragged columns over a legal band that runs hard left from the content edge gave the footer
   two conflicting alignments. Left-aligning puts every heading, every link, the lockup and the
   copyright line on one edge, which is the whole of what "more professional" asked for here. */
.footer-links > div { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-3); text-align: left; }
.footer-links h2 { color: var(--text-tertiary); }
.footer-links a { color: var(--text-secondary); font-size: var(--fs-body-sm); text-decoration: none; }
.footer-links a:hover { color: var(--text-primary); }
.footer-legal { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-start; gap: var(--s-3) var(--s-5); width: min(100%, var(--w-content)); margin-inline: auto; border-top: var(--hairline) solid var(--ink-24); padding-top: var(--s-5); color: var(--text-tertiary); font-size: var(--fs-caption); text-align: left; }
.footer-lockup { width: auto; height: 1.5rem; }

/* V22 restores .footer-social, which V11-I retired when the six destinations became a text column.
   They are glyphs in their own centred strip again, so they need their own rules back.

   Three numbers here are brand requirements, not taste, and the browser suite asserts all three:

   - The glyph renders at 20px, after two rounds of Owen asking for smaller marks on 2026-08-13 (30px
     originally, then 24, then this). NOTE the size floors it now sits against: it is well below
     Instagram's stated 29x29px, and exactly ON YouTube's stated 20px minimum, clearing only
     Facebook's 16px with room. 20px is therefore the last step this row can take without breaking a
     brand's published minimum outright. Those are legibility guidelines rather than conditions of the
     permissions the artwork relies on, and the request was explicit, so it ships with the deviation
     recorded here and in RIGHTS.md. Anything smaller is a new decision, not a tweak.
   - The anchor is 44x44. That is the WCAG 2.5.5 target size, and the padding it puts around the mark
     doubles as brand clear space.
   - The gap is 20px from 480px up ("more spaced out", then "just a tiny bit tighter"), and 16px below
     that. It has to step down, because six 44px anchors need to fit the roughly 350px a 390px phone
     offers inside its gutters, and at one width the row would otherwise have silently become two.
     Both values clear X's clear-space rule ("minimum equal to the logo's width on all sides") by a
     wide margin now: the 20px mark sits 12px inside its anchor, so adjacent marks are 40px apart at
     the narrow gap and 44px at the wide one, against a 20px mark.

   Colour is pure white, deliberately, and NOT the --text-secondary the retired text links used. Grey
   is an approved rendering for exactly one of these six brands (Instagram, "any solid colour"); for
   Facebook, X, LinkedIn and YouTube the approved single-colour options are white or black only. A
   dimmed white is a grey, so opacity on the resting state is out for the same reason. Hover moves the
   whole anchor's background instead, which changes what is behind the mark and never the mark. */
.footer-social { display: flex; flex-wrap: wrap; justify-content: flex-start; align-items: center; gap: var(--s-4); width: min(100%, var(--w-content)); margin: 0 auto; padding: 0; list-style: none; }
/* Immediately after its own base rule, not in a shared breakpoint block further down: a media query
   adds no specificity, so one declared before these rules would lose to them. V21 lost a whole
   afternoon to exactly that. */
@media (min-width: 480px) { .footer-social { gap: 1.25rem; } }
.footer-social a { display: flex; align-items: center; justify-content: center; width: 2.75rem; height: 2.75rem; border-radius: 50%; color: var(--brand-white); transition: background-color 120ms ease; }
.footer-social a:hover { background: var(--ink-24); }
.footer-social__glyph { display: block; width: 1.25rem; height: 1.25rem; }
/* The anchor is 44px so the touch target clears WCAG 2.5.5, which leaves the 24px mark inset by 10px
   inside it. Left-aligning the row is not enough on its own: without this the first glyph would sit
   10px right of VEHICLES and the lockup below it, which is the misalignment the row was moved left to
   fix. The offset is derived from the two sizes rather than typed, so it stays correct if either
   changes. */
.footer-social > li:first-child { margin-inline-start: calc((2.75rem - 1.25rem) / -2); }
/* Owen on 2026-08-13: "I want it pushed down a bit closer to the line". The footer is a grid with one
   --sf-m gap between every band, which left the marks floating midway between the columns and the
   legal hairline. Half that gap comes back off the bottom so the row sits with the rule it belongs to
   rather than in the middle of the space. The 44px anchor is untouched: the row reads tighter, and
   the touch target still clears WCAG 2.5.5. */
.footer-social { margin-bottom: calc(var(--sf-m) / -2); }

/* V10 footer rows. The list resets its own styling rather than relying on a global reset the site
   does not have; the policy page's lists are the only ones that want markers. */
.footer-legal__links { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--s-3) var(--s-5); margin: 0 0 0 auto; padding: 0; list-style: none; }
.footer-legal__links a { color: var(--text-tertiary); text-decoration: none; }
.footer-legal__links a:hover { color: var(--text-primary); }

/* Policy pages. Running legal copy, so it takes the narrow measure and real list markers: a
   numbered obligation set reads as a list, and this is the one place on the site that has them. */
.policy { display: flex; flex-direction: column; gap: var(--sf-s); }
.policy__section { display: flex; flex-direction: column; gap: var(--s-4); }
.policy h2 { font-size: var(--fs-display-s); }
.policy p { max-width: var(--w-text); }
.policy ul { max-width: var(--w-text); margin: 0; padding-left: var(--s-5); color: var(--text-secondary); }
.policy li + li { margin-top: var(--s-2); }
/* The source prints these two references as bare URLs on their own line, so they stay that way and
   simply become reachable. Wrapping is forced because they are long enough to widen a phone. */
.policy__url a { overflow-wrap: anywhere; }

@media (min-width: 480px) {
  .header-request { display: inline-flex; }
}

@media (min-width: 400px) and (max-width: 1023px) {
  .brand img { height: 1.375rem; }
}

@media (max-width: 399px) {
  .site-header__inner { gap: var(--s-3); }
}

@media (min-width: 640px) {
  .card-grid--concepts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 768px) {
  .hero__content { min-height: var(--hero-min); }
  .hero--content-end .hero__content { align-items: flex-end; }
  .hero--content-end .hero__content > * { width: min(100%, 34rem); text-align: left; }
  .form-grid--pairs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* Fractions, not percentages: 40% + 60% + a gap totals more than the row, so every row
     overflowed its own bottom border by one gap and the values started a gap late. */
  .spec-row { grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); gap: var(--s-4); }
  .split { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--sf-l); }
  /* V11-D. A grid-area swap, the move .vehicle-section--reverse already makes, so the DOM keeps the
     body first and reading order and tab order are unchanged by a purely visual flip. */
  .split--media-first .split__media { grid-area: 1 / 1; }
  .split--media-first .split__body { grid-area: 1 / 2; }
  .gts-figures { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s-5); }
  /* V13 adds Experience, so there are five. Two columns below 768, three here, five at 1024: five
     equal columns at tablet width leaves each one narrower than its longest link label, and the app
     store links in Owners wrap to three lines. Three columns lays five out as a row of three and a
     row of two, which reads as deliberate; five at 1024 gives each column room. */
  .footer-links { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .vehicle-section { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--sf-l); }
  .vehicle-section--reverse .vehicle-section__media { grid-area: 1 / 2; }
  .vehicle-section--reverse .vehicle-section__body { grid-area: 1 / 1; }
  /* V16-I: the owner library's photograph column and its V11-G centring rule are retired with the
     photographs; every group is the plain single-column list at every width. */
}

@media (min-width: 1024px) {
  /* V22: the footer's 4-column rule is gone with the Follow column. Three columns settle at 768px
     and stay there, so there is no wider footer grid to declare here. */
  .brand img { height: 1.75rem; }
  .desktop-nav { display: flex; }
  .menu-button { display: none; }
  .card-grid--concepts { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  /* Only the modules that carry figures split into two columns. A label with nothing beside it
     keeps the full width and sits under its photograph. */
  .photo-module:not(.photo-module--plain) { grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr); gap: var(--sf-m); }
  .photo-module--reverse .photo-module__media { grid-area: 1 / 2; }
  .photo-module--reverse .photo-module__body { grid-area: 1 / 1; }
  /* The name and the price sit on one line, the price against the right edge, so the two things a
     visitor came for are the first two things they read. */
  .gts-open__row { flex-direction: row; align-items: flex-end; justify-content: space-between; gap: var(--sf-m); }
  .gts-open__row .price { align-items: flex-end; text-align: right; }
}

/* Ambient video, V10, reduced to one loop in V11-A. Like the concept band, this sits outside the
   @supports (animation-timeline) block below on purpose: it advances with the clock, not with scroll.

   The .ambient block family is retired with the two below-fold loops it styled. What it established
   is kept here on the hero: the poster is the layout, an in-flow image carrying its own width and
   height so the box is reserved before anything loads, and the video is absolutely positioned on top
   at the same size with the same object-fit and the same focal point. Both are 1900 by 900, so the
   two images are the same crop of the same frame and the switch between them cannot move a pixel.
   The poster is never removed, which keeps the transition a cross-fade rather than a flash of black:
   there is always a painted image underneath. */
.hero__toggle {
  min-height: var(--hit-target);
  border: 0;
  padding: 0;
  background: transparent;
  color: var(--text-tertiary);
  font-size: var(--fs-eyebrow);
  font-weight: var(--wt-medium);
  letter-spacing: var(--ls-caps-tight);
  text-transform: uppercase;
  cursor: pointer;
}

/* The homepage hero's loop, and the only video on the site. The poster stays the eagerly fetched
   LCP element beneath it. */
.hero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--hero-focal, 50% 45%);
  opacity: 0;
  transition: opacity var(--dur-4) var(--ease-std);
}
.hero[data-painted] .hero__video { opacity: 1; }
/* V16-D. Below the gate the phone's hero is the tall still and nothing else: the element that
   site.js already refuses to load there now has no box either, so no rendering path can put a
   stalled player behind the still. The pause control goes with it. */
@media (max-width: 767px) {
  .hero__video, .hero__bar { display: none; }
}
/* Top right, under the header and clear of both the bottom-left content column and the safe-area
   insets a phone reserves at the foot of the screen. Solid paper rather than a translucent chip, so
   the label's contrast is a fixed 15:1 against a known colour instead of whatever the film is doing
   behind it in that frame. */
.hero__bar { position: absolute; z-index: 3; top: var(--s-4); right: var(--gutter); }
.hero__toggle {
  border: var(--hairline) solid color-mix(in srgb, var(--dark-ink) 24%, transparent);
  border-radius: var(--r-1);
  padding-inline: var(--s-4);
  background: var(--dark-paper);
  color: var(--dark-ink);
}
.hero__toggle:hover { border-color: var(--dark-ink); }

/* V16-C. The homepage intro veil: the lockup arrives on the site's own paper, holds, and the whole
   veil dissolves into the page. Opacity and transform only, so nothing here can shift layout, and
   the final keyframe holds visibility hidden, which is what ends both the paint and the pointer
   interception without a single line of script. The session gate script in the head is additive:
   without it the veil simply plays again, briefly, on each visit to the homepage. */
@keyframes intro-veil {
  0%, 72% { opacity: 1; visibility: visible; }
  100% { opacity: 0; visibility: hidden; }
}
@keyframes intro-veil-logo {
  0% { opacity: 0; transform: scale(0.975); }
  30%, 74% { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.03); }
}
.intro-veil {
  position: fixed;
  inset: 0;
  z-index: var(--z-veil);
  display: grid;
  place-items: center;
  padding-inline: var(--gutter);
  background: var(--paper);
  animation: intro-veil 2.3s var(--ease-std) both;
}
.intro-veil__logo {
  width: min(100%, 20rem);
  height: auto;
  animation: intro-veil-logo 2.3s var(--ease-std) both;
}
/* Gone entirely, not merely instant, for a visitor who asked for less motion, and gone for the
   rest of a session once it has played. */
@media (prefers-reduced-motion: reduce) { .intro-veil { display: none; } }
html[data-intro-seen] .intro-veil { display: none; }

/* The concept band. Deliberately outside the @supports (animation-timeline: view()) block below.
   V9 amendment 2: the V6 rule "scroll reveals use CSS view() timelines only" governs scroll-driven
   motion, and this advances with the clock rather than with scroll. It is the same class of animation
   as the hover transitions above, just infinite, so it belongs here and not in there. */
.concept-marquee { display: flex; flex-direction: column; gap: var(--s-3); margin-bottom: var(--sf-m); }
/* clip rather than hidden: clip cannot become a scroll container, and transform overflow never
   contributes to scrollWidth, so the 200 percent zoom proxy passes by construction. */
.concept-marquee__viewport { overflow: clip; }
/* Items carry their own margin-right and the track must NOT use gap. With gap, the halfway point of
   the track is nine items plus eight gaps rather than nine of each, and the -50% loop seams. */
.concept-marquee__track { display: flex; width: max-content; }
.concept-marquee__item { display: flex; width: var(--marquee-item); flex: 0 0 auto; flex-direction: column; gap: var(--s-2); margin-right: var(--card-gap); }
.concept-marquee__item img { width: 100%; height: auto; aspect-ratio: var(--ar-cutout); object-fit: cover; border: var(--hairline) solid var(--line); border-radius: var(--r-2); }
.concept-marquee__name { color: var(--text-tertiary); font-size: var(--fs-eyebrow); font-weight: var(--wt-medium); letter-spacing: var(--ls-caps-tight); line-height: var(--lh-caps); text-transform: uppercase; }
/* The bar lands on the same optical line as the content column at every width, the model bar's trick.
   min-height is not cosmetic: the pause button ships hidden and site.js reveals it, so without the
   space reserved the bar grows from nothing to a row after load and shoves the grid below it down.
   Measured, that shift was the page's entire CLS at 0.015 against 0 before the band existed. */
/* V11 amendment, Owen on 2026-08-05: "Take away the pause signs for the videos." Both PAUSE labels
   leave the page, the hero's and this one, and neither control leaves the document.

   Visually hidden until focused, which is the skip link's own pattern applied to a different job.
   The reason it is hidden rather than deleted: the homepage loop starts on its own and runs longer
   than five seconds, and the concept band drifts continuously, so both are exactly the content WCAG
   2.2.2 asks to be pausable. Neither axe nor Lighthouse can see that failure, because neither can
   detect motion, so deleting these would have cost nothing on any score and broken the requirement
   silently. Keyboard and assistive technology still reach both; the page just no longer carries a
   caps-register label a sighted visitor did not ask for.

   min-height goes to zero with the visible control. The V9 note about this reserving the row still
   applies to the case it was written for, a button that appears after load; nothing appears after
   load any more, so there is no row to reserve and no shift to prevent. The focused state does grow
   the bar, which is a user-initiated change and outside CLS by definition. */
.concept-marquee__bar { display: flex; width: min(100%, calc(var(--w-content) + 2 * var(--gutter))); min-height: 0; justify-content: flex-end; margin-inline: auto; padding-inline: var(--gutter); }
.concept-marquee__toggle:not(:focus-visible),
.hero__toggle:not(:focus-visible) {
  position: absolute;
  width: var(--hairline);
  height: var(--hairline);
  min-height: 0;
  padding: 0;
  margin: calc(var(--hairline) * -1);
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.concept-marquee__toggle { min-height: var(--hit-target); border: 0; padding: 0; background: transparent; color: var(--text-tertiary); font-size: var(--fs-eyebrow); font-weight: var(--wt-medium); letter-spacing: var(--ls-caps-tight); text-transform: uppercase; cursor: pointer; }
.concept-marquee__toggle:hover { color: var(--text-primary); }

/* V11-F. Below 768px the band is not rendered at all. A filmstrip beside a title needs a column to
   sit in and a phone has one; the alternative treatments are the strip back under the header, which
   is the thing Owen asked to stop, or a strip behind the title at a width where it would compete
   with it. Not rendering it is also why every tile is lazy: display: none is what keeps a phone from
   spending a single byte on this. */
.concept-marquee { display: none; }

@media (min-width: 768px) {
  .concept-marquee { display: flex; }

  /* The overlap. Both children are assigned the same implicit row of .page, so they occupy the same
     band of the page: the header keeps its content column and the band takes the full bleed column
     behind it. z-index rather than DOM order, because the header ships first so that the title is
     read and reached before the decoration's pause button. Grid items take z-index without needing
     position, so nothing here creates a containing block the sticky header would notice. */
  .page--concepts > .page-header { grid-row: 1; z-index: 1; }
  .page--concepts > .concept-marquee { grid-row: 1; align-self: center; margin-bottom: 0; }

  /* Masked away from the left, so the strip emerges from behind the title rather than beginning at
     the page edge and running into it. The ramp is wide on purpose: a hard edge would read as a
     panel boundary, which is the opposite of "seamless in the background". */
  .page--concepts .concept-marquee__viewport {
    /* Held well below full strength before a single pixel of scroll. This is the difference between
       a filmstrip placed behind a title and a row of cards competing with one: at full opacity the
       tiles read as an index the visitor has already arrived at, which is the job of the nine real
       cards below. --strip-base is the value both dissolve paths start from, so the quiet state and
       the fade are one decision rather than two that can drift. */
    --strip-base: 0.5;
    -webkit-mask-image: linear-gradient(to right, transparent 0%, transparent 30%, var(--pure-black) 68%);
    mask-image: linear-gradient(to right, transparent 0%, transparent 30%, var(--pure-black) 68%);
  }

  /* No frame and no caption in this position. A border and a radius make a tile read as a card, and
     a name under it makes the row read as a second index; both are exactly what the nine cards below
     are for. What is wanted here is the shape of the vehicles drifting behind the title. */
  .page--concepts .concept-marquee__item img { border: 0; border-radius: 0; }
  .page--concepts .concept-marquee__name { display: none; }

  /* The band is decoration behind the page's own title, so nothing in it takes a pointer event
     except the control that stops it. */
  .page--concepts > .concept-marquee { pointer-events: none; }
  .page--concepts .concept-marquee__bar { pointer-events: auto; }
}

/* The dissolve, on the viewport alone so the control never fades with it. Two paths, exactly as the
   scroll reveals have: a view() timeline where it is supported, and an IntersectionObserver in
   site.js where it is not, which writes the strip's own visible fraction into --strip-visibility.
   Both end in the same place, and it is the same place the check measures: fully opaque before the
   visitor has scrolled, fully gone by the time the card grid is on screen.

   Blur and opacity only. Neither reserves nor releases space, so CLS stays 0, and the band's
   min-height on the bar still holds the control's row before site.js reveals the button. */
/* Outside any motion query on purpose. --strip-visibility defaults to 1 and site.js never sets it
   under reduced motion, so this resolves to the quiet base state and a zero blur: a static strip held
   behind the title, which is the right still frame of the thing the other path animates. */
.concept-marquee__viewport {
  opacity: calc(var(--strip-base, 1) * var(--strip-visibility, 1));
  filter: blur(calc((1 - var(--strip-visibility, 1)) * 14px));
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* var() inside a keyframe resolves against the animated element, so both dissolve paths begin at
       the same --strip-base and there is one number to change rather than two. */
    @keyframes strip-dissolve {
      from { opacity: var(--strip-base, 1); filter: blur(0); }
      to { opacity: 0; filter: blur(14px); }
    }
    /* exit, not entry: the band is at the top of the page and is already fully in view at scroll
       zero, so what is being tracked is it leaving.

       Both ends are deliberate and both are measured. The start is NEGATIVE, which means the
       dissolve begins while the band is still entirely on screen rather than waiting for it to reach
       the top of the scrollport: without that, nothing happened for the first 117px of scroll and the
       strip appeared to hang. The end is 45% rather than 100% because Owen asked for it gone much
       sooner: "I want it to fade out a little bit quicker when it hits that they're not offered for
       sale and there is no pricing. I want it to go through the middle of that and fade out a lot
       sooner than that." Measured on this page at 1440 by 1000, the band sits at y=117 and is 254
       tall, so this dissolves between roughly 15px and 231px of scroll, which puts the end of it at
       the moment the intro paragraph's own middle reaches the top of the screen. The card grid does
       not begin until 452. */
    .page--concepts .concept-marquee__viewport {
      animation: strip-dissolve linear both;
      animation-timeline: view();
      animation-range: exit -40% exit 45%;
    }
  }
}

@keyframes concept-drift {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

/* data-ready is set by site.js in the same block that reveals the pause button, so the band never
   drifts on a page that cannot stop it. Without script it is a static filmstrip.
   Longhands, not the animation shorthand. The shorthand resets animation-play-state to running, and
   the first version of this rule also carried a redundant [data-marquee], which put it at four
   class-level components against the pause rules' three. It therefore outranked them and nothing
   stopped: hover and the button both changed state while the band kept drifting. Both halves are
   fixed here, and the longhands are the half that matters, because they leave play-state entirely to
   the pause rules no matter what a future selector does to the specificity. */
.concept-marquee[data-ready] .concept-marquee__track {
  animation-name: concept-drift;
  animation-duration: var(--dur-marquee);
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
/* The button holds state; focus-within does not, so tabbing to the control stops the band under the
   hand that is about to press it and releases it on blur. */
.concept-marquee.is-paused .concept-marquee__track,
.concept-marquee:focus-within .concept-marquee__track { animation-play-state: paused; }

/* Hover response. Gated on a fine pointer with real hover, so a phone never holds a hover state
   after a tap. Every transform matches the scale the concept cards already use. */
@media (hover: hover) and (pointer: fine) {
  .vehicle-section__lead img,
  .vehicle-section__support img { transition: transform var(--dur-4) var(--ease-out); }
  .vehicle-section__lead:hover img,
  .vehicle-section__support:hover img { transform: scale(1.03); }
  .swatch:not([disabled]):hover > span { transform: scale(1.12); }
  .resource-card:hover .resource-card__cue { transform: translateY(var(--s-1)); }
  /* V12-B: both hover-pause rules are gone, per Owen on 2026-08-06: the band should "never stop even
     when you hover so it looks smooth and continuous". A pointer resting anywhere near the title used
     to stall the drift, which read as a stutter rather than as a control. WCAG 2.2.2 still holds
     without them, and by the mechanism it prefers: the band carries a real pause button, hidden until
     focused, and :focus-within above stops the drift while that control has focus. */
}

/* Scroll motion, V12-C. ONE mechanism for the reveals, on every browser.

   V11 shipped two paths: scroll-progress view() timelines where supported, an IntersectionObserver
   transition everywhere else. Both were correct in the sense of running, and Owen still reported
   seeing no scroll motion at all, twice, with Reduce Motion confirmed off. The scrubbed path is why.
   A scrubbed animation has no duration: its progress is the scroll position, so the reveal advances
   only as far as the wheel turns. The range ran entry 0% to entry 65%, which finishes while the
   element is still in the bottom eighth of the screen, and a normal trackpad flick crosses that whole
   range inside two frames. The element was therefore fully resolved before it reached the part of the
   page anybody reads. Nothing was broken; the effect was simply spent off-screen.

   So the reveals are time-based now, and every browser gets the same one: an element is marked when
   it crosses into view and animates on its own clock from there, at a duration a person can see,
   regardless of how fast the page was scrolled. view() survives for exactly the two effects where
   scrubbing IS the effect and a duration would be wrong, both further down: the hero photograph's
   parallax drift and the concept band's dissolve. Neither reveals content.

   Four properties are load-bearing rather than incidental:
   - The start state lives on [data-reveal], and only site.js ever sets that attribute. A page whose
     JavaScript never arrives renders every element in its final state. This was true in V11 and is
     more important now that this is the only path: nothing may be left hidden by a script that
     failed to load.
   - site.js marks only elements that sit entirely below the fold at init. Text the visitor is
     already reading is never re-hidden after load.
   - opacity and transform only, so CLS stays 0. Nothing here reserves or releases space.
   - Reduced motion is honoured in both places: this block is inside no-preference, and site.js bails
     before marking anything. */
@media (prefers-reduced-motion: no-preference) {
  [data-reveal] {
    opacity: 0;
    transform: translateY(var(--rise, 2rem));
    transition: opacity var(--dur-4) var(--ease-out), transform var(--dur-4) var(--ease-out);
    transition-delay: var(--reveal-delay, 0ms);
    will-change: opacity, transform;
  }
  /* Equal specificity to the rule above, so source order is what makes it win. Deliberate: it keeps
     the revealed state readable as "the same selector, later" rather than as a specificity trick. */
  [data-reveal="shown"] { opacity: 1; transform: none; will-change: auto; }

  /* Small things travel less. A specification row or a form field rising a full 2rem reads as a jump
     out of proportion to the element; the shorter distance keeps the stagger legible instead. These
     sit outside the @supports guard because they belong to the transition above, not to view(). */
  .photo-module__specs .spec-row,
  .vehicle-section__row .vehicle-section__support,
  .lead-form > .field,
  .lead-form > .form-submit-row,
  .policy__section { --rise: 1rem; }
}

/* What remains of the view() path, and the reason it remains: a parallax drift is a position, not an
   event. It has no beginning or end to play, so tying it to scroll progress is not a compromise, it
   is the definition of the effect. The concept band's dissolve, further up this file, stays for the
   same reason. Neither reveals content, so neither can hide anything from a browser that lacks view().

   V12-C removed the rest: the rise-in keyframes, the sixteen-selector scrub list, the per-row and
   per-card range offsets, and the word cascade. The cascade went for the same reason as the block
   reveals and worse: a word's whole animation was one line-height of scroll, so it was the least
   visible thing on the site, and it kept a third copy of the selector list in the verification
   suite. The reveals it used to decorate now arrive on a clock, whole. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* The photograph drifts against the scroll. Held to a few percent, and pre-scaled so the
       frame stays covered at the extremes. */
    @keyframes hero-drift {
      from { transform: scale(1.08) translateY(1.5%); }
      to { transform: scale(1.08) translateY(-1.5%); }
    }
    .hero__image {
      animation: hero-drift linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }
  }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { scroll-behavior: auto !important; animation-duration: var(--dur-1) !important; transition-duration: var(--dur-1) !important; }
  .card:hover .card__media img,
  .vehicle-section__lead:hover img,
  .vehicle-section__support:hover img,
  .swatch:hover > span,
  .back-nav a:hover span,
  .resource-card:hover .resource-card__cue { transform: none; }
  /* Scroll-driven animations advance with scroll position rather than time, so clearing the
     duration is not enough: the timeline itself has to go. */
  *, *::before, *::after { animation-timeline: auto !important; animation-name: none !important; }
}

/* ---------------------------------------------------------------------------------------------
   V13. New page types, built out of the existing system rather than beside it. Every colour below is
   a token, every step is on the existing type scale, every gap is a spacing token, and the corners
   stay squared at --r-1 and --r-2. Nothing here introduces a second visual language: if a rule looks
   familiar it is because it is the same rule an existing component already uses.
   --------------------------------------------------------------------------------------------- */

/* Footnotes. The mark is small, quiet, and never the accent: an orange asterisk beside a figure reads
   as a warning rather than as a reference. */
.fn-ref { font-size: var(--fs-caption); line-height: 0; }
.fn-ref a { color: var(--text-tertiary); text-decoration: none; padding-inline: 1px; }
.fn-ref a:hover { color: var(--text-primary); }
.footnotes { display: flex; flex-direction: column; gap: var(--s-2); }
.footnote { display: block; max-width: var(--w-text); color: var(--text-tertiary); font-size: var(--fs-caption); }
/* The sticky header would otherwise cover a note jumped to from its reference. The same offset is why
   the reference is reachable coming back: both ends of the jump clear the header. */
.footnote, .fn-ref a { scroll-margin-top: calc(var(--header-h) + var(--s-5)); }
.footnote:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
.footnote__back { color: var(--text-tertiary); text-decoration: none; }
.footnote__back:hover { color: var(--text-primary); }

/* The sample marker on mock operational content. Deliberately not styled as a warning: red is reserved
   for genuine safety severity and form errors, so this is a hairline-bordered block in the quiet
   surface with a caps tag, which reads as a label rather than an alarm. */
/* V15-F: the .sample-note and .sample-tag rules are retired with the markers they styled. */
/* p and h have zero margin sitewide, so a paragraph followed by an action needs the gap declared rather than
   inherited. Used where a short block is text-then-button: the past-model inventory note and the Launch
   Edition's opening. */
.stack { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-5); }
.stack p { max-width: var(--w-text); }

/* Past models on /vehicles/. Quieter than a vehicle section by construction: two compact cards in a
   grid rather than two full-bleed alternating rows. */
.past-grid { display: grid; grid-template-columns: 1fr; gap: var(--card-gap); }
.past-card { display: flex; min-width: 0; flex-direction: column; gap: var(--s-4); }
.past-card__media { display: block; aspect-ratio: var(--ar-feature); overflow: hidden; border-radius: var(--r-2); background: var(--surface-1); }
.past-card__media img { width: 100%; height: 100%; object-fit: cover; }
.past-card__body { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-3); }
.past-card__title { font-size: var(--fs-display-s); }
.past-card__title a { text-decoration: none; }
.past-card__title a:hover { text-decoration: underline; text-underline-offset: var(--s-2); }
.past-card p { max-width: var(--w-text); font-size: var(--fs-body-sm); }

/* The past-model gallery. The concept gallery's shape: a photograph at the content width with its
   caption beneath, stacked. No frame, no carousel, no controls. */
.photo-gallery { display: flex; flex-direction: column; gap: var(--sf-m); }
.photo-gallery__figure { margin: 0; }
.photo-gallery__media { overflow: hidden; border-radius: var(--r-2); background: var(--surface-1); }
.photo-gallery__media img { width: 100%; height: auto; }
.photo-gallery__caption { margin-top: var(--s-3); color: var(--text-tertiary); font-size: var(--fs-eyebrow); font-weight: var(--wt-medium); letter-spacing: var(--ls-caps-tight); line-height: var(--lh-caps); text-transform: uppercase; }

/* Editorial. Long-form copy takes the narrow measure, which is the rule the policy page already
   follows: running text set to the full 1200px column is unreadable at any type size. */
.prose { display: flex; flex-direction: column; gap: var(--s-5); max-width: var(--w-text); }
.prose p { max-width: var(--w-text); }
.prose h2 { margin-top: var(--sf-s); font-size: var(--fs-display-s); }
.prose h3 { font-size: var(--fs-title); }
.prose ul { max-width: var(--w-text); margin: 0; padding-left: var(--s-5); color: var(--text-secondary); }
.prose li + li { margin-top: var(--s-2); }
.prose__quote { margin: 0; border-left: 2px solid var(--accent); padding-left: var(--s-5); }
.prose__quote p { color: var(--text-primary); font-size: var(--fs-body-lg); }
.prose__quote footer { margin-top: var(--s-2); color: var(--text-tertiary); font-size: var(--fs-caption); }
.prose__figure { margin: 0; }
.prose__figure img { width: 100%; height: auto; border-radius: var(--r-2); }
.prose__figure figcaption { margin-top: var(--s-2); color: var(--text-tertiary); font-size: var(--fs-caption); }

/* The inquiry address is one unbreakable 22-character token. In the two-column footer grid at 320px its
   column is about 130px wide, so without this it pushed the document wider than the viewport on every page.
   Breaking mid-address is not pretty; it keeps the whole address visible, which is what Owen asked for, and a
   truncated or clipped address would fail the requirement outright. */
.footer-email { overflow-wrap: anywhere; }

.card-grid--posts { grid-template-columns: 1fr; }
.post-card { display: flex; min-width: 0; flex-direction: column; gap: var(--s-4); }
/* V16-G: a hairline and clear air between the feed's stories, so consecutive posts never read as
   one collage. Scoped to the Experience module; the three-column archive grids keep their gaps. */
.experience-module .post-card + .post-card { border-top: var(--hairline) solid var(--line); padding-top: var(--sf-m); margin-top: var(--sf-m); }
.post-card__media { overflow: hidden; border-radius: var(--r-2); aspect-ratio: var(--ar-feature); background: var(--surface-1); }
.post-card__media img, .post-card__media a { display: block; width: 100%; height: 100%; }
.post-card__media img { object-fit: cover; transition: transform var(--dur-4) var(--ease-out); }
.post-card__body { display: flex; min-width: 0; flex-direction: column; gap: var(--s-2); }
/* V15: the per-card category kicker is retired; the feed presents every story the same way. */
.post-card__title { font-size: var(--fs-display-s); }
.post-card__link { text-decoration: none; }
.post-card__link:hover { text-decoration: underline; text-underline-offset: var(--s-2); }
.post-card__excerpt { max-width: var(--w-text); font-size: var(--fs-body-sm); }
.post-card__pending { color: var(--text-tertiary); font-size: var(--fs-caption); }
.post-meta { display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-4); color: var(--text-tertiary); font-size: var(--fs-caption); }
.post-meta__author { color: var(--text-secondary); }
.article-header__standfirst { max-width: var(--w-text); font-size: var(--fs-body-lg); }
.article-hero { margin: 0; }
.article-hero img { width: 100%; height: auto; border-radius: var(--r-2); }
.experience-module + .experience-module { border-top: var(--hairline) solid var(--line); }

/* Careers and safety records. One list treatment for both: a hairline-ruled stack rather than a card
   wall, because both are things to read down rather than to browse across. */
.record-list { display: flex; flex-direction: column; gap: 0; }
.record-card { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-3); border-top: var(--hairline) solid var(--line); padding-block: var(--sf-s); }
.record-card:last-child { border-bottom: var(--hairline) solid var(--line); }
.record-card__head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2) var(--s-4); }
.record-card__title { font-size: var(--fs-display-s); }
.record-card__title a { text-decoration: none; }
.record-card__title a:hover { text-decoration: underline; text-underline-offset: var(--s-2); }
.record-card__summary { max-width: var(--w-text); }
.record-card__meta { color: var(--text-tertiary); font-size: var(--fs-caption); }
.record-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3) var(--s-5); }
/* V17: a recall title is a full sentence naming the model, the model years and the hazard, and at the
   display-L a job title takes it ran to seven lines before the facts. One step down, the same move
   V16-G made on the featured story, for the same reason: the size was fighting the sentence. */
.record-head--notice h1 { font-size: var(--fs-display-m); }
.record-section + .record-section { margin-top: var(--sf-m); }
.record-section h2 { margin-bottom: var(--s-4); font-size: var(--fs-display-s); }
.record-section ul { max-width: var(--w-text); margin: 0; padding-left: var(--s-5); color: var(--text-secondary); }
.record-section li + li { margin-top: var(--s-2); }
.record-legal, .record-fallback { max-width: var(--w-text); color: var(--text-tertiary); font-size: var(--fs-caption); }
.fact-row { display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-4); margin: 0; padding: 0; list-style: none; color: var(--text-tertiary); font-size: var(--fs-caption); }
.fact-row li { display: inline-flex; align-items: center; }
/* A separator that is not a character in the accessible name: a bullet typed between list items is
   read out, and a border is not. */
.fact-row li + li { border-left: var(--hairline) solid var(--line-strong); padding-left: var(--s-4); }
.fact-row--detail { font-size: var(--fs-body-sm); }
.apply-disabled { flex-direction: column; align-items: flex-start; gap: var(--s-3); }
.apply-disabled .button[disabled] { cursor: not-allowed; opacity: 0.55; }

/* A safety notice's facts are a definition list, because that is what they are: a fixed set of labelled
   values a visitor scans for one answer. Two columns from 640px, label above value below it. */
.notice-facts { display: grid; grid-template-columns: 1fr; gap: var(--s-1) var(--s-4); margin: 0; }
.notice-facts dt { color: var(--text-tertiary); font-size: var(--fs-eyebrow); font-weight: var(--wt-medium); letter-spacing: var(--ls-caps-tight); line-height: var(--lh-caps); text-transform: uppercase; }
.notice-facts dd { margin: 0 0 var(--s-3); max-width: var(--w-text); color: var(--text-secondary); font-size: var(--fs-body-sm); }
.notice-facts--detail dd { font-size: var(--fs-body); }
.empty-state { border: var(--hairline) solid var(--line); border-radius: var(--r-2); padding: var(--sf-m); }
.empty-state p { max-width: var(--w-text); }

/* Privacy. The document keeps its narrow measure and gains a contents column beside it above 1024px. */
.policy-header__meta { display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-4); margin: 0; color: var(--text-tertiary); font-size: var(--fs-caption); }
.policy-header__meta dt { font-weight: var(--wt-medium); }
.policy-header__meta dd { margin: 0 var(--s-4) 0 0; }
.policy-layout { display: grid; grid-template-columns: 1fr; gap: var(--sf-m); }
.policy__section { scroll-margin-top: calc(var(--header-h) + var(--s-5)); }
.policy-toc__desktop { display: none; }
.policy-toc__mobile summary { display: flex; min-height: var(--hit-target); align-items: center; color: var(--text-primary); font-size: var(--fs-body-sm); font-weight: var(--wt-medium); cursor: pointer; }
.policy-toc__list { display: flex; flex-direction: column; gap: var(--s-2); margin: var(--s-3) 0 0; padding-left: var(--s-5); color: var(--text-tertiary); font-size: var(--fs-body-sm); }
.policy-toc__list a { color: var(--text-secondary); text-decoration: none; }
.policy-toc__list a:hover { color: var(--text-primary); text-decoration: underline; text-underline-offset: var(--s-2); }
.policy-toc__title { color: var(--text-tertiary); font-size: var(--fs-eyebrow); font-weight: var(--wt-medium); letter-spacing: var(--ls-caps-tight); text-transform: uppercase; }

/* The Launch Edition page. Typography-led on purpose: no supplied photograph proves numbered badging or
   a carbon-fiber part, so the page states the equipment in type rather than implying it with imagery.
   V20: the page carries exactly one caps eyebrow, in the hero. Both wide bands share one measure so the
   reading column stops changing width on the way down, and the highlights read as a list rather than as
   ten headlines. */
/* D-V20-2. The market qualifier, not a second eyebrow. Same text, held by two suites; a hairline chip in
   secondary ink instead of a full-width pure-white caps line. Styled on its own class rather than by
   borrowing .model-tag, which three suites count sitewide and which this is not. */
.launch-market { align-self: start; border: var(--hairline) solid color-mix(in srgb, var(--pure-white) 45%, transparent); border-radius: var(--r-pill); padding: var(--s-1) var(--s-3); color: color-mix(in srgb, var(--pure-white) 80%, transparent); font-size: var(--fs-eyebrow); font-weight: var(--wt-medium); letter-spacing: var(--ls-caps-tight); line-height: var(--lh-caps); text-transform: uppercase; }
.launch-lede { align-items: center; text-align: center; }
.launch-lede p { margin-inline: auto; }
/* D-V20-3. Both wide bands come in from the 1200px content width to --w-narrow, the measure the lede and
   the closing block already use. Two things follow. The hairline rules hug their text instead of running
   580px past it, and the page settles onto two widths, 800 for everything that is reading and 640 for the
   form, instead of stepping 800 / 1200 / 1200 / 640 / 800 on the way down. */
.launch-band { max-width: var(--w-narrow); margin-inline: auto; }
.launch-highlights { display: grid; grid-template-columns: 1fr; gap: 0; margin: 0; padding: 0; list-style: none; }
/* D-V20-5. --fs-body, not --fs-body-lg: ten rows at the larger step read as ten headlines. text-wrap keeps
   the one item long enough to wrap from dropping "50" onto a line of its own; where it is unsupported the
   line simply breaks as it did before. */
.launch-highlights li { border-top: var(--hairline) solid var(--line); padding-block: var(--s-4); color: var(--text-primary); font-size: var(--fs-body); text-wrap: pretty; }
.launch-highlights li:last-child { border-bottom: var(--hairline) solid var(--line); }
.launch-more { margin-top: var(--sf-s); color: var(--text-tertiary); font-size: var(--fs-caption); }
.launch-facts { display: grid; grid-template-columns: 1fr; gap: var(--sf-m); border-top: var(--hairline) solid var(--line); padding-top: var(--sf-m); }
/* D-V20-6. The h2 element and the heading order stay; the display step comes down so the two facts stop
   competing with the section headings above them. The V18 lineup names took the same treatment. */
.launch-fact h2 { margin-bottom: var(--s-3); font-size: var(--fs-body-lg); font-weight: var(--wt-medium); }
.launch-priority { max-width: var(--w-text); margin: var(--s-3) 0 0; padding-left: var(--s-5); color: var(--text-secondary); }
.launch-priority li + li { margin-top: var(--s-2); }
.launch-state { color: var(--text-primary); font-weight: var(--wt-medium); }
/* D-V20-7. The closing sentence is a closer, not a legal note. The fine print below it keeps the
   .disclosures--centered treatment, which is what V19 centered and what its probe still reads. */
.launch-close { margin-inline: auto; text-align: center; }

/* V21. Reservation status.
   One measure for the whole page, --w-narrow, which is what V20 settled the Launch Edition onto for the
   same reason: a page that steps 800 / 640 / 800 on the way down reads as three pages. The forms inside
   a panel give up their own --w-form cap and fill it, so a bordered box never contains a narrower box. */
/* A definite width, not a max-width, for the reason layout.css records above .narrow: auto margins on
   a grid item turn off stretch, so a max-width box collapses to its own max-content. That is exactly
   what happened here on the first pass. The contact panel's section shrank to 347px while the two
   sections below it, whose max-content happens to exceed the cap, sat at the full 800, and the page
   read as three different documents stacked. .form-shell above is written the same way. */
.reservation-shell { width: min(100%, var(--w-narrow)); margin-inline: auto; }
.reservation-shell .lead-form { width: 100%; max-width: none; }

/* The disclosures. Native details/summary, styled to the specification groups: the same hairline box,
   the same caps-register summary, the same accent affordance on the right. Nothing here needs script,
   which is the whole reason the portal's two toggling panels became these. */
.reservation-panel { overflow: hidden; border: var(--hairline) solid var(--line); border-radius: var(--r-2); }
.reservation-panel summary { display: flex; min-height: var(--hit-target); align-items: center; justify-content: space-between; gap: var(--s-4); padding: var(--s-3) var(--s-4); background: var(--surface-1); color: var(--text-secondary); font-size: var(--fs-eyebrow); font-weight: var(--wt-medium); letter-spacing: var(--ls-caps-tight); line-height: var(--lh-caps); text-transform: uppercase; cursor: pointer; transition: background var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out); }
.reservation-panel summary::-webkit-details-marker { display: none; }
.reservation-panel summary::marker { content: ""; }
.reservation-panel summary::after { content: "Open  +"; flex: 0 0 auto; color: var(--accent); font-size: var(--fs-caption); letter-spacing: var(--ls-caps-tight); }
.reservation-panel summary:hover { background: var(--surface-2); color: var(--text-primary); }
.reservation-panel summary:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: calc(var(--focus-w) * -1); }
.reservation-panel[open] summary { color: var(--text-primary); }
.reservation-panel[open] summary::after { content: "Close  \2212"; }
.reservation-panel__body { display: flex; flex-direction: column; gap: var(--s-5); padding: var(--card-pad); }

/* One reservation. The head, the picker, the dates, the dealer, the tracker, and what is owed, in that
   order and separated by space rather than by more boxes: the panels are already boxes, and nesting
   them inside another one is how a status page starts looking like a form builder. */
.reservation { display: flex; flex-direction: column; gap: var(--sf-s); }
.reservation__title { font-size: var(--fs-display-s); }
/* One labelled fact, so it reads as a line rather than as a table. .notice-facts is a two-column grid
   built for a notice's six rows; with a single row it left the date parked at a tab stop halfway
   across the page with nothing above or below it. The dt keeps its caps treatment from that class. */
.reservation-facts { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-2) var(--s-3); margin-top: var(--s-2); }
.reservation-facts dd { margin: 0; }
/* The dealer block is the one form on the site with two actions, so it is the one place the submit row
   is a row. Everywhere else it holds a single button and a row direction changes nothing. */
.reservation-shell .form-submit-row { flex-direction: row; flex-wrap: wrap; gap: var(--s-3); }
.reservation-dealer { display: flex; flex-direction: column; gap: var(--s-4); }
.reservation-dealer p { max-width: var(--w-text); }
.reservation-dealer__state { color: var(--text-primary); }
/* The blocked state is the one place on this page where the colour has to carry meaning, and it does
   not carry it alone: the sentence says "can not be delivered" in words directly beside the rule. */
.reservation-dealer--blocked .reservation-dealer__state { border-left: var(--focus-w) solid var(--danger); padding-left: var(--s-4); }
.reservation-assign { display: flex; flex-direction: column; gap: var(--s-5); border: var(--hairline) solid var(--line); border-radius: var(--r-2); padding: var(--card-pad); }
.reservation-assign__lede { color: var(--text-secondary); font-size: var(--fs-body-sm); }

/* The paint picker. Native radios with a colour chip beside each name, so the colour is never the only
   thing identifying an option: the name is always printed. Chips are decorative and aria-hidden. */
.reservation-paint { gap: var(--s-5); }
.reservation-paint__tier { display: flex; flex-direction: column; gap: var(--s-3); }
.reservation-paint__tier-name { color: var(--text-tertiary); font-size: var(--fs-eyebrow); font-weight: var(--wt-medium); letter-spacing: var(--ls-caps-tight); line-height: var(--lh-caps); text-transform: uppercase; }
.reservation-paint__options { display: grid; grid-template-columns: 1fr; gap: var(--s-2); }
.reservation-paint__option { display: flex; min-height: var(--hit-target); align-items: center; gap: var(--s-3); }
.reservation-paint__option input { width: var(--icon-size); min-height: var(--icon-size); flex: 0 0 auto; }
.reservation-paint__chip { width: var(--s-5); height: var(--s-5); flex: 0 0 auto; border: var(--hairline) solid var(--line-strong); border-radius: var(--r-pill); background: var(--chip-color); }
.reservation-paint__name { font-size: var(--fs-body-sm); }

/* The four-step tracker. A vertical ladder on a phone with the rule running down the marks, and one row
   of four above 640px with the rule running across them. The rule is a pseudo-element on each step
   rather than a background on the list, so the completed run ends where the completed steps end. */
.reservation-track { display: grid; grid-template-columns: 1fr; gap: 0; margin: 0; padding: 0; list-style: none; }
.reservation-track__step { position: relative; display: grid; grid-template-columns: var(--s-5) minmax(0, 1fr); column-gap: var(--s-4); align-items: start; padding-bottom: var(--s-5); }
.reservation-track__step:last-child { padding-bottom: 0; }
.reservation-track__mark { grid-row: 1 / span 2; width: var(--s-5); height: var(--s-5); border: var(--hairline) solid var(--line-strong); border-radius: var(--r-pill); background: var(--surface-1); }
.reservation-track__step.is-complete .reservation-track__mark { border-color: var(--accent); background: var(--accent); }
.reservation-track__step:not(:last-child)::before { content: ""; position: absolute; top: var(--s-5); bottom: 0; left: calc(var(--s-5) / 2); border-left: var(--hairline) solid var(--line-strong); }
.reservation-track__step.is-complete:not(:last-child)::before { border-color: var(--accent); }
.reservation-track__title { color: var(--text-primary); font-size: var(--fs-body-sm); font-weight: var(--wt-medium); }
.reservation-track__detail { color: var(--text-tertiary); font-size: var(--fs-caption); }

/* The final payment card. Vanderhall's own wording, so the block's job is to be readable rather than
   persuasive: a plain bordered field, and the production notice inside it marked as the thing that
   needs an answer. */
.reservation-payment { display: flex; flex-direction: column; gap: var(--s-4); border: var(--hairline) solid var(--line); border-radius: var(--r-2); padding: var(--card-pad); }
.reservation-payment p { max-width: var(--w-text); color: var(--text-secondary); }
.reservation-payment__heading { font-size: var(--fs-body-lg); font-weight: var(--wt-medium); }
.reservation-payment__notice { display: flex; flex-direction: column; gap: var(--s-5); border-top: var(--hairline) solid var(--line); padding-top: var(--s-5); }
.reservation-payment__notice > p:first-child { color: var(--text-primary); }

/* The reservation breakpoint sits here rather than in the shared 640px block further up the file,
   and that placement is load-bearing rather than tidy: the base rules above are declared after that
   block, so at equal specificity they win, and the tracker stayed a ladder at every width. Media
   queries add no specificity. Anything that overrides a rule declared here has to come after it. */
@media (min-width: 640px) {
  /* The tracker turns from a ladder into a row of four, and the rule turns with it: the mark leads
     its own column and the connector runs from the mark's right edge to the next step. Both
     orientations are the same markup and the same pseudo-element, moved to the other side. */
  .reservation-track { grid-template-columns: repeat(4, minmax(0, 1fr)); column-gap: var(--s-4); }
  .reservation-track__step { grid-template-columns: minmax(0, 1fr); row-gap: var(--s-2); padding-bottom: 0; }
  .reservation-track__mark { grid-row: auto; }
  .reservation-track__step:not(:last-child)::before { top: calc(var(--s-5) / 2); bottom: auto; left: var(--s-5); right: calc(var(--s-4) * -1); border-left: 0; border-top: var(--hairline) solid var(--line-strong); }
  .reservation-track__step.is-complete:not(:last-child)::before { border-top-color: var(--accent); }
  /* Nine colour options read as one long column on a phone and as three tidy rows here. */
  .reservation-paint__options { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-2) var(--s-5); }
}

/* Contact. The progressive form is one document with visible step headings; the step navigation and the
   branch hiding are both site.js's, so without script this is a long, complete, usable form. */
.form-step + .form-step { border-top: var(--hairline) solid var(--line); padding-top: var(--sf-m); }
.form-step[hidden] { display: none; }
.form-step-status { color: var(--text-tertiary); font-size: var(--fs-caption); }
.form-branch { display: flex; flex-direction: column; gap: var(--form-gap); }
.form-branch[hidden] { display: none; }
.form-branch__title { font-size: var(--fs-title); }
.form-nav { display: flex; flex-wrap: wrap; gap: var(--s-3); }
.radio-column { display: flex; flex-direction: column; gap: var(--s-2); }

/* The dealer locator. List and map, and the list is the one that has to work. */
.locator { display: flex; flex-direction: column; gap: var(--sf-s); }
.locator__search { display: grid; grid-template-columns: 1fr; gap: var(--s-5); border: var(--hairline) solid var(--line); border-radius: var(--r-2); padding: var(--card-pad); }
.locator__search-actions { display: flex; flex-wrap: wrap; gap: var(--s-3); }
.locator__bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--s-3); }
.locator__count { color: var(--text-tertiary); font-size: var(--fs-eyebrow); font-weight: var(--wt-medium); letter-spacing: var(--ls-caps-tight); text-transform: uppercase; }
/* V16-E: the mode switch's rules are retired with the control. Both panes render at every width;
   below the two-pane breakpoint the map takes the first position, because a visitor arriving from
   Show on map is sent up to it. */
.locator__state { color: var(--text-secondary); font-size: var(--fs-body-sm); }
.locator__state[hidden], .locator__no-results[hidden] { display: none; }
.locator__panes { display: grid; grid-template-columns: 1fr; gap: var(--sf-s); }
.locator__panes .locator__map { order: -1; }
.locator__list { display: flex; flex-direction: column; gap: 0; }
.locator__no-results { display: flex; flex-direction: column; gap: var(--s-4); border: var(--hairline) solid var(--line); border-radius: var(--r-2); padding: var(--card-pad); }
.locator__map { position: relative; min-height: 20rem; overflow: hidden; border: var(--hairline) solid var(--line); border-radius: var(--r-2); background: var(--surface-1); }
.locator__canvas { position: absolute; inset: 0; }
.locator__map-fallback { position: absolute; inset: 0; display: grid; place-items: center; }
.locator__map-fallback[hidden] { display: none; }
/* V15-E. The illustrative map. Letterboxed (meet), never cropped: a projection that slices its edges
   off is a map that can lose Phoenix on a wide pane. The letterbox is invisible because the pane's
   own surface is the map's only background.
   V16-F: the drawing sits on a world base now. Land is a filled shade of the pane's own surface,
   country borders and the western-state overlay are hairlines whose px weight never follows the
   camera (non-scaling strokes), and city labels are closed until the camera gives them room. */
.locator__map-art { width: 100%; height: 100%; display: block; touch-action: pan-x pan-y; }
.locator__map-art:active { cursor: grabbing; }
@media (hover: hover) { .locator__map-art { cursor: grab; } }
.map-land { fill: color-mix(in srgb, var(--ink) 7%, transparent); stroke: var(--ink-24); stroke-width: 0.75; }
.map-borders { fill: none; stroke: var(--ink-24); stroke-width: 0.5; }
.map-lines { fill: none; stroke: var(--ink-24); stroke-width: 0.5; }
.map-pin__halo { fill: transparent; stroke: var(--ink-24); stroke-width: 1; transition: stroke var(--dur-2) var(--ease-out), fill var(--dur-2) var(--ease-out); }
.map-pin__dot { fill: var(--accent); }
.map-pin__label { display: none; fill: var(--text-secondary); font-size: 16px; font-weight: var(--wt-medium); }
.map-art--labels .map-pin__label, .map-pin.is-selected .map-pin__label { display: block; }
.map-pin.is-dimmed { opacity: 0.25; }
.map-pin.is-selected .map-pin__halo { stroke: var(--accent); fill: color-mix(in srgb, var(--accent) 14%, transparent); }
.map-pin.is-selected .map-pin__label { fill: var(--text-primary); }
.locator__map-controls { position: absolute; z-index: 2; top: var(--s-3); right: var(--s-3); display: flex; flex-direction: column; gap: var(--s-1); }
.locator__map-controls[hidden] { display: none; }
.locator__map-button {
  min-width: var(--hit-target);
  min-height: var(--hit-target);
  border: var(--hairline) solid var(--line-strong);
  border-radius: var(--r-1);
  background: var(--paper);
  color: var(--text-primary);
  font-size: var(--fs-body);
  font-weight: var(--wt-medium);
  cursor: pointer;
}
.locator__map-button--fit { font-size: var(--fs-eyebrow); letter-spacing: var(--ls-caps-tight); text-transform: uppercase; }
.locator__map-button:hover { border-color: var(--ink); }
.locator__map-button:disabled { opacity: 0.4; cursor: default; }
.locator__map-button:disabled:hover { border-color: var(--line-strong); }

.dealer-card { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-3); border-top: var(--hairline) solid var(--line); padding-block: var(--sf-s); }
.dealer-card:last-of-type { border-bottom: var(--hairline) solid var(--line); }
.dealer-card[hidden] { display: none; }
/* Selection is a left rule and a surface, never colour alone: a selected dealer has to be identifiable
   without seeing the accent, which is the same reason the notice severity is a word rather than a hue. */
.dealer-card.is-selected { border-left: 2px solid var(--accent); background: var(--surface-1); padding-left: var(--s-4); }
.dealer-card.is-selected .dealer-card__name::after { content: " (selected)"; color: var(--text-tertiary); font-size: var(--fs-caption); font-weight: var(--wt-regular); }
.dealer-card__head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--s-2) var(--s-4); width: 100%; }
.dealer-card__name { font-size: var(--fs-title); font-weight: var(--wt-medium); }
.dealer-card__distance { color: var(--text-tertiary); font-size: var(--fs-caption); font-variant-numeric: tabular-nums; white-space: nowrap; }
.dealer-card__address { font-size: var(--fs-body-sm); }
.dealer-card__tags { display: flex; flex-wrap: wrap; gap: var(--s-2); margin: 0; padding: 0; list-style: none; }
.dealer-card__tags li { border: var(--hairline) solid var(--line-strong); border-radius: var(--r-pill); padding: var(--s-1) var(--s-3); color: var(--text-tertiary); font-size: var(--fs-eyebrow); font-weight: var(--wt-medium); letter-spacing: var(--ls-caps-tight); text-transform: uppercase; }
.dealer-card__models, .dealer-card__hours { color: var(--text-tertiary); font-size: var(--fs-caption); }
.dealer-card__actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3) var(--s-5); }
.dealer-card__actions a, .dealer-card__select { min-height: var(--hit-target); display: inline-flex; align-items: center; color: var(--text-primary); font-size: var(--fs-body-sm); font-weight: var(--wt-medium); text-decoration: underline; text-decoration-color: var(--line-strong); }
.dealer-card__select { border: 0; padding: 0; background: transparent; cursor: pointer; }

@media (min-width: 640px) {
  .card-grid--posts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .past-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .notice-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 768px) {
  /* D-V20-4. Column flow, not row flow. Five explicit rows filled down column one and then down column
     two, so the eye reads down and down instead of zig-zagging across ten items whose six carbon-fiber
     entries otherwise scatter over both columns. Still ten items on exactly five row tops, which is what
     V19's balance probe asserts. */
  .launch-highlights { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-rows: repeat(5, auto); grid-auto-flow: column; column-gap: var(--sf-m); }
  /* The last row of each column, which is what nth-last-child can no longer identify once the flow runs
     column-wise. Items five and ten sit at the foot of their own column. */
  .launch-highlights li:nth-child(5n) { border-bottom: var(--hairline) solid var(--line); }
  .launch-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .locator__search { grid-template-columns: minmax(0, 2fr) minmax(0, 3fr) auto; align-items: end; }
}

@media (min-width: 1024px) {
  .gts-section--specifications { width: min(100%, var(--w-content)); }
  .gts-section--specifications .spec-table { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: var(--sf-m); }
  .card-grid--posts { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  /* The featured story runs media beside body rather than above it, which is what makes it read as
     featured without a second type scale. */
  /* V16-G, Owen on 2026-08-06: the feed's stories sit a step smaller. Both splits give the
     photograph an even half rather than the wider track, and the featured title comes down one
     display step, so the two stories read as a feed rather than as two heroes. */
  .post-card--featured { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--sf-m); align-items: center; }
  .post-card--featured .post-card__title { font-size: var(--fs-display-s); }
  /* V15. A lone supporting story takes the featured split's shape, mirrored and a step quieter,
     because one card in the three-column grid read as two-thirds of empty page. */
  .post-card--wide { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--sf-m); align-items: center; }
  .post-card--wide .post-card__media { order: 2; }
  .post-card--wide .post-card__title { font-size: var(--fs-display-s); }
  /* Roughly two fifths to three fifths, which is the proportion section 4.1 asks for, and the map is
     sticky so it stays with the list a visitor is scrolling. V15-E: the two-pane layout no longer
     depends on a Google key, because the no-key state carries the illustrative map now rather than a
     one-line note. */
  .locator .locator__panes { grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); gap: var(--sf-m); align-items: start; }
  .locator .locator__list { max-height: 42rem; overflow-y: auto; }
  /* order: 0 returns the map to the second column here; the map-first order is the stacked view's. */
  .locator .locator__map { order: 0; position: sticky; top: calc(var(--header-h) + var(--s-4)); min-height: 34rem; }
  .policy-layout { grid-template-columns: minmax(0, 1fr) minmax(0, 3fr); gap: var(--sf-l); align-items: start; }
  .policy-toc { position: sticky; top: calc(var(--header-h) + var(--s-5)); }
  .policy-toc__mobile { display: none; }
  .policy-toc__desktop { display: block; }
  .policy-toc__desktop .policy-toc__list { max-height: 60svh; overflow-y: auto; margin-top: var(--s-4); }
}

/* V16-E: below 1024px both panes stack, map first (the order rule above), and the retired mode
   switch decides nothing. The map keeps a working height without stealing the whole screen. */
@media (max-width: 1023px) {
  .locator__map { min-height: 24rem; }
}

@media (hover: hover) and (pointer: fine) {
  .post-card__media:hover img { transform: scale(1.03); }
}
