/* ============================================================
   RentX (rentx-com) theme — Tailwind-first. This scoped file carries ONLY what utilities can't
   express: the theme's design tokens (per-site brand vars + layered-gradient / scrim constants), the
   font wiring, the base type/colour surface, two global interaction mechanisms (reduced-motion +
   focus ring), and the styling for the sanitised CMS-HTML blob (`.rx-prose`). Everything else —
   layout, spacing, typography, sizing, colour — lives as Tailwind utility classes in the components.

   Every rule stays scoped under `.rentx-com`, so it can never leak into the admin/chrome or another
   theme. Per-site colours (`--brand`/`--accent`) are injected inline on <body> by the render tier;
   the values below are ONLY preview fallbacks. The reference rentx.com is a monochrome-luxury look
   (near-black buttons on white), so the fallback echoes that — an ink brand with a warm-gold accent —
   but every accent reads from the per-site vars, so the theme recolours to any site's brand.
   Aggregator model: no prices; motion is CSS only.
   ============================================================ */

/* ── THE THEME'S TWO COLOUR ROLES ────────────────────────────────────────────────────────────────
   Every branded colour in this theme resolves through exactly TWO tokens — `--dark` and `--light` —
   and this block is the ONLY place either is defined. Nothing else in the theme references
   `--brand`/`--accent`/`--dark-color`/`--light-color` directly, so an admin changing a colour in the
   CMS repaints every page, band, button, hover, border and icon from these four lines.

     DARK  ([DARK_COLOR]  → `site.darkColor`  → `--dark-color`)  — dark surfaces + dark ink:
           the hero/footer/CTA bands, the fixed header bar, headings, filled buttons, focus rings,
           branded hover text, active map states, dark borders.
     LIGHT ([LIGHT_COLOR] → `site.lightColor` → `--light-color`) — the accent role:
           eyebrow ticks, check/pin/arrow icons, required-field marks, list bullets, badges, the
           accent-filled buttons on the dark bands, and the HOVER fill of every dark filled button.
           It is no longer the label on a dark fill — that is white now; see `--on-dark`.

   FALLBACK CHAIN, and why it is not optional: `site-tokens.ts` injects `--dark-color`/`--light-color`
   ONLY when the site actually sets them (they are optional CMS fields). A site that leaves them empty
   must therefore keep working, so each role falls back to the primary/secondary colour every site
   HAS — `[COLORCODE]`/`[SECONDARY_COLORCODE]` — and only then to the preview literals below. Never
   write `var(--dark-color)` bare anywhere in the theme: on the many sites that have not filled those
   fields in it resolves to nothing and the declaration is dropped.

   THE ONE CONTRACT THIS ASSUMES: DARK is dark and LIGHT is light. The theme pairs them
   (dark surface + light text, light surface + dark text) exactly as the field names promise, and no
   amount of CSS can rescue a site that puts a near-black in the Light field — `color-contrast()` is
   not shippable yet. The pairing is always one against the other, never dark-on-dark. */
