/* ==========================================================================
   Belgee Москва — общие стили (токены, база, компоненты)
   ========================================================================== */

:root {
  --stone: #f4f4f5;
  --surface: #ffffff;
  --ink: #1b1c1e;
  --ink-soft: #5b5e62;
  /* --mint is the exact gold from the logo mark — every other gold shade is
     derived from it via color-mix() instead of being a separately hand-picked
     hex, so text accents, tints and the logo/buttons can never drift apart
     into mismatched browns again. */
  --mint: #c68a2e;
  --mint-ink: color-mix(in srgb, var(--mint) 58%, var(--ink));
  --mint-tint: color-mix(in srgb, var(--mint) 15%, var(--surface));
  --danger: #e5484d;
  --graphite: #6b6f73;
  --border: #dee0e1;

  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-pill: 999px;

  --ease: cubic-bezier(.22, .61, .36, 1);
  --dur-fast: .2s;
  --dur-base: .4s;
  --dur-slow: .7s;

  --font-display: 'Unbounded', 'Arial Black', sans-serif;
  --font-body: 'Manrope', 'Helvetica Neue', Arial, sans-serif;

  --header-h: 72px;
  --topbar-h: 38px;

  /* Shared by the two round arrow buttons (.steps-cta, .to-top) so the pair
     stays a matched set. Two layers each: a tight contact shadow that seats
     the circle on the page, plus a ring that sits at 0 width at rest and
     blooms to 6px of translucent gold when the button lights up — the lift is
     read from the ring rather than a transform, which both buttons need to
     keep free (.reveal.is-visible owns one, .to-top's show/hide owns the
     other). */
  --round-btn-shadow: 0 0 0 0 color-mix(in srgb, var(--mint) 40%, transparent), 0 10px 20px -10px rgba(27,28,30,.5);
  --round-btn-shadow-on: 0 0 0 6px color-mix(in srgb, var(--mint) 16%, transparent), 0 12px 22px -10px rgba(198,138,46,.5);

  color-scheme: light;
}

/* ---------- reset ---------- */
* { box-sizing: border-box; }

/* The single biggest source of "the buttons are slow on my phone". With no
   touch-action declared, a browser that supports double-tap-to-zoom cannot
   know whether a finger-up ends the gesture or starts a second tap, so it sits
   on the event for ~300ms before dispatching click. Every tap on the site paid
   that, and on a control that closes something — the modal ✕ — it reads as the
   dialog refusing to go away. `manipulation` keeps panning and pinch-zoom and
   gives up only double-tap zoom, which is what buys the delay back.

   width=device-width in the viewport meta already removes this on Chrome, but
   iOS Safari still applies it in plenty of cases, so the property is the only
   reliable fix rather than a redundant one.

   input[type=range] is excluded on purpose: the down-payment slider relies on
   the UA's own touch gesture handling, and overriding touch-action on it risks
   a horizontal drag being taken as a page pan instead of a drag of the thumb. */
a, button, label, summary, select, textarea,
input:not([type="range"]), [role="button"] { touch-action: manipulation; }
html { scroll-behavior: smooth; scroll-padding-top: calc(var(--topbar-h) + var(--header-h) + 16px); -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--stone);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 400;
  line-height: 1.6;
  overflow-x: hidden;
}
img, svg { max-width: 100%; display: block; }
/* <picture> вокруг каждой фотографии — обёртка для выбора мобильной копии
   (см. images/mobile). display: contents убирает саму обёртку из раскладки:
   без этого .shot img с его height: 100% считал бы проценты от <picture>,
   а не от рамки .shot, и картинка схлопнулась бы по высоте. Селекторы вида
   `.shot img` продолжают работать — они по потомку, а не по прямому ребёнку. */
picture { display: contents; }
figure { margin: 0; }
a { color: inherit; text-decoration: none; }
button { font: inherit; }
ul { margin: 0; padding: 0; list-style: none; }
h1, h2, h3, h4 { font-family: var(--font-display); margin: 0; text-wrap: balance; letter-spacing: -0.01em; }
p { margin: 0; }
input, select { font: inherit; }

a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible {
  outline: 2px solid var(--mint);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.container {
  max-width: 1200px;
  margin: 0 auto;
  padding-inline: clamp(20px, 4vw, 48px);
}

.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

/* ---------- eyebrow / section head ---------- */
.eyebrow {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--mint-ink); margin-bottom: 16px;
}
.eyebrow::before { content: ""; width: 8px; height: 8px; background: var(--mint); border-radius: 2px; flex: none; }

.section { padding-block: clamp(56px, 8vw, 108px); }
.section.alt { background: var(--surface); border-block: 1px solid var(--border); }

.section-head { max-width: 720px; margin-bottom: 40px; }
.section-head h2 { font-size: clamp(1.6rem, 2.6vw, 2.3rem); font-weight: 800; }
.section-head p { color: var(--ink-soft); font-size: 1.02rem; margin-top: 14px; max-width: 62ch; }
.section-head.center { margin-inline: auto; text-align: center; }

/* ---------- why belgee (dark highlight band) ---------- */
.why-belgee { background: var(--ink); }
.why-belgee .eyebrow { color: var(--mint); }
.why-belgee .eyebrow::before { background: var(--mint); }
.why-belgee .section-head h2 { color: var(--surface); }

.why-list {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px;
  background: color-mix(in srgb, var(--surface) 12%, transparent);
  border-radius: var(--radius-lg); overflow: hidden;
}
.why-item {
  display: flex; align-items: flex-start; gap: 18px;
  background: var(--ink); padding: 30px 34px;
  transition: background var(--dur-base) var(--ease);
}
.why-item:hover { background: color-mix(in srgb, var(--surface) 6%, var(--ink)); }
.why-item__check {
  flex: none; width: 34px; height: 34px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--mint) 20%, transparent);
  color: var(--mint); font-size: 1rem; font-weight: 800;
  transition: transform var(--dur-base) var(--ease);
}
.why-item:hover .why-item__check { transform: scale(1.12); }
.why-item h3 { font-family: var(--font-display); font-size: 1.02rem; font-weight: 800; color: var(--surface); }
.why-item p { margin-top: 6px; font-size: 0.88rem; line-height: 1.55; color: color-mix(in srgb, var(--surface) 68%, transparent); }
@media (max-width: 720px) { .why-list { grid-template-columns: 1fr; } }

/* ---------- buttons ---------- */
.btn {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: none;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.92rem;
  padding: 14px 24px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: box-shadow var(--dur-base) var(--ease), background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.btn-primary { background: var(--ink); color: var(--surface); }
.btn-primary:hover { box-shadow: 0 10px 24px -10px rgba(27,28,30,.5); }
.btn-secondary { background: transparent; color: var(--ink); border: 1px solid var(--border); }
.btn-secondary:hover { border-color: var(--ink); background: var(--stone); }
.btn-sm { padding: 10px 18px; font-size: 0.82rem; }
.btn-block { width: 100%; }

/* ---------- topbar ---------- */
.topbar {
  position: sticky; top: 0; z-index: 41;
  height: var(--topbar-h);
  background: linear-gradient(90deg, var(--ink), color-mix(in srgb, var(--ink) 85%, var(--mint) 15%) 52%, var(--ink));
  color: color-mix(in srgb, var(--surface) 78%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--mint) 22%, transparent);
}
/* grid (not flex) so .topbar__hours sits at the row's true horizontal
   center — the two 1fr side tracks stay equal regardless of how wide the
   address or phone text is, which plain space-between can't guarantee. */
.topbar__inner { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 16px; height: 100%; }
.topbar__location, .topbar__hours, .topbar__phone {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 0.76rem; font-weight: 600; white-space: nowrap;
}
.topbar__location { justify-self: start; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.topbar__hours { justify-self: center; }
.topbar__location svg, .topbar__hours svg, .topbar__phone svg { width: 14px; height: 14px; flex: none; color: var(--mint); }
.topbar__phone {
  justify-self: end;
  color: var(--surface); font-weight: 700;
  margin: -4px -12px; padding: 4px 12px; border-radius: var(--radius-pill);
  transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.topbar__phone:hover { color: var(--mint); background: color-mix(in srgb, var(--mint) 14%, transparent); }
/* Mobile topbar carries the two facts about the showroom — where and when —
   and hands the phone to the header below, where it becomes a round icon
   button. Address pinned left, hours right, nothing in the middle.

   Both strings only fit because "Ежедневно" drops out: at the desktop 0.76rem
   the full pair needs ~352px against the ~335px a 375px screen offers, and
   shrinking the type enough to hold "Ежедневно" would take it under 0.6rem.
   The word is the least informative part of the line once the hours are
   right there, so hiding it beats making both unreadable.

   Keyed to 860px, not the 640px this used to use, so the topbar and the
   header switch to their mobile arrangement at the same width. */
@media (max-width: 860px) {
  .topbar__inner { grid-template-columns: minmax(0, 1fr) auto; gap: 12px; }
  .topbar__phone { display: none; }
  .topbar__hours { display: inline-flex; justify-self: end; }
  .topbar__hours-prefix { display: none; }
  .topbar__location, .topbar__hours { font-size: 0.7rem; }
  .topbar__location svg, .topbar__hours svg { width: 13px; height: 13px; }
}

/* ---------- header / nav ---------- */
/* Opaque, with no backdrop-filter. Two reasons, and the second is the
   important one:

   Blurring what is behind a position: sticky element makes the GPU redo it on
   every scrolled frame, which is one of the few things that can make a whole
   page feel heavy on a phone. body is var(--stone) and so is this, so a solid
   fill looks all but identical.

   And backdrop-filter quietly turns an element into the containing block for
   any position: fixed descendant. .nav lives in here and is fixed, so its
   offsets used to resolve against the header rather than the screen. Declaring
   it here for every device — instead of dropping it on touch only, which is
   what broke the mobile menu — means .nav has one frame of reference
   everywhere, and it is the viewport. */
.site-header {
  position: sticky; top: var(--topbar-h); z-index: 40;
  height: var(--header-h);
  display: flex; align-items: center;
  background: var(--stone);
  border-bottom: 1px solid var(--border);
}
/* width: 100% is load-bearing. .site-header is itself a flex container, which
   makes this .container a flex item — and a flex item sizes to its content by
   default, not to the line. So instead of spanning the header, the row was
   only as wide as logo + actions and .container's own `margin: 0 auto` then
   centred that, leaving equal dead margins on both sides. That is why the logo
   sat well inboard of the address above it while the topbar — a plain block,
   so its .container is auto-width, i.e. full width — lined up correctly.
   max-width: 1200px and the auto margins still do their job on wide screens;
   this only stops the row collapsing to its content. */
.site-header .container { display: flex; align-items: center; justify-content: space-between; gap: 20px; width: 100%; }

.logo { display: flex; align-items: center; }
.logo img { height: 22px; width: auto; }

.nav { display: flex; align-items: center; gap: 4px; }
.nav a {
  padding: 10px 14px; border-radius: var(--radius-sm);
  font-weight: 600; font-size: 0.9rem; color: var(--ink-soft);
  transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.nav a:hover { color: var(--ink); background: var(--surface); }
.nav a[aria-current="page"] { color: var(--ink); background: var(--surface); border: 1px solid var(--border); }

.header-actions { display: flex; align-items: center; gap: 10px; }
/* Hidden on desktop: the topbar directly above already shows the number, and
   two phone numbers one under the other in the same corner is just the same
   fact twice. This link exists only for the mobile layout below, where the
   topbar hands its phone over to the opening hours and this becomes the round
   call button. */
.header-phone { display: none; align-items: center; font-weight: 700; font-size: 0.92rem; white-space: nowrap; }
.header-phone:hover { color: var(--mint-ink); }
/* The icon only exists for the mobile
   round-button form below. */
.header-phone__icon { display: none; width: 18px; height: 18px; flex: none; }
.header-cta-secondary { display: inline-flex; align-items: center; gap: 8px; white-space: nowrap; }
.header-cta-secondary__icon { display: none; width: 16px; height: 16px; flex: none; }

/* Even, robust hamburger — three flex children spaced by justify-content
   instead of three absolutely-positioned bars with hand-tuned top offsets,
   which is what made them read as unevenly spaced before. Flexbox guarantees
   equal gaps regardless of any pixel-math slip. */
.nav-toggle {
  display: none;
  appearance: none; border: 1px solid var(--border); background: var(--surface);
  width: 42px; height: 42px; border-radius: var(--radius-sm); cursor: pointer;
  padding: 0;
}
.nav-toggle__bars {
  display: flex; flex-direction: column; justify-content: space-between;
  width: 20px; height: 14px; margin: 0 auto; pointer-events: none;
}
.nav-toggle__bars span {
  display: block; width: 100%; height: 2px; border-radius: 1px; background: var(--ink);
  transition: transform var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease);
}
.nav-toggle[aria-expanded="true"] .nav-toggle__bars span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

@media (max-width: 860px) {
  /* Both CTA buttons go away — test-drive and call-back are one tap away in
     the hero right below the header. The phone link stays, but changes shape:
     the topbar above dropped it to make room for the opening hours, so this
     is now the only phone on a mobile screen. It becomes a round icon button
     sitting between the logo and the burger — the number itself is what goes,
     not the ability to call. */
  .header-cta-primary, .header-cta-secondary { display: none; }

  /* Proportions read off the reference, as % of screen width so the scale of
     the screenshot doesn't matter: circle ~8% across, bars ~5%, ~8% of clear
     space between them, bars ending ~7% in from the right edge. On a 375px
     phone that is a 30px circle, 19px of bars, a 30px gap.

     The gap value is 18px rather than that 30px because .nav-toggle keeps its
     42px hit area as transparent padding — the bars sit 11px inside their own
     box, so 18px of flex gap is what actually renders as ~29px of daylight
     between the circle and the first bar. */
  .header-actions { gap: 18px; }

  /* Box off, hit area kept. The border + white fill made this read as a second
     button crowding the circle; in the reference the burger is three bare
     lines. Dropping only the paint keeps the 42px target intact.

     The negative margin is what puts the bars ON the container's right edge,
     flush under where "с 08:00 до 21:00" ends in the topbar. The bars are 20px
     inside a 42px button, so they sit (42 - 20) / 2 = 11px in from its edge —
     without pulling that 11px back, the burger reads as indented from the
     column everything else in the header lines up to. The target keeps its
     full 42px and simply overhangs into the container's 20px padding, which
     also makes it easier to hit this close to the screen edge. */
  .nav-toggle { border: none; background: transparent; margin-right: -11px; }

  /* display: flex, not just a size — the base rule keeps this hidden, since
     the number is the topbar's job on desktop. Here is where the link exists
     at all, and it exists as the round call button. */
  .header-phone {
    display: flex; justify-content: center;
    width: 36px; height: 36px; border-radius: 50%;
    background: var(--surface); border: 1px solid var(--border); color: var(--mint-ink);
    transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
  }
  .header-phone:active { border-color: var(--mint); background: var(--mint-tint); }
  .header-phone__icon { display: block; }
  /* Clipped, not display:none — display:none would drop the number out of the
     accessibility tree too, leaving the button with no name at all. Clipped,
     it still reads as "+7 (495) …" to a screen reader, so no aria-label is
     needed to paper over an unlabelled icon. Absolute positioning keeps it out
     of the 36px circle's layout; .site-header is position: sticky, so it
     resolves against that. */
  .header-phone__text {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
  }

  /* Hidden via opacity/visibility rather than transform:translateX(100%) —
     a position:fixed element with a transform still inflates the document's
     horizontal scrollable area in Chromium/Safari even though it's clipped
     to the eye, which produced a real horizontal-scroll bug on mobile.

     top is the full topbar + header stack, because this resolves against the
     viewport: .site-header no longer carries a backdrop-filter, so it is not a
     containing block for a fixed child (position: sticky alone never was one).
     It used to be, which is why this offset was just var(--header-h) — measured
     from the header's own top. The day the filter came off, that same 72px
     started being measured from the top of the screen instead and the panel
     rode up over the header. Anchored to the viewport it is the sum, and the
     height below already assumed exactly that. */
  .nav { position: fixed; top: calc(var(--topbar-h) + var(--header-h)); left: 0; right: 0;
    height: calc(100vh - var(--topbar-h) - var(--header-h)); height: calc(100dvh - var(--topbar-h) - var(--header-h));
    background: var(--stone); flex-direction: column;
    align-items: stretch; padding: 18px; gap: 6px; opacity: 0; visibility: hidden; pointer-events: none;
    transition: opacity var(--dur-base) var(--ease), visibility 0s linear var(--dur-base);
    /* Drag the panel past its own end and the finger carried on scrolling the
       page underneath — scroll chaining. Locking the document doesn't catch
       this: the gesture is addressed to the panel, which is entitled to it.
       contain breaks the chain at this element. */
    overflow-y: auto; overscroll-behavior: contain; }
  .nav.is-open { opacity: 1; visibility: visible; pointer-events: auto; transition: opacity var(--dur-base) var(--ease); }
  .nav a { padding: 16px; font-size: 1.05rem; border-bottom: 1px solid var(--border); border-radius: 0; }
  .nav-toggle { display: block; }
}

/* ---------- hero (shared) ---------- */
.hero { position: relative; overflow: hidden; padding-block: clamp(48px, 8vw, 96px) clamp(48px, 7vw, 80px); }
/* Two soft blurred blobs give the hero some atmosphere instead of a flat
   background — pure CSS, no image weight. They're painted first (::before /
   ::after are first in source order) so the real content just naturally
   stacks on top without needing z-index. */
.hero::before, .hero::after {
  content: ""; position: absolute; border-radius: 50%; filter: blur(90px); pointer-events: none;
}
.hero::before {
  width: 460px; height: 460px; top: -180px; left: -140px;
  background: color-mix(in srgb, var(--mint) 20%, transparent);
}
.hero::after {
  width: 380px; height: 380px; bottom: -200px; right: 6%;
  background: color-mix(in srgb, var(--mint) 12%, transparent);
}
/* minmax(0, 1fr) rather than a bare 1fr — a grid track sized by 1fr alone
   still takes its min-width from content (min-width:auto), so the hero
   carousel's internal overflow-x:auto scroller would otherwise inflate the
   whole column to its unscrolled content width and push the page wider than
   the viewport instead of scrolling inside its own box. */
.hero-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 40px; align-items: center; }
.hero-grid.with-media { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: stretch; }
.hero-grid.with-media > .reveal:first-child { display: flex; flex-direction: column; justify-content: center; }
@media (max-width: 900px) {
  .hero-grid.with-media { grid-template-columns: minmax(0, 1fr); }
  .hero-grid.with-media > .reveal:first-child { display: block; }
  /* Photo first, text below — on a stacked mobile hero the car should be the
     first thing in view, not two screenfuls of heading/copy/buttons down. */
  .hero-photo-wrap { order: -1; }
}

.hero-eyebrow-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 18px; }
.hero h1 { font-size: clamp(2.4rem, 5.6vw, 4.6rem); font-weight: 800; line-height: 0.98; }
.hero .accent {
  background: linear-gradient(100deg, var(--mint-ink), var(--mint) 55%, var(--mint-ink));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.hero-lede { margin-top: 20px; max-width: 56ch; font-size: 1.06rem; color: var(--ink-soft); }
.hero-actions { margin-top: 32px; display: flex; gap: 12px; flex-wrap: wrap; }

/* Both hero CTAs on one row on a phone. At the desktop .btn size the two
   labels want ~470px of row between them and a 375px screen offers ~335, so
   flex-wrap dropped the second onto its own line — two full-width buttons
   stacked, ~108px of hero spent before the calculator card.
   flex: 1 1 0 splits the row in half instead, and the labels wrap to two
   lines inside their own button rather than the type shrinking to hold one.
   That was the choice worth making: forcing "Оставить заявку на звонок" onto
   a single line inside ~163px means ~10px type on the page's primary CTA,
   where letting it break keeps it at a readable 0.78rem and still comes out
   shorter than the stack it replaces (~57px vs ~108px).
   text-align matters because .btn centres with justify-content, which only
   places the text block — it says nothing about the second line inside it. */
@media (max-width: 560px) {
  .hero-actions { margin-top: 26px; gap: 8px; flex-wrap: nowrap; }
  .hero-actions .btn {
    flex: 1 1 0; min-width: 0;
    padding: 12px 10px; font-size: 0.78rem; line-height: 1.25;
    text-align: center;
  }
}
.hero .calc-mini { margin-top: 32px; }
.hero-price { margin-top: 28px; font-family: var(--font-display); font-weight: 800; font-size: 1.6rem; }
.hero-price small { display: block; font-family: var(--font-body); font-weight: 600; font-size: 0.76rem; color: var(--graphite); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 4px; }
.hero-photo { border-radius: var(--radius-lg); width: 100%; box-shadow: 0 40px 70px -36px rgba(27,28,30,.45); }

/* align-self: start keeps the photo at its own natural aspect-ratio height
   instead of stretching to match the text column — stretching cropped cars
   out of frame on wide, multi-car shots (see images/1.webp), which is worse
   than the two columns ending at different heights. */
.hero-photo-wrap { align-self: start; width: 100%; }

/* ---------- hero quick links (compact secondary actions) ---------- */
.hero-quicklinks { margin-top: 14px; display: flex; gap: 8px; flex-wrap: wrap; }
.hero-quicklink {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 14px; border-radius: var(--radius-pill);
  border: 1px solid var(--border); background: color-mix(in srgb, var(--surface) 70%, transparent);
  font-size: 0.8rem; font-weight: 700; color: var(--ink-soft);
  transition: border-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.hero-quicklink svg { width: 14px; height: 14px; flex: none; }
.hero-quicklink:hover { border-color: var(--mint); color: var(--mint-ink); background: var(--mint-tint); }

/* ---------- shot / photo placeholder ---------- */
.shot {
  position: relative;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--border);
}
.shot img { width: 100%; height: 100%; object-fit: cover; }
.shot.ar-16-9 { aspect-ratio: 16 / 9; }
.shot.ar-21-9 { aspect-ratio: 21 / 9; }
.shot.ar-3-2 { aspect-ratio: 3 / 2; }
.shot.ar-4-3 { aspect-ratio: 4 / 3; }
.shot.ar-1-1 { aspect-ratio: 1 / 1; }

.shot__empty {
  position: absolute; inset: 0;
  display: none;
  background:
    repeating-linear-gradient(135deg, color-mix(in srgb, var(--ink) 6%, transparent) 0 2px, transparent 2px 14px),
    var(--stone);
}
.shot--missing .shot__empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; text-align: center; padding: 14px; }
.shot--missing img { display: none; }
.shot__empty::before, .shot__empty::after, .shot__empty .c2, .shot__empty .c3 {
  content: ""; position: absolute; width: 16px; height: 16px; border: 2px solid var(--ink); opacity: .35;
}
.shot__empty::before { top: 10px; left: 10px; border-right: none; border-bottom: none; }
.shot__empty::after { bottom: 10px; right: 10px; border-left: none; border-top: none; }
.shot__empty .c2 { top: 10px; right: 10px; border-left: none; border-bottom: none; }
.shot__empty .c3 { bottom: 10px; left: 10px; border-right: none; border-top: none; }
.shot__num { font-family: var(--font-display); font-weight: 800; font-size: 1.4rem; color: var(--graphite); }
.shot__label { font-size: 0.78rem; color: var(--ink-soft); font-weight: 700; max-width: 22ch; }
.shot__ratio { font-size: 0.68rem; color: var(--graphite); text-transform: uppercase; letter-spacing: 0.06em; }

