/* =============================================================================
 * Deal & Drive showroom — shell stylesheet (WP-3)
 * Tokens: tokens.css. Contract: docs/showroom/DESIGN.md.
 *
 * RULE (verified pitfall, DESIGN.md §3): colours live HERE, never in a JSX
 * `style` attribute — an inline `color` defeats the CSS hover grammar.
 * ========================================================================== */

/* ---------- Reset ---------------------------------------------------------- */

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

* {
  margin: 0;
}

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

/* Smooth anchor gliding on pointer devices ONLY. On iOS the browser adjusts the
   scroll position while its toolbars slide in on a direction change; a root-level
   `scroll-behavior: smooth` runs that internal compensation through the animation
   too, compounding with the finger's momentum — the page visibly overshoots on
   every direction change. Touch devices get default (instant) anchors instead;
   JS scrolls keep their explicit per-call `behavior`. */
@media (hover: hover) and (pointer: fine) {
  html {
    scroll-behavior: smooth;
  }
}

body {
  min-height: 100dvh;
  background: var(--page);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.55;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  transition:
    background-color var(--dur) var(--ease),
    color var(--dur) var(--ease);
}

img,
picture,
svg {
  display: block;
  max-width: 100%;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
}

/* ---------- Focus (monochrome does not excuse an invisible focus ring) ----- */

:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 3px;
}

.skip-link {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 100;
  padding: 10px 16px;
  background: var(--ink);
  color: var(--page);
  text-decoration: none;
  transform: translateY(-200%);
  transition: transform var(--dur-fast) var(--ease);
}

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

/* ---------- Type voices ---------------------------------------------------- */

.display {
  font-weight: 700;
  font-stretch: 112%; /* the expanded-caps display voice */
  letter-spacing: var(--tracking-display);
  line-height: 1.02;
  text-wrap: balance;
}

.label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--muted);
}

.lead {
  font-size: clamp(16px, 1.15vw + 12px, 19px);
  color: var(--muted);
  max-width: 62ch;
  text-wrap: pretty;
}

/* Specs and prices are read in columns — tabular figures, always. */
.num,
.spec,
.price {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* Archivo draws a stylised ampersand; brand-name text wants the classic glyph. */
.amp {
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}

/* ---------- Layout --------------------------------------------------------- */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* Tablet portrait to small desktop: the two fixed rails — the chapter dots on
   the left, the concierge tab on the right — float over the viewport edges,
   and the 24px gutter runs straight under them. The home chapters pull their
   content in so neither rail ever covers it; from ~1420px up the centring
   margins of the 1280px container already clear both, and below 721px the
   dot rail is hidden (the concierge tab alone remains an accepted overlay). */
@media (721px <= width <= 1420px) {
  .home2 .container {
    padding-inline: 64px;
  }
}

.page-main {
  min-height: 60vh;
}

.section {
  padding-block: clamp(56px, 7vw, 112px);
}

.section + .section {
  border-top: 1px solid var(--hairline);
}

/* ---------- Links ---------------------------------------------------------- */

a {
  color: inherit;
}

.link {
  text-decoration: none;
  text-underline-offset: 3px; /* DESIGN.md §3 */
  text-decoration-thickness: 1px;
}

.link:hover,
.link:focus-visible {
  text-decoration: underline;
}

/* ---------- Buttons — the hover grammar (DESIGN.md §3) --------------------- */

.btn {
  --btn-pad-y: 14px;
  --btn-pad-x: 26px;

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: 44px; /* touch target */
  padding: var(--btn-pad-y) var(--btn-pad-x);
  border: 1px solid transparent;
  border-radius: var(--radius);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  transition:
    background-color var(--dur) var(--ease),
    color var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

.btn--sm {
  --btn-pad-y: 9px;
  --btn-pad-x: 16px;

  min-height: 36px;
  font-size: 11px;
}

/* Filled: LIGHTENS on hover (#211F1C→#423D38 day, #F2EFEA→#DCD6CD night). */
.btn--filled {
  background: var(--ink);
  color: var(--page);
}

.btn--filled:hover,
.btn--filled:focus-visible {
  background: var(--ink-lift);
}

/* Outline & ghost: INVERT on hover (ink background, page-colour text). */
.btn--outline {
  border-color: var(--hairline-strong);
  background: transparent;
  color: var(--ink);
}

.btn--outline:hover,
.btn--outline:focus-visible {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--page);
}

.btn--ghost {
  background: transparent;
  color: var(--ink);
}

.btn--ghost:hover,
.btn--ghost:focus-visible {
  background: var(--ink);
  color: var(--page);
}

/* ---------- Header --------------------------------------------------------- */

.header {
  position: fixed;
  inset: 0 0 auto;
  z-index: 50;
  transition:
    background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease),
    color var(--dur) var(--ease),
    backdrop-filter var(--dur) var(--ease);
  border-bottom: 1px solid transparent;
}

/* Transparent over the hero: the nav sits on a lacquer→transparent scrim
   (~200 px) so it stays legible on ANY photograph (DESIGN.md §6). */
.header--hero::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 200px;
  pointer-events: none;
  opacity: 1;
  transition: opacity var(--dur) var(--ease);
  background: linear-gradient(
    to bottom,
    rgb(19 17 16 / 78%) 0%,
    rgb(19 17 16 / 46%) 45%,
    rgb(19 17 16 / 0%) 100%
  );
}

/* Scrolled (and every page without a hero): theme background, blur, hairline. */
.header--solid,
.header[data-scrolled="true"] {
  background: var(--overlay);
  backdrop-filter: saturate(140%) blur(12px);
  border-bottom-color: var(--hairline);
}

.header--solid::before,
.header[data-scrolled="true"]::before {
  opacity: 0;
}

/* Three columns so the nav is optically centred: brand · nav · tools. */
.header__inner {
  position: relative;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 24px;
  min-height: var(--header-h);
}

.header__brand {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  text-decoration: none;
  justify-self: start;
}

/* ---------- Nav ------------------------------------------------------------ */

.nav {
  display: flex;
  align-items: center;
  gap: 28px;
}

.nav__link {
  position: relative;
  padding-block: 6px;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  text-decoration: none;
  white-space: nowrap;
}

/* Nav underline in currentColor (DESIGN.md §3) — grows from the left. */
.nav__link::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur) var(--ease);
}

.nav__link:hover::after,
.nav__link:focus-visible::after,
.nav__link[aria-current="page"]::after {
  transform: scaleX(1);
}

.header__tools {
  display: flex;
  align-items: center;
  gap: 8px;
  justify-self: end;
}

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  transition:
    background-color var(--dur) var(--ease),
    color var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

/* Ghost grammar: inverts on hover. */
.icon-btn:hover,
.icon-btn:focus-visible {
  background: var(--ink);
  color: var(--page);
}

.icon-btn svg {
  width: 18px;
  height: 18px;
}

/* The theme toggle shows the icon of the theme it will switch TO. */
.theme-toggle__sun {
  display: none;
}

:root[data-theme="dark"] .theme-toggle__moon {
  display: none;
}

:root[data-theme="dark"] .theme-toggle__sun {
  display: block;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle__moon {
    display: none;
  }

  :root:not([data-theme="light"]) .theme-toggle__sun {
    display: block;
  }
}

.lang-switch {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  padding: 10px 12px;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
  transition:
    background-color var(--dur) var(--ease),
    color var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

.lang-switch:hover,
.lang-switch:focus-visible {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--page);
}

/* ---------- Mobile slide-nav ---------------------------------------------- */
/* The desktop nav is hidden ≤900px; this is its replacement — a hamburger (revealed by the island)
   opening a right-side sheet with the same links. Desktop never renders it. */
.nav-toggle {
  display: none; /* desktop: the inline nav is enough */
  /* The 18px glyph sits centred in a 40px button, so it floats ~11px inside the gutter and reads as
     off-right. Pull it back so the glyph lines up optically with the logo on the left. */
  margin-right: -10px;
}
@media (width <= 900px) {
  .nav-toggle:not([hidden]) {
    display: inline-flex; /* mobile, once the island has un-hidden it */
  }
}

.mobile-nav-scrim {
  position: fixed;
  inset: 0;
  z-index: 115;
  background: rgb(10 9 7 / 42%);
  opacity: 0;
  visibility: hidden;
  transition:
    opacity var(--dur-slow) var(--ease),
    visibility 0s linear var(--dur-slow);
}
.mobile-nav-scrim[data-open="true"] {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--dur-slow) var(--ease);
}

.mobile-nav {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 120;
  display: flex;
  flex-direction: column;
  width: min(360px, 86%);
  padding: 18px clamp(22px, 7vw, 34px) calc(34px + env(safe-area-inset-bottom, 0px));
  background: var(--page);
  border-left: 1px solid var(--hairline);
  box-shadow: 0 24px 70px -30px rgb(0 0 0 / 45%);
  overflow-y: auto;
  overscroll-behavior: contain;
  transform: translateX(104%);
  visibility: hidden;
  transition:
    transform var(--dur-slow) var(--ease),
    visibility 0s linear var(--dur-slow);
}
.mobile-nav[data-open="true"] {
  transform: none;
  visibility: visible;
  transition: transform var(--dur-slow) var(--ease);
}
.mobile-nav__head {
  display: flex;
  justify-content: flex-end;
  min-height: var(--header-h);
  align-items: center;
}
.mobile-nav__links {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 8px;
}
.mobile-nav__link {
  padding: 14px 2px;
  border-bottom: 1px solid var(--hairline);
  color: var(--ink);
  text-decoration: none;
  font-size: 22px;
  font-weight: 500;
  letter-spacing: 0.01em;
  transition: color var(--dur) var(--ease);
}
.mobile-nav__link:hover,
.mobile-nav__link:focus-visible {
  color: var(--muted);
  outline: none;
}
.mobile-nav__link[aria-current="page"] {
  color: var(--muted);
}
/* Language + contrast, moved off the mobile header into the sheet's foot. */
.mobile-nav__foot {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-top: 26px;
}
.mobile-nav__lang {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
@media (width > 900px) {
  .mobile-nav,
  .mobile-nav-scrim {
    display: none; /* the sheet belongs to mobile only */
  }
}
@media (prefers-reduced-motion: reduce) {
  .mobile-nav,
  .mobile-nav-scrim {
    transition: none;
  }
}

/* ---------- Logo ----------------------------------------------------------- */

/* The official lockup, painted in currentColor through an SVG mask so it follows
   its context (ivory on dark folds, ink on white) — DESIGN.md §5. */
.logo {
  display: block;
  height: 22px;
  aspect-ratio: 1230 / 185; /* the lockup SVG's own ratio */
  background: currentColor;
  -webkit-mask: url("/static/img/logo-lockup.svg") left center / contain no-repeat;
  mask: url("/static/img/logo-lockup.svg") left center / contain no-repeat;
}

.logo--lg {
  height: 30px;
}

/* ---------- Hero ----------------------------------------------------------- */

.hero {
  position: relative;
  display: flex;
  align-items: flex-end;
  min-height: min(88vh, 820px);
  padding-top: var(--header-h);
  padding-bottom: clamp(48px, 6vw, 96px);
  overflow: hidden;
  background: var(--dd-lacquer);
}

.hero__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* One settle, 2.4 s (DESIGN.md §7). */
  animation: hero-settle var(--hero-settle) var(--ease) both;
}

/* Two-axis scrim: bottom-heavy for the copy block, plus a left wash so the
   headline holds its contrast whatever car sits behind it. */
.hero__veil {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(
      to top,
      rgb(19 17 16 / 92%) 0%,
      rgb(19 17 16 / 62%) 38%,
      rgb(19 17 16 / 16%) 100%
    ),
    linear-gradient(to right, rgb(19 17 16 / 72%) 0%, rgb(19 17 16 / 0%) 58%);
}

.hero__content {
  position: relative;
  width: 100%;
}

.hero__title {
  font-size: clamp(40px, 6.4vw, 84px);
  margin-block: 18px 20px;
}

/* On a photograph the smoke-gray secondary is too weak: the hero lead keeps the
   ivory ink, softened by opacity (still ≥ 7:1 over the scrim). */
.hero__content .lead {
  color: var(--ink);
  opacity: 0.88;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 32px;
}

/* On the photo, the outline button inverts to WHITE (DESIGN.md §3). */
.on-photo .btn--outline:hover,
.on-photo .btn--outline:focus-visible {
  background: var(--dd-ivory);
  border-color: var(--dd-ivory);
  color: var(--dd-lacquer);
}

@keyframes hero-settle {
  from {
    opacity: 0;
    transform: scale(1.06);
  }

  to {
    opacity: 1;
    transform: scale(1);
  }
}

/* ---------- Placeholder body (WP-4/5/8 fill these) ------------------------- */

.placeholder {
  padding-top: calc(var(--header-h) + clamp(56px, 8vw, 120px));
  padding-bottom: clamp(72px, 10vw, 160px);
}

.placeholder__badge {
  display: inline-block;
  padding: 6px 12px;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
  margin-bottom: 24px;
}

.placeholder__title {
  font-size: clamp(32px, 4.6vw, 56px);
}

.placeholder__body {
  margin-top: 20px;
}

.placeholder__actions {
  margin-top: 36px;
}

/* ---------- Home shell sections -------------------------------------------- */

.strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 32px;
  border-top: 1px solid var(--hairline);
  padding-top: 32px;
}

.strip__item {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.strip__value {
  font-size: clamp(24px, 2.4vw, 34px);
  font-weight: 700;
  font-stretch: 108%;
  letter-spacing: var(--tracking-display);
}

/* =============================================================================
 * Listing (WP-4) — /vehicules · /en/vehicles
 * Visual contract: the listing mockup (DESIGN.md §8) — filter rail with chips,
 * makes + counts, ranges, active-filter chips, cards WITH price, pagination,
 * mobile bottom-sheet.
 *
 * Monochrome grammar: chips and pager pages INVERT when active; the card lifts
 * and its photograph scales. No hue anywhere. All colours are tokens.
 * ========================================================================== */

.listing {
  padding-top: calc(var(--header-h) + clamp(24px, 3vw, 40px));
  padding-bottom: clamp(56px, 8vw, 96px);

  /* Theme-aware elevation (the only shadow of the site, on the card hover). */
  --card-shadow: 0 18px 50px -22px rgb(20 16 10 / 35%);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .listing {
    --card-shadow: 0 18px 50px -22px rgb(0 0 0 / 60%);
  }
}

:root[data-theme="dark"] .listing {
  --card-shadow: 0 18px 50px -22px rgb(0 0 0 / 60%);
}

:root[data-theme="light"] .listing {
  --card-shadow: 0 18px 50px -22px rgb(20 16 10 / 35%);
}

/* Screen-reader-only (range labels, pager prefixes). */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- Page head ------------------------------------------------------ */

.page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
  padding-bottom: clamp(18px, 3vw, 30px);
}

.listing__heading {
  font-size: clamp(30px, 4vw, 48px);
  text-transform: uppercase;
  margin-top: 6px;
}

.page-head__tools {
  display: flex;
  align-items: center;
  gap: 18px;
  flex-wrap: wrap;
}

.count {
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
}

.sort {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  color: var(--muted);
}

.select {
  font: inherit;
  font-size: 13px;
  background: var(--page);
  color: var(--ink);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
  padding: 9px 12px;
  cursor: pointer;
  transition: border-color var(--dur) var(--ease);
}

.select:hover {
  border-color: var(--ink);
}

/* The sort applies on `change` (island). Without JS the `<noscript>` sheet brings this submit
   button back — the default state assumes JS, so nothing ever flashes on screen. */
.sort__apply {
  display: none;
}

/* ---------- Layout: rail + results ----------------------------------------- */

.layout {
  display: grid;
  grid-template-columns: 264px 1fr;
  gap: clamp(24px, 3vw, 44px);
  align-items: start;
}

.results {
  min-width: 0;
  transition: opacity var(--dur-fast) var(--ease);
  /* The pager's fetch-swap scrolls back up to the results — under the fixed header, not behind it. */
  scroll-margin-top: calc(var(--header-h) + 16px);
}

/* The live-filter island marks the results while a fetch is in flight: the stale grid dims
   instead of flashing away, and clicks are held until the fresh one lands. */
.results[aria-busy="true"] {
  opacity: 0.45;
  pointer-events: none;
}

/* ---------- Filter rail ---------------------------------------------------- */

.rail {
  position: sticky;
  top: calc(var(--header-h) + 16px);
  display: grid;
  gap: 26px;
  max-height: calc(100vh - var(--header-h) - 32px);
  overflow-y: auto;
  overflow-x: hidden;
  /* A long make list makes the rail scroll. The scrollbar is a THIN one whose thumb only paints
     while the pointer is over the rail (the classic UA bar looked like machinery in a gallery);
     the stable gutter still reserves its lane so the facet counts never shift when it appears.
     Older Safari falls back to its native overlay bar — acceptable. */
  scrollbar-gutter: stable;
  scrollbar-width: thin;
  scrollbar-color: transparent transparent;
  transition: scrollbar-color var(--dur) var(--ease);
  /* Right: clearance for the overlay-style scrollbar. Bottom: breathing room at the end of the
     scroll — without it the last control sits flush against the clipped edge. Left: the focus
     ring (2px + 3px offset) of a checkbox/chip at the rail's edge needs 5px of clip box beyond
     the content or `overflow` truncates it — the negative margin keeps the content optically
     aligned with the page grid. `scroll-padding` keeps the ring visible when tabbing scrolls. */
  padding-right: 12px;
  padding-bottom: 24px;
  padding-left: 8px;
  margin-left: -8px;
  scroll-padding-block: 8px;
}

@media (hover: hover) {
  .rail:hover,
  .rail:focus-within {
    scrollbar-color: var(--hairline-strong) transparent;
  }
}

/* No hover on touch: the thumb stays (thin, hairline) or the rail would have no scroll cue. */
@media (hover: none) {
  .rail {
    scrollbar-color: var(--hairline-strong) transparent;
  }
}

.rail__head {
  display: none; /* the sheet header only exists on mobile */
  align-items: center;
  justify-content: space-between;
}

.rail__title {
  font-stretch: 116%;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.rail__actions {
  display: grid;
  gap: 12px;
  justify-items: start;
}

/* With JS the filters apply LIVE on desktop — the submit would be a dead button, so it is hidden
   (the `<noscript>` sheet brings it back). On the mobile sheet it stays: "Voir les véhicules"
   closes the sheet over already-updated results. When nothing is left to show (no reset link
   either), the whole group disappears — an empty fieldset would leave an orphan hairline and
   18px of dead padding at the rail's foot. */
@media (min-width: 961px) {
  .rail__actions .btn {
    display: none;
  }

  .rail__actions:not(:has(.reset)) {
    display: none;
  }
}

/* Filter groups are <fieldset>/<legend> — reset the UA chrome. `min-inline-size: 0` is not
   cosmetic: a fieldset defaults to `min-content` and refuses to shrink, which would push the
   facet counts and the number inputs outside the rail. */
.fgroup {
  border: 0;
  border-top: 1px solid var(--hairline);
  margin: 0;
  padding: 18px 0 0;
  min-inline-size: 0;
}

.fgroup:first-of-type {
  border-top: 0;
  padding-top: 0;
}

.fgroup__title {
  padding: 0;
  margin-bottom: 14px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--muted);
}

/* ---------- Chips (category) — a radio whose label IS the chip -------------- */

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding: 7px 15px;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  transition:
    background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease),
    color var(--dur) var(--ease);
}

/* The input stays focusable (keyboard) but invisible — the chip is the control. */
.chip__input {
  position: absolute;
  opacity: 0;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
}

.chip:hover {
  border-color: var(--ink);
}

/* Checked = INVERTED (the monochrome "selected" grammar). */
.chip:has(.chip__input:checked) {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--page);
}

.chip:has(.chip__input:focus-visible) {
  outline: 2px solid var(--focus-ring);
  outline-offset: 3px;
}

/* ---------- Facet checks (makes, energies) --------------------------------- */

.checks {
  display: grid;
  gap: 10px;
  font-size: 14px;
}

.check {
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  min-height: 24px;
  min-width: 0; /* grid items default to min-content: a long make would push the count out */
}

.check input {
  accent-color: var(--ink);
  width: 15px;
  height: 15px;
  flex: 0 0 auto;
}

.check__label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.check .n {
  margin-left: auto;
  color: var(--muted);
  font-size: 12px;
}

.check:hover .check__label {
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* Hidden by the make-search island when the make does not match. */
.check[hidden] {
  display: none;
}

.search-in {
  width: 100%;
  font: inherit;
  font-size: 13px;
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: 9px 12px;
  margin-bottom: 12px;
}

.search-in::placeholder {
  color: var(--muted);
}

/* ---------- Ranges (price, power) ------------------------------------------ */
/* No-JS: two number inputs. With JS the island injects .range-track above them. */

.range-vals {
  display: flex;
  align-items: center;
  gap: 8px;
}

.range-field {
  flex: 1 1 0;
  min-width: 0;
}

.range-num {
  width: 100%;
  min-width: 0; /* an <input type=number> will not shrink below its default size otherwise */
  font: inherit;
  font-size: 13px;
  background: var(--page);
  color: var(--ink);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
  padding: 9px 10px;
  transition: border-color var(--dur) var(--ease);
}

.range-num:hover,
.range-num:focus-visible {
  border-color: var(--ink);
}

.range-num::placeholder {
  color: var(--muted);
}

.range-sep {
  color: var(--muted);
}

.range-track {
  position: relative;
  height: 2px;
  background: var(--hairline-strong);
  margin: 20px 8px 14px;
}

.range-fill {
  position: absolute;
  inset: 0 auto 0 0;
  background: var(--ink);
}

/* The two overlaid sliders: only their thumbs are hit-testable. */
.range-input {
  position: absolute;
  left: -8px;
  right: -8px;
  top: 50%;
  width: calc(100% + 16px);
  margin: 0;
  transform: translateY(-50%);
  background: none;
  pointer-events: none;
  appearance: none;
  -webkit-appearance: none;
}

.range-input:focus-visible {
  outline: none;
}

.range-input::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  pointer-events: auto;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--page);
  border: 2px solid var(--ink);
  cursor: grab;
}

.range-input::-moz-range-thumb {
  pointer-events: auto;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--page);
  border: 2px solid var(--ink);
  cursor: grab;
}

.range-input:focus-visible::-webkit-slider-thumb {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.range-input:focus-visible::-moz-range-thumb {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* ---------- Reset link ----------------------------------------------------- */

.reset {
  background: none;
  border: 0;
  padding: 0;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  cursor: pointer;
  text-decoration: none;
}

.reset:hover,
.reset:focus-visible {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ---------- Active filters ------------------------------------------------- */

.active-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 20px;
}

.afilter {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  font-size: 12px;
  font-weight: 500;
  padding: 6px 10px;
  text-decoration: none;
  transition:
    background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease),
    color var(--dur) var(--ease);
}

/* Ghost grammar: the whole chip inverts, announcing "click = remove". */
.afilter:hover,
.afilter:focus-visible {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--page);
}