.rentx-com {
  /* Preview-only fallbacks. The live render tier overrides --brand/--accent inline on <body>. */
  --brand: #17181c;
  --accent: #b8863b;

  --dark: var(--dark-color, var(--brand));
  --light: var(--light-color, var(--accent));

  /* The label that sits ON a dark fill (filled buttons, step discs, the footer badge). Kept as its
     own token rather than writing it at those seven call sites, so "what goes on top of dark" stays
     one decision — and this is that line.
     WHITE, not `var(--light)` (user direction, 2026-08-13). The accent label on a dark fill was the
     site's colour used as TYPE rather than as an accent, and it read as the weaker of the two: the
     "GET A QUOTE" button in the header bar and in every hero had a mid-tone label on near-black,
     where white is a flat 21:1 against any Dark a site can set — the accent, by contrast, is only as
     legible as whatever colour the CMS happens to hold, and a site with a mid-green or mid-gold in
     the Light field put its primary CTA under the 4.5:1 floor. The per-site colour is not lost from
     the button: it is the HOVER fill, where `BTN_SOLID` swaps the two outright, so the accent now
     arrives as the response instead of sitting in the resting label. */
  --on-dark: #ffffff;

  /* The theme's dark ink — the band surface AND the heading colour, which are the same role at
     opposite ends (a dark section, and dark type on white). It USED to be a fixed #131317; it now
     follows the site's Dark colour, which is what makes every ink band on every page recolour. The
     literal survives only as the last fallback above. */
  --ink: var(--dark);

  /* Theme-owned NEUTRALS — deliberately NOT per-site, and deliberately not remapped to --dark/--light.
     These are the paper/grey surface the design sits on, not brand colours: binding body copy or a
     hairline to an arbitrary CMS colour is how a page loses its contrast floor. `--muted` in
     particular carries a MEASURED 4.83:1 ratio (see below) that a per-site value would destroy. */
  --body: #55565f;
  /* WCAG AA fix (measured): #8a8b95 is only 3.36:1 on the paper surface, so every "muted" label,
     helper line and small link failed the 4.5:1 body-text ratio — Lighthouse flagged it on the Services
     page. #70717b is 4.83:1, still visibly secondary next to --body. Every use of this token in the
     theme is on white or --sand, so darkening it can only improve contrast. */
  --muted: #70717b;
  --paper: #ffffff;
  --sand: #f7f7f5;
  --line: #e9e9e6;
  --line-strong: #dededb;

  /* Radius scale — kept tight and cohesive with the reference's crisp corners (cards a touch less
     round than before, search ~10px), so the whole page reads as one system. */
  --radius: 1rem;
  --radius-sm: 0.625rem;
  /* EVERY RECTANGULAR BUTTON IN THE THEME, on every page (user direction, 2026-08-13). One token
     rather than a literal at each call site, because the nine button-shaped controls had drifted to
     FOUR different radii — 10px (`BTN`, `BTN_ICON`), 8px (`BTN_BAR`, the contact submit, the header's
     phone pill / menu box / skip link, the coverage View control) and 7px (the map card button) — so
     buttons that sit next to each other were visibly different shapes. Anything reaching for a button
     radius now reads this, and a future change is one line.
     DELIBERATELY NOT APPLIED to the shapes that are round BY DESIGN and are not rectangles at all: the
     slider's circular arrows, the drawer's close disc, the how-it-works step numerals, the coverage
     accordion discs and the pill-shaped filter chips. Squaring those to 6px would not be "one radius",
     it would be a different design. */
  --radius-btn: 6px;

  /* Full-bleed slider alignment. `--rx-gutter` is the page's edge padding; `--rx-inset` is the extra a
     FULL-BLEED track needs so its first card lines up with the WRAP-contained heading above it (WRAP is
     max-w 1200px, centred). Tokens rather than the values inline on the element: the README bans inline
     styles, and the arbitrary-value spelling of that `max(…, calc(…))` is unreadable in a class list.
     `100%` is substituted, not resolved, here — it resolves against whatever element USES the token, so
     these must only be applied to the full-width track itself (`FleetSlider`), never a contained box. */
  --rx-gutter: clamp(20px, 5vw, 44px);
  --rx-inset: max(var(--rx-gutter), calc((100% - 1200px) / 2 + var(--rx-gutter)));

  /* Hero legibility wash — for the CENTRED hero: a radial focus that keeps the middle headline zone
     readable over a bright video frame, plus a vertical scrim that darkens the top (transparent-header
     legibility) and the bottom (search bar + rating strip). Applied via `[background:var(--hero-veil)]`. */
  --hero-veil: radial-gradient(120% 82% at 50% 46%, rgba(12, 12, 15, 0.28) 0%, rgba(12, 12, 15, 0.58) 100%),
    linear-gradient(180deg, rgba(12, 12, 15, 0.62) 0%, rgba(12, 12, 15, 0.36) 42%, rgba(12, 12, 15, 0.72) 100%);

  /* Card image scrim — a soft top highlight + bottom darken so the label stays readable on any photo.
     Applied via `after:[background:var(--card-scrim)]`. */
  --card-scrim: linear-gradient(180deg, rgba(12, 12, 15, 0) 38%, rgba(12, 12, 15, 0.72) 100%);

  background: var(--paper);
  color: var(--body);
  font-family: var(--font-rentx-com), ui-sans-serif, system-ui, -apple-system, "Segoe UI",
    "Helvetica Neue", Arial, sans-serif;
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Pure-CSS interaction mechanisms (no utility variant expresses these globally). */
@media (prefers-reduced-motion: reduce) {
  .rentx-com *,
  .rentx-com *::before,
  .rentx-com *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}
.rentx-com :focus-visible {
  outline: 2px solid var(--dark);
  outline-offset: 3px;
  border-radius: 6px;
}

/* Hero search bar — the ring moves from the FIELD to the BAR.
   The bar is one control made of three parts (icon, text field, Search button), and the field has no
   border of its own, so the global ring above drew a stray black rectangle floating inside the white
   card and butting into the Search button — visible the whole time someone types, since a text input
   matches `:focus-visible` on click too. The input's own outline is suppressed and the form takes the
   ring instead, WHITE and hugging the outside of its own rounded edge (user direction, 2026-08-05).
   Colour and offset go together: the ring was `var(--brand)` inset at -2px, which put a dark rule
   INSIDE the white card and read as a black border drawn across the bar; white at offset 0 sits on
   the dark hero behind it instead, where white is the highest-contrast thing available, and reads as
   the card's own edge lighting up. It follows the bar's live radius, so with the suggestion list open
   it squares off at the bottom with it — and the ring's bottom run falls at the bar/panel seam, white
   on the white panel, so the two stay one card. Focus stays visible for keyboard users (WCAG 2.4.7);
   it just lands on the box a visitor thinks they are in. Only the input is remapped: the Search button
   and the suggestion rows keep the normal ring, so focus is never ambiguous.

   The suggestion panel takes the SAME ring. An outline follows the border box of the element it is
   on, and the panel is absolutely positioned, so the form's box is the bar alone: the ring's vertical
   runs stopped at the seam and left a 2px white shoulder poking out past the panel on both sides —
   a step at the exact place the bar and list are meant to read as one card, and the list only ever
   opens while the input is focused, so the step was there every time anyone saw the list. Ringing the
   panel too puts both edges on the same line and closes the ring around the whole card. */
.rentx-com .rx-searchbar input:focus-visible {
  outline: none;
}
.rentx-com .rx-searchbar:has(input:focus-visible),
.rentx-com .rx-searchbar:has(input:focus-visible) .rx-suggest {
  outline: 2px solid #fff;
  outline-offset: 0;
}

/* Suggestion-list scrollbar. Windows/Linux Chromium paints a full-width classic bar, which lands hard
   against the panel's rounded bottom-right corner and crowds the SELECT badges. Thin, with a
   translucent ink thumb on no track, keeps the "there is more below" affordance without the chrome.
   Both spellings: Chromium honours `scrollbar-width`/`scrollbar-color`, older WebKit the pseudos. */
.rentx-com .rx-scroll-slim {
  scrollbar-width: thin;
  scrollbar-color: rgba(19, 19, 23, 0.28) transparent;
}
.rentx-com .rx-scroll-slim::-webkit-scrollbar {
  width: 8px;
}
.rentx-com .rx-scroll-slim::-webkit-scrollbar-track {
  background: transparent;
}
.rentx-com .rx-scroll-slim::-webkit-scrollbar-thumb {
  background: rgba(19, 19, 23, 0.28);
  background-clip: content-box;
  border: 2px solid transparent;
  border-radius: 999px;
}

/* ── Cross-document page transitions (the "SPA feel" WITHOUT shipping a client router) ──
   Every theme nav link is a plain <a>, so a click is a real document navigation. `@view-transition`
   makes the browser snapshot the old page, paint the new one, and cross-fade between them, so the
   navigation reads as a smooth transition instead of a white flash — zero JS, zero hydration, and it
   degrades to today's instant swap in browsers without support (Safari/Firefox).

   ⚠️ SCOPE EXCEPTION (README "everything stays scoped under .rentx-com"): `@view-transition` and the
   `::view-transition-*` pseudo-elements live on the DOCUMENT, not on an element, so they CANNOT be
   nested under the theme class. They are confined in practice because this stylesheet is imported by
   this theme's pages only — no other theme's pages load it. Do not move these rules to globals.css:
   there they would change navigation behaviour for all 16 themes at once. */
@view-transition {
  navigation: auto;
}
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root) {
    animation: rx-vt-out 0.22s cubic-bezier(0.4, 0, 1, 1) both;
  }
  ::view-transition-new(root) {
    animation: rx-vt-in 0.32s cubic-bezier(0, 0, 0.2, 1) both;
  }
}
@keyframes rx-vt-out {
  to {
    opacity: 0;
    transform: translateY(-8px);
  }
}
@keyframes rx-vt-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
}
/* The header bar and the footer are byte-identical on every page, so give them their own transition
   names: the browser then morphs them in place instead of fading them with the page body — the chrome
   visually PERSISTS across the navigation, which is the part that reads as "only the content changed".

   ⚠️ The name goes on `.rx-headerbar` (the inner bar row), NEVER on `.rx-header` itself. A
   `view-transition-name` makes an element a containing block for its FIXED descendants — and the nav
   drawer + scrim live inside `.rx-header` as `position: fixed`. Named on the header, the drawer stopped
   resolving against the viewport, its off-canvas `translate-x-full` became real document overflow, and
   every page picked up ~2000px of horizontal scroll at every width (measured at 320–1920px). The bar row
   has no fixed descendants, so it is safe — and since both pages' bars are identical, the visual result
   is the same. */