/* ---------- model teaser cards (home) ---------- */
/* auto-fit, а не фиксированные repeat(4, …): моделей в ряду может быть и
   меньше — любую из них можно скрыть через hidden: true в config.js, и тогда
   main.js удаляет её карточку отсюда. С жёсткими четырьмя колонками
   оставшиеся карточки сохранили бы свою ширину, а на месте убранной осталась
   бы дыра. Пустые дорожки auto-fit схлопывает, поэтому три карточки честно
   занимают ряд втроём, а четыре — вчетвером, как раньше. */
.model-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 20px;
}
@media (max-width: 980px) { .model-grid { grid-template-columns: repeat(2, minmax(0,1fr)); } }
@media (max-width: 560px) { .model-grid { grid-template-columns: 1fr; } }

/* .model-grid doubles as the carousel's .bcarousel__viewport (reuses
   initCarousel() from main.js instead of a second copy of the same JS) —
   but that class also carries its own generic display:flex/overflow-x:auto
   rule further down this file, which would otherwise win the grid-vs-flex
   tie on ALL screen sizes just by being declared later. Reasserting grid
   here, on the two-class .model-grid.bcarousel__viewport pair, outranks
   that single-class generic rule regardless of source order, so desktop
   stays a grid; the mobile override below then wins the same tie against
   *this* rule the same way, by being declared after it. */
.model-grid.bcarousel__viewport { display: grid; overflow: visible; border-radius: 0; }

/* Below 560px the grid (already one column, i.e. one card per row) becomes a
   swipeable carousel instead — a touchscreen doesn't need prev/next arrows,
   just the dots to show where you are, so .bcarousel__dots is the only
   carousel chrome rendered here (initCarousel() already skips prev/next
   entirely when it can't find them in the DOM). The slides are the
   .model-card links directly — initCarousel() just takes the viewport's own
   children, no .bcarousel__slide class needed — so .model-card's own
   look (border-radius, image crop, etc.) is never touched by the generic
   .bcarousel__slide rules further down this file. */
@media (max-width: 560px) {
  .bcarousel--models .bcarousel__viewport {
    display: flex; overflow-x: auto; scroll-snap-type: x mandatory; scroll-behavior: auto;
    -webkit-overflow-scrolling: touch; scrollbar-width: none; gap: 12px; padding-right: 32px;
  }
  .bcarousel--models .bcarousel__viewport::-webkit-scrollbar { display: none; }
  /* 85% width, not 100% — the next card's edge peeks in on the right so it
     visibly reads as "one of several, swipe for more" instead of looking
     like a single static card. */
  .bcarousel--models .model-card { flex: 0 0 85%; scroll-snap-align: start; }
  .bcarousel--models .bcarousel__dots { display: flex; }
}
.bcarousel--models .bcarousel__dots { display: none; }

.model-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transition: transform var(--dur-base) var(--ease), border-color var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease);
}
/* Prefixed with the grids that use this card — a bare .model-card:hover is
   the same (0,2,0) specificity as .reveal.is-visible's own transform rule,
   and since that rule sits later in the file it would otherwise win once the
   card has faded in, silently cancelling the hover lift. */
.model-grid .model-card:hover, .related-grid .model-card:hover { transform: translateY(-8px); border-color: var(--mint); box-shadow: 0 32px 56px -28px rgba(27,28,30,.35); }

.model-card-media { position: relative; }
.model-card-media .shot { border: none; border-radius: 0; }
.model-card-badge {
  position: absolute; top: 14px; right: 14px; z-index: 2;
  background: color-mix(in srgb, var(--surface) 90%, transparent); backdrop-filter: blur(6px);
  color: var(--ink); font-weight: 800; font-size: 0.76rem; white-space: nowrap;
  padding: 6px 12px; border-radius: var(--radius-pill);
}
.model-card-overlay {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  padding: 46px 18px 14px;
  background: linear-gradient(180deg, transparent, rgba(15,16,17,.86) 82%);
  display: flex; align-items: flex-end; justify-content: space-between; gap: 10px;
}
.model-card-index { font-family: var(--font-display); font-weight: 800; font-size: 0.86rem; color: color-mix(in srgb, var(--surface) 55%, transparent); }
.model-card-name { font-family: var(--font-display); font-weight: 800; font-size: 1.6rem; line-height: 1; color: var(--surface); }

.model-card-body { padding: 18px 20px 6px; display: flex; flex-direction: column; gap: 10px; flex: 1; }
.model-card-tagline { font-size: 0.86rem; color: var(--ink-soft); }
.model-card-specs { display: flex; gap: 10px; flex-wrap: wrap; font-size: 0.76rem; color: var(--graphite); font-variant-numeric: tabular-nums; margin-top: auto; }
.model-card-specs span { background: var(--stone); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 4px 8px; }
.model-card-link {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 16px 20px; margin-top: 4px; border-top: 1px solid var(--border);
  font-weight: 700; font-size: 0.86rem;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.model-card-link span { transition: transform var(--dur-fast) var(--ease); }
.model-card:hover .model-card-link { background: var(--mint-tint); color: var(--mint-ink); }
.model-card:hover .model-card-link span { transform: translateX(3px); }

/* ---------- payment methods (pricing-style cards) ---------- */
/* The pair spans the full container, same as every other grid on the page, so
   the cards line up with the left edge of the section heading above them. The
   earlier attempt at capping the row at 780px and centring it did keep the
   cards near their old third-of-a-row width, but it left a wide gutter on both
   sides and floated the block off the heading's axis — which read as a hole in
   the page rather than as a deliberately narrow column. What actually needed
   solving was the cards being too EMPTY at that width, and that's handled in
   .pay-card below by laying them out as rows instead of stacks. */
.pay-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; align-items: stretch; }
/* One column: now the cap is doing real work, since a single full-width card
   would be a very wide box around two short lines. */
@media (max-width: 860px) { .pay-grid { grid-template-columns: 1fr; max-width: 460px; margin-inline: auto; } }

/* Same trick as .model-grid above: .pay-grid doubles as the carousel's
   .bcarousel__viewport so it can reuse initCarousel() from main.js, and the
   generic .bcarousel__viewport display:flex rule further down the file would
   otherwise beat the grid on every screen size purely by source order. The
   two-class pair reasserts grid; the mobile block below then out-orders
   *this* rule the same way. */
.pay-grid.bcarousel__viewport { display: grid; overflow: visible; border-radius: 0; }

/* Below 560px (matching the models carousel) the stacked column becomes a
   swipeable row. Dots only, no arrows — initCarousel() skips prev/next when
   they're absent from the DOM. Slides are the .pay-card elements themselves,
   so their own styling is untouched by the generic .bcarousel__slide rules. */
