/* =========================================================================
   HINDERER MOTOR COMPANY — main stylesheet
   Loads after Bootstrap 5.3 and after tokens.css.
   Layer order: base → layout → typography → controls → components → utilities
   ========================================================================= */

/* =========================================================================
   1. BASE
   ========================================================================= */

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

/* No `overflow-x: clip` here. It was a safety net against horizontal overflow,
   but a non-visible overflow value on html/body makes them the clipping
   ancestor and silently kills `position: sticky` for everything inside —
   which is what stopped the pinned trade-in stage from pinning at all.
   The real guarantee is the max-width rule below plus the measured audit:
   document scrollWidth stays under the viewport at 320, 390 and 1500 on every
   page. If that ever regresses, fix the element that overflows. */
html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--text-body);
  line-height: var(--lh-body);
  letter-spacing: var(--track-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img, svg, video { max-width: 100%; height: auto; display: block; }

/* Anchor targets clear the persistent header (U10) */
[id] { scroll-margin-block-start: calc(var(--header-h) + var(--space-5)); }

/* Focus is always visible and never removed (color I3) */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
  border-radius: var(--radius-1);
}
:focus:not(:focus-visible) { outline: none; }

.skip-link {
  position: absolute;
  inset-block-start: var(--space-2);
  inset-inline-start: var(--space-2);
  z-index: 200;
  padding: var(--space-3) var(--space-5);
  background: var(--ink);
  color: var(--ink-inverse);
  font-size: var(--text-xs);
  font-weight: var(--weight-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  text-decoration: none;
  border-radius: var(--radius-2);
  transform: translateY(-200%);
}
.skip-link:focus { transform: none; }

.visually-hidden-focusable:not(:focus):not(:focus-within) { position: absolute !important; }


/* =========================================================================
   2. LAYOUT
   ========================================================================= */

/* .wrap, .section*, .stack now live in system.css — layout and rhythm are
   one layer, components are another. */

/* ACTION ROWS
   Six semantic wrappers, one interval. What sits above a set of actions is a
   property of the actions, not of the section that happens to hold them — so
   it is stated once here rather than re-decided at each call site, which is
   how the same gap ended up written as both 48px and 64px. */
.btn-row,
.hero__actions,
.build__cta,
.cta-centre,
.reviews__cta { margin-block-start: var(--gap-cta-text); }

/* The grid case. These four follow a strip, a rail or a card grid, whose lower
   edge is hard — border, shadow, a straight line of card feet — and against
   that edge 48px reads tighter than it measures. */
#gallery .cta-centre,
.scene__content .cta-centre,
.reviews__cta { margin-block-start: var(--gap-cta-grid); }

/* Inventory's rail already carries 64px of shadow room below its cards, so the
   full token on top of it put 128px on screen where 64 was specified. The
   interval belongs to the cards, not to the scroll container around them. */
#inventory .cta-centre {
  margin-block-start: calc(var(--gap-cta-grid) - var(--rail-shadow-room, var(--space-8)));
}

/* Where the parent is a grid, its row-gap already supplies part of the
   interval; adding the full token on top counted it twice (72 instead of 48).
   The margin here is the REMAINDER, written as the subtraction it is. */
.nationwide__body .btn-row {
  margin-block-start: calc(var(--gap-cta-text) - var(--space-5));
}

.btn-row,
.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
}

.build__cta,
.cta-centre,
.reviews__cta { display: flex; justify-content: center; }


/* =========================================================================
   3. TYPOGRAPHY
   ========================================================================= */

h1, h2, h3, h4 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--weight-heading);
  letter-spacing: var(--track-heading);
  line-height: var(--lh-heading);
  text-wrap: balance;
}

/* Display sizes drop back to 400: at 40–64px a 500 stem reads as weight the
   design never asked for. */
.hero__title,
.section-title,
.contact__title { font-weight: var(--weight-display); }

p { margin: 0; text-wrap: pretty; }
p + p { margin-block-start: var(--space-5); }

/* Down a step from the display tier. At 64px every one of the ten section
   titles carried the same weight as the page's own headline, so the hero had
   no rank of its own and the page read as ten first pages. 52px keeps them
   unmistakably display-scale while leaving the h1 (68px) alone at the top —
   a 24% step, which is a rank you can see rather than one you can measure.
   The value is the existing --text-hero-2 stop, not a new number: the second
   hero line already occupies this rank, and one scale step serving one rank is
   the point of having a scale. */
.section-title {
  font-size: var(--text-hero-2);
  line-height: var(--lh-display);
  letter-spacing: var(--track-display);
  color: var(--accent);              /* 3.67:1 — 30px floor, still large-text AA */
}

/* The sub-line is the second half of one heading thought, so it takes the
   display voice too. Left in the body face it came out WIDER than the title
   above it, which reads as two unrelated ranks rather than one. */
.section-sub {
  font-family: var(--font-display);
  font-size: var(--text-h2);
  font-weight: var(--weight-heading);
  letter-spacing: var(--track-heading);
  line-height: var(--lh-tight);
  color: var(--ink);
}

.section-head {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.section-head--centre { align-items: center; text-align: center; }

.lead {
  font-size: var(--text-lead);
  line-height: var(--lh-lead);
  color: var(--ink);
  max-inline-size: var(--measure);
}

.body-muted {
  color: var(--ink-muted);
  max-inline-size: var(--measure);
}

/* Label register: uppercase, tracked, and never more than a few words —
   extended uppercase measurably slows reading. */
.eyebrow {
  font-size: var(--text-xs);         /* the 14px floor — this carries information */
  font-weight: var(--weight-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  line-height: var(--lh-tight);
  color: var(--accent-ink);          /* 5.34:1 — below 24px, so the light step */
}

/* A sentence-length lead-in is not a label. Same role, sentence case, so it
   stays readable at length. */
.standfirst {
  font-size: var(--text-lead);
  line-height: var(--lh-lead);
  color: var(--accent-ink);
  max-inline-size: var(--measure-tight);
}

.num-tabular { font-variant-numeric: tabular-nums; }

/* Inline prose links stay underlined and are distinguishable by more than
   colour; nav links are not underlined — that is the opposite rule. */
.prose a {
  color: var(--accent-ink);
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-thickness: 1px;
}


/* =========================================================================
   4. CONTROLS
   ========================================================================= */

.btn-hmc {
  --btn-bg: transparent;
  --btn-ink: var(--ink);
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  min-block-size: 64px;
  padding: var(--space-4) var(--space-6);
  border: 0;
  border-radius: var(--radius-2);
  background: var(--btn-bg);
  color: var(--btn-ink);
  font-family: var(--font-display);
  font-size: var(--text-btn);
  line-height: var(--lh-tight);
  /* Caps on the control tier, as the Figma style now sets. Safe here because
     every label is two to four words — uppercase removes the ascender and
     descender variation the eye reads word shapes by, so it stops at labels
     and never reaches a lead, a body or a caption. */
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  text-align: start;
  text-decoration: none;
  cursor: pointer;
  transition:
    background-color var(--dur-1) var(--ease-out),
    border-color     var(--dur-1) var(--ease-out),
    transform        var(--dur-1) var(--ease-out);
}

.btn-hmc:hover  { transform: translateY(-2px); }
.btn-hmc:active { transform: none; }

.btn-hmc__icon { flex: 0 0 auto; inline-size: 30px; }

/* One primary action per view. Copper is the primary; slate is the parallel
   secondary; ghost is the tertiary. */
.btn-hmc--primary   { --btn-bg: var(--accent); }
.btn-hmc--primary:hover  { --btn-bg: var(--accent-hover); }

.btn-hmc--secondary { --btn-bg: var(--slate); }
.btn-hmc--secondary:hover { --btn-bg: var(--slate-lift); }

.btn-hmc--ghost {
  --btn-bg: transparent;
  border: 1px solid var(--rule-strong);
}
.btn-hmc--ghost:hover { border-color: var(--ink); }

.btn-hmc--centred { justify-content: center; text-align: center; }
.btn-hmc--wide    { inline-size: min(300px, 100%); }

/* The hero pair is the one place the control is the page's primary mass rather
   than a control inside a section, so it takes a taller box. It is a modifier
   and not a rule inside .hero__actions, because a section that reaches into a
   component is how one exception becomes four. Returns to the base height in
   one column, where the extra height buys nothing and costs a screen. */
.btn-hmc--tall { min-block-size: 84px; flex: 1 1 260px; max-inline-size: 300px; }

@media (max-width: 47.99rem) {
  .btn-hmc--tall { min-block-size: 64px; max-inline-size: none; }
}

/* A pair of parallel actions stays a pair: they share the row and only wrap
   when the column genuinely cannot hold both. Stacking them earlier would
   rank one above the other, which is not what the choice is. */
.btn-row .btn-hmc--wide { flex: 1 1 13.75rem; inline-size: auto; max-inline-size: 300px; }

/* Icon-only round control — carousel arrows, media play */
.btn-round {
  display: inline-grid;
  place-items: center;
  inline-size: 56px;
  block-size: 56px;
  padding: 0;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-pill);
  background: rgb(var(--bg-rgb) / 0.55);
  color: var(--ink);
  cursor: pointer;
  transition:
    background-color var(--dur-1) var(--ease-out),
    border-color     var(--dur-1) var(--ease-out);
}
.btn-round:hover { background: rgb(var(--bg-rgb) / 0.85); border-color: var(--ink); }
.btn-round svg   { inline-size: 22px; block-size: 22px; }


/* =========================================================================
   5. SITE HEADER — a persistent mass, counted in the composition
   ========================================================================= */

.site-header {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 100;
  background: linear-gradient(to bottom, rgb(var(--bg-rgb) / 0.82), rgb(var(--bg-rgb) / 0));
  transition: background-color var(--dur-2) var(--ease-out);
}

/* After the first screen the header stops being an overlay on photography and
   becomes a solid band, so nav contrast no longer depends on the image.

   It also gets out of the way: once the visitor is reading, the header is a
   means of navigation rather than part of the composition, so it gives back
   roughly a third of its height. The band is what persists — its size is not. */
.site-header.is-scrolled {
  background: var(--bg);
  border-block-end: 1px solid var(--rule);
}

/* The band does lose height — enough to register as a change of state — but the
   nav does not slide sideways while it happens: the brand keeps its full-size
   column width and only the wordmark inside it shrinks. Horizontal drift is
   what makes a menu need re-finding; a little vertical give does not. */
.site-header.is-scrolled { --header-h: 96px; }
/* Once the page has moved, the header is over content rather than over the top
   of it, and a bar floating on a photograph with no edge reads as part of the
   photograph. Soft and wide: the shadow states elevation, it does not draw a
   line — a hard edge here would compete with the hairline the header already
   carries. */
.site-header.is-scrolled {
  box-shadow: 0 10px 30px -18px rgb(var(--shadow-rgb) / 0.85);
}
.site-header.is-scrolled .site-header__inner { padding-block: var(--space-2); }
.site-header.is-scrolled .brand__mark { inline-size: clamp(150px, 12vw, 200px); }
.site-header.is-scrolled .collection-pill { padding-block: var(--space-2); min-block-size: 40px; }

/* The size change is a transition, not a jump — a header that snaps between two
   heights reads as a bug on the frame where it lands. */
.site-header__inner,
.brand__mark,
.collection-pill {
  transition: min-block-size var(--dur-2) var(--ease-out),
              inline-size var(--dur-2) var(--ease-out),
              padding-block var(--dur-2) var(--ease-out);
}

@media (prefers-reduced-motion: reduce) {
  .site-header__inner,
  .brand__mark,
  .collection-pill { transition: none; }
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  min-block-size: var(--header-h);
  padding-block: var(--space-3);
}

.brand {
  flex: 0 0 clamp(180px, 18vw, 272px);   /* the slot, held at full-size width */
  display: block;
  line-height: 0;   /* kills the inline descender gap under the mark — a layout
                       reset, not a type value, so it takes no --lh token */
}
.brand__mark {
  inline-size: clamp(180px, 18vw, 272px);
  aspect-ratio: 272 / 94;
  object-fit: cover;
  object-position: 50% 45.5%;        /* the Figma mask crop of the square lockup */
}

/* The header is three masses: logo, nav, counter. space-between on the inner
   wrap distributes them, so nav no longer needs to shove itself right. */
.site-nav { flex: 0 1 auto; }

.site-nav__list {
  display: flex;
  align-items: center;
  /* The Figma sets 40 here, which is on no scale the rest of the page uses, so
     the 48 step is the desktop value. It is not affordable at every width this
     row exists at, though: the burger only takes over below 48rem, and between
     there and ~69rem a fixed 48 broke TRADE IN and WHO WE ARE onto second
     lines. The interval closes to the 24 step instead of the row wrapping —
     the same clamp-between-two-tokens construction as --gutter, not a third
     scale. */
  gap: clamp(var(--space-5), 2.6vw, var(--space-7));
  flex-wrap: nowrap;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Nav is set in the display voice, uppercase — Oswald is drawn for caps, and
   at nav length every item is one or two words, which is where uppercase is
   safe. The rule sits UNDER the active item, not above it. */
.site-nav__link {
  position: relative;
  display: block;
  white-space: nowrap;
  padding-block: var(--space-2);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
  letter-spacing: var(--track-body);
  text-transform: uppercase;
  text-decoration: none;
  transition: color var(--dur-1) var(--ease-out);
}
.site-nav__link::after {
  content: "";
  position: absolute;
  inset-block-end: 0;
  inset-inline: 0;
  block-size: 3px;
  background: var(--accent);
  opacity: 0;
  transition: opacity var(--dur-1) var(--ease-out);
}
.site-nav__link:hover,
.site-nav__link:focus-visible { color: var(--accent-ink); }
.site-nav__link:hover::after,
.site-nav__link:focus-visible::after { opacity: 1; }

/* The current page is marked twice — copper ink AND the rule — because meaning
   carried by hue alone is invisible to a large share of visitors. */
.site-nav__link[aria-current="page"] { color: var(--accent-ink); }
.site-nav__link[aria-current="page"]::after { opacity: 1; }


/* ---- nav disclosure -----------------------------------------------------
   "Who We Are" holds three destinations, so it is a button rather than a link:
   a control that both navigates and discloses is two jobs on one target, and
   on touch the first tap has to guess which one was meant. The place it used
   to go — About — is the first item inside, so nothing was taken away.

   THE MENU IS REACHABLE WITH NO SCRIPT. Three independent openers: hover,
   focus-within, and the button's own aria-expanded. The first two are pure CSS,
   so a keyboard reaches the items by tabbing and a mouse by pointing even
   before main.js binds — and with JS off entirely the whole submenu is simply
   left open, the same escape the burger uses.
   ------------------------------------------------------------------------- */
.site-nav__item--menu { position: relative; }

.site-nav__disclosure {
  border: 0;
  background: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  /* The UA gives a button horizontal padding that .site-nav__link's
     padding-block never touched, so this one item sat 4px right of every other
     in the column. */
  padding-inline: 0;
}

/* Drawn, not an icon file: one triangle, the same clip-path language the
   accordion's caret uses, so the two rotate the same way for the same reason. */
.site-nav__caret {
  inline-size: 8px;
  block-size: 8px;
  flex: 0 0 auto;
  background: currentColor;
  clip-path: polygon(50% 72%, 6% 26%, 18% 14%, 50% 46%, 82% 14%, 94% 26%);
  transition: rotate var(--dur-2) var(--ease-out);
}
.site-nav__disclosure[aria-expanded="true"] .site-nav__caret { rotate: 180deg; }

.site-nav__submenu {
  display: none;
  position: absolute;
  inset-block-start: 100%;
  inset-inline-start: calc(var(--space-3) * -1);
  z-index: 30;
  min-inline-size: 11rem;
  padding: var(--space-3);
  border: 1px solid var(--rule);
  border-radius: var(--radius-3);
  /* Deep rather than raised: this panel floats over the page, and the ground
     under a header that may be sitting on a photograph cannot be translucent. */
  background: var(--bg-deep);
  box-shadow: var(--shadow-pop);
}
html:not(.js) .site-nav__submenu { position: static; display: block; box-shadow: none; }
.site-nav__item--menu:hover > .site-nav__submenu,
.site-nav__item--menu:focus-within > .site-nav__submenu,
.site-nav__disclosure[aria-expanded="true"] + .site-nav__submenu { display: block; }

.site-nav__sublink {
  display: block;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-1);
  color: var(--ink);                   /* 16.51:1 on --bg-deep */
  font-family: var(--font-body);
  font-size: var(--text-sm);
  text-decoration: none;
  white-space: nowrap;
  transition: background-color var(--dur-1) var(--ease-out),
              color var(--dur-1) var(--ease-out);
}
.site-nav__sublink:hover,
.site-nav__sublink:focus-visible {
  background: rgb(var(--ink-rgb) / 0.08);
  color: var(--accent-ink);
}
.site-nav__sublink[aria-current="page"] { color: var(--accent-ink); }

@media (prefers-reduced-motion: reduce) {
  .site-nav__caret { transition: none; }
}

/* Finance sits between two held scenes, and a section with no ground of its
   own lets a stuck photograph show through from above and below — the seam
   the eye reads as a defect. Every overlap section paints; so does this one. */
#finance { position: relative; z-index: 1; background: var(--bg); }

/* The phone number is the one gradient on the page. The Figma ramp ended at
   #2B6459, which is 2.46:1 on the ground — legal nowhere. The end stop is
   lifted to --tel so both ends clear AA.

   The gradient sits on the NUMBER, not on the link. background-clip: text needs
   `color: transparent`, and a transparent colour is what every child inherits —
   so an icon drawn in currentColor beside it would be invisible. Moving the
   clip one level in gives the link a real colour for the icon to take. */
.site-nav__link--phone {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--tel);
}
.site-nav__phone-icon { flex: 0 0 auto; }
.site-nav__phone-num {
  background-image: linear-gradient(90deg, var(--tel-lift) 0%, var(--tel) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* A call is not a page, so hover gives it a pill rather than the rule that
   marks navigation targets. The inline padding is permanent and cancelled by
   an equal negative margin, so the pill appears without shifting the row.

   The pill is --tel-deep, not --tel: white on --tel measures 3.54:1 and fails,
   and the label here is the page's own ink rather than a second inverted rule
   for one control. On --tel-deep it is 4.86:1. */
.site-nav__link--phone {
  padding-inline: var(--space-3);
  margin-inline: calc(var(--space-3) * -1);
  border-radius: var(--radius-pill);
  transition: background-color var(--dur-1) var(--ease-out),
              color var(--dur-1) var(--ease-out);
}
.site-nav__link--phone::after { content: none; }

.site-nav__link--phone:hover,
.site-nav__link--phone:focus-visible {
  background-color: var(--tel-deep);
  color: var(--ink);
}
/* The number drops its gradient and takes the link's ink, so the icon and the
   digits are one colour on hover rather than two. */
.site-nav__link--phone:hover .site-nav__phone-num,
.site-nav__link--phone:focus-visible .site-nav__phone-num {
  background-image: none;
  -webkit-background-clip: border-box;
  background-clip: border-box;
  color: inherit;
}


/* background-clip:text leaves no colour for a forced-colours renderer, so the
   fallback is restored where the gradient cannot paint. */
@media (forced-colors: active) {
  .site-nav__link--phone { background-image: none; color: LinkText; }
}

/* Saved-vehicle counter — a real means of the task, so it survives every
   breakpoint rather than being tucked into the burger. */
.collection-pill {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-block-size: 44px;
  padding: var(--space-3) var(--space-5);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  transition: border-color var(--dur-1) var(--ease-out),
              background-color var(--dur-1) var(--ease-out);
}
.collection-pill:hover {
  border-color: var(--ink);
  background: rgb(var(--ink-rgb) / 0.04);
}
.collection-pill .num-tabular { color: var(--accent-ink); }

.nav-toggle {
  display: none;
  margin-inline-start: auto;
  padding: var(--space-2);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-2);
  background: transparent;
  color: var(--ink);
  cursor: pointer;
}

@media (max-width: 61.99rem) {
  /* Sized and pinned, not just shown. As a shrinkable flex item beside the
     logo and the counter it was being squeezed to an 18px box with its own
     icon crushed to nothing — a bordered empty square where the menu should
     be, and an 18px target where 44 is the floor. */
  .nav-toggle {
    display: inline-grid;
    place-items: center;
    flex: 0 0 auto;
    inline-size: 44px;
    block-size: 44px;
    padding: 0;
  }
  .nav-toggle svg { inline-size: 24px; block-size: 24px; flex: 0 0 auto; }
  .site-nav {
    position: absolute;
    inset-block-start: 100%;
    inset-inline: 0;
    margin: 0;
    padding: var(--space-5) var(--gutter) var(--space-7);
    background: var(--bg);
    border-block-end: 1px solid var(--rule);
    max-block-size: calc(100dvh - var(--header-h));
    overflow-x: hidden;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
  }
  /* Collapsed only where a toggle exists to reopen it. Without JS the class
     is never added, the nav renders as a stacked list, and every destination
     stays reachable. */
  html.js .site-nav { display: none; }
  html.js .nav-toggle[aria-expanded="true"] ~ .site-nav { display: block; }
  .site-nav__list { flex-direction: column; align-items: stretch; gap: var(--space-4); }
  /* One column gives the nav room, so the display voice steps up a size; the
     rule stays under the item, where it now lives on every viewport. */
  .site-nav__link { font-size: var(--text-h3); }
  .site-nav__link::after { inset-inline: 0 auto; inline-size: 3rem; }

  /* In the burger the submenu is a nested list, not a floating panel: there is
     nothing to float over, and a panel positioned against a one-column stack
     would cover the items under it. Indented and ruled on its leading edge so
     it reads as belonging to the item above rather than as three more peers.

     With JS, submenus stay collapsed until the disclosure is tapped — the WP
     menu has enough children that opening every nest by default overflows the
     phone and leaves no room to scroll. Without JS they stay open so every
     destination remains reachable. */
  .site-nav__submenu {
    position: static;
    display: block;
    min-inline-size: 0;
    margin-block-start: var(--space-3);
    margin-inline-start: var(--space-3);
    padding: 0 0 0 var(--space-4);
    border: 0;
    border-inline-start: 1px solid var(--rule);
    border-radius: 0;
    background: none;
    box-shadow: none;
  }
  html.js .site-nav__submenu { display: none; }
  html.js .site-nav__item--menu:hover > .site-nav__submenu,
  html.js .site-nav__item--menu:focus-within > .site-nav__submenu {
    display: none;
  }
  html.js .site-nav__disclosure[aria-expanded="true"] + .site-nav__submenu {
    display: block;
  }
  .site-nav__sublink { padding-inline: 0; font-size: var(--text-body); }
  /* Caret always visible on load; flips when aria-expanded toggles. */
  .site-nav__disclosure {
    inline-size: 100%;
    justify-content: space-between;
    cursor: pointer;
  }
  .site-nav__caret {
    display: block;
    inline-size: 12px;
    block-size: 12px;
    margin-inline-start: var(--space-3);
    opacity: 1;
    /* Closed = points down (open me); expanded = flip to point up. */
    rotate: 0deg;
  }
  .site-nav__disclosure[aria-expanded="true"] .site-nav__caret {
    rotate: 180deg;
  }

  /* Three masses in one bar: at 375px the logo, the toggle and the counter
     together overrun the viewport, so all three step down rather than the
     counter being dropped into the burger — it is a real means of the task
     and has to survive the breakpoint. */
  .site-header__inner { gap: var(--space-3); }
  /* The fixed slot exists so the desktop header can shrink the mark without
     the nav shifting. There is no shrink on one column, and holding 180px
     there pushed the counter 44px off the right edge — where it was clipped,
     not scrolled, so it simply was not there. The slot follows the mark. */
  .brand { flex: 0 1 auto; min-inline-size: 0; }
  .brand__mark { inline-size: clamp(128px, 34vw, 180px); max-inline-size: 100%; }
  /* Three masses, and below this width one of them has to give. The counter
     keeps its number and loses its words rather than the identity being
     reduced to an illegible 51px — and it is hidden the accessible way, so a
     screen reader still reads the whole label. */
  @media (max-width: 23.5rem) {
    .collection-pill__label {
      position: absolute;
      inline-size: 1px;
      block-size: 1px;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
    }
    .collection-pill { padding-inline: var(--space-4); }
  }
  .collection-pill {
    padding-inline: var(--space-3);
    font-size: var(--text-xs);
    gap: var(--space-1);
  }
}


/* =========================================================================
   6. HERO
   ========================================================================= */

/* Mass sits slightly above the geometric centre, with the deeper field of
   space below it — the same relationship the Figma frame holds (headline at
   30% of an 796px band) and the reason the hero hands off downward. */
.hero {
  position: relative;
  isolation: isolate;
  display: grid;                 /* grid, not flex: the container must stretch
                                    to full width or its own gutter is lost */
  align-items: center;
  min-block-size: min(760px, 90svh);
  padding-block: calc(var(--header-h) + var(--space-6)) var(--space-9);
  overflow: hidden;
}

.hero__media {
  position: absolute;
  inset: 0;
  z-index: -3;
}
.hero__media img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  object-position: 72% 50%;      /* keeps the car in frame as the format narrows */
}

/* Legibility is fixed at the background layer, never on the type.
   Left-to-right scrim: dark under the headline, clear over the vehicle. */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -2;
  background:
    linear-gradient(to right,
      rgb(var(--bg-deep-rgb) / 0.88) 0%,
      rgb(var(--bg-deep-rgb) / 0.62) 34%,
      rgb(var(--bg-deep-rgb) / 0.10) 62%,
      rgb(var(--bg-deep-rgb) / 0) 78%);
}

/* The warm radial and the hand-off into the page ground, both from the Figma */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(58% 46% at 62% 34%,
      rgb(var(--glow-in-rgb) / 0.20) 0%,
      rgb(var(--glow-mid-rgb) / 0.10) 55%,
      rgb(var(--glow-rgb) / 0) 100%),
    linear-gradient(to bottom,
      rgb(var(--bg-rgb) / 0) 68%,
      var(--bg) 100%);
}

.hero__content { position: relative; inline-size: 100%; }
.hero__text     { max-inline-size: 46rem; }

.hero__title {
  font-size: var(--text-hero);
  line-height: var(--lh-display);
  letter-spacing: var(--track-display);
  color: var(--ink);
}
.hero__title span {
  display: block;
  font-size: var(--text-hero-2);
  line-height: var(--lh-display);
}

.hero__tagline {
  margin-block-start: var(--space-5);
  font-size: var(--text-lead);
  color: var(--ink);
}


@media (max-width: 47.99rem) {
  /* The portrait file IS the composition — car in the upper third, ground
     falling to near-black beneath it — so the phone hero stops being a short
     band with type laid across the vehicle and becomes the full frame it was
     cut for. Content moves to the end of the grid: the car reads clean above
     the headline instead of behind the two CTAs. */
  .hero {
    min-block-size: 100svh;
    align-items: end;
    padding-block: calc(var(--header-h) + var(--space-6)) var(--space-8);
  }
  .hero__media img { object-position: 50% 0%; }
  /* The file carries its own falloff, so the page scrim only lifts the top for
     the header and deepens the foot under the type. Laying the desktop's
     three-stop gradient over it doubled the darkness through the middle and
     flattened the car out of its own frame. */
  .hero::before {
    background: linear-gradient(to bottom,
      rgb(var(--bg-deep-rgb) / 0.55) 0%,
      rgb(var(--bg-deep-rgb) / 0.10) 32%,
      rgb(var(--bg-deep-rgb) / 0.55) 62%,
      rgb(var(--bg-deep-rgb) / 0.80) 100%);
  }
}


/* =========================================================================
   7. INVENTORY RAIL
   A scroll-snapped rail rather than a chromed carousel: the arrows sit
   outside the photograph, and the rail is reachable by keyboard, wheel,
   trackpad and touch without any control at all.
   ========================================================================= */

.rail-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  /* Wraps, because at 320px the two arrows overhung the right edge by 3px and
     put a horizontal scrollbar under the whole page. They drop to their own
     line instead. */
  flex-wrap: wrap;
  gap: var(--space-5);
  padding-block-end: var(--space-6);
}