.afilter__x {
  color: var(--muted);
  line-height: 1;
}

.afilter:hover .afilter__x,
.afilter:focus-visible .afilter__x {
  color: var(--page);
}

/* ---------- Card grid ------------------------------------------------------ */

.grid-cars {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: clamp(18px, 2.2vw, 28px);
}

.car {
  /* When the concierge takes the visitor to a card it annotated, the fixed header must not cover it. */
  scroll-margin-top: calc(var(--header-h) + 16px);
  position: relative;
  display: block;
  background: var(--page);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  text-decoration: none;
  transition:
    box-shadow var(--dur) var(--ease),
    transform var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

.car:hover,
.car:focus-visible {
  box-shadow: var(--card-shadow);
  border-color: var(--hairline-strong);
  transform: translateY(-3px);
}

.car-media {
  position: relative;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  background: var(--surface);
}

.car-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease);
}

.car:hover .car-media img {
  transform: scale(1.04);
}

/* ---------- Card slideshow (Swiper) ---------------------------------------- */
/* The card's photo is a swipeable, autoplaying slideshow (island). Un-initialised — no JS, script
   still loading, vendor missing — the first slide alone shows: exactly the old static card. */

.car-swiper {
  height: 100%;
}

/* Clip each slide to its own frame: the hover zoom (scale 1.04) would otherwise, mid-transition
   when two slides share the viewport, spill its scaled edge over the incoming slide and let the
   neighbour show through at the seam. Per-slide clipping keeps every zoom inside its own photo. */
.car-swiper .swiper-slide {
  overflow: hidden;
}

.car-swiper:not(.swiper-initialized) .swiper-slide:nth-child(n + 2) {
  display: none;
}

/* Monochrome bullets ON the photograph: ivory (the on-photo ink, DESIGN.md), hairlined with
   lacquer so they read on a light coachwork. No hue — Swiper's blue default must never paint. */
.car-swiper .swiper-pagination {
  pointer-events: none; /* the whole card is a link — bullets indicate, never intercept */
}

.car-swiper .swiper-pagination-bullet {
  width: 6px;
  height: 6px;
  background: var(--dd-ivory);
  box-shadow: 0 0 0 1px rgb(19 17 16 / 30%);
  opacity: 0.55;
  transition: opacity var(--dur) var(--ease);
}

.car-swiper .swiper-pagination-bullet-active {
  opacity: 1;
}

.car-media__empty {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* "NEUF" ribbon. Above the slideshow AND its bullets (Swiper's pagination sits at z-index 10). */
.badge {
  position: absolute;
  top: 14px;
  left: 14px;
  z-index: 11;
  background: var(--ink);
  color: var(--page);
  font-size: 10px;
  font-weight: 700;
  font-stretch: 112%;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 6px 10px;
  border-radius: var(--radius);
}

.car-body {
  padding: 18px 20px 20px;
}

.car-make {
  display: block;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
}

.car-title {
  font-size: 16px;
  font-weight: 600;
  font-stretch: 112%;
  line-height: 1.25;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  margin: 6px 0 14px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  min-height: 2.5em;
}

.car-specs {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 4px 14px;
  padding: 13px 0;
  border-top: 1px solid var(--hairline);
  font-size: 13px;
  color: var(--muted);
}

.car-specs span + span::before {
  content: "·";
  margin-right: 14px;
  color: var(--hairline-strong);
}

/* Price only (the VAT scheme lives on the fiche, not in the grid). */
.car-price {
  padding-top: 14px;
  border-top: 1px solid var(--hairline);
}

.car-price .price {
  font-size: 18px;
  font-weight: 650;
  font-stretch: 116%;
}

/* ---------- Empty state ---------------------------------------------------- */

.empty {
  padding: clamp(48px, 8vw, 96px) 0;
  border-top: 1px solid var(--hairline);
  text-align: center;
}

.empty__title {
  font-size: clamp(20px, 2.2vw, 26px);
  font-weight: 600;
  font-stretch: 108%;
  margin-bottom: 12px;
}

.empty .lead {
  margin-inline: auto;
}

.empty__actions {
  margin-top: 28px;
}

/* ---------- Pagination ----------------------------------------------------- */

.pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin-top: clamp(32px, 5vw, 52px);
}

.pager__page,
.pager__step {
  min-width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding-inline: 8px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  text-decoration: none;
  transition:
    background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease),
    color var(--dur) var(--ease);
}

a.pager__page:hover,
a.pager__page:focus-visible,
.pager__step:hover,
.pager__step:focus-visible {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--page);
}

.pager__page[aria-current="page"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--page);
}

.pager__gap {
  color: var(--muted);
  padding-inline: 4px;
}

/* ---------- Mobile: bottom-sheet rail --------------------------------------
 * The DEFAULT stylesheet assumes JavaScript (sheet + FAB), and the page's
 * `<noscript>` block reverts it to a plain stacked rail. Doing it this way
 * round means NEITHER visitor ever sees a flash: the no-JS styles land with the
 * document, not when a script gets around to stamping a class on <html>.
 * ------------------------------------------------------------------------- */

.fab,
.scrim {
  display: none; /* desktop: neither exists */
}

@media (width <= 960px) {
  .layout {
    grid-template-columns: 1fr;
  }

  .rail {
    position: fixed;
    inset: auto 0 0;
    z-index: 60;
    max-height: 78vh;
    overflow: auto;
    margin: 0;
    padding: 22px clamp(20px, 5vw, 32px) 0;
    background: var(--page);
    border: 0;
    border-top: 1px solid var(--hairline);
    box-shadow: var(--card-shadow);
    transform: translateY(105%);
    transition: transform var(--dur-slow) var(--ease);
    visibility: hidden;
  }

  .rail[data-open="true"] {
    transform: translateY(0);
    visibility: visible;
  }

  .rail__head {
    display: flex;
  }

  /* The sheet's footer: "Voir les véhicules" stays reachable while the filter list scrolls
     (the results are already fresh behind the sheet — the button just closes it). */
  .rail__actions {
    position: sticky;
    bottom: 0;
    z-index: 1;
    margin: 0 calc(-1 * clamp(20px, 5vw, 32px));
    padding: 14px clamp(20px, 5vw, 32px) 18px;
    background: var(--page);
    border-top: 1px solid var(--hairline);
    justify-items: stretch;
  }

  .rail__actions .btn {
    justify-content: center;
  }

  .rail__actions .reset {
    text-align: center;
  }

  .fab {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    position: fixed;
    bottom: calc(22px + env(safe-area-inset-bottom, 0px));
    left: 50%;
    transform: translateX(-50%);
    z-index: 55;
    background: var(--ink);
    color: var(--page);
    border-radius: var(--radius);
    padding: 15px 28px;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.13em;
    text-transform: uppercase;
    box-shadow: var(--card-shadow);
  }

  .fab__count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 20px;
    height: 20px;
    padding-inline: 5px;
    border-radius: 10px;
    background: var(--page);
    color: var(--ink);
    font-size: 11px;
  }

  /* Always in the markup (the live-filter island swaps its text) — invisible when no filter. */
  .fab__count:empty {
    display: none;
  }

  .scrim[data-open="true"] {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 50;
    background: rgb(10 9 7 / 40%);
  }

  .page-head__tools {
    width: 100%;
    justify-content: space-between;
  }
}

/* ---------- Contact drawer -------------------------------------------------
 * The always-present side panel: ask about a car (RDV / leasing / question)
 * WITHOUT leaving the page. Whole block is a no-JS no-op: the handle ships
 * [hidden], the panel is visibility:hidden, the fiche CTAs stay plain links.
 * Z-scale: above the fixed header (100) — drawer 110, its scrim 105; the
 * handle sits UNDER every overlay (40).
 * ------------------------------------------------------------------------- */

.drawer-handle {
  position: fixed;
  right: 0;
  top: 50%;
  z-index: 40;
  transform: translateY(-50%);
  writing-mode: vertical-rl;
  min-height: 44px;
  padding: 18px 11px;
  background: var(--ink);
  color: var(--page);
  border: 0;
  border-radius: var(--radius) 0 0 var(--radius);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background-color var(--dur) var(--ease);
}

.drawer-handle:hover {
  background: var(--ink-lift);
}

.drawer-handle:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.drawer-scrim {
  position: fixed;
  inset: 0;
  z-index: 105;
  background: rgb(10 9 7 / 40%);
  opacity: 0;
  visibility: hidden;
  transition:
    opacity var(--dur-slow) var(--ease),
    visibility 0s linear var(--dur-slow);
}

.drawer-scrim[data-open="true"] {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--dur-slow) var(--ease);
}

.drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 110;
  display: flex;
  flex-direction: column;
  gap: 22px;
  /* Wide enough that "Objet de la demande" + its OBLIGATOIRE flag hold on ONE line per column
     (measured: ≥230px a column; 580 gives 242 with the shared grid's gap and this padding). */
  width: min(680px, 100%);
  padding: 24px clamp(22px, 3vw, 36px) calc(28px + env(safe-area-inset-bottom, 0px));
  overflow-y: auto;
  background: var(--page);
  border-left: 1px solid var(--hairline);
  box-shadow: 0 24px 70px -30px rgb(0 0 0 / 45%);
  transform: translateX(104%);
  visibility: hidden;
  transition:
    transform var(--dur-slow) var(--ease),
    visibility 0s linear var(--dur-slow);
  /* Same overlay-scrollbar treatment as the filter rail. */
  scrollbar-width: thin;
  scrollbar-color: transparent transparent;
}

.drawer[data-open="true"] {
  transform: none;
  visibility: visible;
  transition: transform var(--dur-slow) var(--ease);
}

@media (hover: hover) {
  .drawer:hover,
  .drawer:focus-within {
    scrollbar-color: var(--hairline-strong) transparent;
  }
}

/* The island drives visibility with the `hidden` attribute; the display classes below (grid,
   flex) would silently defeat the UA's `[hidden]` rule without this guard. */
.drawer [hidden] {
  display: none !important;
}

/* The showroom's coordinates inside the drawer — a quiet block under the form (address, phone, email,
   hours), separated by a hairline. Same facts as the /contact aside, compact for the panel. */
.drawer__info {
  margin-top: 26px;
  padding-top: 22px;
  border-top: 1px solid var(--hairline);
  display: flex;
  flex-direction: column;
  gap: 10px;
  font-size: 14px;
  line-height: 1.5;
}
.drawer__info .label {
  margin-bottom: 2px;
}
.drawer__info-block {
  margin: 0;
  font-style: normal;
}
.drawer__info-block span {
  display: block;
}
.drawer__info-note {
  margin: 0;
  font-size: 13px;
  color: var(--muted);
}

.drawer__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--hairline);
}

.drawer__title {
  font-stretch: 116%;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.drawer__form .form__actions {
  margin-top: 18px;
}

.drawer__form .form__actions .btn {
  width: 100%;
  justify-content: center;
}

/* The confirmation the drawer folds over. */
.drawer__sent {
  display: grid;
  gap: 10px;
  align-content: center;
  justify-items: start;
  flex: 1;
  padding-block: 24px;
}

.drawer__sent-title {
  font-size: 22px;
  font-weight: 650;
  font-stretch: 112%;
  line-height: 1.2;
  text-wrap: balance;
}

.drawer__sent-body {
  color: var(--muted);
  max-width: 40ch;
}

/* ---------- Footer --------------------------------------------------------- */

.footer {
  border-top: 1px solid var(--hairline);
  padding-block: clamp(48px, 6vw, 80px) 32px;
  /* Always-dark slab in absolute black. Flip the foreground tokens the way the dark
     theme does, so text stays legible on #000 in BOTH light and dark themes. */
  background: #000;
  --ink: var(--dd-ivory);
  --muted: var(--dd-smoke);
  --hairline: rgb(242 239 234 / 16%);
  --hairline-strong: rgb(242 239 234 / 32%);
  --page: #000; /* the outline button's hover invert needs the flipped pair */
  color: var(--ink);
}

.footer__grid {
  display: grid;
  grid-template-columns: 1.4fr repeat(3, minmax(0, 1fr));
  gap: 40px;
}

.footer__col {
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
}

.footer__title {
  margin-bottom: 4px;
}

.footer__list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  list-style: none;
  padding: 0;
}

.footer__list a,
.footer__address a {
  font-size: 14px;
}

.footer__address {
  font-style: normal;
  font-size: 14px;
  color: var(--muted);
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* Icon + text rows for the contact facts (footer + contact drawer): address,
   phone, email, Instagram. The icon sits outside the <a> so the hover
   underline stays on the text alone. */
.contact-row {
  display: flex;
  align-items: flex-start;
  gap: 8px;
}

.contact-row__icon {
  flex: none;
  /* Optically centre the 21px glyph on the first 14px/1.55 text line. */
  margin-top: 0;
  color: var(--muted);
}

/* The discreet "S'y rendre" button under the address block — both host columns
   are stretch flex columns, so pin it to the start. */
.contact-directions {
  align-self: flex-start;
  margin-top: 4px;
}

.footer__tagline {
  font-size: 14px;
  color: var(--muted);
  max-width: 34ch;
  margin-top: 16px;
}

.footer__bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-top: clamp(40px, 5vw, 64px);
  padding-top: 24px;
  border-top: 1px solid var(--hairline);
  font-size: 12px;
  color: var(--muted);
}

.footer__legal {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  list-style: none;
  padding: 0;
}

/* ---------- Responsive ----------------------------------------------------- */

@media (width <= 900px) {
  .nav {
    display: none; /* replaced by the slide-nav sheet */
  }

  /* With the nav gone, the 3-column grid would strand the tools in the middle (an empty 1fr column
     to their right) — collapse to brand · tools so the hamburger sits flush right. */
  .header__inner {
    grid-template-columns: 1fr auto;
  }

  /* Language + contrast + the bespoke-search entry leave the header on mobile — they live in the
     slide-nav, so only the hamburger remains on the right. */
  .header__tools .lang-switch,
  .header__tools [data-theme-toggle],
  .header__tools .bespoke-link {
    display: none;
  }

  .footer__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (width <= 560px) {
  .footer__grid {
    grid-template-columns: 1fr;
  }

  .hero {
    min-height: 78vh;
  }
}

/* ---------- Reduced motion (DESIGN.md §7 — full fallback, mandatory) -------- */

@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;
  }

  .hero__media {
    animation: none;
  }
}

/* =============================================================================
 * Vehicle detail — the fiche (WP-5) · /vehicules/:slug · /en/vehicles/:slug
 * Visual contract: the detail mockup (DESIGN.md §8) — gallery + lightbox, spec
 * grid, warranty block, description, equipment, sticky price rail, similar strip.
 *
 * The price rail is `.price-rail`, NOT `.rail`: the listing's filter rail owns
 * that class (and turns into a mobile bottom-sheet under 960 px).
 * ========================================================================== */

.fiche {
  padding-top: calc(var(--header-h) + clamp(16px, 2vw, 28px));
  padding-bottom: clamp(56px, 8vw, 96px);

  /* Same elevation token as the listing: the similar cards hover identically. */
  --card-shadow: 0 18px 50px -22px rgb(20 16 10 / 35%);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .fiche {
    --card-shadow: 0 18px 50px -22px rgb(0 0 0 / 60%);
  }
}

:root[data-theme="dark"] .fiche {
  --card-shadow: 0 18px 50px -22px rgb(0 0 0 / 60%);
}

:root[data-theme="light"] .fiche {
  --card-shadow: 0 18px 50px -22px rgb(20 16 10 / 35%);
}

/* ---------- Breadcrumbs + head --------------------------------------------- */

.crumbs {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding-block: 22px 0;
  font-size: 13px;
  color: var(--muted);
}

.crumbs__current {
  color: var(--ink);
}

.fiche-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
  padding-block: 14px 26px;
}

.fiche-head__make {
  display: block;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink);
}

.fiche-head__title {
  font-size: clamp(26px, 3.2vw, 40px);
  text-transform: uppercase;
  margin-top: 8px;
  /* ⚠️ ch measures the "0" glyph (~0.44em here) — these are UPPERCASE display caps (~0.75em
     each), so 52ch ≈ 30 characters per line, not 52. */
  max-width: 52ch;
}

.fiche-head__meta {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 26px;
  font-size: 14px;
  color: var(--muted);
}

/* The wide title (52ch) usually wraps the meta onto its own row — keep it anchored to the right
   there, as it was when it still fit beside the title. Desktop only: the mobile head has always
   stacked with the meta on the left. */
@media (width > 700px) {
  .fiche-head__meta {
    margin-left: auto;
  }
}

/* ---------- Gallery --------------------------------------------------------
 * Every tile is an <a> to the full-size photo: the lightbox is enhancement, and
 * without JavaScript the links still open the photographs.
 * ------------------------------------------------------------------------- */

.gallery {
  display: grid;
  grid-template-columns: 2.2fr 1fr;
  gap: 10px;
  /* Clips the directional entrance slides (main from the left, side tiles from top / bottom) so a
     translated tile never overflows the page or triggers a scrollbar. `overflow: clip` with a
     small clip-margin does the same but still lets a FOCUSED tile's outline (2px @ 3px offset)
     show through instead of being shaved off — Tab focus on the three lead photos must stay
     visible. `overflow: hidden` is the fallback for browsers without `clip`. */
  overflow: hidden;
  overflow: clip;
  overflow-clip-margin: 6px;
}

.gallery--empty {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 3 / 1;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
}

.g-main,
.g-side__item,
.g-thumb {
  position: relative;
  display: block;
  overflow: hidden;
  background: var(--surface);
  border-radius: var(--radius);
  cursor: zoom-in;
}

.g-main {
  aspect-ratio: 3 / 2;
}

.g-main img,
.g-side__item img,
.g-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease);
}

.g-main:hover img,
.g-side__item:hover img {
  transform: scale(1.03);
}

/* On the photograph, ivory ink whatever the theme (tokens.css `.on-photo` rule). */
.g-count,
.g-more {
  position: absolute;
  color: var(--dd-ivory);
  background: rgb(10 9 7 / 62%);
  font-weight: 600;
  letter-spacing: 0.08em;
  backdrop-filter: blur(2px);
}

.g-count {
  right: 12px;
  bottom: 12px;
  padding: 8px 14px;
  border-radius: var(--radius);
  font-size: 12px;
}

.g-more {
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  /* A plain dark veil over the last tile — no blur (overrides the shared .g-count/.g-more
     backdrop-filter); a touch more opaque so the "+ N photos" reads without the blur. */
  background: rgb(10 9 7 / 66%);
  backdrop-filter: none;
}

.g-side {
  display: grid;
  grid-template-rows: 1fr 1fr;
  gap: 10px;
}

.g-thumbs {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(86px, 1fr));
  gap: 8px;
  margin-top: 10px;
}

.g-thumb {
  aspect-ratio: 3 / 2;
  border: 2px solid transparent;
  opacity: 0.82;
  transition:
    opacity var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

.g-thumb:hover,
.g-thumb:focus-visible,
.g-thumb[aria-current="true"] {
  opacity: 1;
  border-color: var(--ink);
}

@media (width <= 820px) {
  .gallery {
    grid-template-columns: 1fr;
  }

  .g-side {
    grid-template-columns: 1fr 1fr;
    grid-template-rows: auto;
  }

  .g-side__item {
    aspect-ratio: 3 / 2;
  }
}

/* ---------- Layout: content + price rail ----------------------------------- */

.fiche-layout {
  display: grid;
  grid-template-columns: 1fr 380px;
  gap: clamp(28px, 4vw, 56px);
  align-items: start;
  padding-block: clamp(30px, 4.5vw, 52px) 0;
}

.fiche-main {
  min-width: 0;
}

@media (width <= 980px) {
  .fiche-layout {
    grid-template-columns: 1fr;
  }

  /* On a phone the price comes FIRST: it is the question the visitor is asking. */
  .price-rail {
    order: -1;
  }
}

/* ---------- Spec grid ------------------------------------------------------ */

.specs {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  border-top: 1px solid var(--hairline);
  border-left: 1px solid var(--hairline);
  margin: 0;
}

.spec {
  border-right: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
  padding: 16px 18px;
}

.spec dt {
  margin-bottom: 6px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--muted);
}

.spec dd {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  font-stretch: 108%;
}

/* ---------- Content blocks -------------------------------------------------- */

.block {
  margin-top: clamp(36px, 5vw, 56px);
}

.block__title {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 18px;
  font-size: 18px;
  font-weight: 650;
  font-stretch: 112%;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

/* The hairline that runs off the heading — the mockup's signature block rule. */
.block__title::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--hairline);
}

.prose {
  max-width: 68ch;
}

.prose p {
  margin-block: 0.9em;
  text-wrap: pretty;
}

.prose ul,
.prose ol {
  margin-block: 0.9em;
  padding-left: 1.3em;
}

.prose li {
  margin-block: 0.3em;
}

.prose h3,
.prose h4 {
  margin-block: 1.2em 0.5em;
  font-size: 16px;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.prose blockquote {
  margin-block: 1em;
  padding-left: 16px;
  border-left: 2px solid var(--hairline-strong);
  color: var(--muted);
}

/* ---------- Warranty / inspection / service book --------------------------- */

.assurances {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
}

.assurance {
  padding: 20px 22px;
  border-right: 1px solid var(--hairline);
}

.assurance:last-child {
  border-right: 0;
}

.assurance strong {
  display: block;
  margin-bottom: 4px;
  font-size: 14px;
  font-weight: 650;
  font-stretch: 108%;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.assurance span {
  font-size: 14px;
  color: var(--muted);
}

@media (width <= 700px) {
  .assurance {
    border-right: 0;
    border-bottom: 1px solid var(--hairline);
  }

  .assurance:last-child {
    border-bottom: 0;
  }
}

/* ---------- Equipment / accessories ---------------------------------------- */

.equip {
  columns: 2;
  column-gap: 3em;
  max-width: 76ch;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 15px;
}

.equip li {
  break-inside: avoid;
  padding-block: 8px;
  border-bottom: 1px solid var(--hairline);
}

/* The em-dash bullet: monochrome, and it inherits the ink. */
.equip li::before {
  content: "—";
  margin-right: 12px;
  color: var(--ink);
}

@media (width <= 700px) {
  .equip {
    columns: 1;
  }
}

/* ---------- Price rail ------------------------------------------------------ */

.price-rail {
  position: sticky;
  top: calc(var(--header-h) + 16px);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--page);
}

@media (width <= 980px) {
  .price-rail {
    position: static;
  }
}

.price-rail__price {
  padding: 26px 26px 22px;
  border-bottom: 1px solid var(--hairline);
}

.price-rail__amount {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-top: 6px;
}

.price-rail__amount .price {
  font-size: 30px;
  font-weight: 670;
  font-stretch: 116%;
  letter-spacing: 0.01em;
}

.price-rail__vat-included {
  font-size: 14px;
  color: var(--muted);
}

/* The VAT pill — the fiche's most load-bearing line of copy (recoverable vs margin scheme). */
.vat-note {
  display: inline-flex;
  align-items: center;
  margin-top: 12px;
  padding: 7px 11px;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink);
}