@media (max-width: 560px) {
  .bcarousel--pay .bcarousel__viewport {
    /* max-width:none undoes the centring cap from the 860px block — a swipe row
       must span the full container, otherwise the last card's peek and the
       scroll padding get measured against a narrower box. */
    display: flex; max-width: none; overflow-x: auto; scroll-snap-type: x mandatory; scroll-behavior: auto;
    -webkit-overflow-scrolling: touch; scrollbar-width: none; gap: 12px;
    /* overflow-x:auto forces overflow-y to `auto` too: всё, что вылезает за
       рамку ленты сверху и снизу, обрезается. Отсюда все четыре значения, и
       каждое — под конкретный выступающий элемент.

       Сверху 24px: бейдж выделенной карточки висит на top:-14px, и вокруг него
       пульсирует кольцо box-shadow до 7px (см. badgePulse) — 21px в пике.

       Снизу 56px — с запасом, а не «впритык по расчёту». Видимая тень гаснет
       примерно на offsetY + spread + blur/2 (у выделенной карточки это
       30 - 30 + 28 = 28px), но рисует браузер её до полного радиуса размытия,
       то есть до offsetY + spread + blur = 56px. Отсчёт «по видимой части»
       и оставлял ту самую ровную линию: обрез приходился туда, где тень уже
       еле заметна, но ещё есть. 56px — это гарантированно всё.

       margin-bottom возвращает layout на место: 46 из этих 56 пикселей — запас
       под тень, а не отступ, и точки под лентой не должны из-за него уезжать.
       Отрицательный margin ничего не обрезает — в отличие от padding он не
       участвует в отсечении содержимого.

       Справа 32px: последней карточке нужен тот же зазор для подглядывания,
       что остальные получают от scroll-snap. */
    padding: 24px 32px 56px 0;
    margin-bottom: -46px;
  }
  .bcarousel--pay .bcarousel__viewport::-webkit-scrollbar { display: none; }
  /* 85%, not 100% — the next card peeks in so it reads as "swipe for more". */
  .bcarousel--pay .pay-card { flex: 0 0 85%; scroll-snap-align: start; }
  .bcarousel--pay .bcarousel__dots { display: flex; }
}
.bcarousel--pay .bcarousel__dots { display: none; }

.pay-card {
  position: relative;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: 40px 30px; text-align: center;
  display: flex; flex-direction: column; justify-content: center;
  box-shadow: 0 24px 48px -34px rgba(27,28,30,.22);
  transition: transform var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease), border-color var(--dur-base) var(--ease);
}
/* .pay-grid-prefixed selectors below aren't just for scoping — every card
   here also carries .reveal for the scroll-in fade, and .reveal.is-visible
   sets its own `transform: translateY(0)` at equal (0,2,0) specificity. Since
   that rule sits later in the file, a plain `.pay-card--featured:hover
   { transform: … }` would lose the fight and silently do nothing post-reveal.
   Stacking on the extra .pay-grid/.pay-card ancestor classes pushes these
   past that specificity so the hover/scale transforms actually win. */
.pay-grid .pay-card:hover { transform: translateY(-6px); border-color: color-mix(in srgb, var(--mint) 45%, var(--border)); box-shadow: 0 28px 52px -30px rgba(27,28,30,.3); }
.pay-card--featured {
  background: var(--ink); border-color: var(--ink);
  box-shadow: 0 30px 56px -30px rgba(27,28,30,.4), 0 0 0 1px color-mix(in srgb, var(--mint) 35%, transparent);
}
.pay-grid .pay-card.pay-card--featured:hover {
  border-color: var(--mint); transform: translateY(-6px);
  box-shadow: 0 36px 64px -26px rgba(27,28,30,.5), 0 0 0 2px var(--mint);
}
.pay-card__badge {
  position: absolute; top: -14px; left: 50%; transform: translateX(-50%);
  background: var(--mint); color: var(--mint-ink); font-size: 0.7rem; font-weight: 800;
  letter-spacing: 0.06em; text-transform: uppercase; padding: 6px 14px; border-radius: var(--radius-pill);
  white-space: nowrap;
  animation: badgePulse 2.2s var(--ease) infinite;
  transition: transform var(--dur-fast) var(--ease);
}
.pay-card--featured:hover .pay-card__badge { transform: translateX(-50%) scale(1.1); animation-play-state: paused; }
@keyframes badgePulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--mint) 45%, transparent); }
  50% { box-shadow: 0 0 0 7px color-mix(in srgb, var(--mint) 0%, transparent); }
}
.pay-card__label { font-size: 0.72rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em; color: var(--graphite); }
.pay-card--featured .pay-card__label { color: color-mix(in srgb, var(--surface) 55%, transparent); }
.pay-card__rate { margin-top: 6px; font-family: var(--font-display); font-weight: 800; font-size: clamp(2.4rem, 4vw, 3.2rem); color: var(--mint-ink); line-height: 1; }
.pay-card--featured .pay-card__rate { color: var(--mint); }
.pay-card__rate span { font-size: 1.4rem; font-weight: 700; }
.pay-card h3 { margin-top: 14px; font-size: 1.05rem; font-weight: 800; }
.pay-card--featured h3 { color: var(--surface); }
.pay-card p { margin-top: 8px; font-size: 0.88rem; color: var(--ink-soft); line-height: 1.5; }
.pay-card--featured p { color: color-mix(in srgb, var(--surface) 68%, transparent); }

/* From 861px up — the width at which the grid goes back to two columns — the
   card turns on its side: the rate becomes a left-hand figure and the name and
   copy sit beside it, split by a hairline. That is what makes a ~530px card
   look filled rather than padded out; the same content stacked and centred is
   what left the "too much empty space" impression, because a 3-word paragraph
   centred in a 530px box is mostly margin.
   The measure works out too — the copy column lands near 40 characters, which
   is a normal line, where the full card width would have been an awkward 70.
   Everything below 861px keeps the centred stack it already had: that layout
   is right for one column and right inside the phone carousel. */
@media (min-width: 861px) {
  .pay-card { flex-direction: row; align-items: center; text-align: left; padding: 32px 34px; gap: 24px; }
  /* flex: none so the figure keeps its natural width and the copy takes the
     rest — the default flex-shrink would let a long rate squeeze itself. */
  .pay-card__rate-block { flex: none; }
  /* align-self: stretch, not the row's centre alignment, so the hairline runs
     the full height of the card instead of only as tall as the text it happens
     to sit next to — the grid already makes both cards the same height, so the
     two dividers then match each other exactly. The inner column re-centres the
     text that stretching would otherwise have pinned to the top. */
  .pay-card__body {
    flex: 1 1 auto; align-self: stretch;
    display: flex; flex-direction: column; justify-content: center;
    border-left: 1px solid var(--border); padding-left: 24px;
  }
  .pay-card--featured .pay-card__body { border-left-color: color-mix(in srgb, var(--surface) 22%, transparent); }
  /* The 14px belonged to the vertical stack: here the h3 starts its own column
     and any top margin just knocks it out of line with the rate beside it. */
  .pay-card__body h3 { margin-top: 0; }
}

/* ---------- calculator: shared controls ---------- */
/* Fixed 9-column template (not grid-auto-columns: 1fr) so every chip is the
   same width everywhere — with auto-columns, the 9-chip down-payment row and
   the 8-chip term row each divided their own row into a different number of
   equal shares, so the two rows' chips ended up visibly different sizes.
   The term row's 8 buttons fill columns 1-8, which is also what lines each
   one up under the down-payment chip directly above it — the 9th column
   just sits empty under "90%". */
.chip-row { display: grid; grid-template-columns: repeat(9, minmax(0, 1fr)); gap: 4px; }
.chip-btn {
  appearance: none; border: 1px solid var(--border); background: var(--surface); color: var(--ink-soft);
  font-family: var(--font-body); font-weight: 700; font-size: 0.76rem;
  padding: 10px 2px; border-radius: var(--radius-sm); cursor: pointer; min-width: 0;
  text-align: center; white-space: nowrap; overflow: hidden;
  transition: border-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
/* Without nowrap, a label that's a hair too wide for its column ("6 мес.",
   "2 года"…) wraps to two lines while its shorter neighbors ("50%", "1 год")
   stay on one — same grid column, suddenly a different height, so the two
   rows stop reading as one uniform shape. nowrap+overflow:hidden pins every
   chip to a single line no matter what; the shrink below is what keeps that
   line from clipping once the column gets too narrow to hold it. */
@container calc-controls (490px < width <= 560px) {
  .chip-btn { font-size: 0.7rem; padding: 9px 2px; }
}

/* Below ~490px of controls column, 9 chips across stops being a usable
   control: each column is under 30px, which forced the font down to ~9px and
   left tap targets far below the ~44px minimum. So the row reflows to 4
   columns and the type goes back UP to a readable size — two comfortable rows
   beat one unreadable one.

   The down-payment chips are dropped outright here rather than reflowed: the
   range slider directly above them (min 10, max 90, step 10) already offers
   exactly the same nine values, so the chips were pure duplication paid for in
   vertical space — and 9 of them don't divide into 4 columns without leaving a
   ragged third row anyway. The adjacent-sibling selector is what picks out
   that one row: only the down-payment field has a .range-input before its
   .chip-row, so the term row is untouched. The hidden buttons stay in the DOM
   and keep getting their aria-pressed synced by calculator.js, so nothing
   there needs to know about this.

   Keyed to the container rather than the viewport on purpose. The calculator
   ships in two places — inline in the page and inside #calculatorModal — whose
   controls columns are ~303px and ~275px on a 375px phone but differ wildly on
   desktop, so this is the one condition that makes both instances render
   identically at any given width, which a viewport query can't do. It also
   catches the genuinely narrow desktop cases (the wide modal's controls column
   is ~387px even on a 1440px screen), which is intended: they had the same
   cramped 9-across row. */
@container calc-controls (width <= 490px) {
  .calc-field .chip-row { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
  .chip-btn { font-size: 0.8rem; padding: 12px 4px; }
  .range-input + .chip-row { display: none; }
}
.chip-btn:hover { border-color: var(--ink); color: var(--ink); }
.chip-btn[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--surface); }

/* Vertical centring of the thumb without a margin, deliberately — a negative
   margin-top cannot centre it in both engines at once, which is exactly why it
   kept sitting off the line:

     Chromium lays the thumb out at the TOP of the track box, so it needs
     margin-top: -(thumb - track) / 2 to look centred.
     WebKit (Safari, so every iPhone too) centres the thumb on the track by
     itself, and reads that same margin as a shift — the more you correct for
     Chromium, the higher it rides in Safari.

   The way out is to leave the margin at 0 and give the TRACK the thumb's own
   height, because then "top of the track" and "centre of the track" are the
   same place and both engines agree. The visible 4px line is no longer the
   track box at all: it's painted as the input's own background, one gradient
   sized 100% x --range-track and positioned dead centre. The track
   pseudo-elements just become a transparent full-height lane for the thumb to
   sit in. Side effect worth having: the control is now as tall as its thumb
   rather than 4px, so the whole strip is draggable instead of a hairline.

   The horizontal half of the same alignment: the fill gradient used to run to
   a plain percentage of the width, but the thumb's CENTRE only travels between
   thumb/2 and width - thumb/2 — it never reaches either edge. So at 10% the
   boundary sat left of the thumb and at 90% right of it. --range-pos insets the
   span by the thumb width, which is the geometry the browser itself uses, so
   the two now agree at every step. paintRange() in calculator.js reports only
   the 0..1 position; the arithmetic lives here, where the thumb's size is
   known. Both the mobile overrides below are just the two variables. */
.range-input {
  --range-track: 4px;
  --range-thumb: 26px;
  --range-pos: calc((100% - var(--range-thumb)) * var(--range-pct, 0) + var(--range-thumb) / 2);
  display: block; width: 100%; height: var(--range-thumb); margin: 0;
  -webkit-appearance: none; appearance: none; outline: none; cursor: pointer;
  background: linear-gradient(to right,
      var(--ink) 0, var(--ink) var(--range-pos),
      var(--border) var(--range-pos), var(--border) 100%)
    center / 100% var(--range-track) no-repeat;
}
.range-input::-webkit-slider-runnable-track { height: 100%; background: transparent; border: none; }
.range-input::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; box-sizing: border-box; margin-top: 0;
  width: var(--range-thumb); height: var(--range-thumb); border-radius: 50%; background: var(--ink); cursor: pointer;
  border: 3px solid var(--surface); box-shadow: 0 0 0 1px var(--border);
  transition: transform var(--dur-fast) var(--ease);
}
.range-input::-webkit-slider-thumb:hover { transform: scale(1.1); }
.range-input::-moz-range-track { height: 100%; background: transparent; border: none; }
/* box-sizing has to be spelled out: `*` at the top of this file matches
   elements, not pseudo-elements, so without it the 3px border would land
   outside the width and Firefox would draw a bigger thumb than WebKit. */
.range-input::-moz-range-thumb {
  box-sizing: border-box; border: 3px solid var(--surface);
  width: var(--range-thumb); height: var(--range-thumb); border-radius: 50%; background: var(--ink); cursor: pointer;
}

/* ---------- calculator: full block ---------- */
.calc {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.calc-trims { display: flex; flex-wrap: wrap; gap: 8px; padding: 20px clamp(20px, 4vw, 36px); border-bottom: 1px solid var(--border); }
.trim-btn {
  appearance: none; border: 1px solid var(--border); background: var(--stone); color: var(--ink-soft);
  font-family: var(--font-display); font-weight: 700; font-size: 0.9rem;
  padding: 10px 18px; border-radius: var(--radius-sm); cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.trim-btn:hover { border-color: var(--ink); color: var(--ink); }
.trim-btn[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--surface); }

.calc-body { display: grid; grid-template-columns: 1.15fr 0.85fr; }
@media (max-width: 900px) { .calc-body { grid-template-columns: 1fr; } }

/* Named so the chip rows can query it by width — the same 9-column chip row
   sits both inline on the page (usually plenty of room) and in the narrower
   calculator modal, so a viewport media query can't tell those two cases
   apart the way a size container query keyed to this element can. */
.calc-controls { padding: clamp(24px, 4vw, 40px); display: flex; flex-direction: column; gap: 26px; container: calc-controls / inline-size; }

.calc-price-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; }
.calc-price-old { font-size: 0.86rem; color: var(--graphite); text-decoration: line-through; font-variant-numeric: tabular-nums; }
.calc-price-new { font-family: var(--font-display); font-weight: 800; font-size: clamp(1.7rem, 3.4vw, 2.3rem); letter-spacing: -0.01em; font-variant-numeric: tabular-nums; }
.calc-discount-badge { background: var(--mint-tint); color: var(--mint-ink); font-weight: 700; font-size: 0.8rem; padding: 7px 12px; border-radius: var(--radius-sm); white-space: nowrap; }

.calc-field label { display: flex; justify-content: space-between; align-items: baseline; font-weight: 700; font-size: 0.88rem; margin-bottom: 12px; }
.calc-field label .val { font-family: var(--font-display); font-weight: 800; color: var(--mint-ink); font-variant-numeric: tabular-nums; }
.calc-field .chip-row { margin-top: 12px; }

.calc-mode-badge {
  display: inline-flex; align-items: center; gap: 8px; align-self: flex-start;
  font-weight: 800; font-size: 0.86rem; padding: 8px 14px; border-radius: var(--radius-sm);
  background: var(--stone); border: 1px solid var(--border);
}
.calc-mode-badge.is-installment { background: var(--mint-tint); border-color: transparent; color: var(--mint-ink); }
.calc-mode-badge.pulse { animation: calcModePulse 0.7s var(--ease); }
@keyframes calcModePulse {
  0% { transform: scale(1); }
  35% { transform: scale(1.12); }
  100% { transform: scale(1); }
}

.calc-result { background: var(--ink); color: var(--surface); padding: clamp(24px, 4vw, 40px); display: flex; flex-direction: column; justify-content: center; gap: 22px; }
.calc-result-row { display: flex; justify-content: space-between; align-items: baseline; font-size: 0.86rem; color: color-mix(in srgb, var(--surface) 70%, transparent); padding-bottom: 14px; border-bottom: 1px solid color-mix(in srgb, var(--surface) 18%, transparent); }
.calc-result-row span:last-child { color: var(--surface); font-weight: 700; font-variant-numeric: tabular-nums; }
.calc-payment .label { font-size: 0.76rem; text-transform: uppercase; letter-spacing: 0.08em; color: color-mix(in srgb, var(--surface) 65%, transparent); }
.calc-payment .amount { font-family: var(--font-display); font-weight: 800; font-size: clamp(2.2rem, 5vw, 3rem); letter-spacing: -0.01em; color: var(--mint); margin-top: 8px; font-variant-numeric: tabular-nums; }
.calc-payment .amount small { font-family: var(--font-body); font-size: 1.05rem; font-weight: 700; color: color-mix(in srgb, var(--surface) 80%, transparent); }
.calc-daily { margin-top: 8px; font-size: 0.84rem; color: color-mix(in srgb, var(--surface) 70%, transparent); font-variant-numeric: tabular-nums; }
.calc-daily strong { color: var(--surface); }
.calc-disclaimer { font-size: 0.72rem; color: color-mix(in srgb, var(--surface) 55%, transparent); margin-top: -6px; }
.calc-result .btn-primary { background: var(--mint); color: var(--mint-ink); margin-top: 4px; }
.calc-result .btn-primary:hover { box-shadow: 0 12px 22px -12px rgba(198,138,46,.5); }