.rail-head__text { max-inline-size: 46rem; }
.rail-head .eyebrow { margin-block-end: var(--space-text); }

/* The sub-line under the section title. It was briefly set in the serif, which
   gave the page a second serif role and made the inventory heading behave
   unlike every other heading on the site; the serif now belongs to the price
   alone. Only the interval is declared here — the voice is .section-sub. */
.rail-head__text .section-sub { margin-block-start: var(--space-text); }

.rail-head__note {
  margin-block-start: var(--space-4);
  max-inline-size: 60ch;
  font-size: var(--text-body);
  color: var(--ink-muted);                      /* 6.99:1 */
}

.rail-nav { display: flex; gap: var(--space-3); flex: 0 0 auto; }
.rail-nav[hidden] { display: none; }

/* A disabled arrow has to LOOK disabled. Both were being switched off
   correctly and still rendering at full strength, so the control kept
   promising a move it would not make. */
.btn-round:disabled {
  opacity: 0.35;
  cursor: default;
}

/* FILTERS
   Pills, because each one is a state the row can be in and only one holds at a
   time — the same shape as a radio set, which is why the active one is filled
   rather than merely brighter. */
.filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  padding-block-end: var(--space-6);
}

.filter {
  min-block-size: 44px;
  padding: var(--space-2) var(--space-6);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--text-xs);
  font-weight: var(--weight-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease-out),
              border-color var(--dur-1) var(--ease-out),
              color var(--dur-1) var(--ease-out);
}
.filter:hover { border-color: var(--ink); }

/* Copper ground under dark ink: 5.34:1, AA at this size. White on copper would
   have been 3.67 and would not. */
.filter.is-active {
  background: var(--accent-ink);
  border-color: var(--accent-ink);
  color: var(--ink-inverse);
}

/* Cards leaving and arriving under a filter, so the row does not simply
   blink into a different set. */
.vehicle[hidden] { display: none; }

/* Declared on the section, not on the rail, because BOTH the rail and the CTA
   below it need the number: the rail spends it as padding, the CTA subtracts
   it. On .rail alone the button could not see it — it is not a descendant —
   and would have silently fallen back to a duplicate literal. */
#inventory { --rail-shadow-room: var(--space-8); }

/* The rail's own inline padding reproduces the content edge of .wrap, so the
   first card aligns with the section heading while the last one bleeds off
   the right — the row reads as continuing rather than as ending.

   The block padding is room for what grows out of the cards: overflow-x also
   clips vertically, the status disc overhangs the top, and the hover shadow
   falls past the bottom. Unlike the reviews rail, `overflow-x: clip` is not an
   option here — this one has to actually scroll. */
.rail {
  --rail-edge: max(var(--gutter), calc((100vw - var(--container)) / 2));
  display: grid;
  grid-auto-flow: column;
  /* Fixed, not minmax: allowed to shrink, four cards squeezed themselves to fit
     the viewport exactly — the row stopped overflowing, the last card stopped
     bleeding off the right, and the arrows hid themselves because there was
     nothing left to scroll. The rail is supposed to run past the edge. */
  grid-auto-columns: clamp(280px, 22vw, 381px);
  gap: var(--space-6);
  margin: 0;
  /* overflow-x also clips vertically, so the rail has to carry room for what
     grows out of the cards: the status disc overhangs the top, and the hover
     shadow falls ~50px past the bottom. Without this the shadow gets a hard
     straight edge where the rail ends — which reads as a rendering fault. */
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--rail-edge);
  padding-block: var(--space-7) var(--rail-shadow-room);
  padding-inline: var(--rail-edge);
  /* The bar is hidden, not the ability: wheel, trackpad, touch and keyboard all
     still scroll this, and the arrows above are the visible affordance — which
     is why hiding it costs nothing here. They are shown whenever the row
     overflows and removed when it does not, so the means of scrolling is never
     unannounced. */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.rail::-webkit-scrollbar { display: none; }
.rail > * { scroll-snap-align: start; }

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

   Third pass, and the diagnosis moved from craft to hierarchy.

   The card was bone; the inventory photograph has a white studio wall behind
   it. Light on light — no figure-ground inside the object, and worse, the
   CARD became the brightest thing while the car became a small item sitting
   inside it. The dominant has to be the subject, never the device presenting
   it, so the card goes dark and the photograph becomes the only lit mass in
   the row. Five cards then read as five cars, not as five paper rectangles.

   Two rules in the whole card. The record carries no per-row lines at all —
   those were splitting in the middle wherever the grid had a column gap,
   which is the artefact that made the block look broken.
   -------------------------------------------------------------------------- */

.vehicle {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--bg-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius-2);
  /* Not clipped: the status disc overhangs the top edge on purpose. The corners
     are rounded on the photograph and the last row instead. */
  overflow: visible;
  transition:
    transform var(--dur-3) var(--ease-out),
    box-shadow var(--dur-3) var(--ease-out),
    border-color var(--dur-2) var(--ease-out),
    background-color var(--dur-2) var(--ease-out);
}

.vehicle__media {
  position: relative;
  aspect-ratio: 381 / 278;
  overflow: hidden;
  background: var(--bg);
}
/* The photograph is lit and the card body is not, so without this the image
   stops on a hard horizontal line halfway down the card. The gradient carries
   the photograph INTO the body instead of ending it — and it darkens the strip
   the title sits closest to, which is where a bright number plate or a pale
   wall used to compete with the type below. */
.vehicle__media::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  block-size: 55%;
  pointer-events: none;
  background: linear-gradient(to bottom,
    rgb(var(--bg-rgb) / 0) 0%,
    rgb(var(--bg-rgb) / 0.45) 55%,
    rgb(var(--bg-rgb) / 0.92) 100%);
}
.vehicle__media img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  transition: transform var(--dur-3) var(--ease-out);
}

/* SAVE — on the photograph, opposite the status disc.
   Saving is an action on the vehicle itself, so it belongs on the vehicle, not
   in the row of things you can do about it. Top-right also keeps it clear of
   the badge top-left, and out of the way of the title underneath.

   z-index above the whole-card link: the card is one big target to the detail
   page, and this control must not be swallowed by it. 44px box around a 20px
   glyph — the tap target is the button, not the icon. */
.vehicle__save {
  position: absolute;
  z-index: 3;
  inset-block-start: var(--space-3);
  inset-inline-end: var(--space-3);
  display: grid;
  place-items: center;
  inline-size: 44px;
  block-size: 44px;
  padding: 0;
  border: 1px solid rgb(var(--ink-rgb) / 0.22);
  border-radius: var(--radius-pill);
  background: rgb(var(--bg-rgb) / 0.55);
  backdrop-filter: blur(6px);
  color: var(--ink);
  cursor: pointer;
  transition: background-color var(--dur-2) var(--ease-out),
              border-color var(--dur-2) var(--ease-out),
              color var(--dur-2) var(--ease-out);
}
.vehicle__save:hover,
.vehicle__save:focus-visible {
  background: rgb(var(--bg-rgb) / 0.82);
  border-color: var(--ink);
}

.vehicle__save-icon {
  inline-size: 20px;
  block-size: 20px;
  background: currentColor;
  -webkit-mask: url(../icons/bookmark.svg) center / contain no-repeat;
  mask: url(../icons/bookmark.svg) center / contain no-repeat;
}

/* Saved reads as copper filled, not as a different icon: the state changes, the
   thing does not. aria-pressed carries the same fact for anyone not seeing it. */
.vehicle__save[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--ink);
}

/* Status names the state in words as well as in colour, and sits on the
   photograph rather than floating half off the card's top edge. It is a
   sibling of the image, so the hover move below never drags it along. */
/* A disc that breaks the top edge of the photograph rather than a tab tucked
   into a corner. Sitting half outside the frame is what makes it read as
   applied TO the car — a stamp on the print — instead of as another band of
   card furniture. The circle is a shape, not information: the word inside it
   carries the meaning, so nothing is lost where shapes are overridden. */
.vehicle__status {
  position: absolute;
  z-index: 2;
  inset-block-start: calc(var(--status-size) / -2.6);
  inset-inline-start: var(--space-5);
  display: grid;
  place-items: center;
  /* A percentage resolves against WIDTH on the inline axis and against HEIGHT
     on the block axis, so the same token produced 94×84 — a disc by intention
     and an oval on screen. One axis sized, the other derived. */
  inline-size: var(--status-size);
  block-size: auto;
  aspect-ratio: 1;
  margin: 0;
  padding: 0;
  border-radius: var(--radius-pill);
  background: var(--bg-control);          /* white label 9.89:1 */
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  line-height: var(--lh-none);
  letter-spacing: var(--track-caps);
  text-align: center;
  text-transform: uppercase;
}

.vehicle {
  --status-size: clamp(84px, 26%, 112px);
}

/* Sold takes the ground itself — the darkest disc on the page, and the only one
   that is not a colour of the brand. It reads as closed rather than as another
   offer, which is what the state means. White on it is 16.83:1. */
.vehicle--sold .vehicle__status { background: var(--bg);     }
.vehicle--new  .vehicle__status { background: var(--slate);  }  /* 7.27:1 */
.vehicle--just .vehicle__status { background: var(--accent); }  /* 4.58:1 */

/* The media has to stop clipping, or the disc loses the half that overhangs. */
.vehicle__media { overflow: visible; }
.vehicle__media img { border-radius: var(--radius-2) var(--radius-2) 0 0; }

/* A sold car stays browsable but is no longer the offer. */
.vehicle--sold .vehicle__media img { filter: saturate(0.3) brightness(0.8); }

/* --- identity ------------------------------------------------------------ */

.vehicle__body {
  /* Named so the action row can cancel it exactly. */
  --card-pad: var(--space-4);
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  padding: var(--space-5) var(--card-pad) 0;
}

.vehicle__title {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  min-block-size: 2.6em;
  font-family: var(--font-body);
  font-size: var(--text-lead);
  font-weight: var(--weight-body);
  letter-spacing: var(--track-body);
  line-height: var(--lh-tight);
  color: var(--ink-muted);                     /* 6.99:1 */
  transition: color var(--dur-2) var(--ease-out);
}
.vehicle__title a { color: inherit; text-decoration: none; }
/* Whole card is the target, and it stays behind the real controls */
.vehicle__title a::after { content: ""; position: absolute; inset: 0; z-index: 0; }

/* THE SERIF, ROLE ONE — the price.
   The one number a buyer came for, and the only place on the card where the
   face changes. Sans everywhere else means the change of voice IS the emphasis;
   no size jump or colour is doing that work. Old-style figures would fight a
   column of prices, so the serif is held to lining tabular. */
.vehicle__price {
  /* Same hairline as the action row, and drawn the same way: pulled out through
     the body's padding so it crosses the whole card, then the text is padded
     back in. A rule that stops short of the edge reads as a mistake; this one
     separates what the car IS from what it COSTS. */
  margin-block-start: var(--space-4);
  margin-inline: calc(var(--card-pad) * -1);
  padding-block-start: var(--space-4);
  padding-inline: var(--card-pad);
  border-block-start: 1px solid var(--rule);
  font-family: var(--font-serif);
  font-size: var(--text-price);
  font-weight: var(--weight-body);
  line-height: var(--lh-tight);
  color: var(--ink);                           /* 16.34:1 */
  font-variant-numeric: lining-nums tabular-nums;
}

/* --- the record ---------------------------------------------------------- */
/* One line, not a table. Mileage and stock are two facts a buyer scans past on
   the way to the price, and a two-column grid gave them the weight of a spec
   sheet they are not. */

.vehicle__meta {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-block: var(--space-3) var(--space-5);
  font-size: var(--text-xs);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  line-height: var(--lh-tight);
  color: var(--ink-muted);                     /* 6.99:1 */
}
.vehicle__meta-sep { color: var(--rule-strong); }

/* --- actions ------------------------------------------------------------- */

.vehicle__actions {
  position: relative;
  z-index: 1;                                  /* above the whole-card link */
  display: flex;
  margin-block-start: auto;
  /* Pulled back out through the body's padding: a rule that stops short of the
     card edge reads as a mistake rather than as a division. If a line is drawn
     across a card, it crosses the whole card. */
  margin-inline: calc(var(--card-pad) * -1);
  border-block-start: 1px solid var(--rule);
}

.btn-card {
  flex: 1 1 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-block-size: 46px;
  padding-inline: var(--space-2);
  border: 0;
  background: transparent;
  color: var(--ink-muted);                     /* 6.99:1 */
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: var(--weight-label);
  line-height: var(--lh-tight);
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
}
.btn-card + .btn-card { border-inline-start: 1px solid var(--rule); }
.btn-card:hover { background: rgb(var(--ink-rgb) / 0.06); color: var(--ink); }

/* The exported glyphs are baked #DDDDDD, so they only ever worked on a dark
   ground. Drawn as a mask over currentColor they follow the text on any
   ground the system has, and they dim and lift with the label.
   Paths sit in the class, not in a custom property: url() inside a custom
   property resolves against the declaring stylesheet, which browsers have
   never agreed on. These are relative to this file. */
.btn-card__icon {
  display: block;
  inline-size: 15px;
  block-size: 17px;
  flex: 0 0 auto;
  background-color: currentColor;
  -webkit-mask-repeat: no-repeat;  mask-repeat: no-repeat;
  -webkit-mask-position: center;   mask-position: center;
  -webkit-mask-size: contain;      mask-size: contain;
}
.btn-card__icon--share { -webkit-mask-image: url(../icons/share.svg);    mask-image: url(../icons/share.svg); }
.btn-card__icon--save  { -webkit-mask-image: url(../icons/bookmark.svg); mask-image: url(../icons/bookmark.svg); }
.btn-card__icon--text  { -webkit-mask-image: url(../icons/message.svg);  mask-image: url(../icons/message.svg); }


/* --- hover --------------------------------------------------------------- */
/* One gesture, not three: the photograph eases in as though the card were a
   window being leaned into, and the edge firms to say the object is live.
   Nothing moves under the cursor and no text is resampled, so the labels stay
   crisp. Keyboard gets the identical state through :focus-within. */

/* The WHOLE card lifts now, not just the photograph inside it — the card is the
   thing being chosen, so the card is what responds. It also rises above its
   neighbours while it grows, or the rail's next card would clip the edge that
   just moved. */
/* The card lifts as one object, and a soft shadow appears under it while it is
   raised. The shadow is the only honest part of the gesture: the card really is
   above its neighbours for that moment, so the cue is true rather than
   decorative — and it disappears again the instant the card settles. */
.vehicle {
  transform-origin: center;
  box-shadow: var(--shadow-none);
}

@media (hover: hover) {
  .vehicle:hover {
    z-index: 3;
    transform: scale(1.035);
    border-color: var(--rule-strong);
    background: var(--bg-hover);
    box-shadow: var(--shadow-lift);
  }
  .vehicle:hover .vehicle__title { color: var(--ink); }
}
.vehicle:focus-within {
  z-index: 3;
  transform: scale(1.035);
  border-color: var(--rule-strong);
  background: var(--bg-hover);
  box-shadow: var(--shadow-lift);
}
.vehicle:focus-within .vehicle__title { color: var(--ink); }

/* Reduced motion keeps the state — the card still reads as picked out by ink,
   edge and shadow — it simply stops growing. Removing the whole cue would take
   the information away with the animation. */
@media (prefers-reduced-motion: reduce) {
  .vehicle,
  .vehicle__media img { transition: none; }
  .vehicle:hover,
  .vehicle:focus-within { transform: none; }
}


/* =========================================================================
   8. BUILD YOUR OWN
   ========================================================================= */

/* Capped at the reading measure — a line the full 1564px wide makes the eye lose
   its place on the return sweep, whatever the aesthetic — and additionally kept
   out of the vehicle's column, the same way the options are. The bonnet rises
   level with this paragraph, so without the second cap the two would meet. */
.build__intro {
  max-inline-size: min(
    var(--measure),
    calc(100% - var(--cutout-w) + var(--cutout-overhang) - var(--space-6))
  );
}

/* The cutout's width and its overhang are declared once on the section, because
   two things depend on them: the vehicle itself, and the button that has to sit
   centred beneath it. Hard-coding either would put them back out of step the
   first time the clamp resolves differently. */
#build {
  /* Sized so the group — vehicle, interval, button — is TALLER than the plate:
     that surplus is what lifts the bonnet clear of the top edge. Anchored at the
     foot, a bigger car rises rather than sinks. */
  /* Sized so the right column lands close to the left one in height — the two
     masses then read as a pair rather than one trailing a hole beneath it. */
  --cutout-w: clamp(320px, 30vw, 580px);
  /* Negative pushes past the plate's right edge, positive pulls in from it. */
  --cutout-overhang: -56px;
  --cutout-rise: clamp(40px, 4.5vw, 86px);  /* how far the bonnet clears the plate */
  --cta-w: 320px;
  --panel-pad: clamp(var(--space-5), 2.6vw, 48px);

  /* A pool of light for the plate to stand in. One ellipse would read as a
     decal — a shape someone drew — so three overlap at different centres and
     their union carries no axis of symmetry. The largest sits under the plate,
     the second behind the car where the mass already is, and the third pulls
     the contour out to the lower left so the outline never resolves into a
     figure. Copper at 13% and below: felt, not seen. */
  background:
    radial-gradient(72% 46% at 54% 60%,
      rgb(var(--accent-rgb) / 0.13) 0%, rgb(var(--accent-rgb) / 0.05) 52%, rgb(var(--bg-rgb) / 0) 78%),
    radial-gradient(38% 56% at 78% 38%,
      rgb(var(--accent-rgb) / 0.12) 0%, rgb(var(--accent-rgb) / 0.04) 46%, rgb(var(--bg-rgb) / 0) 74%),
    radial-gradient(46% 30% at 24% 80%,
      rgb(var(--accent-rgb) / 0.08) 0%, rgb(var(--bg-rgb) / 0) 70%),
    var(--bg);
}

/* The options sit on a raised plate that lifts out of the ground at the top and
   settles back into it at the bottom, so the block reads as one surface rather
   than a box drawn on the page. */
.build__panel {
  position: relative;
  margin-block-start: var(--space-6);
  border-radius: var(--radius-4);
  background: linear-gradient(to bottom, var(--bg-panel) 0%, var(--bg) 100%);
}

/* The copper hairline is inset from the plate edge rather than drawn on it —
   two edges a few pixels apart read as one considered frame, where a single
   border would just read as a box. */
.build__panel::after {
  content: "";
  position: absolute;
  inset: 11px;
  border: 1px dashed var(--accent);
  border-radius: var(--radius-3);
  pointer-events: none;
}

/* Two columns in normal flow rather than a column plus a floating mass. Flow is
   what makes the overhang dependable: absolutely positioned, the vehicle sank
   or rose depending on how the left-hand text happened to wrap, so at 1440 it
   sat 41px INSIDE the plate while at 1920 it cleared it by 98. */
.build__panel-inner {
  position: relative;
  padding: var(--panel-pad);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-6);
  align-items: start;
}

/* Hairlines between the three options, not boxes: they are one list read in
   order, and a border round each would make them look like three offers. */
/* Centred in its column. The vehicle side is the taller of the two, so pinning
   the options to the top left all the slack at the bottom — the plate read as
   half-empty below the last line. Centring splits that slack evenly, and the
   distance to the frame is the same top and bottom. */
.spec-list {
  align-self: center;
  display: grid;
  gap: var(--space-5);
  max-inline-size: 44rem;
}
.spec-list > * + * {
  padding-block-start: var(--space-5);
  border-block-start: 1px solid var(--rule);
}

/* Stepped down from the h3 scale: three options read as one list, and at 28px
   each title was competing with the section title above them. Near-white ink,
   because the copper that used to sit here was below the 24px threshold where
   copper is legal. */
.spec-list__title {
  font-size: var(--text-lead);
  font-weight: var(--weight-heading);
  line-height: var(--lh-tight);
  /* Caps, matching the display tier. Safe at this length — three or four words
     each — and it separates the option titles from their own body copy by more
     than size alone. */
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  color: var(--ink);
}
/* One step down from body copy. These are three parallel descriptions read in
   comparison, not a passage read straight through — and 17px keeps clear of the
   14px floor under anything that carries information. */
.spec-list__body {
  margin-block-start: var(--space-text);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

/* The right-hand mass: the vehicle above its action, held together and anchored
   to the foot of the plate. Anchoring the PAIR is what keeps them one interval
   apart instead of however much the left column's text happens to leave over —
   the options can grow without opening a hole under the car.

   Above the stitched frame: that frame is an ::after on the plate, so it comes
   last in document order and would otherwise paint across the vehicle, which
   reads as a line drawn ON the car rather than behind it. */
.build__aside {
  position: relative;
  z-index: 2;
  inline-size: var(--cutout-w);
  /* Lifts out of the plate by a fixed amount, and pushes past the right edge by
     the overhang. Both are margins, so they hold whatever the text does. */
  margin-block-start: calc(var(--cutout-rise) * -1);
  margin-inline-end: calc(var(--cutout-overhang) * -1);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-7);
}

/* The cutout breaks the plate on two edges — it rises above the top and pushes
   past the right. That overlap is what stops the vehicle reading as a picture
   filed inside a box: it is the subject, and the plate is behind it. */
.build__media {
  inline-size: 100%;
  margin: 0;
  pointer-events: none;
}
.build__media img {
  display: block;
  inline-size: 100%;
  block-size: auto;
  object-fit: contain;
}

/* Inside the group, so it centres on the vehicle by construction rather than by
   arithmetic — the two share a column and the same centre line. */
.build__cta {
  display: flex;
  justify-content: center;
  margin: 0;
}
.build__cta .btn-hmc { inline-size: var(--cta-w); max-inline-size: 100%; }

/* Below the two-column width the cutout has nowhere to overlap into: it
   returns to the flow under the options and the plate stops being crowded. */
/* Below the two-column width the group has nowhere to overlap into: it returns
   to the flow under the options and stops crowding the plate. */
@media (max-width: 74.99rem) {
  .build__panel-inner { grid-template-columns: minmax(0, 1fr); }
  .build__aside {
    inline-size: min(560px, 100%);
    margin: var(--space-6) auto 0;
    gap: var(--space-5);
  }
  /* Desktop intro leaves room for the car column; that calc collapses to a
     few characters on a phone. Full reading width once the layout is one column. */
  .build__intro {
    max-inline-size: none;
  }
}

/* Phone: full-width actions, breathing room around the build CTA, less dead
   air under the build plate before Trade In. */
@media (max-width: 47.99rem) {
  .build__intro {
    max-inline-size: none;
    inline-size: 100%;
  }

  #build {
    --section-bottom: var(--space-5);
    --cta-w: 100%;
    --panel-pad: var(--space-5);
  }

  /* Overlap sections that follow a released scene — drop the large desktop tops. */
  #about,
  #gallery {
    --section-top: var(--space-6);
    --section-bottom: var(--space-7);
  }

  #reviews {
    --section-top: var(--space-6);
  }

  .nationwide__body {
    max-inline-size: none;
    inline-size: 100%;
    margin-block-start: var(--space-5);
  }

  .build__panel {
    margin-block-start: var(--space-5);
  }

  .build__panel-inner {
    padding-block-end: var(--space-5);
  }

  .build__aside {
    inline-size: 100%;
    margin-block-start: var(--space-5);
    gap: var(--space-5);
    padding-inline: var(--space-3);
    padding-block-end: var(--space-3);
  }

  .build__cta {
    inline-size: 100%;
    margin-block-start: 0;
    padding-inline: var(--space-2);
  }

  .build__cta .btn-hmc {
    inline-size: 100%;
    max-inline-size: none;
  }

  /* Stacked action pairs (Contact + phone, finance CTAs, …) span the column. */
  .btn-row {
    flex-direction: column;
    align-items: stretch;
    width: 100%;
  }

  .btn-row .btn-hmc,
  .btn-row .btn-hmc--wide,
  .nationwide__body .btn-hmc,
  .nationwide__body .btn-hmc--wide,
  .split__body .btn-hmc--wide {
    inline-size: 100% !important;
    max-inline-size: none !important;
    flex: 1 1 auto;
  }

  .cta-centre {
    justify-content: stretch;
    width: 100%;
  }

  .cta-centre .btn-hmc--wide {
    inline-size: 100%;
    max-inline-size: none;
  }

  /* One review at a time on the phone — no side peeks. */
  .reviews__stage {
    margin-block-start: var(--space-5);
    padding-block-end: var(--space-8);
  }

  .reviews__track {
    gap: 0;
  }

  .reviews__slide {
    flex: 0 0 100%;
  }

  .reviews__slide .review {
    transform: none;
    opacity: 1;
    padding: var(--space-5);
  }

  .reviews__slide.is-selected .review {
    transform: none;
    opacity: 1;
    box-shadow: var(--shadow-pop);
  }

  /* Clones / neighbours stay in the track for the rail math, but must not show. */
  .reviews__viewport {
    overflow-x: hidden;
    padding-block-end: var(--space-2);
  }
}



/* =========================================================================
   9. TRADE IN — photographic band, faded into the ground top and bottom
   ========================================================================= */

/* The clip belongs to the stage, not to the section. An ancestor with a
   non-visible overflow becomes the sticky containing block, so  here was quietly making  inside it behave as
   static — the pinned trade-in stage never pinned. The stage clips its own
   photograph, which is all the clip was ever for. */
/* A held scene. The section is taller than the viewport; the stage inside it
   sticks and fills one screen, so the photograph stops while the page keeps
   scrolling and the content assembles itself against a still background. The
   extra height IS the runway the animation plays along — no JS, no scroll
   listener, and the browser drives it off its own compositor. */
.scene {
  position: relative;
  isolation: isolate;
  padding-block: 0;
  /* Taller than the screen so the stage has something to stick THROUGH — this
     surplus is the runway the assembly plays along. Without it the section is
     exactly one viewport, sticky has nothing to hold, and the animation has no
     scroll left to run on. */
  /* The whole hand-off in two turns of the wheel instead of four.

     The arithmetic: hold-before-overlap = height − overlap − one screen. The
     overlap cannot drop below 100vh (a sticky element releases once less than
     its own height of container is left, and the stage is exactly one screen),
     so shortening has to come out of the hold. At 250vh the hold is half a
     screen — long enough to read the plates, short enough that nothing feels
     stuck. */
  min-block-size: 250vh;
  view-timeline-name: --trade;
  view-timeline-axis: block;
}


.scene__stage {
  position: sticky;
  inset-block-start: 0;
  block-size: 100vh;
  block-size: 100svh;
  display: grid;
  align-items: center;
  isolation: isolate;
  overflow: hidden;
}

/* Everything must fit one screen, so the scene sets its own tighter rhythm
   rather than inheriting the page's section intervals. */
.scene__content {
  padding-block: clamp(var(--space-6), 5vh, var(--space-9));
}

/* Each element of the scene is driven individually on the way out — the exit is
   the assembly played backwards, so it belongs to the pieces, not to the block
   that holds them. */
.js .scene__content [data-rise] { will-change: transform, filter, opacity; }

/* THE HAND-OFF
   The next section climbs over the held stage instead of following it. The
   overlap is what makes the scene read as being left behind rather than as
   simply ending — the photograph stays, its content sinks away beneath the
   arriving one.

   It carries its own ground: without an opaque background the stage would show
   straight through, and the two sections would read as one confused layer. */
.section--overlap {
  position: relative;
  z-index: 2;
  /* A full screen of overlap, and that number is not a taste call.

     A sticky element releases once less than its own height of container
     remains. The stage is 100vh, so the photograph stays pinned only while at
     least 100vh of .trade is left — and what is left below the stage is exactly
     this overlap. Any value under 100vh means the background starts sliding
     away part-way through the hand-off, which is the one thing it must not do.
     The section's own height carries a matching +100vh so nothing is lost. */
  margin-block-start: -100vh;
  background: var(--bg);
  border-start-start-radius: var(--radius-4);
  border-start-end-radius: var(--radius-4);
  /* The leading edge needs to be legible as an edge — this is the one moment on
     the page where two sections genuinely occupy the same space, so the shadow
     is stating a true fact about which one is on top. */
  box-shadow: var(--shadow-scene);
}