.rentx-com .rx-headerbar {
  view-transition-name: rx-header;
}
.rentx-com .rx-footer {
  view-transition-name: rx-footer;
}
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-group(rx-header),
  ::view-transition-group(rx-footer) {
    animation-duration: 0.2s;
  }
}

/* Smooth in-page scrolling for the anchor links (nav → section ids, "back to top", accordion targets).
   `:has()` keeps a ROOT-level property theme-scoped: it only applies to documents that actually render
   this theme. Wheel/trackpad scrolling is untouched — this is only the animated jump behaviour. */
@media (prefers-reduced-motion: no-preference) {
  :root:has(.rentx-com) {
    scroll-behavior: smooth;
  }
}

/* ── Section reveal on scroll (scroll-driven, so there is NO JS observer and no main-thread work) ──
   Every band below the hero rises as it enters the viewport. Three deliberate constraints:
   · TRANSFORM ONLY — no opacity. MEASURED: a fade (even from 0.55, never 0) drops the effective text
     contrast of whatever is mid-animation below 4.5:1, and Lighthouse/axe compute contrast WITH the
     animated opacity — a first attempt at this took accessibility from 100 to 96 on three pages. A
     translate never changes contrast, and nothing is ever invisible in a non-scrolling screenshot
     either (the README's "never hidden at opacity: 0" rule), so this is the only safe reveal here.
   · gated on `@supports (animation-timeline: view())`, so browsers without scroll-driven animation
     get the plain, final layout rather than a broken one;
   · gated on reduced-motion. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* What moves is the band's CONTENT, not the band. Animating the <section> itself dragged its
       full-bleed background down with it: mid-reveal a band sat up to 18px below the one above it and
       the page's white paper showed through as a stray full-width strip (measured 14.8px between the
       client wall and the sand band below it on the home page; the same gap opens white-on-ink above
       the CTA band), and the last band on every page overlapped the footer by the same 18px. With the
       section anchored, backgrounds, borders and the `rx-px` parallax frames all stay put and the
       bands stay flush at every scroll position — the rise is unchanged.
       The exclusions are the bands' own backdrop layers, which must NOT move independently of the box
       that clips them: `img`/`video` (the filled `<ThemeImage>` media and the hero loop) and the
       `.absolute` scrim/gradient overlays. Matching `img`/`video` by TAG is what makes that robust —
       the media used to be positioned by `next/image fill`'s inline styles with no class to match on,
       and is now positioned by `absolute inset-0 h-full w-full` utilities, so the `.absolute` clause
       would catch it too; the tag selectors mean this rule never depended on which of the two it was.
       `script`
       (JSON-LD) has no box to move. Everything else in a band is in-flow content. */
    .rentx-com main > section:nth-child(n + 2) > :not(img, video, script, .absolute) {
      animation: rx-in 1s linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 65%;
    }
    /* …except inside the full-bleed media bands, which clip with `overflow: hidden` and are therefore
       SCROLL CONTAINERS themselves: an anonymous `view()` on their content would measure it against
       the band it already sits in, where it never enters or leaves, and freeze at 0% — leaving that
       content parked 18px low forever. (Same trap the parallax block below documents.) Those bands
       already publish a named timeline of themselves for their media; the content rides that one. */
    .rentx-com main > section.rx-px:nth-child(n + 2) > :not(img, video, script, .absolute) {
      animation-timeline: --rx-px;
    }
    /* Stagger for card grids/rows: each child picks up a slightly later slice of its own view()
       range, so a row of cards arrives in sequence rather than as one block. */
    .rentx-com .rx-stagger > * {
      animation: rx-in 1s linear both;
      animation-timeline: view();
    }
    .rentx-com .rx-stagger > :nth-child(1) {
      animation-range: entry 0% entry 55%;
    }
    .rentx-com .rx-stagger > :nth-child(2) {
      animation-range: entry 6% entry 62%;
    }
    .rentx-com .rx-stagger > :nth-child(3) {
      animation-range: entry 12% entry 68%;
    }
    .rentx-com .rx-stagger > :nth-child(n + 4) {
      animation-range: entry 18% entry 74%;
    }
  }
}
@keyframes rx-in {
  from {
    transform: translateY(18px);
  }
  to {
    transform: none;
  }
}