.price-rail__ctas {
  display: grid;
  gap: 10px;
  padding: 22px 26px;
}

.price-rail__ctas .btn {
  width: 100%;
}

.price-rail__contact {
  display: grid;
  gap: 8px;
  padding: 18px 26px 22px;
  border-top: 1px solid var(--hairline);
  font-size: 15px;
}

.price-rail__hours {
  font-size: 13px;
  color: var(--muted);
}

/* ---------- Similar strip ---------------------------------------------------- */

.fiche-similar {
  margin-top: clamp(48px, 7vw, 88px);
}

/* ---------- Lightbox (enhancement only — `hidden` until the island runs) ----- */

/* Full-screen gallery: header (counter + close), main viewer (1fr), thumbnail rail. */
.lb {
  position: fixed;
  inset: 0;
  z-index: 120; /* a modal — above the floating contact tab and everything else */
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  gap: 12px;
  padding: 14px clamp(12px, 3vw, 30px) 18px;
  background: rgb(8 7 6 / 97%);
}

.lb[hidden] {
  display: none;
}

/* Opening from the fiche: the whole gallery rises out of a soft blur ("flou levant"). It replays
   on every open (display:none → grid restarts the animation); the transform/blur never change the
   box, so Swiper still measures a correct size. Gated on reduced motion. */
@media (prefers-reduced-motion: no-preference) {
  .lb {
    animation: lb-in 0.5s var(--ease) both;
  }
}

@keyframes lb-in {
  from {
    opacity: 0;
    transform: translateY(10px);
    filter: blur(16px);
  }
  to {
    opacity: 1;
    transform: none;
    filter: blur(0);
  }
}

.lb__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  color: var(--dd-ivory);
}

.lb__count {
  font-size: 13px;
  letter-spacing: 0.08em;
  color: rgb(242 239 234 / 78%);
  font-variant-numeric: tabular-nums;
}

.lb__close {
  min-width: 44px;
  min-height: 44px;
  border: 1px solid rgb(242 239 234 / 35%);
  border-radius: var(--radius);
  color: var(--dd-ivory);
  font-size: 15px;
  transition:
    background-color var(--dur) var(--ease),
    color var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

/* Main viewer — one contained full-size photo per slide. */
.lb__main {
  position: relative;
  width: 100%;
  min-height: 0;
}

.lb__main .swiper-wrapper {
  align-items: center;
}

.lb__main .swiper-slide,
.lb__main .swiper-zoom-container {
  display: flex;
  align-items: center;
  justify-content: center;
}

.lb__main img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
}

/* Prev / next — solid black discs with a white border and a white chevron: clearly readable on
   the photograph the moment the viewer opens (no transparency to hunt for). */
/* Square, not a pill: the viewer is a grid of rectangles (the photo, the rail, the frame) and a
   circle was the one round thing in it. The glyph is a drawn SVG chevron — wide, 1.5px, square
   cap — because a text "❮" renders at the mercy of whatever font falls through. */
.lb__nav {
  position: absolute;
  top: 50%;
  z-index: 2;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  border: 1px solid rgb(250 250 249 / 0.28);
  border-radius: 2px;
  background: rgb(8 7 6 / 0.62);
  backdrop-filter: blur(6px);
  color: var(--dd-ivory);
  line-height: 0;
  transition:
    background-color var(--dur) var(--ease),
    color var(--dur) var(--ease),
    border-color var(--dur) var(--ease),
    opacity var(--dur) var(--ease);
}

.lb__nav svg {
  width: 26px;
  height: 26px;
}

.lb__nav--prev {
  left: 12px;
}

.lb__nav--next {
  right: 12px;
}

/* Ghost grammar on a photo backdrop: controls invert to IVORY (DESIGN.md §3). */
.lb__close:hover,
.lb__close:focus-visible,
.lb__nav:hover,
.lb__nav:focus-visible {
  background: var(--dd-ivory);
  border-color: var(--dd-ivory);
  color: var(--dd-lacquer);
}

.lb__close:focus-visible,
.lb__nav:focus-visible {
  outline: 2px solid var(--dd-ivory);
  outline-offset: 3px;
}

/* Thumbnail rail — synced to the viewer (Swiper thumbs), scrollable in fullscreen. */
.lb__thumbs {
  width: 100%;
  max-width: min(1400px, 96vw);
  height: 74px;
  margin-inline: auto;
}

.lb__thumbs .swiper-slide {
  width: 104px;
  height: 100%;
  overflow: hidden;
  border-radius: 2px;
  opacity: 0.45;
  cursor: pointer;
  transition: opacity var(--dur) var(--ease);
}

.lb__thumbs .swiper-slide:hover {
  opacity: 0.75;
}

.lb__thumbs .swiper-slide-thumb-active {
  opacity: 1;
  outline: 2px solid var(--dd-ivory);
  outline-offset: -2px;
}

.lb__thumbs img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@media (max-height: 560px) {
  .lb__thumbs {
    height: 56px;
  }
}

/* Phone rotated to LANDSCAPE (touch + landscape + phone-short viewport — a
   tablet is taller): the photo IS the screen. The thumbnail rail and the frame
   paddings go; the head (counter + close — the way out) floats over the image. */
@media (hover: none) and (pointer: coarse) and (orientation: landscape) and (max-height: 560px) {
  .lb {
    grid-template-rows: minmax(0, 1fr);
    gap: 0;
    padding: 0;
  }

  .lb__head {
    position: absolute;
    inset: 10px 12px auto;
    z-index: 2;
  }

  /* .lb prefix: the vendored `.swiper { display: block }` sheet loads after this
     one and would win the tie at (0,1,0). */
  .lb .lb__thumbs {
    display: none;
  }
}

/* =============================================================================
 * Editorial & legal (WP-8) — home body, Services, À propos, Mentions légales,
 * Confidentialité.
 *
 * Visual contract: the home mockup (DESIGN.md §8). Monochrome grammar throughout: hierarchy is
 * carried by value, weight and width; the only colour is the photography. Every colour below is a
 * token — no hue, no inline style (an inline `color` would defeat the CSS hover grammar).
 * ========================================================================== */

/* The featured strip reuses the listing's card, so the card's elevation token must exist outside
   `.listing` too. Same values, declared for the whole page (the listing keeps its own scope). */
.page-main {
  --card-shadow: 0 18px 50px -22px rgb(20 16 10 / 35%);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .page-main {
    --card-shadow: 0 18px 50px -22px rgb(0 0 0 / 60%);
  }
}

:root[data-theme="dark"] .page-main {
  --card-shadow: 0 18px 50px -22px rgb(0 0 0 / 60%);
}

:root[data-theme="light"] .page-main {
  --card-shadow: 0 18px 50px -22px rgb(20 16 10 / 35%);
}

/* ---------- Hero stats (the counter is the LIVE published count) ------------ */

.hero__meta {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(18px, 4vw, 56px);
  margin: clamp(28px, 5vh, 52px) 0 0;
  padding-top: 22px;
  border-top: 1px solid var(--hairline);
}

.hero__stat dt {
  font-size: clamp(20px, 2.4vw, 26px);
  font-weight: 650;
  font-stretch: 116%;
  line-height: 1.1;
}

.hero__stat dd {
  margin: 4px 0 0;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ---------- Section head + "more" link -------------------------------------- */

.section--tight {
  padding-block: clamp(40px, 6vw, 72px);
}

.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 24px;
  margin-bottom: clamp(26px, 4vw, 44px);
}

.section-head__title {
  font-size: clamp(24px, 3vw, 37px);
  margin-top: 10px;
}

.section__lead {
  margin: -12px 0 clamp(24px, 3vw, 36px);
}

.more {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--ink);
  text-decoration: none;
}

.more::after {
  content: " →";
}

.more:hover,
.more:focus-visible {
  text-decoration: underline;
  text-underline-offset: 4px;
}

/* ---------- Editorial band (full-bleed photograph) -------------------------- */

.band {
  position: relative;
  overflow: hidden;
  isolation: isolate;
}

.band__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 45%;
  z-index: -2;
}

/* Same two-axis wash as the hero: readable copy on ANY photograph. */
.band__veil {
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(90deg, rgb(10 9 7 / 84%) 0%, rgb(10 9 7 / 48%) 55%, rgb(10 9 7 / 18%) 100%),
    linear-gradient(180deg, rgb(8 7 6 / 40%) 0%, rgb(8 7 6 / 10%) 40%, rgb(8 7 6 / 45%) 100%);
}

.band__inner {
  position: relative;
  max-width: 560px;
  padding-block: clamp(64px, 10vw, 130px);
}

.band__title {
  font-size: clamp(26px, 3.4vw, 42px);
  margin: 12px 0 16px;
}

.band__inner .lead {
  margin-bottom: 28px;
}

/* ---------- Services (home teaser + Services page) -------------------------- */

.services {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  border-top: 1px solid var(--hairline);
}

.service {
  padding: 30px 26px 34px;
  border-bottom: 1px solid var(--hairline);
}

.service + .service {
  border-left: 1px solid var(--hairline);
}

.service__index {
  display: block;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.12em;
  color: var(--muted);
  margin-bottom: 14px;
}

.service__title {
  font-size: 15px;
  font-weight: 650;
  font-stretch: 112%;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin: 0 0 10px;
}

.service__body {
  font-size: 14px;
  color: var(--muted);
  margin: 0;
}

.service__detail {
  font-size: 15px;
  margin: 14px 0 0;
  text-wrap: pretty;
}

/* The detailed grid breathes: exactly TWO columns (four services in a 2×2 block — an auto-fit
   grid would leave the fourth one orphaned in a row of its own), each with the long paragraph.
   No top "coiffe" here (override the shared `.services` border-top): the block hangs on its
   internal cross only. `overflow-x: clip` fences in the staged sideways entrance below. */
.services--detailed {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  overflow-x: clip;
  border-top: 0;
}

/* Services PAGE: keep ONLY the two separators between the four cells — no outer frame, no top
   bar, no bottom rule. The internal cross is the column divider plus the row divider below. */
.services--detailed .service {
  padding: 34px 32px 40px;
  border-left: 0;
  border-bottom: 0;
}

/* The vertical rule between the two columns (right edge of Entretien / Detailing). */
.services--detailed .service:nth-child(even) {
  border-left: 1px solid var(--hairline);
}

/* The horizontal rule between the two rows (top edge of Detailing / Transport). */
.services--detailed .service:nth-child(3),
.services--detailed .service:nth-child(4) {
  border-top: 1px solid var(--hairline);
}

@media (width <= 980px) {
  .service + .service {
    border-left: none;
  }
}

@media (width <= 760px) {
  .services--detailed {
    grid-template-columns: minmax(0, 1fr);
  }

  /* Stacked to one column: the column divider is meaningless, so drop it and instead separate
     every card from the one above (a top rule on all but the first). */
  .services--detailed .service:nth-child(even) {
    border-left: 0;
  }

  .services--detailed .service:nth-child(2) {
    border-top: 1px solid var(--hairline);
  }
}

/* Services PAGE — staged entrance ("mode Staged"): the four services rise out of a soft blur
   ("flou levant") from alternating sides — odds from the left, evens from the right — one after
   another. The grid sits at the fold, so this is pure CSS, render-blocking → it always ends
   visible (no flash) and needs no observer. `.services--detailed` clips the sideways travel. */
@media (prefers-reduced-motion: no-preference) {
  .services--detailed .service:nth-child(odd) {
    animation: service-from-left 0.9s var(--ease) both;
  }

  .services--detailed .service:nth-child(even) {
    animation: service-from-right 0.9s var(--ease) both;
  }

  .services--detailed .service:nth-child(1) {
    animation-delay: 80ms;
  }
  .services--detailed .service:nth-child(2) {
    animation-delay: 200ms;
  }
  .services--detailed .service:nth-child(3) {
    animation-delay: 320ms;
  }
  .services--detailed .service:nth-child(4) {
    animation-delay: 440ms;
  }
}

@keyframes service-from-left {
  from {
    opacity: 0;
    transform: translateX(-52px);
    filter: blur(12px);
  }
  to {
    opacity: 1;
    transform: none;
    filter: blur(0);
  }
}

@keyframes service-from-right {
  from {
    opacity: 0;
    transform: translateX(52px);
    filter: blur(12px);
  }
  to {
    opacity: 1;
    transform: none;
    filter: blur(0);
  }
}

/* ---------- CTA band (rachat teaser, page closings) ------------------------- */

.cta-band {
  background: var(--surface);
  border-block: 1px solid var(--hairline);
}

.cta-band__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 32px;
  padding-block: clamp(44px, 7vw, 72px);
}

.cta-band__copy {
  max-width: 56ch;
}

.cta-band__actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
}

.cta-band__title {
  font-size: clamp(22px, 2.8vw, 34px);
  margin: 12px 0 12px;
}

.cta-band__copy .lead {
  margin: 0;
}

/* ---------- Proof strip ----------------------------------------------------- */

.proof {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: clamp(20px, 4vw, 48px);
  padding-block: clamp(36px, 6vw, 60px);
  border-top: 1px solid var(--hairline);
}

.proof__figure {
  font-size: clamp(32px, 4.5vw, 52px);
  font-weight: 660;
  font-stretch: 118%;
  line-height: 1;
}

.proof__body {
  flex: 1 1 320px;
  max-width: 46ch;
  color: var(--muted);
  margin: 0;
}

/* ---------- Brand directory (the footer marquee) ---------------------------- */

.brands {
  padding-top: clamp(20px, 3vw, 32px);
}

.brands .label {
  display: block;
  margin-bottom: 18px;
}

.marquee {
  display: flex;
  overflow: hidden;
  border-block: 1px solid var(--hairline);
  padding-block: 18px;
}

.marquee__track {
  display: flex;
  flex: 0 0 auto;
  align-items: center;

  /* A showroom with four makes must still fill the strip: each lap spans AT LEAST the viewport,
     so the clone always covers what the first lap leaves behind — no gap mid-loop. */
  min-width: 100%;
  justify-content: space-around;

  list-style: none;
  margin: 0;
  padding: 0;
  white-space: nowrap;
  animation: marquee-scroll 60s linear infinite;
}

/* A moving target is unusable: the track stops under the pointer AND under the keyboard. */
.marquee:hover .marquee__track,
.marquee:focus-within .marquee__track {
  animation-play-state: paused;
}

@keyframes marquee-scroll {
  from {
    transform: translateX(0);
  }

  to {
    transform: translateX(-100%);
  }
}

.marquee__item {
  display: inline-block;
  padding-inline: clamp(16px, 1.6vw, 26px);
  font-size: 12px;
  font-weight: 600;
  font-stretch: 118%;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
  text-decoration: none;
  transition: color var(--dur) var(--ease);
}

.marquee__item:hover,
.marquee__item:focus-visible {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 4px;
}

/* ---------- Editorial page header ------------------------------------------ */

.page-header {
  padding-top: calc(var(--header-h) + clamp(32px, 5vw, 64px));
  padding-bottom: clamp(24px, 3vw, 40px);
  border-bottom: 1px solid var(--hairline);
}

.page-header__title {
  font-size: clamp(30px, 4.4vw, 56px);
  margin: 14px 0 20px;
}

.page-header__lead {
  max-width: 62ch;
}

.stats {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(24px, 4vw, 56px);
  margin: clamp(28px, 4vw, 40px) 0 0;
}

.stats__item dt {
  font-size: clamp(22px, 2.6vw, 30px);
  font-weight: 650;
  font-stretch: 116%;
}

.stats__item dd {
  margin: 4px 0 0;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ---------- Editorial / legal prose columns --------------------------------
 * NOT `.prose` — that namespace belongs to the fiche's sanitized description (WP-5).
 * ------------------------------------------------------------------------- */

.editorial {
  display: flex;
  flex-direction: column;
}

.editorial__section {
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr);
  gap: 0 24px;
  padding-block: clamp(28px, 3.5vw, 44px);
  border-top: 1px solid var(--hairline);
}

.editorial__section:first-child {
  border-top: 0;
  padding-top: 0;
}

.editorial__index {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.12em;
  color: var(--muted);
  padding-top: 4px;
}

.editorial__body {
  max-width: 68ch;
}

.editorial__title {
  font-size: clamp(18px, 2vw, 22px);
  font-weight: 650;
  font-stretch: 110%;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  margin: 0 0 14px;
}

.editorial__body p {
  margin: 0 0 14px;
  text-wrap: pretty;
}

.editorial__body p:last-child {
  margin-bottom: 0;
}

.editorial__list {
  list-style: none;
  margin: 0 0 14px;
  padding: 0;
}

.editorial__list li {
  position: relative;
  padding-left: 20px;
  margin-bottom: 8px;
  color: var(--muted);
}

.editorial__list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.7em;
  width: 8px;
  height: 1px;
  background: var(--hairline-strong);
}

.editorial__link {
  margin-top: 14px;
  word-break: break-word;
}

/* ---------- About — the visit film ------------------------------------------
 * The prose keeps one vertical half; the other pins a portrait clip whose
 * playback is scrubbed by the page's own scroll (all-I encode, island in
 * showroom.js). Without JS or under reduced motion the first frame stays: a
 * photograph. Mobile: the film becomes a pinned top band the prose slides
 * under. Travel is measured section − pin by the island, never innerHeight
 * (iOS bars make it breathe).
 * ------------------------------------------------------------------------- */

/* Desktop: a PORTRAIT film pinned on the LEFT edge, its column cut to the
   clip's exact 9/16 aspect (full frame, zero crop); the prose sits to its
   right on the plain page background — black or white with the theme. Mobile
   keeps the pinned top band + prose below (landscape clip). */
/* Flush section: the film glues to the neighbouring sections — no block
   padding, no hairline seam on either junction. */
.about-split.section {
  padding-block: 0;
  border-top: 0;
}

.about-split + .section,
.about-split + .cta-band {
  border-top: 0;
}

.about-split__grid {
  display: grid;
}

.about-split__film {
  grid-area: 1 / 1;
  position: sticky;
  top: var(--header-h);
  height: calc(100svh - var(--header-h));
}

.about-split__video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Poster doubled as CSS background: on iOS a reload restored mid-section
     paints neither video nor poster until the first gesture. */
  background: var(--dd-lacquer) url("/static/img/about-visit-poster.jpg") center / cover no-repeat;
}

@media (width >= 861px) {
  .about-split__grid {
    /* A straight half-half split (ju): the portrait clip covers its half — the
       frame crops top/bottom a little instead of showing whole. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    /* Ultra-wide bridle (ju): past 1920px the halves stop growing — unbounded,
       they over-crop the portrait film and strand the prose column rightwards.
       Normal desktops (≤1920) keep the full-bleed split unchanged. */
    max-width: 1920px;
    margin-inline: auto;
  }

  .about-split__film {
    width: 100%;
  }

  /* Desktop plays the portrait clip — its poster behind the frame, not the
     landscape band's (the attribute is swapped by the island). */
  .about-split__video {
    background-image: url("/static/img/about-visit-left-poster.jpg");
  }

  .about-split__content {
    grid-column: 2;
    grid-row: 1;
    padding: clamp(36px, 6vh, 72px) clamp(32px, 5vw, 96px);
  }

  /* The prose centres in the right half, on the page background. */
  .about-split__content .editorial {
    max-width: 64ch;
    margin-inline: auto;
  }

  /* Larger type: this is the page's reading column — let it breathe and
     stretch the section (more travel for the film beside it). */
  .about-split__content .editorial__title {
    font-size: clamp(22px, 1.7vw, 27px);
  }

  .about-split__content .editorial__body {
    font-size: 17px;
    line-height: 1.65;
  }

  .about-split__content .editorial__section {
    padding-block: clamp(36px, 4.5vh, 58px);
  }
}

@media (width <= 860px) {
  .about-split__film {
    grid-area: auto;
    grid-column: 1;
    grid-row: 1;
    top: var(--header-h);
    /* A fifth for the film, the rest to the prose. */
    height: 20svh;
    z-index: 2;
  }

  /* The prose doesn't cut under the film band abruptly: it dissolves into the
     page colour through a discreet fade hanging under the pinned band. */
  .about-split__film::after {
    content: "";
    position: absolute;
    inset: 100% 0 auto 0;
    height: 52px;
    background: linear-gradient(to bottom, var(--page), transparent);
    pointer-events: none;
  }

  .about-split__content {
    grid-column: 1;
    grid-row: 2;
    /* The section's own block padding is gone (flush film) — the prose keeps a
       little air of its own before the next section. */
    padding: 20px var(--gutter) 32px;
  }
}

/* ---------- À propos — the premises gallery ("Passez nous voir.") ----------- */

/* The gallery lives INSIDE the CTA band, under the copy/actions row: the band's
   inner padding already opens the section, so the slot only closes it and
   tightens the seam to the row above. */
.cta-band__below {
  margin-top: calc(-1 * clamp(8px, 2vw, 24px));
  padding-bottom: clamp(44px, 7vw, 72px);
}

/* 15 landscape photographs on a 3-column lattice; the façade leads at double
   size (2×2 = 4 cells; 4 + 14 = 18 = 3 × 6, the lattice closes with no hole).
   Same tile grammar as the fiche gallery: surface ground, radius, zoom-in. */
.about-gallery__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}

.about-gallery__tile {
  position: relative;
  display: block;
  overflow: hidden;
  aspect-ratio: 3 / 2;
  background: var(--surface);
  border-radius: var(--radius);
  cursor: zoom-in;
}

.about-gallery__tile:first-child {
  grid-column: span 2;
  grid-row: span 2;
  /* Height comes from the two spanned rows, not from the ratio. */
  aspect-ratio: auto;
}

.about-gallery__tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease);
}

.about-gallery__tile:hover img,
.about-gallery__tile:focus-visible img {
  transform: scale(1.03);
}

@media (max-width: 760px) {
  /* 2 columns, the façade full-width double-height (4 + 14 = 18 = 2 × 9). */
  .about-gallery__grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 8px;
  }
}

/* ---------- Company identity (legal notice, privacy, about) ----------------- */

.identity {
  margin-top: 18px;
  padding: 18px 20px;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
}

.identity__name {
  font-weight: 650;
  font-stretch: 108%;
  margin: 0 0 8px;
}

.identity__address {
  font-style: normal;
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 14px;
  color: var(--muted);
}

.identity__trade {
  margin: -4px 0 8px;
  font-size: 14px;
  color: var(--muted);
}

.identity__legal {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 2px 16px;
  margin: 12px 0 0;
  font-size: 14px;
}