/* Nothing to climb over once the scene is released. */
@media (prefers-reduced-motion: reduce), (max-width: 61.99rem) {
  .section--overlap {
    margin-block-start: 0;
    border-start-start-radius: 0;
    border-start-end-radius: 0;
    /* Nothing overlaps here, so the edge shadow would just be a dark band
       across a join — a cue for a layering that is not happening. */
    box-shadow: none;
  }
  .scene__content [data-rise] { filter: none; transform: none; opacity: 1; }

  /* No pin / runway on small screens: a 250vh sticky stage left a tall empty
     band under Build before "Ready to Trade In?" could sit normally. */
  .scene {
    min-block-size: 0;
    view-timeline-name: none;
  }
  .scene__stage {
    position: relative;
    inset-block-start: auto;
    block-size: auto;
    overflow: visible;
    align-items: start;
    /* One pad only — content used to add a second full section pad on top. */
    padding-block: var(--space-6) var(--space-5);
  }
  .scene__content {
    padding-block: 0;
  }
}
.trade__head { display: grid; gap: var(--space-3); }

/* All three lines share the left edge. The indent read as three separate
   starts rather than one head — the staircase belongs to how they ARRIVE,
   which the slide-in already carries, not to where they sit. */
.scene__line.lead { max-inline-size: 54ch; }

/* A single warm bloom off the right edge, level with the heading. The section
   arrives on flat ground straight off a photographic scene, and one soft source
   gives it somewhere to come from — copper at a tenth of its strength, which is
   a light, not a colour: it never rises far enough to become a surface. */
#about {
  background:
    /* Second source, off the left edge and higher up. Same copper as the other
       one — one light in two places rather than two different lights, so the
       section reads as lit by a single warm source it happens to catch twice.
       Smaller in reach, because the eye is meant to end up on the right, where
       the heading is. */
    radial-gradient(46% 44% at -6% 8%,
      rgb(var(--accent-rgb) / 0.16) 0%,
      rgb(var(--accent-rgb) / 0.06) 48%,
      rgb(var(--bg-rgb) / 0) 74%),
    radial-gradient(58% 62% at 104% 26%,
      rgb(var(--accent-rgb) / 0.13) 0%,
      rgb(var(--accent-rgb) / 0.05) 45%,
      rgb(var(--bg-rgb) / 0) 72%),
    var(--bg);
}

/* Nationwide's copy holds the left, the photograph keeps the right. */
.nationwide__body {
  display: grid;
  gap: var(--space-5);
  justify-self: start;
  max-inline-size: 46rem;
  margin-block-start: var(--space-7);
}
.nationwide__body p { color: var(--ink-quiet); }

@media (min-width: 62rem) {
  #nationwide .scene__content {
    display: grid;
    align-content: center;
    justify-items: start;
    text-align: start;
  }
  #nationwide .section-head { align-items: flex-start; text-align: start; }
}

/* The scrim runs ACROSS rather than down: solid where the words are, clearing
   to nothing over the car. A vertical scrim would either bury the vehicle or
   leave the copy sitting on lit bodywork — this one does the work only where
   the reading happens.

   The photograph shifts right so the car clears the text column and sits in
   the part of the frame the gradient leaves open. */
/* Colour stays. The grayscale the trade-in stage uses would throw away what
   this photograph is carrying — the scrim alone does the legibility work, laid
   OVER the image rather than draining it. A touch of contrast and a slight
   darkening keep the gradient from having to fight a bright frame. */
#nationwide .scene__media img {
  object-position: 68% center;
  filter: contrast(1.06) saturate(0.94) brightness(0.9);
}

#nationwide .scene__stage::before {
  background:
    linear-gradient(to right,
      var(--bg) 0%,
      /* The copy reaches 48% of the frame — measured, not judged by eye — so
         the scrim holds full weight just past it and only then opens up. */
      rgb(var(--bg-rgb) / 0.94) 52%,
      rgb(var(--bg-rgb) / 0.62) 68%,
      rgb(var(--bg-rgb) / 0.18) 87%,
      rgb(var(--bg-rgb) / 0) 100%),
    /* A short vertical wash top and bottom so the scene still joins the
       sections above and below it rather than starting with a hard edge. */
    linear-gradient(to bottom,
      var(--bg) 0%,
      rgb(var(--bg-rgb) / 0) 18%,
      rgb(var(--bg-rgb) / 0) 82%,
      var(--bg) 100%);
}

@media (max-width: 61.99rem) {
  /* One column: the copy sits over the middle of the frame, so the wash has to
     go back to vertical or the last lines land on open bodywork. */
  #nationwide .scene__stage::before {
    background: linear-gradient(to bottom,
      var(--bg) 0%,
      rgb(var(--bg-rgb) / 0.88) 22%,
      rgb(var(--bg-rgb) / 0.88) 78%,
      var(--bg) 100%);
  }
  #nationwide .scene__media img { object-position: 60% center; }
}
.scene__media { position: absolute; inset: 0; z-index: -2; }
.scene__media img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.05);   /* the Figma's luminosity blend */
}

.scene__stage::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  /* Measured against the worst case, not the average: the chrome headlight
     in the upper third is the brightest region the heading and lead cross,
     so the scrim is heaviest exactly there. */
  background: linear-gradient(to bottom,
    var(--bg) 0%,
    rgb(var(--bg-rgb) / 0.86) 22%,
    rgb(var(--bg-rgb) / 0.84) 48%,
    rgb(var(--bg-rgb) / 0.80) 70%,
    var(--bg) 100%);
}

.steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-5);
  margin-block-start: clamp(var(--space-6), 4vh, var(--space-8));
}

/* No drop shadow: the plates sit in the page, not above it. Separation from
   the photograph behind them is carried by the warm-to-ground gradient the
   Figma already specifies, plus a hairline. */
/* Compact enough that four of them plus the head clear one screen. */
.step {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-block-size: 0;
  padding: clamp(var(--space-5), 3vh, var(--space-7));
  border: 1px solid var(--rule);
  border-radius: var(--radius-3);
  background: linear-gradient(to bottom, var(--bg-plate), var(--bg));
}

/* HOVER — a lift, not a drawn outline.
   The travelling line was doing too much: four edges animating in sequence on a
   card whose whole job is to be read in one glance, and it held the card after
   the pointer had already left. A few pixels of rise with the shadow that
   implies is the same information — this one is under the cursor — stated once
   and reversible instantly. */
/* The lift uses `translate`, NOT `transform`. The reveal system owns transform
   on these cards and its selector — .js [data-rise].is-in — outranks any plain
   .step:hover, so a transform here would simply never apply. `translate` is its
   own property: the two compose instead of competing, and neither needs to know
   about the other. */
/* Written at .js .step[data-rise] specificity on purpose: the reveal rule
   (.js [data-rise]) sets its own transition list and outranks a bare .step, so
   a plain .step { transition } was being discarded and the lift snapped. This
   restates the reveal's two properties and adds the hover's three.

   0.4s rather than 0.25s, and the shadow deliberately runs slower than the
   movement: a card that rises and darkens in lockstep reads as one flat switch,
   while a shadow still deepening after the card has settled reads as weight.
   --ease-out lands soft, so there is no click at the top. */
.step,
.js .step[data-rise] {
  transition: opacity var(--dur-3) var(--ease-out),
              transform var(--dur-3) var(--ease-out),
              translate var(--dur-3) var(--ease-out),
              border-color var(--dur-3) var(--ease-out),
              box-shadow var(--dur-4) var(--ease-out);
  will-change: translate;
}

.step:hover,
.step:focus-within {
  translate: 0 -4px;
  border-color: var(--rule-strong);
  box-shadow: var(--shadow-nudge);
}

@media (prefers-reduced-motion: reduce) {
  .step,
  .js .step[data-rise] { transition: none; }
  .step:hover,
  .step:focus-within { translate: none; }
}

/* THE ASSEMBLY
   Head slides in from the left, line after line, along the axis its staircase
   already implies; the plates rise from below, one at a time.

   Driven by an IntersectionObserver rather than by scroll-driven CSS. The CSS
   version only ran in Chrome and Safari 26 — everywhere else the @supports test
   fell through to static, so for most visitors the motion simply did not exist.
   A transition every browser can run beats a timeline most of them ignore.

   The .js gate matters: without scripting nothing would ever add the reveal
   class, so the scene must never START hidden. */
.js [data-rise] {
  opacity: 0;
  transition:
    opacity var(--dur-3) var(--ease-out),
    transform var(--dur-3) var(--ease-out);
}
.js [data-rise="left"]  { transform: translate3d(-44px, 0, 0); }
.js [data-rise="right"] { transform: translate3d(44px, 0, 0); }
.js [data-rise="up"]    { transform: translate3d(0, 48px, 0); }

.js [data-rise].is-in {
  opacity: 1;
  transform: none;
}

/* RELEASING THE SCENE
   Two cases, one identical outcome — plain section, normal flow, everything
   visible and still. Placed after every rule it overrides, or the pin and the
   hidden start state survive it.

   · reduced motion — a scene that assembles itself is precisely what that
     request is asking to be spared
   · one column — four stacked plates plus the head measure 1289px against an
     844px screen, so "fits one screen" is not available at that width. Pinning
     it anyway would trap the visitor inside a scene that cannot resolve. */
@media (prefers-reduced-motion: reduce), (max-width: 61.99rem) {
  .scene { min-block-size: 0; }
  .scene__stage {
    position: relative;
    block-size: auto;
    /* Keep in sync with the earlier release block — do NOT re-add section pads. */
    padding-block: var(--space-6) var(--space-5);
  }
  .scene__content { padding-block: 0; }
}

/* The reveal itself survives the narrow breakpoint — only the pinned stage is
   released there. A build panel scrolling past on a phone still benefits from
   its rows arriving in order. */
@media (prefers-reduced-motion: reduce) {
  .js [data-rise] {
    opacity: 1;
    transform: none;
    transition: none;
  }
}
@media (max-width: 61.99rem) {
  .scene__line.lead { max-inline-size: none; }
}

.step__index {
  font-size: var(--text-xs);        /* the Figma sets these near-invisible; floored */
  font-weight: var(--weight-label);
  letter-spacing: var(--track-caps);
  color: var(--accent-ink);
  font-variant-numeric: tabular-nums;
}
/* THE ICON SLOT — it replaces .step__index, and takes the same rank the numeral
   had: the copper, the label size, the top of the plate. A numeral is right for
   four steps taken in order; these are four qualities held at once, so a glyph.

   MATERIAL SYMBOLS, and that is not a new language on this site: the header's
   telephone glyph is already one — same 0 -960 960 960 box, same filled
   geometry. The four are volunteer_activism, verified_user, trending_up and
   local_fire_department, taken from the set rather than drawn, because this
   project's icons folder is not a set to draw into: badge.svg is a plain filled
   circle, star is 17px, arrow-build is a pencil at 30px, message is 13.77px
   wide — different sources, different grids, no shared stroke. */
.step__icon {
  display: block;
  inline-size: 28px;
  block-size: 28px;
  color: var(--accent-ink);            /* 5.34:1 — the rank the numeral held */
}
.step__icon svg { inline-size: 100%; block-size: 100%; display: block; }

.step__title {
  margin-block-start: var(--space-1);
  font-size: var(--text-lead);
  line-height: var(--lh-tight);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  color: var(--ink);
}
.step__body { color: var(--ink-muted); }   /* 5.56:1 against the plate's top stop */

/* Shared: a single action centred under a block that has just ended */


/* =========================================================================
   10. SPLIT BANDS — about, finance
   The two bands mirror each other rather than repeating, so the page
   develops instead of running a 50/50 metronome.
   ========================================================================= */

/* Top-aligned, and the image column is the wider of the two — the vehicle
   and the workshop are the evidence, the copy explains it. Centring the two
   columns instead left a dead field above and below the shorter one. */
.split {
  display: grid;
  gap: var(--space-8);
  align-items: start;
  /* `clip` for the reason .about-open, .contact-open and .who all carry it: the
     media enters on [data-rise="right"], which starts it 44px to the trailing
     side, and at 320–390px that hangs past the viewport and gives the page a
     horizontal scrollbar until it lands. `clip` cuts it without making the band
     a scroll container, which `hidden` would. */
  overflow-x: clip;
}

@media (min-width: 62rem) {
  .split { grid-template-columns: 1.12fr 1fr; gap: var(--space-10); }
  .split--reverse { grid-template-columns: 1fr 1.12fr; }
  .split--reverse .split__media { order: 2; }
}

.split__media {
  position: relative;
  border-radius: var(--radius-3);
  overflow: hidden;
}
.split__media img,
.split__media video { inline-size: 100%; block-size: 100%; object-fit: cover; }

.split__media--16x9 { aspect-ratio: 869 / 489; }
.split__media--4x3  { aspect-ratio: 774 / 544; }

.split__media--warm::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--wash-warm);
  mix-blend-mode: soft-light;
  opacity: 0.77;
  pointer-events: none;
}

.split__body { max-inline-size: var(--measure); color: var(--ink); }
.split__body p + p { margin-block-start: var(--space-4); }


/* =========================================================================
   11. NATIONWIDE — one wide image band with the copy inside it
   ========================================================================= */

.band {
  position: relative;
  isolation: isolate;
  display: grid;
  align-items: center;
  min-block-size: clamp(520px, 50vw, 720px);
  padding: clamp(var(--space-6), 4vw, var(--space-9));
  border-radius: var(--radius-3);
  overflow: hidden;
}

.band__media { position: absolute; inset: 0; z-index: -2; }
.band__media img { inline-size: 100%; block-size: 100%; object-fit: cover; }

.band::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  /* The copy column starts at ~36% of the band, so the scrim has to be doing
     its work before it — the earlier version only reached 0.75 at 60%, which
     left the first two lines sitting over a lit street. */
  background: linear-gradient(105deg,
    rgb(var(--bg-rgb) / 0) 30%,
    rgb(var(--bg-rgb) / 0.35) 46%,
    rgb(var(--bg-rgb) / 0.88) 60%,
    rgb(var(--bg-rgb) / 0.98) 72%);
}

/* Wide enough to hold the two parallel actions on one line — the pair is a
   choice, and stacking them would rank one above the other untruthfully. */
.band__content {
  justify-self: end;
  max-inline-size: 34rem;
  color: var(--ink);
}

@media (max-width: 61.99rem) {
  .band::before {
    background: linear-gradient(to bottom,
      rgb(var(--bg-rgb) / 0.35) 0%,
      rgb(var(--bg-rgb) / 0.88) 45%,
      rgb(var(--bg-rgb) / 0.96) 100%);
  }
  .band__content { justify-self: stretch; }
}


/* =========================================================================
   12. GALLERY
   ========================================================================= */

/* Source order is column-major (a,d · b,f · c,e) so the mosaic keeps the
   Figma's column pairing on desktop and still reads as a sensible sequence
   when it collapses to one column. */
/* The same pool of light as Build — three overlapping ellipses whose union has
   no axis of symmetry, so the mosaic stands in it the way the options plate
   does rather than sitting on a drawn shape. */
#gallery {
  background:
    radial-gradient(72% 46% at 54% 60%,
      rgb(var(--accent-rgb) / 0.13) 0%, rgb(var(--accent-rgb) / 0.05) 52%, rgb(var(--bg-rgb) / 0) 78%),
    radial-gradient(38% 56% at 78% 38%,
      rgb(var(--accent-rgb) / 0.12) 0%, rgb(var(--accent-rgb) / 0.04) 46%, rgb(var(--bg-rgb) / 0) 74%),
    radial-gradient(46% 30% at 24% 80%,
      rgb(var(--accent-rgb) / 0.08) 0%, rgb(var(--bg-rgb) / 0) 70%),
    var(--bg);
}

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--space-4);
  margin-block-start: var(--space-7);
}


.gallery__item {
  position: relative;
  display: block;
  /* A grid item defaults to min-width:auto, so an image with an intrinsic
     size larger than its track pushes the track wider and the whole mosaic
     overflows its container — which is what was clipping the outer tiles and
     cutting their captions. */
  min-inline-size: 0;
  min-block-size: 0;
  padding: 0;
  border: 0;
  border-radius: var(--radius-3);
  background: var(--bg-raised);
  overflow: hidden;
  cursor: default;
}
.gallery__item img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  /* The photograph moves inside its tile, never the tile itself — the mosaic
     packs edge to edge, so a tile that grew would push its neighbours. */
  transition: opacity var(--dur-2) var(--ease-out),
              scale var(--dur-3) var(--ease-out);
}
.gallery__item:hover img,
.gallery__item:focus-within img { scale: 1.06; }

/* The tiles arrive slower than the rest of the page (240ms apart, set on the
   markup) and travel a little further, because they land as a group and the
   eye should have time to follow the sequence rather than catch the aftermath. */
.js .gallery__item[data-rise] {
  transition-duration: var(--dur-4, 720ms);
}

@media (prefers-reduced-motion: reduce) {
  .gallery__item img { transition: none; }
  .gallery__item:hover img,
  .gallery__item:focus-within img { scale: 1; }
}
.gallery__item--a { aspect-ratio: 615 / 390; }
.gallery__item--b { aspect-ratio: 457 / 614; }
.gallery__item--c { aspect-ratio: 457 / 352; }
.gallery__item--d { aspect-ratio: 615 / 573; }
.gallery__item--e { aspect-ratio: 457 / 525; }
.gallery__item--f { aspect-ratio: 457 / 573; }

/* A mosaic packs only if the grid owns the geometry. Per-tile aspect ratios
   made each column as tall as its own photographs, so unequal columns left
   the holes: the tracks define the shape now and the photographs crop into
   it. Ratios stay for the single-column phone layout, where nothing packs. */
@media (min-width: 62rem) {
  .gallery {
    max-inline-size: 76rem;
    margin-inline: auto;
    aspect-ratio: 16 / 10;
    grid-template-columns: 1.35fr 1fr 1fr;
    grid-template-rows: repeat(6, minmax(0, 1fr));
    gap: var(--space-3);
  }
  .gallery__item { aspect-ratio: auto; }
  .gallery__item--a { grid-column: 1; grid-row: 1 / 4; }
  .gallery__item--d { grid-column: 1; grid-row: 4 / 7; }
  .gallery__item--b { grid-column: 2; grid-row: 1 / 5; }
  .gallery__item--f { grid-column: 2; grid-row: 5 / 7; }
  .gallery__item--c { grid-column: 3; grid-row: 1 / 3; }
  .gallery__item--e { grid-column: 3; grid-row: 3 / 7; }
}

/* Only items that really open something get the play affordance (input honesty) */
.gallery__item--video { cursor: pointer; }
.gallery__item--video:hover img,
.gallery__item--video:focus-visible img { opacity: 0.82; }

/* Scoped past `.gallery__item img`, which would otherwise stretch the glyph
   to the full tile the way it stretches the photograph. */
.gallery__item img.gallery__play {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  translate: -50% -50%;
  inline-size: clamp(56px, 22%, 142px);   /* proportional to the tile, not the viewport */
  block-size: auto;
  object-fit: contain;
  pointer-events: none;
}

.gallery__caption {
  position: absolute;
  inset-block-end: 0;
  inset-inline: 0;
  padding: var(--space-6) var(--space-4) var(--space-3);
  background: linear-gradient(to top, rgb(var(--bg-deep-rgb) / 0.9), rgb(var(--bg-deep-rgb) / 0));
  color: var(--ink);
  font-size: var(--text-xs);
  text-align: start;
}


/* =========================================================================
   13. REVIEWS
   ========================================================================= */

/* Same measure as the mosaic above, so the two blocks share one left and one
   right edge down the page. */
.reviews__stage {
  position: relative;
  max-inline-size: 76rem;
  margin-inline: auto;
  margin-block-start: var(--space-7);
}
/* A rail, not a re-order. The cards never trade places — the whole track
   slides so the active one lands on the centre line, which is why nothing
   jumps. Heights are equal and stay equal: the active card is enlarged with a
   transform, which paints outside its box without touching layout, so the
   viewport never changes height mid-transition. */
.reviews__viewport {
  /* `overflow-x: clip` instead of `overflow: hidden`: the rail must be cut off
     at the sides, but NOT top and bottom — the active card's scale and shadow
     paint outside the box on purpose. The old fix was vertical padding cancelled
     by an equal negative margin, and that negative margin then ate half the
     interval under the section: 48px was declared and 24px arrived. Clipping
     one axis removes the need for the trick entirely. */
  overflow-x: clip;
  /* The active card is scaled, so it hangs ~10px below the row. The box has to
     contain that overhang, otherwise the gap to the button is measured from the
     wrong edge and reads short under the one card the eye is actually on. */
  padding-block-end: var(--space-3);
  cursor: grab;
}
.reviews__viewport.is-dragging { cursor: grabbing; }

.reviews__track {
  display: flex;
  align-items: stretch;
  gap: var(--space-4);
  will-change: transform;
  transition: transform var(--dur-3) var(--ease-out);
}
.reviews__track.is-dragging { transition: none; }

.reviews__slide {
  flex: 0 0 calc((100% - var(--space-4) * 2) / 3);
  display: flex;
}

.reviews__slide .review {
  inline-size: 100%;
  opacity: 0.82;
  transform: scale(0.97);
  transform-origin: center;
  transition: transform var(--dur-3) var(--ease-out),
              opacity var(--dur-3) var(--ease-out),
              box-shadow var(--dur-3) var(--ease-out);
}

/* The active quote lifts and covers its neighbours' inner edges. */
.reviews__slide.is-selected { z-index: 3; }
.reviews__slide.is-selected .review {
  opacity: 1;
  transform: scale(1.07);
  box-shadow: var(--shadow-pop);
}

/* Text selection would fight the drag. */
.reviews__viewport * { user-select: none; }

/* Arrows live in the margin beside the cards where the page is empty anyway.
   Below that width there is no margin to live in, so they drop under the row
   rather than sit on top of the quotes they are meant to reveal. */
.reviews__arrow {
  position: absolute;
  inset-block-start: 50%;
  z-index: 4;
  display: grid;
  place-items: center;
  inline-size: 52px;
  block-size: 52px;
  padding: 0;
  border: 1px solid rgb(var(--accent-ink-rgb) / 0.55);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--accent-ink);
  cursor: pointer;
  translate: 0 -50%;
  transition: background-color var(--dur-1) var(--ease-out),
              border-color var(--dur-1) var(--ease-out),
              color var(--dur-1) var(--ease-out);
}
.reviews__arrow svg { inline-size: 24px; block-size: 24px; }
.reviews__arrow:hover,
.reviews__arrow:focus-visible {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--ink);
}
.reviews__arrow--prev { inset-inline-start: calc(var(--space-9) * -1); }
.reviews__arrow--next { inset-inline-end: calc(var(--space-9) * -1); }

@media (max-width: 90rem) {
  .reviews__stage { padding-block-end: var(--space-9); }
  .reviews__arrow {
    inset-block-start: auto;
    inset-block-end: 0;
    translate: none;
  }
  .reviews__arrow--prev { inset-inline-start: calc(50% - 64px); }
  .reviews__arrow--next { inset-inline-end: calc(50% - 64px); }
}

.review {
  display: flex;
  margin-inline: 0;
  flex-direction: column;
  gap: var(--space-4);
  block-size: 100%;
  padding: var(--space-6);
  border-radius: var(--radius-3);
  background: var(--bg-card-plain);
  color: var(--ink-card);
}

.review__name {
  font-family: var(--font-display);
  font-size: var(--text-lead);
  line-height: var(--lh-tight);
  color: var(--ink-inverse);
}

.review__stars { display: flex; gap: var(--space-1); margin-block-start: calc(var(--space-2) * -1); }
.review__stars img { inline-size: 16px; }

/* Two things were fighting here. A stray margin stacked on top of the flex
   gap, so the 16px between stars and quote rendered as 40; and equal-height
   cards stretched to the longest quote left 240px of blank white under the
   short ones — emptiness that says nothing, which is the one kind the dialect
   doesn't buy. The rhythm is the gap alone, and the quotes are clamped so the
   cards match on content rather than on stretch. Anyone wanting the full text
   has "View all reviews" directly below. */
.review__body {
  margin-block: 0;
  font-size: var(--text-sm);
  line-height: var(--lh-body);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 5;
  line-clamp: 5;
  overflow: hidden;
}

/* The card is narrow enough on a phone that 32px of padding costs more in
   measure than it returns in air — one step down, never to zero (I4). */
@media (max-width: 48rem) {
  .review { padding: var(--space-5); }
}

/* Bootstrap carousel, crossfaded rather than slid: the reviews never moved
   through space, only their identity changed. */
#reviewsCarousel .carousel-item { transition-duration: var(--dur-3); }
#reviewsCarousel .carousel-inner { padding-block: var(--space-2); }

.reviews__nav {
  display: flex;
  justify-content: center;
  gap: var(--space-4);
  margin-block-start: var(--space-text-obj);
}



/* =========================================================================
   14. CONTACT
   ========================================================================= */

/* CONTACT — full-bleed map beside a warm panel
   The page has run on a near-black ground for its whole length; this is the one
   place it changes state. A large copper field at the end reads as arrival, and
   the map earns full width because a location cropped into a text column stops
   being a place and becomes an illustration of one. */
.contact {
  position: relative;
  display: grid;
  grid-template-columns: 1fr;
}

@media (min-width: 62rem) {
  .contact { grid-template-columns: 1.9fr 1fr; }
}

/* LIVE MAP
   OpenStreetMap ships one tile set, and it is light — a white rectangle on a
   near-black page reads as a hole punched through it. The frame is inverted and
   hue-rotated back, which is the only way to darken third-party tiles you do not
   host: invert() flips the paper to ink AND the ink to paper, so labels stay
   legible, and hue-rotate(180deg) returns the greens and blues to roughly
   themselves after the flip. */
.contact__map {
  position: relative;
  min-block-size: 420px;
  background: var(--bg-raised);   /* holds the shape while tiles load */
  overflow: hidden;
}

/* Edge to edge, so no radius and no border: a framed map inside a full-bleed
   band would be a picture of the treatment rather than the treatment. */
.contact__map__frame {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  min-block-size: 420px;
  border: 0;
  filter: invert(1) hue-rotate(180deg) brightness(0.92) contrast(0.95) saturate(0.75);
}

/* An iframe cannot be wrapped in a link, so the route out is its own control —
   and it is a real target, not a hover-only affordance. */
.contact__map__link {
  position: absolute;
  inset-block-end: var(--space-5);
  inset-inline-start: var(--space-5);
  display: inline-flex;
  align-items: center;
  min-block-size: 44px;
  padding: var(--space-2) var(--space-5);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-2);
  background: rgb(var(--bg-rgb) / 0.88);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--text-xs);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  text-decoration: none;
  transition: background-color var(--dur-1) var(--ease-out),
              border-color var(--dur-1) var(--ease-out);
}
.contact__map__link:hover,
.contact__map__link:focus-visible {
  background: var(--bg);
  border-color: var(--ink);
}

/* The filter is a cosmetic layer, not information — a renderer that overrides
   colour has its own contrast model and must not be fought. */
@media (forced-colors: active) {
  .contact__map__frame { filter: none; }
}

/* THE DARK PANEL
   The ground is --bg-raised, which is what DS.html has documented for this panel
   all along ("Card title band, footer, contact panel"). It had drifted to a
   solid copper gradient — an entire saturated slab, which is the accent used as
   an identity rather than as a setting (colour D3), and it broke the site's dark
   restraint everywhere the eye landed on it. On the dark ground copper returns to
   what it is elsewhere: the label ink, the watermark, and the one action. The map
   beside it is dark too, so the two columns read as one field split by a hairline
   rather than a cool photo bolted to a warm billboard (anti-patterns U11). */
.contact__panel {
  position: relative;
  display: grid;
  align-content: center;
  gap: var(--space-7);
  padding: clamp(var(--space-7), 4vw, var(--space-9));
  padding-inline-end: clamp(var(--space-8), 6vw, var(--space-10));
  background: var(--bg-raised);
  color: var(--ink);
  overflow: hidden;
  /* Hairline seam between the two dark columns: on top when the panel stacks
     under the map (mobile), on the leading edge when it sits beside it. */
  border-block-start: 1px solid var(--rule);
}
@media (min-width: 62rem) {
  .contact__panel { border-block-start: 0; border-inline-start: 1px solid var(--rule); }
}

