/* ============================================================
   THEME.CSS — the public site's design system
   ============================================================

   A port of the Skylight Buslines Apple theme
   (skylight-web/frontend/design/tokens.mjs + app/globals.css) into plain CSS
   custom properties and classes. No Tailwind, no build step, no runtime.

   SCOPE. Everything that changes SIZE or RHYTHM is scoped to `.public-main`,
   the anonymous public shell. The dealer portal and the tuner panel share
   base.html and portal.css, and they are a dense working interface that must
   NOT inherit a 17px marketing type scale. Tokens are declared on :root because
   they are inert until something reads them; the scoped rules are where they
   take effect.

   RELATIONSHIP TO THE EXISTING CSS.
   * portal.css owns the colour tokens (admin-editable, injected per request
     from SiteSettings in base.html) and both shells' chrome. This file never
     redefines a colour token — it reads them, so changing the accent in admin
     still works.
   * landing.css owns the mature .lp-* components used by ~40 guide pages. This
     file re-tunes the shared primitives underneath them rather than forking a
     second system; anything named .lp-* stays where it is.
   * What is genuinely NEW here is the part protunes never had: photography.
     .media-* is the whole reason the site can look like the reference.

   WHY THE PUBLIC TYPE SCALE IS BIGGER. portal.css sets body to 14px site-wide.
   That is the right density for a dashboard and the wrong density for a page
   trying to sell something — browsers default to 16px and Apple's own marketing
   runs 17-21px. Scoping the larger scale to .public-main is what lets both be
   true at once.
   ============================================================ */

:root {
  /* ── Radii. Skylight's base is 1rem with 2rem for photography; portal.css
     ships 10px, which reads as "app chrome" rather than "marketing". Both
     coexist: --radius stays for the portal, these are the public ones. ── */
  --r-sm: 0.5rem;
  --r-md: 0.75rem;
  --r-lg: 1rem;
  --r-xl: 1.5rem;
  --r-2xl: 2rem;

  /* ── Elevation. Three named shadows, deliberately few. ── */
  --sh-soft: 0 2px 8px -2px rgba(0, 0, 0, .06), 0 4px 24px -4px rgba(0, 0, 0, .06);
  --sh-lift: 0 8px 30px -6px rgba(0, 0, 0, .12);
  --sh-glow: 0 10px 40px -8px rgba(var(--color-accent-rgb), .35);

  /* ── Motion. ONE easing curve, used by every reveal, zoom and hover. ── */
  --ease-out-soft: cubic-bezier(0.22, 1, 0.36, 1);
  --dur-reveal: .7s;

  /* ── Section rhythm. ── */
  --sec-y: clamp(3.5rem, 7vw, 7rem);
  --measure: 1120px;
}

/* ============================================================
   TYPE — public shell only
   ============================================================ */

.public-main {
  font-size: 17px;
  line-height: 1.55;
  color: var(--color-text);
  /* One measure across the whole public site. The nav and footer are already
     centred at 1120px; without this the store spanned the full window above a
     centred footer. Heroes opt out with .full-bleed, which already exists. */
}

.public-main :where(p, li) { text-wrap: pretty; }

.public-main :where(h1, h2, h3, h4) {
  color: var(--color-text);
  font-weight: 600;
  letter-spacing: -0.018em;
  text-wrap: balance;
}

/* The display scale. Each size carries its own line-height and tracking, the
   way the reference does — a heading is a single decision, not three. */
.t-display-xl {
  font-size: clamp(2.75rem, 6vw, 4.5rem);
  line-height: 1.05; letter-spacing: -0.022em; font-weight: 700;
}
.t-display-lg {
  font-size: clamp(2.25rem, 4.5vw, 3.5rem);
  line-height: 1.08; letter-spacing: -0.022em; font-weight: 700;
}
.t-display-md {
  font-size: clamp(1.75rem, 3vw, 2.5rem);
  line-height: 1.15; letter-spacing: -0.018em; font-weight: 600;
}
.t-display-sm {
  font-size: clamp(1.4rem, 2vw, 1.875rem);
  line-height: 1.2; letter-spacing: -0.014em; font-weight: 600;
}
.t-lead { font-size: clamp(1.0625rem, 1.4vw, 1.3125rem); line-height: 1.55; color: var(--color-muted); }
.t-muted { color: var(--color-muted); }