/* On a phone the two calc panels are already stacked (the 900px rule above),
   so the desktop breathing room turns into a very long scroll for what is
   really one short form. This trims the padding and the inter-field gaps —
   the layout is unchanged, just tightened.

   Deliberately a viewport media query, not the calc-controls container query
   used for the chips: .calc-trims sits outside that container, so the trim
   rows can't be reached from it, and the whole block needs to move together
   to stay visually consistent. */
@media (max-width: 560px) {
  .calc-trims { padding: 12px 14px; gap: 6px; }
  /* Smaller type + tighter padding so all four model buttons hold one row on
     a 375px screen instead of wrapping "S50" onto a second line. */
  .trim-btn { font-size: 0.82rem; padding: 9px 12px; }

  .calc-controls { padding: 16px 14px; gap: 16px; }

  /* The result panel's two summary rows are short label/value pairs that each
     wasted a full row of a phone screen. Grid instead of the desktop flex
     column pairs them into two stat tiles on one row; everything after them
     (payment, disclaimer, CTA) explicitly spans both columns, so the rest of
     the panel is unchanged. Stacking label over value inside each tile is what
     lets them survive a ~130px column — side by side they'd collide. */
  .calc-result {
    padding: 18px 14px;
    display: grid; grid-template-columns: 1fr 1fr; align-content: center;
    column-gap: 12px; row-gap: 14px;
  }
  .calc-result > :not(.calc-result-row) { grid-column: 1 / -1; }
  .calc-result-row {
    flex-direction: column; align-items: flex-start; justify-content: flex-start; gap: 3px;
    font-size: 0.76rem; padding-bottom: 10px;
  }
  .calc-result-row span:last-child { font-size: 0.9rem; }

  .calc-price-row { gap: 10px; }
  .calc-discount-badge { font-size: 0.74rem; padding: 6px 10px; }
  .calc-mode-badge { font-size: 0.8rem; padding: 7px 12px; }

  .calc-field label { font-size: 0.84rem; margin-bottom: 10px; }
  .calc-field .chip-row { margin-top: 10px; }

  /* Still the biggest thing in the panel, just no longer the ~35px the desktop
     clamp lands on — that height bought nothing once the panel was the only
     thing on screen. */
  .calc-payment .amount { font-size: 2rem; }
  .calc-payment .amount small { font-size: 0.95rem; }
  .calc-daily { font-size: 0.8rem; }
  .calc-disclaimer { font-size: 0.68rem; }

  /* With the preset chips gone at this width the slider is the only way to set
     the down payment, so it gets a bigger track and thumb to match. Only the
     two variables change — the control's height, the line's thickness and the
     fill's inset all derive from them up where the control is defined. */
  .range-input { --range-track: 6px; --range-thumb: 32px; }
}

/* 320-380px (iPhone SE and the narrow end of Android). The model row needs
   ~261px at the sizes above and only gets ~252px here, so "S50" was dropping
   to a second line — the last width at which anything in the calculator still
   overflows. Shaving the type and padding brings it to ~239px and the row
   holds. The panel paddings come down with it so the controls column doesn't
   lose more width to chrome than it has to. */
@media (max-width: 380px) {
  .calc-trims { padding: 10px 12px; }
  .trim-btn { font-size: 0.78rem; padding: 8px 10px; }
  .calc-controls { padding: 14px 12px; gap: 14px; }
  .calc-result { padding: 16px 12px; }
}

/* ---------- calculator: hero mini ---------- */
.calc-mini {
  position: relative;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: 26px 28px 26px 32px; display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap;
  box-shadow: 0 28px 56px -30px rgba(27,28,30,.26);
  overflow: hidden;
}
.calc-mini::before {
  content: ""; position: absolute; inset: 0 auto 0 0; width: 4px;
  background: linear-gradient(180deg, var(--mint), color-mix(in srgb, var(--mint) 35%, var(--surface)));
}
.calc-mini-row { display: flex; align-items: stretch; gap: 26px; flex-wrap: wrap; }
.calc-mini-block { display: flex; flex-direction: column; justify-content: center; min-height: 54px; }
.calc-mini-block .calc-mini-label { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(--graphite); font-weight: 700; }
.calc-mini-price { margin-top: 8px; font-family: var(--font-display); font-weight: 800; font-size: 1.6rem; letter-spacing: -0.01em; font-variant-numeric: tabular-nums; }
.calc-mini-price .old { font-family: var(--font-body); font-weight: 600; font-size: 0.8rem; color: var(--graphite); text-decoration: line-through; margin-right: 8px; }
.calc-mini-divider { width: 1px; align-self: stretch; background: var(--border); }
.calc-mini-payment {
  margin-top: 8px; font-family: var(--font-display); font-weight: 800; font-size: 1.6rem;
  color: var(--mint-ink); font-variant-numeric: tabular-nums; letter-spacing: -0.01em;
}
.calc-mini-payment small { font-family: var(--font-body); font-weight: 700; font-size: 0.86rem; margin-left: 2px; }
.calc-mini-link {
  display: inline-flex; align-items: center; gap: 8px;
  font-weight: 700; font-size: 0.86rem; white-space: nowrap;
  padding: 13px 22px; border-radius: var(--radius-pill);
  background: var(--ink); color: var(--surface);
  transition: background var(--dur-fast) var(--ease), box-shadow var(--dur-base) var(--ease);
}
.calc-mini-link svg { width: 16px; height: 16px; transition: transform var(--dur-fast) var(--ease); }
.calc-mini-link:hover { background: var(--mint-ink); box-shadow: 0 12px 22px -12px rgba(198,138,46,.5); }
.calc-mini-link:hover svg { transform: translateX(3px); }

/* The card was sized for a desktop hero — on a phone (now the first thing
   under the photo, see .hero-photo-wrap order above) its 26-32px padding and
   1.6rem price numbers read as oversized. Smaller padding/type plus a
   full-width, centered CTA reads as a deliberate compact card instead of a
   shrunk-down desktop one. */
@media (max-width: 560px) {
  .calc-mini { padding: 18px 20px 18px 24px; gap: 14px; }
  /* nowrap: "Кредит" belongs to the right of "Цена", same as desktop — it
     was dropping to its own line below once the row ran out of width, not
     staying beside it, so the fix is smaller type to fit one row, not a
     wrap onto a second one. */
  .calc-mini-row { gap: 14px; flex-wrap: nowrap; }
  /* Belt to the modeLabelShort braces: this caption is the only text in the
     card that changes as you drag the slider, so it is the only one that can
     resize the card under the visitor's finger. Pinning it to one line means
     no future wording can reintroduce that jump. Only the payment block gets
     this — the left block's label is the model name, which is long enough
     ("Belgee X50 Plus Prestige") that it genuinely needs to wrap. */
  .calc-mini-block--payment .calc-mini-label { white-space: nowrap; }
  .calc-mini-price, .calc-mini-payment { font-size: 1.1rem; }
  .calc-mini-price .old { font-size: 0.7rem; margin-right: 6px; }
  .calc-mini-link { width: 100%; justify-content: center; padding: 12px 18px; }
}

/* ---------- modal ---------- */
/* Not inset: 0. The overlay is laid over the VISIBLE part of the screen, which
   on iOS is not the same box a fixed element gets by default — the keyboard
   pushes the visible area up inside an unchanged layout viewport, and the
   address bar changes its height on scroll. --vv-h / --vv-top are set from
   window.visualViewport by js/main.js for as long as a modal is open, and the
   fallbacks are what every other browser (and any browser without the API)
   uses: exactly the old inset: 0 behaviour.
   Everything inside inherits the correction for free — the dialog's
   max-height: min(88vh, 100%) now measures 100% of what can be SEEN, so with
   the keyboard up the window shrinks into what is left of the screen and
   .modal-scroll takes over, instead of the top of it (the heading and the ✕)
   sliding up under the address bar. */
.modal {
  position: fixed; left: 0; right: 0;
  top: var(--vv-top, 0px);
  height: var(--vv-h, 100%);
  z-index: 100; display: none;
}
.modal.is-open { display: flex; align-items: center; justify-content: center; padding: 24px; }
.modal-backdrop {
  position: absolute; inset: 0; background: color-mix(in srgb, var(--ink) 55%, transparent);
  opacity: 0; transition: opacity var(--dur-base) var(--ease);
}
.modal.is-open .modal-backdrop { opacity: 1; }
/* The dialog does not scroll — .modal-scroll inside it does. That split is the
   whole point, and it exists because the ✕ has to be pinned to a box that never
   moves. Two earlier attempts pinned it inside the scroller itself, first with
   position: absolute (which pins to the top of the CONTENT, so it scrolled away
   in the long modals) and then with position: sticky (correct in Chromium at
   every scroll offset I measured — but sticky on a FLOAT is exactly the corner
   of the spec engines have historically disagreed about, and the browser that
   actually matters here, iOS Safari, is the one I cannot test).
   With the scrolling moved one level in, the button needs no sticky, no float
   and no offset arithmetic: it is position: absolute against a box whose size
   and place never change. That is behaviour no engine gets wrong. */
.modal-dialog {
  --modal-pad-y: clamp(24px, 4vw, 34px);
  --modal-pad-x: clamp(24px, 4vw, 34px);
  position: relative; z-index: 1; margin: 0; width: min(420px, calc(100% - 32px));
  background: var(--surface); border-radius: var(--radius-lg);
  /* overflow: hidden both keeps the scrolled content inside the rounded corners
     and stops this box from ever becoming a scroller itself. */
  display: flex; flex-direction: column; overflow: hidden;
  /* The 100% is the half that matters on iOS, and it is why the ✕ was out of
     reach on a real phone in a way no desktop window reproduces: vh there is
     the LARGE viewport — the page as it would be with the browser chrome
     hidden — while this fixed overlay is only as tall as what you can actually
     see. So an 88vh dialog centred in a shorter box overflowed it equally top
     and bottom, and the top, the part with the heading and the ✕ in it, ended
     up underneath Safari's address bar. Tapping there hits the address bar.
     100% resolves against this overlay's own content box, so the dialog can
     never be taller than the space it is centred in, whatever the browser is
     doing with its toolbars. min() keeps the 88vh proportion where there is
     room to honour it. */
  max-height: min(88vh, 100%);
  opacity: 0; transform: translateY(16px) scale(.98);
  transition: opacity var(--dur-base) var(--ease), transform var(--dur-base) var(--ease);
}
/* The dialog takes focus on open (see openModal in js/main.js) purely so the
   keyboard and screen readers land inside it; it is not something the visitor
   navigated to, so it gets no ring. Real controls keep theirs. */
.modal-dialog:focus { outline: none; }
/* min-height: 0 because a flex item's default min-height: auto refuses to
   shrink below its content — without it the scroller grows to the full list and
   the dialog overflows the screen instead of scrolling inside it. */
.modal-scroll {
  min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  padding: var(--modal-pad-y) var(--modal-pad-x);
}
.modal.is-open .modal-dialog { opacity: 1; transform: translateY(0) scale(1); }

/* Phone: the dialog hangs from the top of the overlay instead of being centred
   in it, and that is entirely about the ✕. Centred, the button's distance from
   the top of the screen is a function of the dialog's HEIGHT — so it sat lower
   in a short modal than in a tall one, and worse, it slid downward mid-modal
   whenever the content grew under it: a validation message appearing, the
   equipment tabs switching to a longer list, the calculator swapping rows. The
   thumb aims where the button was, not where the re-centring just put it.
   Anchored to the top edge, the ✕ has one fixed place in every modal and stays
   there while the content changes; the dialog grows downward into the space it
   has and .modal-scroll takes over when it runs out.
   The safe-area inset is what keeps it clear of a notch in landscape — this
   overlay is only as tall as the visual viewport, so 16px is measured from
   under the browser chrome, not under the status bar.

   The second half of the condition is what this was missing. Keyed to 560px
   alone, it never applied to a phone held sideways — an iPhone in landscape is
   844-932 CSS pixels — so exactly there the dialog was still being centred, in
   a box whose height changes under it every time Safari collapses or restores
   its address bar. A centred dialog re-centres on every one of those changes,
   and the ✕ rides down with it. (hover: none) is the input device, not the
   screen: every touchscreen gets the anchored top, whatever its width or which
   way up it is held. */
@media (max-width: 560px), (hover: none) {
  .modal.is-open {
    align-items: flex-start;
    padding-top: max(16px, env(safe-area-inset-top, 0px));
  }

  /* Полоса под ✕ — «верхняя граница» окна, и вот зачем она.
     Кнопка висит absolute над содержимым, а содержимое под ней прокручивается.
     Пока список в начале, под ✕ оказывается заголовок, который для того и
     отодвинут padding-right'ом — выглядит нормально. Но стоит прокрутить, и
     под кнопкой едут строки таблицы: ✕ садится поверх значения и закрывает
     его собой ("255 Н·м (300…" вместо "255 Н·м (300 Н·м с гибридом)"). Кнопка
     при этом стоит на месте — вниз, в содержимое, её "опускает" сама
     прокрутка.
     Полоса — непрозрачная, во всю ширину рамки, ровно по высоте кнопки с
     отступами, и лежит МЕЖДУ скроллером и ✕ (z-index 1 против 2). Содержимое
     уходит под неё и там пропадает, кнопка остаётся на чистом фоне, а первая
     строка списка начинается уже под полосой — за это отвечает padding-top
     скроллера той же высоты.
     Скругление сверху повторяет рамку: полоса лежит в её углу, и без этого
     угол окна стал бы прямым. */
  .modal-dialog { --modal-head-h: 60px; }
  .modal-dialog::before {
    content: ""; position: absolute; top: 0; left: 0; right: 0; z-index: 1;
    height: var(--modal-head-h);
    background: var(--surface);
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  }
  .modal-dialog--dark::before { background: var(--ink); }
  .modal-scroll { padding-top: var(--modal-head-h); }
  /* Отступ под кнопку заголовку больше не нужен: она теперь не над ним.
     Через .modal-scroll ради веса, а не ради адреса: базовое
     .modal-dialog h3 с его padding-right стоит НИЖЕ по файлу и при равной
     специфичности перебило бы этот сброс. */
  .modal-dialog .modal-scroll h3 { padding-right: 0; }

  /* Две формы — исключение из прижатия к верху: они открываются по центру.
     Верхний якорь заведён для окон с длинным содержимым (комплектация,
     характеристики, калькулятор): они и так во весь экран, и там важно, чтобы
     ✕ был в одном и том же месте. Тест-драйв и заявка на звонок — короткие,
     на экран они занимают едва половину, и прижатые к верху висят с пустотой
     под собой, будто оборвались. По центру они читаются как то, ради чего
     открылись.
     На высоту это не влияет: max-height у окна прежний, и если форме всё же
     не хватит места (открытая клавиатура, мелкий экран), она упрётся в
     границы и прокрутится внутри, как и любое другое окно.
     По id, а не по классу: специфичность (1,2,0) перебивает и правило выше, и
     общий блок ниже по файлу, где .modal.is-open переопределяет padding. */
  #testDriveModal.modal.is-open,
  #callModal.modal.is-open { align-items: center; }
}
/* На тач-экранах ✕ 44px вместо 34px (см. блок (hover: none) ниже) — полоса
   пересчитывается под неё, иначе кнопка вылезала бы за её нижний край. */