/* ── Media parallax (scroll-driven, so again NO JS observer and no main-thread work) ──
   The section reveal above moves a band as a whole; this gives the band DEPTH while it scrolls. An
   `.rx-px` frame (a clipped, aspect-locked box) declares a view timeline, and its `.rx-px-media` photo
   drifts vertically against it, so the image travels slower than the page it sits on.

   · A NAMED timeline, not a bare `view()`. Every frame here clips with `overflow: hidden` — which makes
     it a SCROLL CONTAINER — so an anonymous `view()` resolved on the media would measure the media
     against its own frame, where it never enters or leaves, and sit frozen at 0% forever. Declared on
     the FRAME, the timeline measures the frame against the page scrollport, which is the motion wanted.
   · TRANSFORM-CLASS ONLY, never opacity — same reason as `rx-in`: an animated opacity is what
     Lighthouse/axe compute contrast against, and nothing may be invisible in a still screenshot.
   · Overscan, so the drift can never expose an edge: the media is sized to 118% of the frame and
     travels ±6% of its OWN height — 7.08% of the frame, inside the 9% margin. MEASURED: without the
     overscan the drift pulled a blank strip of up to 33px into frame on every band; at 116% the
     worst-case margin measured a thin 3.8px, so it is 118% to keep sub-pixel rounding off the edge.
   · The overscan keeps `!important`; the drift does NOT. The competing `inset: 0` / `height: 100%`
     now arrive as the Tailwind utilities `inset-0 h-full` on the `<ThemeImage>` itself (they used to
     be `next/image fill`'s INLINE styles, which no stylesheet rule can outrank without `!important` —
     that is what these two declarations were originally written to beat). The `!important` is kept
     deliberately rather than re-tuned to the new specificity: it makes the two static declarations win
     outright no matter how the utilities are layered, which is the property this needs. `translate`
     still lands unaided because a CSS animation outranks both. Keeping height/inset OUT of the
     keyframe is deliberate: a constant box means the animation stays purely compositor-driven, with no
     per-frame layout. (`bottom` is left at 0 — with top + height + bottom all set the box is
     over-constrained and `bottom` is the one dropped, which is exactly the resolution wanted here.)
   · Gated on `@supports` + reduced-motion. Without either, the media keeps the plain `inset: 0` box
     from its utilities and simply doesn't move — the full photo, correctly framed, never a broken one. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .rentx-com .rx-px {
      view-timeline-name: --rx-px;
      view-timeline-axis: block;
    }
    .rentx-com .rx-px-media {
      top: -9% !important;
      height: 118% !important;
      animation: rx-px linear both;
      animation-timeline: --rx-px;
      animation-range: cover 0% cover 100%;
    }
  }
}
@keyframes rx-px {
  /* The `translate` property, NOT `transform`: several framed photos also carry a Tailwind `scale-*`
     hover, and v4 compiles that to the separate `scale` property. Driving the drift through `translate`
     lets the two compose — animate `transform` here instead and the running animation would win over
     the hover, killing the zoom. */
  from {
    translate: 0 -6%;
  }
  to {
    translate: 0 6%;
  }
}