/* The word runs the full height of the panel, letter by letter, hard against
   the screen edge — a watermark built into the surface rather than a label
   placed on it. Each letter is its own box so the column can be distributed
   across the height instead of being one word floating in the middle of it.

   PHYSICAL top/bottom/right on purpose: an absolutely positioned box resolves
   its LOGICAL insets in its own writing-mode, and this one is vertical-rl — so
   inset-inline-end meant the bottom and inset-block-start meant the right edge.
   Under vertical-rl the flex main axis already runs top-to-bottom, which is why
   space-between distributes down the panel and the glyphs stay rotated.

   aria-hidden because the section already has a heading; the same word read
   twice is one reading too many. */
.contact__spine {
  position: absolute;
  top: 0;
  bottom: 0;
  right: -0.06em;
  display: flex;
  justify-content: space-between;
  align-items: center;
  /* Physical again, same reason as the insets: under vertical-rl padding-block
     is the HORIZONTAL pair, so it left the letters jammed against the top and
     bottom edges. */
  padding-top: clamp(var(--space-5), 3vw, var(--space-7));
  padding-bottom: clamp(var(--space-5), 3vw, var(--space-7));
  font-family: var(--font-display);
  font-size: var(--text-spine);
  font-weight: var(--weight-heading);
  line-height: var(--lh-none);
  text-transform: uppercase;
  /* Copper watermark embossed into the dark ground — the one committed gesture,
     material rather than loud. Decorative (aria-hidden), so no contrast floor. */
  color: rgb(var(--accent-ink-rgb) / 0.14);
  writing-mode: vertical-rl;
  pointer-events: none;
  user-select: none;
}

.contact__body {
  display: grid;
  gap: var(--space-6);
  max-inline-size: 22rem;
}

/* Label to its value is the tight interval; item to item is the loose one —
   the pairs have to read as pairs before anything is read (spacing I1). */
.contact__item { display: grid; gap: var(--space-2); }

/* Label and value must not read as two sizes of one voice. The label is small,
   heavy and widely tracked; the value is large and light. Size, weight, register
   and tracking all move together — one axis of difference is what reads as an
   accident, four read as a decision (typography I1). */
.contact__label {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-family: var(--font-display);
  font-size: var(--text-xs);
  font-weight: var(--weight-label);
  /* --track-caps (0.03em), the site's single documented uppercase tracking. The
     original 0.22em read "cheap"; the fix is NOT a wider label step — DS.html
     records that a separate wide track was tried twice and read as gaps in this
     condensed face. The label register is carried by copper, case and weight,
     not by air between letters. (typography I4's 0.08em number yields to the
     face; its intent — legible, cohesive uppercase — is met.) */
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--accent-ink);   /* copper is the brand's label ink — 5.19:1 on --bg-raised */
}
/* Material Symbols, inlined and drawn in currentColor — so the icon takes the
   copper of its label directly. It is the label register in a glyph, so it
   carries the same ink rather than a filtered approximation of it. */
.contact__icon { flex: 0 0 auto; opacity: 0.9; }

/* THE VALUE OUTRANKS ITS OWN LABEL, and now the tone does the ranking the size
   used to do alone. On the dark ground the split is clean: the value is
   near-white (--ink, 16.3:1), the label is copper (--accent-ink). The number and
   the address — what a visitor came for — sit at the site's highest contrast,
   the label recedes into the copper register, and the two read as figure and
   caption at a glance. Size still helps (--text-lead over --text-xs). */
.contact__value {
  margin: 0;
  /* --text-body, which is the size the summary panel's pair uses: BASE at 14 in
     the label voice over Rolling Chassis at 16 in the reading voice. I had this
     at the lead size to correct a hierarchy the label was winning; it is the
     LABEL that was too loud, and it lost its 0.22em tracking, so the pair works
     at the pair's own sizes now. */
  font-size: var(--text-body);
  font-weight: var(--weight-body);
  font-style: normal;
  line-height: var(--lh-lead);
  letter-spacing: var(--track-body);
  color: var(--ink);
}
/* The number is the one thing here anyone copies by eye — tabular figures and a
   little air between them, the way a phone number is read aloud. */
.contact__value .num-tabular { letter-spacing: var(--track-num); }
/* A phone number is a control on a phone, so it takes the target floor like
   one. inline-flex rather than block, so the box grows without the line moving. */
.contact__value a {
  display: inline-flex;
  align-items: center;
  min-block-size: 44px;
  color: inherit;
  text-decoration: none;
}
.contact__value a:hover,
.contact__value a:focus-visible { text-decoration: underline; text-underline-offset: 0.2em; }

.contact__value.social { gap: var(--space-2); margin-inline-start: calc(var(--space-3) * -1); }
/* On the dark ground the marks read light: brightness(0) flattens each brand SVG
   to one ink, invert(1) turns that ink near-white. Sits with the white values,
   not the copper labels — a social row is data, not a label. */
.contact__panel .social a img { filter: brightness(0) saturate(100%) invert(1); opacity: 0.72; }
.contact__panel .social a:hover img,
.contact__panel .social a:focus-visible img { opacity: 1; }

.contact__cta { justify-self: start; margin-block-start: var(--space-2); }

/* A copper button on a copper field would be invisible, so the CTA inverts:
   ink ground, copper-free, 16.83:1 the other way round. */
.btn-hmc--dark {
  background: var(--ink-inverse);
  border-color: var(--ink-inverse);
  color: var(--ink);
}
.btn-hmc--dark:hover,
.btn-hmc--dark:focus-visible { background: var(--bg-deep); border-color: var(--bg-deep); }

.social {
  display: flex;
  gap: var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}
.social a {
  display: grid;
  place-items: center;
  inline-size: 44px;                 /* touch target, not the glyph size */
  block-size: 44px;
  border-radius: var(--radius-2);
  transition: background-color var(--dur-1) var(--ease-out);
}
.social a:hover { background: rgb(var(--ink-rgb) / 0.14); }
/* Brand marks are not square — facebook is 9×21, youtube 31×21. Forcing both
   into a 21px box stretched one and squashed the other. Height is the constant
   the eye actually reads across a row of marks; width follows the artwork. */
.social img { block-size: 20px; inline-size: auto; max-inline-size: 28px; }




/* =========================================================================
   15. FOOTER
   ========================================================================= */

/* COPPER, as a SOLID FILL — which is the one ground use the token itself
   permits: --accent is documented "display ≥24px + solid fills only". A band
   closing the page is a fill, not a panel, so this does not reopen the deleted
   --accent-panel or the copper-slab drift it was removed with.

   THE INK IS WHITE HERE, and it is the opposite choice from every other copper
   on the site for a measured reason: near-black on --accent is 3.67:1 and
   fails, white is 4.58:1 and passes. The lighter coppers carry dark ink because
   there the ratio runs the other way. Neither is a house style — both are the
   side of the threshold that colour lands on.

   No hairline: it existed to separate a dark footer from a dark page and the
   colour now does that. No top margin: a band that closes the page sits against
   what it closes. */
.site-footer {
  padding-block: var(--space-7) var(--space-8);
  background: var(--accent);
  color: var(--ink);                   /* 4.58:1 on --accent */
  font-size: var(--text-xs);
}

/* Baseline, not the browser's default. The two runs are different heights —
   one wraps, one does not — and with alignment unset they lined up on their
   bottoms by accident: 889 and 913 from the top of the band, 24px apart and
   nobody's decision. Baseline puts them on the line they are read from. */
.site-footer__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-4) var(--space-6);
  justify-content: space-between;
}

/* The pair from the summary panel, at the sizes it uses: the credit takes the
   label voice — Oswald, caps, 14 — and the copyright line takes the value
   voice, Inter Tight at the body size. Two runs of legal text set identically
   is what made this row read as one undifferentiated strip. */
/* --text-xs is 14px and that is the floor, not a preference: this line carries
   the copyright and the only route to Sitemap and Privacy, so it is text a
   visitor may have to read. 12px was asked for and 12px is where a label stops
   being information and becomes decoration — the step down is real, it just
   stops at the last size that is still legible. */
.site-footer__legal {
  font-size: var(--text-xs);
}

.site-footer__credit {
  font-family: var(--font-display);
  font-size: var(--text-xs);
  font-weight: var(--weight-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}

/* --ink-quiet was right on the dark ground and is 3.53:1 on copper — under the
   floor. On this fill a link takes full ink, and it is underlined at rest
   rather than on hover: inside a run of sentence text, colour alone is not an
   affordance, and here there is no colour difference left to rely on. The
   anti-pattern this looks like — underlined links — is about NAVIGATION;
   a link inside a paragraph is the case underlines exist for. */
.site-footer a {
  color: var(--ink);                   /* 4.58:1 on --accent */
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-thickness: 1px;
}
.site-footer a:hover,
.site-footer a:focus-visible { text-decoration-thickness: 2px; }


/* =========================================================================
   15b. SEARCH RESULTS (inventory.html)

   The list IS the page, so everything else is apparatus and is built to stay
   subordinate: the facets are a quiet rail on the page's own ground, and the
   results bar is a single hairline-separated row. The Figma floats the whole
   listing over a photograph of the dealership and puts the facets in a cream
   panel — both are dropped. Twelve vehicle photographs already carry the
   depth, and a thirteenth photograph behind them destroys the figure-ground
   the buyer is scanning; a bone panel would be the only surface of its kind
   on the page and would out-rank the cars it is there to filter.
   ========================================================================= */

/* The panel has to clear the fold, and the fold is arithmetic: header, page
   head, section interval, then the panel itself. Every one of those four was
   costing more than it earned on a 900px window, so the two intervals this
   page controls are the shortest the rhythm allows. */
.srp { display: grid; gap: var(--space-8); }
/* A grid item defaults to min-width:auto and refuses to shrink below its
   content, which is how a card grid pushes its own column wider instead of
   reflowing. Named here rather than left implicit — the class existed in the
   markup with no rule behind it. */
.srp__results { min-inline-size: 0; }

@media (min-width: 62rem) {
  .srp {
    /* The rail is fixed and the results take the rest: a percentage column
       would grow the facets on a wide screen, and a facet panel does not get
       better by being wider — it just takes room from the subject. */
    grid-template-columns: 17rem minmax(0, 1fr);
    gap: var(--space-7) var(--space-8);
    align-items: start;
  }
  /* Sticky, because the list is long and a filter you have to scroll back up
     to reach is a filter nobody uses twice. */

  /* Sticky only while it fits. Opened, the advanced panel can be taller than
     the window, and a pinned block taller than the viewport has a bottom
     nobody can reach — the filters you just opened would be the ones you
     cannot see. */
  .srp__facets:has(#advanced[open]) { position: static; }

  .srp__facets {
    position: sticky;
    inset-block-start: calc(var(--header-h) + var(--space-5));
    /* No max-height and no inner scroll: the panel is sized to fit the fold,
       so a scrollbar here would mean the sizing failed rather than that the
       content is long. */
    /* The scrollbar is real — the list is genuinely longer than a short window
       — so it is styled rather than hidden: a scrollable region with no
       scrollbar hides the fact that there is more below. Drawn in the page's
       own hairline, and the fields are padded clear of it so it stops cutting
       the right edge off the "To" input. */
    scrollbar-width: thin;
    scrollbar-color: var(--rule-strong) transparent;
    padding-inline-end: var(--space-4);
  }
  .srp__facets::-webkit-scrollbar { inline-size: 6px; }
  .srp__facets::-webkit-scrollbar-thumb {
    background: var(--rule-strong);
    border-radius: var(--radius-pill);
  }
}

/* ---- the lit entrance ---------------------------------------------------
   The same warm source the home page carries on #about and #gallery: copper at
   a tenth of its strength, which is a light rather than a colour — it never
   rises far enough to become a surface. Here it is bounded to the top of the
   page on purpose. Twelve photographs are the subject, and a glow that ran the
   full length of the list would sit behind every one of them and tint the cars;
   this one lights the way in and is gone before the first row.

   The bound is the background-size, not a gradient stop: percentages in a
   radial resolve against the positioning area, so declaring that area 1200px
   tall puts the whole falloff inside the first screen and leaves the rest of
   the page on flat ground. */
.lit-top {
  background:
    radial-gradient(54% 34% at 10% 2%,
      rgb(var(--accent-rgb) / 0.15) 0%,
      rgb(var(--accent-rgb) / 0.05) 46%,
      rgb(var(--bg-rgb) / 0) 74%),
    radial-gradient(44% 28% at 94% 16%,
      rgb(var(--accent-rgb) / 0.11) 0%,
      rgb(var(--accent-rgb) / 0.04) 44%,
      rgb(var(--bg-rgb) / 0) 72%),
    var(--bg);
  background-repeat: no-repeat;
  background-size: 100% 1200px, 100% 1200px, auto;
}

/* ---- page head ----------------------------------------------------------
   Headline and sort share a line. Sort is a statement about the whole list
   rather than about the rows beneath it, so it belongs beside the thing it
   sorts — and the row it used to occupy above the grid is a row the cars get
   back. Baseline-aligned to the headline, not centred: two masses of very
   different height centre-aligned read as accidentally adjacent. */
.srp-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-4) var(--space-6);
  padding-block-end: var(--space-4);
  border-block-end: 1px solid var(--rule);
}

.srp-head__sort { display: flex; align-items: center; gap: var(--space-3); }
.srp-head__sort .field__label { white-space: nowrap; color: var(--ink-muted); }
.srp-head__sort .control { inline-size: auto; min-inline-size: 14rem; }

/* The select sits on the baseline of the headline's last line, which the flex
   end-alignment gives it — but the headline carries display leading and the
   control does not, so it needs a nudge to look level rather than to be it. */
@media (min-width: 62rem) {
  .srp-head__sort { padding-block-end: var(--space-2); }
}

/* ---- the card grid ------------------------------------------------------
   Not the rail. A rail is for a sample of the inventory beside other content;
   this page IS the inventory, so it gets a grid that grows downward and can
   be read row by row. auto-fill rather than a fixed count, so the card keeps
   its designed width instead of stretching to whatever the column allows. */
/* Three across, declared rather than derived. auto-fill gave three at 1440 and
   four at 1600, so the same page had two different card widths depending on the
   window — and the card was drawn at one. Three is also what lets the title,
   the price and the metadata sit on the lines they were designed for. */
.results {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  /* Rows and columns are NOT the same interval here. The status disc hangs
     roughly a third of its diameter above the card — about 32px — so a 32px
     row gap puts the second row's badges straight onto the first row's feet.
     The row interval is the named block distance, which leaves clear air
     above every badge; the column interval stays tight because nothing
     overhangs sideways. */
  column-gap: var(--space-6);
  row-gap: var(--space-block);
  margin-block-start: var(--space-6);
  /* Same reason as the rail: the status disc overhangs the top of a card and
     the hover shadow falls past its foot, and neither may be clipped. */
  padding-block: var(--space-6) var(--space-8);
}
.results .vehicle { inline-size: auto; }

/* Three needs room. Below the two-column breakpoint the rail column is gone,
   so the results have the full width and step down by content rather than by
   an arbitrary count. */
@media (max-width: 74.99rem) {
  .results { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 47.99rem) {
  .results { grid-template-columns: minmax(0, 1fr); }
}

/* Nothing matched. A results page that goes silently blank reads as broken,
   so the empty state says what happened and offers the way back. */
.results-empty {
  grid-column: 1 / -1;
  padding: var(--space-9) var(--space-6);
  border: 1px solid var(--rule);
  border-radius: var(--radius-3);
  text-align: center;
}
.results-empty__title {
  font-family: var(--font-display);
  font-size: var(--text-h3);
  font-weight: var(--weight-heading);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  color: var(--ink);
}
.results-empty__body {
  margin-block-start: var(--space-text);
  margin-inline: auto;
  max-inline-size: 44ch;
  color: var(--ink-muted);
}
.results-empty .btn-hmc { margin-block-start: var(--gap-cta-text); }

/* ---- My Collection hub --------------------------------------------------
   Same results grid as the SRP. Empty state reuses .results-empty. */
.collection .results {
  margin-block-start: 0;
}
.collection .results-empty[hidden],
.collection .results[hidden] {
  display: none;
}

/* ---- pager --------------------------------------------------------------
   The count is repeated here because by this point the top of the page is
   thousands of pixels away, and "Load more" with no idea how much more is a
   control that cannot be judged. */
.pager { text-align: center; }
.pager__state {
  margin-block-end: var(--space-text-obj);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}


/* =========================================================================
   15c. FORM CONTROLS

   The first real controls in the system. They are built as a set rather than
   per-page: an input invented inside one screen is how a second visual
   language gets in. Every one clears the 44px target floor and shows focus
   with the same ring as every other control on the site.
   ========================================================================= */

/* THE PANEL IS BONE, as the Figma has it. It is the one light field on the
   listing, and it is what makes the search read as an instrument sitting on
   the page rather than as more page. Measured on #f2efe9: --ink-inverse
   14.66:1, --slate 6.33:1 (the Find button), white input fields 16.83:1 with
   ink. --accent-ink is 2.74:1 here and is therefore banned inside this panel —
   which is why the link below the button is ink, not copper.

   Everything inside inverts together. A light island with dark-ground rules
   and dark-ground ink is how a surface ends up half-converted. */
.facets {
  /* Compact on purpose. This is an instrument, not a piece of editorial: the
     air that makes a section read as considered makes a control panel read as
     unfinished, and every pixel it takes is a pixel the cars do not get. One
     step down on every interval inside it — padding, group, field and action —
     rather than one dramatic cut in one place, so the internal hierarchy the
     panel had is the hierarchy it keeps. */
  padding: var(--space-5);
  border-radius: var(--radius-3);
  background: var(--bg-card);
  color: var(--ink-inverse);
}
@media (min-width: 62rem) {
  .facets { padding-block: var(--space-4); }
}



/* A fieldset is the right element — it is what groups controls under one label
   for a screen reader — but it arrives with a UA border on all four sides and a
   legend notched into it. Both are stripped: the grouping is carried by the one
   rule underneath and by the interval, like every other group on this site. */
/* The panel had a "Filter by" rubric over an "Availability" rubric — two
   labels stacked with nothing between them, and neither is in the Figma. Both
   are gone from the drawing; the Availability legend stays in the markup as
   .sr-only, because a fieldset without a legend stops grouping its checkboxes
   and the three switches become three orphans to a screen reader. */
.facet {
  min-inline-size: 0;   /* fieldset's UA min-width refuses to shrink in a grid */
  margin: 0;
  /* Asymmetric on purpose: the space ABOVE a legend separates it from the
     previous group's rule and has to be the largest interval in the panel;
     the space below the group's last control only has to reach the next
     rule. */
  padding-block: var(--space-5) var(--space-4);
  padding-inline: 0;
  border: 0;
  border-block-end: 1px solid var(--rule-inverse);
}
.facet:first-of-type { padding-block-start: 0; }
.facet:last-of-type { border-block-end: 0; }

/* A GROUP HEADING: bold caps at the label size, tracked at --track-caps.
   Uppercase needs its counters opened or they jam, but only one notch —
   --track-label at 0.12em is the eyebrow's treatment and it spread "VEHICLE"
   across half the panel while announcing louder than the control beneath it.
   0.03em is the difference between caps that hold together and caps that
   shout.

   The FIELD captions under it stay sentence case deliberately. Give a caption
   the same treatment as the heading above it and the heading stops out-ranking
   the fields it contains — the group loses its head. */
/* A <legend> is not laid out like anything else in CSS: the UA lifts it onto
   the fieldset's top border and its own padding-block-start applies BELOW it.
   That is why the group separation measured 0 above the word and 32 below it —
   the interval I set was landing on the wrong side of the label. Floating it
   full-width puts it back in normal flow, where the sequence can be stated;
   the element after it clears. */
.facet__legend {
  float: inline-start;
  inline-size: 100%;
  padding: 0;
  font-size: var(--text-xs);
  font-weight: var(--weight-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  line-height: var(--lh-tight);
  color: var(--ink-inverse);
}
/* Label to control is the tight interval; facet to facet is the loose one, so
   a group reads as a group before it reads as a list of fields. */
/* THE SEQUENCE, and it is read as proximity rather than chosen as numbers.
   Each step must be visibly smaller than the one outside it, or an element
   ends up nearer to something it does not belong to:

     label → its own control      4    they are one unit
     legend → the control it names 8   ALSO one unit: "Year" and the slider it
                                       labels are the same thing, so the legend
                                       sits nearer to it than the slider sits
                                       to anything else
     control → next control      16    two controls in one group
     group → group (over a rule) 24    the rule is the boundary, and the
                                       legend must never sit against it

   The last one is the one that was wrong: at 4px above and 4px below, a
   legend belonged to neither the rule above it nor the control under it, and
   the hairline read as an underline for the word rather than as the edge
   between two groups. */
/* The interval goes on the LEGEND, not on the element after it. Clearance
   places a cleared box below the float's margin box and consumes that box's
   own top margin — so a margin-block-start on the sibling measured zero. */
.facet__legend { margin-block-end: var(--space-2); }
.facet__legend + * { clear: both; }

.field { display: grid; gap: var(--space-1); }
/* Field to field inside one group is the tight interval; group to group is the
   loose one, and the rule between them carries the separation — so stacked
   fields do not need the distance a new group does.

   DIRECT children only. Written as a descendant selector it also caught the
   From/To pair inside .field-pair, which is a row, and pushed "To" 16px below
   "From" — a pair that no longer reads as a pair. */
.facet > .field + .field { margin-block-start: var(--space-4); }
.field__label {
  font-size: var(--text-xs);
  /* Stated, not inherited. This caption sits inside three different type
     contexts across the site and the one thing it must not do is pick up a
     rubric's tracking on its way down. */
  letter-spacing: var(--track-body);
  /* The box hugs the word. Left at reading leading, a 14px caption sat on
     16px of line box and the 4px gap under it measured 4 and read as 12 —
     which is why the label looked detached from the control it names. */
  line-height: var(--lh-tight);
  color: var(--ink-muted);
}
/* Inside the bone panel every ink flips. --ink-muted is 4.32:1 on bone and
   fails, so the label takes full ink and is separated from its value by size,
   weight and register instead of by tone. */
:is(.facets, .vdp-panel, .contact-form, .offer) .field__label { color: var(--ink-inverse); }

/* The same flip for the three text components that also appear on bone. --ink
   is white and disappears here; --ink-muted measures 4.32:1 on #f2efe9 and
   fails. Both take full ink, and their ranks are carried by size, weight and
   register instead of by tone — the same answer the label above takes, for the
   same reason. Written as one rule because a panel that inverts three of its
   four inks is the half-converted surface the .facets note warns about. */
:is(.facets, .vdp-panel, .contact-form, .offer) :is(.lead, .body-muted, .section-sub) {
  color: var(--ink-inverse);
}

/* The shared control surface. One ground, one border, one focus behaviour —
   declared once so a select and a text input cannot drift apart. */
.control {
  inline-size: 100%;
  min-block-size: 44px;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-2);
  background: rgb(var(--ink-rgb) / 0.04);
  color: var(--ink);
  font-size: var(--text-body);
  transition: border-color var(--dur-1) var(--ease-out),
              background-color var(--dur-1) var(--ease-out);
}
.control::placeholder { color: var(--ink-muted); opacity: 1; }

/* On bone the control is a white field with a soft edge — the Figma's own
   treatment, and the only way an input reads as an input on a light card.

   THE SCOPE IS THE SURFACE, not one component's name. Written as `.facets` and
   again as `.summary`, the second copy went dead the moment that block was
   renamed, and the configurator's whole enquiry form fell back to the dark-
   ground rules: white ink and a 4%-white field, both invisible on cream. Any
   bone surface joins this list; nothing else needs to know. */
:is(.facets, .vdp-panel, .contact-form, .offer) .control {
  background: var(--bg-card-plain);
  border-color: var(--rule-inverse-strong);
  color: var(--ink-inverse);
}
:is(.facets, .vdp-panel, .contact-form, .offer) .control::placeholder { color: var(--ink-card); }   /* 4.95:1 on white */
:is(.facets, .vdp-panel, .contact-form, .offer) .control:hover { border-color: var(--ink-inverse); }
:is(.facets, .vdp-panel, .contact-form, .offer) .control:focus-visible {
  outline-color: var(--ink-inverse);
  border-color: var(--ink-inverse);
}
:is(.facets, .vdp-panel, .contact-form, .offer) select.control option { background: var(--bg-card-plain); color: var(--ink-inverse); }
:is(.facets, .vdp-panel, .contact-form, .offer) .select::after { background: var(--ink-inverse); }
.facets .range { --rail: var(--rule-inverse-strong); --knob-ink: var(--ink-inverse); }
.control:hover { border-color: var(--ink); }
.control:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-color: var(--focus);
}

/* A select carries a UA arrow that cannot be styled and does not match the
   page, so it is removed and redrawn on a wrapper. Not as an SVG data URI:
   var() does not resolve inside url(), so the icon would have to carry its
   colour as a literal hex — the one raw value this file may not hold. It is
   the same clip-path chevron the facets disclosure uses, in the same ink, so
   the page has one caret rather than two that nearly match. */
select.control {
  appearance: none;
  padding-inline-end: var(--space-8);
}
.select { position: relative; display: block; }
.select::after {
  content: "";
  position: absolute;
  inset-inline-end: var(--space-4);
  inset-block-start: 50%;
  translate: 0 -50%;
  inline-size: 14px;
  block-size: 14px;
  background: var(--ink-muted);
  clip-path: polygon(50% 72%, 6% 26%, 18% 14%, 50% 46%, 82% 14%, 94% 26%);
  pointer-events: none;   /* the caret must not eat the click that opens the list */
}
/* The list itself is drawn by the OS and inherits nothing, so its two colours
   are set explicitly — otherwise an open dropdown is white-on-white. */
select.control option { background: var(--bg-raised); color: var(--ink); }

/* ---- range slider -------------------------------------------------------
   The Figma draws a two-handle track above the From/To fields, and it was
   missing from the first build. Built as TWO native range inputs stacked on
   one track rather than as a scripted widget: each handle is then a real
   slider with its own label, operable from the keyboard and announced with
   its value, which no div-and-mousemove version manages. The pair writes into
   the From/To fields, and the fields write back — one value, two ways in. */
.range {
  position: relative;
  block-size: 24px;
  margin-block-end: var(--space-1);
  /* Ground-agnostic. Written straight against the bone panel's inks the whole
     control vanished on the dark stage — a component that only works on one
     surface is a trap for whoever builds the next page with it. These two are
     the dark-ground defaults; the panel overrides them. */
  --rail: var(--rule-strong);
  --knob-ink: var(--ink);
  /* The two dimensions everything else here is derived from, declared rather
     than guessed: the target box and the handle drawn inside it. */
  --range-target: 44px;
  --range-thumb: 22px;
}
/* The rail and the From/To row are two controls in one group, so they take the
   control-to-control interval. At 4px the slider sat nearer to the caption of
   the next control than to the legend of its own group. */
.facet .range { margin-block-end: var(--space-4); }
/* The 44px targets overhang the 24px rail, which is intended — but they must
   not swallow the clicks of whatever sits above and below them. */
.range input { pointer-events: none; }
/* The rail both handles run on. */
/* The rail spans the handles' TRAVEL, not the box. A native thumb can never
   reach closer than half its own width to either end, so a full-width rail
   leaves 11px of track sticking out past where any handle can sit — which
   reads as a control that does not go all the way. */