@media (hover: none) {
  .modal-dialog { --modal-head-h: 70px; }
}

/* The closing state, and the reason it exists at all.
   Before this, closing was one class removal that put .modal straight back to
   display: none — the dialog did not fade, it blinked out of existence between
   two frames. Nothing about that reads as "the button you pressed did
   something"; it reads as the page glitching, which is exactly the "человек не
   понимает, что он нажимает" complaint. The transitions declared above never
   ran either, in either direction: an element going to or from display: none
   has no start style to animate from.
   --modal-exit is deliberately at the very bottom of what can be seen at all.
   Below ~100ms a fade stops reading as motion and starts reading as a cut, so
   this is the shortest exit that still confirms the press — and it is well
   inside the ~200ms that feels instantaneous. pointer-events: none so a dialog
   on its way out cannot swallow the next tap. */
:root { --modal-exit: 120ms; }
/* Under a thumb even that reads as a beat of hesitation between the tap and the
   window going away — a cursor watches a modal close, a finger has already
   moved on. 70ms is past the point where the fade registers as a distinct
   motion, which is the point: on touch the press state on the ✕ (below) is what
   confirms the tap, so the window itself no longer has to spend time doing it.
   js/main.js reads this value back off the document rather than repeating it,
   so the display: none can't drift away from the animation. */
@media (hover: none) { :root { --modal-exit: 70ms; } }
.modal.is-closing { pointer-events: none; }
.modal.is-closing .modal-backdrop { opacity: 0; transition-duration: var(--modal-exit); }
.modal.is-closing .modal-dialog {
  opacity: 0; transform: translateY(4px) scale(.985);
  transition-duration: var(--modal-exit);
}
/* The catcher for everything the closing tap sends after the window is already
   gone. Closing happens on pointerdown, so the pointerup and the click that
   follow it land on whatever the page has under the finger by then — and the ✕
   and the burger sit in the same top-right corner, which is why closing a modal
   opened the menu. Raised by raiseShield() in js/main.js for as long as the
   finger is down, so those leftovers hit nothing. Invisible and above
   everything, including the toast at z-index 200: it is not UI, it is the
   absence of UI for a moment. Nothing in the page ever styles or shows it. */
.input-shield { position: fixed; inset: 0; z-index: 300; background: transparent; }
/* Absolute against .modal-dialog, which no longer scrolls — so this is a fixed
   14px corner, not an offset into a moving coordinate system. The content
   scrolls underneath it; the button's own background and border are what keep
   it readable while that happens. */
.modal-close {
  position: absolute; top: 14px; right: 14px; z-index: 2;
  width: 34px; height: 34px;
  appearance: none; border: 1px solid var(--border); background: var(--stone); color: var(--ink);
  border-radius: var(--radius-sm); cursor: pointer; font-size: 1.1rem; line-height: 1;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.modal-close:hover { border-color: var(--ink); }
/* The press state. It is a class rather than :active because the button closes
   the dialog on pointerdown — the browser resolves :active a beat later and on
   touch not at all reliably, so by the time it would apply there is nothing
   left to show it on. JS adds this in the same handler that starts the close,
   which paints it in the very frame the finger lands; the dialog then spends
   its (short) exit animation with the button visibly pushed in, and that is the
   whole confirmation that the tap registered. transition: none because the
   colour change belongs to the frame of the press — easing INTO a press state
   defeats the point; the movement is an animation instead (below), which starts
   in that same frame and then has somewhere to go.

   Scoped through .modal-dialog for weight, not for reach: the touch block near
   the end of this file hands out :active transforms to the whole control set,
   and at equal specificity a later rule there would quietly win this one's
   transform while the finger is still down. */
.modal-dialog .modal-close.is-pressing {
  transition: none;
  background: var(--border); border-color: var(--ink); color: var(--ink);
  /* forwards, not a plain transform: the window is gone in ~70ms, so what the
     press has to do is happen INSIDE those 70ms and then hold. The curve is
     front-loaded — most of the travel is in the first frames, so the button is
     visibly in by the time the fade starts — and it settles a hair back out
     instead of sitting at the bottom, which is what a real key does. This is
     the half of the press that happens ON the button, and it gets one clean
     frame at full opacity before the exit begins (js/main.js defers the close
     by a frame for exactly that); .press-echo below is the half that survives
     the button. */
  animation: modalClosePress 90ms cubic-bezier(.2, .8, .3, 1) forwards;
}
.modal-dialog--dark .modal-close.is-pressing {
  background: color-mix(in srgb, var(--surface) 26%, transparent);
  border-color: var(--surface); color: var(--surface);
}
@keyframes modalClosePress {
  0%   { transform: scale(1); }
  55%  { transform: scale(.84); }
  100% { transform: scale(.9); }
}

/* The press ripple, and why it is a free-standing element on the page instead
   of a ::after on the button.
   On the button it could not be seen. The dip and the ring both started in the
   frame of the press — and so did the window's exit, which fades the button out
   in 70ms and then puts it in display: none. The ring had ~4 frames of life
   inside a box that was itself fading to nothing, and both were gone before the
   eye had anything to follow: "анимации нет" was literally true, whatever the
   keyframes said.
   pressEcho() in js/main.js measures the button and drops this at those exact
   coordinates, on the body — so it is not inside the thing that disappears. The
   window can leave at full speed while the ripple keeps expanding over the page
   for another quarter second, out from under the fingertip, at the exact spot
   that was touched. That is the part you actually see, and it is why the close
   no longer looks like the screen simply blinking.
   Sized in code (width/height = the button's own box), centred on the point
   with translate so that the animated scale has the transform to itself. */
.press-echo {
  position: fixed; z-index: 301; pointer-events: none;
  translate: -50% -50%;
  border-radius: 50%; border: 2px solid var(--ink);
  /* The ring spends its first frames over whatever the dialog was — including
     the black test-drive one, where ink on ink is nothing — and the rest over
     the page. The pale hairline outside it is what carries it across that
     handover; on the light page it is simply not noticeable. */
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--surface) 65%, transparent);
  animation: pressEcho 260ms cubic-bezier(.2, .7, .3, 1) forwards;
}
@keyframes pressEcho {
  0%   { opacity: .5; scale: .85; }
  100% { opacity: 0; scale: 1.85; }
}
/* The button is out of the flow again, so the heading has to keep its own
   clearance the plain way. 48px = 34 button + 14 gap; the touch block near the
   end of the file widens the button and this with it. */
.modal-dialog h3 { font-size: 1.25rem; font-weight: 800; padding-right: 48px; }
.modal-dialog .form { margin-top: 20px; }
.modal-dialog:not(.modal-dialog--dark) .form-field input[type="text"], .modal-dialog:not(.modal-dialog--dark) .form-field input[type="tel"] {
  border: 1px solid var(--border); background: var(--stone); color: var(--ink);
}
.modal-dialog:not(.modal-dialog--dark) .form-field input::placeholder { color: var(--graphite); }
.modal-dialog:not(.modal-dialog--dark) .form-status.ok { color: var(--mint-ink); }
.modal-dialog:not(.modal-dialog--dark) .form-consent { color: var(--ink-soft); }
/* Set while the nav panel or a modal is open.

   On <body> ONLY, and that is the whole point. While <html> keeps overflow:
   visible, the body's overflow propagates to the viewport — the viewport stops
   scrolling while body itself stays a normal, non-scrolling box, so the sticky
   topbar and header go on resolving against the viewport and stay where they
   are. Add overflow: hidden to <html> as well and that propagation stops:
   body becomes a real scroll container, the sticky pair starts sticking to it
   instead, and since body is never scrolled they snap back to their static
   place at the top of the document — off-screen, taking the header and its ✕
   with them the moment the menu is opened anywhere but the top of the page.

   See main.js for why the position: fixed variant of this lock is wrong here
   for the same reason. */
/* overscroll-behavior — вторая половина того же замка, и она про крестик.
   overflow: hidden останавливает прокрутку страницы, но не её «резинку»: у
   верхнего края экран всё равно можно оттянуть, а на время оттяжки Safari
   тащит за страницей ВСЕ position: fixed элементы — то есть весь оверлей с
   окном и крестиком. Кнопка при этом на своём месте, под ней уезжает экран,
   и выглядит это ровно как «крестик опускается». none гасит саму оттяжку,
   пока открыто окно; в обычной жизни страница пружинит как пружинила.
   На body, а не на html: отсюда значение распространяется на вьюпорт — тем
   же путём, что и overflow выше. */
body.scroll-locked { overflow: hidden; overscroll-behavior: none; }

/* The back-to-top button lives in the root stacking context at z-index 50,
   while .nav is trapped inside .site-header's at 40 — so it floated over the
   open menu. It has nothing to do while the page cannot scroll anyway. */
body.scroll-locked .to-top { opacity: 0; pointer-events: none; }

/* Dark variant — used by the test-drive modal so it lands in the same black
   style as the "Записаться на тест-драйв" card at the bottom of the page,
   instead of the light dialog every other modal uses. */
.modal-dialog--dark { background: var(--ink); color: var(--surface); }
.modal-dialog--dark .modal-close {
  background: color-mix(in srgb, var(--surface) 12%, transparent);
  border-color: color-mix(in srgb, var(--surface) 22%, transparent);
  color: var(--surface);
}
.modal-dialog--dark .modal-close:hover { border-color: var(--surface); }
.modal-dialog--dark .btn-primary { background: var(--mint); color: var(--mint-ink); }
.modal-dialog--dark .btn-primary:hover { box-shadow: 0 12px 22px -12px rgba(198,138,46,.5); }
.modal-dialog--dark .form-status.ok { color: var(--mint); }

/* ---------- interior (bento grid: one large shot + four detail shots) ---------- */
.interior-head { position: relative; max-width: 720px; margin-bottom: 32px; overflow: hidden; }
/* Big outline model-name watermark behind the heading — pure typography, no
   image asset. -webkit-text-stroke keeps it a hairline outline rather than a
   solid block so it reads as texture, not a competing headline. */
.interior-watermark {
  position: absolute; top: -0.3em; left: -0.03em; z-index: 0;
  font-family: var(--font-display); font-weight: 800; font-size: clamp(5rem, 14vw, 11rem);
  line-height: 1; letter-spacing: -0.02em; white-space: nowrap;
  color: transparent; -webkit-text-stroke: 1px color-mix(in srgb, var(--ink) 8%, transparent);
  pointer-events: none; user-select: none;
}
.interior-head .eyebrow, .interior-head h2, .interior-head > p { position: relative; z-index: 1; }
.interior-head > p { color: var(--ink-soft); margin-top: 14px; font-size: 1.02rem; max-width: 60ch; }

.interior-facts { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 12px; margin-bottom: 24px; }
@media (max-width: 720px) { .interior-facts { grid-template-columns: repeat(2, minmax(0,1fr)); } }
.interior-fact {
  text-align: center; padding: 16px 10px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md);
}
.interior-fact .val { display: block; font-family: var(--font-display); font-weight: 800; font-size: 1.15rem; color: var(--mint-ink); }
.interior-fact .label { display: block; margin-top: 4px; font-size: 0.72rem; color: var(--ink-soft); }

/* Same 4-column track + gap as .interior-facts above, so the grid lines up
   exactly: media spans the first 3 columns, the side card takes the 4th —
   same width as the "FWD / AWD" fact box, and (default align-items: stretch)
   the same height as the photo column next to it. */
.interior-layout { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.interior-side { grid-column: span 1; }
@media (max-width: 960px) {
  .interior-layout { grid-template-columns: 1fr; }
  .interior-media, .interior-side { grid-column: auto; }
}

/* Four equal-size detail shots, plus one wide dashboard shot below — that
   photo is natively ~16:9 (1440×803 on every model), so the strip box uses
   that same ratio instead of a more aggressive one: at 2.6:1 the box was
   noticeably shorter than the source photo, and object-fit: cover had to
   crop ~30% off the top to compensate — fine on X70, where that top slice is
   mostly overexposed windshield, but on S50 it sliced straight through the
   top of the steering wheel and instrument cluster. 16:9 keeps the crop to
   what's needed to fill a rectangle instead of manufacturing one. */
.interior-media { grid-column: span 3; display: flex; flex-direction: column; gap: 16px; }
.interior-grid {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px;
}
@media (max-width: 860px) { .interior-grid { grid-template-columns: repeat(2, 1fr); } }

.interior-card { position: relative; border-radius: var(--radius-lg); overflow: hidden; background: var(--stone); }
.interior-card .shot { border: none; border-radius: 0; aspect-ratio: 4 / 3; }
.interior-strip .shot { aspect-ratio: 16 / 9; }
.interior-strip .shot img { object-position: center bottom; }
/* Caption overlays the photo (gradient scrim) instead of sitting in its own
   box below it — the shot stays in normal flow so it still sizes the card;
   the caption is just positioned over its bottom edge. */
.interior-card figcaption {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 34px 16px 14px;
  background: linear-gradient(180deg, transparent, rgba(15,16,17,.82) 80%);
  color: var(--surface); font-weight: 700; font-size: 0.84rem; line-height: 1.35;
}

/* ---------- interior on a phone: description, photos, captions ----------
   The two standalone text blocks around the gallery are dropped — the four
   fact tiles and the "Что внутри" panel. The captions stay: they belong to the
   photos, naming what each one shows, which is different from repeating specs
   in a tile of their own.

   Placed below every .interior-* base rule on purpose. Each selector here is
   the same specificity as the one it overrides, so source order is the only
   thing deciding it — sitting above them, `display: none` would lose, and the
   caption sizing below simply would not apply. */
@media (max-width: 560px) {
  .interior-facts, .interior-side { display: none; }

  /* Bento re-cut for two columns: the two scene shots (screen, second row) run
     full width at 16:9, the two close details pair up beside them.
     Squares rather than 4:3 for that pair, because of the captions: at ~160px
     wide a 4:3 box is only ~120px tall, and the longest caption here needs
     three lines of scrim, which would cover most of the photo under it. A
     square gives it ~160px to sit against and holds the same text in two. */
  .interior-media { gap: 12px; }
  .interior-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
  .interior-grid .interior-card:nth-child(1),
  .interior-grid .interior-card:nth-child(4) { grid-column: span 2; }
  .interior-grid .interior-card:nth-child(1) .shot,
  .interior-grid .interior-card:nth-child(4) .shot { aspect-ratio: 16 / 9; }
  .interior-grid .interior-card:nth-child(2) .shot,
  .interior-grid .interior-card:nth-child(3) .shot { aspect-ratio: 1 / 1; }

  .interior-card figcaption { padding: 24px 12px 10px; font-size: 0.72rem; }
}

.interior-side {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: 26px 24px;
}
.interior-side-label { font-size: 0.72rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em; color: var(--graphite); margin-bottom: 4px; }
.interior-side-list li {
  position: relative; padding: 14px 0 14px 16px; border-top: 1px solid var(--border);
  font-size: 0.9rem; line-height: 1.4;
}
.interior-side-list li::before {
  content: ""; position: absolute; left: 0; top: 20px; width: 6px; height: 6px; border-radius: 50%; background: var(--mint);
}
.interior-side-list li:last-child { padding-bottom: 0; }

/* Once the layout stacks, .interior-side stops being a side card: it lands
   full width, under five photos, where five short lines written for a narrow
   column read as something left over rather than a designed block.

   Two changes. It moves ahead of the gallery, so the section runs heading →
   numbers → equipment → photos and closes on the images, which are the whole
   point of an "Интерьер" section; a list of safety and seating specs is what
   you read before looking, not after. And it takes the site's dark-panel
   treatment — the same one .calc-result and the test-drive card use — because
   at full width the block needs to be deliberate contrast against the white
   fact boxes above it, not a stretched sidebar in the same white as everything
   else.

   This block MUST stay below the .interior-side rules above: every selector
   here is single-class, i.e. the same specificity as its counterpart, so
   source order is the only thing deciding the winner. Sitting above them, the
   dark background lost to the base `background: var(--surface)` while the
   white `color` — which the base rule never sets, so nothing overrode it —
   still applied. White on white: the list was invisible. */
@media (max-width: 960px) {
  .interior-side {
    order: -1;
    background: var(--ink); border-color: var(--ink); color: var(--surface);
    padding: 22px 20px;
    box-shadow: 0 28px 52px -32px rgba(27,28,30,.42);
  }
  .interior-side-label { color: var(--mint); margin-bottom: 10px; }
  .interior-side-list li {
    border-top-color: color-mix(in srgb, var(--surface) 16%, transparent);
    padding: 13px 0 13px 18px; font-size: 0.88rem;
  }
  .interior-side-list li::before { top: 19px; background: var(--mint); }
}

/* ---------- color picker (body-color swatch gallery) ---------- */
.color-picker { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr); gap: 28px; align-items: center; max-width: 980px; }
@media (max-width: 760px) { .color-picker { grid-template-columns: 1fr; } }
.color-picker__photo {
  border-radius: var(--radius-lg); border: 1px solid var(--border); background: var(--surface);
  box-shadow: 0 24px 48px -34px rgba(27,28,30,.22);
}
/* Product cutouts already have their own white padding baked in — cover
   would zoom past that and crop the car, so contain + a touch of padding. */