/* Fixed overlay header (rentx.com pattern): transparent over the dark hero at the top of the page,
   solidifying to the ink bar as you scroll — the logo/controls are white in BOTH states, so no swap.
   FALLBACK (no scroll-driven-animation support, e.g. Safari): a solid ink bar always — still legible
   over every section, just without the transparent-at-top flourish. Zero JS.

   The solid state is the site's DARK colour, in all three places below (base, keyframe, reduced-motion
   override) — they must stay in step or the bar changes colour when it settles. It was a fixed #0b0b0e,
   which was the one dark surface in the theme that did NOT recolour: on a site whose Dark colour is not
   near-black the header sat as a stray black slab above its own hero. The `var()` resolves on the bar
   itself, inside the theme scope, so it works in the keyframe exactly as it does in the base rule. */
.rentx-com .rx-header {
  background-color: var(--dark);
}
@supports (animation-timeline: scroll()) {
  .rentx-com .rx-header {
    background-color: transparent;
    animation: rx-header-solidify linear both;
    animation-timeline: scroll(root);
    animation-range: 20px 140px;
  }
  @keyframes rx-header-solidify {
    to {
      background-color: var(--dark);
      box-shadow: 0 1px 0 rgba(255, 255, 255, 0.08);
    }
  }
}
@media (prefers-reduced-motion: reduce) {
  .rentx-com .rx-header {
    animation: none !important;
    background-color: var(--dark) !important;
  }
}

/* Anchor targets sit under the fixed header — offset their scroll landing so headings aren't hidden. */
.rentx-com [id] {
  scroll-margin-top: 90px;
}

/* ── Drawer open → lock the page behind it (zero JS) ──
   The nav drawer is `position: fixed` and full-height, but that alone doesn't stop the DOCUMENT from
   scrolling underneath it: measured on a 390×844 phone, the page behind travelled 600px with the menu
   open, so a flick on the scrim (or past the end of the drawer's own scroll) left the visitor somewhere
   else entirely the moment they closed it. Keyed off the drawer's own checkbox, so it costs nothing
   while the menu is shut.

   `:root`, not `body`: the scroll port here is the document element (the header's solidify animation
   rides `scroll(root)`), and `:has()` keeps this root-level property theme-scoped — the rule only
   matches a document that actually renders this theme. The scope class is on <body>, so the checkbox is
   a descendant of `.rentx-com`, not of `:root` — hence the descendant combinator.

   `scrollbar-gutter: stable` is the other half, for desktop: without it, hiding the overflow removes
   the classic scrollbar and the whole page (plus the fixed bar) jumps ~15px right as the drawer opens.
   Reserving the gutter on every page of the theme keeps that width constant in both states; it's a
   no-op with the overlay scrollbars phones use. */
:root:has(.rentx-com) {
  scrollbar-gutter: stable;
}
:root:has(.rentx-com #rx-nav:checked) {
  overflow: hidden;
}