.range::before {
  content: "";
  position: absolute;
  inset-inline: calc(var(--range-thumb) / 2);
  inset-block-start: 50%;
  translate: 0 -50%;
  block-size: 4px;
  border-radius: var(--radius-pill);
  background: var(--rail);
}
.range input {
  position: absolute;
  inset-inline: 0;
  /* The DRAWING is the thin rail above; the TARGET is 44px, centred on it.
     Left at the rail's own height the two handles were 28px tall — the one
     control in the system under the floor the system states, and the floor is
     not a look, it is whether a finger lands on the thing it aimed at. */
  inset-block-start: 50%;
  translate: 0 -50%;
  inline-size: 100%;
  block-size: var(--range-target);
  margin: 0;
  padding: 0;
  background: none;
  appearance: none;
  /* The two inputs are stacked, so the upper one would swallow every click
     meant for the lower. Only the handles take the pointer. */
  pointer-events: none;
}
.range input::-webkit-slider-thumb {
  appearance: none;
  pointer-events: auto;
  inline-size: var(--range-thumb);
  block-size: var(--range-thumb);
  /* WebKit hangs the thumb from the TOP of the runnable track rather than
     centring it on the track, so on a 44px target the 22px handle sat with its
     own top on the rail and its upper half above the box. The offset is the
     arithmetic, not a nudge. */
  margin-block-start: calc((var(--range-target) - var(--range-thumb)) / 2);
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--knob-ink);
  cursor: grab;
}
/* Firefox centres its thumb on the track already, so it takes no offset. */
.range input::-moz-range-thumb {
  pointer-events: auto;
  inline-size: var(--range-thumb);
  block-size: var(--range-thumb);
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--knob-ink);
  cursor: grab;
}
.range input:focus-visible::-webkit-slider-thumb { outline: 2px solid var(--knob-ink); outline-offset: 2px; }
.range input:focus-visible::-moz-range-thumb { outline: 2px solid var(--knob-ink); outline-offset: 2px; }
/* Both tracks are transparent — the rail above is the only one drawn, so the
   two inputs cannot paint two rails on top of each other. */
.range input::-webkit-slider-runnable-track { background: none; block-size: var(--range-target); }
.range input::-moz-range-track { background: none; block-size: var(--range-target); }

.field-pair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }
@media (max-width: 30rem) {
  /* Two 44px controls side by side stop being usable well before the layout
     stops fitting them. */
  .field-pair { grid-template-columns: 1fr; }
}

/* A textarea is the one control that must not be a fixed height: it is sized
   by rows and then left alone, because a box that grows under the pointer
   moves everything below it while someone is typing into it. */
.control--area {
  min-block-size: 0;
  padding-block: var(--space-3);
  line-height: var(--lh-body);
  resize: vertical;
}

.enquiry__legal {
  margin-block-start: var(--space-4);
  font-size: var(--text-xs);
  color: var(--ink-muted);
}

/* Switch. Built from a real checkbox so it is keyboard-operable and announced
   as a checkbox without a single ARIA attribute — the state lives in the
   element, not in a class a script has to remember to keep in sync. */
.switch-row { display: grid; gap: var(--space-3); }

.switch {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-block-size: 44px;
  cursor: pointer;
}
.switch input {
  position: absolute;
  opacity: 0;
  inline-size: 1px;
  block-size: 1px;
}
.switch__track {
  position: relative;
  flex: 0 0 auto;
  inline-size: 40px;
  block-size: 22px;
  border-radius: var(--radius-pill);
  background: var(--bg-control);
  /* The boundary token that exists for exactly this ground: 3.78:1 on
     --bg-control, which is the floor at which a UI edge is still an edge. */
  border: 1px solid var(--rule-control);
  --knob: var(--ink);
  transition: background-color var(--dur-2) var(--ease-out),
              border-color var(--dur-2) var(--ease-out);
}
.switch__track::after {
  content: "";
  position: absolute;
  inset-block-start: 2px;
  inset-inline-start: 2px;
  inline-size: 16px;
  block-size: 16px;
  border-radius: var(--radius-pill);
  background: var(--knob);
  transition: translate var(--dur-2) var(--ease-out);
}
.switch input:checked + .switch__track {
  background: var(--slate);
  border-color: var(--slate-lift);
}
.switch input:checked + .switch__track::after { translate: 18px 0; }
.switch input:focus-visible + .switch__track {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}
.switch__label { font-size: var(--text-body); color: var(--ink); }

/* Off, on bone, is a pale grey track with a white knob — the Figma's state.
   On is slate, which is the same slate the secondary button uses, so "on"
   means the same thing everywhere on the site. */
.facets .switch__track { background: var(--rule-inverse-strong); border-color: transparent; }
.facets .switch input:checked + .switch__track { background: var(--slate); border-color: var(--slate); }
.facets .switch__label { color: var(--ink-inverse); }

/* The three availability switches sit in a row with the word above the
   control, as the Figma has them — a label beside a 40px track wastes the
   width a three-up row needs. */
.switch-row--inline {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-3);
}
.switch-row--inline .switch {
  display: grid;                 /* grid, not column-flex: the word and the
                                    control get their own rows, so a label that
                                    wraps cannot push its own switch out of
                                    line with the two beside it. */
  grid-template-rows: auto 1fr;
  align-items: start;
  justify-items: start;
  gap: var(--space-2);
  min-block-size: 0;
  padding-block: var(--space-2);
}
.switch-row--inline .switch__label {
  font-size: var(--text-sm);
  order: -1;   /* the word first, the control under it */
}

/* One switch, not a row. Deliberately shaped unlike .switch-row--inline: the
   panel already opens with three switches in a row, and a second row of three
   further down read as the same control asked twice — which is what it very
   nearly was. */
.switch--single { gap: var(--space-3); }
.switch--single .switch__label { font-size: var(--text-body); }

@media (prefers-reduced-motion: reduce) {
  .switch__track,
  .switch__track::after { transition: none; }
}

/* The panel has no apply button — the filters take effect as they are set —
   so the only thing below the last group is Clear, and it is hidden until
   there is something to clear. Its own margin carries the interval, because a
   wrapper around one sometimes-hidden child leaves its margin behind when the
   child goes. */
.facets__clear[hidden] { display: none; }

/* Clearing filters is a real action but not the primary one, so it is a link
   rather than a third button competing with Find. It only appears once
   something is actually filtered — a Clear that clears nothing is a lie. */
.facets__clear {
  display: block;
  inline-size: 100%;
  margin-block-start: var(--space-4);
  min-block-size: 44px;
  padding: var(--space-2);
  background: none;
  border: 0;
  color: var(--ink-inverse);
  font-size: var(--text-sm);
  text-decoration: underline;
  text-underline-offset: 0.25em;
  cursor: pointer;
}
.facets__clear:hover { color: var(--ink-inverse); text-decoration-thickness: 2px; }

/* "Advanced Search" — in the Figma and dropped from the first build. It is a
   link, under the button, in ink rather than copper: copper is 2.74:1 on this
   ground and illegal at any size. */
/* The disclosure that holds the advanced filters. It carries no border and no
   ground of its own — inside the panel it is a continuation of the same
   instrument, not a second panel bolted underneath it. */
.advanced { margin-block-start: var(--space-2); }
.advanced > summary { list-style: none; }
.advanced > summary::-webkit-details-marker { display: none; }
.advanced .facet:first-of-type { padding-block-start: var(--space-4); }

.facets__advanced {
  display: block;
  cursor: pointer;
  margin-block-start: var(--space-2);
  min-block-size: 44px;
  padding-block: var(--space-3);
  box-sizing: border-box;
  text-align: center;
  color: var(--ink-inverse);
  font-size: var(--text-sm);
  text-decoration: underline;
  text-underline-offset: 0.25em;
}
.facets__advanced:hover { text-decoration-thickness: 2px; }
.facets__advanced:focus-visible { outline: 2px solid var(--ink-inverse); outline-offset: 2px; }
/* State is carried by a caret, not by changing the word — the same rotating
   chevron the phone disclosure uses, so one gesture means one thing across the
   site. The underline stays on the words alone; a rule running under the caret
   would read as part of the glyph. */
.facets__advanced { display: flex; align-items: center; justify-content: center; gap: var(--space-2); text-decoration: none; }
.facets__advanced > span { text-decoration: underline; text-underline-offset: 0.25em; }
.facets__advanced::after {
  content: "";
  inline-size: 14px;
  block-size: 14px;
  background: currentColor;
  clip-path: polygon(50% 72%, 6% 26%, 18% 14%, 50% 46%, 82% 14%, 94% 26%);
  transition: rotate var(--dur-2) var(--ease-out);
}
.advanced[open] > .facets__advanced::after { rotate: 180deg; }
@media (prefers-reduced-motion: reduce) {
  .facets__advanced::after { transition: none; }
}

/* On one column the facets stop being a rail and become a disclosure, so the
   list is not pushed a screen and a half down the page. */
@media (max-width: 61.99rem) {
  .facets { padding: 0; }
  .facets > summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    min-block-size: 44px;
    padding: var(--space-4) var(--space-5);
    cursor: pointer;
    font-family: var(--font-display);
    font-size: var(--text-lead);
    text-transform: uppercase;
    letter-spacing: var(--track-caps);
    color: var(--ink-inverse);
  }
  .facets > summary::-webkit-details-marker { display: none; }
  .facets > summary::after {
    content: "";
    inline-size: 18px;
    block-size: 18px;
    background: currentColor;
    clip-path: polygon(50% 72%, 6% 26%, 18% 14%, 50% 46%, 82% 14%, 94% 26%);
    transition: rotate var(--dur-2) var(--ease-out);
  }
  .facets[open] > summary::after { rotate: 180deg; }
  .facets__inner { padding: 0 var(--space-5) var(--space-5); }
}

@media (min-width: 62rem) {
  /* The disclosure is a phone affordance only: on the rail everything is
     already visible, so the summary would be a control that does nothing. */
  .facets > summary { display: none; }
}


/* ---- fitting the fold ---------------------------------------------------
   The panel must clear the fold, and the fold is arithmetic: header, page
   head, section interval, panel. These two tiers are placed LAST in the file
   on purpose — written earlier they lost to the base rules they exist to
   override, and every measurement came back unchanged.
   ------------------------------------------------------------------------- */
@media (min-width: 62rem) {
  /* SHORT WINDOWS. The panel must clear the fold on a 768px laptop too, and
     the answer is not an inner scrollbar — a filter you have to scroll to see
     is a filter half the visitors never learn is there. It tightens instead:
     the From/To captions go (the slider and the placeholders already say what
     the two fields are), and every remaining interval takes its floor value.
     The threshold is 928px rather than the 768px laptop it was written for:
     at 900px the panel cleared the fold by two pixels, and two pixels is not
     clearance, it is a coincidence waiting for a longer word. */
  @media (max-height: 58rem) {
    .facets { padding-block: var(--space-3); }
    /* The top interval holds at every height. It is the one that carries the
       grouping, and a panel that fits by destroying its own grouping has not
       fitted, it has collapsed. */
    .facet { padding-block: var(--space-5) var(--space-2); }
    /* The captions go, not the fields. A select whose first option reads "All
       makes" is already telling you what it filters, and "From"/"To" is already
       said by the slider they sit under — so the words that survive are the
       ones carrying information the control does not. They stay in the markup
       as .sr-only, so nothing is lost to a screen reader. */
    .field-pair .field__label,
    .facet > .field > .field__label {
      position: absolute;
      inline-size: 1px;
      block-size: 1px;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
    }
    .facets__clear { margin-block-start: var(--space-3); }
    .facets__advanced { margin-block-start: var(--space-1); padding-block: var(--space-2); }
    .range { block-size: 20px; margin-block-end: 0; }
  }

  /* A 1366x768 laptop is the floor this has to clear, and at that height the
     page head itself is what pushes the panel past the fold. The opening
     intervals take their shortest value rather than the panel being crushed
     further — a filter nobody can read is not a filter that fits. */
  @media (max-height: 50rem) {
    .section--below-header { padding-block-start: calc(var(--header-h) + var(--space-2)); }
    .section--tight-top { padding-block-start: var(--space-2); }
    /* The head's own rule can sit closer to the row above it than a filter
       group's rule can sit to its legend — one is a page divider, the other
       is what tells you which controls belong together. */
    .srp-head { padding-block-end: var(--space-2); }
    /* The eyebrow and the headline are one thought; on a short screen they can
       be read as one at the tighter interval too. */
    .stack > .eyebrow + * { margin-block-start: var(--space-2); }
    /* Tight, not crushed. The first pass took 110px more than the fold
       needed and the panel read as squeezed; these are the values that clear
       768px with room to spare and still look like a considered instrument. */
    .facets { padding-block: var(--space-3); }
    /* The top interval holds at every height. It is the one that carries the
       grouping, and a panel that fits by destroying its own grouping has not
       fitted, it has collapsed. */
    .facet { padding-block: var(--space-5) var(--space-2); }
    .facet__legend + * { margin-block-start: var(--space-1); }
    .facet > .field + .field { margin-block-start: var(--space-2); }
    .range { block-size: 20px; }
  }
}


/* =========================================================================
   15d. VEHICLE DETAIL (vehicle.html)

   One object, so the composition can afford a dominant: the photograph is the
   subject and the bone panel beside it is the one light field on the page,
   holding the two facts the visitor came for — what it is and what it costs.

   This is where --bg-card finally has a job. It was declared as "the vehicle
   card" and used by nothing; the card on the listing is dark, because a bone
   card among eleven others would out-rank the cars. Here there is one car, and
   out-ranking is the point.

   CONTRAST ON BONE, measured (#f2efe9):
     --ink-inverse  14.66:1  everything textual
     --slate         6.33:1  the secondary button
     --accent        3.99:1  LARGE type and rules only — never a label
     --accent-ink    2.74:1  banned here; it is the dark-ground copper
   Because no muted ink clears 4.5:1 on this surface, the label/value pair is
   separated by size, weight, register and tracking instead of by tone — four
   axes, which read as a decision where one reads as an accident.
   ========================================================================= */

/* The detail page runs narrower than the listing. One car and one column of
   reading do not need the full 1564 content width: at that width the frame's own
   proportion made the photograph nearly a metre tall and pushed the price panel
   a long way from it. 1280 keeps the image large without it becoming the page.

   Scoped to these three blocks, NOT to .wrap. Written as `.vdp-page .wrap` it
   also caught the contact band at the foot of the page, which is full-bleed by
   construction — the map is a place, and a place cropped into a content column
   reads as a picture of a place. */
/* WIDE AGAIN, and the width is derived rather than picked: the left column is
   the reading measure plus the surface's own padding, the right is the rail, and
   the page is the sum of the two with the gap. Narrowing one without the other
   is what produced first an empty third inside the panel and then 200px of
   nothing between the columns. */
.vdp-page .vdp,
.vdp-page .photos,
.vdp-page .vdp-actions { max-inline-size: 1300px; margin-inline: auto; }

.vdp { display: grid; gap: var(--space-6); }
.vdp__main { min-inline-size: 0; }
/* The reading column is capped where reading happens, not where the grid ends
   — the gallery above it is allowed the full width. */
.vdp__main > p,
.vdp__main > .enquiry { max-inline-size: var(--measure); }

@media (min-width: 62rem) {
  .vdp {
    /* The reading column IS the left column, and the rail is the right one.
       Capping the panel inside a wider column instead left 200px of nothing
       between the text and the rail — the hole moved, it did not close.

       The rail is a PROPORTION with a ceiling, not a fixed 30rem. Fixed, it
       took 480px of a 906px content box at 1024 and left the photograph 395 —
       the subject rendered smaller than its own caption on every laptop and
       every landscape tablet. The percentage is set so the clamp reaches its
       30rem cap exactly at this grid's own maximum (1300px here, capped above;
       the configurator's box is the full wrap, so its figure differs). Desktop
       is therefore unchanged and only the widths below it are repaired. */
    grid-template-columns: minmax(0, 49rem) clamp(20rem, 37%, 30rem);
    /* Two rows, named by what sits in them. The photograph and the price panel
       share the first: one grid row, one height, so the panel ends where the
       image ends instead of hanging past its foot. */
    gap: var(--space-6);
    align-items: start;
  }
  /* EXACT equality of the two columns cannot hold, and it is worth writing down
     why: the left one is sized by an aspect ratio and therefore by the window's
     WIDTH, the right one by how much text is in it. Tie them together and one of
     them stretches — either a hole above the buttons, or, with the slack
     distributed, a price floating in the middle of half a metre of nothing.

     So they are independent, and the photograph is capped instead. Bounded by
     the viewport height it stays near the panel's own length at every width
     rather than running away from it on a wide screen, which is what "aligned"
     actually has to mean here. */
  .vdp { grid-template-rows: auto auto; }
  .vdp > .stage     { grid-column: 1; grid-row: 1; }
  .vdp > .vdp-panel { grid-column: 2; grid-row: 1; }
  .vdp > .tabs      { grid-column: 1; grid-row: 2; }
  .vdp > .promos    { grid-column: 2; grid-row: 2; }
}

/* ---- lit from both sides ------------------------------------------------
   Two sources, one per edge, and neither is a circle. A radial with equal radii
   reads as a spotlight someone aimed; two overlapping ellipses at different
   sizes and slightly different centres read as light falling — which is the
   only kind of glow that belongs behind a photograph of a car.

   TWO TEMPERATURES, NOT ONE HUE. Both edges were copper, and measured on the
   page that made this wash the single largest copper surface on the site —
   14.6% of the right margin, running the full height of every page. With the
   ground warm, the secondary ink warm sand and the panel bone, there was
   nothing on the page for an accent to be an accent AGAINST: it read brown from
   edge to edge. Copper on one side and slate on the other is the pair the
   palette already declares as opposites, and two lights of different
   temperature is what a real room does anyway. Alphas are unchanged — the two
   luminances are 0.179 and 0.166, so neither side outweighs the other.

   Bounded to the top of the page by the positioning area, same as the listing:
   the gallery below is the subject and a glow running its full length would sit
   behind every photograph in it.
   ------------------------------------------------------------------------- */
.lit-sides {
  background:
    /* left, warm — the larger and lower of the two */
    radial-gradient(38% 30% at -8% 14%,
      rgb(var(--accent-rgb) / 0.17) 0%,
      rgb(var(--accent-rgb) / 0.06) 44%,
      rgb(var(--bg-rgb) / 0) 72%),
    radial-gradient(22% 46% at 4% 30%,
      rgb(var(--accent-rgb) / 0.10) 0%,
      rgb(var(--bg-rgb) / 0) 68%),
    /* right, cool — tighter and higher, so the two do not mirror each other */
    radial-gradient(30% 26% at 106% 6%,
      rgb(var(--slate-lift-rgb) / 0.15) 0%,
      rgb(var(--slate-lift-rgb) / 0.05) 42%,
      rgb(var(--bg-rgb) / 0) 70%),
    radial-gradient(18% 40% at 96% 22%,
      rgb(var(--slate-lift-rgb) / 0.09) 0%,
      rgb(var(--bg-rgb) / 0) 66%),

    /* TWO MORE, LOWER DOWN. The four above are bounded to the top 1500px, and
       this page runs past 4000 — so everything below the fold sat on flat
       ground with no light in it at all. These two are sized to the whole
       element rather than to a pixel band, so they hold their place whatever
       the page's length turns out to be.

       Neither is a circle and neither matches the other's proportion: one is
       wide and shallow across the middle, the other tall and narrow at the
       right, and their centres are off the axis of the pair above them. Equal
       radii read as a spotlight someone aimed; unequal ones at different
       heights read as light falling, which is the only kind that belongs behind
       a photograph of a car.

       Warm at the centre, cool at the right, continuing the temperature axis
       the top of the page already sets. Weaker than the upper four — 0.10 and
       0.08 — because there is content over these rather than open ground: at
       0.10 the composite lifts to #2d241c, where white still measures 15.2:1
       and the muted ink 6.5:1. */
    radial-gradient(50% 15% at 44% 52%,
      rgb(var(--accent-rgb) / 0.14) 0%,
      rgb(var(--accent-rgb) / 0.05) 48%,
      rgb(var(--bg-rgb) / 0) 78%),
    radial-gradient(19% 30% at 90% 70%,
      rgb(var(--slate-lift-rgb) / 0.12) 0%,
      rgb(var(--slate-lift-rgb) / 0.04) 46%,
      rgb(var(--bg-rgb) / 0) 74%),

    var(--bg);
  background-repeat: no-repeat;
  background-size:
    100% 1500px, 100% 1500px, 100% 1500px, 100% 1500px,
    100% 100%, 100% 100%,
    auto;
}

/* ---- action bar ---------------------------------------------------------
   Back first and leftmost: this page is reached from a list, and the way back
   to the list is the most-used control on any detail page. */
.vdp-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-block-end: var(--space-5);
}
.vdp-actions__spacer { flex: 1 1 auto; }
/* PILLS on this bar. .btn-card is built to share a card's action row three
   ways — it carries flex: 1 and a divider between siblings, which is right
   inside a card foot where the three sit in one bordered strip. Out here there
   is no strip for a divider to divide, so each one closes into its own pill
   and the dividers go. */
.vdp-actions .btn-card {
  flex: 0 0 auto;
  min-block-size: 44px;
  padding-inline: var(--space-5);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-pill);
  border-inline-start: 1px solid var(--rule-strong);   /* beats the divider rule */
}
.vdp-actions .btn-card:hover,
.vdp-actions .btn-card:focus-visible {
  border-color: var(--ink);
  background: rgb(var(--ink-rgb) / 0.06);
  color: var(--ink);
}
/* Saved reads as copper filled here too, so the state means the same thing it
   means on a card. */
.vdp-actions .btn-card[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--ink);                                   /* 4.58:1 on copper */
}

.btn-back {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-block-size: 44px;
  padding-inline: var(--space-5);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  text-decoration: none;
  transition: border-color var(--dur-1) var(--ease-out),
              background-color var(--dur-1) var(--ease-out);
}
.btn-back:hover { border-color: var(--ink); background: rgb(var(--ink-rgb) / 0.06); }
.btn-back__arrow {
  inline-size: 16px;
  block-size: 16px;
  background: currentColor;
  clip-path: polygon(45% 8%, 57% 20%, 33% 44%, 100% 44%, 100% 60%, 33% 60%, 57% 84%, 45% 96%, 0 52%);
}

/* ---- gallery stage ------------------------------------------------------ */
/* min-inline-size: 0 on every link in the chain. A grid and a flex item both
   default to min-width:auto, which refuses to shrink below their content — so
   a scrolling thumbnail strip pushes its own column wider instead of
   scrolling, and the whole page acquires a horizontal scrollbar. */
.stage { position: relative; min-inline-size: 0; }
.stage__frame {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-3);
  background: var(--bg-raised);
  /* The photographs are 1024x768, so 4:3 crops nothing.

     NO max-block-size here. With an aspect ratio AND a height cap, the cap wins
     on the block axis and the ratio then drags the INLINE axis down with it —
     the frame shrank to 725px inside an 1100px column and left a field of empty
     page beside it. The image is width-driven; the container column is what
     bounds it, and that is already capped at the page's own content width. */
  aspect-ratio: 4 / 3;
}
.stage__frame img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  display: block;
}

/* The arrows sit ON the photograph here — unlike the listing rail, where they
   sit outside it. On a single image there is no row for them to sit beside,
   and a control floated below the frame reads as belonging to the next block. */
.stage__nav {
  position: absolute;
  inset-block-start: 50%;
  translate: 0 -50%;
  z-index: 2;
}
.stage__nav--prev { inset-inline-start: var(--space-4); }
.stage__nav--next { inset-inline-end: var(--space-4); }

/* Position within the set, over the image's own dark foot. Twelve thumbnails
   below still leave "which one am I on" unanswered while the strip scrolls. */
.stage__index {
  position: absolute;
  inset-block-end: var(--space-4);
  inset-inline-end: var(--space-4);
  z-index: 2;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-pill);
  background: rgb(var(--bg-deep-rgb) / 0.72);
  backdrop-filter: blur(6px);
  font-size: var(--text-xs);
  color: var(--ink);
}

.thumbs {
  display: flex;
  min-inline-size: 0;
  gap: var(--space-3);
  margin-block-start: var(--space-3);
  overflow-x: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--rule-strong) transparent;
  padding-block-end: var(--space-2);
}
.thumb {
  flex: 0 0 auto;
  inline-size: 7rem;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-2);
  background: none;
  cursor: pointer;
  overflow: hidden;
  line-height: var(--lh-none);
  opacity: 0.55;
  transition: opacity var(--dur-2) var(--ease-out),
              border-color var(--dur-2) var(--ease-out);
}
.thumb img { inline-size: 100%; aspect-ratio: 16 / 10; object-fit: cover; display: block; }
.thumb:hover { opacity: 0.85; }
/* Selection is carried by BOTH opacity and a border. Opacity alone is a
   difference nobody can name when four thumbnails are already dim. */
.thumb[aria-current="true"] { opacity: 1; border-color: var(--slate-lift); }   /* 3.37:1 */

/* ---- the bone panel ----------------------------------------------------- */
.vdp-panel {
  padding: var(--space-5);
  border-radius: var(--radius-3);
  background: var(--bg-card);
  color: var(--ink-inverse);
}
@media (min-width: 62rem) {
  /* Its OWN length. Stretched to the row it matched the photograph exactly and
     bought that with a hole: the actions were pushed to the foot and everything
     between the last spec row and the buttons was empty panel. A card that ends
     where its content ends is worth more than two columns ending on one line. */

}

/* The name out-ranks nothing else on the page, but it must not be dwarfed by
   the number: at h3 against a 64px price the car read as a caption on its own
   price tag. One step up, one step down, and the pair now reads as a title
   with its value rather than a value with a footnote. */
.vdp-panel__title {
  font-family: var(--font-display);
  font-size: var(--text-h2);
  font-weight: var(--weight-heading);
  line-height: var(--lh-tight);
  letter-spacing: var(--track-heading);
  color: var(--ink-inverse);
}

/* Playfair, and this is the same role it was given on the card: the price.
   Near-black rather than copper — copper clears only 3.99:1 on bone, which is
   legal at this size but is 3.5× less contrast on the one number the whole
   page exists to state. */
.vdp-panel__price {
  margin-block-start: var(--space-text);
  font-family: var(--font-serif);
  font-size: var(--text-hero-2);
  font-weight: var(--weight-body);
  line-height: var(--lh-display);
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink-inverse);
}

/* A rank above the price, for a panel whose number needs saying what it is —
   the configurator's total means one thing while groups are unanswered and
   another once they are all in. The vehicle page's price needs no such caption,
   so the class exists but is not compulsory. */
.vdp-panel__eyebrow {
  margin-block-start: var(--space-4);
  font-size: var(--text-xs);
  font-weight: var(--weight-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-inverse);
}
.vdp-panel__eyebrow + .vdp-panel__price { margin-block-start: var(--space-2); }

.vdp-panel__note {
  margin-block-start: var(--space-4);
  font-size: var(--text-sm);
  color: var(--ink-card);              /* 4.95:1 on bone */
}

/* A second block inside the panel, so it opens at the block interval rather
   than at the paragraph one — it is a different subject, not the next sentence. */
.vdp-panel__foot { margin-block-start: var(--space-7); }

/* Slate. The bone panel is the largest single warm surface on either page, and
   a copper rule across it left the whole block in one temperature. Slate is the
   declared secondary — it sits opposite copper on the wheel — and it measures
   6.33:1 on bone, well past the 3:1 a rule has to clear. */
.vdp-panel__rule {
  block-size: 2px;
  margin-block: var(--space-4);
  background: var(--slate);
  border: 0;
}

/* Label and value: four axes apart, because tone cannot carry it here. */
.spec-pairs { display: grid; gap: var(--space-3); margin: 0; }

/* The same pairs with a price in a second column. An extension of the component
   the vehicle page already uses, not a second one built beside it. */
.spec-pairs__row--cost {
  grid-template-columns: minmax(0, 1fr) auto;
  padding-block-end: var(--space-3);
  border-block-end: 1px solid var(--rule-inverse);
}
.spec-pairs__row--cost dt { grid-column: 1; }
/* :not(), and it is load-bearing. Written as a plain `dd` this rule is 0,2,1 and
   quietly outranked .spec-pairs__cost at 0,1,0 — so the figure was dragged into
   column 1 and printed underneath its own label instead of beside it. Excluding
   the cost means the two rules cannot argue at all. */
.spec-pairs__row--cost dd:not(.spec-pairs__cost) { grid-column: 1; }
/* Beside the LABEL, not beside the value: the value is free text and can run to
   two lines, and a figure tied to it drops with it. On row 1 the prices hold one
   straight column down the panel however long the answers are. */