/* The accent word inside a display heading — the reference's "Where the *sky*
   is the limit". One word, never a whole clause. */
.t-accent { color: var(--color-accent); }

/* ============================================================
   SECTIONS
   ============================================================ */

.sec { padding-block: var(--sec-y); }
.sec--tint { background: var(--color-surface-hover, #f5f5f7); }
.sec--flush { padding-block-start: 0; }

/* Side padding is NOT optional here. .wrap is used inside .full-bleed sections,
   which cancel the public shell's 20px gutter (--page-gutter on .public-main) so
   the background can span the window — and without its own padding the COPY
   spans it too, touching the edge on a phone. Matching the shell's gutter keeps
   one measure whether a section bleeds or not. */
.wrap {
  max-width: calc(var(--measure) + 2 * var(--page-gutter, 20px));
  margin-inline: auto;
  width: 100%;
  padding-inline: var(--page-gutter, 20px);
}
.wrap--narrow { max-width: calc(760px + 2 * var(--page-gutter, 20px)); }

/* Eyebrow. The accent at this size and weight is ordinary text, so it must
   clear WCAG AA 4.5:1 — --color-accent is #0071e3 at 4.56:1 on white. Do not
   lighten it here without re-measuring; core/tests_colour_contrast.py exists
   because a comment claiming a ratio is not a measurement. */
.eyebrow {
  display: inline-flex; align-items: center; gap: .5rem;
  font-size: .8125rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .14em;
  color: var(--color-accent);
  margin-bottom: .75rem;
}

.sec-head { max-width: 720px; margin-bottom: clamp(2rem, 4vw, 3rem); }
.sec-head p { margin: .75rem 0 0; }

/* ============================================================
   PHOTOGRAPHY — the part the site did not have
   ============================================================
   Every photograph renders through .media-frame + .media-img, so the house
   style (corner radius, hairline, scrim strength, hover) is one decision here
   rather than a judgement call at each call site. */

.media-frame {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border-radius: var(--r-2xl);
  /* A tinted base, not white: it sits behind the image while it loads and
     behind transparent cut-outs, and reads as intentional rather than broken. */
  background: var(--color-surface-hover, #f5f5f7);
}

.media-frame > img,
.media-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* GPU-composited transform only. Animating width/height here would force
     layout on every frame. */
  transition: transform var(--dur-reveal) var(--ease-out-soft);
}

.media-frame--card { box-shadow: var(--sh-lift); }
.media-frame--4x3 { aspect-ratio: 4 / 3; }
.media-frame--16x9 { aspect-ratio: 16 / 9; }
.media-frame--3x2 { aspect-ratio: 3 / 2; }
.media-frame--square { aspect-ratio: 1 / 1; }
.media-frame--tall { aspect-ratio: 4 / 5; }

.media-zoom:hover .media-img,
.media-zoom:focus-within .media-img { transform: scale(1.04); }

/* Scrims are fixed gradients, so legibility is a property of the component
   rather than of whichever photograph happens to sit under it. Tuned to hold
   white semibold text above 4.5:1 on the brightest image in the library. */
.media-scrim {
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(to top,
    rgb(0 0 0 / .72) 0%, rgb(0 0 0 / .42) 35%, rgb(0 0 0 / 0) 68%);
}
.media-scrim--full {
  position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(to right, rgb(12 12 14 / .84) 0%, rgb(12 12 14 / .58) 45%, rgb(12 12 14 / .30) 100%),
    linear-gradient(to top, rgb(12 12 14 / .50) 0%, rgb(12 12 14 / 0) 55%);
}

/* Caption sitting on the image. */
.media-caption {
  position: absolute; left: 1.25rem; right: 1.25rem; bottom: 1.1rem;
  color: #fff; font-weight: 600; font-size: 1.0625rem; line-height: 1.35;
}
.media-caption span { display: block; font-weight: 500; font-size: .8125rem; opacity: .85; margin-top: .15rem; }

/* Provenance, small but present. An uncredited photograph is a licensing
   problem, not a design one — and on this site it is also the line between
   describing heritage and implying a current capability. */
.media-credit {
  position: absolute; right: .75rem; bottom: .5rem;
  font-size: .625rem; font-weight: 500; letter-spacing: .02em;
  color: rgb(255 255 255 / .78);
  text-shadow: 0 1px 2px rgb(0 0 0 / .6);
}
.media-credit--below {
  position: static; display: block; margin-top: .5rem;
  color: var(--color-muted); text-shadow: none; font-size: .6875rem;
}

/* ============================================================
   MEDIA CARD — photo, an icon badge riding the seam, then copy
   ============================================================ */

.card-grid { display: grid; gap: clamp(1rem, 2vw, 1.75rem); }
.card-grid--2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); }
.card-grid--3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr)); }
.card-grid--4 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 13.5rem), 1fr)); }