/* Horizontal card sliders (Vehicles/Services) — scrollbar hidden for the clean reference look; the
   arrow buttons + autoplay are the affordance (native touch/trackpad/keyboard scroll still works). */
.rentx-com .rx-hscroll {
  scrollbar-width: none; /* Firefox */
}
.rentx-com .rx-hscroll::-webkit-scrollbar {
  display: none; /* WebKit/Chromium */
}

/* Continuous, pure-CSS marquee drift — the coverage rail and the "Trusted by …" client-logo strip
   (BrandWall.tsx). Both render their list twice and translate -50% for a seamless loop; decorative,
   halts under prefers-reduced-motion via the block above. */
@keyframes rx-marquee {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-50%);
  }
}

/* Accordion open/close (Faq.tsx, CoverageAccordion.tsx) — a native <details> can't transition its own
   box, so the panel is eased through `::details-content`: `interpolate-size: allow-keywords` makes the
   `0 → auto` block-size interpolable, and `transition-behavior: allow-discrete` lets the discrete
   `content-visibility` flip ride along — so the panel glides BOTH open AND closed, with the body fading
   in behind it. Zero JS, no measured heights.

   Browsers without `::details-content` fall back to the reveal keyframe: the opened panel still fades
   + slides in, it just snaps shut — a graceful degrade, never a broken panel. Both paths are stilled
   under prefers-reduced-motion.

   ⚠️ CORRECTION, 2026-08-13 (measured in real WebKit, Safari 26.5 engine, via Playwright): this note
   used to name "Safari/Firefox" as the browsers taking that fallback. SAFARI NO LONGER DOES — it ships
   `::details-content`, so it takes the branch below, not the `@supports not` one. What Safari does NOT
   ship is `interpolate-size: allow-keywords`, so `block-size: 0 → auto` is not interpolable there and
   the panel SNAPS open instead of gliding. Verified end-to-end on all three disclosures the theme has
   (the FAQ, the coverage accordion and the drawer's "Near {City}" group): every one opens, reaches full
   height and reveals its last child in WebKit — the degrade is the easing, never the disclosure. Don't
   "fix" it by moving these rules out of the `@supports`: that would take the animation away from Chrome
   too, for a browser that is already functional. */