.spec-pairs__cost {
  grid-column: 2;
  grid-row: 1;
  justify-self: end;
  align-self: baseline;
  font-variant-numeric: lining-nums tabular-nums;
}
/* Not "$0.00". A zero is a price; an unanswered group has no price yet, and
   printing one in the cost column is a lie in the only column nobody may
   misread. */
.spec-pairs__row--empty dd { opacity: 0.55; font-style: italic; }

.vdp-panel__render {
  border-radius: var(--radius-2);
  overflow: hidden;
  background: var(--bg-raised);
  margin-block-end: var(--space-5);
}
/* The crop is measured off the subject, not chosen by eye. In the 1000×680
   source the car occupies y 99–404: 99px of empty above it and 275 below, so a
   16/11 box was mostly background AND hung the car high in its own frame.
   21/9 leaves the car ~62px of source margin top and bottom, and the 15% offset
   is what centres the CAR in the window rather than centring the file. */
.vdp-panel__render img {
  inline-size: 100%;
  aspect-ratio: 21 / 9;
  object-fit: cover;
  object-position: center 15%;
  display: block;
}
.spec-pairs__row { display: grid; gap: var(--space-1); }
/* The display voice, not the body one. Inter Tight stops at 600 and these
   labels needed to read heavier than the values beneath them; Oswald at the
   same weight is condensed, so it is denser on the line without being larger —
   which is the difference between a label with presence and a label shouting.
   The contact panel's labels already work this way. */
.spec-pairs dt {
  font-family: var(--font-display);
  font-size: var(--text-xs);
  font-weight: var(--weight-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-inverse);
}
.spec-pairs dd {
  margin: 0;
  font-size: var(--text-body);
  font-weight: var(--weight-body);
  color: var(--ink-inverse);
}
.spec-pairs dd.num-tabular { letter-spacing: var(--track-num); }

.vdp-panel__actions { display: grid; gap: var(--space-3); margin-block-start: var(--space-6); }

/* ---- jump nav -----------------------------------------------------------
   Not tabs. The Figma hides Description, Premium options, Photos, Videos, Buy
   Now and Contact behind six of them — which puts the car's own specification
   one click away from being seen at all, and out of reach of a search engine.
   These are links to sections that are all present, all scrollable, all
   indexable. */
.jump {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding-block: var(--space-4);
  border-block: 1px solid var(--rule);
}
.jump a {
  min-block-size: 44px;
  display: inline-flex;
  align-items: center;
  padding-inline: var(--space-4);
  border-radius: var(--radius-pill);
  color: var(--ink-muted);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  text-decoration: none;
  transition: color var(--dur-1) var(--ease-out),
              background-color var(--dur-1) var(--ease-out);
}
.jump a:hover { color: var(--ink); background: rgb(var(--ink-rgb) / 0.06); }

/* ---- tabs ---------------------------------------------------------------
   The Figma's own device, built so it costs nothing. Every panel is in the
   document; without JS the bar is hidden and all four render in order, so a
   search engine and a visitor with no script see the whole page. Script is
   what turns them into tabs — which is the only honest way to use a control
   that hides content.
   ------------------------------------------------------------------------- */
.tabs { margin-block-start: var(--space-6); }
/* Inside the grid the row gap already provides that interval, so the component's
   own margin pushed the tab bar 32px below the rail it is supposed to start
   level with. */
.vdp > .tabs { margin-block-start: 0; }

/* The bar holds the tablist and the jump link on one line: they read as one
   row of choices even though only five of the six switch a panel. */
.tabs__bar {
  display: flex;
  flex-wrap: wrap;
  /* END, not centre. The selected tab is taller than its neighbours by the
     overlap that joins it to the surface, so centring the bar lifted the jump
     link 9px above the row it belongs to. */
  align-items: flex-end;
  gap: var(--space-2);
}
.tabs__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.tabs__tab {
  min-block-size: 44px;
  /* Six labels have to hold one line in a 704px column. At the wider inline
     padding they came to 768px and the row broke, which puts one tab on a shelf
     of its own — a bar that wraps stops reading as a set of choices. */
  padding-inline: var(--space-4);
  border: 0;
  border-radius: var(--radius-2);
  background: transparent;
  /* Copper, as the Figma has the resting tabs — and --accent-ink rather than
     --accent, because at this size the darker copper is 3.67:1 and illegal. */
  color: var(--accent-ink);              /* 5.34:1 */
  font-family: var(--font-display);
  font-size: var(--text-sm);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  white-space: nowrap;                   /* the Figma's own tabs broke "Descript / ion" */
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease-out),
              color var(--dur-1) var(--ease-out);
}
.tabs__tab:hover { background: rgb(var(--ink-rgb) / 0.06); color: var(--ink); }

/* Selected is carried by ground AND ink, not by ground alone: a filled tab that
   only changes colour is invisible to anyone who cannot separate copper from
   white, and this is the control that decides what the page is showing. */
/* The selected tab and the panel are one shape. Its foot squares off and sits
   directly on the surface below with no gap, so the plate reads as growing out
   of the panel rather than floating above it. */
/* ONE SHAPE, and it is made of one colour rather than of matched geometry. The
   selected tab takes the surface's own ground, so the plate and the panel are
   literally the same field with a step in its top edge — which is what a tab is.
   In slate they were two objects touching, and no amount of radius matching was
   going to change that. 15.4:1 with the white label. */
.tabs__tab[aria-selected="true"] {
  background: var(--bg-raised);
  color: var(--ink);
  /* The outline runs over the top of the plate, down its sides and then away
     along the panel's own edge. There is no line UNDER it — that is the whole
     device: the tab is open into the panel, and the two are one enclosure with
     a step in its top edge. The -1px pulls the plate over the panel's border so
     its own ground covers that segment. */
  border: 1px solid var(--rule);
  border-block-end: 0;
  border-start-start-radius: var(--radius-3);
  border-start-end-radius: var(--radius-3);
  border-end-start-radius: 0;
  border-end-end-radius: 0;
  margin-block-end: -1px;
  position: relative;
  z-index: 1;
}
.tabs__bar { position: relative; z-index: 1; }
/* The surface keeps its corners. Squaring the top-left permanently was only
   correct while the FIRST tab was selected — choose any other and the plate sits
   in the middle of the bar while a cut corner stands on its own at the left, and
   the one shape falls apart. The corner is squared only when the plate is
   actually standing on it. */
/* The surface keeps all four corners now: the plate carries its own outline, so
   nothing has to be cut away to meet it. */

/* Looks like the tab beside it and behaves as a jump. It carries .tabs__tab,
   so there is one definition of what a tab looks like; this only removes the
   underline an anchor arrives with and states that it never becomes selected. */
/* An <a> is inline, so min-block-size on .tabs__tab never applied to it: its
   box came out shorter than the buttons beside it and the word sat higher in
   the row even with their feet level. */
.tabs__tab--jump {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}
.tabs__tab--jump:hover { background: rgb(var(--ink-rgb) / 0.06); color: var(--ink); }

/* A SURFACE under the panels, so the tab bar has something to sit on and the
   reading column stops floating on the page ground. Dark rather than the
   Figma's cream: the description runs to several hundred words and a light
   field that size in the middle of a dark page is a second hero. */
.tabs__panels {
  /* No cap. The column is the measure now, so the surface fills it — capping
     the surface inside a wider column is what put the hole between the columns
     instead of inside the panel. */
  margin-block-start: 0;
  padding: var(--space-6);
  border: 1px solid var(--rule);
  border-start-start-radius: 0;
  border-radius: var(--radius-3);
  background: var(--bg-raised);
}


.tabs__panel { padding-block-start: 0; }
/* An inline link in body copy had no colour rule and was falling through to the
   UA default — browser blue, in the middle of a warm near-black page. */
.tabs__panel a:not(.btn-hmc):not(.btn-card) {
  color: var(--accent-ink);            /* 5.34:1 */
  text-underline-offset: 0.2em;
}
.tabs__panel a:not(.btn-hmc):not(.btn-card):hover { color: var(--ink); }
/* The measure cap has to reach inside the panels. .vdp__main > p stops at the
   panel boundary, and a description tab is exactly where long copy lands. */
.tabs__panel > p,
.tabs__panel > .body-muted { max-inline-size: var(--measure); }
@media (min-width: 62rem) {
  /* 78ch, three above the norm's ceiling, and stated rather than slipped in:
     it is what lets the surface fill a column wide enough to sit against the
     rail without an empty strip down its right side. Beyond this the line stops
     being readable and the trade is no longer worth making. */
  .vdp-page .tabs__panel > p,
  .vdp-page .tabs__panel > .body-muted { max-inline-size: 78ch; }
}
/* An intro that introduces a full-width block is not body copy — capped at the
   reading measure it broke at 60 characters above a grid three times its width,
   and the two read as unrelated. */
.tabs__panel > .body-muted--intro { max-inline-size: 78ch; }
.tabs__panel > p + p { margin-block-start: var(--space-5); }
.tabs__panel:focus-visible { outline: 2px solid var(--focus); outline-offset: 4px; }

/* No script: no tabs. The bar would be a row of controls that do nothing, and
   the panels are already all visible, which is the correct fallback. */
.tabs__list { display: none; }
.js .tabs__list,
[data-ds-tabs] .tabs__list { display: flex; }
.tabs__bar { display: none; }
.js .tabs__bar { display: flex; }   /* the DS specimen has no panels */
.js .tabs__panel[hidden] { display: none; }

/* ---- buy summary --------------------------------------------------------
   A <dl> renders with the UA's own 40px indent on every dd, which is what was
   showing here: the markup shipped with class names and no rules behind them.
   Same failure as an unstyled <blockquote> earlier in this build — a class in
   the HTML is a promise, and the stylesheet is where it gets kept.
   ------------------------------------------------------------------------- */
.buy-summary { margin: 0; max-inline-size: var(--measure); }

.buy-summary__row {
  display: grid;
  grid-template-columns: minmax(0, 11rem) minmax(0, 1fr);
  gap: var(--space-4);
  align-items: baseline;
  padding-block: var(--space-4);
  border-block-end: 1px solid var(--rule);
}
.buy-summary__row dt {
  font-size: var(--text-xs);
  font-weight: var(--weight-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-muted);
}
.buy-summary__row dd {
  margin: 0;
  font-size: var(--text-body);
  color: var(--ink);
}
/* The one figure the panel exists to state, so it takes the price role the card
   and the spec panel already use — same voice, same tabular figures. */
.buy-summary__price {
  font-family: var(--font-serif);
  font-size: var(--text-price);
  line-height: var(--lh-tight);
  font-variant-numeric: lining-nums tabular-nums;
}

@media (max-width: 34rem) {
  .buy-summary__row { grid-template-columns: minmax(0, 1fr); gap: var(--space-1); }
}

/* ---- fitted options -----------------------------------------------------
   Rows, not a scatter. Eight items of uneven length in three columns produced
   three different rhythms and a ragged foot: two wrapped to a second line, six
   did not, and nothing lined up with anything. They now take the same row
   rhythm as the specification table directly beneath them in the same panel —
   one hairline per item, one rhythm for the whole tab.

   The copper dash marker is gone with it. The rule under each row already says
   "this is a list of facts"; a second marker saying the same thing is noise,
   and it was hanging off the first line of the two that wrapped.
   ------------------------------------------------------------------------- */
.options {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
  column-gap: var(--space-8);
  margin-block-start: var(--space-text-obj);
}
.options__item {
  padding-block: var(--space-4);
  border-block-end: 1px solid var(--rule);
  font-size: var(--text-body);
  color: var(--ink);
}

.photos { margin-block-start: var(--space-9); scroll-margin-block-start: calc(var(--header-h) + var(--space-6)); }

/* ---- photo mosaic -------------------------------------------------------
   The Figma's composition: six cells at four different shapes, not six crops of
   one shape. A cockpit is not the same shape as a side profile, and forcing six
   subjects through a single aspect ratio is how a gallery stops reading as a set
   of photographs and starts reading as a contact sheet.

   Clicking a cell sends it to the stage above rather than opening a lightbox —
   the stage IS the large view, and a second one would be a second answer to the
   same question.
   ------------------------------------------------------------------------- */
.photo-mosaic {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-3);
  margin-block-start: var(--space-text-obj);
}

.photo-mosaic__item {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  padding: 0;
  border: 0;
  border-radius: var(--radius-3);
  overflow: hidden;
  background: var(--bg-raised);
  cursor: zoom-in;
  line-height: var(--lh-none);
}
.photo-mosaic__item img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  display: block;
  transition: scale var(--dur-4) var(--ease-out);
}
.photo-mosaic__item:hover img,
.photo-mosaic__item:focus-visible img { scale: 1.05; }
.photo-mosaic__item:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }

/* One column keeps each photograph at its own proportion; the mosaic only
   exists where there is width for three columns to mean anything. */
.photo-mosaic__cell img { aspect-ratio: 4 / 3; }

@media (min-width: 48rem) {
  .photo-mosaic {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-template-rows: repeat(6, minmax(0, 1fr));
    aspect-ratio: 16 / 11;
  }
  /* Spans written per cell, so the composition is stated once and cannot drift
     into "whatever auto-placement did". */
  .photo-mosaic__cell--a { grid-column: 1; grid-row: 1 / 4; }
  .photo-mosaic__cell--b { grid-column: 2; grid-row: 1 / 5; }
  .photo-mosaic__cell--c { grid-column: 3; grid-row: 1 / 3; }
  .photo-mosaic__cell--d { grid-column: 1; grid-row: 4 / 7; }
  .photo-mosaic__cell--e { grid-column: 2; grid-row: 5 / 7; }
  .photo-mosaic__cell--f { grid-column: 3; grid-row: 3 / 7; }
  /* The cell decides the shape here, so the image stops declaring one. */
  .photo-mosaic__cell img { aspect-ratio: auto; }
}

@media (prefers-reduced-motion: reduce) {
  .photo-mosaic__item img { transition: none; }
}

/* ---- full specification ------------------------------------------------- */
.spec-table {
  inline-size: 100%;
  table-layout: fixed;   /* long VINs must wrap, not widen the column */
  border-collapse: collapse;
  margin-block-start: var(--space-text-obj);
}
.spec-table th,
.spec-table td {
  padding: var(--space-4) var(--space-4) var(--space-4) 0;
  border-block-end: 1px solid var(--rule);
  text-align: start;
  vertical-align: baseline;
}
.spec-table th {
  inline-size: 34%;
  font-size: var(--text-xs);
  font-weight: var(--weight-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-muted);
}
.spec-table td { font-size: var(--text-body); color: var(--ink); overflow-wrap: anywhere; }
.spec-table caption {
  text-align: start;
  padding-block-end: var(--space-4);
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

/* ---- promos -------------------------------------------------------------
   The old site's four cards, and they are photographs by nature: each one is a
   place, a person or a car. I had them as a text strip for a while, which
   removed the one thing that made them work.

   Every card carries a scrim, and the scrim is not decoration — the label sits
   on it rather than on whatever the photograph happens to be doing behind it,
   which is the only way white type over uncontrolled imagery is legible at all.
   Legibility is fixed at the background layer here as everywhere else on this
   site: no text shadows.
   ------------------------------------------------------------------------- */
.promos {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: var(--space-4);
}

.promo {
  position: relative;
  display: block;
  min-block-size: 15rem;
  overflow: hidden;
  border-radius: var(--radius-3);
  background: var(--bg-raised);
  text-decoration: none;
  isolation: isolate;
}
.promo__media {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  z-index: -2;
  transition: scale var(--dur-4) var(--ease-out);
}
/* Warm, and weighted to the side the type sits on. The copper is the same wash
   the photography already carries elsewhere on the site, so four different
   pictures still read as one set. */
.promo::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(90deg,
      rgb(var(--bg-deep-rgb) / 0.25) 0%,
      rgb(var(--bg-deep-rgb) / 0.72) 52%,
      rgb(var(--bg-deep-rgb) / 0.86) 100%),
    linear-gradient(0deg,
      rgb(var(--accent-rgb) / 0.22) 0%,
      rgb(var(--accent-rgb) / 0.05) 60%,
      rgb(var(--accent-rgb) / 0) 100%);
}

.promo__body {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-5);
  block-size: 100%;
  min-block-size: 15rem;
  padding: var(--space-5);
  text-align: end;
}
.promo__title {
  font-family: var(--font-display);
  font-size: var(--text-h3);
  font-weight: var(--weight-heading);
  line-height: var(--lh-tight);
  letter-spacing: var(--track-heading);
  color: var(--ink);
  max-inline-size: 16ch;
}
/* A span styled as the button it replaces — the whole card is the link, so a
   real <a> inside it would be a link inside a link. */
.promo__cta {
  display: inline-flex;
  align-items: center;
  min-block-size: 44px;
  padding-inline: var(--space-5);
  border-radius: var(--radius-2);
  background: var(--accent);
  color: var(--ink);              /* 4.58:1 on copper */
  font-family: var(--font-display);
  font-size: var(--text-sm);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  transition: background-color var(--dur-1) var(--ease-out);
}
.promo:hover .promo__media,
.promo:focus-visible .promo__media { scale: 1.06; }
.promo:hover .promo__cta { background: var(--accent-hover); }
.promo:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }

@media (min-width: 62rem) {
  /* In the rail they stack: the column is 24rem wide and four cards across it
     would be four thumbnails. */
  .promos { grid-template-columns: minmax(0, 1fr); align-content: start; }
  /* Sized so the rail lands near the tab block beside it. At 11rem the four
     cards ran 185px past the reading column and left that much dead page under
     it — the rail is support, and support that outruns the thing it supports
     is what leaves a hole in the middle of the page. */
  /* Stacked, as the reference has them, at the height where four of them land
     beside the reading column rather than 377px past it. Laid out side by side
     instead they fitted, and the titles came out squeezed into three lines in a
     12-character measure — fitting is not the only thing a card has to do. */
  .promo { min-block-size: 10rem; }
  .promo__body { min-block-size: 10rem; gap: var(--space-3); padding: var(--space-4); }
  /* Two lines each, and the copy is cut to fit rather than the measure widened
     to hide it: at three lines one card is a paragraph and the four stop
     reading as a set. */
  .promo__title { font-size: var(--text-lead); max-inline-size: 17ch; }
}

@media (prefers-reduced-motion: reduce) {
  .promo__media { transition: none; }
}


/* =========================================================================
   15e. CONFIGURATOR (build.html)

   Two columns, the same arrangement as the vehicle page: the work on the left,
   the running summary on the right. The summary is bone, which is now its third
   owner on this site and its most literal one — it is a quotation being written
   while you watch.

   COLOUR ON THIS SURFACE, restated because the mockup breaks it: the Figma sets
   "Items Included" and the total in copper on the cream panel. --accent-ink is
   2.74:1 there and is illegal at any size. Headings and the total take
   --ink-inverse; slate is available at 6.33:1 where a second voice is needed.
   ========================================================================= */

.build { display: grid; gap: var(--space-6); }
/* A grid item defaults to min-width:auto and refuses to shrink below its
   content, which is how an option grid pushes its own column wider. */
.build__main { min-inline-size: 0; }

@media (min-width: 62rem) {
  .build {
    /* Same proportion as the vehicle page's rail, and for the same reason: a
       fixed 30rem left the option grid narrower than the summary at 1024. */
    grid-template-columns: minmax(0, 1fr) clamp(20rem, 34%, 30rem);
    gap: var(--space-6);
    align-items: start;
  }
  /* The summary follows the work: nine groups is a long scroll, and a total you
     have to scroll back up to read is a total nobody reads. */
  .build__summary { position: sticky; inset-block-start: calc(var(--header-h) + var(--space-5)); }
}

/* ---- the page head ------------------------------------------------------
   The site's page head, not a second one: .eyebrow + .section-title, the same
   pair INVENTORY opens with. It carries the model rather than the page name —
   this page is only ever about one car at a time. */
.build-head { margin-block-end: var(--space-6); }

/* ---- the base --------------------------------------------------------- */
/* The base is a step of its own, not the first of ten groups: it gets the
   interval a block gets, and the option list below opens with its own heading.
   Without that separation the chassis reads as one more thing to choose. */
.build-lead { margin-block-end: var(--space-6); }
.build-lead .body-muted { max-inline-size: var(--measure); }

/* ---- progress -----------------------------------------------------------
   Nine decisions over four and a bit screens, and the only status the page had
   was one sentence inside the summary panel — on the other side of the page
   from the work, and below the fold for most of the scroll. A visitor could not
   tell how much of this there is, how far in they are, or whether they had
   finished. That is the largest single thing the page was missing.

   It lives with the WORK, in the left column, because that is where the
   question "how much is left" is asked. The panel keeps its own sentence: the
   panel answers "what have I chosen", which is a different question.
   ------------------------------------------------------------------------- */
.build-progress {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-4);
  margin-block-start: var(--space-6);
}
.build-progress__count {
  font-size: var(--text-xs);
  font-weight: var(--weight-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-muted);            /* 7.19:1 */
}
.build-progress__count strong { color: var(--ink); font-weight: var(--weight-label); }
.build-progress__track {
  flex: 1 1 12rem;
  block-size: 4px;
  border-radius: var(--radius-pill);
  background: var(--rule-strong);
  /* NOT overflow: hidden — the fill's glow has to be allowed past the track's
     own edge, which is the whole point of it. The fill is clipped by its own
     radius instead. */
  overflow: visible;
}
/* The header's telephone gradient, not a colour that resembles it: the same two
   stops in the same order, --tel-lift into --tel. Teal is the page's only live
   wire and this is the page's only live measure, so they are the same thing said
   twice rather than two greens that nearly match.

   It does cost the palette something and the cost is named: slate was the colour
   of a state that has settled, and the progress fill reports settled states. It
   keeps that job on the group marks and the option cards, where a visitor is
   choosing; the bar is a running total of them and takes the live colour. 6.49:1
   at the light stop, 4.87:1 at the dark one — both past the 3:1 a 4px graphic
   needs.

   The glow is the fill's own colour thrown a short distance, so it reads as the
   line being lit rather than as an effect sitting on top of one. Tight radius and
   under half opacity: a lit edge, not a bloom. */
.build-progress__fill {
  display: block;
  block-size: 100%;
  inline-size: 0;
  border-radius: var(--radius-pill);
  background: linear-gradient(90deg, var(--tel-lift) 0%, var(--tel) 100%);
  box-shadow:
    0 0 6px  rgb(var(--tel-lift-rgb) / 0.75),
    0 0 16px rgb(var(--tel-lift-rgb) / 0.40);
  transition: inline-size var(--dur-3) var(--ease-out);
}
/* Nothing chosen yet is nothing to light. A zero-width box still casts its
   shadow, which would leave a glowing dot claiming a progress that has not
   started — read off the ARIA state the track already carries correctly. */
.build-progress__track[aria-valuenow="0"] .build-progress__fill { box-shadow: none; }

@media (prefers-reduced-motion: reduce) {
  .build-progress__fill { transition: none; }
}

/* ---- the three sections -------------------------------------------------
   Nine drawers presented as nine peers is not an information architecture. The
   grouping already exists in the subject — five interior decisions, two about
   the body, two under it — and the page was the only place it was invisible.
   Six of the nine were shut, identical, 80px tall and stacked: repetition with
   no variation, and nothing for the eye to hold.

   This heading takes the h3 rank, which means the DRAWER title had to step down
   to the lead size. That is a correction on its own: a shut drawer's label was
   28px while the option inside it is 15px, so the wrapper shouted and the
   content whispered.
   ------------------------------------------------------------------------- */
/* A DIVIDER, in the label register — not a third sentence-case heading.
   Measured, the left column ran four ranks that differed only in size and all
   of them Oswald 300, sentence case, white: 34 for the instruction, 28 for this,
   28 for "Rolling Chassis", 20 for a group title. Two of those were not near
   identical, they WERE identical — a structural section heading and a value
   inside the base card, set exactly alike. Size alone at 1.2× is not a rank.

   So this stops competing: uppercase, tracked, muted, at label size, with the
   rule running to the end of the column. Caps read wider than their size, and
   the rule gives it the width — it divides the list without out-shouting the
   things it divides, which is what a divider is for. Every neighbouring pair now
   differs on at least two axes: case, colour, size or face. */
.section-divider {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-block: var(--space-8) var(--space-5);
  /* THE DISPLAY TIER, not the label one — and I had this backwards twice. I
     first argued it must not out-rank the titles it divides. It CONTAINS them:
     "Interior" holds "Interior Upholstery", so it is the higher structural rank
     and reading larger is correct. Nothing is confusable either way — the two
     differ in case, face and colour, not only in size.

     It also carries its own tracking rather than inheriting the label register's,
     which is why the rule is written here rather than left in that list — at this
     size it is no longer a micro-label.

     Muted, not white: the size states the rank, the tone keeps a divider from
     competing with the answers underneath it. */
  font-family: var(--font-display);
  font-size: var(--text-h3);
  /* --weight-display, not the label weight: the home page sets every caps line
     at display size in 400 — the hero, the section titles, the buttons — and 500
     was a rung this site does not otherwise have at this scale. */
  font-weight: var(--weight-display);
  line-height: var(--lh-tight);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-muted);            /* 7.19:1 */
}
/* The rule runs to the end of the column, so the heading reads as a division of
   the list rather than as one more label sitting on top of it. */
.section-divider::after {
  content: "";
  flex: 1 1 auto;
  block-size: 1px;
  background: var(--rule);
}
/* The first divider still opens — just not by the full block interval, because
   the lead above it already carries its own. Set to 0 it landed 20px under the
   progress line, and those two are the same register with a rule across the
   column each: side by side they read as a pair rather than as a status and a
   structure. */
.section-divider:first-of-type { margin-block-start: var(--space-7); }

.build-base {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-3);
  margin-block-end: var(--space-text-obj);
  background: var(--bg-raised);
}
.build-base img {
  inline-size: 100%;
  aspect-ratio: 16 / 6;
  object-fit: cover;
  display: block;
}

/* A card ON the photograph, at the left — where the shot is empty pavement and
   the car sits to the right, so it fills dead frame rather than covering the
   subject.

   IT CARRIES ITS OWN GROUND. That is the whole difference between this and the
   overlay it replaces: text laid straight onto the picture landed on the bright
   part of it and read as a watermark, and its contrast changed with every
   photograph. This ground is the phone bar's, which already exists — deep at
   0.92 with a blur behind it. Composited over pure white, the worst case a
   photograph can offer, the ground still measures #272623: white on it is
   15.2:1 and the copper marker 4.81:1. The contrast is a property of the card,
   not of the picture underneath. */
.build-base__bar {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: var(--space-6);
  translate: 0 -50%;
  display: grid;
  max-inline-size: 20rem;
  padding: var(--space-5);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-3);
  background: rgb(var(--bg-deep-rgb) / 0.92);
  backdrop-filter: blur(10px);
  box-shadow: var(--shadow-lift);
}
/* The intervals are the COMPONENT'S, stated as margins rather than as a grid
   gap. All three lines are paragraphs, so the page's prose rhythm — `p + p`,
   24px — landed on top of the gap and pushed the marker 32px off the name it
   marks, while the name sat 16px from its own price: the grouping read
   backwards. A gap cannot win that argument; a rule at component specificity
   can, and it is the same construction .stack already uses.

   The marker separates further than the pair below it, because the pair IS the
   object and the marker is the instruction above it. */
.build-base__bar > * + * { margin-block-start: var(--space-2); }
.build-base__bar > .build-base__name { margin-block-start: var(--space-4); }

.build-base__step {
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: var(--weight-label);
  /* The box hugs the word. At reading leading a 15px marker sat in a 25px line
     box, and the 16px under it measured 16 and read as 26. */
  line-height: var(--lh-none);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--accent-ink);
}
.build-base__name {
  font-family: var(--font-display);
  font-size: var(--text-h3);
  font-weight: var(--weight-heading);
  line-height: var(--lh-tight);
  color: var(--ink);
}
.build-base__price {
  font-family: var(--font-serif);
  font-size: var(--text-price);
  line-height: var(--lh-none);
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink);
}