.media-card {
  display: flex; flex-direction: column;
  background: var(--color-card);
  border: 1px solid var(--color-border);
  border-radius: var(--r-xl);
  overflow: hidden;
  text-decoration: none; color: inherit;
  transition: box-shadow .25s var(--ease-out-soft), transform .25s var(--ease-out-soft);
}
.media-card:hover { box-shadow: var(--sh-lift); transform: translateY(-2px); }
.media-card .media-frame { border-radius: 0; }

/* The body MUST be positioned. .media-frame is `position: relative`, so it
   paints in the positioned layer (CSS 2.1 painting order step 8) while a static
   sibling paints back at step 4 — underneath it, however late it appears in the
   DOM. The symptom is a badge sliced in half by the photograph. */
.media-card__body { position: relative; display: flex; flex-direction: column; flex: 1; padding: 1.25rem 1.25rem 1.4rem; }
.media-card__badge {
  position: absolute; top: -1.375rem; left: 1.25rem;
  width: 2.75rem; height: 2.75rem; border-radius: var(--r-md);
  display: grid; place-items: center;
  background: var(--color-accent); color: #fff; font-size: 1.15rem;
  box-shadow: var(--sh-soft);
}
.media-card__badge + h3, .media-card__badge ~ h3 { margin-top: 1.5rem; }
.media-card h3 { font-size: 1.0625rem; margin: 0 0 .4rem; }
.media-card p { margin: 0; color: var(--color-muted); font-size: .9375rem; line-height: 1.5; }
.media-card__more { margin-top: auto; padding-top: .9rem; color: var(--color-accent); font-weight: 600; font-size: .875rem; }

/* ============================================================
   BUTTONS — pill, one accent
   ============================================================ */