.identity__legal dt {
  color: var(--muted);
}

.identity__legal dd {
  margin: 0;
}

.identity__list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 20px;
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  font-size: 14px;
}

/* ---------- Visit card (hours + address; composed by the contact page) ------ */

.visit {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 28px;
  margin-top: 20px;
}

.visit__col {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
}

.visit__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  font-variant-numeric: tabular-nums;
}

.visit__note {
  font-size: 14px;
  color: var(--muted);
  margin: 0;
}

/* ---------- Editorial responsive ------------------------------------------- */

@media (width <= 720px) {
  .editorial__section {
    grid-template-columns: minmax(0, 1fr);
    gap: 8px;
  }

  /* Two counters per row — the wrapping flex left the long-labelled ones alone
     on their line. */
  .stats {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 20px 24px;
  }

  .cta-band__inner {
    align-items: flex-start;
  }
}

/* ---------- Reduced motion (DESIGN.md §7) ---------------------------------- */

@media (prefers-reduced-motion: reduce) {
  /* The directory must stay reachable once the loop is off: it scrolls by hand, and the clone
     (a duplicate of the same links) disappears. */
  .marquee {
    overflow-x: auto;
  }

  .marquee__track {
    animation: none;
  }

  .marquee__track--clone {
    display: none;
  }
}

/* =============================================================================
 * Public forms (WP-6) — /contact · /rachat (+ /en twins)
 *
 * Monochrome grammar, same as the rest: no hue anywhere, hierarchy by value and
 * weight. Focus is a real ring (a monochrome design does NOT get to skip it),
 * an invalid field is marked by a thicker rule, not by a colour, and errors are
 * spelled out in words next to the field — never conveyed by colour alone
 * (WCAG 1.4.1).
 * ========================================================================== */

.form-page {
  padding-top: calc(var(--header-h) + clamp(24px, 3vw, 40px));
  padding-bottom: clamp(56px, 8vw, 96px);
}

.form-page__head {
  max-width: 62ch;
  padding-bottom: clamp(24px, 4vw, 40px);
}

.form-page__title {
  font-size: clamp(30px, 4vw, 48px);
  text-transform: uppercase;
  margin: 6px 0 14px;
}

.form-page__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
  gap: clamp(28px, 5vw, 64px);
  align-items: start;
}

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

.form-panel {
  border-top: 1px solid var(--hairline);
  padding-top: clamp(24px, 3vw, 32px);
}

.form__set {
  border: 0;
  padding: 0;
  margin: 0 0 clamp(24px, 3vw, 36px);
}

.form__legend {
  padding: 0 0 16px;
  color: var(--muted);
}

.form__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 20px 24px;
}

.form__context {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 14px 16px;
  margin-bottom: 24px;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
  background: var(--surface);
}

.form__context-value {
  font-weight: 600;
  letter-spacing: -0.01em;
}

.form__actions {
  display: flex;
  gap: 12px;
  padding-top: 8px;
}

/* The submit is a real <button> (the site's <a class="btn"> would not submit). */
.form__actions .btn {
  border: 1px solid transparent;
  font: inherit;
  cursor: pointer;
}

/* ---------- Fields --------------------------------------------------------- */

.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}

.field--wide {
  grid-column: 1 / -1;
}

.field__label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  font-size: 13px;
  font-weight: 600;
}

.field__flag {
  font-size: 10px;
  font-weight: 500;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--muted);
}

.field__input {
  width: 100%;
  padding: 11px 13px;
  font: inherit;
  font-size: 15px;
  color: var(--ink);
  background: var(--page);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
  transition: border-color 160ms ease-out, box-shadow 160ms ease-out;
}

.field__input::placeholder {
  color: var(--muted);
}

.field__input:hover {
  border-color: var(--ink);
}

.field__input:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  border-color: var(--ink);
}

.field__input--area {
  resize: vertical;
  min-height: 132px;
}

.field__input--select {
  appearance: none;
  padding-right: 36px;
  /* Monochrome chevron, drawn with the ink token (no image asset, no hue). */
  background-image: linear-gradient(45deg, transparent 50%, currentcolor 50%),
    linear-gradient(135deg, currentcolor 50%, transparent 50%);
  background-position: calc(100% - 18px) calc(50% + 1px), calc(100% - 13px) calc(50% + 1px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

.field__input--file {
  padding: 9px 12px;
  cursor: pointer;
}

.field__input--file::file-selector-button {
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-right: 12px;
  padding: 7px 12px;
  color: var(--page);
  background: var(--ink);
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
  transition: background-color 160ms ease-out;
}

.field__input--file:hover::file-selector-button {
  background: var(--ink-lift);
}

/* Invalid: a heavier rule + the sentence below. Never colour alone. */
.field__input[aria-invalid="true"] {
  border-width: 2px;
  border-color: var(--ink);
}

.field__hint {
  font-size: 12px;
  color: var(--muted);
}

.field__error {
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
}

.field__error::before {
  content: "— ";
}

/* ---------- Consent (GDPR) -------------------------------------------------- */

.consent__row {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  cursor: pointer;
}

.consent__box {
  flex: none;
  width: 18px;
  height: 18px;
  margin-top: 2px;
  accent-color: var(--ink);
  cursor: pointer;
}

.consent__box:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.consent__text {
  font-size: 13px;
  line-height: 1.55;
  color: var(--muted);
}

/* ---------- The honeypot: invisible to humans, present for bots -------------- */

.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* ---------- Alerts ---------------------------------------------------------- */

.form-alert {
  padding: 16px 18px;
  margin-bottom: 28px;
  border: 2px solid var(--ink);
  border-radius: var(--radius);
  background: var(--surface);
}

.form-alert__title {
  font-weight: 700;
  letter-spacing: -0.01em;
}

.form-alert__body {
  margin-top: 4px;
  font-size: 14px;
  color: var(--muted);
}

/* ---------- Aside ----------------------------------------------------------- */

.form-aside {
  position: sticky;
  top: calc(var(--header-h) + 24px);
  border-top: 1px solid var(--hairline);
  padding-top: clamp(24px, 3vw, 32px);
  font-size: 14px;
}

.form-aside__block {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 18px;
  font-style: normal;
  line-height: 1.65;
}

.form-aside__note {
  margin-top: 18px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--muted);
}

.form-aside__steps {
  margin: 18px 0 0;
  padding-left: 18px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  line-height: 1.55;
  color: var(--muted);
}

.form-aside__steps li::marker {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  color: var(--ink);
}

/* ---------- Thank-you (the Post/Redirect/Get landing) ------------------------ */

.form-sent {
  max-width: 60ch;
  padding: clamp(40px, 8vw, 88px) 0;
}

.form-sent__title {
  font-size: clamp(28px, 4vw, 44px);
  text-transform: uppercase;
  margin: 10px 0 16px;
}

.form-sent__body {
  margin-bottom: 32px;
}

.form-sent__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

/* ---------- Responsive ------------------------------------------------------ */

@media (width <= 900px) {
  .form-page__grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .form-aside {
    position: static;
  }
}

@media (width <= 560px) {
  .form__grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ---------- Delivered gallery (WP-4b) --------------------------------------
   The listing's grammar, one aisle further on: the SAME .car card, minus the
   price rail and minus the link. Colours stay in the stylesheet (DESIGN.md);
   the variant is one rule, not a second card. */

.car--delivered {
  cursor: default;
}

/* A delivered card is an <article>, not an <a>: it must not offer the hover
   affordance of something you can click into. The photograph keeps its quiet
   scale — the grid still has to feel alive. */
.car--delivered:hover {
  box-shadow: none;
  border-color: var(--hairline);
  transform: none;
}

.car--delivered:hover .car-media img {
  transform: scale(1.02);
}

/* The last block of a delivered card is the spec row: it has no price rail to
   sit against, so it closes the card itself. */
.car--delivered .car-specs {
  padding-bottom: 0;
}

/* The archive's filter bar: two selects and a submit, on one line. No rail, no
   bottom sheet, no island — make + year is the whole contract (the gallery is
   proof, not a shop), and it works with JavaScript disabled. */
.dbar {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 14px 18px;
  padding: 18px 0 clamp(24px, 3vw, 32px);
  border-top: 1px solid var(--hairline);
}

.dbar__field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.dbar__label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}

.dbar .reset {
  margin-bottom: 10px;
}

@media (width <= 560px) {
  .dbar__field {
    flex: 1 1 100%;
  }
}

/* =============================================================================
 * HOME — the blocks that live BELOW the film
 *
 * The opening film is L'Immersion, and it owns its own stylesheet (immersion.css)
 * and its own fixed stage. What is left here is the hero CARD it hosts in scene 1
 * (`.home-hero__inner`, with the ivory beam that laps its border) and the business
 * folds that stack after the film — services, rachat, proof, brands.
 *
 * The home is dark in BOTH themes: the photography carries it.
 *
 * Two earlier phases used to live in this section and are gone: the fixed
 * `.home-stage` with its looping <video>, and the 8-state `.acts` deck with its
 * frame-sequence canvas and gauge. Both were replaced by the film, and their CSS
 * outlived them by a while — none of it had a selector left in any markup.
 * ========================================================================== */

.home {
  position: relative;
  z-index: 0; /* own stacking context: the film sits under the islands (1) */
}

/* ---- Islands: every content block scrolls above the film ------------------ */

.home-services,
.home-buyback,
.home-proof,
.home .brands {
  position: relative;
  z-index: 1;
}

/* A local dark scrim behind the DARK big-type fold, so copy reads over ANY frame. */
.home-buyback::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(
    90deg,
    rgb(19 17 16 / 56%) 0%,
    rgb(19 17 16 / 14%) 62%,
    rgb(19 17 16 / 0%) 100%
  );
}

/* White folds — a SOLID, fully-opaque white block over the stage, hard-edged: a
   clean cut from the dark photographic folds, no feather, no halo. Text and cards
   switch back to the light tokens (clean white gallery tiles). */
.home-fold--light {
  --page: var(--dd-white);
  --ink: var(--dd-ink);
  --ink-lift: var(--dd-ink-lift);
  --muted: var(--dd-smoke);
  --hairline: rgb(33 31 28 / 14%);
  --hairline-strong: rgb(33 31 28 / 28%);
  --focus-ring: var(--dd-ink);

  background: var(--dd-white);
  color: var(--dd-ink);
}

/* On a light fold, the filled CTA stays ink (it inverts against white correctly);
   the outline button reverts to the standard ink-on-hover grammar (not the
   on-photo ivory inversion). */
.home-fold--light .btn--outline:hover,
.home-fold--light .btn--outline:focus-visible {
  background: var(--dd-ink);
  border-color: var(--dd-ink);
  color: var(--dd-white);
}

/* ---- The hero card -------------------------------------------------------- */

/* A quiet "encadré": frames the hero copy in a fine ivory hairline over a soft
   lacquer wash, with the same frosted backdrop the scrolled header uses — extra
   legibility over the photo (and the phase-2 video) without a heavy glass card. */
.home-hero__inner {
  max-width: 760px;
  position: relative;
  padding: clamp(26px, 3.4vw, 46px);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: linear-gradient(to bottom, rgb(19 17 16 / 32%), rgb(19 17 16 / 52%));
  /* A properly frosted pane: the film behind the card should read as a soft wash
     of light, not as a legible second image competing with the headline. */
  backdrop-filter: saturate(125%) blur(18px);
}

/* An angle we can animate: a plain custom property doesn't interpolate. */
@property --beam-angle {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}

/* The same ivory glint as the "Découvrir" line, travelling around the frame: a
   conic sweep masked down to the 1-px border ring, riding on top of the static
   hairline. Draws the eye to the content card in the dark hero. */
.home-hero__inner::after {
  content: "";
  position: absolute;
  inset: -1px; /* cover the border box, so the glint sits ON the hairline */
  border-radius: var(--radius);
  padding: 1px;
  background: conic-gradient(
    from var(--beam-angle),
    transparent 0deg,
    transparent 12deg,
    rgb(242 239 234 / 85%) 42deg,
    transparent 72deg,
    transparent 360deg
  );
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
  opacity: 0;
  /* One lap every 30 s: a slow ~4.5 s sweep, then dark until the next cycle. */
  animation: beam-lap-card 30s linear infinite;
}

@keyframes beam-lap-card {
  0% {
    --beam-angle: 0deg;
    opacity: 0;
  }

  1.5% {
    opacity: 1;
  }

  15% {
    --beam-angle: 360deg;
    opacity: 1;
  }

  17% {
    --beam-angle: 360deg;
    opacity: 0;
  }

  100% {
    --beam-angle: 360deg;
    opacity: 0;
  }
}

/* The same travelling glint on a key CTA (the "estimer" button), but far more
   often — a lap roughly every 7 s. Uses its OWN --beam-angle (@property
   inherits:false), so it never syncs with the card's beam. */
.btn--beam {
  position: relative;
}

.btn--beam::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: var(--radius);
  padding: 1px;
  background: conic-gradient(
    from var(--beam-angle),
    transparent 0deg,
    transparent 12deg,
    rgb(242 239 234 / 85%) 42deg,
    transparent 72deg,
    transparent 360deg
  );
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
  opacity: 0;
  animation: beam-lap-btn 7s linear infinite;
}

@keyframes beam-lap-btn {
  0% {
    --beam-angle: 0deg;
    opacity: 0;
  }

  4% {
    opacity: 1;
  }

  40% {
    --beam-angle: 360deg;
    opacity: 1;
  }

  46% {
    --beam-angle: 360deg;
    opacity: 0;
  }

  100% {
    --beam-angle: 360deg;
    opacity: 0;
  }
}

.home-hero__title {
  font-size: clamp(44px, 8vw, 92px);
  margin: 16px 0 20px;
}

.home-hero__lead {
  color: var(--dd-ivory);
  opacity: 0.9;
  max-width: 46ch;
}

.home-hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 34px;
}

.home-hero__stats {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(20px, 4vw, 52px);
  margin-top: clamp(26px, 4vh, 44px);
}

/* Each figure sits over its own hairline. No hover affordance: the metrics are
   not interactive, so a hover state would promise an action that isn't there. */
.home-hero__stat {
  position: relative;
  padding-bottom: 12px;
}

.home-hero__stat dt {
  font-size: clamp(26px, 3.4vw, 40px);
  font-weight: 650;
  font-stretch: 116%;
  line-height: 1;
  /* A quiet curtain reveal on load — NOT a rolling counter. */
  animation: stat-reveal 640ms var(--ease) both;
  animation-delay: calc(260ms + var(--i, 0) * 120ms);
}

.home-hero__stat dd {
  margin-top: 7px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  animation: stat-label 560ms var(--ease) both;
  animation-delay: calc(400ms + var(--i, 0) * 120ms);
}

/* The hairline draws in left→right just after its figure lands. */
.home-hero__stat::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 1px;
  background: var(--hairline-strong);
  transform: scaleX(0);
  transform-origin: left;
  animation: stat-rule 720ms var(--ease) both;
  animation-delay: calc(500ms + var(--i, 0) * 120ms);
}

@keyframes stat-reveal {
  from {
    opacity: 0;
    clip-path: inset(0 0 100% 0);
    transform: translateY(0.26em);
  }

  to {
    opacity: 1;
    clip-path: inset(0 0 -14% 0);
    transform: translateY(0);
  }
}