.color-picker__photo img { object-fit: contain; padding: 16px; transition: opacity var(--dur-fast) var(--ease); }
.color-picker__panel { display: flex; flex-direction: column; gap: 16px; }
.color-picker__caption { font-size: 0.72rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em; color: var(--graphite); }
.color-picker__current { font-family: var(--font-display); font-weight: 800; font-size: 1.3rem; }
.color-picker__swatches { display: flex; flex-wrap: wrap; gap: 12px; }

/* Hero variant: the picker took the static hero photo's place, so it stacks
   inside the hero's half-width column — car on top, caption + swatches under
   it — instead of the side-by-side pair the standalone section could afford.
   The tighter gaps and smaller colour name keep the whole block inside the
   height the photo used to occupy, so the text column beside it still lines
   up and the calculator card stays in the first screenful. */
.color-picker--hero { grid-template-columns: minmax(0, 1fr); gap: 18px; max-width: none; align-items: start; }
.color-picker--hero .color-picker__panel { gap: 8px; }
.color-picker--hero .color-picker__current { font-size: 1.1rem; }
.color-picker--hero .color-picker__swatches { margin-top: 4px; }
/* 36px is a comfortable cursor target and a marginal thumb one. In the hero the
   swatches are the first control on the page, and on a phone they sit above the
   fold with nothing else to compete for the tap — so they go up to the ~44px a
   fingertip reliably lands on. Even S50's 11 colours still take two rows. */
@media (max-width: 560px) {
  .color-picker--hero .color-swatch { width: 44px; height: 44px; }
}
.color-swatch {
  position: relative; width: 36px; height: 36px; border-radius: 50%; flex: none;
  background: var(--swatch); border: 2px solid var(--surface);
  box-shadow: 0 0 0 1px var(--border);
  cursor: pointer; padding: 0;
  transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.color-swatch:hover { transform: translateY(-2px) scale(1.08); }
.color-swatch.is-active { box-shadow: 0 0 0 1px var(--border), 0 0 0 3px var(--surface), 0 0 0 5px var(--mint); }

/* ---------- spec table ---------- */
.spec-table { width: 100%; border-collapse: collapse; }
.modal-dialog .spec-table { margin-top: 18px; }
.modal-dialog .spec-table td { font-size: 0.9rem; }
.spec-table td { padding: 16px 0; border-top: 1px solid var(--border); font-size: 0.96rem; }
.spec-table tr:first-child td { border-top: none; }
.spec-table td:first-child { color: var(--ink-soft); }
.spec-table td:last-child { text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; }

/* ---------- CTA / form ---------- */
.form-row { display: flex; flex-direction: column; gap: 12px; }
.form-field input[type="text"], .form-field input[type="tel"] {
  width: 100%; padding: 14px 16px; border-radius: var(--radius-sm);
  border: 1px solid color-mix(in srgb, var(--surface) 25%, transparent);
  background: color-mix(in srgb, var(--surface) 8%, transparent);
  color: var(--surface); font-weight: 600;
}
.form-field input::placeholder { color: color-mix(in srgb, var(--surface) 55%, transparent); }
.form-consent { display: flex; align-items: flex-start; gap: 10px; font-size: 0.78rem; color: color-mix(in srgb, var(--surface) 65%, transparent); }
.form-consent input { margin-top: 3px; accent-color: var(--mint); }
/* Ловушка для спам-ботов (honeypot): поле, которого человек не видит, а
   автоматический бот заполняет вместе со всеми остальными — php/crm.php
   отбрасывает такую заявку.

   Уводим за край экрана, а не прячем display:none — многие боты уже умеют
   пропускать поля, скрытые очевидным способом. aria-hidden в разметке держит
   его невидимым и для скринридеров, а tabindex="-1" — недостижимым с
   клавиатуры, так что для живых посетителей поля не существует. */
.form-hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.form-status { font-size: 0.86rem; font-weight: 700; display: none; }
.form-status.is-visible { display: block; }
.form-status.ok { color: var(--mint); }
.form-status.error { color: var(--danger); animation: formStatusPop 0.22s var(--ease); }
@keyframes formStatusPop {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Invalid-field nudge (missing phone, unchecked consent): a red ring/shake
   instead of just a color change, since that's easy to miss on a small
   checkbox or a field that's off-screen when the message appears.
   `label.form-consent.invalid` (not just `.form-consent.invalid`) so this
   reliably outranks `.contact-card--ghost .form-consent`'s own color rule
   regardless of each rule's position in the file. */
label.form-consent.invalid { color: var(--danger); animation: formFieldShake 0.4s var(--ease); }
label.form-consent.invalid input[type="checkbox"] {
  outline: 2px solid var(--danger); outline-offset: 3px; border-radius: 3px;
  animation: formFieldPing 1s var(--ease);
}
.form-field input.invalid {
  outline: 2px solid var(--danger); outline-offset: 2px;
  animation: formFieldShake 0.4s var(--ease);
}
@keyframes formFieldShake {
  20%, 60% { transform: translateX(-5px); }
  40%, 80% { transform: translateX(5px); }
}
@keyframes formFieldPing {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--danger) 45%, transparent); }
  100% { box-shadow: 0 0 0 8px transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .form-status.error, label.form-consent.invalid, label.form-consent.invalid input[type="checkbox"], .form-field input.invalid { animation: none; }
}

/* ---------- chip group (a single "pick one" row: model, callback time…) ---------- */
/* Built entirely off currentColor instead of separate light/dark rules — it
   drops into the black test-drive card/modal and the light "ghost" callback
   card/modal alike and always reads correctly, with the same gold accent the
   rest of the site uses for the active/selected state. The test-drive and
   callback forms each get their own field built from this (model / preferred
   callback time) so the two stay the same shape and size without either
   copying the other's actual question. */
.form-field-label {
  display: block; font-size: 0.74rem; font-weight: 800; text-transform: uppercase;
  letter-spacing: 0.05em; margin-bottom: 10px; color: color-mix(in srgb, currentColor 65%, transparent);
}
.chip-group { display: flex; flex-wrap: wrap; gap: 8px; }
.chip-pill {
  appearance: none; cursor: pointer; font-family: var(--font-display); font-weight: 700; font-size: 0.84rem;
  padding: 9px 15px; border-radius: var(--radius-sm); color: inherit;
  border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
  background: color-mix(in srgb, currentColor 6%, transparent);
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.chip-pill:hover { border-color: currentColor; }
.chip-pill[aria-pressed="true"] { background: var(--mint); border-color: var(--mint); color: var(--mint-ink); }

/* The model pills need ~288px of flex row, which is one pixel more than the
   ~287px a contact card has inside a 375px phone — so "S50" tipped onto a
   second line on exactly the most common screen width (and stayed on one line
   at 390px and up, which is why it looks intermittent). A grid makes fitting
   deterministic instead of a coin-flip on string width, and squares the pills
   up into an even row while it's at it.

   Sized by content, not in equal columns. Equal columns are tidier, but they
   force the longest label to fit inside one quarter of the row: at 375px that
   is a ~63px cell for "X50 Plus", which the text filled almost exactly, so it
   read as jammed against its own border. Letting the pills take their natural
   widths and share the leftover space (flex: 1 1 auto) gives that one pill a
   wider box than the three short ones and puts real padding back around the
   label, while the row still fills its full width.

   nowrap on the group is what actually guarantees the single row; min-width/
   overflow on the pill are the safety net that keeps a longer label shrinking
   its own box instead of pushing the row wide again. Above 420px the default
   wrapping flex row has room to spare and reads better at its natural
   spacing. */
@media (max-width: 420px) {
  .chip-group { flex-wrap: nowrap; gap: 6px; }
  .chip-pill {
    flex: 1 1 auto; font-size: 0.7rem; padding: 9px 10px;
    text-align: center; white-space: nowrap; overflow: hidden; min-width: 0;
  }
}
/* 320-380px: the same tier the calculator's trim buttons get, for the same
   reason — the row is down to ~232px of usable width here, so the type comes
   with it. Note this tier is what a 375px iPhone actually gets. */
@media (max-width: 380px) {
  .chip-group { gap: 5px; }
  .chip-pill { font-size: 0.64rem; padding: 8px 7px; }
}

/* ---------- toast (submit confirmation) ---------- */
/* Submitting a form never locks it — it resets and stays usable (someone who
   just requested a callback may want to book a test drive next), so the
   "sent" confirmation lives here instead of replacing the form in place. */
.toast {
  position: fixed; top: 16px; left: 50%; z-index: 200;
  display: flex; align-items: center; gap: 10px;
  background: #1f9d55; color: var(--surface);
  padding: 14px 20px; border-radius: var(--radius-pill);
  font-size: 0.88rem; font-weight: 700;
  box-shadow: 0 20px 40px -16px rgba(15,85,45,.5);
  opacity: 0; pointer-events: none;
  transform: translate(-50%, -12px);
  transition: opacity var(--dur-base) var(--ease), transform var(--dur-base) var(--ease);
}
.toast.is-visible { opacity: 1; transform: translate(-50%, 0); }
.toast::before {
  content: ""; flex: none; width: 18px; height: 18px; border-radius: 50%;
  background: var(--surface);
  -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.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12l5 5L19 7'/%3E%3C/svg%3E") center/12px 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.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12l5 5L19 7'/%3E%3C/svg%3E") center/12px no-repeat;
}
@media (max-width: 480px) { .toast { left: 16px; right: 16px; transform: translateY(-12px); width: auto; } .toast.is-visible { transform: translateY(0); } }

/* ---------- contact (map + dual CTA cards) ---------- */
/* align-items: stretch (the grid default) so the map column always matches
   the cards column's full height instead of sitting at its own fixed 400px —
   without this the shorter side left a slab of bare background at the
   bottom, and the two columns' bottom edges never lined up. */
.contact-layout { display: grid; grid-template-columns: 1.1fr 0.9fr; gap: 24px; }
@media (max-width: 900px) { .contact-layout { grid-template-columns: 1fr; } }

.contact-map {
  border-radius: var(--radius-lg); overflow: hidden; border: 1px solid var(--border);
  box-shadow: 0 28px 52px -32px rgba(27,28,30,.28);
  min-height: 400px;
}
.contact-map iframe { display: block; width: 100%; height: 100%; min-height: 400px; }

.contact-forms { display: flex; flex-direction: column; gap: 20px; }
.contact-card { border-radius: var(--radius-lg); padding: clamp(24px, 4vw, 32px); transition: transform var(--dur-base) var(--ease); }
.contact-forms .contact-card:hover { transform: translateY(-4px); }
.contact-card h3 { font-family: var(--font-display); font-weight: 800; font-size: 1.2rem; }
.contact-card > p { margin-top: 8px; font-size: 0.88rem; }
.contact-card form { margin-top: 18px; }

.contact-card:not(.contact-card--ghost) {
  background: var(--ink); color: var(--surface);
  box-shadow: 0 28px 52px -32px rgba(27,28,30,.4);
}
.contact-card:not(.contact-card--ghost) > p { color: color-mix(in srgb, var(--surface) 70%, transparent); }
/* .btn-primary is var(--ink) on var(--ink) elsewhere on the site's own dark
   panels too — invisible on this card's identical background. Swap it to the
   gold used for primary actions inside dark contexts (see .calc-result). */
.contact-card:not(.contact-card--ghost) .btn-primary { background: var(--mint); color: var(--mint-ink); }
.contact-card:not(.contact-card--ghost) .btn-primary:hover { box-shadow: 0 12px 22px -12px rgba(198,138,46,.5); }

/* ---------- gold CTA: readable label + a press you can see ----------
   Three places use the same gold button on a dark panel: the calculator's
   "Оставить заявку", the dark modal's submit and the contact card's. All of
   them wrote their label in var(--mint-ink) — gold mixed into black, which is
   fine as text ON white but on the gold fill itself lands a shade off the
   background and reads as faded, half-transparent type on a dead control.
   Black is the only label that carries at this fill. The fill is untouched.

   The other half of "it doesn't feel like a button": the white .btn-secondary
   answers a press with a border + background swap you cannot miss, while this
   one had only a gold glow — cast on a nearly black panel, so invisible. Same
   order of feedback here without touching the colour: a lift under a cursor,
   a dip on the press itself. The dip is deliberately outside the (hover: none)
   block at the end of the file, so a mouse gets it too and not just a thumb —
   with :hover alone there was nothing at all happening during the click. */
.calc-result .btn-primary,
.modal-dialog--dark .btn-primary,
.contact-card:not(.contact-card--ghost) .btn-primary {
  color: var(--ink);
  transition: box-shadow var(--dur-base) var(--ease), background var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.calc-result .btn-primary:hover,
.modal-dialog--dark .btn-primary:hover,
.contact-card:not(.contact-card--ghost) .btn-primary:hover {
  transform: translateY(-2px);
  box-shadow: 0 14px 26px -12px rgba(198,138,46,.65);
}
.calc-result .btn-primary:active,
.modal-dialog--dark .btn-primary:active,
.contact-card:not(.contact-card--ghost) .btn-primary:active {
  transform: translateY(0) scale(.97);
  box-shadow: 0 6px 14px -12px rgba(198,138,46,.5);
  transition-duration: 0s;
}

.contact-card--ghost {
  background: var(--surface); border: 1px solid var(--border); color: var(--ink);
  box-shadow: 0 24px 48px -34px rgba(27,28,30,.18);
}
.contact-card--ghost > p { color: var(--ink-soft); }
.contact-card--ghost .form-field input[type="text"], .contact-card--ghost .form-field input[type="tel"] {
  background: var(--stone); border-color: var(--border); color: var(--ink);
}
.contact-card--ghost .form-field input::placeholder { color: var(--graphite); }
.contact-card--ghost .form-consent { color: var(--ink-soft); }

/* Desktop shows both cards side by side with the map, so the segmented control
   has nothing to do — main.js still keeps its state in sync, it just isn't
   drawn. Declared outside the media query so it's the default, not an
   override. */
.contact-switch { display: none; }

/* ---------- contact: stacked (phone / tablet) ---------- */
/* Once the two columns collapse, source order puts a 400px slab of map above
   both forms — the visitor scrolls past the thing they can't act on to reach
   the thing they can. Ordering the map last is the whole point of this block;
   it applies from the same 900px the layout stacks at, because the problem
   starts exactly there and is no better on a tablet. */
@media (max-width: 900px) {
  .contact-forms { order: 1; }
  .contact-map { order: 2; }
}

/* Below 700px the two cards become one long, near-duplicate wall of name /
   phone / consent — the same fields twice, ~760px of scroll, with nothing
   marking where one request ends and the other begins. The segmented control
   turns that into two clearly separate choices showing one form at a time:
   what a carousel would have done, minus the swiping that fights a form. */
@media (max-width: 700px) {
  .contact-layout { gap: 28px; }

  .contact-switch {
    display: grid; grid-template-columns: 1fr 1fr; gap: 4px;
    padding: 4px; background: var(--stone);
    border: 1px solid var(--border); border-radius: var(--radius-pill);
  }
  .contact-switch-btn {
    appearance: none; border: 0; background: transparent; color: var(--ink-soft);
    font-family: var(--font-body); font-weight: 800; font-size: 0.86rem;
    padding: 11px 8px; border-radius: var(--radius-pill); cursor: pointer;
    transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
  }
  .contact-switch-btn[aria-pressed="true"] { background: var(--ink); color: var(--surface); }

  .contact-forms .contact-card:not(.is-active) { display: none; }
  /* Both cards carry .reveal, i.e. opacity:0 until the observer in main.js
     sees them scroll into view — but a display:none card never intersects
     anything, so the one behind the switch would have unhidden into a fully
     transparent form. Opting the visible card out of the reveal entirely is
     the deterministic fix; relying on IntersectionObserver to re-fire on a
     display change would be betting the form's visibility on a timing detail.
     Outranks .reveal.is-visible's own transform at (0,3,0) vs (0,2,0). */
  .contact-forms .contact-card.is-active { opacity: 1; transform: none; }
  /* A -4px lift is a pointer affordance; on touch it just sticks after a tap. */
  .contact-forms .contact-card:hover { transform: none; }

  /* Gives the map its own titled card instead of leaving it as an unlabelled
     slab at the bottom of the section — ::before rather than markup so the
     desktop grid item stays exactly the single element it is today. The strip
     paints inside .contact-map's existing border-radius + overflow:hidden, so
     it reads as the card's header. */
  /* Holds the space the widget will take (≈45px header strip + the 320px
     iframe below). The map is injected on approach rather than at load, so
     without a reserved height the box would be 45px tall until the script
     arrives and then shove the footer down — a layout shift landing exactly
     where the visitor is scrolling. */
  .contact-map { min-height: 365px; }
  .contact-map::before {
    content: "Как нас найти";
    display: block; padding: 13px 16px;
    /* --stone, not --surface: this section is .section.alt, which is already
       white, so a --surface strip would vanish into the page. */
    background: var(--stone); border-bottom: 1px solid var(--border);
    font-size: 0.72rem; font-weight: 800; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--graphite);
  }
  /* height, not just min-height: the widget's own height=400 URL param comes
     through as an inline attribute on the injected iframe, which min-height
     alone wouldn't bring back down. */
  .contact-map iframe { height: 320px; min-height: 0; }
}

/* ---------- footer ---------- */
.site-footer { border-top: 1px solid var(--border); padding-block: 32px; background: var(--surface); }
.footer-top {
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px;
  padding-bottom: 24px; margin-bottom: 24px; border-bottom: 1px solid var(--border);
}
.footer-logo { font-family: var(--font-display); font-weight: 800; font-size: 1.05rem; letter-spacing: 0.02em; }
.footer-bottom { display: flex; flex-direction: column; gap: 16px; }
.footer-bottom-row { display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; gap: 10px; }
.footer-copy { font-size: 0.82rem; font-weight: 700; color: var(--ink-soft); }
.footer-legal-links { display: flex; gap: 18px; flex-wrap: wrap; }
.footer-legal-links a { font-size: 0.78rem; color: var(--ink-soft); }
.footer-legal-links a:hover { color: var(--ink); }
.footer-disclaimer { font-size: 0.74rem; line-height: 1.6; color: var(--graphite); max-width: 92ch; }

/* ---------- steps ("Как купить") ---------- */
.steps-grid { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 20px; counter-reset: step; }
@media (max-width: 860px) { .steps-grid { grid-template-columns: 1fr; } }
.step-card {
  position: relative;
  padding: 32px 28px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
  transition: transform var(--dur-base) var(--ease), border-color var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease);
}
/* .steps-grid prefix bumps specificity past .reveal.is-visible's own
   transform rule (see the .pay-grid comment above for why that's needed). */