@media (max-width: 47.99rem) {
  /* A phone gives the photograph about 130px of height; a card cannot live on
     it. The three facts come back into the flow underneath, which is what the
     space below the picture is for. */
  .build-base__bar {
    position: static;
    translate: none;
    max-inline-size: none;
    border: 0;
    border-block-start: 1px solid var(--rule);
    border-radius: 0;
    background: none;
    backdrop-filter: none;
    box-shadow: none;
  }
}

/* ---- option group -------------------------------------------------------
   A <details>, so it opens with no script and every option is in the document
   for a crawler. Two states, one surface: the Figma uses three — white when
   open, grey when open-and-something-else, a light pill when shut — for a
   component that has exactly two.
   ------------------------------------------------------------------------- */
.optgroup {
  border: 1px solid var(--rule);
  border-radius: var(--radius-3);
  background: var(--bg-raised);
  transition: background-color var(--dur-2) var(--ease-out);
}
/* THE OPEN ONE LIFTS. Only one group is open at a time now, so the surface can
   carry that fact instead of leaving it to a caret: the group being worked on
   comes forward and the eight shut ones stay back. --bg-panel is the step this
   page already owns — the next ground up from --bg-raised — so this is a rung
   on the existing ramp rather than a new surface. White on it measures 12.97:1
   and the muted ink 5.54:1. */
.optgroup[open] { background: var(--bg-panel); }

@media (prefers-reduced-motion: reduce) {
  .optgroup { transition: none; }
}

.optgroup + .optgroup { margin-block-start: var(--space-4); }

.optgroup__head {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  min-block-size: 64px;
  padding: var(--space-4) var(--space-5);
  cursor: pointer;
  list-style: none;
}
.optgroup__head::-webkit-details-marker { display: none; }

/* The lead size, not h3. A drawer is a rank BELOW the section that contains it,
   and at 28px a shut drawer's label was nearly twice the size of the option name
   inside it — the wrapper louder than its own content.

   AND THE BODY FACE, not the display one. Weight was the first answer and it
   was treating a symptom: the real problem is that Oswald is drawn for capitals
   — the display layer says so in as many words, "tall caps, short descenders, a
   narrow set… which is why uppercase stops at the display tier". Nine repeated
   titles at 20px in condensed LOWERCASE is the application that comment warns
   against: narrow counters and short descenders take away exactly the
   ascender/descender variation the eye uses to recognise word shapes, and this
   column is scanned rather than read.

   And the treatment is not invented for this page: THE SITE ALREADY HAS THIS
   ROLE. A repeated item title in a list is .vehicle__title on the inventory
   cards, and it has been the reading face at the lead size since the home page
   was built. Same role, same setting — the only difference kept is the ink,
   full rather than muted, because this title is a control that opens a drawer
   and the card's is a link under a photograph that carries the emphasis. */
.optgroup__name {
  font-family: var(--font-body);
  font-size: var(--text-lead);
  font-weight: var(--weight-body);
  line-height: var(--lh-tight);
  letter-spacing: var(--track-body);
  color: var(--ink);
}

/* What is chosen, shown in the shut head. Nine groups closed in a column is
   nine words and no answers; this is what stops the visitor opening each one
   again to remember what they picked. */
.optgroup__chosen {
  margin-inline-start: auto;
  text-align: end;
  font-size: var(--text-sm);
  color: var(--ink-muted);
}
/* An answer takes full ink; "Not selected" does not. Both were white, so the
   two states differed only in the words — and a column of nine is read at a
   glance, not word by word. */
.optgroup__chosen strong { display: block; font-weight: var(--weight-body); color: var(--ink-muted); }
.optgroup[data-answered] .optgroup__chosen strong { color: var(--ink); }
/* The value is the answer and takes full ink; its price is what the answer
   costs, so it takes the muted step. Two ranks, one hue. */
.optgroup__chosen .num-tabular { color: var(--ink-muted); }   /* 7.19:1 */

/* ---- the answered state -------------------------------------------------
   Said at the HEAD of the row, not only in the value at its end. Nine groups
   is a checklist and a checklist is read down its left edge: the eye should be
   able to run the column once and see what is still open.

   The mark is the option card's own — the same ring, the same tick, the same
   copper — because a group being answered and a card being chosen are one fact
   one level apart, and drawing them differently would make them look like two.
   Present before it is filled, for the reason the cards' marks are: a slot that
   appears only once answered cannot show you what is unanswered.
   ------------------------------------------------------------------------- */
.optgroup__mark {
  flex: 0 0 auto;
  inline-size: 22px;
  block-size: 22px;
  display: grid;
  place-items: center;
  /* Muted ink, not a rule colour. --rule-strong measured 2.54:1 on this ground,
     under the 3:1 floor a non-text indicator has to clear — and the EMPTY ring
     is the state that says a group is still open, so it is the one that must
     not be faint. */
  border: 2px solid var(--ink-muted);        /* 6.99:1 */
  border-radius: var(--radius-pill);
  transition: background-color var(--dur-1) var(--ease-out),
              border-color var(--dur-1) var(--ease-out);
}
.optgroup__mark::after {
  content: "";
  inline-size: 11px;
  block-size: 11px;
  background: var(--ink);
  clip-path: polygon(14% 52%, 8% 60%, 38% 88%, 92% 26%, 84% 18%, 38% 72%);
  opacity: 0;
  transition: opacity var(--dur-1) var(--ease-out);
}
/* SLATE, not copper. The two colours already divide the work elsewhere on this
   site and the division is worth stating: copper marks what the visitor should
   act on — the instruction, the primary action, "just arrived", saved — and
   slate marks a state that has SETTLED: a switch that is on, a car that is in
   stock, a group that has been answered. Answered is the second kind. It also
   gives this page the counterweight it had none of: every layer here is warm —
   the ground, the sand secondary ink, the bone panel, the side wash — so copper
   on top of it had nothing to be an accent against. 3.37:1 as a fill on this
   ground, white tick on it 4.85:1. */
.optgroup[data-answered] .optgroup__mark {
  background: var(--slate-lift);
  border-color: var(--slate-lift);
}
.optgroup[data-answered] .optgroup__mark::after { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .optgroup__mark,
  .optgroup__mark::after { transition: none; }
}

.optgroup__caret {
  flex: 0 0 auto;
  inline-size: 16px;
  block-size: 16px;
  background: currentColor;
  color: var(--ink-muted);
  clip-path: polygon(50% 72%, 6% 26%, 18% 14%, 50% 46%, 82% 14%, 94% 26%);
  transition: rotate var(--dur-2) var(--ease-out);
}
.optgroup[open] .optgroup__caret { rotate: 180deg; }
.optgroup__body { padding: 0 var(--space-5) var(--space-5); }
/* The interval is a property of the BODY, not of one pair inside it. Written as
   a rule on the pair, the next thing added to a group arrives with no gap at
   all — which is how a note ended up touching the grid it introduces. */
.optgroup__body > * + * { margin-block-start: var(--space-text-obj); }

@media (prefers-reduced-motion: reduce) {
  .optgroup__caret { transition: none; }
}

/* ---- option cards -------------------------------------------------------
   THE INDICATOR IS ALWAYS THERE, empty until chosen, and its SHAPE states the
   mechanic before the first click: a circle means one of these, a square means
   as many as you like. The Figma draws neither — it marks one group with a
   copper border and another with a tick, so two different rules look like one,
   and a visitor cannot tell whether a second choice adds or replaces.
   ------------------------------------------------------------------------- */
/* The gap between cards has to stay LARGER than the gap inside one, or a card
   stops reading as a photograph-with-its-caption and starts reading as a list of
   loose parts. With the caption now 16px under its image, 16px between cards
   made the two distances equal and the grouping disappeared. Rows open further
   than columns because it is the rows the captions grow into. */
.options-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
  gap: var(--space-6) var(--space-5);
}
@media (max-width: 47.99rem) {
  /* Two across on a phone, not one. At the desktop minimum only a single card
     fitted 342px, and a column of one is a list — but these are chosen by
     looking, and looking means comparing two side by side. */
  .options-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-6) var(--space-5); }
  /* The label keeps the 14px floor — it carries information, and shrinking type
     to make a box fit is how a floor stops being a floor. The padding gives way
     instead. */
  .optcard__await { padding: var(--space-2); }
}

.optcard { position: relative; display: block; cursor: pointer; }
.optcard input {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  opacity: 0;
}
.optcard__frame {
  display: block;
  overflow: hidden;
  border: 2px solid transparent;
  border-radius: var(--radius-2);
  background: var(--bg);
  transition: border-color var(--dur-2) var(--ease-out);
}
.optcard__frame img {
  inline-size: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  display: block;
}
/* NOT a stand-in photograph. There is no shot of "Red Interior" or of any other
   option in this project, and any car picture put here would be telling the
   visitor they are looking at the thing they are buying. An empty frame that
   says so is worth more than a convincing wrong one. */
/* SHARED, and the scope is the condition rather than a block's name: a frame
   waiting for a photograph looks the same wherever it waits. It began inside the
   configurator and the team page needs exactly it — five more frames the client
   has not supplied. Written twice it would drift; written here it cannot. */
.optcard__await,
.await-photo {
  display: grid;
  place-items: center;
  aspect-ratio: 4 / 3;
  padding: var(--space-4);
  background: repeating-linear-gradient(135deg,
    rgb(var(--ink-rgb) / 0.03) 0 8px, rgb(var(--ink-rgb) / 0) 8px 16px);
  text-align: center;
  font-size: var(--text-xs);
  line-height: var(--lh-tight);
  color: var(--ink-muted);
}
.optcard:hover .optcard__frame { border-color: var(--rule-strong); }
.optcard input:focus-visible + .optcard__frame { outline: 2px solid var(--focus); outline-offset: 2px; }
.optcard input:checked + .optcard__frame { border-color: var(--accent); }

.optcard__mark {
  position: absolute;
  inset-block-start: var(--space-3);
  inset-inline-end: var(--space-3);
  z-index: 2;
  inline-size: 24px;
  block-size: 24px;
  display: grid;
  place-items: center;
  border: 2px solid var(--ink);
  background: rgb(var(--bg-deep-rgb) / 0.55);
  backdrop-filter: blur(4px);
  transition: background-color var(--dur-1) var(--ease-out),
              border-color var(--dur-1) var(--ease-out);
}
/* ONE SHAPE. The mark used to be a circle for one-of and a square for any-of,
   on the reasoning that the shape could state the mechanic for free. Seven of
   the nine groups are one-of, so in practice the page showed a long run of
   circles with two squares in it, and two odd shapes among many read as a
   defect before they read as a rule — a convention only teaches when you meet
   both halves of it side by side, which nobody does here.

   The mechanic is now said in WORDS, in the group that departs from the norm.
   That is the better channel anyway: it does not ask anyone to decode a shape,
   and it satisfies the rule that meaning is never carried by one visual
   property alone. The circle is the site's own language — the status discs, the
   buttons, the group marks are all pills. */
.optcard__mark { border-radius: var(--radius-pill); }

.optcard__mark::after {
  content: "";
  inline-size: 12px;
  block-size: 12px;
  background: var(--ink);
  clip-path: polygon(14% 52%, 8% 60%, 38% 88%, 92% 26%, 84% 18%, 38% 72%);
  opacity: 0;
  transition: opacity var(--dur-1) var(--ease-out);
}
/* THE MARK INVERTS, and this is the correction to a real defect: the unselected
   mark is legible on any photograph because of its WHITE RING, and the selected
   one used to drop that ring for a flat mid-tone fill. Choosing a card made its
   indicator harder to see than not choosing it — and on a dark interior shot a
   slate disc with a slate edge is very nearly invisible.

   So selection flips the disc rather than tinting it: white fill, slate glyph.
   White against a car interior is the loudest thing available, the slate tick
   on it is 7.27:1, and where the photograph is bright instead the slate-lift
   ring separates the disc at 3.37:1. Nothing about it depends on what the
   picture underneath happens to be doing. */
.optcard input:checked ~ .optcard__mark {
  background: var(--ink);
  border-color: var(--slate-lift);
  backdrop-filter: none;
}
.optcard input:checked ~ .optcard__mark::after {
  opacity: 1;
  background: var(--slate);            /* 7.27:1 on the white disc */
}

/* And the state is carried by the CARD, not only by a 24px disc in its corner.
   The frame has held a transparent 2px border and a transition for exactly this
   since it was written; nothing ever set the colour. --slate-lift rather than
   --slate because this edge is judged against the dark ground, where --slate is
   2.25:1 and under the floor. */
.optcard input:checked ~ .optcard__frame { border-color: var(--slate-lift); }   /* 3.37:1 */

/* THE STRONGEST SIGNAL IS THE ONES NOT CHOSEN. A 24px disc and a 2px edge are
   both small marks on a 200px photograph, and no amount of colour makes a small
   mark large. Once a group has an answer, the options it did not take step back
   — and the difference a visitor actually reads is between the bright card and
   the eight dim ones, which is the whole card, not a corner of it.

   Only once answered: with nothing chosen, every option is equally available
   and dimming any of them would be a lie about what is on offer. */
.optgroup[data-answered] .optcard__frame img,
.optgroup[data-answered] .optcard__await,
.optgroup[data-answered] .optcard__mark {
  opacity: 0.42;
  transition: opacity var(--dur-2) var(--ease-out);
}
.optgroup[data-answered] .optcard input:checked ~ .optcard__frame img,
.optgroup[data-answered] .optcard input:checked ~ .optcard__frame .optcard__await,
.optgroup[data-answered] .optcard input:checked ~ .optcard__mark { opacity: 1; }
/* The name and price of a card nobody took recede with it, but only to the
   muted step — they stay above the 4.5:1 floor, because a visitor comparing
   options still has to read them. */
.optgroup[data-answered] .optcard__name { color: var(--ink-muted); }            /* 7.19:1 */
.optgroup[data-answered] .optcard input:checked ~ .optcard__name { color: var(--ink); }

/* Hovering a dimmed card brings it back: stepping back is a summary of what was
   decided, not a barrier to changing it. */
.optgroup[data-answered] .optcard:hover .optcard__frame img,
.optgroup[data-answered] .optcard:hover .optcard__await,
.optgroup[data-answered] .optcard:hover .optcard__mark { opacity: 1; }
.optgroup[data-answered] .optcard:hover .optcard__name { color: var(--ink); }
/* Keyboard reaches the card through its hidden input, so the same restore has to
   hang off focus — otherwise tabbing through an answered group moves a focus
   ring across cards that are still dimmed. */
.optgroup[data-answered] .optcard input:focus-visible ~ .optcard__frame img,
.optgroup[data-answered] .optcard input:focus-visible ~ .optcard__frame .optcard__await,
.optgroup[data-answered] .optcard input:focus-visible ~ .optcard__mark { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .optgroup[data-answered] .optcard__frame img,
  .optgroup[data-answered] .optcard__await,
  .optgroup[data-answered] .optcard__mark { transition: none; }
}

/* display: block, and it is the whole fix rather than a tidy-up. These were
   inline <span>s, and a vertical margin does not apply to an inline box — so
   the 12px written above the name was silently discarded and the only thing
   separating the caption from its photograph was 4px of line leading. The rule
   was there, correct-looking and doing nothing, which is why asking for more
   air kept changing nothing.

   As blocks the caption also stops depending on the length of the name: it was
   one line for "Diamond Stitch $1,875" and two for "Carbon Fiber Dash, Auto X
   Gauges", so no two cards in a row agreed about where the price sat. */
.optcard__name {
  display: block;
  margin-block-start: var(--space-4);
  font-size: var(--text-sm);
  line-height: var(--lh-tight);
  color: var(--ink);
}
.optcard__price {
  display: block;
  margin-block-start: var(--space-2);
  font-size: var(--text-sm);
  line-height: var(--lh-none);
  color: var(--ink-muted);
  font-variant-numeric: lining-nums tabular-nums;
}
/* Tone, not a second hue. The chosen card already carries slate at its mark;
   putting copper on its price too would be two accents on one object, and with
   28 cards on the page it was also the largest copper surface in the column.
   Full ink against the muted default is the same distinction without it. */
.optcard input:checked ~ .optcard__price { color: var(--ink); }   /* 16.83:1 */

@media (prefers-reduced-motion: reduce) {
  .optcard__frame, .optcard__mark, .optcard__mark::after { transition: none; }
}

/* Said where the choosing happens, and only by the groups that depart from the
   norm: seven of nine take one answer, so "choose one" on every group would be
   nine lines of noise to inform two. */
.optgroup__mech {
  font-size: var(--text-xs);
  font-weight: var(--weight-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-muted);            /* 7.19:1 */
}

/* ---- the summary --------------------------------------------------------
   A quotation being written while you watch. Bone, and everything on it takes
   --ink-inverse: the mockup sets its headings and its total in copper, which is
   2.74:1 here and illegal at any size.
   ------------------------------------------------------------------------- */
/* This block used to carry a second, parallel copy of the bone panel — its own
   render, title, total, rule and rows, and its own control colours. The panel
   already existed on the vehicle page, so the copy was deleted and the markup
   moved to .vdp-panel. What is left here is only what is TRUE OF THE
   CONFIGURATOR'S panel and of nothing else. The bone control colours now live
   with the control, scoped to the surface rather than to a block's name. */

/* Full width, because a panel this narrow has no second column to share with
   and a button that stops short of its own container reads as unfinished. */
.build__summary .btn-hmc { inline-size: 100%; }

/* ---- the phone bar ------------------------------------------------------
   The summary cannot simply drop below the configurator: the total is needed
   WHILE choosing, not after. It cannot be sticky whole either — it is tall. So
   the one number rides the bottom of the screen and the full list opens on
   request, which is when it is actually wanted.
   ------------------------------------------------------------------------- */
.build-bar { display: none; }

@media (max-width: 61.99rem) {
  .build-bar {
    position: fixed;
    inset-inline: 0;
    inset-block-end: 0;
    z-index: 40;
    display: flex;
    align-items: center;
    gap: var(--space-4);
    block-size: var(--build-bar-h);
    padding-inline: var(--gutter);
    border-block-start: 1px solid var(--rule-strong);
    background: rgb(var(--bg-deep-rgb) / 0.94);
    backdrop-filter: blur(10px);
  }
  .build-bar__total {
    font-family: var(--font-serif);
    font-size: var(--text-price);
    font-variant-numeric: lining-nums tabular-nums;
    color: var(--ink);
    line-height: var(--lh-none);
  }
  .build-bar__count { font-size: var(--text-xs); color: var(--ink-muted); }
  .build-bar .btn-hmc { margin-inline-start: auto; min-block-size: 44px; font-size: var(--text-sm); }

  /* Room for the bar. It has to be on the LAST thing in the document, not on
     main: the footer comes after main, so padding there left the footer's last
     row permanently under the bar — its links unreachable on every phone. */
  .build-page ~ .site-footer { padding-block-end: calc(var(--space-8) + var(--build-bar-h)); }
  .build__summary { order: 2; }
}

/* =========================================================================
   15g. WHO WE ARE (who_we_are_team.html)

   Three cells across the top, then the team. The mockup gives the film more
   width than the two beside it, which is right: it is the only one of the three
   a visitor can act on, and a 16/9 frame at a third of the row would be a
   postage stamp of a man talking.
   ========================================================================= */

/* THE BAND, rebuilt — and the version before this one failed on exactly the
   point it was written to fix.

   It put the statement in a 5fr column beside a 7fr film and claimed the words
   would dominate "by scale and isolation". They did not. The statement sat at
   the standfirst rank, 20px, against a 500px frame holding a man's face — and a
   face in a photograph is the strongest attractor a page can contain. Side by
   side at those weights the photograph wins every time, which is the same
   diagnosis I had written about the mockup an hour earlier.

   Two further faults measured in the same screenshot: align-items: center hung
   the short text block against the tall film and left a void above and below it
   on the left; and .standfirst is copper, so a forty-word statement was set in
   the accent colour — accent body text, which the palette forbids outright.

   So the arrangement stops being side-by-side. The statement runs first, at
   display scale, in full ink, across a reading measure with air under it. The
   film follows BELOW it, offset and narrower — support, in reading order,
   where it can no longer compete for the same glance. Downward exhale.
   ------------------------------------------------------------------------- */
/* `clip` for the same reason .about-open and .contact-open carry it: the film
   enters on [data-rise="right"], which starts it 44px to the trailing side, and
   at 320–390px that hangs past the viewport and hands the page a horizontal
   scrollbar until it lands. Measured: 325 of scrollWidth at a 320 viewport,
   305 with this. `clip` rather than `hidden` — hidden would make the block a
   scroll container. */
.who { display: grid; gap: var(--space-6); overflow-x: clip; }

/* align-items: stretch is the default and it is the point — the card takes the
   film's height rather than its own content's, so the band reads as two objects
   of one size instead of a short note beside a tall picture. */
@media (min-width: 62rem) {
  .who { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); }
  /* Both cells stretch to the taller of the two, and the card is the taller
     one — so the poster has to fill its frame or the film ends 53px short of
     its own container and shows a band of empty ground under itself. cover,
     not contain: the crop costs the edges of a room, and the alternative was
     letterboxing a man in a box he does not fill. */
  .who .ytfacade img { block-size: 100%; object-fit: cover; }
}

/* A dark card, the same surface language the option groups use: one ground, one
   hairline, one radius. The words are centred in it because the card is sized by
   the film beside it rather than by the text, so the text has room it did not
   ask for — and text left-aligned in a box taller than itself hangs from the top
   and reads as unfinished. */
.who__panel {
  display: grid;
  align-content: center;
  gap: var(--space-6);
  margin: 0;
  padding: var(--space-7);
  border: 1px solid var(--rule);
  border-radius: var(--radius-3);
  background: var(--bg-raised);
  text-align: center;
}

/* The cutout as it was supplied — no disc, no gradient. Its own soft base is
   baked in at rgb(35,32,33), which is this card's ground, so it needs nothing
   behind it. */
.who__portrait {
  display: block;
  inline-size: auto;
  block-size: 180px;
  margin-inline: auto;
}

/* NOT --ink. Pure white on this ground is 16.83:1 and reads as a headline
   shouting; the statement is something the company says quietly and means. One
   step down the ramp, 12.6:1, which is still far past the floor. */
.who__vision {
  font-size: var(--text-lead);
  line-height: var(--lh-lead);
  color: var(--ink-quiet);                          /* 12.60:1 on --bg-raised */
  max-inline-size: none;
  text-wrap: pretty;
}

/* Stacked, not side by side — and 4px between them, the same interval the team
   cards give the same pair. A name and the role under it are one label; on one
   line the role reads as a second, separate fact. */
.who__attrib {
  display: grid;
  justify-items: center;
  gap: var(--space-1);
}
.who__attrib-name {
  font-family: var(--font-display);
  font-size: var(--text-lead);
  font-weight: var(--weight-label);
  color: var(--ink-quiet);
}
.who__attrib-role {
  font-size: var(--text-xs);
  font-weight: var(--weight-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-muted);                          /* 6.99:1 on --bg-raised */
}

/* ---- the team ---------------------------------------------------------- */
/* auto-FIT, not auto-fill: there are four people and auto-fill reserved a fifth
   empty track, so the row ended short of the column it sits in and read as a
   grid missing a card. */
/* No top margin of its own: the divider above it already carries the interval,
   and both would have stacked into 128px between a heading and the thing it
   heads. */
.team {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--space-7) var(--space-5);
}
/* The card is the hook the whole component hangs on, so it carries the frame
   rather than leaving its children to imply one. */
.team-card { display: flex; flex-direction: column; align-items: stretch; }
/* stretch, not flex-start: the placeholder is a ratio box with no intrinsic
   width, and against flex-start it collapsed to its own text and rendered as a
   thumbnail in a card built for a portrait. */
.team-card .await-photo { inline-size: 100%; border-radius: var(--radius-2); }
/* The same p + p correction: name, role and contact are one block of label. */
.team-card > p + p { margin-block-start: var(--space-1); }
.team-card__name {
  margin-block-start: var(--space-4);
  font-size: var(--text-lead);
  color: var(--ink);
}
.team-card__role { color: var(--ink-muted); }
/* A title nobody has supplied is marked as missing rather than left blank —
   blank reads as "this person has no role", which is worse than "not yet". */
.team-card__role--await { font-style: italic; }
.team-card__contact {
  margin-block-start: var(--space-3);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}
.team-card__contact a { color: inherit; text-decoration: none; }
.team-card__contact a:hover,
.team-card__contact a:focus-visible { color: var(--ink); }
/* Underlined at rest: it sits under two other lines of the same size and colour,
   and a link that announces itself only on hover is not discoverable on touch. */
.team-card__action {
  display: inline-flex;
  align-items: center;
  margin-block-start: var(--space-2);
  min-block-size: 44px;
  font-size: var(--text-sm);
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-thickness: 1px;
}
.team-card__action:hover,
.team-card__action:focus-visible {
  color: var(--accent-ink);
  text-decoration-thickness: 2px;
}


/* =========================================================================
   15i. ABOUT (who_we_are_about.html)

   Two masses across the top — where the company came from, and what it says it
   is — then the four values, then a coda. The origin sits on a card because it
   is a different KIND of text from everything around it: a date and a place
   rather than a claim, and the mockup boxes it for the same reason.
   ========================================================================= */

/* THE OPENING BAND — a full screen of photograph at every size, never folded.
   The box is what carries the height and the picture covers it; the file's own
   1.7:1 governed nothing here, and following it meant a 162px letterbox on a
   phone and a band the content could not fit into on a desktop. */
/* `overflow-x: clip`, not `hidden`: the reveal starts the origin note 44px to
   the trailing side, which at 375px puts its edge 20px past the viewport and
   hands the page a horizontal scrollbar until the note lands. `clip` cuts it
   without making the section a scroll container, which `hidden` would. */
.about-open {
  position: relative;
  isolation: isolate;
  padding-block-start: 0;
  overflow-x: clip;
}

.about-open__media {
  position: relative;
  block-size: 100svh;
  margin-block-end: var(--space-block);
}

/* THE SEAM, and the mask goes on the IMAGE, not on its box. The picture's own
   black measures L=0.01376 and the page ground 0.0124 — near enough to be one
   colour, except that #main lays a copper radial wash over the ground and not
   over the image, so the two met as a visible hairline. Matching the colour
   cannot work: the wash is a gradient and changes across the width. The foot
   dissolves instead and whatever the page is doing underneath shows through.
   On the container it also faded the type standing on the foot — a mask applies
   to everything the element paints, text included. */
.about-open__media img {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  /* A screen-tall box out of a landscape frame keeps about a quarter of its
     width, so the crop is held on the car with the rolling chassis behind it —
     the pair the photograph is for. The tub at the leading edge is what the
     format costs; keeping all three would mean showing none of them. */
  object-position: 72% 50%;
  /* `black` here is not a colour: a mask reads the alpha channel only, so the
     two stops mean "opaque" and "clear". No palette value belongs in it. */
  -webkit-mask-image: linear-gradient(to bottom, black 88%, transparent 100%);
          mask-image: linear-gradient(to bottom, black 88%, transparent 100%);
}

/* ONE COLUMN: a screen-tall box out of a landscape frame keeps about a quarter
   of its width, so the crop is held on the car and the rolling chassis behind
   it — the pair the photograph is for. The tub at the leading edge is what the
   format costs; keeping all three would mean showing none of them. */


/* AND the leading edge, WHERE the text stands on the picture. This frame's
   left third is the lit glass wall at peak luminance 0.94–1.00, so a title and a
   paragraph there have no ground at all — measured before this, both were under
   1.1:1. Same mechanism the home page's hero uses: dark under the type, clear
   over the vehicle. It costs the glass wall some of its light and the car
   nothing, which is the right way round — the car is the subject. */
.about-open__media::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  display: none;                 /* switched on with the overlay, below */
  /* The leading stops are set by the TITLE, the hardest thing on this band:
     copper display type reads 3.67:1 on flat ground and it crosses the glass
     wall between 5% and 18% of the width. At 0.90/0.82 the ground came out at
     L=0.0348 and the title at 2.70:1 — under the 3:1 large-text bar. */
  background: linear-gradient(to right,
    rgb(var(--bg-deep-rgb) / 0.94) 0%,
    rgb(var(--bg-deep-rgb) / 0.90) 34%,
    rgb(var(--bg-deep-rgb) / 0.62) 52%,
    rgb(var(--bg-deep-rgb) / 0.18) 64%,
    rgb(var(--bg-deep-rgb) / 0) 74%);
}