.btn-pill {
  display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
  padding: .8rem 1.5rem; border-radius: 999px;
  font-size: .9375rem; font-weight: 600; line-height: 1;
  text-decoration: none; cursor: pointer; border: 1px solid transparent;
  transition: background .2s var(--ease-out-soft), box-shadow .2s var(--ease-out-soft),
              transform .2s var(--ease-out-soft), color .2s var(--ease-out-soft);
}
.btn-pill--primary { background: var(--color-accent); color: var(--color-accent-text, #fff); }
.btn-pill--primary:hover { box-shadow: var(--sh-glow); transform: translateY(-1px); color: var(--color-accent-text, #fff); }
.btn-pill--ghost { background: var(--color-card); color: var(--color-text); border-color: var(--color-border); }
.btn-pill--ghost:hover { background: var(--color-surface-hover); transform: translateY(-1px); }

/* ============================================================
   STAT / PROOF TILES
   ============================================================ */

.stat-tile {
  background: var(--color-card); border: 1px solid var(--color-border);
  border-radius: var(--r-lg); padding: 1.1rem 1.25rem;
}
.stat-tile b { display: block; font-size: clamp(1.5rem, 2.6vw, 2.125rem); font-weight: 700; letter-spacing: -0.02em; line-height: 1.1; }
.stat-tile span { display: block; margin-top: .25rem; font-size: .8125rem; color: var(--color-muted); }

/* A tile floating over a photograph, as in the reference hero. */
.float-tile {
  position: absolute; z-index: 2;
  background: rgb(255 255 255 / .96);
  backdrop-filter: saturate(180%) blur(20px);
  border-radius: var(--r-lg); box-shadow: var(--sh-lift);
  padding: .7rem .95rem; min-width: 5.5rem;
}
.float-tile b { display: block; font-size: 1.25rem; font-weight: 700; line-height: 1.1; letter-spacing: -0.02em; color: var(--color-text); }
.float-tile span { display: block; font-size: .6875rem; color: var(--color-muted); margin-top: .1rem; line-height: 1.3; }

/* ============================================================
   REVEAL ON SCROLL
   ============================================================
   Progressive enhancement: the class does nothing until reveal.js adds
   .is-visible. base.html carries a <noscript> rule that forces content
   visible, so a JS failure can never hide the page. */

/* Scoped to .js — set on <html> by an inline script in base.html before paint.
   Without JS the rule never matches and the content is simply visible. */
.js .reveal {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity var(--dur-reveal) var(--ease-out-soft),
              transform var(--dur-reveal) var(--ease-out-soft);
}
.js .reveal.is-visible { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .js .reveal, .js .reveal.is-visible { opacity: 1; transform: none; transition: none; }
  .media-img, .media-card, .btn-pill { transition: none; }
  .media-zoom:hover .media-img { transform: none; }
}

/* ============================================================
   RESPONSIVE
   ============================================================ */

@media (max-width: 767px) {
  .public-main { font-size: 16px; }
  .media-caption { left: 1rem; right: 1rem; bottom: .9rem; font-size: .9375rem; }
  .float-tile { padding: .55rem .75rem; }
  .float-tile b { font-size: 1.05rem; }
}

/* ============================================================
   HOME — hero + proof
   ============================================================
   Page-specific composition built from the primitives above. Kept here rather
   than in a <style> block on the template so the inline-style ratchet in
   core/tests_css_debt.py keeps falling. */

.home-hero {
  background: linear-gradient(180deg, var(--color-surface-hover, #f5f5f7) 0%, var(--color-bg) 100%);
  padding-block: clamp(2.5rem, 6vw, 5.5rem);
}

.home-hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
}

.home-hero__title { margin: 0 0 1rem; }
.home-hero__lead { margin: 0 0 1.75rem; max-width: 34em; }

.home-hero__actions { display: flex; flex-wrap: wrap; gap: .75rem; margin-bottom: 1.75rem; }

.home-hero__points {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: .5rem 1.5rem;
  font-size: .875rem; color: var(--color-muted);
}
.home-hero__points li { display: inline-flex; align-items: center; gap: .4rem; }
.home-hero__points i { color: var(--color-accent); }

/* The figure is the positioning context for the floating tiles, so it must not
   be `overflow: hidden` — the tiles deliberately hang outside the frame. */
.home-hero__media { position: relative; margin: 0; }

.home-hero__tile--a { top: 1.25rem; left: -1rem; }
.home-hero__tile--b { bottom: 1.25rem; right: -1rem; }

/* ── Proof cards ── */
.home-proof__grid { margin-bottom: 1.75rem; }

.proof-card {
  display: flex; flex-direction: column; gap: .2rem;
  padding: 1.1rem 1.25rem;
  background: var(--color-card);
  border: 1px solid var(--color-border);
  border-radius: var(--r-lg);
  text-decoration: none; color: inherit;
  transition: box-shadow .25s var(--ease-out-soft), transform .25s var(--ease-out-soft);
}
.proof-card:hover { box-shadow: var(--sh-lift); transform: translateY(-2px); }
.proof-card__veh { font-weight: 600; font-size: .9375rem; color: var(--color-text); }
.proof-card__veh em { font-style: normal; color: var(--color-muted); font-weight: 500; }
.proof-card__stage {
  font-size: .6875rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
  color: var(--color-muted); margin-bottom: .35rem;
}
.proof-card__figs { font-size: 1.125rem; font-weight: 700; letter-spacing: -0.01em; color: var(--color-text); }
.proof-card__figs--sub { font-size: .8125rem; font-weight: 500; color: var(--color-muted); }

.home-proof__more { margin: 0; }

@media (max-width: 900px) {
  .home-hero__grid { grid-template-columns: 1fr; }
  /* Photo first on a phone: the copy is long and the image is the reason to
     keep reading. Order, not a second markup path. */
  .home-hero__media { order: -1; }
  .home-hero__tile--a { left: .75rem; }
  .home-hero__tile--b { right: .75rem; }
}

/* The hero photograph is cropped at SOURCE (prepare_site_images --crop), not
   here: a CSS crop still downloads the discarded pixels and shifts with the
   breakpoint. Nothing to correct at this end. */

/* A short scrim under the credit. Without it the credit sits on whatever colour
   the photograph happens to have at that corner — on this image, a red wing. */
.home-hero__media .media-frame::after {
  content: ""; position: absolute; inset: auto 0 0 0; height: 4.5rem;
  background: linear-gradient(to top, rgb(0 0 0 / .45), rgb(0 0 0 / 0));
  pointer-events: none;
}
.home-hero__media .media-credit { z-index: 1; }

/* Tiles hang over the frame's edge, not past the grid column — a tile clipped
   by the viewport reads as a rendering fault rather than a design. */
.home-hero__tile--a { top: 1.25rem; left: -0.75rem; }
.home-hero__tile--b { bottom: 1.25rem; right: 0.75rem; }

/* ── Numbered process steps ── */
.step-grid { list-style: none; margin: 0; padding: 0; counter-reset: none; }
.step-card {
  position: relative;
  background: var(--color-card); border: 1px solid var(--color-border);
  border-radius: var(--r-xl); padding: 1.5rem 1.35rem 1.4rem;
}
.step-card__n {
  display: grid; place-items: center;
  width: 2.25rem; height: 2.25rem; border-radius: 999px;
  background: var(--color-accent); color: var(--color-accent-text, #fff);
  font-weight: 700; font-size: .9375rem; margin-bottom: .85rem;
}
.step-card h3 { font-size: 1.0625rem; margin: 0 0 .4rem; }
.step-card p { margin: 0; color: var(--color-muted); font-size: .9375rem; line-height: 1.55; }

/* ── Store band: photo beside copy ── */
.store-band {
  display: grid; grid-template-columns: minmax(0, .85fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 3rem); align-items: center;
}
.store-band__copy h2 { margin: 0 0 .6rem; }
.store-band__copy p { margin: 0 0 1.1rem; }
.store-band__cats { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1.25rem; }

.chip-link {
  display: inline-flex; align-items: center;
  padding: .4rem .85rem; border-radius: 999px;
  border: 1px solid var(--color-border); background: var(--color-card);
  font-size: .8125rem; font-weight: 500; color: var(--color-text); text-decoration: none;
  transition: border-color .2s var(--ease-out-soft), background .2s var(--ease-out-soft);
}
.chip-link:hover { border-color: var(--color-accent); background: var(--color-surface-hover); color: var(--color-text); }

@media (max-width: 860px) {
  .store-band { grid-template-columns: 1fr; }
}

/* ── Gallery ──
   auto-fill rather than auto-fit: with auto-fit a final short row stretches its
   one item across the full width, which on a photo grid reads as a mistake. */
.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr));
  gap: clamp(.75rem, 1.6vw, 1.25rem);
}
.gallery-item { margin: 0; }
.gallery-item .media-frame { border-radius: var(--r-lg); }
.gallery-item figcaption {
  margin-top: .5rem; font-size: .8125rem; font-weight: 500; color: var(--color-muted);
}
.gallery-foot { margin-top: clamp(1.5rem, 3vw, 2.5rem); font-size: .875rem; max-width: 60ch; }

/* ── Gallery filtering ──
   A left rail of disclosure groups beside the grid, the shape Apple uses on its
   own browse pages: the controls live in one narrow column, and a long list of
   makes scrolls inside its own group rather than pushing the photographs down
   the page. Below 900px the rail becomes a card above the grid.

   `is-out` is the ONE hiding mechanism here. The template applies it server-side
   and gallery.js toggles the same class, so there is no second way for something
   to be hidden and no chance of the two disagreeing. It needs the compound
   selectors below because `.gallery-item` and the option rows already set
   `display`, and a bare `.is-out` would lose to them on specificity. */
.is-out { display: none; }

.gal-layout {
  display: grid;
  grid-template-columns: 15rem minmax(0, 1fr);
  gap: clamp(1.5rem, 3vw, 2.75rem);
  align-items: start;
  margin-top: clamp(1.25rem, 3vw, 2rem);
}

.gal-side {
  /* Sticky so the filters stay reachable 400 photographs down. The offset clears
     the fixed header; `max-height`+`overflow` keep the rail itself scrollable on
     a short window rather than clipping its last group. */
  position: sticky; top: 5.5rem;
  max-height: calc(100vh - 7rem); overflow-y: auto;
  padding-right: .25rem;
}
.gal-side__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; padding-bottom: .5rem; margin-bottom: .25rem;
  border-bottom: 1px solid var(--color-border);
}
.gal-side__title {
  margin: 0; font-size: .75rem; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--color-muted);
}
.gal-side__clear {
  font-size: .8125rem; font-weight: 500; color: var(--color-accent); text-decoration: none;
}
.gal-side__clear:hover { text-decoration: underline; }
.gal-side__clear.is-out { display: none; }

.gal-group { border-bottom: 1px solid var(--color-border); }
.gal-group.is-out { display: none; }
.gal-group__sum {
  display: flex; align-items: center; gap: .5rem;
  padding: .85rem 0; cursor: pointer; list-style: none;
  font-size: .9375rem; font-weight: 600; color: var(--color-text);
}
/* Safari still paints the default triangle without this. */
.gal-group__sum::-webkit-details-marker { display: none; }
.gal-group__sum:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; border-radius: 4px; }
.gal-group__name { flex: 0 0 auto; }
.gal-group__val {
  flex: 1 1 auto; min-width: 0; text-align: right;
  font-weight: 400; font-size: .8125rem; color: var(--color-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The chevron: a CSS square rotated 45°, so there is no icon font to load and
   nothing to go missing the way the Bootstrap icons once did. */
.gal-group__sum::after {
  content: ""; flex: 0 0 auto; width: .45rem; height: .45rem;
  border-right: 1.5px solid var(--color-muted); border-bottom: 1.5px solid var(--color-muted);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .2s var(--ease-out-soft);
}
.gal-group[open] > .gal-group__sum::after { transform: rotate(-135deg) translate(-2px, -2px); }
/* With a group open, its current value is in the list below it as a ticked row. */
.gal-group[open] > .gal-group__sum .gal-group__val { opacity: 0; }

.gal-opts {
  list-style: none; margin: 0 0 .85rem; padding: 0;
  max-height: 17rem; overflow-y: auto; scrollbar-width: thin;
}
.gal-opt-li.is-out { display: none; }
.gal-opt {
  display: flex; align-items: center; gap: .5rem;
  padding: .4rem .5rem .4rem .55rem; border-radius: var(--r-sm, 8px);
  font-size: .875rem; color: var(--color-text); text-decoration: none;
  transition: background .15s var(--ease-out-soft);
}
.gal-opt:hover { background: var(--color-surface-hover); color: var(--color-text); }
.gal-opt:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }
.gal-opt.is-on { color: var(--color-accent); font-weight: 600; }
/* A tick rather than a filled pill: the row is a state, not a button, and the
   tick is what the rest of the platform uses for a chosen item in a list. */
.gal-opt.is-on::before {
  content: ""; width: .7rem; height: .38rem; flex: 0 0 auto;
  margin-left: -.35rem; margin-right: -.15rem;
  border-left: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(-45deg) translate(1px, -2px);
}
.gal-n {
  margin-left: auto; padding-left: .5rem;
  font-size: .75rem; font-weight: 400; color: var(--color-muted);
  font-variant-numeric: tabular-nums;
}

.gal-count { font-size: .875rem; color: var(--color-muted); margin: 0 0 1rem; }
.gal-count.is-out, .gal-count__sel.is-out { display: none; }
.gal-count strong { color: var(--color-text); font-variant-numeric: tabular-nums; }
.gal-empty { font-size: .9375rem; color: var(--color-muted); padding: 2rem 0; }
.gal-empty.is-out { display: none; }

.gallery-item.is-out { display: none; }
/* The photograph is a link; nothing about it should look like body copy. */
.gal-open { display: block; text-decoration: none; color: inherit; cursor: zoom-in; }
.gal-open:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 3px; border-radius: var(--r-lg); }