@keyframes stat-label {
  from {
    opacity: 0;
    transform: translateY(6px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes stat-rule {
  to {
    transform: scaleX(1);
  }
}

/* ---- Section heads (no eyebrow — the type carries the hierarchy) ---------- */

.home-sec-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 24px;
  margin-bottom: clamp(24px, 3.5vw, 40px);
}

.home-sec-head__title {
  font-size: clamp(28px, 4vw, 46px);
}

/* ---- Fold rhythm ---------------------------------------------------------- */

.home-services,
.home-proof {
  padding-block: clamp(64px, 9vw, 132px);
}

.home-buyback {
  padding-block: clamp(84px, 12vw, 180px);
}

/* ---- Rachat: big type, the film shows through ----------------------------- */

.home-buyback__inner {
  max-width: 720px;
}

.home-buyback__title {
  font-size: clamp(30px, 5vw, 60px);
  margin-bottom: 20px;
}

.home-buyback__inner .lead {
  margin-bottom: 30px;
  max-width: 54ch;
}

.home-services__lead {
  margin: -8px 0 clamp(24px, 3vw, 40px);
}

/* ---- Proof figures ------------------------------------------------------- */

.home-proof__figures {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(32px, 7vw, 96px);
  margin: 0 0 28px;
}

.home-proof__figure dt {
  font-size: clamp(44px, 8vw, 104px);
  line-height: 0.95;
}

.home-proof__figure dd {
  margin-top: 8px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
}

.home-proof__body {
  max-width: 52ch;
  margin-bottom: 18px;
}

/* ---- Reveal system -------------------------------------------------------
   Content is visible by DEFAULT (no JS, reduced motion, crawlers, hidden tabs).
   The hidden pre-state applies ONLY when the JS added `.reveal-anim` to <html>,
   which it does solely when JS runs AND motion is allowed. */

/* The transition lives on the TARGET (.is-in) state, never on the armed one: if
   the browser painted a frame before the island armed the page (streamed HTML, a
   media element forcing an early render), arming must SNAP to hidden — a
   transition there would play a 0.85s fade-OUT on visible content and the whole
   entrance would read as "no animation" (the À propos film regression). */
.reveal-anim [data-reveal] {
  opacity: 0;
  transform: translateY(26px);
  will-change: opacity, transform;
}

.reveal-anim [data-reveal].is-in {
  opacity: 1;
  transform: none;
  transition:
    opacity 0.7s var(--ease),
    transform 0.7s var(--ease);
  transition-delay: calc(var(--i, 0) * 60ms);
  will-change: auto;
}

/* Listing entrance — a sober "focus-in" of the whole vitrine, since this is the first
   thing a client sees. Pure CSS, defined in this render-blocking stylesheet, so it is
   in force at the very first paint: NO JS gate, NO observer timing, NO flash. The stock
   grid replays it for free after a live filter/sort swap (the swap replaces the grid's
   innerHTML, so the fresh cards are new elements that re-run it). Gated on
   `no-preference` for reduced motion (no-JS keeps it, harmless). Premium material: a
   short lens-blur that sharpens as each element settles — the same "flou levant"
   language as the home, bounded to the first screen and brief. The first (LCP) card
   rises WITHOUT a fade or blur, so its paint is never held behind opacity 0. */
@media (prefers-reduced-motion: no-preference) {
  /* The writing leads: the eyebrow, then the display title, rise out of a soft blur. */
  .listing .page-head__title > .label {
    animation: text-focus 0.65s var(--ease) both;
  }
  .listing .listing__heading {
    animation: text-focus 0.85s var(--ease) both;
    animation-delay: 60ms;
  }

  /* The filter rail: each group fades + rises in a short stagger, just behind the title. */
  .rail .fgroup {
    animation: group-in 0.6s var(--ease) both;
  }
  .rail .fgroup:nth-child(2) {
    animation-delay: 110ms;
  }
  .rail .fgroup:nth-child(3) {
    animation-delay: 165ms;
  }
  .rail .fgroup:nth-child(4) {
    animation-delay: 220ms;
  }
  .rail .fgroup:nth-child(5) {
    animation-delay: 275ms;
  }
  .rail .fgroup:nth-child(6) {
    animation-delay: 330ms;
  }
  .rail .fgroup:nth-child(n + 7) {
    animation-delay: 370ms;
  }

  /* The cars are the moment the page lands on: they rise further, out of a deeper blur. */
  .grid-cars > .car {
    animation: card-enter 0.75s var(--ease) both;
    animation-delay: 100ms;
  }
  .grid-cars > .car:nth-child(2) {
    animation-delay: 165ms;
  }
  .grid-cars > .car:nth-child(3) {
    animation-delay: 230ms;
  }
  .grid-cars > .car:nth-child(4) {
    animation-delay: 295ms;
  }
  .grid-cars > .car:nth-child(5) {
    animation-delay: 360ms;
  }
  .grid-cars > .car:nth-child(6) {
    animation-delay: 425ms;
  }
  .grid-cars > .car:nth-child(7) {
    animation-delay: 490ms;
  }
  .grid-cars > .car:nth-child(n + 8) {
    animation-delay: 540ms;
  }

  /* LCP-safe: the first card rises without fading or blurring — its paint is never held. */
  .grid-cars > .car:first-child {
    animation-name: card-rise;
    animation-delay: 100ms;
  }

  @keyframes text-focus {
    from {
      opacity: 0;
      transform: translateY(14px);
      filter: blur(10px);
    }
    to {
      opacity: 1;
      transform: none;
      filter: blur(0);
    }
  }

  @keyframes group-in {
    from {
      opacity: 0;
      transform: translateY(16px);
    }
    to {
      opacity: 1;
      transform: none;
    }
  }

  @keyframes card-enter {
    from {
      opacity: 0;
      transform: translateY(44px);
      filter: blur(13px);
    }
    to {
      opacity: 1;
      transform: none;
      filter: blur(0);
    }
  }

  @keyframes card-rise {
    from {
      transform: translateY(44px);
    }
    to {
      transform: none;
    }
  }
}

/* Fiche détail — the same sober "focus-in" as the listing, reusing its `text-focus`,
   `group-in` and `card-rise` keyframes. Pure CSS, render-blocking → no flash, no JS
   timing; gated on reduced motion. The gallery holds the LCP image, so it rises WITHOUT
   a fade or blur (its paint is never held). The similar-cars strip animates through the
   shared `.grid-cars > .car` rule already. */
@media (prefers-reduced-motion: no-preference) {
  .fiche .crumbs {
    animation: group-in 0.55s var(--ease) both;
  }

  /* The writing leads: make + title rise out of a soft blur, then the meta line. */
  .fiche-head__make {
    animation: text-focus 0.65s var(--ease) both;
    animation-delay: 40ms;
  }
  .fiche-head__title {
    animation: text-focus 0.85s var(--ease) both;
    animation-delay: 90ms;
  }
  /* The meta line (registration · km · power) rises out of a soft blur, inscription by
     inscription — the "flou levant", reusing the title's blur-rise keyframe. */
  .fiche-head__meta span {
    animation: text-focus 0.7s var(--ease) both;
  }
  .fiche-head__meta span:nth-child(1) {
    animation-delay: 200ms;
  }
  .fiche-head__meta span:nth-child(2) {
    animation-delay: 300ms;
  }
  .fiche-head__meta span:nth-child(3) {
    animation-delay: 400ms;
  }
  .fiche-head__meta span:nth-child(n + 4) {
    animation-delay: 480ms;
  }

  /* The gallery images enter from VARIED directions (clipped by `.gallery` overflow).
     The main tile holds the LCP image, so it slides in from the LEFT with transform only
     — its paint is never held. The top side tile drops from above, the bottom one rises. */
  .fiche .g-main {
    animation: gal-from-left 0.9s var(--ease) both;
    animation-delay: 150ms;
  }
  .fiche .g-side__item:nth-child(1) {
    animation: gal-from-top 0.85s var(--ease) both;
    animation-delay: 240ms;
  }
  .fiche .g-side__item:nth-child(2) {
    animation: gal-from-bottom 0.85s var(--ease) both;
    animation-delay: 300ms;
  }

  /* The detail blocks fade + rise in a short stagger; the price rail comes with them. */
  .fiche-main > * {
    animation: group-in 0.65s var(--ease) both;
  }
  .fiche-main > *:nth-child(1) {
    animation-delay: 280ms;
  }
  .fiche-main > *:nth-child(2) {
    animation-delay: 340ms;
  }
  .fiche-main > *:nth-child(3) {
    animation-delay: 400ms;
  }
  .fiche-main > *:nth-child(n + 4) {
    animation-delay: 450ms;
  }

  .fiche-layout > .price-rail {
    animation: group-in 0.7s var(--ease) both;
    animation-delay: 320ms;
  }

  @keyframes gal-from-left {
    from {
      transform: translateX(-90px);
    }
    to {
      transform: none;
    }
  }

  @keyframes gal-from-top {
    from {
      opacity: 0;
      transform: translateY(-70px);
    }
    to {
      opacity: 1;
      transform: none;
    }
  }

  @keyframes gal-from-bottom {
    from {
      opacity: 0;
      transform: translateY(70px);
    }
    to {
      opacity: 1;
      transform: none;
    }
  }
}

/* Fiche "similar" strip — below the fold, so it reveals on SCROLL instead of on load.
   Cancel the shared on-load card entrance and gate a scroll reveal on `.reveal-anim`
   (added by the fiche-similar island); no-JS / reduced-motion keep the strip static and
   visible. Same blur + rise as a loading card, staggered across the (few) tiles. */
.fiche-similar .grid-cars > .car {
  animation: none;
}

@media (prefers-reduced-motion: no-preference) {
  /* Transition on the TARGET state only — arming after an early paint must snap,
     not fade out (see the reveal-system note above). */
  .reveal-anim .fiche-similar .grid-cars > [data-similar-reveal] {
    opacity: 0;
    transform: translateY(44px);
    filter: blur(13px);
  }

  .reveal-anim .fiche-similar .grid-cars > [data-similar-reveal].is-in {
    opacity: 1;
    transform: none;
    filter: blur(0);
    transition:
      opacity 0.75s var(--ease),
      transform 0.75s var(--ease),
      filter 0.75s var(--ease);
    transition-delay: calc(var(--i, 0) * 70ms);
  }
}

/* About PAGE editorial sections — same treatment as the Services cells, delivered on SCROLL
   because the numbered sections run down a long page. Each stages in out of a soft blur ("flou
   levant") from alternating sides (odds from the left, evens from the right) as it enters view.
   Scoped to `[data-editorial-animate]` so the legal pages (same component) stay still; gated on
   `.reveal-anim` (added by the about island) so no-JS / reduced-motion keep every section static
   and visible. `overflow-x: clip` fences in the sideways travel. */
[data-editorial-animate] .editorial {
  overflow-x: clip;
}

@media (prefers-reduced-motion: no-preference) {
  /* Transition on the TARGET state only — arming after an early paint must snap,
     not fade out (see the reveal-system note above). */
  .reveal-anim [data-editorial-animate] .editorial__section[data-editorial-reveal] {
    opacity: 0;
    filter: blur(12px);
  }

  .reveal-anim [data-editorial-animate] .editorial__section[data-editorial-reveal]:nth-child(odd) {
    transform: translateX(-58px);
  }

  .reveal-anim [data-editorial-animate] .editorial__section[data-editorial-reveal]:nth-child(even) {
    transform: translateX(58px);
  }

  .reveal-anim [data-editorial-animate] .editorial__section[data-editorial-reveal].is-in {
    opacity: 1;
    transform: none;
    filter: blur(0);
    transition:
      opacity 0.85s var(--ease),
      transform 0.85s var(--ease),
      filter 0.85s var(--ease);
  }

  /* Generic stage items (Services cells, Rachat panels) — same beat, plain
     fade-up in place: no sideways travel, so no overflow fence needed. */
  .reveal-anim [data-editorial-animate] [data-stage-item][data-editorial-reveal] {
    opacity: 0;
    transform: translateY(26px);
    filter: blur(10px);
  }

  .reveal-anim [data-editorial-animate] [data-stage-item][data-editorial-reveal].is-in {
    opacity: 1;
    transform: none;
    filter: blur(0);
    transition:
      opacity 0.7s var(--ease),
      transform 0.7s var(--ease),
      filter 0.7s var(--ease);
  }
}

/* Fiche gallery thumbnails — they sit AT the fold, so they animate ON LOAD (a scroll
   trigger left them blank at opacity 0 until scrolled). A MARKED entrance: each tile
   surges up from off screen out of a heavy blur, staggered. Pure CSS, render-blocking →
   always ends visible, no flash; gated on reduced motion. */
@media (prefers-reduced-motion: no-preference) {
  .g-thumbs > .g-thumb {
    animation: thumb-in 0.8s var(--ease) both;
  }
  .g-thumbs > .g-thumb:nth-child(2) {
    animation-delay: 55ms;
  }
  .g-thumbs > .g-thumb:nth-child(3) {
    animation-delay: 110ms;
  }
  .g-thumbs > .g-thumb:nth-child(4) {
    animation-delay: 165ms;
  }
  .g-thumbs > .g-thumb:nth-child(5) {
    animation-delay: 220ms;
  }
  .g-thumbs > .g-thumb:nth-child(6) {
    animation-delay: 275ms;
  }
  .g-thumbs > .g-thumb:nth-child(7) {
    animation-delay: 330ms;
  }
  .g-thumbs > .g-thumb:nth-child(n + 8) {
    animation-delay: 370ms;
  }

  @keyframes thumb-in {
    from {
      opacity: 0;
      transform: translateY(80px);
      filter: blur(20px);
    }
    to {
      opacity: 1;
      transform: none;
      filter: blur(0);
    }
  }
}

/* Fiche spec grid — the values BUILD in on scroll, a beat after the gallery (the fiche
   text island drives it): each cell fades up in a quick stagger and its value types out
   behind a blinking caret ("Machine à écrire"). The block-level group-in is cancelled so
   the cells lead. Gated on `.is-anim` (added by the island) so no-JS / reduced-motion get
   the full static grid. */
.fiche-main > .specs {
  animation: none;
}

@media (prefers-reduced-motion: no-preference) {
  .specs.is-anim .spec {
    opacity: 0;
    transform: translateY(12px);
    transition:
      opacity 0.45s var(--ease),
      transform 0.45s var(--ease);
  }

  .specs.is-anim .spec.is-in {
    opacity: 1;
    transform: none;
  }

  .specs dd.is-typing::after {
    content: "";
    display: inline-block;
    width: 1px;
    height: 1em;
    margin-left: 2px;
    vertical-align: -0.12em;
    background: currentColor;
    animation: caret 1s steps(1) infinite;
  }

  @keyframes caret {
    50% {
      opacity: 0;
    }
  }
}

/* Word-level reveal — the "flou levant": each word rises out of a soft blur in a
   short left-to-right stagger. Same opt-in gate as [data-reveal] above — the words
   exist only once JS splits them, and hide only under `.reveal-anim`, so a JS-less
   or reduced-motion visitor reads the full, static heading. */
.reveal-anim [data-blur] .word {
  display: inline-block;
  opacity: 0;
  transform: translateY(0.42em);
  filter: blur(9px);
  transition:
    opacity 0.6s var(--ease),
    transform 0.72s var(--ease),
    filter 0.72s var(--ease);
  transition-delay: calc(var(--rise-base, 0ms) + var(--i, 0) * var(--rise-step, 60ms));
}

.reveal-anim [data-blur].is-in .word {
  opacity: 1;
  transform: none;
  filter: blur(0);
}

/* Editorial page headers (À propos, Services) and the form-page head (Rachat) —
   staged entrance: the eyebrow fades up, the title rises word by word, the lead
   follows a beat later, then the page's items arrive (À propos: the stats on
   their inline `--i`, figures rolling up in JS; then the staged sections). */
.page-header__title[data-blur],
.form-page__head .display[data-blur] {
  --rise-base: 140ms;
  --rise-step: 68ms;
}

.page-header__lead[data-blur],
.form-page__head .lead[data-blur] {
  --rise-base: 480ms;
  --rise-step: 26ms;
}

/* The title leads; its description follows a beat later and staggers faster, since
   it carries more words. */
.chapter__title[data-blur] {
  --rise-step: 68ms;
}

.chapter__intent[data-blur] {
  --rise-step: 30ms;
  --rise-base: 150ms;
}

/* On the opening, hold the copy back so the image settles into focus FIRST — a
   clearer beat between the frame arriving and the words rising. (The focus-in runs
   1.7s; the title comes in around its midpoint, the description a touch later.) */
.chapter--media .chapter__title[data-blur] {
  --rise-base: 640ms;
}

.chapter--media .chapter__intent[data-blur] {
  --rise-base: 880ms;
}

@media (prefers-reduced-motion: reduce) {
  /* No travelling glint around the card, the CTA, or the hero tag under reduced
     motion (each static hairline frame stays). */
  .home-hero__inner::after,
  .btn--beam::after,
  .hero-tag::after {
    display: none;
  }

  /* The metrics render their FINAL state immediately — no reveal, and crucially
     no delay-driven backwards-fill that would hide them for a beat. */
  .home-hero__stat dt,
  .home-hero__stat dd {
    animation: none;
  }

  .home-hero__stat::after {
    animation: none;
    transform: scaleX(1);
  }
}

/* =============================================================================
 * HOME — the chapters (skeleton)
 *
 * A page of chapters of deliberately UNEVEN length. `--chapter-screens` is set
 * per section in the markup and is the only thing this skeleton commits to: the
 * rhythm is the design decision, and it has to be legible before there is any
 * content to put in it. Native scroll throughout — nothing here pins, hijacks or
 * scrubs, and each chapter stays free to bring its own mechanic later.
 * ========================================================================== */

.home2 {
  position: relative;
}

.chapter {
  /* Slideshow: every chapter is one full screen, mobile and desktop. `svh` (small
     viewport height) so a mobile toolbar can never push a section past the fold.
     ⚠️ iOS Chrome re-evaluates the svh family WHILE its toolbars slide in/out on a
     scroll-direction change — and with ~6 chapters stacked in viewport units, a
     ~60pt bar transition became a ~360pt page leap (measured on device). The JS
     island freezes the measured height into `--chapter-vh` at load (width-change
     refresh only); svh stays as the no-JS fallback. */
  min-height: 100svh;
  min-height: var(--chapter-vh, 100svh);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: clamp(48px, 8vh, 120px);
  border-top: 1px solid var(--hairline);
}

/* Light chapters punctuate the dark ones — the alternation is the page's pulse,
   not decoration. Tokens flip so everything inside follows without a per-element
   override. */
.chapter--light {
  --page: var(--dd-white);
  --ink: var(--dd-ink);
  --ink-lift: var(--dd-ink-lift);
  --muted: var(--dd-smoke);
  --hairline: rgb(33 31 28 / 14%);
  --hairline-strong: rgb(33 31 28 / 28%);

  background: var(--dd-white);
  color: var(--dd-ink);
}

/* A chapter backed by a photograph — the opening. The image reads as a dark
   surface in BOTH themes (the brand's only permitted colour is the car), so the
   copy and furniture go ivory over it. */
.chapter--media {
  position: relative;
  isolation: isolate;
  border-top: 0;
  /* The frames swipe (the island swaps on a horizontal drag); vertical
     stays the page's own scroll. */
  touch-action: pan-y;
  /* Painted ON photography, so the whole surface reads "dark" in BOTH themes: the
     ink/page tokens invert to ivory-on-lacquer, and text AND buttons follow (a
     filled CTA becomes ivory with dark text, an outline CTA an ivory hairline). */
  color: var(--dd-ivory);
  --ink: var(--dd-ivory);
  --page: var(--dd-lacquer);
  --ink-lift: var(--dd-ivory-lift);
  --muted: rgb(242 239 234 / 68%);
  --hairline: rgb(242 239 234 / 16%);
  --hairline-strong: rgb(242 239 234 / 34%);
  /* Full-page hero: exactly one viewport, and NEVER taller than what's on screen
     (svh so a mobile toolbar can't push it over). The fixed header floats on top;
     the photo runs full-bleed behind it. */
  min-height: 100svh;
  min-height: var(--chapter-vh, 100svh);
  height: 100svh;
  height: var(--chapter-vh, 100svh);
  /* Copy sits bottom-left — the scrim is at its densest in that corner (left band
     + bottom floor), so it is also the most legible place for it. */
  justify-content: flex-end;
}

/* Hero CTAs: a row under the copy. On the opening they rise in a beat AFTER the
   description has read, so the frame → title → text → actions sequence holds. */
.chapter__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: clamp(26px, 3.5vh, 40px);
}

/* The concierge CTA over the opening photograph wears FROST: the image stays present under it,
   blurred to a glow — the glassy sibling of the filled stock CTA beside it. Resting state ONLY —
   the id-scoped selector would otherwise outweigh `.btn--outline:hover`'s ink fill and leave the
   hover's dark text on frosted glass. */
#ouverture .chapter__actions .btn--outline:not(:hover):not(:focus-visible) {
  background: rgb(255 255 255 / 10%);
  backdrop-filter: blur(9px);
  -webkit-backdrop-filter: blur(9px);
}

/* The opening on a phone strips to its ESSENTIALS: no title, no paragraph —
   the two CTAs floating mid-frame (the chapter's own centring), the
   photograph owning the frame, and the typed vehicle tag bottom-right. The
   caption does the talking. */
@media (max-width: 760px) {
  #ouverture.chapter--media {
    justify-content: center; /* the media chapter's flex-end serves the DESKTOP copy */
  }

  #ouverture .chapter__title,
  #ouverture .chapter__intent {
    display: none;
  }

  #ouverture .chapter__actions {
    /* Wrap allowed: with the concierge glyph aboard, the pair may not fit one
       narrow line — two centred lines beat two truncated buttons. */
    flex-wrap: wrap;
    justify-content: center;
    /* Dead centre of the frame — the concierge tab is tucked away on the
       opening (the hero carries its own concierge CTA), so the middle of
       the right edge is free. */
    margin-top: 0;
  }

  /* Compact: both CTAs trimmed just enough to fit the gutters. */
  #ouverture .chapter__actions .btn {
    --btn-pad-x: 14px;

    font-size: 12px;
  }
}

.chapter--media .chapter__actions {
  animation: cta-rise 0.7s var(--ease) 1.15s both;
}

@keyframes cta-rise {
  from {
    opacity: 0;
    transform: translateY(0.6em);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.chapter__media {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  background: var(--dd-lacquer); /* holds the frame while the photo loads */
  /* Loads INTO focus: the frame arrives soft and racks sharp — a small cinematic
     "camera settling" on the opening. */
  animation: media-focus-in 1.7s var(--ease) both;
}

/* Photographic slides stacked in the frame; the island cross-fades them. The
   "floated" transition: an inactive slide is blurred, scaled up and transparent,
   so a switch reads as one frame drifting OUT of focus while the next drifts IN —
   never a hard cut. `isolation` keeps each slide's sharp frame and blurred halo in
   their own stacking context. */
.chapter__slide {
  position: absolute;
  inset: 0;
  isolation: isolate;
  opacity: 0;
  filter: blur(14px);
  transform: scale(1.05);
  transition:
    opacity 1.4s var(--ease),
    filter 1.4s var(--ease),
    transform 1.4s var(--ease);
}

.chapter__slide.is-active {
  opacity: 1;
  filter: blur(0);
  transform: none;
}

.chapter__slide img {
  position: relative;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  animation: media-zoom 22s ease-in-out infinite alternate;
}

/* Pointing at the vehicle tag freezes the background: the slow Ken Burns zoom
   halts (the JS island also stops the 8 s frame-swap on hover) so the image the
   visitor is about to click through to never drifts under the pointer. */
.chapter--media:has(.hero-tag:hover) .chapter__slide img {
  animation-play-state: paused;
}

/* Radial focus — the cinematic ambiance, per slide: a blurred copy of that frame,
   shown only toward the EDGES through a radial mask, so the centre stays crisp and
   the picture falls off into a soft vignette-blur (shallow depth of field). */
.chapter__slide::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--slide-bg) center / cover no-repeat;
  filter: blur(16px);
  transform: scale(1.12); /* overscan, so the blur never bleeds a soft edge inward */
  -webkit-mask-image: radial-gradient(125% 125% at 50% 46%, transparent 28%, #000 80%);
  mask-image: radial-gradient(125% 125% at 50% 46%, transparent 28%, #000 80%);
}

/* Legibility scrim: heaviest at the left where the copy sits, opening up to the
   right so the detail still breathes; a soft floor grounds the bottom edge. Sits
   above every slide, so it holds whichever frame is showing. */
.chapter__scrim {
  position: absolute;
  inset: 0;
  z-index: 1; /* above the slides (which stack at z-index auto) */
  pointer-events: none;
  background:
    linear-gradient(100deg, rgb(15 13 12 / 86%) 0%, rgb(15 13 12 / 58%) 42%, rgb(15 13 12 / 10%) 100%),
    linear-gradient(to top, rgb(15 13 12 / 52%), rgb(15 13 12 / 0%) 46%);
}

/* Ambient loop chapter (the method): the clip plays itself under the scrim; its
   black fade-in/out are baked into the file, so the native loop restart passes
   through black invisibly. Both video and poster-less start read as the plain
   dark chapter (the fade opens FROM black). */
.chapter__loop-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Unlike the opening (copy seated bottom-left, under the hero CTAs and tag
   baseline), a film-backed chapter has nothing else to align with — its copy
   sits vertically centred over the video. */
.chapter--film {
  justify-content: center;
  /* Clip the chapter: the ARMED slat waits outside its own box (above it on
     mobile, right of it on desktop) — without the clip it hovers OVER the
     neighbouring section, smoked glass on the white method chapter (seen on
     iOS). Clipped, it simply emerges from the chapter's own edge. */
  overflow: hidden;
}

/* Before the deferred clip loads (and behind its baked black fades) the film
   chapter is plain DEEP BLACK — the media's default lacquer reads warm. */
.chapter--film .chapter__media {
  background: #000;
}

/* An autoplaying background is exactly what reduced-motion asks us not to run —
   drop it entirely (scrim over lacquer keeps the chapter legible). The play-once
   film has no autoplay attribute: the island never starts it under reduced
   motion, so it stays a static poster and may keep showing. */
@media (prefers-reduced-motion: reduce) {
  .chapter__loop-video[autoplay] {
    display: none;
  }
}



/* Vehicle tags — BOTTOM-right, desktop only, one per slide, only the active one
   shown; the island swaps them in step with the frames and TYPES the name in
   (machine à écrire, with a caret). NO box: text over the photo, held legible by a
   soft dark text-shadow. "en stock" appears once the name has finished typing. The
   whole thing links to that car's fiche. */
.hero-tag {
  position: absolute;
  /* Matches the section's own padding-block so the tag's bottom edge lands on the
     same baseline as the hero CTAs (which sit at that padding edge, bottom-left). */
  bottom: clamp(48px, 8vh, 120px);
  /* Track the centred Container's inner-right edge (mirroring the CTAs on the left)
     rather than gluing to the viewport edge — otherwise the tag drifts far out of
     the content column on ultra-wide screens. Falls back to the gutter when the
     viewport is narrower than the container. */
  right: max(var(--gutter), calc(50% - var(--container) / 2 + var(--gutter)));
  z-index: 4; /* above the media, scrim and copy */
  display: flex;
  flex-direction: column;
  align-items: flex-end; /* anchored to the bottom-right corner */
  gap: 3px;
  color: var(--dd-ivory);
  text-decoration: none;
  text-align: right;
  text-shadow:
    0 1px 16px rgb(15 13 12 / 72%),
    0 0 3px rgb(15 13 12 / 55%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.6s var(--ease);
}

.hero-tag.is-active {
  opacity: 1;
  pointer-events: auto;
}

/* Hover: a hairline frame materialises around the whole block, lapped by the
   same ivory beam glint the CTAs use. Offset OUTWARD (negative inset) so the
   tag stays box-less at rest — the frame only exists while you point at it. */
.hero-tag::before,
.hero-tag::after {
  content: "";
  position: absolute;
  inset: -13px -17px;
  border-radius: var(--radius);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.35s var(--ease);
}

.hero-tag::before {
  border: 1px solid rgb(242 239 234 / 24%);
}

.hero-tag::after {
  padding: 1px;
  background: conic-gradient(
    from var(--beam-angle),
    transparent 0deg,
    transparent 12deg,
    rgb(242 239 234 / 85%) 42deg,
    transparent 72deg,
    transparent 360deg
  );
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
}

.hero-tag:hover::before,
.hero-tag:focus-visible::before {
  opacity: 1;
}

.hero-tag:hover::after,
.hero-tag:focus-visible::after {
  opacity: 1;
  animation: beam-lap-btn 4s linear infinite;
}

/* The flourish also plays ITSELF, once, when the machine finishes typing
   (the island adds the class): frame and beam FADE in, the beam laps, and
   both FADE back out — one self-contained breath (the keyframes end at 0,
   so the island's class removal after the run changes nothing visible).
   On touch there is no pointer to reveal the frame, and even under one it
   says "this is a button" unprompted. */
.hero-tag.is-flourish::before {
  animation: tag-flourish-fade 4.6s var(--ease) both;
}

.hero-tag.is-flourish::after {
  animation:
    tag-flourish-fade 4.6s var(--ease) both,
    beam-lap-btn 4.6s linear both;
}

@keyframes tag-flourish-fade {
  0% {
    opacity: 0;
  }

  18% {
    opacity: 1;
  }

  82% {
    opacity: 1;
  }

  100% {
    opacity: 0;
  }
}

/* "● en stock" — a REC-style status flag above the model name: a green dot with
   its label, blinking gently like a recording light so the car reads as live in
   the stock. Green is a deliberate status accent on the otherwise monochrome
   brand — the visitor's one splash of "available". */
.hero-tag__rec {
  --rec: #4ade80;
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 3px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--rec);
  text-shadow:
    0 1px 14px rgb(15 13 12 / 78%),
    0 0 3px rgb(15 13 12 / 60%);
  animation: hero-rec-blink 1.9s ease-in-out infinite;
}

.hero-tag__rec-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--rec);
  box-shadow:
    0 0 6px 1px rgb(74 222 128 / 65%),
    0 0 2px rgb(74 222 128 / 90%);
}

@keyframes hero-rec-blink {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.4;
  }
}

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