.steps-grid .step-card:hover { transform: translateY(-6px); border-color: color-mix(in srgb, var(--mint) 45%, var(--border)); box-shadow: 0 28px 52px -32px rgba(27,28,30,.32); }
/* Big outline step number tucked in the corner, same device as the interior
   section's model watermark — keeps the numbering legible without a literal
   "1/2/3" badge competing with the action icon below it. */
.step-card__ghost {
  position: absolute; top: 6px; right: 10px; z-index: 0;
  font-family: var(--font-display); font-weight: 800; font-size: 5rem; line-height: 1;
  color: transparent; -webkit-text-stroke: 1px color-mix(in srgb, var(--ink) 8%, transparent);
  pointer-events: none; user-select: none;
}
.step-card__icon {
  position: relative; z-index: 1;
  display: inline-flex; align-items: center; justify-content: center; width: 46px; height: 46px; border-radius: 50%;
  background: var(--mint-tint); color: var(--mint-ink); margin-bottom: 18px;
  transition: transform var(--dur-base) var(--ease);
}
.step-card__icon svg { width: 22px; height: 22px; }
.step-card:hover .step-card__icon { transform: scale(1.1); }
.step-card h3, .step-card p { position: relative; z-index: 1; }
.step-card h3 { font-size: 1.05rem; font-weight: 800; }
.step-card p { margin-top: 8px; font-size: 0.88rem; color: var(--ink-soft); line-height: 1.5; }
/* Chevron floating in the grid gap, hinting at the 1→2→3 flow — a border-only
   shape rather than a "→" glyph, since that character renders as a thin,
   barely-there stroke in the display font at this size. Centered on the
   20px .steps-grid gap (right: -15px puts its own center ~10px out, matching
   half the gap) — offset much past that and the next card's opaque
   background paints over it, cropping the chevron in half. Desktop only, no
   gap to float it in once the grid collapses to one column. */
@media (min-width: 861px) {
  .step-card:not(:last-child)::before {
    content: ""; position: absolute; top: 50%; right: -15px; z-index: 2;
    width: 10px; height: 10px;
    border-top: 3px solid var(--mint); border-right: 3px solid var(--mint);
    border-radius: 0 2px 0 0;
    transform: translateY(-50%) rotate(45deg);
  }
}
/* Same chevron once the grid collapses, turned a further 90° to point down the
   column and floated in the vertical gap instead of the horizontal one. Without
   it the stacked cards read as three unrelated boxes — the 01/02/03 watermarks
   are the only thing saying they're a sequence, and they're deliberately faint.
   The gap widens to 26px here purely to give the arrow air; bottom: -19px puts
   the 12px shape's centre exactly halfway down it. Rotating a bordered square
   grows its bounding box to ~17px, which still clears the gap on both sides,
   so neither card's opaque background crops it. */
@media (max-width: 860px) {
  .steps-grid { gap: 26px; }
  .step-card:not(:last-child)::before {
    content: ""; position: absolute; z-index: 2;
    left: 50%; bottom: -19px;
    width: 12px; height: 12px;
    border-top: 3px solid var(--mint); border-right: 3px solid var(--mint);
    border-radius: 0 2px 0 0;
    transform: translateX(-50%) rotate(135deg);
  }
}

/* Step 03 ends on "оформите покупку", which the visitor can only act on in the
   contact form. On desktop that form is the next thing on screen; stacked on a
   phone it is three full-height cards further down, with nothing at the end of
   the flow pointing at it — so the sequence just stops. This closes it.
   Hidden by default rather than inside the media query so desktop, where the
   section already ends beside the form, is the explicit default.

   Carries none of the .btn classes: a round icon-only control shares almost
   nothing with them but the transition, and overriding .btn's padding and
   radius costs more than styling it outright. No visible text means the
   accessible name has to come from the aria-label on the anchor. */
.steps-cta { display: none; }
@media (max-width: 860px) {
  .steps-cta {
    display: flex; align-items: center; justify-content: center;
    /* auto margins centre it under the column. 40px of clearance rather than
       the 26px grid gap: with no chevron tying card 03 to it, the button needs
       to read as the section signing off, not as a fourth item in the run. */
    width: 42px; height: 42px; margin: 40px auto 0;
    border-radius: 50%; background: var(--ink); color: var(--surface);
    box-shadow: var(--round-btn-shadow);
    transition: box-shadow var(--dur-base) var(--ease), background var(--dur-fast) var(--ease);
  }
  .steps-cta:hover, .steps-cta:active { background: var(--mint-ink); box-shadow: var(--round-btn-shadow-on); }
  /* The press nudge lives on the svg, not the button: .reveal.is-visible sets
     its own transform at equal specificity further down this file, so a
     transform on .steps-cta itself would silently lose to it once revealed. */
  .steps-cta svg { width: 17px; height: 17px; transition: transform var(--dur-fast) var(--ease); }
  .steps-cta:hover svg, .steps-cta:active svg { transform: translateY(3px); }
}

.steps-grid .step-card.reveal:nth-child(1) { transition-delay: 0ms; }
.steps-grid .step-card.reveal:nth-child(2) { transition-delay: 100ms; }
.steps-grid .step-card.reveal:nth-child(3) { transition-delay: 200ms; }

/* ---------- related models ----------
   Reuses .model-card (the homepage model grid's photo cards, see
   ---------- models grid ---------- above) rather than its own plain
   text-row style, so "other models" reads as the same premium product card
   everywhere on the site instead of a second, plainer component. */
/* auto-fit по той же причине, что и у .model-grid выше: скрытая через
   config.js модель исчезает и отсюда, и оставшиеся карточки должны заполнить
   ряд, а не оставить пустую колонку. */
.related-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; }
@media (max-width: 720px) { .related-grid { grid-template-columns: 1fr; } }

/* Same swipeable-on-mobile treatment as the homepage model grid and the
   payment cards — see the .model-grid.bcarousel__viewport block above for why
   the grid has to be reasserted on the two-class pair (the generic
   .bcarousel__viewport display:flex rule sits later in the file and would
   otherwise win on every screen size by source order).

   The cards here are injected by model-page.js, not written in the HTML, but
   initCarousel() reads the viewport's children at call time and main.js is the
   last script on the page — so its DOMContentLoaded handler runs after
   model-page.js has already appended them. */
.related-grid.bcarousel__viewport { display: grid; overflow: visible; border-radius: 0; }

@media (max-width: 560px) {
  .bcarousel--related .bcarousel__viewport {
    display: flex; overflow-x: auto; scroll-snap-type: x mandatory; scroll-behavior: auto;
    -webkit-overflow-scrolling: touch; scrollbar-width: none; gap: 12px; padding-right: 32px;
  }
  .bcarousel--related .bcarousel__viewport::-webkit-scrollbar { display: none; }
  .bcarousel--related .model-card { flex: 0 0 85%; scroll-snap-align: start; }
  .bcarousel--related .bcarousel__dots { display: flex; }
}
.bcarousel--related .bcarousel__dots { display: none; }

/* ---------- reveal ---------- */
.reveal { opacity: 0; transform: translateY(22px); transition: opacity var(--dur-slow) var(--ease), transform var(--dur-slow) var(--ease); }
.reveal.is-visible { opacity: 1; transform: translateY(0); }

/* Grids reveal item-by-item instead of all at once — each card's transition-delay
   is staggered by its position, so the eye reads the group as a single wave. */
.model-grid .model-card.reveal:nth-child(1) { transition-delay: 0ms; }
.model-grid .model-card.reveal:nth-child(2) { transition-delay: 80ms; }
.model-grid .model-card.reveal:nth-child(3) { transition-delay: 160ms; }
.model-grid .model-card.reveal:nth-child(4) { transition-delay: 240ms; }
.pay-grid .pay-card.reveal:nth-child(1) { transition-delay: 0ms; }
.pay-grid .pay-card.reveal:nth-child(2) { transition-delay: 80ms; }
.related-grid .model-card.reveal:nth-child(1) { transition-delay: 0ms; }
.related-grid .model-card.reveal:nth-child(2) { transition-delay: 80ms; }
.related-grid .model-card.reveal:nth-child(3) { transition-delay: 160ms; }
.why-list .why-item.reveal:nth-child(1) { transition-delay: 0ms; }
.why-list .why-item.reveal:nth-child(2) { transition-delay: 60ms; }
.why-list .why-item.reveal:nth-child(3) { transition-delay: 120ms; }
.why-list .why-item.reveal:nth-child(4) { transition-delay: 180ms; }
.why-list .why-item.reveal:nth-child(5) { transition-delay: 240ms; }
.why-list .why-item.reveal:nth-child(6) { transition-delay: 300ms; }

/* ---------- value pulse (reused for calculator numbers on change) ---------- */
.pulse { animation: calcModePulse 0.7s var(--ease); }

/* ---------- header scroll state ---------- */
.site-header { transition: box-shadow var(--dur-base) var(--ease), background var(--dur-base) var(--ease); }
/* Opaque like the base rule — a translucent background here would have needed
   the backdrop-filter that was removed to keep .nav's containing block sane. */
.site-header.is-scrolled { box-shadow: 0 8px 24px -18px rgba(27,28,30,.35); background: var(--stone); }

/* ---------- back to top ---------- */
/* Deliberately the same 42px circle, gold-ring hover and 17px stroked arrow as
   .steps-cta — the two are the site's only round arrow buttons and now read as
   one pair pointing opposite ways. The glyph moved from a "↑" character to an
   inline SVG for the same reason the step chevrons aren't "→": the arrow
   characters render thin and off-centre next to the drawn icons everywhere
   else on the page. */