/* ONE scrim more, and it is for the HEADER rather than for anything below it.
   The frame is bright across its whole top — 0.94–1.00 in the upper rows, the
   lit glass and the ceiling lamps — and the fixed header floats over it. The
   header carries a gradient of its own; this deepens the first band under it. */
.about-open__media::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: calc(var(--header-h) * 2);
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to bottom,
    rgb(var(--bg-deep-rgb) / 0.72) 0%,
    rgb(var(--bg-deep-rgb) / 0.34) 46%,
    rgb(var(--bg-deep-rgb) / 0) 100%);
}

/* ONE COLUMN IS A DIFFERENT COMPOSITION, not this one narrowed. A phone screen
   holds a title or a paragraph, never a title, a paragraph and four plates, so
   below 62rem the whole block leaves the picture and runs after it: the
   photograph keeps its full screen and the text gets the page's own ground.
   A counterweight that no longer fits is not a counterweight. */
.about-open__overlay { position: relative; z-index: 2; margin-block-start: var(--space-7); }
.about-open__overlay .about__creed { margin-block-start: var(--space-6); }

/* The values keep their name but not the section rule that used to carry it: a
   full-width hairline drawn across a photograph is a line the picture did not
   ask for. Same label register as every eyebrow on the site. */
.about-open__label {
  margin-block-start: var(--space-8);
  font-family: var(--font-display);
  font-size: var(--text-xs);
  font-weight: var(--weight-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-muted);                          /* 6.99:1 */
}
.about-open__overlay .steps { margin-block-start: var(--space-5); }

/* THE CONDITION IS THE ROOM, not the width. Standing on the picture takes four
   plates in ONE row and a band tall enough to hold head, claim, label and plates
   — 66rem is where the plate grid stops wrapping and 52rem is what the stack
   measures. Keyed on width alone at 62rem, a 1024×768 laptop wrapped the plates
   to two rows and ran them 344px past the foot of the photograph and over the
   text below it. Below this, the block simply runs after the picture, which is
   the same arrangement the phone gets. */
@media (min-width: 66rem) and (min-height: 52rem) {
  .about-open__media::after { display: block; }
  .about-open__media img { object-position: 62% 50%; }

  /* Absolute against the SECTION, and sized to the band rather than to it — the
     section runs on past the picture, so an inset-block-end would put the plates
     at the foot of the page instead of the foot of the photograph. */
  .about-open__overlay {
    position: absolute;
    inset-block-start: calc(var(--header-h) + var(--space-7));
    inset-inline: 0;
    block-size: calc(100svh - var(--header-h) - var(--space-7) - var(--space-8));
    margin-block-start: 0;
    /* ONE MASS AT THE FOOT. The spare height was going into the gap between the
       claim and the values label, which split the block in two and left the
       head floating on its own at the top of the picture. It goes above the
       whole group instead: head, claim, label and plates read as one thing
       standing on the photograph, and the picture keeps its open field. */
    display: grid;
    grid-template-rows: auto auto auto auto;
    align-content: end;
  }
  .about-open__label { margin-block-start: var(--space-7); }

  /* At BODY size rather than lead. The claim was set larger to outrank the
     historical note when the two stood side by side; the note is a screen away
     now, so that comparison no longer happens and position on the photograph is
     the rank. Lead size cost eight lines of the band's height and left the
     plates nothing to breathe into. */
  .about-open__overlay .about__creed { max-inline-size: 56%; }
  .about-open__overlay .about__creed p {
    font-size: var(--text-body);
    line-height: var(--lh-body);
    max-inline-size: none;
  }
}

/* THE ORIGIN BAND: the place, and the paragraph about it. 7:5 with the
   photograph leading — a town square is the evidence and the date is the
   caption, not the other way round. Centred against each other, because the two
   masses are different heights and aligning their tops would leave the shorter
   one hanging. */
.about { display: grid; gap: var(--space-6); }

@media (min-width: 62rem) {
  .about {
    grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
    gap: var(--space-7);
    align-items: center;
  }
}

.about__place {
  border-radius: var(--radius-3);
  overflow: hidden;
}
/* img OR video: the block held a still and now holds the film that still is the
   poster of, and the frame must not care which. A player left at its intrinsic
   width sits narrower than its column the moment the column is wider than the
   encode. */
.about__place :is(img, video) { display: block; inline-size: 100%; block-size: auto; }

/* THE CLAIM. WHITE, because it sits on a photograph and nothing else on the page
   does. Sand is the secondary ink on flat ground; over this image it measured
   3.27:1 at the brightest point the text overlaps and failed. Type over
   photography takes near-white or ink, never a mid tone that depends on what the
   picture is doing underneath.

   The size and the measure are set where it lands — on the band, in the query
   above. Off the band, at one column, it is a lead and reads as one. */
.about__creed { color: var(--ink); }
.about__creed p {
  max-inline-size: var(--measure-tight);
  font-size: var(--text-lead);
  line-height: var(--lh-lead);
}

/* NO BOX. It had one when it stood alone and needed a surface to be a thing at
   all; with a photograph beside it the block already has one, and a panel next
   to a picture is two containers doing one container's work. */
.about__origin { color: var(--ink-quiet); }         /* 12.60:1 */
.about__origin p { max-inline-size: var(--measure); }

/* ---- the coda -----------------------------------------------------------
   It closes the page, so it is centred and capped short — a last word read in
   one breath rather than a column of body copy. */
/* A BLOCK interval, not a section's. The values and the coda are parts of one
   movement — the company says where it came from, what it holds to, and what
   drives it — so the distance between them is the block interval the system
   names for exactly that. As separate sections they stacked two full section
   paddings and put 320px of nothing between the cards and the last word. */
.about-coda {
  max-inline-size: var(--measure);
  margin-block-start: var(--space-block);
  margin-inline: auto;
  text-align: center;
  color: var(--ink-muted);
}
/* The line the brand signs everything with, so it takes the display voice and
   full ink — the only rank change in the block, and it lands on the last line. */
.about-coda__sign {
  margin-block-start: var(--space-6);
  font-family: var(--font-display);
  font-size: var(--text-h3);
  font-weight: var(--weight-display);
  line-height: var(--lh-tight);
  color: var(--ink);
}


/* =========================================================================
   15h. VIDEO FACADE (who_we_are_team.html)

   A poster and a real button, with the third-party player written in only when
   someone asks for it. An embed on load costs roughly a megabyte of script and
   its cookies on every visit to this page, spent on a film most visitors will
   never press — and it is the one component here whose weight is not ours to
   optimise. The facade is ours.

   It also keeps the page honest with itself: everything else on this site is a
   local asset served from one origin, and a silent iframe would be the only
   place that stopped being true.
   ========================================================================= */

.ytfacade {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-3);
  background: var(--bg-deep);
}
.ytfacade img,
.ytfacade iframe {
  display: block;
  inline-size: 100%;
  block-size: auto;
  aspect-ratio: 16 / 9;
  border: 0;
}
/* The poster is a still of a film, so it takes the same slight darkening the
   rest of the page's photography carries under text and controls. */
.ytfacade img { opacity: 0.86; transition: opacity var(--dur-2) var(--ease-out); }
.ytfacade:hover img { opacity: 1; }

/* A real 88px target centred on the frame, not a glyph floating on a picture.
   Copper, because pressing this is the one action the block offers. */
.ytfacade__play {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  translate: -50% -50%;
  display: grid;
  place-items: center;
  inline-size: 88px;
  block-size: 88px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--ink);                   /* 4.58:1 on copper */
  cursor: pointer;
  box-shadow: var(--shadow-pop);
  transition: background-color var(--dur-1) var(--ease-out),
              scale var(--dur-2) var(--ease-out);
}
.ytfacade__play svg { inline-size: 40px; block-size: 40px; margin-inline-start: 4px; }
.ytfacade:hover .ytfacade__play,
.ytfacade__play:focus-visible { background: var(--accent-hover); scale: 1.04; }

@media (prefers-reduced-motion: reduce) {
  .ytfacade img,
  .ytfacade__play { transition: none; }
  .ytfacade:hover .ytfacade__play,
  .ytfacade__play:focus-visible { scale: 1; }
}


/* =========================================================================
   15j. CONTACT PAGE (who_we_are_contact.html)

   The form stands ON the opening photograph, at the trailing side, and the head
   holds the leading one. That is the whole composition: the page's one task and
   the picture of the work behind it in a single screen, with the bone panel as
   the only light mass on it — the strongest figure-ground the system owns, and
   the same surface the search, price and quotation panels already use.

   Everything else — the number, the address, the socials, the map — is the
   site's own .contact band, unchanged, exactly as the other five pages carry it.
   It is not a duplicate here: on those pages the band summarises this page and
   links to it, and here the thing it was summarising is above it. Only its CTA
   is dropped, because on this page it would point at the page you are standing
   on.

   WHY THIS IS NOT .about-open. The two bands share a structure and almost no
   values: the crop and the scrim stops are the whole substance of each, and both
   are measured on one photograph and transfer to no other. About's frame has a
   lit glass wall down its leading edge; this one keeps every highlight in the
   top half. Merging them would leave a component whose body is two sets of
   overrides.
   ========================================================================= */

.contact-open {
  position: relative;
  isolation: isolate;
  /* The band carries the page's own clearance; the section adds none. */
  padding-block: 0;
  /* `clip`, not `hidden`: the reveal starts [data-rise="right"] 44px to the
     trailing side, which at 390px hangs past the viewport and hands the page a
     horizontal scrollbar until it lands. `clip` cuts that without making the
     section a scroll container, which `hidden` would — and which would kill
     sticky positioning for everything inside. */
  overflow-x: clip;
}

/* THE PICTURE IS A LAYER, NOT A BAND. It used to be a 100svh box with the head
   absolutely positioned against it, and both had to be told the same height —
   two places holding one number, which drift the first time either is touched.
   Here the section is sized by what stands on it, the media is pinned to that,
   and the height is nobody's decision to keep in sync. */
.contact-open__media {
  position: absolute;
  inset: 0;
  z-index: 0;
}
.contact-open__media img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  /* Centred, and it needs no per-breakpoint nudge: at desktop the box is within
     a few percent of the frame's own ratio, and at one column the centre of the
     frame is the subject. */
  object-position: 50% 50%;
}

/* THE SCRIM RUNS ACROSS, and this is the second time these stops have been
   redrawn — because the type moved. When the head sat at the foot it ran upward,
   which was right for that: measured on this frame the bottom third is mean
   L=0.021 with nothing over L=0.35. With the head on the leading side at mid
   height it is the LEFT that has to be floored, and the left is not quiet — the
   leading quarter means L=0.10–0.12 with peaks at 0.99 and 7% of its upper
   quarter over L=0.35.

   At 0.96 of the page's own ground over that: copper title 3.15:1 against a hot
   patch and 3.6:1 against the mean, both past the 3:1 large-text bar; white lead
   16.5:1. The stops fall away by 72% so the stacks — the subject — keep their
   light, and the panel needs none of it because bone is opaque.

   The second gradient is the seam: the foot lands on --bg, the page's own
   ground, so the picture dissolves into exactly what is under it and there is no
   hairline to match and no mask to write. */
.contact-open__media::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to right,
      rgb(var(--bg-rgb) / 0.96) 0%,
      rgb(var(--bg-rgb) / 0.90) 28%,
      rgb(var(--bg-rgb) / 0.55) 46%,
      rgb(var(--bg-rgb) / 0.14) 60%,
      rgb(var(--bg-rgb) / 0) 72%),
    linear-gradient(to bottom,
      rgb(var(--bg-rgb) / 0) 84%,
      rgb(var(--bg-rgb) / 1) 100%);
}

/* ONE COLUMN IS A DIFFERENT SCRIM, not this one narrowed. The gradient above
   runs ACROSS because the head holds the leading half and the panel the
   trailing one. At one column the head spans the full width and walks straight
   off the end of it — measured at 390px, the last third of every line of the
   lead sat on unscrimmed chrome, which is exactly where this frame peaks at
   L=1.00.

   So below the two-column width it runs DOWN instead, and it is deliberately
   heavy: 0.94 through the whole reading zone floors even a hot patch to
   L=0.026, which is the 3:1 the copper title needs. The picture keeps its
   texture rather than its light, and that is the right way round on a phone —
   legibility is fixed at the background layer, never on the type, and the
   photograph does its real work on the screen that has room for it. */
@media (max-width: 61.99rem) {
  .contact-open__media::after {
    background: linear-gradient(to bottom,
      rgb(var(--bg-rgb) / 0.94) 0%,
      rgb(var(--bg-rgb) / 0.94) 46%,
      rgb(var(--bg-rgb) / 0.78) 62%,
      rgb(var(--bg-rgb) / 0.86) 88%,
      rgb(var(--bg-rgb) / 1) 100%);
  }
}

/* ONE scrim more, and it is for the HEADER rather than for anything below it.
   The top band of this frame means L=0.130 with 11% of it over L=0.35, and the
   fixed header floats on that. The header carries a gradient of its own; this
   deepens the first band under it. */
.contact-open__media::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: calc(var(--header-h) * 2);
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to bottom,
    rgb(var(--bg-deep-rgb) / 0.72) 0%,
    rgb(var(--bg-deep-rgb) / 0.34) 46%,
    rgb(var(--bg-deep-rgb) / 0) 100%);
}

/* The header is fixed, so the band opens underneath it and has to say so. The
   foot takes the section interval the rest of the site ends on. */
.contact-open__inner {
  position: relative;
  z-index: 2;
  display: grid;
  gap: var(--space-8);
  padding-block: calc(var(--header-h) + var(--space-7)) var(--space-9);
}

@media (min-width: 62rem) {
  /* The panel is sized, the head takes what is left — not a ratio. A form has a
     width it is comfortable at and a column of prose does not, so the fixed
     track goes to the control and the fluid one to the text. Centred against
     each other because both are masses now; there is no short column to hang. */
  .contact-open__inner {
    grid-template-columns: minmax(0, 1fr) minmax(0, 30rem);
    gap: var(--space-9);
    align-items: center;
  }
}

/* Capped short. It is a standfirst on a photograph, not a passage — the reading
   measure would run it under the panel. */
.contact-open__head .lead { max-inline-size: var(--measure-tight); }


/* ---- the bone panel -----------------------------------------------------
   The same surface as .facets and .vdp-panel. Joining the inversion group in
   15c is the whole cost of a third bone surface — the ink, the controls, the
   caret and the option list all follow from the name being on that list. */
.contact-form {
  padding: clamp(var(--space-5), 2.5vw, var(--space-6));
  border-radius: var(--radius-3);
  background: var(--bg-card);
  color: var(--ink-inverse);
}

/* The panel's internal rhythm, written here rather than as utilities in the
   markup so it is one thing to read: heading → its own lead-in is the tight
   interval, lead-in → the first control is the text-to-object one. Below that
   the fields carry u-mt-5, which is the same 24. */
.contact-form .section-sub + .lead { margin-block-start: var(--space-text); }
.contact-form .lead + * { margin-block-start: var(--space-text-obj); }

/* On the photograph the panel is the page's primary mass, so it gets a shadow —
   the one place on this site a resting shadow states something true, because it
   genuinely sits above the picture rather than in the page. */
.contact-open .contact-form { box-shadow: var(--shadow-deep); }


/* =========================================================================
   15k. TRADE IN (trade-in.html)

   The page has one job and it is a number: four answers and the visitor knows
   what the car is worth. So the offer bar is the dominant — bone, full content
   width, four fields in a row — and everything below it is what happens once
   that offer is accepted.

   NO TYPE ON THE PHOTOGRAPH HERE, and that is measured rather than preferred.
   The about and contact bands stand their heads on their pictures because those
   frames have a quiet region to stand in; this one has none — mean L=0.148 with
   15.9% of the frame above L=0.35 and no quarter under 10%. Flooring it for a
   copper title would take a scrim at 0.94 across the whole picture, which is a
   dark rectangle where a photograph used to be. So the head takes the page's own
   ground and the picture keeps its light beside it.
   ========================================================================= */

/* ---- the opening band ---------------------------------------------------
   The same move the contact page opens with — a photograph the content stands
   on — and it took a change of picture to make it legal here. The lift shot has
   no quiet region at all (mean L=0.148, 15.9% of the frame over L=0.35), so type
   on it would have needed a scrim at 0.94 and the photograph would have become a
   dark rectangle. promo-trade.jpg is the frame this page should have had all
   along: the roadster and a modern coupé in one workshop, which IS "we buy all
   makes and models" stated as a picture — and it measures L=0.036 with 1.3% hot
   where the head sits, so a light scrim is enough and the cars keep their light.

   The instrument runs ACROSS the foot rather than standing in a column at the
   trailing side the way the contact form does. Four short answers are a bar, not
   a panel, and a bar wants the width. */
.tradein-open {
  position: relative;
  isolation: isolate;
  padding-block: 0;
  overflow-x: clip;
}

.tradein-open__media { position: absolute; inset: 0; z-index: 0; }
.tradein-open__media img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  /* The roadster is the subject and it sits left of centre; at a tall crop the
     frame is held on it rather than on the coupé behind. */
  object-position: 38% 50%;
}

/* MUCH LIGHTER than the contact band's, and that is the whole point of changing
   the picture. 0.80 at the leading edge over a ground already at L=0.036 puts
   the copper title at 3.60:1 and the white lead at 16.5:1, and it is clear by
   66% so both cars keep their light. The second gradient is the seam: the foot
   lands on --bg, so the picture dissolves into the page's own ground. */
.tradein-open__media::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to right,
      rgb(var(--bg-rgb) / 0.80) 0%,
      rgb(var(--bg-rgb) / 0.66) 26%,
      rgb(var(--bg-rgb) / 0.34) 46%,
      rgb(var(--bg-rgb) / 0.10) 58%,
      rgb(var(--bg-rgb) / 0) 66%),
    linear-gradient(to bottom,
      rgb(var(--bg-rgb) / 0) 86%,
      rgb(var(--bg-rgb) / 1) 100%);
}

/* One column puts the head across the full width, so the gradient that runs
   across stops covering the end of every line. Same answer as the contact band:
   below the two-column width it runs down instead. */
@media (max-width: 61.99rem) {
  .tradein-open__media::after {
    background: linear-gradient(to bottom,
      rgb(var(--bg-rgb) / 0.88) 0%,
      rgb(var(--bg-rgb) / 0.86) 44%,
      rgb(var(--bg-rgb) / 0.68) 62%,
      rgb(var(--bg-rgb) / 0.84) 88%,
      rgb(var(--bg-rgb) / 1) 100%);
  }
}

/* For the header, which floats over the top of the frame. */
.tradein-open__media::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: calc(var(--header-h) * 2);
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to bottom,
    rgb(var(--bg-deep-rgb) / 0.72) 0%,
    rgb(var(--bg-deep-rgb) / 0.34) 46%,
    rgb(var(--bg-deep-rgb) / 0) 100%);
}

.tradein-open__inner {
  position: relative;
  z-index: 2;
  display: grid;
  gap: var(--space-8);
  padding-block: calc(var(--header-h) + var(--space-8)) var(--space-8);
}

@media (min-width: 62rem) {
  /* The head is capped at just over half so the coupé on the trailing side is
     never behind text — the picture is making the page's argument and covering
     it would be arguing over it. */
  .tradein-open__head { max-inline-size: 56%; }
  .tradein-open__inner { gap: var(--space-9); }
}


/* ---- the offer bar ------------------------------------------------------
   The same bone surface as .facets, .vdp-panel and .contact-form, and it joins
   the inversion group in 15c for the same one-line cost. Wide rather than tall
   because four short answers read as one question in a row and as an
   interrogation in a column. */
.offer {
  padding: clamp(var(--space-5), 2.5vw, var(--space-6));
  border-radius: var(--radius-3);
  background: var(--bg-card);
  color: var(--ink-inverse);
}

.offer__fields {
  display: grid;
  gap: var(--space-4);
}

@media (min-width: 48rem) {
  .offer__fields { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 62rem) {
  /* Four across, and the action sits in the row with them rather than under it:
     the bar is one instrument, and a button on its own line reads as a second
     step that does not exist. `auto` on the last track so the control keeps its
     own width instead of being stretched to a quarter of the bar. */
  .offer__fields {
    grid-template-columns: repeat(4, minmax(0, 1fr)) auto;
    gap: var(--space-5);
    align-items: end;
  }
}

/* The control is the row's last cell, so it has to sit on the controls' own
   baseline rather than on the labels'. Its height is the control height, not
   the button height the rest of the site uses — a 64px button beside a 44px
   select is a row that does not line up. */
.offer__action {
  min-block-size: 44px;
  padding-block: 0;
  white-space: nowrap;
}

.offer__note {
  margin-block-start: var(--space-4);
  font-size: var(--text-xs);
  color: var(--ink-card);
}


/* ---- recently purchased -------------------------------------------------
   Four photographs, no captions. They are evidence that cars actually get
   bought here, and a caption on each would make them read as inventory — which
   is the opposite claim. */
/* The gallery's own interval, not a section interval: 16px is what the mosaic on
   the home page sets between tiles, and four photographs that belong to one
   claim should sit at the same distance from each other as those do. At 24 and
   32 they read as four separate pictures. */
.recent {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (min-width: 62rem) {
  .recent { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

.recent__item {
  border-radius: var(--radius-3);
  overflow: hidden;
  background: var(--bg-raised);
}
.recent__item img {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  /* Landscape, not square. A square tile is the tallest a four-across row can
     be, and this block is evidence rather than a subject — it should be read in
     one glance and left. */
  aspect-ratio: 4 / 3;
  object-fit: cover;
  transition: transform var(--dur-3) var(--ease-out);
}

/* Hover zooms the PHOTOGRAPH inside a frame that does not move — deliberately
   not the .vehicle gesture, which lifts the whole tile with a shadow. That one
   says "this is a control, press it", and these four have nothing behind them:
   they are evidence that cars get bought here, not stock for sale. An image
   growing inside a fixed frame reads as the photograph breathing instead.

   It also keeps the row still. Four tiles that each lifted would nudge their
   neighbours or need a z-index argument to sit over them; scaling inside
   `overflow: hidden` moves nothing and resamples no text, because there is
   none on these tiles.

   `hover: hover` so a touch device never gets stuck holding the zoomed state
   after a tap. */
@media (hover: hover) {
  .recent__item:hover img { transform: scale(1.06); }
}


/* ---- FAQ ----------------------------------------------------------------
   A <details> each, so every answer is in the document with no script: with JS
   off the page still answers the questions, and a search engine reads all of
   them. The separator is a hairline under each row rather than a box around it,
   because these are one list read in order — the same reason .spec-list is
   ruled and not carded. */
/* THE RULE ENDS WHERE THE TEXT ENDS. The answers are capped at the reading
   measure, as body copy must be; the question rows were not, so a hairline ran
   1560px under a column of text 620 wide and every answer looked like it had
   been cut off early. Capping the block instead of widening the answer keeps
   the measure — a 98ch line is not a fix, it is a worse one. */
.faq {
  max-inline-size: var(--measure);
  /* Centred, and the head and the CTA are centred with it. Capping the column
     without centring it left the block hard against the leading edge with 720px
     of nothing beside it and the centred button below floating away from the
     list it belongs to. One column, three parts, one axis. */
  margin-inline: auto;
  border-block-start: 1px solid var(--rule);
}

.faq__item { border-block-end: 1px solid var(--rule); }

.faq__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  /* Tightened from 24px to 16px. The row still clears the 44px target floor by
     construction — 16 + a line of lead type + 16 is 56 — and five questions now
     read as a list you can take in at once rather than a column you scroll. */
  padding-block: var(--space-4);
  font-size: var(--text-lead);
  line-height: var(--lh-tight);
  color: var(--ink);
  cursor: pointer;
  list-style: none;
  /* A summary is not a heading, so it never got the balance h1–h4 have. Without
     it "…the most accurate offer?" broke with "offer?" alone on line two. */
  text-wrap: pretty;
  transition: color var(--dur-1) var(--ease-out);
}
.faq__q::-webkit-details-marker { display: none; }
.faq__q:hover { color: var(--accent-ink); }
.faq__q:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* The same clip-path chevron the select and the nav disclosure draw, so the
   page has one caret rather than three that nearly match. It rotates to point
   up when the row is open — state stated by direction, not only by position. */
.faq__caret {
  flex: 0 0 auto;
  inline-size: 16px;
  block-size: 16px;
  background: var(--ink-muted);
  clip-path: polygon(50% 72%, 6% 26%, 18% 14%, 50% 46%, 82% 14%, 94% 26%);
  transition: rotate var(--dur-2) var(--ease-out), background-color var(--dur-1) var(--ease-out);
}
.faq__item[open] .faq__caret { rotate: 180deg; background: var(--accent-ink); }

.faq__a {
  padding-block-end: var(--space-5);
  /* No cap of its own: the block above is the measure now, and two caps in one
     column is how the inner one silently wins and nobody knows which. */

  /* Question and answer are two voices, and they were not reading as two.
     --ink-quiet is #e2e2e2 against a white question — a 12.99:1 step next to a
     16.83:1 one, which the eye does not resolve as a change of speaker at all.

     --ink-muted is the answer, and not only because it is darker: it is the
     one WARM step in the ink scale, so the question stays neutral and the
     answer takes the page's own warmth. The difference is in temperature as
     well as in weight, which is what makes it read as a different voice rather
     than as the same voice dimmed.

     It is also the token the system already names for exactly this job —
     "warm sand, all secondary body" in DS.html — and the one .step__body and
     .optcard use for the same relationship. 7.19:1: comfortably AA for a
     paragraph. */
  color: var(--ink-muted);
}
.faq__a p + p { margin-block-start: var(--space-4); }
/* A numbered answer is a sequence, so it is an <ol> and reads as one. */
.faq__a ol { margin: 0; padding-inline-start: var(--space-5); }
/* `pretty` is on <p> globally but a list item is not a p, which is why the last
   line of answer one was the single word "give." — the browser had no
   instruction not to leave it there. */
.faq__a li { text-wrap: pretty; }
.faq__a li + li { margin-block-start: var(--space-3); }

/* The panel's own travel is animated in main.js, because a closed <details>
   has no height for CSS to transition from. What belongs here is the shape
   that travel needs: a box that clips while it grows, so the answer is
   revealed rather than squashed. Nothing here hides content — with the script
   absent the panel has no inline height and behaves natively. */
.faq__a { will-change: height; }

@media (prefers-reduced-motion: reduce) {
  .faq__caret { transition: none; }
  /* And the disclosure is instant: main.js does not bind at all under reduced
     motion, so the native open/close is what runs. */
  .faq__a { will-change: auto; }
  /* The zoom goes with it — a health setting, not a preference toggle. */
  .recent__item img { transition: none; }
  .recent__item:hover img { transform: none; }
}


/* =========================================================================
   16. UTILITIES — the only place a one-off adjustment is allowed, and every
   value still resolves to a token.
   ========================================================================= */

.u-centre-text  { text-align: center; }
.u-mt-5         { margin-block-start: var(--space-5); }
.u-mt-6         { margin-block-start: var(--space-6); }
.u-mb-7         { margin-block-end: var(--space-7); }
.u-measure      { max-inline-size: var(--measure); margin-inline: auto; }


/* =========================================================================
   17. MOTION LAYER
   The animation pass lands here. Content is visible by default and is only
   ever hidden by script, and only when motion is allowed — so a JS failure
   or a reduced-motion request still leaves a complete page.
   ========================================================================= */

[data-reveal] { opacity: 1; }

html.motion-ok [data-reveal="pending"] {
  opacity: 0;
  transform: translateY(8px);
}
html.motion-ok [data-reveal="in"] {
  opacity: 1;
  transform: none;
  transition:
    opacity   var(--dur-3) var(--ease-out),
    transform var(--dur-3) var(--ease-out);
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .rail { scroll-behavior: auto; }
}

.u-mt-7 { margin-block-start: var(--space-7); }