.hero-tag__name {
  font-size: clamp(16px, 1.7vw, 20px);
  font-weight: 660;
  letter-spacing: -0.01em;
  min-height: 1.2em; /* hold the line height while it types from empty */
}

/* The caret — present only while the island is typing. Given NET-ZERO advance: it
   paints its 2px bar just past the last glyph, but the negative right margin cancels
   its own width so the line measures the same with or without it. Because the block
   is right-aligned, that is what keeps a finished line from recoiling rightward when
   the caret is removed (and the name from twitching as typing hands off to line 2). */
.hero-tag__caret {
  display: inline-block;
  width: 2px;
  height: 0.95em;
  margin-left: 0.06em;
  margin-right: calc(-2px - 0.06em);
  transform: translateY(0.12em);
  background: currentColor;
  animation: hero-caret 1s steps(1) infinite;
}

@keyframes hero-caret {
  50% {
    opacity: 0;
  }
}

/* The spec line — power · displacement · en stock — held back until the name has
   typed, then typed in itself: the island composes the three spans into one string
   so "en stock" rides the same machine-à-écrire as the rest (it is embedded in the
   line, not a separate reveal). */
.hero-tag__specs {
  /* Inline flow, NOT flex: a flex `gap` would sit between the typed text and the
     caret and vanish when the caret is removed, jolting this right-aligned line.
     Inline + a net-zero caret keeps it stable; spacing rides the separator below. */
  font-size: 11.5px;
  font-weight: 500;
  color: rgb(242 239 234 / 74%);
  font-variant-numeric: tabular-nums;
  transition: opacity 0.4s var(--ease);
}

.hero-tag.is-typing-name .hero-tag__specs {
  opacity: 0;
}

.hero-tag__spec + .hero-tag__spec::before {
  content: "·";
  margin: 0 9px;
  color: rgb(242 239 234 / 40%);
}

/* The tag stands bottom-right on desktop AND on the phone (where the copy
   moved to the frame's top, freeing its corner). Only the tablet in-between
   hides it: there the copy still owns the bottom and the two would collide. */
@media (761px <= width < 1024px) {
  .hero-tag {
    display: none;
  }
}

/* Slideshow pagination — bottom-centre indicators for the hero frames. Dots for
   the inactive frames; the current one stretches into a bright dash (tiret). Only
   meaningful once JS drives the swap, so hidden until the island adds `.is-live`
   (no-JS / reduced motion never reach it, and the first frame simply holds). */
.hero-dots {
  position: absolute;
  bottom: clamp(22px, 3.5vh, 40px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 4;
  display: none;
  align-items: center;
  gap: 8px;
}

.hero-dots.is-live {
  display: flex;
}

.hero-dots__dot {
  width: 7px;
  height: 7px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: rgb(242 239 234 / 38%);
  cursor: pointer;
  appearance: none;
  transition:
    width 0.45s var(--ease),
    background 0.45s var(--ease);
}

.hero-dots__dot:hover {
  background: rgb(242 239 234 / 62%);
}

.hero-dots__dot:focus-visible {
  outline: 2px solid var(--dd-ivory);
  outline-offset: 3px;
}

.hero-dots__dot.is-active {
  width: 24px;
  background: var(--dd-ivory);
  cursor: default;
}

@keyframes media-focus-in {
  from {
    filter: blur(12px);
    transform: scale(1.05);
    opacity: 0;
  }
  to {
    filter: blur(0);
    transform: none;
    opacity: 1;
  }
}

/* Ken-Burns light: a very slow, eased breath of zoom on the opening frame. It
   ping-pongs (alternate) on an ease-in-out curve, so it drifts in, slows, and
   reverses without ever snapping. The blurred halo drifts on the same beat from
   its 1.12 overscan, so sharp centre and soft edge move together. */
@keyframes media-zoom {
  from {
    transform: scale(1);
  }
  to {
    transform: scale(1.05);
  }
}

@media (prefers-reduced-motion: reduce) {
  /* The radial ambiance stays (it is not motion); the focus-in, the slow zoom, the
     cross-fade drift and the CTA rise do not play (nothing may stay hidden — the
     first slide simply holds). */
  .chapter__media,
  .chapter__slide,
  .chapter__slide img,
  .chapter--media .chapter__actions {
    animation: none;
    transition: none;
  }
}

.chapter__title {
  font-size: clamp(30px, 5vw, 64px);
  margin-bottom: 16px;
}

/* One voice for every chapter's description (standardised on the film
   chapter's treatment, 2026-07-30): a size up from the base lead, medium
   weight, FULL ink — the muted grey read washed out. `--ink` flips with the
   chapter (ivory over dark/media, ink on the white ones). */
.chapter__intent {
  max-width: 54ch;
  font-size: clamp(18px, 1.3vw + 13px, 24px);
  font-weight: 500;
  color: var(--ink);
}

/* Over photography or film, a double shadow (tight contact + wide halo) lifts
   the copy off the bright parts of the frame. Media chapters only — a shadow
   on a white page reads as dirt. */
.chapter--media .chapter__intent {
  text-shadow:
    0 1px 2px rgb(15 13 12 / 45%),
    0 4px 28px rgb(15 13 12 / 60%);
}

/* ---- The method chain (white chapter) ----------------------------------------
   The four stations of the work (selection → inspection → preparation →
   delivery) on a rail spanning the full container; an ink dot travels the
   rail and each station is revealed as it passes (reveal delays matched to
   the dot's stops). The island arms the stations hidden then sets the chain
   in motion — without JS or under reduced motion it stands revealed. Chosen
   on the five-variant artifact (V2 « La chaîne », 2026-07-30). */
.method-head {
  margin-bottom: clamp(10px, 2vh, 24px);
}

.method-head .chapter__intent {
  max-width: 68ch;
}

.method-rail {
  width: 100%;
}

.method-rail__track {
  position: relative;
  height: 1px;
  background: var(--hairline-strong);
  margin: clamp(30px, 6vh, 64px) 0 clamp(24px, 4vh, 44px);
}

.method-rail__tick {
  position: absolute;
  top: -7px;
  width: 1px;
  height: 15px;
  background: var(--hairline-strong);
}

.method-rail__dot {
  position: absolute;
  top: 50%;
  left: 87.5%;
  width: 11px;
  height: 11px;
  background: var(--ink);
  border-radius: 50%;
  transform: translate(-50%, -50%);
  opacity: 0;
}

.method-rail.in .method-rail__dot {
  animation: method-convoy 4.4s var(--ease) 0.2s forwards;
}

/* The dot pauses at each station — the holds are what read as "stops". */
@keyframes method-convoy {
  0% {
    opacity: 0;
    left: 0%;
  }

  4% {
    opacity: 1;
    left: 6%;
  }

  18% {
    left: 12.5%;
  }

  28% {
    left: 12.5%;
  }

  44% {
    left: 37.5%;
  }

  54% {
    left: 37.5%;
  }

  70% {
    left: 62.5%;
  }

  80% {
    left: 62.5%;
  }

  96% {
    left: 87.5%;
  }

  100% {
    opacity: 1;
    left: 87.5%;
  }
}

.method-rail__steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  column-gap: clamp(14px, 2.5vw, 40px);
}

.method-rail__num {
  display: block;
  font-size: clamp(30px, 3.6vw, 54px);
  font-weight: 260;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  margin-bottom: 12px;
}

.method-rail__name {
  font-size: clamp(16px, 1.5vw, 22px);
  font-weight: 700;
  font-stretch: 108%;
  margin: 0 0 8px;
}

.method-rail__proof {
  font-size: clamp(13.5px, 1vw, 15.5px);
  line-height: 1.55;
  color: var(--muted);
  margin: 0;
  text-wrap: pretty;
}

/* Each station rises with the house "flou levant" (the words split by the
   reveal island), but PHASED ON THE DOT'S STOPS instead of the observer:
   these rail-scoped rules carry more specificity than the generic
   `[data-blur].is-in` one, so `is-in` (which fires on mere visibility) never
   reveals a station early — only the rail's own `in` does, on the station's
   `--step-delay`. Number and name rise together; the proof follows a beat
   later, staggering faster (the head's own grammar). */
.method-rail__step:nth-child(1) {
  --step-delay: 0.35s;
}

.method-rail__step:nth-child(2) {
  --step-delay: 1.5s;
}

.method-rail__step:nth-child(3) {
  --step-delay: 2.65s;
}

.method-rail__step:nth-child(4) {
  --step-delay: 3.8s;
}

.reveal-anim .method-rail.is-armed [data-blur] .word {
  opacity: 0;
  transform: translateY(0.42em);
  filter: blur(9px);
}

.reveal-anim .method-rail.in [data-blur] .word {
  opacity: 1;
  transform: none;
  filter: blur(0);
  transition-delay: calc(var(--step-delay, 0s) + var(--i, 0) * 50ms);
}

.reveal-anim .method-rail.in .method-rail__proof[data-blur] .word {
  transition-delay: calc(var(--step-delay, 0s) + 150ms + var(--i, 0) * 24ms);
}

@media (max-width: 760px) {
  .method-rail__track {
    display: none;
  }

  /* Centred composition on a phone, like the proof and Instagram chapters. */
  #methode .chapter__title,
  #methode .chapter__intent,
  .method-rail__step {
    text-align: center;
  }

  .method-rail__steps {
    position: relative;
    grid-template-columns: 1fr 1fr;
    column-gap: 18px;
    /* 13px each side of the divider row (below): the air UNDER 01/02 equals
       the air OVER 03/04 by construction — equalising row HEIGHTS (1fr)
       left the shorter top row with the bigger hole under it. */
    row-gap: 13px;
  }

  /* No rail, no dot on a phone — instead the stations rise first (one quick
     cascade), then the 2×2 ENSEMBLE closes around them in two beats, both
     from the centre: a vertical hairline grows, then the horizontal, drawing
     the four cells. Default (no JS / reduced motion): the cross stands
     drawn. */
  .method-rail__steps::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: 1px;
    background: var(--hairline-strong);
  }

  /* The horizontal is a GRID ITEM on its own middle row (03/04 auto-flow
     past it), so it sits mid-gap whatever each row's content height. */
  .method-rail__steps::after {
    content: "";
    grid-row: 2;
    grid-column: 1 / -1;
    height: 1px;
    background: var(--hairline-strong);
  }

  .method-rail.is-armed .method-rail__steps::before {
    transform: scaleY(0);
  }

  .method-rail.is-armed .method-rail__steps::after {
    transform: scaleX(0);
  }

  .method-rail.in .method-rail__steps::before {
    transform: scaleY(1);
    transition: transform 0.55s var(--ease) 1.05s;
  }

  .method-rail.in .method-rail__steps::after {
    transform: scaleX(1);
    transition: transform 0.55s var(--ease) 1.65s;
  }

  /* The stations first, in one quick reading-order cascade — the cross
     closes on CONTENT, never on empty cells. */
  .method-rail__step:nth-child(1) {
    --step-delay: 0.1s;
  }

  .method-rail__step:nth-child(2) {
    --step-delay: 0.25s;
  }

  .method-rail__step:nth-child(3) {
    --step-delay: 0.4s;
  }

  .method-rail__step:nth-child(4) {
    --step-delay: 0.55s;
  }
}

@media (prefers-reduced-motion: reduce) {
  .method-rail .method-rail__dot {
    animation: none;
    opacity: 1;
  }
}

/* ---- The proof chapter: the odometer over the register ------------------------
   V1 + V2 of the proof artifact composed: the house figures roll like an
   odometer (one 0-9 reel per digit, columns cascading left to right), then the
   register of the latest keys handed over writes itself line by line, each one
   stamped. Same grammar as the method chain, but split in THREE beats
   ([data-proof-part]: counters, register, radiance), each armed hidden and set
   `in` by the island on ITS OWN first visible pixels — one chapter-level
   trigger played the register and the map unseen. The scoped [data-blur] rules
   out-rank the observer's `.is-in` so mere visibility reveals nothing early.
   No JS / reduced motion: reels landed on their digits, lines set. */
.proof-chapter {
  margin-top: clamp(20px, 3vh, 36px);
}

/* The proof composition nearly fills its screen, so the fixed header would
   float over the title at exact top alignment — this chapter alone carries
   the compensation (the short chapters centre well clear of it). */
#preuve.chapter {
  padding-top: calc(var(--header-h) + 16px);
  /* The ARMED stamps wait at scale(2.4) on the register's right edge, and
     transforms count as scrollable overflow: iOS grew a horizontal pan for
     the whole page from the top. `clip` (never `hidden`: one hidden axis
     would turn the chapter into a vertical scroll container) caps every
     armed-state bleed at the chapter's edge. */
  overflow-x: clip;
}

.proof__grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  column-gap: clamp(14px, 2.5vw, 40px);
  row-gap: 26px;
}

.proof__cell {
  --pd: calc(0.45s + var(--cell, 0) * 0.15s);

  border-top: 1px solid var(--hairline-strong);
  padding-top: 14px;
}

.proof-odo {
  display: flex;
  align-items: baseline;
  font-size: clamp(38px, 4.2vw, 60px);
  font-weight: 300;
  font-stretch: 78%;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  letter-spacing: 0.01em;
}

.proof-odo__col {
  display: block;
  overflow: hidden;
  height: 1em;
}

/* Default = LANDED on the digit: the no-JS and reduced-motion page simply
   reads the number. Only the island's armed state parks the reels at 0. */
.proof-odo__reel {
  display: block;
  transform: translateY(calc(var(--d) * -1em));
}

.proof-odo__reel span {
  display: block;
  height: 1em;
}

[data-proof-part].is-armed .proof-odo__reel {
  transform: translateY(0);
  transition: none;
}

[data-proof-part].in .proof-odo__reel {
  transform: translateY(calc(var(--d) * -1em));
  transition: transform 1.9s var(--ease);
  transition-delay: calc(0.35s + var(--cell, 0) * 0.15s + var(--col, 0) * 0.14s);
}

.proof-odo__gap {
  width: 0.18em;
}

.proof-odo__suffix {
  font-size: 0.5em;
  font-weight: 400;
  margin-left: 0.08em;
}

.proof__label {
  margin: 10px 0 0;
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}

.proof__sub {
  margin: 6px 0 0;
  font-size: 13px;
  line-height: 1.45;
  color: var(--muted);
  text-wrap: pretty;
}

/* ---- The register ---- */
.proof__register-wrap {
  margin-top: clamp(18px, 3vh, 32px);
}

.proof__register {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--hairline-strong);
}

.proof__line {
  --pd: calc(0.2s + var(--line, 0) * 0.35s);

  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  gap: 16px;
  align-items: center;
  padding: 10px 2px;
  border-bottom: 1px solid var(--hairline);
  font-size: clamp(13px, 1.2vw, 15.5px);
}

.proof__line-model {
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.proof__line-year {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.proof__stamp {
  justify-self: end;
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  border: 1.5px solid var(--ink);
  border-radius: var(--radius);
  padding: 3px 8px;
  transform: rotate(-6deg);
}

.proof__foot {
  --pd: 2.1s;

  margin-top: 16px;
  display: flex;
  justify-content: flex-end;
}

/* One LIGHT button — the register's only exit. */
.proof__foot .btn {
  --btn-pad-y: 10px;
  --btn-pad-x: 18px;

  font-size: 12px;
}

/* ---- The radiance: everything leaves from Luxembourg ---- */
.proof__ray {
  margin-top: clamp(20px, 3.5vh, 40px);
  display: grid;
  grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.7fr);
  column-gap: clamp(20px, 4vw, 56px);
  align-items: center;
}

.proof__ray-copy {
  /* Its own beat now: the countries figure rolls as the radiance enters
     (the generic reel delay applies — no register to wait for anymore). */
  --pd: 0.2s;
}

.proof__ray-svg {
  width: 100%;
  height: auto;
  display: block;
  /* Default (hidden) overflow on purpose: the big rings bleed past the frame
     and clip at its edges — never painting over the copy above the chart. */
}

.proof__ray-ring {
  fill: none;
  stroke: var(--hairline);
  stroke-width: 1;
}

.proof__ray-home {
  fill: var(--ink);
}

/* Arcs and labels: DEFAULT = finished map (no-JS / reduced motion). The
   armed state clears them; `in` draws each arc on its --ad, the comet — a
   short round dash slid along the normalised path — travels the route like
   the method's ink dot and PARKS on the destination as its marker. */
.proof__ray-arc {
  fill: none;
  stroke: var(--hairline-strong);
  stroke-width: 1;
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
}

.proof__ray-comet {
  fill: none;
  stroke: var(--ink);
  stroke-width: 3;
  stroke-linecap: round;
  /* The gap (2) must comfortably exceed the normalised path (1): with a ~1
     period, the NEXT repetition of the dash landed exactly on the path's end
     and its round cap pre-painted the destination dot in the ARMED state —
     the animation could be guessed before it played. */
  stroke-dasharray: 0.012 2;
  stroke-dashoffset: -0.988; /* the dash sits on the destination end */
}

.proof__ray-label {
  font-family: var(--font-sans);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  fill: var(--muted);
}

.proof__ray-label--home {
  font-weight: 800;
  fill: var(--ink);
}

[data-proof-part].is-armed .proof__ray-arc {
  stroke-dashoffset: 1;
}

[data-proof-part].in .proof__ray-arc {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 1.1s var(--ease) var(--ad, 0s);
}

[data-proof-part].is-armed .proof__ray-comet {
  stroke-dashoffset: 0.03; /* parked clear before the start: nothing shows */
}

[data-proof-part].in .proof__ray-comet {
  stroke-dashoffset: -0.988;
  transition: stroke-dashoffset 1.15s var(--ease) var(--ad, 0s);
}

/* The rings are part of the sequence too: armed blank, then they appear one
   after another — the stage briefly builds itself — right before the routes
   draw. Default (no JS / reduced motion): visible with the finished map. */
[data-proof-part].is-armed .proof__ray-ring {
  opacity: 0;
}

[data-proof-part].in .proof__ray-ring {
  opacity: 1;
  transition: opacity 0.55s var(--ease) var(--rd, 0.2s);
}

[data-proof-part].is-armed .proof__ray-label {
  opacity: 0;
}

[data-proof-part].in .proof__ray-label {
  opacity: 1;
  transition: opacity 0.6s var(--ease) calc(var(--ad, 0s) + 0.9s);
}

[data-proof-part].is-armed .proof__ray-home {
  opacity: 0;
}

[data-proof-part].in .proof__ray-home {
  opacity: 1;
  transition: opacity 0.5s var(--ease) 0.55s;
}

/* The sequence: rows and stamps are armed hidden, then rise on their --pd. */
[data-proof-part].is-armed .proof__line,
[data-proof-part].is-armed .proof__foot {
  opacity: 0;
  transform: translateY(8px);
}

[data-proof-part].in .proof__line,
[data-proof-part].in .proof__foot {
  opacity: 1;
  transform: none;
  transition:
    opacity 0.55s var(--ease) var(--pd),
    transform 0.55s var(--ease) var(--pd);
}

[data-proof-part].is-armed .proof__stamp {
  opacity: 0;
  transform: rotate(-6deg) scale(2.4);
}

[data-proof-part].in .proof__stamp {
  opacity: 0.92;
  transform: rotate(-6deg) scale(1);
  transition:
    opacity 0.28s ease-out calc(var(--pd) + 0.4s),
    transform 0.28s cubic-bezier(0.2, 1.6, 0.4, 1) calc(var(--pd) + 0.4s);
}

/* The figures rise with the SAME "flou levant" as the chapter title — armed
   blank, revealed a beat before their reels roll (the roll then plays under
   the lifting blur). Harmonised with the head's grammar (0.6/0.72s, --ease). */
[data-proof-part].is-armed .proof-odo {
  opacity: 0;
  transform: translateY(0.42em);
  filter: blur(9px);
}

[data-proof-part].in .proof-odo {
  opacity: 1;
  transform: none;
  filter: blur(0);
  transition:
    opacity 0.6s var(--ease),
    transform 0.72s var(--ease),
    filter 0.72s var(--ease);
  transition-delay: calc(var(--pd, 0s) - 0.2s);
}

/* The words themselves: the house "flou levant", phased on the sequence —
   labels rise with their cell, lines with their row, exactly the method
   chain's grammar (these rules out-rank the generic `[data-blur].is-in`). */
.reveal-anim [data-proof-part].is-armed [data-blur] .word {
  opacity: 0;
  transform: translateY(0.42em);
  filter: blur(9px);
}

.reveal-anim [data-proof-part].in [data-blur] .word {
  opacity: 1;
  transform: none;
  filter: blur(0);
  transition-delay: calc(var(--pd, 0s) + var(--i, 0) * 40ms);
}

.reveal-anim [data-proof-part].in .proof__sub[data-blur] .word {
  transition-delay: calc(var(--pd, 0s) + 150ms + var(--i, 0) * 24ms);
}

@media (max-width: 760px) {
  /* One column: every counter is a full-width rule + figure + label, so the
     breathing between the rules is equal by construction (the 2+1 grid gave
     unequal row heights — a two-line label opened a hole under its
     neighbour). */
  .proof__grid {
    grid-template-columns: 1fr;
    row-gap: 22px;
  }

  .proof__cell {
    --pd: calc(0.25s + var(--cell, 0) * 0.12s);
  }

  /* One screen is tighter here: the register writes faster. */
  .proof__line {
    --pd: calc(0.2s + var(--line, 0) * 0.22s);
  }

  .proof__foot {
    --pd: 1.5s;
  }

  .proof__line-year {
    display: none;
  }

  .proof__sub {
    display: none;
  }

  /* Centred composition on a phone: title, intent, cards, the countries
     block, the radar and the register foot all sit on the axis (the
     register lines alone stay a table). */
  #preuve .chapter__title,
  #preuve .chapter__intent {
    text-align: center;
  }

  .proof__cell,
  .proof__ray-copy {
    text-align: center;
  }

  .proof__foot {
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 10px;
  }

  .proof-odo {
    justify-content: center;
  }

  .proof__ray {
    grid-template-columns: 1fr;
    row-gap: 10px;
    justify-items: center;
  }

  /* The viewBox (720 units) renders at ~0.5 scale on a phone: sizes are in
     viewBox units, so the numbers below are ~half on screen. */
  .proof__ray-label {
    font-size: 16px;
  }

  /* The cells drop their subs on a phone, but the radiance keeps its
     punchline — it is the block's only prose. */
  .proof__ray-copy .proof__sub {
    display: block;
  }

  .proof__ray-comet {
    stroke-width: 5;
  }

  .proof__ray-home {
    r: 8;
  }
}

/* ---- The Instagram chapter (the closing white beat) ---------------------------
   Four stills of the house feed, re-served from our origin, each a link to its
   post. Same grammar as the proof: the island arms the block, frames rise in
   file order (--card), captions follow as flou levant, the reel mark pops
   last. No JS / reduced motion: the strip stands set. */