.to-top {
  position: fixed; right: 20px; bottom: 20px; z-index: 50;
  width: 42px; height: 42px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--ink); color: var(--surface); border: none; cursor: pointer;
  box-shadow: var(--round-btn-shadow);
  opacity: 0; transform: translateY(12px) scale(.9); pointer-events: none;
  transition: opacity var(--dur-base) var(--ease), transform var(--dur-base) var(--ease),
              background var(--dur-fast) var(--ease), box-shadow var(--dur-base) var(--ease);
}
.to-top svg { width: 17px; height: 17px; transition: transform var(--dur-fast) var(--ease); }
.to-top.is-visible { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
.to-top:hover, .to-top:active { background: var(--mint-ink); box-shadow: var(--round-btn-shadow-on); }
.to-top:hover svg, .to-top:active svg { transform: translateY(-3px); }

/* ---------- shot hover zoom ---------- */
.shot img { transition: transform var(--dur-slow) var(--ease); }
.model-card .shot, .bcarousel .shot { overflow: hidden; }
.model-card:hover .shot img, .bcarousel__slide:hover .shot img { transform: scale(1.06); }

/* ---------- carousel ---------- */
.bcarousel { position: relative; }
.bcarousel__viewport {
  display: flex; overflow-x: auto; scroll-snap-type: x mandatory; scroll-behavior: auto;
  -webkit-overflow-scrolling: touch; scrollbar-width: none; border-radius: var(--radius-lg);
}
.bcarousel__viewport::-webkit-scrollbar { display: none; }
.bcarousel__viewport.is-animating { scroll-snap-type: none; }
.bcarousel__slide { flex: 0 0 100%; scroll-snap-align: start; position: relative; }
.bcarousel__slide .shot { border-radius: var(--radius-lg); width: 100%; height: 100%; background: var(--stone); }
/* Source photos come in three different native aspect ratios (studio/road/
   action shots aren't cropped the same). object-fit: cover on a fixed-ratio
   box would then crop each one differently and unpredictably — contain
   guarantees every photo is shown whole, letterboxed on the stone
   background instead of randomly clipped. */
.bcarousel__slide .shot img { object-fit: contain; }
.bcarousel__arrow {
  position: absolute; top: 50%; translate: 0 -50%; z-index: 2;
  width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--border);
  background: var(--surface); color: var(--ink); cursor: pointer; font-size: 1.2rem; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 10px 22px -14px rgba(27,28,30,.4);
  transition: transform var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.bcarousel__arrow:hover { border-color: var(--ink); transform: translateY(-50%) scale(1.06); }
.bcarousel__arrow--prev { left: 14px; }
.bcarousel__arrow--next { right: 14px; }
@media (max-width: 560px) { .bcarousel__arrow { width: 34px; height: 34px; font-size: 1rem; } }
.bcarousel__dots { display: flex; justify-content: center; gap: 8px; margin-top: 14px; }
.bcarousel__dots button {
  appearance: none; border: none; width: 8px; height: 8px; border-radius: 50%;
  background: var(--border); cursor: pointer; padding: 0;
  transition: background var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.bcarousel__dots button.is-active { background: var(--ink); transform: scale(1.25); }
.bcarousel--hero .bcarousel__slide .shot { aspect-ratio: 16 / 9; }
.bcarousel--gallery .bcarousel__slide .shot { aspect-ratio: 4 / 3; }
@media (min-width: 720px) { .bcarousel--gallery .bcarousel__slide .shot { aspect-ratio: 16 / 9; } }

/* ---------- equipment modal ---------- */
.modal-dialog.modal-dialog--wide { width: min(880px, calc(100% - 32px)); }
.modal-dialog.modal-dialog--wide h3 { font-size: 1.4rem; }

/* The calculator ships twice — inline in the page and inside this modal — and
   the two have to look the same. They didn't: the chip rows size themselves to
   the controls column, and at an 880px dialog that column came out ~387px
   against the page's ~555px, which is either side of the threshold where the
   row switches between nine chips and four. So on any desktop screen the page
   showed nine chips with the down-payment presets and the modal showed four
   without them.
   Widening the dialog to 1040 and trimming the controls padding brings the
   column to ~511px — the same band as the page — and the two now track each
   other at every width, including through the point where both drop to four
   columns (~1050px) and where .calc-body itself unstacks (900px).
   Two ranges, because what constrains the dialog changes at 900px. Above it
   the dialog is capped by its own max width, so 1040 is what closes the gap.
   Below it the cap is the screen, and .calc-body has unstacked to one column
   for both copies — there the page hands its calculator the full container
   while the modal was still paying ~100px for modal padding + dialog padding,
   which is enough to drop it a band on its own. So in that range the dialog
   goes near full width, the same as it already does on a phone.

   Scoped by id rather than to .modal-dialog--wide, because the equipment modal
   shares that class and has no reason to grow. Both ranges stay above 560px,
   where the phone rules already size this dialog and its controls. */
@media (min-width: 561px) and (max-width: 900px) {
  #calculatorModal.modal.is-open { padding: 12px; }
  /* The pad variables travel with the padding — the ✕ is positioned off them. */
  #calculatorModal .modal-dialog--wide {
    width: calc(100% - 8px);
    /* Только переменные: сам padding живёт на .modal-scroll, который их наследует. */
    --modal-pad-y: 18px; --modal-pad-x: 18px;
  }
  #calculatorModal .calc-controls { padding: 24px; }
}
@media (min-width: 901px) {
  #calculatorModal .modal-dialog--wide { width: min(1040px, calc(100% - 32px)); }
  #calculatorModal .calc-controls { padding: 24px; }
}

/* The calculator lives in two places, and the modal copy (opened by the hero's
   "Настроить платёж" button) was much the worse of the two on a phone: four
   nested paddings — 24 modal + 16 --wide inset + 24 dialog + 16 calc-controls —
   ate 160px of a 375px screen before a single control got to draw, which is
   what forced the model buttons to wrap and squeezed the chip grid. Trimming
   the first three brings the modal's controls column to ~279px, near enough to
   the ~307px the inline calculator gets that both hit the same container-query
   band and render identically. */
@media (max-width: 560px) {
  .modal.is-open { padding: 12px; }
  /* The pad variables travel with the padding — the ✕ is positioned off them. */
  .modal-dialog {
    width: min(420px, calc(100% - 8px)); max-height: min(92vh, 100%);
    /* Только переменные: сам padding живёт на .modal-scroll, который их наследует. */
    --modal-pad-y: 22px; --modal-pad-x: 18px;
  }
  .modal-dialog.modal-dialog--wide { width: calc(100% - 8px); }
  .modal-dialog h3 { font-size: 1.1rem; }
  .modal-dialog.modal-dialog--wide h3 { font-size: 1.2rem; }
}
.equipment-tabs { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 18px; }
.equipment-tab-btn {
  appearance: none; border: 1px solid var(--border); background: var(--stone); color: var(--ink-soft);
  font-family: var(--font-display); font-weight: 700; font-size: 0.88rem;
  padding: 10px 20px; border-radius: var(--radius-sm); cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.equipment-tab-btn:hover { border-color: var(--ink); color: var(--ink); }
.equipment-tab-btn[aria-selected="true"] { background: var(--ink); border-color: var(--ink); color: var(--surface); }
.equipment-note {
  font-size: 0.88rem; color: var(--ink-soft); margin-top: 14px; max-width: 68ch;
  padding: 12px 16px; background: var(--stone); border: 1px solid var(--border); border-radius: var(--radius-md);
}
.equipment-groups { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 14px; margin-top: 20px; }
@media (max-width: 640px) { .equipment-groups { grid-template-columns: 1fr; } }
.equipment-group {
  background: var(--stone); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 18px 20px;
  transition: border-color var(--dur-fast) var(--ease);
}
.equipment-group:hover { border-color: color-mix(in srgb, var(--mint) 45%, var(--border)); }
.equipment-group h4 {
  display: flex; align-items: center; gap: 9px;
  font-size: 0.88rem; font-weight: 800; letter-spacing: 0.01em;
  margin-bottom: 12px; padding-bottom: 10px; border-bottom: 1px solid var(--border);
}
.equipment-group h4::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--mint); flex: none; }
.equipment-group ul { display: flex; flex-direction: column; gap: 9px; }
.equipment-group li { position: relative; padding-left: 24px; font-size: 0.85rem; color: var(--ink-soft); line-height: 1.4; }
.equipment-group li::before {
  content: "✓"; position: absolute; left: 0; top: 0.5px;
  width: 16px; height: 16px; border-radius: 50%;
  background: color-mix(in srgb, var(--mint) 16%, transparent); color: var(--mint-ink);
  font-size: 0.6rem; font-weight: 800; line-height: 16px; text-align: center;
}
/* ---------- legal pages (policy / privacy) ---------- */
/* overflow-wrap is inherited, so this alone covers every heading/paragraph
   below it — long unhyphenated Russian legal terms ("конфиденциальности")
   otherwise refuse to break and push the whole page wider than the viewport
   on narrow screens. */
.legal-page { max-width: 780px; margin: 0 auto; padding-block: clamp(40px, 6vw, 72px); overflow-wrap: break-word; }
.legal-breadcrumb { font-size: 0.82rem; color: var(--graphite); margin-bottom: 18px; }
.legal-breadcrumb a { color: var(--ink-soft); }
.legal-breadcrumb a:hover { color: var(--ink); }
.legal-title { font-size: clamp(1.7rem, 3.4vw, 2.4rem); font-weight: 800; margin-bottom: 28px; }
.legal-body h2 { font-size: 1.1rem; font-weight: 800; margin-top: 36px; margin-bottom: 12px; }
.legal-body h2:first-child { margin-top: 0; }
.legal-body p { color: var(--ink-soft); margin-bottom: 12px; }
.legal-body ul { margin: 12px 0 12px 0; display: flex; flex-direction: column; gap: 8px; }
.legal-body li { position: relative; padding-left: 20px; color: var(--ink-soft); }
.legal-body li::before { content: ""; position: absolute; left: 0; top: 9px; width: 6px; height: 6px; border-radius: 50%; background: var(--mint); }
.legal-contacts { margin-top: 24px; padding: 20px 22px; background: var(--stone); border: 1px solid var(--border); border-radius: var(--radius-md); }
.legal-contacts p { color: var(--ink); margin-bottom: 6px; font-weight: 600; }
.legal-contacts p:last-child { margin-bottom: 0; font-weight: 400; color: var(--ink-soft); }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
  .reveal { opacity: 1; transform: none; }
  .shot img { transition: none; }
}

/* ---------- touch input ----------
   Why the controls felt slow to react on a phone: their feedback lived
   entirely in :hover, and a touchscreen cannot deliver :hover while the finger
   is down. The browser only resolves it after the tap completes — so pressing
   a button showed nothing at all during the press, then the state arrived late
   and stuck there afterwards. Nothing was actually delayed; there was just no
   sign the tap had registered. 21 controls were in that state.

   :active is the press itself, so it lands on finger-down. transition-duration
   goes to 0s on the pressed state so the change is immediate, and back to the
   fast easing on release. The base durations drop too — 0.4s is a graceful
   settle under a cursor and plain lag under a thumb.

   Keyed to (hover: none) rather than a width, because this is about the input
   device, not the screen size: a small window on a laptop still has a cursor
   and should keep the hover behaviour. */
@media (hover: none) {
  .btn, .chip-btn, .trim-btn, .chip-pill, .contact-switch-btn, .nav-toggle,
  .calc-mini-link, .equipment-tab-btn, .modal-close, .color-swatch,
  .hero-quicklink, .topbar__phone, .header-phone, .nav a, .model-card, .pay-card {
    transition-duration: var(--dur-fast);
  }

  /* .modal-close is deliberately NOT in this list, and it is the one control
     here that must not be: :active latches onto the last touched element on
     touch and stays there until something else is touched (the same latching
     the :hover block below neutralises for cards). Every other control in the
     list survives that, because it is still on screen and the next tap clears
     it — but the ✕ REMOVES ITSELF from view the moment it is pressed, so the
     latch had nothing to clear it, and the button came back pressed-looking the
     next time the modal was opened: a press state visible before any press,
     which is what "анимация изначально есть" was.
     Its press state is the .is-pressing class instead — added on pointerdown,
     removed by finishClose() in js/main.js, i.e. bounded by code rather than by
     whatever the browser decides :active means after the element disappears. */
  .btn:active, .chip-btn:active, .trim-btn:active, .chip-pill:active,
  .contact-switch-btn:active, .nav-toggle:active, .calc-mini-link:active,
  .equipment-tab-btn:active, .hero-quicklink:active,
  .topbar__phone:active, .nav a:active, .model-card:active, .pay-card:active,
  .contact-card:active, .step-card:active, .why-item:active {
    transform: scale(.97);
    transition-duration: 0s;
  }
  /* Its own rule: .color-swatch is small enough that a 3% dip reads as nothing. */
  .color-swatch:active { transform: scale(.9); transition-duration: 0s; }

  /* The other half of the same problem. These lifts are cursor affordances —
     on touch they cannot preview anything, they just latch on after a tap and
     stay until you touch something else, which is what makes a tapped card
     look stuck. Neutralised so :active above is the only press state. */
  .model-grid .model-card:hover, .related-grid .model-card:hover,
  .pay-grid .pay-card:hover, .pay-grid .pay-card.pay-card--featured:hover,
  .steps-grid .step-card:hover, .contact-forms .contact-card:hover,
  .color-swatch:hover { transform: none; }
  .model-card:hover .shot img, .bcarousel__slide:hover .shot img { transform: none; }

  /* .steps-cta and .to-top light up on :hover as well as :active, and neither
     button leaves the screen when tapped (they scroll the page, they don't
     close) — so the sticky :hover this block neutralises everywhere else left
     these two lit indefinitely, until some other element was touched. Reset
     back to the resting look here; :active in the base rule still lights the
     button for the actual press. */
  .steps-cta:hover, .to-top:hover { background: var(--ink); box-shadow: var(--round-btn-shadow); }

  /* Scroll-in reveals run 0.7s, and the grids stagger their cards on top of
     that — the fourth model card finished ~0.94s after it entered view. Under
     a thumb, which scrolls faster and further than a wheel, that means reading
     a section while it is still assembling itself. Halved here; the effect is
     intact, it just keeps up. Same reason .modal-dialog comes down from 0.4s:
     a panel that takes almost half a second to arrive reads as the tap not
     having worked, which is what the ✕ fix below is also about. */
  .reveal { transition-duration: .35s; }
  .model-grid .model-card.reveal, .related-grid .model-card.reveal,
  .pay-grid .pay-card.reveal, .steps-grid .step-card.reveal,
  .why-list .why-item.reveal { transition-delay: 0s; }
  .modal-dialog, .modal-backdrop { transition-duration: var(--dur-fast); }

  /* The ✕ is a 34px box — under the ~44px a fingertip reliably lands on, so a
     near miss did nothing and the second try read as the first one being slow.
     It used to buy the missing hit area with a ::before overhang because the
     visual size had to stay 34px: the heading cleared it with a hard-coded
     40px padding-right, and a genuinely wider button would have run under the
     text. Now that the button floats, the heading steps aside by whatever width
     it actually has — so on touch it can simply BE 44px. */
  .modal-close { width: 44px; height: 44px; font-size: 1.3rem; }
  /* Отступа под кнопку здесь больше нет: на тач-экранах ✕ сидит в своей полосе
     сверху (см. --modal-head-h), а не над заголовком. Правило стояло позже
     того блока и перебивало его собственный padding-right: 0. */
}

/* ---------- carousels: no shift on swipe or on load ----------
   Two separate jumps, both only visible once these grids become swipe
   carousels below 560px.

   1. The slides carry .reveal, i.e. opacity: 0 and translateY(22px) until the
      IntersectionObserver marks them visible — and that observer watches the
      viewport, which cards 2+ never enter, because they are off-screen
      *horizontally* inside a scroll container, not below the fold. So every
      card stayed hidden until it was swiped in, then slid 22px upward while
      the finger was still dragging it across. A reveal is for content arriving
      as you scroll down a page; inside a carousel the slides are already
      "there", so they are opted out entirely. The section heading above still
      reveals, which is what the effect was for.

   2. .bcarousel__dots is empty in the markup — initCarousel() appends the
      buttons on DOMContentLoaded, so the strip went from 0 to 8px tall and
      nudged everything below it down just after first paint. min-height
      reserves that row up front.

   Specificity beats .reveal.is-visible's (0,2,0) at (0,3,0), so this holds
   regardless of where it sits, but it lives at the end of the file next to
   the other late overrides. */
@media (max-width: 560px) {
  .bcarousel--models .model-card.reveal,
  .bcarousel--pay .pay-card.reveal,
  .bcarousel--related .model-card.reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }
}
.bcarousel__dots { min-height: 8px; }