@supports selector(::details-content) {
  .rentx-com details.rx-acc {
    interpolate-size: allow-keywords;
  }
  .rentx-com details.rx-acc::details-content {
    block-size: 0;
    overflow: clip;
    opacity: 0;
    /* `allow-discrete` MUST ride inside the shorthand: as a standalone `transition-behavior` the CSS
       minifier hoists it above `transition`, which then resets it to `normal` — and without it the
       closed panel stays `content-visibility: hidden`, so there is no box to ease and it snaps. */
    transition: block-size 0.34s cubic-bezier(0.22, 0.61, 0.36, 1),
      content-visibility 0.34s allow-discrete, opacity 0.26s ease;
  }
  .rentx-com details.rx-acc[open]::details-content {
    block-size: auto;
    opacity: 1;
  }
  /* ARRIVING FROM AN ANCHOR opens the row. The states grid links each tile at its own row
     (`/coverage#alaska` — see `CoverageStates`), and landing on a COLLAPSED row would answer "do you
     cover Alaska?" with a closed box: the visitor would have to find and click View to see anything.
     `:target` opens it on arrival with no JS.

     `content-visibility` has to be set explicitly here, unlike in the `[open]` rule above. The UA
     keeps `::details-content` at `content-visibility: hidden` for as long as the element has no
     `open` attribute — and `:target` does not add one — so without this the panel has a height and
     an opacity but still renders nothing. */
  .rentx-com details.rx-acc:target::details-content {
    block-size: auto;
    opacity: 1;
    content-visibility: visible;
  }
  /* The global reduced-motion reset above can't reach a pseudo-element — repeat it here. */
  @media (prefers-reduced-motion: reduce) {
    .rentx-com details.rx-acc::details-content {
      transition-duration: 0.001ms !important;
    }
  }
}
@supports not selector(::details-content) {
  .rentx-com details.rx-acc[open] > :not(summary),
  .rentx-com details[open] > .rx-reveal {
    animation: rx-reveal 0.28s ease both;
  }
}
@keyframes rx-reveal {
  from {
    opacity: 0;
    transform: translateY(-6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* ── Sanitised CMS-HTML (`page.content`) — utilities can't target editor-written markup, so the
   theme's type styles are reproduced here as scoped descendants. Applied on the `.rx-prose`
   container in About. ── */
/* ── CMS-BODY TYPE SCALE (raised 2026-08-12, user direction) ───────────────────────────────────
   READ THE FLOORS, NOT THE CAPS. Every size here is a clamp, and the `vw` term only overtakes the
   floor above ~1200px — so the floor IS the size at every laptop and tablet width. A first pass at
   this set `h3` to `clamp(1.25rem, 1.7vw, 1.5rem)`: a 24px cap that looked like a real increase and
   a 20px floor that was 0.8px above the flat 1.2rem it replaced, i.e. no visible change at all on
   the window it was reported from. Raise the FLOOR when the ask is "make it bigger".

   The floors are also what keeps the hierarchy from inverting: below ~1200px every level sits at its
   own floor, so those four numbers must already descend on their own (27.2 → 22.4 → 18.4 → 17.6px).
   That is why `h3` could not be raised alone — `h2`'s floor was 21.6px and `h3` would have overtaken
   it on exactly the widths the change was for. ── */
.rentx-com .rx-prose h2 {
  margin: 1.7rem 0 0.65rem;
  font-size: clamp(1.7rem, 2.6vw, 2.1rem);
  font-weight: 700;
  line-height: 1.18;
  letter-spacing: -0.02em;
  color: var(--ink);
}
/* Scaled up 2026-08-12 (user direction). `h3` was a flat 1.2rem — 19.2px against 16px body copy, a
   3px step that did not read as a heading at all. It matters more than the number suggests because
   the editors' bodies routinely START at h3 (the area pages' "Limo Rental in Adjuntas" and its
   siblings use h3/h4 only, no h2), so on those pages 19.2px was the LARGEST type in the whole
   section. Now fluid, like `h2` above, so the step holds at every width. */
.rentx-com .rx-prose h3 {
  margin: 1.5rem 0 0.55rem;
  font-size: clamp(1.4rem, 1.9vw, 1.7rem);
  font-weight: 600;
  line-height: 1.28;
  letter-spacing: -0.01em;
  color: var(--ink);
}
/* Body copy lifted off the 16px browser default to 1.06rem, and the leading opened to 1.75 with it —
   at the measure these CMS bodies run to, 16px/1.7 is the low end of comfortable. Same size the
   Coverage map's lede was raised to earlier today, so the two agree. */
.rentx-com .rx-prose p {
  margin: 0 0 1.1rem;
  font-size: 1.1rem;
  color: var(--body);
  line-height: 1.75;
}
.rentx-com .rx-prose a {
  color: var(--dark);
  font-weight: 600;
  text-underline-offset: 3px;
}
.rentx-com .rx-prose a:hover {
  text-decoration: underline;
}
.rentx-com .rx-prose ul {
  margin: 0 0 1rem;
  padding-left: 1.15rem;
  list-style: disc;
}
/* `li` tracks `p` — these bodies mix paragraphs and bullets in one run, and a list left at the 16px
   default reads a size smaller than the prose around it. */
.rentx-com .rx-prose li {
  margin: 0.3rem 0;
  font-size: 1.1rem;
  line-height: 1.7;
  color: var(--body);
}

/* ── Tags the LEGAL documents use and no other CMS body in this theme does (added 2026-08-12 with
   `pages/Legal.tsx`). Without these the global reset strips them: an `<ol>` lost its numbers, so the
   CCPA policy's nine numbered consumer rights rendered as an unmarked stack of sentences, and `<h4>`
   (the heading level the terms documents use most) came out at body size and weight. ── */
.rentx-com .rx-prose ol {
  margin: 0 0 1rem;
  padding-left: 1.35rem;
  list-style: decimal;
}
.rentx-com .rx-prose :is(ul, ol) :is(ul, ol) {
  margin: 0.35rem 0 0.5rem;
}
/* Nested bullets take the second marker, as a UA would — a policy's sub-clauses are routinely two
   deep and identical markers make the levels indistinguishable. */
.rentx-com .rx-prose ul ul {
  list-style: circle;
}
.rentx-com .rx-prose ol ol {
  list-style: lower-alpha;
}
/* `h4` sits BELOW `h3` in the same ramp. Note `Legal.tsx` demotes a document's headings by one on the
   way in (its own `<h1>` would be a second H1 under the banner's), so a document's `<h3>` arrives here
   as `<h4>` — which is why this level has to be styled at all. */
.rentx-com .rx-prose h4 {
  margin: 1.3rem 0 0.45rem;
  font-size: clamp(1.15rem, 1.4vw, 1.3rem);
  font-weight: 600;
  line-height: 1.35;
  color: var(--ink);
}
.rentx-com .rx-prose blockquote {
  margin: 1.2rem 0;
  padding: 0.1rem 0 0.1rem 1rem;
  border-left: 3px solid var(--line-strong);
  color: var(--body);
}
.rentx-com .rx-prose hr {
  margin: 2rem 0;
  border: 0;
  border-top: 1px solid var(--line);
}
/* The legal HTML leans on inline emphasis for defined terms and "Last updated" lines; the reset
   flattens both to normal weight/style. */
.rentx-com .rx-prose strong {
  font-weight: 600;
  color: var(--ink);
}
.rentx-com .rx-prose em {
  font-style: italic;
}

/* Editor-written TABLES. There were no rules for these at all, so a `<table>` in `page.content` fell
   back to the UA default on top of the global reset: no padding, no rules, headers centred and the
   columns collapsed onto each other — the comparison tables editors write for category/area bodies
   ("Vehicle | Typical capacity | Best for") were unreadable. Same reasoning as every rule above: a
   utility class can't reach markup the editor wrote, so the theme's table style lives here.

   ONE style for all of them, on `.rx-prose` rather than per page, so the category page, the area
   pages and every other CMS body (Services, Vehicles, Coverage, About) render a table identically. */
/* The scroll box `wrapTables()` puts around every editor-written table (see that function for the
   measured bug: a ~450px min-content table in a CMS body scrolled the WHOLE page sideways by 110px at
   360px wide). `clip` is not usable here — the point is that the table must stay reachable — so this is
   the one place in the theme that opts into a real horizontal scroller.

   `overscroll-behavior-x: contain` stops a swipe that reaches the end of the table from chaining into
   the page/browser back-gesture. NOT IN SAFARI — measured in WebKit 2336 (Safari 26.5): it supports
   neither the shorthand nor the axis longhands, so on iOS/macOS Safari a swipe past the end of the
   table still chains to the page. Kept anyway: it is a progressive enhancement that costs nothing
   where it is unsupported, and the table itself scrolls correctly in both engines. The header drawer's
   `overscroll-contain` has exactly the same caveat.

   The focus ring is the theme's own, because `tabindex="0"` makes this box a tab stop and an invisible
   one would be worse than no tab stop at all. */
.rentx-com .rx-table-scroll {
  max-width: 100%;
  /* The table's own `margin: 1.6rem 0` moves out here, so the gap above and below is unchanged from
     before the wrapper existed — a margin left on the table would sit INSIDE the scroll box and both
     double the spacing and become scrollable dead space. */
  margin: 1.6rem 0;
  overflow-x: auto;
  overscroll-behavior-x: contain;
}
.rentx-com .rx-table-scroll:focus-visible {
  outline: 2px solid var(--light);
  outline-offset: 3px;
}
/* The wrapper is now what separates the table from the copy around it; leaving the margin on the table
   as well would double the gap. */
.rentx-com .rx-table-scroll > table {
  margin: 0;
}
.rentx-com .rx-prose table {
  width: 100%;
  margin: 1.6rem 0;
  border: 1px solid var(--line);
  border-collapse: collapse;
  font-size: 0.95rem;
  line-height: 1.6;
}
/* `caption` is what an editor's "Common airport transfer options…" line becomes. The UA centres it;
   left-aligning it makes it read as the table's label rather than a stray centred sentence. */
.rentx-com .rx-prose caption {
  margin-bottom: 0.5rem;
  color: var(--ink);
  font-weight: 600;
  text-align: left;
}
.rentx-com .rx-prose :is(th, td) {
  padding: 0.68rem 0.9rem;
  border-bottom: 1px solid var(--line);
  /* Cells hold sentences of different lengths, so top-align them — centred cells leave a one-word
     column floating against a three-line one. */
  vertical-align: top;
  text-align: left;
}
.rentx-com .rx-prose th {
  background: var(--sand);
  color: var(--ink);
  font-weight: 600;
}
.rentx-com .rx-prose td {
  color: var(--body);
}
/* Zebra striping on the BODY only, and from `--sand` at a fraction of its strength, so the banding
   reads on white without competing with the header row's full `--sand`. */
.rentx-com .rx-prose tbody tr:nth-child(even) td {
  background: color-mix(in srgb, var(--sand) 55%, white);
}

/* NARROW SCREENS: the table stays a TABLE and reflows. It buys back the width it needs from padding
   and type size rather than from the columns, which is the whole trick — at 390px the default
   0.9rem side padding was costing ~14% of the row.

   DELIBERATELY NOT the usual `display: block; overflow-x: auto` swipe-strip, though it was built and
   measured first (it does scroll, and the page never overflows). `display: block` on a `<table>`
   drops the table role from the accessibility tree in every engine — the rows and cells stop being
   rows and cells, and a screen-reader user loses the column headers that are the only thing making a
   comparison table readable. The fix for that is `role="table"`/`role="row"`/`role="cell"` on the
   markup, and this markup is written by an editor in the CMS: CSS cannot put the roles back. A table
   that wraps is worse-looking than one that scrolls; a table with no headers is worse, full stop. */
@media (width < 40rem) {
  .rentx-com .rx-prose table {
    font-size: 0.875rem;
  }
  .rentx-com .rx-prose :is(th, td) {
    padding: 0.55rem 0.5rem;
  }
}