.insta-chapter {
  margin-top: clamp(26px, 4.5vh, 46px);
}

/* Taller than one viewport on a phone: landing at the chapter start must not
   slide the title under the fixed header (the proof's rule, same reason). */
#instagram.chapter {
  padding-top: calc(var(--header-h) + 16px);
}

.insta-rail {
  --insta-gap: clamp(14px, 1.6vw, 22px);
  position: relative;
}

/* Native scroll container: touch swipes for free, the arrows just drive
   `scrollBy` — the fullscreen viewer's navigation over a rail instead of a
   stage. The scrollbar is hidden (the arrows and the cut-off card edge are
   the affordance). */
.insta__viewport {
  overflow-x: auto;
  /* NEVER a vertical scroller: `overflow-x: auto` alone computes overflow-y
     to auto too, and the armed cards' translateY made the gallery pannable
     DOWN under a finger — the strip swipes on one axis only, the page keeps
     the other. */
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  outline: none;
}

.insta__viewport::-webkit-scrollbar {
  display: none;
}

.insta__track {
  display: flex;
  gap: var(--insta-gap);
}

/* One PAGE = one full view of EIGHT (4 × 2, row-major like the profile grid);
   the arrows turn pages, the snap keeps them whole. */
.insta__page {
  display: grid;
  flex: 0 0 100%;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--insta-gap);
  scroll-snap-align: start;
}

/* The arrows stand BELOW the strip, in its foot line, drawn chevron inside.
   Nothing overlays the cards anymore. */
.strip-nav {
  display: flex;
  gap: 10px;
}

.insta__nav {
  display: flex;
  align-items: center;
  justify-content: center;
  /* Alone on their line now (the follow button moved under the intent), the
     pair drops to a discreet square. */
  width: 40px;
  height: 40px;
  border: 1px solid var(--dd-lacquer);
  border-radius: 2px;
  background: var(--dd-lacquer);
  color: var(--dd-ivory);
  line-height: 0;
  cursor: pointer;
  transition:
    background-color var(--dur) var(--ease),
    color var(--dur) var(--ease),
    border-color var(--dur) var(--ease),
    opacity var(--dur) var(--ease);
}

.insta__nav svg {
  width: 18px;
  height: 18px;
}

/* The inverted (white) state belongs to REAL hover only: on touch, :hover
   sticks after the tap and a white button reads as "disabled? active?" —
   there, a button is black (touchable) or dimmed (not), nothing else. */
@media (hover: hover) {
  .insta__nav:hover:not([disabled]),
  .insta__nav:focus-visible {
    background: transparent;
    border-color: var(--dd-lacquer);
    color: var(--dd-lacquer);
  }
}

/* A static toolbar button DIMS at a dead end (vanishing would leave a hole
   in the pair). */
.insta__nav[disabled] {
  opacity: 0.25;
  pointer-events: none;
}

.insta__card {
  --pd: calc(0.35s + min(var(--card, 0), 7) * 0.1s);
  min-width: 0;
  color: inherit;
  text-decoration: none;
}

/* A series fetched by the island lands after the entrance already played —
   each fresh card gets its own small rise instead. */
.insta__card--fresh {
  animation: instaCardIn 0.5s var(--ease) both;
}

@keyframes instaCardIn {
  from {
    opacity: 0;
    transform: translateY(18px);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

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

.insta__frame {
  position: relative;
  display: block;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  border: 1px solid var(--hairline);
}

.insta__frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.9s var(--ease);
}

.insta__card:hover .insta__frame img,
.insta__card:focus-visible .insta__frame img {
  transform: scale(1.04);
}

/* The reel mark — same lacquer chip in both themes (it sits on photography). */
.insta__reel {
  position: absolute;
  top: 10px;
  right: 10px;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: rgb(19 17 16 / 72%);
  color: var(--dd-ivory);
}

/* The veil: caption + date slide up over the still under a lacquer gradient
   when the pointer hovers (the whole card stays one link to the post). */
.insta__veil {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 5px;
  padding: 16px 14px 14px;
  background: linear-gradient(
    to top,
    rgb(19 17 16 / 0.84),
    rgb(19 17 16 / 0.38) 46%,
    transparent 72%
  );
  color: var(--dd-ivory);
  opacity: 0;
  transition: opacity 0.4s var(--ease);
}

.insta__veil > * {
  transform: translateY(10px);
  transition: transform 0.55s var(--ease);
}

.insta__veil-caption {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  font-size: 13.5px;
  font-weight: 500;
  line-height: 1.45;
}

/* The quiet lines under the caption: the tags, then likes · comments · date. */
.insta__veil-tags {
  overflow: hidden;
  font-size: 11.5px;
  letter-spacing: 0.03em;
  white-space: nowrap;
  text-overflow: ellipsis;
  opacity: 0.72;
}

.insta__veil-meta {
  display: flex;
  align-items: center;
  gap: 12px;
}

.insta__veil-stat {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.insta__veil-stat svg {
  display: block;
}

.insta__veil-date {
  margin-left: auto;
  font-size: 12px;
  letter-spacing: 0.04em;
  white-space: nowrap;
  opacity: 0.78;
}

.insta__card:hover .insta__veil,
.insta__card:focus-visible .insta__veil {
  opacity: 1;
}

.insta__card:hover .insta__veil > *,
.insta__card:focus-visible .insta__veil > * {
  transform: none;
}

/* Touch has no hover: the island stamps `is-brushed` on the card the finger
   GRAZES (passive touchstart — the native tap still opens the post first
   try), and the veil rises for that one card, lighter, then fades back. */
@media (hover: none) {
  .insta__veil {
    background: linear-gradient(
      to top,
      rgb(19 17 16 / 0.72),
      rgb(19 17 16 / 0.22) 40%,
      transparent 62%
    );
  }

  .insta__card.is-brushed .insta__veil {
    opacity: 1;
  }

  .insta__card.is-brushed .insta__veil > * {
    transform: none;
  }
}

/* The follow CTA under the chapter's intent, ahead of the rail. */
.insta__follow {
  --pd: 0.15s;
  margin-bottom: clamp(18px, 3vh, 28px);
}

.insta__foot {
  --pd: 1.15s;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  margin-top: clamp(20px, 3vh, 30px);
}

/* The sequence: follow, cards and foot armed hidden, rising on their --pd. */
.insta-chapter.is-armed .insta__follow,
.insta-chapter.is-armed .insta__card,
.insta-chapter.is-armed .insta__foot {
  opacity: 0;
  transform: translateY(24px);
}

.insta-chapter.in .insta__follow,
.insta-chapter.in .insta__card,
.insta-chapter.in .insta__foot {
  opacity: 1;
  transform: none;
  transition:
    opacity 0.6s var(--ease) var(--pd),
    transform 0.7s var(--ease) var(--pd);
}

.insta-chapter.is-armed .insta__reel {
  opacity: 0;
  transform: scale(0.4);
}

.insta-chapter.in .insta__reel {
  opacity: 1;
  transform: none;
  transition:
    opacity 0.28s ease-out calc(var(--pd) + 0.55s),
    transform 0.3s cubic-bezier(0.2, 1.6, 0.4, 1) calc(var(--pd) + 0.55s);
}

@media (max-width: 760px) {
  .insta-rail {
    --insta-gap: 12px;
  }

  /* The eight of a page in two columns; the swipe turns pages natively, the
     arrows remain for thumbs that prefer them. */
  .insta__page {
    grid-template-columns: repeat(2, 1fr);
  }

  .insta__card {
    --pd: calc(0.25s + min(var(--card, 0), 7) * 0.08s);
  }

  /* Small frames: the persistent veil must stay a footer, not a poster. */
  .insta__veil {
    gap: 4px;
    padding: 12px 10px 10px;
  }

  .insta__veil-caption {
    -webkit-line-clamp: 2;
    font-size: 12.5px;
  }

  .insta__veil-tags {
    font-size: 10.5px;
  }

  .insta__veil-meta {
    gap: 8px;
  }

  .insta__veil-stat {
    gap: 4px;
    font-size: 11px;
  }

  .insta__veil-date {
    font-size: 10.5px;
  }

  /* Centred composition on a phone, like the proof above. */
  #instagram .chapter__title,
  #instagram .chapter__intent {
    text-align: center;
  }

  .insta__foot {
    --pd: 0.9s;
  }

  /* The follow CTA sits on the centred axis with the title and intent. */
  .insta__follow {
    text-align: center;
  }
}

/* ---- The chapter's second beat: the Google-reviews band ---------------------
   What the clients write, under the rail: the UNFILTERED aggregate as the
   header, then a native-scroll strip of review cards (the rail's arrows, the
   rail's snap), closed by the one line that says the cards are a selection.
   Fully SSR — the only JS is the arrows' scrollBy. */
.greviews {
  margin-top: clamp(34px, 6vh, 56px);
  padding-top: clamp(22px, 3.5vh, 34px);
  border-top: 1px solid var(--hairline);
}

.greviews__head {
  --pd: 0.35s;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 10px 16px;
  margin-bottom: clamp(16px, 2.5vh, 24px);
}

.greviews__score {
  display: inline-flex;
  align-items: baseline;
  gap: 10px;
}

.greviews__figure {
  font-size: clamp(26px, 2.6vw, 34px);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}

.greviews__stars {
  display: inline-flex;
  gap: 3px;
}

.greviews__stars svg {
  display: block;
}

.greviews__count {
  font-size: 14px;
  opacity: 0.72;
}

.greviews__all {
  margin-left: auto;
  font-size: 13px;
  letter-spacing: 0.02em;
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-color: var(--hairline-strong);
  transition: text-decoration-color var(--dur) var(--ease);
}

.greviews__all:hover,
.greviews__all:focus-visible {
  text-decoration-color: currentcolor;
}

.greviews__viewport {
  /* The cards' offsetParent: their offsetLeft must read in SCROLL coordinates
     (the drag-release lands on `card.offsetLeft` — without this anchor the
     offsets carry the container gutter and every landing sits off by it). */
  position: relative;
  overflow-x: auto;
  overflow-y: hidden; /* one-axis swipe — the gallery's rule */
  /* MANDATORY: a finger swipe must always LAND on a card, never rest between
     two (the desktop drag suspends this and lands itself). */
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  outline: none;
}

/* Desktop swipe (both strips): the open hand on the strip, the closed hand
   while dragging — with the snap suspended so the drag follows the mouse
   instead of fighting the magnet (the island lands on a stop on release). */
@media (hover: hover) {
  .insta__viewport,
  .greviews__viewport {
    cursor: grab;
  }
}

.insta__viewport.is-dragging,
.greviews__viewport.is-dragging {
  cursor: grabbing;
  user-select: none;
  scroll-snap-type: none;
}

.greviews__viewport::-webkit-scrollbar {
  display: none;
}

.greviews__strip {
  display: flex;
  gap: clamp(14px, 1.6vw, 22px);
}

.greviews__card {
  --pd: calc(0.5s + min(var(--card, 0), 7) * 0.08s);
  display: flex;
  flex: 0 0 clamp(280px, 24vw, 350px);
  flex-direction: column;
  gap: 12px;
  padding: 20px 20px 22px;
  border: 1px solid var(--hairline);
  color: inherit;
  text-decoration: none;
  scroll-snap-align: start;
  transition: border-color var(--dur) var(--ease);
}

.greviews__card:hover,
.greviews__card:focus-visible {
  border-color: var(--hairline-strong);
}

.greviews__card-head {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

.greviews__avatar {
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  object-fit: cover;
}

.greviews__who {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.greviews__name {
  overflow: hidden;
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.greviews__when {
  font-size: 12px;
  opacity: 0.62;
}

.greviews__card-stars {
  display: inline-flex;
  flex: 0 0 auto;
  gap: 2px;
  margin-left: auto;
}

.greviews__card-stars svg {
  display: block;
}

.greviews__text {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 5;
  overflow: hidden;
  font-size: 13.5px;
  line-height: 1.55;
  opacity: 0.86;
}

/* The band's foot: the arrow pair, RIGHT — mirroring the Instagram foot. */
.greviews__foot {
  --pd: 1.5s;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  margin-top: 14px;
}

/* The band rides the chapter's armed/in sequence, phased after the rail. */
.insta-chapter.is-armed .greviews__head,
.insta-chapter.is-armed .greviews__card,
.insta-chapter.is-armed .greviews__foot {
  opacity: 0;
  transform: translateY(24px);
}

.insta-chapter.in .greviews__head,
.insta-chapter.in .greviews__card,
.insta-chapter.in .greviews__foot {
  opacity: 1;
  transform: none;
  transition:
    opacity 0.6s var(--ease) var(--pd),
    transform 0.7s var(--ease) var(--pd);
}

@media (max-width: 760px) {
  .greviews__card {
    --pd: calc(0.35s + min(var(--card, 0), 7) * 0.07s);
    flex-basis: min(78vw, 320px);
    padding: 16px 16px 18px;
  }

  .greviews__head {
    justify-content: center;
  }

  .greviews__all {
    margin-left: 0;
  }
}

/* ---- The film chapter's frosted panel ----------------------------------------
   Once the film actually plays, a SLIM VERTICAL SLAT of glass slides in from
   the right (top sheet on mobile) — borderless, one block that belongs to the
   frame: the browser's native frosted glass (backdrop-filter) melts the film
   behind it into abstract moving shapes while the film stays sharp around
   it. The copy then rises with the house "flou levant", phased after the
   slat lands. Without JS or under reduced motion the panel stands in place,
   copy set. */
.chapter__glass {
  position: absolute;
  /* A vertical slat, full height, standing in the right third — wide enough
     for the harmonised type (the method chapter's title/intent sizes); the
     island lets it wander gently, sideways, on its own. */
  inset: 0 14% 0 auto;
  width: clamp(360px, 31vw, 480px);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: rgb(19 17 16 / 40%);
  -webkit-backdrop-filter: blur(26px) saturate(120%);
  backdrop-filter: blur(26px) saturate(120%);
}

.chapter__glass-body {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: clamp(12px, 2vh, 20px);
  height: 100%;
  justify-content: center;
  padding: calc(var(--header-h) + clamp(8px, 2vh, 20px)) clamp(22px, 2.2vw, 40px) clamp(22px, 4vh, 44px);
}

/* The description carries no weight — plain body colour on the glass. */
.chapter__glass-body .chapter__intent {
  font-weight: 400;
}

.chapter__glass-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: clamp(8px, 1.6vh, 16px);
}

/* Armed by the island (JS + motion OK): the slab waits off-stage;
   `is-film-playing` (set when playback actually starts) slides it in, then
   the copy rises. The armed class is dropped as playing lands, so these
   pre-states can never pin the panel off-stage (mobile cascade order). */
.chapter--film.glass-armed .chapter__glass {
  transform: translateX(calc(14vw + 110%));
}

.chapter--film.is-film-playing .chapter__glass {
  transform: none;
  transition: transform 0.9s var(--ease) 0.5s;
}

/* The copy rides the house "flou levant" (words split by the reveal island),
   but PHASED ON THE SLAB'S ARRIVAL, not the observer: these glass-scoped
   rules out-rank the generic `[data-blur].is-in`, so mere visibility never
   reveals the panel copy early. */
.reveal-anim .glass-armed .chapter__glass [data-blur] .word {
  opacity: 0;
  transform: translateY(0.42em);
  filter: blur(9px);
}

.reveal-anim .is-film-playing .chapter__glass [data-blur] .word {
  opacity: 1;
  transform: none;
  filter: blur(0);
  transition-delay: calc(1.2s + var(--i, 0) * 45ms);
}

.glass-armed .chapter__glass-actions {
  opacity: 0;
}

.is-film-playing .chapter__glass-actions {
  opacity: 1;
  transition: opacity 0.7s var(--ease) 2.2s;
}

/* The slat WANDERS ON ITS OWN: once the entrance has landed, the island
   drives a slow sideways float — two out-of-phase sines (periods 19s / 31s,
   phases randomised per visit), so the motion reads as aimless yet is
   C-infinity smooth: no jolt is even possible. Mobile's top sheet stays
   put. */

@media (max-width: 860px) {
  /* Mobile: the slat is a FULL-WIDTH band stepped a touch below the
     chapter's top — a thin strip of SHARP film shows above it (owner's
     ask) — static (the wander island never writes on mobile), the film
     sharp below; it drops in from above. Its height is EXACTLY its
     content — no third-of-section floor, no dead band (owner's call:
     never truncate, never pad out) — and the symmetric padding keeps the
     copy vertically centred in the band. */
  .chapter__glass {
    inset: clamp(32px, 5.5vh, 52px) 0 auto 0;
    width: 100%;
    height: auto;
    border-radius: 0;
  }

  .chapter__glass-body {
    justify-content: center;
    gap: 8px;
    height: auto;
    min-height: 0;
    padding-top: clamp(16px, 2.5vh, 24px);
    padding-bottom: clamp(16px, 2.5vh, 24px);
  }

  .chapter__glass-body .chapter__title {
    margin-bottom: 0;
  }

  .chapter__glass-actions {
    margin-top: 2px;
  }

  .chapter__glass-actions .btn {
    --btn-pad-y: 10px;
    --btn-pad-x: 14px;

    font-size: 12px;
  }

  .chapter--film.glass-armed .chapter__glass {
    transform: translateY(-108%);
  }
}

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

/* ---- The dots: a plain vertical progress indicator down the left edge --------
   One dot per chapter, no joining line — a pagination-dots set stacked vertically;
   the dots fill as you descend. An indicator, never a control. Monochrome — the
   island flips the tone so the dots read on the dark chapters AND the white ones,
   in either site theme:
     - default (a dark chapter, or the whole page in light theme): ink in light
       theme, ivory in dark theme, carried by `color` and inherited by every dot;
     - a `chapter--light` (white in BOTH themes): the island stamps
       [data-tone="light"] and the dots go ink, whatever the theme. */
.timeline {
  position: fixed;
  /* Pixel-snap the axis so a fractional left (from 2.2vw) can't render the dots on
     a half-column and blur them. round() lands the column on a whole pixel; the
     plain value above is the fallback where round() is unsupported. */
  left: clamp(14px, 2.2vw, 34px);
  left: round(clamp(14px, 2.2vw, 34px), 1px);
  top: 0;
  bottom: 0;
  z-index: 3;
  display: flex;
  align-items: center; /* centre the rail down the viewport */
  pointer-events: none;
  color: var(--dd-ink); /* light theme / white page: ink dots */
  opacity: 1;
  transition: opacity 500ms var(--ease);
}

/* Furniture for the content sections only: kept off the hero (the island adds
   this while the opening owns the screen), revealed from the second section on. */
.timeline.is-hidden {
  opacity: 0;
}

.timeline__rail {
  display: flex;
  flex-direction: column;
  /* Tight — the dots read as one compact set, not a scattered column. */
  gap: clamp(8px, 1.2vh, 12px);
}

.timeline__dot {
  width: 7px;
  height: 7px;
  border-radius: 999px;
  /* Solid pills throughout — state is carried by opacity and length, matching the
     slideshow pagination. Upcoming sections are dimmed dots. */
  background: currentcolor;
  opacity: 0.35;
  transition:
    height 400ms var(--ease),
    opacity 400ms var(--ease);
}

/* Passed sections: the dot comes up to full strength. */
.timeline__dot[data-on] {
  opacity: 1;
}

/* Current section: the dot stretches into a bright vertical dash (the vertical
   twin of the slideshow's active tiret). */
.timeline__dot[data-now] {
  opacity: 1;
  height: 22px;
}

/* Tone: dark theme lifts the default dots to ivory (a light chapter keeps ink). */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .timeline:not([data-tone="light"]) {
    color: var(--dd-ivory);
  }
}

:root[data-theme="dark"] .timeline:not([data-tone="light"]) {
  color: var(--dd-ivory);
}

.timeline[data-tone="light"] {
  color: var(--dd-ink);
}

/* Over a photographic (dark) chapter the dots stay ivory whatever the theme. */
.timeline[data-tone="dark"] {
  color: var(--dd-ivory);
}

/* Phones: the chapters carry their own headings; the rail would only crowd the
   reading column. */
@media (width <= 720px) {
  .timeline {
    display: none;
  }
}

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

/* ============================================================================
 * Concierge panel (V1) — the visitor concierge. Docked-right chat panel + a
 * bottom-right launcher pill. Same tokens and motion as the contact drawer.
 * ========================================================================== */
/* Concierge launcher — a round chat bubble in the bottom-right corner (it replaced the contact
   tab), with the live green dot ("loupiote") as its badge. */
.concierge-tab {
  position: fixed;
  right: 18px;
  bottom: calc(18px + env(safe-area-inset-bottom, 0px));
  z-index: 40;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  padding: 0;
  background: var(--ink);
  color: var(--page);
  border: 0;
  border-radius: 50%;
  box-shadow: 0 14px 34px -14px rgb(0 0 0 / 50%);
  cursor: pointer;
  transition:
    background-color var(--dur) var(--ease),
    opacity 250ms var(--ease),
    transform var(--dur-slow) var(--ease);
}

/* Our `display` would silently defeat the UA's `[hidden]` rule (same guard as the drawer's):
   without JS the launcher must not ship a dead button. */
.concierge-tab[hidden] {
  display: none;
}

/* Swallowed while the floating line speaks: the line IS the bubble mid-morph — nothing else may sit
   in its seat. Instant on the way in (the seed circle covers the swap, pixel-identical); on release
   the base 250ms opacity transition plays — the bubble's half of the landing CROSS-fade. */
.concierge-tab[data-swallowed="true"] {
  visibility: hidden;
  opacity: 0;
  transition: none;
}

/* On the home OPENING the bubble yields — the hero carries its own "Parler au
   concierge" CTA (same slide as the tuck; the timeline island drives it).
   It returns the moment the second chapter owns the screen. */
.concierge-tab.is-hero-tucked {
  transform: translateX(calc(100% + 18px));
  transition: transform var(--dur) var(--ease);
}
.concierge-tab:hover {
  background: var(--ink-lift);
}
.concierge-tab:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* On the home the tab ADAPTS to the chapter under it (stamped by the timeline
   island, exactly like the rail's dots): ink-filled over a white chapter; the
   default (theme ink — ivory on the dark home) everywhere else, photography
   included. AFTER the plain :hover rule so the toned ground never falls back
   to the theme's lift. */
.concierge-tab[data-tone="light"] {
  background: var(--dd-ink);
  color: var(--dd-white);
}

.concierge-tab[data-tone="light"]:hover {
  background: var(--dd-ink-lift);
}
/* The concierge glyph carries the life itself (the green LED is gone — monochrome charter): whole
   at rest, and every 5s a gleam of its OWN colour crosses it once — the icon is the mask, a band
   passes over it, the glyph dips only for the beat the light takes to go by. Shared by the corner
   launcher and the home hero's "Parler au concierge" CTA. */
.concierge-glyph {
  position: relative;
  display: inline-flex;
}
.concierge-glyph svg {
  display: block;
  animation: concierge-sheen-dim 5s ease-in-out infinite;
}
.concierge-glyph::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(115deg, transparent 32%, currentColor 50%, transparent 68%);
  background-size: 300% 100%;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7.9 20A9 9 0 1 0 4 16.1L2 22Z'/%3E%3Ccircle cx='8' cy='12' r='1.15' fill='black' stroke='none'/%3E%3Ccircle cx='12' cy='12' r='1.15' fill='black' stroke='none'/%3E%3Ccircle cx='16' cy='12' r='1.15' fill='black' stroke='none'/%3E%3C/svg%3E")
    center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7.9 20A9 9 0 1 0 4 16.1L2 22Z'/%3E%3Ccircle cx='8' cy='12' r='1.15' fill='black' stroke='none'/%3E%3Ccircle cx='12' cy='12' r='1.15' fill='black' stroke='none'/%3E%3Ccircle cx='16' cy='12' r='1.15' fill='black' stroke='none'/%3E%3C/svg%3E")
    center / contain no-repeat;
  opacity: 0;
  animation: concierge-sheen-pass 5s ease-in-out infinite;
}
@keyframes concierge-sheen-dim {
  0%, 72%, 100% {
    opacity: 1;
  }
  80%, 92% {
    opacity: 0.45;
  }
}
@keyframes concierge-sheen-pass {
  0%, 72% {
    background-position: 120% 0;
    opacity: 0;
  }
  76% {
    opacity: 1;
  }
  96% {
    opacity: 1;
  }
  100% {
    background-position: -20% 0;
    opacity: 0;
  }
}

.concierge-scrim {
  position: fixed;
  inset: 0; /* fallback anchoring for engines without dvh */
  /* Same iOS toolbar lag as the panel: the dynamic viewport height keeps the veil sealed to the
     bottom edge while Safari's toolbar comes and goes. */
  height: 100dvh;
  z-index: 105;
  background: rgb(10 9 7 / 40%);
  /* Swallow touch gestures on the backdrop — a drag over the scrim must not scroll the page. */
  touch-action: none;
  opacity: 0;
  visibility: hidden;
  transition:
    opacity var(--dur-slow) var(--ease),
    visibility 0s linear var(--dur-slow);
}
.concierge-scrim[data-open="true"] {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--dur-slow) var(--ease);
}

.concierge {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0; /* fallback anchoring for engines without dvh */
  z-index: 110;
  display: flex;
  flex-direction: column;
  /* iOS Safari: the layout viewport the top/bottom anchoring tracks lags the retracting toolbar,
     leaving a strip of page visible under the panel. The DYNAMIC viewport height follows the
     toolbar in real time (and overrides the bottom anchor wherever dvh is understood). */
  height: 100dvh;
  width: min(420px, 100%);
  background: var(--page);
  border-left: 1px solid var(--hairline);
  box-shadow: 0 24px 70px -30px rgb(0 0 0 / 45%);
  transform: translateX(104%);
  visibility: hidden;
  transition:
    transform var(--dur-slow) var(--ease),
    visibility 0s linear var(--dur-slow);
}
.concierge[data-open="true"] {
  transform: none;
  visibility: visible;
  transition: transform var(--dur-slow) var(--ease);
}
/* Painted open by the pre-paint head script (panel was open before a proactive nav): appear at once,
   no slide — the island removes the flag on boot so later open/close animate normally. */
html[data-concierge-preopen] .concierge {
  transform: none;
  visibility: visible;
  transition: none;
}
/* The scrim is pre-painted ONLY where the panel is modal (the full-screen phone one). On desktop the
   panel is docked beside the page and never dims it, so pre-painting a scrim there would flash a
   veil over the car for one frame before the island boots. */
@media (max-width: 520px) {
  html[data-concierge-preopen] .concierge-scrim {
    opacity: 1;
    visibility: visible;
    transition: none;
  }
}

.concierge__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  /* Match the site header's total height (--header-h + its 1px hairline) so the two bottom borders
     line up: the panel is docked at top:0 like the header, so equal heights align the borders. */
  min-height: calc(var(--header-h) + 1px);
  padding: 0 clamp(18px, 3vw, 26px);
  border-bottom: 1px solid var(--hairline);
}
.concierge__title {
  font-size: 15px;
  letter-spacing: 0.02em;
}