@media (max-width: 899px) {
  /* One column: the rail becomes a card above the grid. Not sticky — a panel
     pinned over a phone screen costs more room than it saves. */
  .gal-layout { grid-template-columns: minmax(0, 1fr); }
  .gal-side {
    position: static; max-height: none; overflow: visible;
    background: var(--color-card); border: 1px solid var(--color-border);
    border-radius: var(--r-lg); padding: .25rem 1rem .5rem;
  }
  .gal-side__head { padding-top: .75rem; }
  .gal-group:last-of-type { border-bottom: 0; }
}

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

/* ── Gallery lightbox ──
   A native <dialog>: the browser owns the modal semantics, the focus trap and
   Escape. ::backdrop is the browser's own layer, so no stacking-context fight. */
.gal-box {
  border: 0; padding: 0; background: transparent; max-width: min(96vw, 1400px);
  max-height: 94vh; overflow: visible; color: #fff;
}
.gal-box::backdrop { background: rgb(10 10 12 / .88); }
.gal-box__stage { display: block; }
.gal-box__img {
  display: block; max-width: 100%; max-height: 82vh; margin: 0 auto;
  border-radius: var(--r-lg); background: #0c0c0e;
}
.gal-box__cap {
  margin: .75rem 0 0; text-align: center; font-size: .9375rem; font-weight: 500;
  text-shadow: 0 1px 3px rgb(0 0 0 / .6);
}
.gal-box__pos { margin-left: .6rem; opacity: .6; font-variant-numeric: tabular-nums; }
.gal-box__close, .gal-box__nav {
  position: fixed; border: 0; cursor: pointer; color: #fff; line-height: 1;
  background: rgb(255 255 255 / .12); border-radius: 999px;
  display: grid; place-items: center;
  transition: background .2s var(--ease-out-soft);
}
.gal-box__close:hover, .gal-box__nav:hover { background: rgb(255 255 255 / .26); }
.gal-box__close:focus-visible, .gal-box__nav:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
/* 44px minimum: the WCAG 2.2 target size, and the size a thumb actually hits. */
.gal-box__close { top: 1rem; right: 1rem; width: 2.75rem; height: 2.75rem; font-size: 1.75rem; }
.gal-box__nav { top: 50%; transform: translateY(-50%); width: 2.75rem; height: 2.75rem; font-size: 2rem; }
.gal-box__nav--prev { left: 1rem; }
.gal-box__nav--next { right: 1rem; }

@media (prefers-reduced-motion: reduce) {
  .gal-box__close, .gal-box__nav { transition: none; }
}
.home-proof__more { display: flex; flex-wrap: wrap; gap: .75rem; }

/* ── Featured dyno chart ──
   The SVG is emitted at 1000x500 with presentation attributes; constraining it
   here rather than at render time keeps one asset usable at any width. */
.proof-chart {
  margin: 0 0 clamp(1.5rem, 3vw, 2.5rem);
  background: var(--color-card);
  border: 1px solid var(--color-border);
  border-radius: var(--r-xl);
  padding: clamp(.75rem, 2vw, 1.25rem);
  box-shadow: var(--sh-soft);
}
.proof-chart svg { display: block; width: 100%; height: auto; max-width: 100%; }
.proof-chart figcaption {
  margin-top: .65rem; font-size: .8125rem; color: var(--color-muted); line-height: 1.5;
}

/* ── Dyno results listing: sort control + row sparkline ── */
.spec-sort {
  display: flex; align-items: center; flex-wrap: wrap; gap: .4rem;
  margin: 0 0 1rem;
}
.spec-sort__label {
  font-size: .6875rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .08em; color: var(--color-muted); margin-right: .35rem;
}
.spec-sort__opt {
  padding: .3rem .7rem; border-radius: 999px; font-size: .8125rem; font-weight: 500;
  border: 1px solid var(--color-border); background: var(--color-card);
  color: var(--color-text); text-decoration: none;
}
.spec-sort__opt:hover { border-color: var(--color-accent); color: var(--color-text); }
.spec-sort__opt.is-on {
  background: var(--color-accent); border-color: var(--color-accent);
  color: var(--color-accent-text, #fff);
}

/* aria-hidden on the element: the figures beside it already state the numbers,
   so announcing the curve would repeat them without adding anything. */
.spec-spark { display: inline-flex; align-items: center; opacity: .95; }
.spec-spark svg { display: block; }
@media (max-width: 640px) { .spec-spark { display: none; } }


/* ── About: photo hero + heritage strip ── */
.about-crumb { font-size: .75rem; color: var(--color-muted); margin-bottom: .9rem; }
.about-crumb a { color: var(--color-muted); }

.about-hero {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, .9fr);
  gap: clamp(1.5rem, 4vw, 3rem); align-items: center;
  margin-bottom: clamp(2rem, 4vw, 3rem);
}
.about-hero__copy h1 { margin: 0 0 .75rem; }
.about-hero__copy p { margin: 0 0 .75rem; }
.about-hero__note { font-size: .9375rem; }
.about-hero__media { margin: 0; }