.concierge__log {
  flex: 1;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 20px clamp(18px, 3vw, 26px);
  scrollbar-width: thin;
  scrollbar-color: var(--hairline-strong) transparent;
  /* The ONLY scroller in the panel — keep its overscroll here so a drag past the ends never
     chains to the page behind (mobile: the background must stay put while chatting). */
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

.concierge-bubble {
  max-width: 85%;
  margin: 0;
  padding: 11px 14px;
  border-radius: 14px;
  font-size: 14px;
  line-height: 1.5;
}
/* The concierge speaks from under a face: a square top-left corner points the bubble at the
   little "bonhomme" beside it — the dialogue has someone on the other end. */
.concierge-bubble--agent {
  align-self: flex-start;
  position: relative;
  margin-left: 32px;
  max-width: calc(85% - 32px);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: 0 14px 14px 14px;
}
/* The face is pure CSS (circle + masked glyph), so it survives every rewrite of the bubble's
   text — the typewriter clears textContent, hydration rebuilds bubbles, the intro is
   server-rendered: none of them have to know the avatar exists. */
.concierge-bubble--agent::before {
  content: "";
  position: absolute;
  top: 0;
  left: -32px;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--ink);
}
.concierge-bubble--agent::after {
  content: "";
  position: absolute;
  top: 5px;
  left: -27px;
  width: 14px;
  height: 14px;
  background: var(--page);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='8' r='4'/%3E%3Cpath d='M20 21a8 8 0 0 0-16 0'/%3E%3C/svg%3E")
    center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='8' r='4'/%3E%3Cpath d='M20 21a8 8 0 0 0-16 0'/%3E%3C/svg%3E")
    center / contain no-repeat;
}
.concierge-bubble--you {
  align-self: flex-end;
  background: var(--ink);
  color: var(--page);
  border-bottom-right-radius: 4px;
}
/* The "thinking" indicator: three bouncing dots (WhatsApp-style), not words. */
.concierge-typing {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 14px 16px;
}
.concierge-typing span {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ink);
  opacity: 0.35;
  animation: concierge-bounce 1.3s ease-in-out infinite;
}
.concierge-typing span:nth-child(2) {
  animation-delay: 0.18s;
}
.concierge-typing span:nth-child(3) {
  animation-delay: 0.36s;
}
@keyframes concierge-bounce {
  0%,
  60%,
  100% {
    transform: translateY(0);
    opacity: 0.35;
  }
  30% {
    transform: translateY(-5px);
    opacity: 0.9;
  }
}

/* The typewriter caret — a soft blinking bar while a reply types itself out. */
/* The caret takes NO width: it is drawn beside the last written letter without occupying a single
   pixel of the line. A caret with a real width would push the not-yet-written text and could still
   send a word to the next line — the very jitter the transparent remainder exists to prevent. */
.concierge-caret {
  position: relative;
  display: inline-block;
  width: 0;
  height: 1em;
  vertical-align: text-bottom;
  animation: concierge-blink 1s step-end infinite;
}
.concierge-caret::after {
  content: "";
  position: absolute;
  left: 1px;
  top: 0;
  bottom: 0;
  width: 2px;
  background: currentColor;
  opacity: 0.7;
}

/* What has not been written yet: present in the layout, invisible on the screen. It is what fixes
   the line breaks from the first frame. */
.concierge-ghost {
  opacity: 0;
}
@keyframes concierge-blink {
  0%,
  100% {
    opacity: 0.7;
  }
  50% {
    opacity: 0;
  }
}

.concierge__form {
  display: flex;
  gap: 10px;
  align-items: center;
  padding: 16px clamp(18px, 3vw, 26px);
  /* The panel now runs under the iOS home indicator (viewport-fit=cover) — the input must not. */
  padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--hairline);
}
.concierge__input {
  flex: 1;
  min-width: 0;
  padding: 11px 14px;
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--hairline-strong);
  border-radius: 999px;
  font: inherit;
  font-size: 14px;
}
.concierge__input:focus-visible {
  outline: none;
  border-color: var(--ink);
}
/* Icon-only round send button — keeps the input wide. */
.concierge__send {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  padding: 0;
  background: var(--ink);
  color: var(--page);
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  transition:
    opacity var(--dur) var(--ease),
    transform var(--dur) var(--ease);
}
.concierge__send:hover {
  transform: translateY(-1px);
}
.concierge__send:disabled {
  opacity: 0.4;
  cursor: default;
  transform: none;
}

/* ── Dictation (Lot 5) ─────────────────────────────────────────────────────
   The mic sits between the input and the send button — outline where the send
   is filled, so the pair reads as one family with one primary. While recording,
   the input's own text steps aside and the waveform canvas draws OVER it; while
   transcribing, the button wears a turning ring (the icon stays — the visitor
   keeps the context of WHAT is busy). */
.concierge__input-wrap {
  position: relative;
  flex: 1;
  min-width: 0;
  display: flex;
}
.concierge__input-wrap .concierge__input {
  flex: 1;
  min-width: 0;
}
.concierge__wave {
  position: absolute;
  inset: 0;
  /* A canvas is a REPLACED element: `inset: 0` alone doesn't stretch it — without these two it
     keeps its intrinsic 300×150 and overflows the pill. */
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease);
}
.concierge__mic {
  position: relative;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  padding: 0;
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--hairline-strong);
  border-radius: 50%;
  cursor: pointer;
  transition:
    color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    transform var(--dur) var(--ease);
}
@media (hover: hover) {
  .concierge__mic:hover:not(:disabled) {
    transform: translateY(-1px);
  }
}
.concierge__mic:disabled {
  opacity: 0.4;
  cursor: default;
  transform: none;
}
/* Recording: the mic goes red — constant ring so the state holds BETWEEN ripple cycles, plus a
   single expanding wave (one wave: a capture, not a broadcast). */
.concierge__mic[aria-pressed="true"] {
  color: var(--concierge-recording);
  border-color: var(--concierge-recording);
  box-shadow: 0 0 0 1px var(--concierge-recording);
}
.concierge__mic[aria-pressed="true"]::after {
  content: "";
  position: absolute;
  inset: -1px;
  border: 2px solid var(--concierge-recording);
  border-radius: 50%;
  pointer-events: none;
  animation: concierge-mic-ripple 1.6s var(--ease) infinite;
}
@keyframes concierge-mic-ripple {
  from {
    transform: scale(1);
    opacity: 0.45;
  }
  to {
    transform: scale(2);
    opacity: 0;
  }
}
/* Transcribing: the waveform froze on the visitor's tap; the border becomes a turning ring until
   the text lands. `aria-busy` and `aria-pressed="true"` never coexist (disjoint windows). */
.concierge__mic[aria-busy="true"] {
  border-color: transparent;
}
.concierge__mic[aria-busy="true"]::before {
  content: "";
  position: absolute;
  inset: -1px;
  border: 2px solid var(--concierge-recording);
  border-top-color: transparent;
  border-radius: 50%;
  pointer-events: none;
  animation: concierge-mic-spin 0.8s linear infinite;
}
@keyframes concierge-mic-spin {
  to {
    transform: rotate(360deg);
  }
}
/* While the bars draw over the input, its own text (and placeholder) steps aside. */
.concierge__form--recording .concierge__wave {
  opacity: 1;
}
.concierge__form--recording .concierge__input {
  color: transparent;
  caret-color: transparent;
}
.concierge__form--recording .concierge__input::placeholder {
  color: transparent;
}
/* Dictation status line — visible feedback AND the aria-live channel. Hidden when empty. */
.concierge__mic-status {
  margin: 0;
  padding: 0 clamp(18px, 3vw, 26px) 8px;
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--muted);
}
@media (prefers-reduced-motion: reduce) {
  .concierge__mic[aria-pressed="true"]::after {
    animation: none;
    opacity: 0.45;
    transform: none;
  }
  .concierge__mic[aria-busy="true"]::before {
    animation: none;
    border-top-color: var(--concierge-recording); /* full static ring instead of a spinner */
  }
  .concierge__wave {
    transition: none;
  }
}

/* The links a message earned, on ONE line under its bubble: a selection recall next to the vehicle
   (or the passage). They are a single offer read in a glance, not a little stacked menu — and they
   wrap rather than overflow when the panel is narrow. */
.concierge__recalls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 10px;
  align-self: flex-start;
  max-width: 100%;
}

/* Sober per-message "recall this view" link — tucked under the concierge bubble that proposed it.
   Small icon + muted label; it repeats on every proposal (and every past one), so it stays discreet
   and never competes with the message. */
.concierge__recall {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: -6px 0 2px 2px;
  padding: 3px 4px;
  background: none;
  border: 0;
  color: var(--muted);
  font: inherit;
  font-size: 12px;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: color var(--dur) var(--ease);
}
.concierge__recall svg {
  flex: 0 0 auto;
  opacity: 0.7;
}
.concierge__recall:hover,
.concierge__recall:focus-visible {
  color: var(--ink);
  outline: none;
}
.concierge__recall:hover svg,
.concierge__recall:focus-visible svg {
  opacity: 1;
}

/* The floating line (phones): once the panel folds to reveal what it just changed, the concierge
   keeps speaking from here — same bubble as in the log, laid over the page above the launcher tab.
   Tapping it re-opens the conversation; a scroll or a tap elsewhere sends it away. */
.concierge-toast {
  position: fixed;
  right: 16px;
  bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  left: 16px;
  z-index: 108;
  max-width: 460px;
  margin-left: auto;
  padding: 12px 15px;
  /* The LAUNCHER's material, not the panel's: ink on the light theme, ivory in the dark room —
     always the page's negative. The cartouche is the bubble mid-shape, so it keeps its skin (and
     the inverted ground needs no border to stand off the page). */
  background: var(--ink);
  color: var(--page);
  border-radius: 14px 14px 4px 14px;
  box-shadow: 0 18px 44px -22px rgb(0 0 0 / 55%);
  font-size: 14px;
  line-height: 1.5;
  text-wrap: pretty;
  cursor: pointer;
  opacity: 0;
  transform: translateY(10px);
  transition:
    opacity var(--dur) var(--ease),
    transform var(--dur) var(--ease);
}
.concierge-toast[data-open="true"] {
  opacity: 1;
  transform: none;
}
/* Leaving is quicker than arriving: the visitor has just reached for the page, and a line that
   lingers on its way out is in the way. The veil behind it leaves on the same beat. */
/* Unhurried on the way out — 900 ms, still readable as it goes. A line that vanishes reads as
   information taken back, and the visitor must never wonder what they missed. */
.concierge-toast:not([data-open="true"]) {
  transition:
    opacity 900ms var(--ease),
    transform 900ms var(--ease);
}

/* Mid-morph — the launcher's circle becoming the message rectangle (and back). The geometry
   travels inline (the rectangle's size depends on its sentence, the JS drives it); only the
   appearance and the journey's easing live here. The text holds its breath until the shape has
   arrived. */
.concierge-toast[data-morph] {
  opacity: 1;
  transform: none;
  overflow: hidden;
  transition:
    width 0.75s var(--ease),
    height 0.75s var(--ease),
    right 0.75s var(--ease),
    bottom 0.75s var(--ease),
    border-radius 0.75s var(--ease),
    padding 0.75s var(--ease),
    background-color 0.75s var(--ease),
    border-color 0.75s var(--ease);
}
.concierge-toast[data-morph] .concierge-toast__line,
.concierge-toast[data-morph] .concierge-toast__hint {
  opacity: 0;
  transition: opacity 150ms var(--ease);
}

/* The sentence, and under it the way out: set back, quieter, and only offered once the line has
   finished writing itself. */
.concierge-toast__line {
  display: block;
  /* Bold on purpose: the concierge's one line over the page must carry — it is the event. */
  font-weight: 600;
}
.concierge-toast__hint {
  display: block;
  margin-top: 10px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--page);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}
/* Quieter than the sentence above it — the way out whispers (0.65 on the inverted ground plays
   the role --muted played on the panel's). */
.concierge-toast__hint[data-open="true"] {
  opacity: 0.65;
}

/* The panel's backdrop, borrowed by the floating line to put it forward. A finger goes THROUGH it:
   the gesture that dismisses the veil should scroll the page it uncovers, not be eaten by it. */
.concierge-scrim[data-toast] {
  touch-action: auto;
  /* Arrives with the bubble, not on the panel's slower beat — the two are one gesture. */
  transition: opacity var(--dur) var(--ease);
}
.concierge-scrim[data-toast]:not([data-open="true"]) {
  transition:
    opacity 900ms var(--ease),
    visibility 0s linear 900ms;
}

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

/* Reset confirmation — a sober veil INSIDE the panel: one question, two buttons. */
.concierge-confirm {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: grid;
  place-items: center;
  padding: 24px;
  background: rgb(10 9 7 / 26%);
  /* Frosted: the conversation stays present but unreadable — the question owns the moment.
     (backdrop-filter is binary under an opacity transition: the frost lands with the fade, it
     does not ramp — accepted, the veil is quick.) */
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
  opacity: 0;
  visibility: hidden;
  transition:
    opacity var(--dur) var(--ease),
    visibility 0s linear var(--dur);
}
.concierge-confirm[data-open="true"] {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--dur) var(--ease);
}
.concierge-confirm__card {
  width: min(320px, 100%);
  padding: 20px;
  background: var(--page);
  border: 1px solid var(--hairline);
  border-radius: 10px;
  box-shadow: 0 18px 44px -22px rgb(0 0 0 / 55%);
}
.concierge-confirm__title {
  margin: 0 0 16px;
  font-size: 14px;
  line-height: 1.5;
  text-wrap: pretty;
}
.concierge-confirm__actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}
.concierge-confirm__btn {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 9px 14px;
  background: transparent;
  color: var(--ink);
  border: 1px solid var(--hairline-strong);
  border-radius: 8px;
  cursor: pointer;
  transition: background-color var(--dur) var(--ease);
}
.concierge-confirm__btn:hover {
  background: var(--surface);
}
.concierge-confirm__btn--go {
  background: var(--ink);
  color: var(--page);
  border-color: var(--ink);
}
.concierge-confirm__btn--go:hover {
  background: var(--ink-lift);
}
.concierge-confirm__btn:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* Icon-only header actions (reset · close). */
.concierge__head-actions {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.concierge__icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-radius: 8px;
  opacity: 0.55;
  cursor: pointer;
  transition:
    background-color var(--dur) var(--ease),
    opacity var(--dur) var(--ease);
}
.concierge__icon-btn:hover {
  background: var(--surface);
  opacity: 1;
}

@media (max-width: 520px) {
  .concierge {
    width: 100%;
    /* Full screen: the left border is the DESKTOP seam (it detaches the docked panel from the
       page beside it). With nothing beside it, it reads as a stray grey line on the left edge. */
    border-left: 0;
  }
  /* iOS Safari zooms the whole page in when a focused input is < 16px — 16px holds the view steady. */
  .concierge__input {
    font-size: 16px;
  }
}
@media (prefers-reduced-motion: reduce) {
  .concierge,
  .concierge-scrim,
  .concierge-tab,
  .concierge-confirm {
    transition: none;
  }
  .concierge-glyph svg,
  .concierge-glyph::after,
  .concierge-typing span,
  .concierge-caret {
    animation: none;
  }
}

/* ============================================================================
 * The concierge's EVIDENCE layer — the marker stroke.
 *
 * A card is a summary and a fiche is long: when the visitor selects on a fact
 * neither surface shows ("warranted at least a year", "leather interior"), the
 * concierge writes that fact under the thumbnail, or marks the passage that
 * carries it, and the stroke SWEEPS as it enters the viewport — a highlighter
 * drawn over the line, not a colour that was always there.
 *
 * The pigment is a THEME TOKEN (`--mark`, tokens.css): full strength on paper,
 * tempered and faintly blooming in the dark room. Its own ink stays the dark
 * ink in both themes — a highlighter is translucent, the text shows through.
 * Nothing here hard-codes a colour, so a theme flip re-calibrates the marker.
 * ========================================================================== */

.cx-note {
  margin: 14px 0 0;
  padding-top: 13px;
  border-top: 1px solid var(--hairline);
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink);
  text-wrap: pretty;
}

.cx-hl {
  /* The stroke is a background LAYER, so it grows from nothing to full width without reflowing a
     single character — and the un-drawn state is simply a marker that has not passed yet. The last
     few pixels thin out: a marker tip lays less pigment at the leading edge, and that soft edge is
     what makes the sweep read as a stroke rather than a wipe. */
  background-image: linear-gradient(
    90deg,
    var(--mark) 0%,
    var(--mark) calc(100% - 6px),
    color-mix(in oklab, var(--mark), transparent 55%) 100%
  );
  background-repeat: no-repeat;
  background-position: 0 0;
  background-size: 0% 100%;
  color: inherit;
  transition:
    background-size var(--dur-slow) var(--ease),
    box-shadow var(--dur-slow) var(--ease),
    color var(--dur-slow) var(--ease);
}

/* The INLINE stroke (a note under a card, a marked phrase in a paragraph): it bleeds a couple of
   pixels around the words, like a marker that overshoots, and clones that box on every wrapped
   line. Square-edged at `--radius`, like every other frame on the site. */
.cx-note .cx-hl,
mark.cx-hl {
  padding: 2px 4px;
  margin: 0 -2px;
  border-radius: var(--radius);
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}

mark.cx-hl {
  background-color: transparent; /* the UA's own yellow would show before the sweep */
}

.cx-hl.is-lit {
  background-size: 100% 100%;
  color: var(--mark-ink);
  /* Nil in the light theme (paper does not glow); in the dark room the pigment carries its own
     halo, so the mark reads as lit rather than pasted onto the lacquer. */
  box-shadow: 0 0 26px -10px var(--mark-bloom);
}

/* A whole block lit (a spec cell, an assurance cell, a paragraph) when the quoted words are split
   across elements. Same stroke, swept across the box itself: no padding and no display of its own,
   because a bordered spec cell must keep the grid geometry it already has. */
.cx-hl--block {
  border-radius: var(--radius);
}

/* A marker covers everything under it — including the tracked label of a spec cell, which would
   otherwise stay smoke-grey on the pigment (3.4:1). The label keeps its rank at 72% ink: still
   secondary, still ≥ 4.5:1 on both pigments. */
.cx-hl--block.is-lit,
.cx-hl--block.is-lit > * {
  color: var(--mark-ink);
  transition: color var(--dur-slow) var(--ease);
}

.cx-hl--block.is-lit > dt,
.cx-hl--block.is-lit > .label {
  color: var(--mark-ink-soft);
}

@media (prefers-reduced-motion: reduce) {
  .cx-hl,
  .cx-hl--block.is-lit > * {
    transition: none;
  }
}