.about-strip { margin-top: clamp(2rem, 4vw, 3rem); }
.about-strip h2 { margin: 0 0 .5rem; }
.about-strip > p { margin: 0 0 1.25rem; }
.about-strip__grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
  gap: clamp(.65rem, 1.5vw, 1rem);
}
.about-strip__grid .media-frame { border-radius: var(--r-lg); display: block; }

.about-actions { display: flex; gap: .875rem; flex-wrap: wrap; margin-top: 1.75rem; }

@media (max-width: 860px) { .about-hero { grid-template-columns: 1fr; } }
.about-stats {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  gap: 1rem; margin-bottom: clamp(2rem, 4vw, 2.5rem);
}
.about-stats .stat-tile b { color: var(--color-accent); }

/* ── Dyno results listing: row thumbnail ──
   The detail page captions a representative photo in words; a 72px thumbnail has
   no room for that, so a representative shot carries a small marked corner and
   says so in its alt text. An unmarked thumbnail is always our own photograph of
   the car in that result. */
.spec-thumb {
  position: relative; flex: 0 0 auto;
  width: 72px; height: 54px; border-radius: var(--r-sm);
  overflow: hidden; background: var(--color-surface-hover);
  border: 1px solid var(--color-border);
}
.spec-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.spec-thumb__rep {
  position: absolute; right: 2px; bottom: 2px;
  width: 13px; height: 13px; border-radius: 50%;
  background: rgb(0 0 0 / .62); color: #fff;
  font-size: 9px; font-style: normal; font-weight: 700; line-height: 13px;
  text-align: center; cursor: help;
}
@media (max-width: 640px) { .spec-thumb { width: 56px; height: 42px; } }

/* The listing row is a wrapping flex line. Adding a thumbnail pushed the
   trailing "View" onto a second line and broke the alignment down the column,
   so the two text columns now shrink instead of wrapping, and the action is
   pinned to the end of the row. */
.spec-card { flex-wrap: nowrap; }
.spec-card .spec-card-model { min-width: 0; flex: 1 1 auto; }
.spec-card .spec-card-action { margin-left: auto; flex: 0 0 auto; }
.spec-privacy-note { font-size: .75rem; color: var(--color-muted); margin-top: .35rem; max-width: 62ch; }
.spec-privacy-note i { margin-right: .3rem; }

@media (max-width: 860px) {
  /* Below this the row genuinely needs to wrap — but the figures and the action
     wrap as a unit rather than the action stranding itself on its own line. */
  .spec-card { flex-wrap: wrap; }
  .spec-card .spec-card-action { margin-left: 0; }
}
