/* ==========================================================================
   main.css – tokens, layout, typography. No framework, no build step.
   Metrics copied 1:1 from the v2 docs site (header, controls, sidebar,
   toc, prose, search dialog). Theme contract: JS sets
   <html data-theme="dark|light">; colour-scheme follows, which is what
   makes Zola's inline light-dark() code colours flip.
   ========================================================================== */

/* ---------- Design tokens (sampled from the CarBox brand palette) ---------- */
:root,
html[data-theme="dark"] {
  --bg: #0c0d0e;
  --fg: #efefef;
  --muted: #929292;
  --border: #3a3a3a;
  --surface: #181a1d;
  --surface-hover: #23262b;
  /* --accent: the brand orange from carbox_logo.svg (#e54808). Used
     everywhere the site expresses brand identity: links, focus rings,
     primary buttons, selection, the car prices. On light backgrounds it
     steps down to a same-hue deep orange (#c43c00 below) – the brand
     orange only clears 4.5:1 text contrast over the dark background
     (4.79:1), not over white (2.9:1). --blue stays a tone token
     (pill / callout palettes), not the accent. */
  --accent: #e54808;
  --link: #e54808;
  --blue: #73a7ff;
  --green: #70c997;
  --yellow: #e0af68;
  --red: #e67c7c;
  --on-accent: #0a0a0a;
  --tip-border: #28503a;
  --tip-background: #0b1b11;
  --warning-border: #62471f;
  --warning-background: #1c1408;
  --backdrop: #000000b8;
  /* Structural separator lines (header, sidebar, mobile strip, rows, hr,
     table rules, footer): the v2 docs site renders its header hairline as a
     70% mix over transparent; we keep that exact tone but mix over --bg so
     the line is opaque. A translucent line composites to a different pixel
     depending on the layer it lands on – the sticky, scrollable sidebar's
     GPU layer rounded the blend to rgb(37) while every other line rounded
     to rgb(36) (measured; the docs site has the same ±1 quirk). Mixing over
     --bg computes the same color (36 dark / 235 light) once, up front, so
     all lines match everywhere, in both themes (both vars resolve per
     theme on <html>). Cost: content no longer ghosts through the 1px line
     under the sticky header. Component outlines – buttons, chips, code
     blocks, photos, panels – keep the full-strength --border. */
  --separator: color-mix(in srgb, var(--border) 70%, var(--bg));
  /* Current-item highlight – neutral grays of the brand palette
     (Dark: #23262b is exactly --surface-hover, as claimed above. Light
     deliberately sits one step lighter, on --surface: #e9e9eb would read as
     a hover flicker over the #f5f5f6 surfaces; the highlight stays distinct
     through the bold text plus the 2px --active-item-edge strip.) */
  --active-item-bg: #23262b;
  --active-item-edge: #181a1d;
  color-scheme: dark;
}

/* Structural metrics must sit on a plain :root, never under a theme
   selector. In a list like `:root, html[data-theme="dark"]` these same
   declarations match <html> at 0-1-1 while dark, which beats the mobile
   override further down (`@media (max-width: 48rem) { :root { --gutter:
   1rem } }`, 0-1-0) – the gutter, and every padded element with it,
   jumped when the theme toggled below 768px. On a plain :root the media
   query wins in both themes, exactly like the v2 docs site. */
:root {
  --header-height: 3.875rem;
  --frame-width: 96rem;
  --content-width: 60rem;
  --gutter: 1.5rem;
}

html[data-theme="light"] {
  --bg: #ffffff;
  --fg: #1c1b18;
  --muted: #6b6b6b;
  --border: #d8d8da;
  --surface: #f5f5f6;
  --surface-hover: #e9e9eb;
  --accent: #c43c00;
  --link: #c43c00;
  --blue: #1d63d8;
  --green: #1a7f4b;
  --yellow: #9a6700;
  --red: #c62f2f;
  --on-accent: #ffffff;
  --tip-border: #b5dcc3;
  --tip-background: #eff9f2;
  --warning-border: #ebd2a4;
  --warning-background: #fdf6e8;
  --backdrop: #00000052;
  --active-item-bg: #f5f5f6;
  --active-item-edge: #ececee;
  color-scheme: light;
}

/* No dark/light OS fall-back: the site is dark by default on every system,
   JS or not – the :root block above IS the no-attribute look, and the boot
   script (base.html) always bakes a data-theme anyway. The light theme
   exists only as an explicit choice: html[data-theme="light"] above, set
   by the toggle and remembered in localStorage. */

/* ---------- Fonts (vendored OpenTUI Mono, SIL OFL) ---------- */
@font-face {
  font-family: "OpenTUI Mono";
  src: url("fonts/OpenTUIMono-Regular.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "OpenTUI Mono";
  src: url("fonts/OpenTUIMono-Bold.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "OpenTUI Mono";
  src: url("fonts/OpenTUIMono-Italic.woff2") format("woff2");
  font-weight: 400; font-style: italic; font-display: swap;
}
@font-face {
  font-family: "OpenTUI Mono";
  src: url("fonts/OpenTUIMono-BoldItalic.woff2") format("woff2");
  font-weight: 700; font-style: italic; font-display: swap;
}

/* ---------- Base (docs-site metrics) ---------- */
*, *::before, *::after { box-sizing: border-box; }
* { scrollbar-color: var(--border) transparent; scrollbar-width: thin; }
/* No focus rings anywhere – deliberate house style: the blue default ring
   and the site's own accent rings are all off, on every element, keyboard
   or pointer. The :focus-visible hover-wash / state cues stay. */
:focus, :focus-visible { outline: none; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 999px; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; scrollbar-gutter: stable; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  font-family: "OpenTUI Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 0.875rem;           /* docs-site metric: 14px */
  line-height: 1.8em;
  color: var(--fg);
  background: var(--bg);
}
button, input { color: inherit; font: inherit; }

::selection { background: color-mix(in srgb, var(--accent) 35%, transparent); }

a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; }

img { max-width: 100%; height: auto; }

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

.skip {
  position: absolute; left: -999px; top: 0; z-index: 100;
  background: var(--fg); color: var(--bg);
  padding: 0.5rem 0.75rem; border: 0; border-radius: 0;
}
.skip:focus { left: 1rem; top: 1rem; }

.icon { vertical-align: -0.15em; }

/* ---------- Header (1:1 with the docs site) ---------- */
.site-header {
  position: sticky; top: 0; z-index: 50;
  height: var(--header-height);
  /* Docs-identical hairline: the v2 docs site's exact 70% color-mix, now
     tokenized as --separator and shared by every structural line. */
  border-bottom: 1px solid var(--separator);
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
}
.header-inner {
  height: 100%;
  display: flex; align-items: center; gap: 1.5rem;
  padding-inline: var(--gutter);
}
.brand { display: inline-flex; align-items: center; min-width: 0; color: var(--fg); }
.brand:hover { text-decoration: none; }
/* CarBox wordmark (logo.svg pulled in via <use>, ~9.5:1): the letter paths
   are currentColor so they follow the theme ink, the orange accents stay
   #e54808.
   width:auto + the 6872x724 viewBox keeps the lockup's ratio; max-width and
   the .brand min-width:0 let it scale down instead of squeezing the header
   controls when a window gets very narrow. */
.brand-logo { height: calc(1.375rem * 1.02); width: auto; max-width: 100%; }
/* flex:none: the header row turns tight below ~880px, and the brand lockup
   (min-width:0) is there to absorb the squeeze. Left shrinkable, the nav
   itself took a fraction of a pixel too – enough to wrap the only
   multi-word label ("Про Нас") onto a second line, lifting its box above
   the row. Held at its content width, the labels never re-wrap. */
.site-nav { display: flex; align-items: center; gap: 1.25rem; flex: none; }
.site-nav a, .nav-link {
  color: var(--muted); font-weight: 700;
  padding-block: 0.25rem; border-radius: 0;
}
.site-nav a:hover, .nav-link:hover { color: var(--fg); text-decoration: none; }
.site-nav a.active, .nav-link.active { color: var(--fg); }
/* Long/short nav labels (see [extra].nav short in zola.toml): both texts
   are baked into the link, CSS shows exactly one. The long label leads;
   below 56rem the header row gets tight (the last link starts to shrink
   around 880px), so the header nav swaps to the short label there – the
   mobile menu (only visible at all at ≤48rem) always uses it. */
.nav-short { display: none; }
.mobile-navigation .nav-long { display: none; }
.mobile-navigation .nav-short { display: inline; }
.header-spacer { flex: 1; }

/* Header controls share one square: 2.25rem tall, 1px border, hover
   lightens the border instead of adding a background. */
.search-trigger {
  display: inline-flex; align-items: center; justify-content: space-between;
  min-width: 9.5rem; height: 2.25rem; padding: 0 0.75rem;
  color: var(--muted); background: transparent;
  border: 1px solid var(--border); border-radius: 0; cursor: pointer;
  flex-shrink: 0;
}
.search-trigger:hover { border-color: var(--muted); color: var(--fg); }
.search-trigger kbd, .search-trigger .kbd { font: inherit; font-size: 0.75rem; margin-left: auto; }
/* The Remix search glyph lives in the button at every width but only
   paints on the collapsed mobile trigger (see the 48rem block): on the
   desktop's wide button the "Пошук" label says the same thing twice. */
.search-trigger .search-ic { display: none; }
.theme-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.25rem; height: 2.25rem; padding: 0;
  color: var(--muted); background: transparent;
  border: 1px solid var(--border); border-radius: 0; cursor: pointer;
  flex-shrink: 0;
}
.theme-toggle:hover { border-color: var(--muted); color: var(--fg); }
html:not(.js) .theme-toggle { display: none; }
html:not(.js) .search-trigger { display: none; }
html[data-theme="dark"] .icon-moon { display: none; }
html[data-theme="light"] .icon-sun { display: none; }
.kbd { font: inherit; font-size: 0.75rem; }

/* Mobile menu: at <=48rem a sticky glass strip below the header with the
   nav links inline and centered (shown by the 48rem block). */
.mobile-navigation { display: none; }

/* ---------- Shell / layout variants ---------- */
.frame { width: min(100%, var(--frame-width)); margin-inline: auto; }

main {
  display: grid;
  min-height: calc(100vh - var(--header-height));
  /* Mobile browsers resize the URL bar while scrolling: svh is the visible
     height with the bars expanded, so the page doesn't overshoot. Engines
     without svh keep the vh line above. */
  min-height: calc(100svh - var(--header-height));
  align-items: start;
}
.layout-list { grid-template-columns: 15rem minmax(0, 1fr); }
/* The /cars/ facet panel is the widest sidebar on the site: group titles
   like "Стан автомобіля" and the "Від"/"До" number pairs must fit on one line,
   so the sidebar column gets an extra 2.5rem there. */
.layout-list:has(.car-filters) { grid-template-columns: 17.5rem minmax(0, 1fr); }
.layout-doc  { grid-template-columns: 16rem minmax(0, 1fr) 16rem; }
/* ToC-only doc pages (page.html without a sidebar): the DOM order is
   content-then-toc, so the columns flip compared to .layout-doc – using
   that one here would put the article in the 15rem sidebar track. */
.layout-toc  { grid-template-columns: minmax(0, 1fr) 16rem; }
.layout-home { grid-template-columns: minmax(0, 1fr); }
.layout-page { grid-template-columns: minmax(0, 1fr); }

/* Content column: docs-site main padding */
.content { padding: 3rem clamp(2rem, 4vw, 4rem) 5rem; }
.layout-home .content { margin-inline: auto; max-width: var(--content-width); padding: 4.5rem var(--gutter) 5rem; }

/* Left sidebar (contextual – never a copy of the top menubar).
   docs-site .sidebar-column + .docs-sidebar metrics. The side padding is
   the gutter on BOTH sides (the docs site's 2rem against the separator
   read closer to the panel's middle than its edge does): the "Інші Авто"
   previews then start exactly where the panel's own text starts and end
   the same distance from the separator. */
.sidebar {
  position: sticky; top: var(--header-height);
  /* vh fallback first, svh (visible viewport) second – same mobile URL-bar
     pairing as .main and .toc above. */
  height: calc(100vh - var(--header-height));
  height: calc(100svh - var(--header-height));
  overflow-y: auto;
  /* Wheeling the panel to its end must not hand the leftover scroll on
     to the page behind it. */
  overscroll-behavior: contain;
  /* The gap between the panel's edge and its previews has to read the same
     on both sides, and a scrollbar is what breaks that: the thumb paints
     over the right-hand padding, so the white space beside a preview came
     out narrower on the right than on the left. The compensation would be
     a padding of exactly one thumb width – but whether the thumb sits on
     top of the padding (macOS overlay scrollbars, which reserve nothing)
     or in a reserved gutter inside it (the "always show scroll bars"
     setting) is a system setting, not something CSS can read, and the two
     cases want opposite arithmetic. So the panel simply doesn't paint a
     thumb: the wheel, the trackpad and touch all still scroll it, and the
     hairline the list ends on reads as its edge. Same recipe the make
     dropdown and the lightbox strip already use, and the previews then sit
     the gutter value off both edges in every browser and every setting. */
  scrollbar-width: none;
  border-right: 1px solid var(--separator);
  padding: 2.25rem var(--gutter) 3rem;
}
.sidebar::-webkit-scrollbar { width: 0; height: 0; display: none; }
/* On car pages the "Інші Авто" heading opens exactly on the return
   button's line (the content block starts 1.5rem under the header). */
body:has(.car-crumbs) .sidebar { padding-top: 1.5rem; }
.side-title, .toc-title {
  margin: 0 0 0.625rem; font-size: 0.75rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.04em; color: var(--fg);
}
.side-list { list-style: none; margin: 0; padding: 0; }
.side-list li + li { margin-top: 0.375rem; }
.side-list a {
  display: flex; flex-direction: column; gap: 0.1rem;
  color: var(--muted); padding-block: 0.125rem;
}
.side-list a:hover { color: var(--fg); text-decoration: none; }
.side-list a.active { color: var(--fg); font-weight: 700; }
/* Folder-style tags in the filter sidebar (kind = "folder" in tag_meta): a
   name, not a hashtag. Same shade as the topic tags – the small square marker
   plus the name itself is the only distinction. Display only: the tag itself
   stays flat. */
.side-list a.tag-folder {
  flex-direction: row; align-items: center; gap: 0.4rem;
}
.side-list a.tag-folder::before {
  content: ""; flex: 0 0 auto;
  width: 0.375rem; height: 0.375rem; border: 1px solid currentColor;
}
/* Filter active (site.js toggles .active on every [data-tag-filter] link):
   the square fills with the link's own colour – solid fg in the sidebar
   (.side-list a.active sets color: var(--fg)), muted on in-page chips.
   Works in both themes; covers the .chip-folder copies of the same marker. */
.side-list a.tag-folder.active::before, .chip-folder.active::before { background: currentColor; }
.side-link-title { color: inherit; }
.side-link-meta { font-size: 0.75rem; color: var(--muted); }
/* Sidebar prices (side_posts thumbs) are car prices like every other on
   the site, so they carry the bold price weight; a plain meta line (a
   project's period, a date) stays regular. */
.side-link-price { font-weight: 700; font-size: 1rem; color: var(--accent); }
/* Preview squares in the car page's "Інші Авто" sidebar (side_posts
   thumbs={true}): the same shape as the /cars/ grid cards – a full-width
   framed 4:3 shot with the title/meta under it – just stacked in one
   column inside the sidebar's own padding. The panel's gutters are equal
   (see .sidebar), so the shots sit flush under the "Інші Авто" heading
   with the same air left and right – no nudge needed. */
.side-list a.has-thumb { flex-direction: column; align-items: stretch; gap: 0.5rem; }
.side-thumb {
  display: block; width: 100%;
  border: 1px solid var(--border); overflow: hidden;
}
.side-thumb img { display: block; width: 100%; height: auto; }
.side-link-body { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
/* Wider rhythm between previewed entries than plain text links (0,3,2
   outranks the plain li + li rule everywhere, media queries included). */
.side-list li:has(.side-thumb) + li:has(.side-thumb) { margin-top: 1.125rem; }

/* Right TOC: exactly the docs-site .toc-column + .docs-toc metrics – plain,
   no panel. Inline padding lives on the links themselves (as on the docs
   site) so the active-item highlight can run edge to edge. */
.toc {
  position: sticky; top: calc(var(--header-height) + 2rem);
  max-height: calc(100vh - var(--header-height) - 4rem);
  max-height: calc(100svh - var(--header-height) - 4rem);
  overflow-y: auto;
  padding: 3rem 0;
  /* No visible scrollbar chrome on the TOC (docs-site look) while it stays
     internally scrollable for the scroll-spy nudge. Must come after the
     global thin-scrollbar rules to win on specificity. */
  scrollbar-width: none;
}
.toc::-webkit-scrollbar { width: 0; }
.toc-title { padding-inline: 2rem var(--gutter); }
.toc-list { list-style: none; margin: 0; padding: 0; }
.toc-list li + li { margin-top: 0.375rem; }
.toc-list ul { list-style: none; margin: 0; padding: 0; }
.toc-item a {
  display: block; color: var(--muted);
  padding: 0.125rem var(--gutter) 0.125rem 2rem;
}
.toc-list ul .toc-item a { padding-left: 3rem; }
.toc-item a:hover { color: var(--fg); text-decoration: none; }
/* Active heading while scrolling – the highlight opencode.ai/docs gives the
   current page in its left sidebar: background block + 2px edge strip. */
.toc-item a.active {
  color: var(--fg); font-weight: 700;
  background: var(--active-item-bg);
  border-left: 2px solid var(--active-item-edge);
  padding-left: calc(2rem - 2px);
}
.toc-list ul .toc-item a.active { padding-left: calc(3rem - 2px); }

/* ---------- Page headers ---------- */
.page-head { margin-bottom: 2rem; }
.page-head h1 { margin: 0.25rem 0 0.5rem; font-size: 1.875rem; line-height: 1.2; font-weight: 700; }
/* Long-word insurance outside .prose (headings/cards/rows may hold slugs or
   URLs; overflow-wrap only breaks them when they would overflow). */
.page-head h1, .card h2, .card h3, .row-body h2 { overflow-wrap: break-word; }
.page-desc { margin: 0; color: var(--muted); max-width: 46rem; }
.crumbs { margin: 0; font-size: 0.75rem; }
/* The car listings' content block starts half the usual air under the
   header, so the return crumb (the first thing in it) sits 1.5rem below
   the bar and – with its matching 1.5rem bottom margin – 1.5rem above
   the leading photo: both gaps are exactly half of what they were. */
.content:has(.car-crumbs) { padding-top: 1.5rem; }

/* The listing page's return link (car-crumbs) is a real .btn – framed,
   standard size like every other button on the site, so this crumb skips
   the 0.75rem crumb micro-size and keeps the body text size; only the ink
   stays muted, as the crumbs' (see the .crumbs a rule below).
   The margin-bottom mirrors half of the content block's 3rem top padding:
   the button sits mid-way between the site header and the leading photo
   – equal 1.5rem air above and below it. (The mobile block drops the
   same pair to 1rem, half of the 2rem padding phones use.) */
.car-crumbs { font-size: inherit; margin-bottom: 1.5rem; }
.crumbs a { color: var(--muted); }
.crumbs a:hover { color: var(--fg); }

/* ---------- Prose (docs-site rhythm) ---------- */
.prose { max-width: var(--content-width); overflow-wrap: break-word; }
.prose h1, .prose h2, .prose h3, .prose h4 {
  color: var(--fg); font-weight: 700;
  scroll-margin-top: calc(var(--header-height) + 1rem);
}
.prose h1 { margin: 0 0 3rem; font-size: 1.875rem; line-height: 1.2; }
.prose h2 { margin: 3rem 0 0.75rem; font-size: 1.25rem; line-height: 1.5; }
.prose h3 { margin: 2.25rem 0 0.75rem; font-size: 1.125rem; line-height: 1.5; }
.prose h4 { margin: 2rem 0 0.75rem; font-size: 1rem; }
.prose > :first-child { margin-top: 0; }
.prose p, .prose ul, .prose ol { margin: 0 0 1.5rem; }
.prose ul, .prose ol { padding-left: 1.5rem; }
.prose li + li { margin-top: 0.5rem; }
/* Bullet markers: not round – the site's squares, same 6px marker with the
   1px border the folder tags use (.tag-folder::before). Top level filled,
   nested hollow: the old disc/circle logic, in square form. The swap rides
   on :has() support – the same condition the task-list rule below needs;
   old engines keep the browser's default bullets. */
@supports selector(:has(*)) {
  .prose ul { list-style: none; }
  .prose ul > li { position: relative; }
  .prose ul > li:not(:has(> input[type="checkbox"]))::before {
    content: ""; position: absolute; left: -1.25rem;
    /* Optical centring, bracketed by builds: the geometric line-box centre
       (middle at 12.6 under the line-box top) read a hair high against
       prose, an all-lowercase build at 13.0 read a hair low; the kept
       value for these markers is ~12.9, so +0.3px over geometric centre.
       At dsf 2 that puts the edge on a fractional device pixel (227.4):
       Chrome never snaps positioned layers, it antialiases – a ~0.1px
       feather, the same softness the 1px frame already carries here.
       The nudge is for bullets against word bands only – the checkboxes
       below and the folder chip squares keep the exact line-box centre.
       0.375rem = the square's border-box height. */
    top: calc((1lh - 0.375rem) / 2 + 0.3px);
    width: 0.375rem; height: 0.375rem;
    border: 1px solid currentColor; background: currentColor;
  }
  .prose ul ul > li:not(:has(> input[type="checkbox"]))::before { background: none; }
}
/* Task lists (`- [ ]` / `- [x]`): Zola emits a disabled checkbox as the
   li's first child but keeps the bullet – the bullet goes for those items
   only, the checkbox rides where it was. A single-level :has() selector,
   no nesting. The native control (round corners on macOS) is squared off:
   hairline border, empty fill, sharp corners – same square family as the
   markers and folder tags; a checked item fills the square with the text
   colour (white in dark, ink in light), like the filled top-level bullet.
   The -1.26px nudge puts its middle at 12.6 under the line-box top –
   exact line-box centre (12.6 under the line-box top), where the folder
   chips sit too. The list markers above carry a +0.3px optical nudge;
   this box does not – it centres on its own check glyph, not a baseline.
   (vertical-align: middle alone lands it at 13.86). */
.prose li:has(> input[type="checkbox"]) { list-style: none; }
.prose li > input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  width: 0.875rem; height: 0.875rem; margin: 0 0.5em 0 0;
  border: 1px solid var(--muted); background: none; border-radius: 0;
  vertical-align: middle;
  position: relative; top: -1.26px;
}
.prose li > input[type="checkbox"]:checked { border-color: var(--fg); background: var(--fg); }
/* Footnotes (bottom_footnotes = true in zola.toml): the markers are
   <sup class="footnote-reference"> in the text; Zola appends the list as
   <section class="footnotes"><ol class="footnotes-list"> at the end of
   the page content, back-reference links riding inside each item. */
.prose .footnotes {
  margin-top: 3rem; padding-top: 1rem;
  border-top: 1px solid var(--separator);
  color: var(--muted); font-size: 0.8125rem;
}
.prose .footnotes p { margin: 0; }
.prose .footnotes li + li { margin-top: 0.5rem; }
/* Jump targets need the same sticky-header clearance as the headings
   (scroll-margin-top there), or the hash jump parks them under the
   header: the <sup> markers (id="fr-…", hit by the ↩ back-links) and the
   bottom-list items (id="fn-…", hit by the [n] forward links). */
.prose .footnote-reference, .prose .footnotes li {
  scroll-margin-top: calc(var(--header-height) + 1rem);
}
.prose a { color: var(--link); }
.prose hr { border: 0; border-top: 1px solid var(--separator); margin: 3rem 0 1.5rem; }
.prose img { max-width: 100%; height: auto; }

/* Inline code: docs-site metric (tinted background, no border) */
.prose code, .row code, .card code { font: inherit; }
.prose :not(pre) > code, .row :not(pre) > code, .card :not(pre) > code {
  background: color-mix(in srgb, currentColor 10%, transparent);
  padding: 0.0625rem 0.4ch; border-radius: 0;
}

/* Fenced code: Zola inline dual-theme -> colours arrive as light-dark().
   The !important background mirrors the docs site, which also overrides the
   theme's own block fill: the code sits on the page background inside a
   plain 1px frame – no separate panel colour, in either theme. */
.prose pre {
  border: 1px solid var(--border); border-radius: 0;
  margin: 0 0 1.5rem; padding: 0.875rem 1rem;
  font: inherit; font-size: 0.8125rem;
  overflow-x: auto; tab-size: 4;
  background: var(--bg) !important;
}
.prose pre code { background: none; padding: 0; }
.prose pre.giallo { white-space: pre; }

/* Code-block line spans (Zola 0.23): the highlighter emits one .giallo-l
   per source line (and a .giallo-ln span per number with linenos), the
   lines separated by literal newlines. The spans become block boxes so
   line-wide decorations reach edge-to-edge; white-space:normal on the
   code element makes those separator newlines block-level whitespace,
   which is dropped, while white-space:pre inside each span keeps every
   character. Auto width + negative inline margins (the pre's horizontal
   padding) bleed the boxes to the frame, so an hl_lines tint – Zola
   bakes it as an inline light-dark() background on the span – becomes a
   full-width band. min-height:1lh keeps a blank line's empty span on the
   grid. The 1rem text inset is a generated ::before spacer, never
   padding: some engines paint the selection fill across a line's whole
   padding box, which drags the highlight into the empty inset, while
   generated content is never part of a selection. Metrics are
   untouched: font-size and the inherited line-height simply pass
   through. Verified in headless Chrome, pixel by pixel. */
.prose pre.giallo code { white-space: normal; }
.prose .giallo-l {
  display: block; white-space: pre;
  margin-inline: -1rem;    /* box bleeds to the frame … */
  min-height: 1lh;
}
.prose .giallo-l::before {
  content: "";             /* … the 1rem text inset is this spacer: as
                              generated content it is never part of a
                              selection, so the highlight always starts
                              at the text – some engines would paint a
                              line's padding box and drag the fill into
                              the empty inset, as the numbered blocks
                              (counter ::before) already prove safe */
  display: inline-block; width: 1rem;
}
/* hl_lines fill: Zola bakes the *theme's* highlight colour into the span
   as an inline light-dark() background (the github themes ship #f6f8fa /
   #2b3036 – greyed-out syntax colours, not this site's palette). The brand
   palette has one neutral current-item fill: --surface-hover (#23262b
   dark / #e9e9eb light) – what the site uses to highlight rows. Use it,
   !important beats Zola's inline style; only tinted lines carry a
   background attribute, so the selector hits exactly the hl_lines. */
.prose .giallo-l[style*="background"],
.prose .giallo-ln[style*="background"] {
  background-color: var(--surface-hover) !important;
}
/* linenos gutter: Zola bakes the numbers into each line as real text
   spans – digits a reader can highlight and copy. The stylesheet hides
   those spans and draws the numbers as generated content (a CSS counter
   on the line boxes): the gutter becomes pure layout, so no digit can
   land in a selection or the clipboard – drag, triple-click or Cmd+A,
   in any browser. Spacing is symmetric: 1rem from the frame to the
   number, 1rem from the number to the code – the same inset regular
   paragraphs use. The numbers are left-aligned. When a block's numbers
   cross 9 -> 10, site.js adds .ln-pad and the single-digit ones are
   zero-padded ("01"), so every line in the block keeps one code column
   and no digit shift moves the text. The colour is the site's own
   --muted rather than the themes' grey. site.js offsets the counter
   start for linenostart=.
   Without :has support (older browsers) the real spans stay put, with
   the spacer before them and the same 1rem gap after, as the softer
   previous fallback. The counter box also replaces the plain spacer
   above: margin-left restores the 1rem inset, so one ::before rule per
   line serves both block types. */
.prose pre.giallo:has(.giallo-ln) .giallo-ln { display: none; }
.prose pre.giallo:has(.giallo-ln) code { counter-reset: ln; }
.prose pre.giallo:has(.giallo-ln) .giallo-l { counter-increment: ln; }
.prose pre.giallo:has(.giallo-ln) .giallo-l::before {
  content: counter(ln);
  display: inline-block; width: auto;
  min-width: 1ch; margin-left: 1rem; margin-right: 1rem;
  text-align: left; color: var(--muted);
}
.prose pre.giallo:has(.giallo-ln) code.ln-pad .giallo-l::before {
  content: counter(ln, decimal-leading-zero);
}
.prose .giallo-ln {
  display: inline-block; min-width: 1ch; margin-right: 1rem;
  text-align: left; user-select: none; -webkit-user-select: none;
}
/* Copy button (docs site .docs-code-block): site.js wraps every fenced
   block in a position:relative .docs-code-box div and drops a small
   button in the block's top-right corner – surface fill, muted 0.75rem
   text, no border, exactly like the docs. The label flips to "Copied"
   for 1.5s. Blocks that already sit inside tabs lose their own frame
   (the tab box owns it), as in the docs. */
.docs-code-box { position: relative; }
.docs-code-box > button {
  cursor: pointer; position: absolute; top: 0.5rem; right: 0.5rem; z-index: 1;
  background: var(--surface); color: var(--muted);
  border: 0; border-radius: 0; padding: 0.25rem 0.5rem;
  font: inherit; font-size: 0.75rem;
}
.docs-code-box > button:hover { color: var(--fg); }

/* Filename bar (fence suffix name=…, see components.md): with
   data_attr_position = "pre" in zola.toml the <pre> carries data-name;
   site.js mirrors it onto the .docs-code-box wrapper, and the bar is
   generated on that wrapper – above the horizontal scroller, so it
   stays put when a long line scrolls sideways (Zola's pre kept its own
   padding, exactly like an untitled block). The strip carries the
   frame's top and sides plus the hairline under it; the pre drops its
   top border so the rule under the strip is not doubled. Total strip
   height: 1px border + 0.5rem padding + 25.2px line (the site's line
   rhythm is an absolute length inherited from body) + 0.5rem padding +
   1px hairline = 43.2px. Type metric is the docs site's own title bar
   (astro-code-title): 0.75rem muted text, 0.5rem/1rem padding. */
.docs-code-box[data-name]::before {
  content: attr(data-name);
  display: block;
  padding: 0.5rem 1rem;
  border: 1px solid var(--border);
  color: var(--muted);
  font-size: 0.75rem;
}
.docs-code-box[data-name] > pre { border-top: 0; }
/* Inside a code-tab box the tab frame owns the border – the strip only
   keeps its hairline. */
.docs-code-tabs .docs-code-box[data-name]::before {
  border: 0; border-bottom: 1px solid var(--border);
}
/* With a filename bar the button can't ride the frame's corner (the
   strip owns it), so it sits 0.5rem under the strip's hairline – the
   exact 0.5rem top/right inset the untitled blocks' corner button
   keeps. (The docs never pair a title bar with a button, so this
   pairing is ours.) */
.docs-code-box[data-name] > button { top: calc(43.2px + 0.5rem); }

/* Code tabs + content tabs (docs site .docs-code-tabs / .docs-plan-tabs).
   Components: <tabs> with <tab label="…"> bodies (templates/components.html).
   In the markdown a tab is just a .docs-tab-panel div with its label in
   data-tab-label; site.js reads the labels, builds the docs' own
   .docs-code-tabs-list / .docs-plan-tabs-list markup (role=tab buttons,
   hairline strip, selected tab gets a bottom rule) and hides the inactive
   panels. Without JS every panel simply renders stacked in reading order –
   the content is fully usable either way.
   <tabs box={false}> switches the plain underline style (docs plan tabs):
   no surrounding frame, 2px selection rule, bolder labels. */
.docs-code-tabs { border: 1px solid var(--border); margin: 0 0 1.5rem; }
.docs-code-tabs-list { border-bottom: 1px solid var(--border); display: flex; flex-wrap: wrap; }
.docs-code-tabs-list button {
  cursor: pointer; color: var(--muted); font: inherit; font-size: 0.75rem;
  background: none; border: 0; border-bottom: 1px solid transparent;
  padding: 0.5rem 0.875rem; flex-shrink: 0; margin-bottom: -1px;
}
.docs-code-tabs-list button:hover,
.docs-code-tabs-list button[aria-selected=true] { color: var(--fg); }
.docs-code-tabs-list button[aria-selected=true] { border-bottom-color: var(--fg); }
.docs-code-tabs .docs-code-box pre { border: 0; margin: 0; }
.docs-code-tabs .docs-tab-panel > :last-child { margin-bottom: 0; }

.docs-plan-tabs { margin-bottom: 1.5rem; }
.docs-plan-tabs-list { border-bottom: 1px solid var(--border); display: flex; gap: 0; margin-bottom: 1rem; }
.docs-plan-tabs-list button {
  cursor: pointer; color: var(--muted); font: inherit; font-weight: 600;
  background: none; border: 0; border-bottom: 2px solid transparent;
  padding: 0.5rem 1rem; margin-bottom: -1px;
}
.docs-plan-tabs-list button[aria-selected=true] {
  border-bottom-color: var(--fg); color: var(--fg);
}
.docs-plan-tabs .docs-tab-panel { overflow-x: auto; }
.docs-plan-tabs .docs-tab-panel > :last-child,
.docs-plan-tabs .docs-tab-panel > :last-child > :last-child { margin-bottom: 0; }

/* Tables (docs-site metric: rules under rows only) */
.prose table { border-collapse: collapse; width: 100%; margin-bottom: 1.5rem; font-size: 0.8125rem; }
.prose th, .prose td {
  border: 0; border-bottom: 1px solid var(--separator);
  text-align: left; vertical-align: top; padding: 0.5rem 1rem 0.5rem 0;
}
.prose th { color: var(--muted); font-size: 0.75rem; font-weight: 400; background: none; }

/* Blockquotes + GitHub-style alerts + the callout component. The box spec
   is the live docs site's .callout, 1:1: a plain 1px frame on the page
   background, 1rem padding, an uppercase bold label line (the docs render
   <strong>Note</strong>; here the label rides a ::before for the native
   alerts and a real <strong> for the component), and Tip/Warning take the
   palette's tinted border+fill tokens. Note/important/caution: the docs
   only ever show the plain frame (note) – the two extra GitHub tones
   extend it with the pills' tint formula so they never look broken. */
.prose blockquote {
  border-left: 2px solid var(--muted); color: var(--muted);
  margin: 0 0 1.5rem; padding-left: 1rem;
}
.prose blockquote p:last-child { margin-bottom: 0; }

/* Zola emits one combined class per alert (markdown-alert-tip, …), not a
   GitHub-style pair – hence [class*=…] attribute selectors. */
.prose blockquote[class*=markdown-alert], .callout {
  margin: 0 0 1.5rem; padding: 1rem; color: var(--fg);
  border: 1px solid var(--border); border-radius: 0;
}
.prose blockquote[class*=markdown-alert]::before, .callout > strong {
  display: block; text-transform: uppercase;
  font-weight: 700; margin-bottom: 0.5rem;
}
.prose blockquote[class*=markdown-alert] p:last-child, .callout > div > :last-child { margin-bottom: 0; }
.prose .markdown-alert-note::before      { content: "Note"; }
.prose .markdown-alert-tip::before       { content: "Tip"; }
.prose .markdown-alert-important::before { content: "Important"; }
.prose .markdown-alert-warning::before   { content: "Warning"; }
.prose .markdown-alert-caution::before   { content: "Caution"; }
/* Custom alert headline: "> [!TIP My own words]" (or the callout's
   title= parameter) replaces the fixed kind label. site.js promotes the
   non-GitHub marker form to the alert class + data-alert-title; the
   callout bakes the attribute directly. The label is no longer forced
   uppercase – custom copy reads in its author's casing. */
.prose blockquote[data-alert-title]::before { content: attr(data-alert-title); }
.prose blockquote[data-alert-title]::before, .callout[data-alert-title] > strong { text-transform: none; }
/* Tone overrides carry the same compound shape so they outweigh the base
   rule above regardless of source order. */
.prose blockquote[class*=markdown-alert].markdown-alert-tip, .callout-tip       { border-color: var(--tip-border); background: var(--tip-background); }
.prose blockquote[class*=markdown-alert].markdown-alert-warning, .callout-warning { border-color: var(--warning-border); background: var(--warning-background); }
.prose blockquote[class*=markdown-alert].markdown-alert-important, .callout-important {
  border-color: color-mix(in srgb, var(--blue) 45%, transparent);
  background: color-mix(in srgb, var(--blue) 8%, transparent);
}
.prose blockquote[class*=markdown-alert].markdown-alert-caution, .callout-caution {
  border-color: color-mix(in srgb, var(--red) 45%, transparent);
  background: color-mix(in srgb, var(--red) 8%, transparent);
}

/* ---------- Timeline module (LinkedIn-style: rail + dots) ----------
   Components: {% <timeline> %} wrapper + {% <title> %} entries (see
   templates/components.html); live on /cv/, demoed on /components/.
   Org names are real markdown headings inside the wrapper (the page ToC
   keeps their anchors), but the rail belongs to the roles, not the orgs:
   headings draw no line, and each org gets its own continuous stroke
   running from the first role's dot to the last one. Entries paint a
   full-height ::before segment by default so consecutive roles under one
   heading connect; the group's first entry starts its segment at the dot
   (top: 0.75rem – the 6px ::after square centred on the role's first
   text line) and the group's last one stops there. A role alone in its
   org gets just the dot. All vertical spacing is padding, never margin,
   so segments butt together. */
.timeline { position: relative; padding-left: 1.5rem; }
.timeline > h2, .timeline > h3, .timeline .tl-title { position: relative; margin: 0; }
.timeline > h2, .timeline > h3 { padding: 1.75rem 0 0.5rem; }
.timeline > h2:first-child, .timeline > h3:first-child { padding-top: 0; }

/* Rail: role entries only – full block height, so roles within one org
   connect into a single stroke. Headings carry no line. */
.timeline .tl-title::before {
  content: ""; position: absolute; left: -1.25rem; top: 0; bottom: 0;
  width: 1px; background: var(--separator);
}
/* Dots: square, on the role lines only (work mode) – centred on the rail
   and on the entry's first text line. Not their own size: they are the
   site's square family – the same 0.375rem border-box marker with a 1px
   border that bullets and folder-chip squares use, filled like a checked
   box or an active chip, in the rail's muted shade. */
.timeline:not(.tl-heading-dots) .tl-title::after {
  content: ""; position: absolute; left: calc(-1.25rem - 2.5px); top: 0.5625rem;
  width: 0.375rem; height: 0.375rem;
  border: 1px solid var(--muted); background: var(--muted);
}

/* Work mode group start: the stroke begins at this org's first dot... */
.timeline:not(.tl-heading-dots) > :is(h2, h3) + .tl-title::before { top: 0.75rem; }
/* ...and ends at the last dot before the next heading (or the end). */
.timeline:not(.tl-heading-dots) .tl-title:has(+ :is(h2, h3))::before,
.timeline:not(.tl-heading-dots) .tl-title:last-child::before { bottom: auto; height: 0.75rem; }

/* Work mode sole role under a heading: start and end rules collide, so
   drop the segment entirely – dot only. */
.timeline:not(.tl-heading-dots) > :is(h2, h3) + .tl-title:has(+ :is(h2, h3))::before,
.timeline:not(.tl-heading-dots) > :is(h2, h3) + .tl-title:last-child::before { content: none; }

/* heading_dots={true} (Education, Volunteering on /cv/): one entry per
   org, so the dots ride the org headings instead and the rail runs
   continuously from the first heading's dot to the last one – the
   entries below the final dot carry no segment and the last heading
   clips its own segment at the dot. The dot sits on the heading's text
   line box: h3 is 1.125rem/1.5 (27px tall), so its centre is 0.84375rem
   below the text top, plus the heading's 1.75rem top padding on all but
   the first. */
.timeline.tl-heading-dots > :is(h2, h3)::before {
  content: ""; position: absolute; left: -1.25rem; top: 0; bottom: 0;
  width: 1px; background: var(--separator);
}
.timeline.tl-heading-dots > :is(h2, h3)::after {
  content: ""; position: absolute; left: calc(-1.25rem - 2.5px); top: 2.40625rem;
  width: 0.375rem; height: 0.375rem;
  border: 1px solid var(--muted); background: var(--muted);
}
.timeline.tl-heading-dots > :is(h2, h3):first-child::before { top: 0.84375rem; }
.timeline.tl-heading-dots > :is(h2, h3):first-child::after { top: 0.65625rem; }
/* Rail terminus: entries with no org heading after them sit below the
   last dot, so they draw no line... */
.timeline.tl-heading-dots .tl-title:not(:has(+ :is(h2, h3)))::before { content: none; }
/* ...and the section's last heading (last-of-type among the org names)
   stops its segment at its own dot. Never nest :has() inside :has() –
   Chromium rejects that, silently dropping the whole rule. */
.timeline.tl-heading-dots > :is(h2, h3):last-of-type:not(:first-child)::before {
  bottom: auto; height: 2.59375rem;
}
/* Sole org: the one heading is both first and last, zero-length rail. */
.timeline.tl-heading-dots > :is(h2, h3):first-child:last-of-type::before { content: none; }

/* The wrapper's final dot also gives back its trailing padding. */
.timeline .tl-title:last-child { padding-bottom: 0; }

.tl-title { padding: 0 0 1.25rem; }
.tl-title > :last-child { margin-bottom: 0; }
/* head, meta and mode are <p>s, so .prose p's 1.5rem margin beats a bare
   class; .timeline joining the selectors is what makes the tight
   role/period/mode rhythm actually stick. */
.timeline .tl-title-head {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem;
  margin: 0 0 0.25rem; color: var(--fg);
}
.tl-title-text { font-weight: 600; }
.timeline .tl-title-meta, .timeline .tl-title-mode { margin: 0 0 0.25rem; color: var(--muted); font-size: 0.875rem; }
.timeline .tl-title-mode { margin-bottom: 0.5rem; }
/* Pills inside a title head sit flush right; their look is the shared
   Pills module down by the buttons. */
.tl-title-head .pill { margin-left: auto; }

/* ---------- HTML niceties (raw elements usable in any .md) ---------- */
.prose kbd {
  display: inline-block; padding: 0.125rem 0.375rem; font-size: 0.8125rem;
  border: 1px solid var(--border); border-bottom-width: 2px; border-radius: 0.25rem;
  background: var(--surface); color: var(--fg);
}
.prose mark { background: color-mix(in srgb, var(--yellow) 28%, transparent); color: inherit; }
.prose del { color: var(--muted); }
.prose ins { text-decoration: none; border-bottom: 1px solid var(--green); }
.prose dl { margin: 0 0 1.5rem; }
.prose dt { font-weight: 600; color: var(--fg); }
.prose dd { margin: 0 0 0.75rem 1rem; color: var(--muted); }
/* Accordions – the docs site's collapsible rows, 1:1: framed box, a
   min-height summary row with a + / − marker pinned right, surface hover,
   hairline under the closed row. The component form adds the docs' tinted
   body panel: <collapse title="…"> body </collapse> (components.html). */
.prose details, details.collapse { margin: 0 0 1.5rem; border: 1px solid var(--border); }
.prose summary, .collapse > summary {
  cursor: pointer; list-style: none; position: relative;
  display: flex; align-items: center; min-height: 4rem;
  padding: 0.875rem 2.5rem 0.875rem 0.75rem;
  font-weight: 600; color: var(--fg);
}
.prose summary::-webkit-details-marker, .collapse > summary::-webkit-details-marker { display: none; }
.prose summary::after, .collapse > summary::after {
  content: "+"; color: var(--muted); font-weight: 400;
  position: absolute; right: 0.75rem;
}
.prose details[open] > summary::after, .collapse[open] > summary::after { content: "-"; }
.prose summary:hover, .collapse > summary:hover { background: var(--surface); }
.prose details[open] > summary, .collapse[open] > summary { border-bottom: 1px solid var(--border); }
.prose details > :not(summary) { margin-inline: 1.25rem; }
.prose details[open] { padding-bottom: 0.5rem; }
.prose details p:last-child, .prose details ul:last-child { margin-bottom: 0; }
.collapse-body {
  border-top: 1px solid var(--border);
  background: color-mix(in srgb, var(--surface) 55%, transparent);
  padding: 1.5rem 1.25rem 2rem;
}
.collapse-body > :last-child { margin-bottom: 0; }
.prose figure { margin: 0 0 1.5rem; padding: 1rem; border: 1px solid var(--separator); }
.prose figcaption { margin-top: 0.5rem; font-size: 0.875rem; color: var(--muted); }
.prose figure pre:last-child, .prose figure p:last-child { margin-bottom: 0; }

/* ---------- Cards (docs-site .docs-card metric) ---------- */
.grid { display: grid; gap: 0.75rem; }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

.card {
  display: block; color: var(--fg);
  border: 1px solid var(--border); border-radius: 0;
  padding: 1rem; background: transparent;
}
.card:hover { background: var(--surface); text-decoration: none; }
.card h2, .card h3 { margin: 0.375rem 0; font-size: 0.875rem; line-height: 1.5; }
/* Car listing cards (the home page's "Свіжі Надходження" and any other
   post_card car) carry the name as the card's headline: one step up
   from the generic card title so it holds its own against the 4:3
   photo and the 1.4rem price under it. */
.card:has(.card-specs) h2, .card:has(.card-specs) h3 { font-size: 1.125rem; line-height: 1.5; }
.card-meta {
  display: flex; justify-content: space-between; gap: 0.5rem;
  font-size: 0.75rem; color: var(--muted);
}
.card-sub { margin: 0 0 0.375rem; color: var(--muted); }
.card-desc { margin: 0 0 0.5rem; color: var(--muted); }

/* Cards written straight in content: <cards cols={2}> + <card title=""
   href=""> description </card> (components.html). Docs-site metric:
   .docs-card-group grid driven by --card-columns, cards = the same 1px
   frame as .card above, title as <strong>, muted description. */
.docs-card-group {
  display: grid; gap: 0.75rem; margin-bottom: 1.5rem;
  grid-template-columns: repeat(var(--card-columns, 2), minmax(0, 1fr));
}
/* !important mirrors the docs site: inside .prose a plain link colour rule
   would otherwise outweight this single class. */
.docs-card { display: block; color: var(--fg) !important; border: 1px solid var(--border); padding: 1rem; }
.docs-card:hover { background: var(--surface); text-decoration: none; }
.docs-card strong { display: block; margin-bottom: 0.375rem; }
.docs-card div { color: var(--muted); }
.docs-card div > :last-child { margin-bottom: 0; }
@media (width <= 48rem) { .docs-card-group { grid-template-columns: 1fr; } }

/* Tag chips – docs-site .api-meta metric (one size everywhere) */
.chips { display: inline-flex; flex-wrap: wrap; gap: 0.625rem; }
.chip {
  display: inline-flex; align-items: center; gap: 0.3rem;
  font-size: 0.75rem; color: var(--muted);
  border: 1px solid var(--border); border-radius: 0;
  padding: 0.375rem 0.625rem;
}
.chip:hover { color: var(--fg); border-color: var(--muted); text-decoration: none; }
.chip-count { color: var(--muted); font-size: 0.6875rem; }
/* Folder-style chips (kind = "folder" in tag_meta) – same look as the filter
   sidebar’s .tag-folder entries: a name plus a small square marker, in the
   same muted shade as the other chips. */
.chip-folder::before {
  content: ""; flex: 0 0 auto;
  width: 0.375rem; height: 0.375rem; border: 1px solid currentColor;
}
.tag-cloud { list-style: none; display: flex; flex-wrap: wrap; gap: 0.625rem; margin: 0 0 2rem; padding: 0; }

/* List rows (cars / projects index) */
.rows { border-top: 1px solid var(--separator); }
.row {
  display: grid; grid-template-columns: 8.5rem minmax(0, 1fr);
  gap: 1.5rem; padding: 1.25rem 0;
  border-bottom: 1px solid var(--separator);
}
.row-meta { font-size: 0.75rem; color: var(--muted); padding-top: 0.3rem; }
.row-body h2 { margin: 0; font-size: 1.125rem; line-height: 1.5; }
.row-body h2 a { color: var(--fg); }
.row-body h2 a:hover { color: var(--link); text-decoration: none; }
/* Car and parts rows keep their title ink on hover, in both views: the
   row/card itself is the affordance (it tints, the photo swaps, the whole
   box is clickable), so an accent title read as a second, separate link. */
.rows .row.row-cars .row-body h2 a:hover { color: var(--fg); }
.row-sub { margin: 0.15rem 0 0; color: var(--muted); }
.row-desc { margin: 0.35rem 0 0.5rem; color: var(--muted); }
.row-foot { display: flex; flex-wrap: wrap; gap: 0.625rem; align-items: center; margin: 0.5rem 0 0; color: var(--muted); }
/* Rows that carry preview images: when ANY post in the section has one, the
   thumb column is reserved on every row so bodies stay aligned (pure CSS –
   Zola 0.23 dropped the Tera filters/namespaces that would do this in the
   template). */
.rows:has(.row-thumb) .row { grid-template-columns: 8.5rem 11rem minmax(0, 1fr); }
.rows:has(.row-thumb) .row > .row-body { grid-column: 3; }

/* ---------- Listing preview images – set [extra] preview = "file" -------------
   Each slot keeps the ratio its template asked resize_image for: 4:3 for
   priced listings (car/part photography is shot in that ratio), 16:9 for
   everything else. No CSS aspect-ratio here – the width/height attributes Zola writes
   from the resized file already define the box, so the layout stays stable
   and both ratios render correctly from the same rules. */
.card-thumb, .row-thumb { display: block; color: inherit; }
.card-thumb img, .row-thumb img {
  display: block; width: 100%; height: auto; border-radius: 0; object-fit: cover;
}
.card-thumb {
  margin: -1rem -1rem 0.75rem;      /* full-bleed inside the card padding */
  border-bottom: 1px solid var(--separator);
}
.row-thumb { align-self: start; border: 1px solid var(--border); }
.card-thumb:hover, .row-thumb:hover { text-decoration: none; }

/* ---------- Car listings – specs on cards/rows, spec sheet on the page -----
   The card/row headline block is emitted by the car_specs component
   (components.html) on every page where the listing's front matter carries
   an [extra].price: the price line, then a 2×2 grid of icon specs (odometer
   + mileage, pump + fuel and displacement, gear + transmission, steering
   wheel + drivetrain). Fields the listing omits simply leave their cell out,
   so a row can show three specs instead of four. */
.card-specs, .row-specs { display: flex; flex-direction: column; gap: 0.5rem; margin: 0.5rem 0; }
.row-specs { margin: 0.4rem 0 0; }
/* Car cards (post_card emits .card-specs) become flex columns so the spec
   block is pinned to the card's bottom edge: cards in one row end up the
   same height, and the price + icon grid lines up across them no matter
   how many lines a title took. Non-car cards keep plain block flow. */
.card:has(.card-specs) { display: flex; flex-direction: column; }
.card:has(.card-specs) .card-specs { margin-top: auto; }
/* Prices carry the brand colour on every surface (cards, rows, listing
   page) and one shared size everywhere: 1.4rem, 20% under the heading
   step it used to sit at, so the number reads as part of the row rather
   than shouting over the title (the listing page's .car-price-big
   matches it). The accent is checked for contrast against both theme
   inks. */
.car-price { font-size: 1.4rem; font-weight: 700; color: var(--accent); }
/* Sale-status pill (car_status component): always rendered on every car
   listing. List view puts it in the title header, justified to the right
   edge (see .row-title); grid view and the post cards instead park it on
   the price line, also right-aligned (see .car-price-line below). All
   three slots are in the markup and CSS shows exactly one per surface –
   no JS. The modifier class is the status slug (status | lower |
   spaces→dashes), so the colours below stay in sync with status_options
   in content/cars/_index.md. All statuses are outline chips: "В наявності"
   takes the accent, the exceptions get their own signal colours, and
   "Продано" stays neutral. The year leads every car title (car-year):
   it lives inside the heading itself, so it inherits the title's font
   outright and takes the title's own colour (never a different ink). */
.row-title { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.375rem 0.75rem; }
/* Every status chip carries the tinted recipe the timeline pills use
   (.pill-green etc., further down): ink in the signal colour, a 45%
   tint of the same hue for the hairline, and a 12% wash behind it. A
   plain outline chip next to those looked like a different component,
   so the tint is now the chip's default everywhere – the neutral
   "Продано" and "Б/В" states just take the muted hue for all three. */
.car-status {
  font-size: 0.8125rem; font-weight: 700; letter-spacing: 0.02em;
  padding: 0.1875rem 0.625rem; border: 1px solid color-mix(in srgb, var(--muted) 45%, transparent);
  color: var(--muted); background: color-mix(in srgb, var(--muted) 12%, transparent);
  white-space: nowrap;
}
.car-status-в-наявності { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, transparent); background: color-mix(in srgb, var(--accent) 12%, transparent); }
/* "В дорозі" reads green: a car en route is still live stock, just not
   on the lot yet – the same "all good" signal as the accent, kept
   distinct only by hue. */
.car-status-в-дорозі { color: var(--green); border-color: color-mix(in srgb, var(--green) 45%, transparent); background: color-mix(in srgb, var(--green) 12%, transparent); }
.car-status-розбір { color: var(--red); border-color: color-mix(in srgb, var(--red) 45%, transparent); background: color-mix(in srgb, var(--red) 12%, transparent); }
/* Parts statuses (content/parts/_index.md): "Новий" reads green like a
   fresh stock signal, "Б/В" (used) takes the neutral wash above – its
   slug is `б-в` because the component dashes the slash too. */
.car-status-новий { color: var(--green); border-color: color-mix(in srgb, var(--green) 45%, transparent); background: color-mix(in srgb, var(--green) 12%, transparent); }
/* The price line (pill + price) is the pill's home on cards and in grid
   view: the pill sits first and the price is justified to the far right.
   List view moves the pair up into the title header instead (.car-title-
   slots, below): both line slots hide, so the line keeps only the icon
   spec grid. CSS-only swap via the baked data-view – specificity stays
   under the hidden-row guards (0,5,0). */
.car-price-line { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.car-price-line .car-price { margin-left: auto; }
.rows[data-view="list"] .car-price-line .car-price { display: none; }
.rows[data-view="list"] .car-price-line .car-status { display: none; }
/* Price/pill height match: the pill's box is fixed explicitly (line-height
   1.8 × its 0.8125rem font + 0.375rem padding + 0.125rem border =
   1.9625rem) and every price on the site gets exactly that box height
   too: line-height 1.3989 is 1.9625rem at the one price font size
   (1.4rem), so price and pill texts sit on same-height boxes wherever
   they appear side by side, and the number nearly fills the pill's
   height. */
.car-price-line .car-status { line-height: 1.8; }
/* B43: the draft ad's status pill IS the "Чернетка" chip (car_status,
   components.html) – it replaces "В наявності" in every slot, listing
   rows and the ad's own rail alike. Same frame as every status chip with
   its own signal colour: the tint recipe the admin chrome's red delete
   chips and green add button use – a 12% wash behind the ink – just in
   the draft's own yellow. The chip only ever ships in the admin build
   (the public one never loads drafts), but the recipe belongs with the
   other status chips, not in the editor sheet. */
.car-status-draft {
  color: #d0b100;
  border: 1px solid color-mix(in srgb, #d0b100 55%, var(--border));
  background: color-mix(in srgb, #d0b100 12%, transparent);
}
.car-status-draft:hover { background: color-mix(in srgb, #d0b100 22%, transparent); }
.car-price { height: 1.9625rem; line-height: 1.3989; }
/* Title header slots (list view only): the price rides the title line
   justified right with the status pill stacked under it, an echo of the
   specs line's pair. Grid view and post cards hide them and keep the
   pair on the line. The title and the slot column align by baseline,
   i.e. the heading sits up on the price's line instead of floating
   centered against the taller two-slot stack.
   The slots stack in a right-aligned column, so both
   flush to the header's right edge; the fixed 1.9625rem slot heights
   (the title pill matches here, not via .car-price-line) keep price and
   pill on same-height boxes. */
.car-title-slots { display: none; flex-direction: column; align-items: flex-end; gap: 0.25rem; margin-left: auto; }
/* Both slot wrappers are flex containers, which is what makes the fixed box
   heights above actually paint: inside a plain inline span the price would
   instead grow to the inherited line-height strut, so its box would only
   match the pill by coincidence of font size. */
.car-title-price, .car-title-pill { display: flex; }
.rows[data-view="list"] .row-title .car-title-slots { display: flex; }
.rows[data-view="list"] .car-title-pill .car-status { line-height: 1.8; }
/* Sold ("Продано") listings are done deals: the content reads grey –
   grayscale hits the photo and every accent (price, year, pill border)
   alike, the slight fade keeps them visually behind the live stock. It
   rides the data-status attribute both rows (section.html) and cards
   (post_card) bake, so no extra class is needed. On a row the wash
   reaches the content parts, NOT the row-admin action line: the filter
   and opacity ride the element they hit, and no child can undo a
   parent's grayscale – greyed buttons read dead, while the actions
   themselves must stay pressable colour (B44). The parts not named
   below are exactly .row-admin and its badge, left in normal colour.
   The status pill stays out of the wash too, and for the same reason:
   the draft ad's Чернетка chip keeps its own yellow even when that ad
   also reads "Продано" – a greyed draft badge hides the one fact the
   editor is looking at (B45). So the wash never lands on the whole
   title or specs block (where it would sweep the pill up with it),
   only on the leaf parts: photo, title heading, price and icon specs. */
.row[data-status="Продано"] > .row-thumb,
.row[data-status="Продано"] > .row-body > :not(.row-admin):not(.row-title):not(.row-specs),
.row[data-status="Продано"] > .row-body > .row-title > h2,
.card[data-status="Продано"] > :not(.card-specs),
.row[data-status="Продано"] .car-price,
.row[data-status="Продано"] .car-ispecs,
.card[data-status="Продано"] .car-price,
.card[data-status="Продано"] .car-ispecs { filter: grayscale(1); opacity: 0.8; }
.row[data-status="Продано"]:hover > .row-thumb,
.row[data-status="Продано"]:hover > .row-body > :not(.row-admin):not(.row-title):not(.row-specs),
.row[data-status="Продано"]:hover > .row-body > .row-title > h2,
.card[data-status="Продано"]:hover > :not(.card-specs),
.row[data-status="Продано"]:hover .car-price,
.row[data-status="Продано"]:hover .car-ispecs,
.card[data-status="Продано"]:hover .car-price,
.card[data-status="Продано"]:hover .car-ispecs { opacity: 1; }
.car-ispecs {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.4rem 1rem;
}
.car-ispec {
  display: flex; align-items: center; gap: 0.4rem; min-width: 0;
  font-size: 0.8125rem; color: var(--fg);
}
/* Spec glyphs carry the brand colour, the value stays ink – the pair reads
   as icon + fact without needing labels. */
.car-ispec .icon { flex: 0 0 auto; color: var(--accent); }

/* List/grid view toggle on /cars/ (site.js stores the choice). Same control
   square as every other button; the pressed one is outlined in the accent. */
.view-toggle { display: flex; gap: 0.5rem; margin: 0 0 1.25rem; }
.view-btn {
  display: inline-flex; align-items: center; gap: 0.4rem;
  height: 2.25rem; padding: 0 0.75rem;
  font: inherit; font-size: 0.8125rem;
  color: var(--muted); background: transparent;
  border: 1px solid var(--border); border-radius: 0; cursor: pointer;
}
.view-btn:hover { color: var(--fg); border-color: var(--muted); }
.view-btn[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); }
/* Cars toolbar: view toggle left, sort dropdown justified right.
   The toggle's inner gap is pulled up to the bar's own 0.75rem so the
   "Сітка"/"Список" pair and the admin bar's "Додати оголошення" that follows
   sit on one evenly-spaced rhythm (the sort field keeps its own margin-
   left:auto jump to the right edge). */
.cars-bar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem;
  margin: 0 0 1.25rem;
}
.cars-bar .view-toggle { margin: 0; gap: 0.75rem; }
/* Sort: the dropdown recipe of the panel's "Марка" field (see brand-dd
   above) – an invisible checkbox precedes the field, its :checked state
   opens the overlay panel, and the ::after chevron rotates with it (the
   gradient-drawn arrow of the old <select> is gone: the chevron IS the
   dropdown icon now). The 2.25rem box matches the neighbouring view
   buttons; fill = --bg like every other field on the page (see
   .filter-num). */
.sort-dd-switch {
  position: absolute; width: 1px; height: 1px;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}
/* The wrapper carries the justification (the checkbox sits out of flow)
   and anchors the panel, which opens flush with the field's right edge
   – right:0 so a full-width field on phones cannot push the panel past
   the viewport. */
.sort-dd { position: relative; margin-left: auto; }
/* Constant field width regardless of the mode shown: the old content-sized
   box jumped every time a choice was mirrored into the label. The site face
   is monospace, so one character is exactly 1ch and the longest mode –
   "Пробіг: високий спочатку", 24 chars (baked on cars only, see
   [extra].sort_mileage) – pins the width down to the pixel. The 2.4375rem
   on top is the field's own furniture: 2×0.75rem padding,
   the 0.5rem gap and the 0.4375rem chevron; the 2px are the borders
   (box-sizing is border-box site-wide). */
.sort-dd-label {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  /* B50 (test report): "Пробіг: високий спочатку" overflowed the field by
     ~1px. The old calc sized the text at 24ch, and ch resolves against
     the "0" glyph – OpenTUI Mono's Cyrillic advances run a hair wider
     (measured live: 24 Cyrillic glyphs = 187.77px vs 24ch = 187.20px),
     which is exactly the overflow. Still a FIXED width sized to the
     widest option (user rule: the field never resizes between modes),
     just sized with 3px of headroom on top of the true widest; nowrap
     guarantees one line even if a future label grows. */
  width: calc(24ch + 2.625rem + 2px);
  white-space: nowrap;
  height: 2.25rem; padding: 0 0.75rem;
  font-size: 0.8125rem; color: var(--muted);
  background: var(--bg); border: 1px solid var(--border); cursor: pointer;
}
.sort-dd-label:hover { color: var(--fg); border-color: var(--muted); }
.sort-dd-label::after {
  content: ""; flex: none;
  width: 0.4375rem; height: 0.4375rem;
  border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted);
  transform: rotate(45deg); transition: transform 0.15s ease;
}
.sort-dd-switch:checked ~ .sort-dd .sort-dd-label::after { transform: rotate(-135deg); }
/* Keyboard focus lands on the invisible checkbox: no ring is painted on
   the visible field – the house style carries no focus rings at all. */
/* Overlay panel: same opaque page-colour fill, border and scroll recipe
   as the make panel – it floats over the listing rows. Width: 100% pins
   it to the field's own box (the wrapper shrink-wraps the fixed-width
   label), so open and closed read as one column – every option fits one
   line because the field is sized by the longest option already. The
   max-height fits the tallest panel – the cars section's eight modes
   measure 297px content – with a little slack for any future option,
   so no scrollbar ever splits the list. */
.sort-dd-panel {
  display: none; position: absolute; right: 0; top: calc(100% + 0.3125rem);
  z-index: 20;
  width: 100%; min-width: 100%; max-width: min(20rem, 88vw);
  max-height: 19rem; overflow-y: auto; overscroll-behavior: contain;
  padding: 0.625rem 0.75rem;
  background: var(--bg); border: 1px solid var(--border);
}
.sort-dd-switch:checked ~ .sort-dd .sort-dd-panel { display: block; }
/* Panel header echoes the filter groups' legend (.filter-title). */
.sort-dd-title {
  margin: 0 0 0.5rem; padding: 0;
  font-size: 0.75rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--fg);
}
/* The rows type at the field's own 0.8125rem (not the facet panels'
   0.875rem): the panel is literally this field's option list – a picked
   row becomes the field's text – and the shared size is what keeps the
   longest option inside the field-pinned panel width on one line. */
.sort-dd-panel .filter-check { font-size: 0.8125rem; }
/* Radio rows reuse the panel's checkbox-row layout (.filter-check: the
   row metrics, hover ink and the checked-row bolding via :has() –
   input-type-agnostic), but with NO checkbox squares: the single choice
   reads through the row's own ink + bold (the active mode) plus the
   field's label mirroring it. The radios stay in the DOM and keyboard
   reachable (arrows move the choice, Tab reaches the group) – only the
   box itself is clipped out of the layout. */
.sort-dd-panel .filter-check input[type="radio"] {
  position: absolute; width: 1px; height: 1px;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}
/* The radios have no ring painted on focus – no focus rings anywhere. */

/* Car rows drop the date column, so their grid is thumb + body only. These
   sit after the generic .rows rules and out-specify them (the :has()
   argument counts toward specificity), which is also why the row-body
   column has to be re-pinned. The thumb column is sized to the 2×4:3
   photo slot (280 CSS px at 1× = 17.6rem; on HiDPI the 560px bake covers
   up to ~1.6× device pixels). */
.rows:has(.row-cars) .row.row-cars { grid-template-columns: 17.6rem minmax(0, 1fr); }
.rows:has(.row-cars) .row.row-cars > .row-body { grid-column: 2; }
/* A priced listing without a [extra].preview (the parts samples have no
   photos yet) collapses the thumb track: without the .row-thumb element
   the 17.6rem column would be an empty gutter before the body. */
.rows .row.row-cars:not(:has(.row-thumb)) { grid-template-columns: minmax(0, 1fr); }
.rows .row.row-cars:not(:has(.row-thumb)) > .row-body { grid-column: 1; }
/* Rows and cards open the listing on a click anywhere (site.js, both
   views), so the row takes
   the link's cursor as a hint (on the row rather than the text column, so
   it survives the phone rule that dissolves the body box), the grey hover
   fill the grid cards have, and the title takes the accent colour as soon
   as any part of the row is hovered (not just the title's own link). The
   body is a flex column with the spec block riding to its bottom edge,
   lining the specs up with the bottom of the tall 2× preview (the rows'
   grid cell stretches the body to that height). */
.rows .row.row-cars { cursor: pointer; }
.rows[data-view="list"] .row.row-cars .row-body {
  display: flex; flex-direction: column;
}
.rows[data-view="list"] .row.row-cars .row-specs { margin-top: auto; }
/* The hover cast paints inside the row's own box – the earlier negative
   side margins bled the tint (and the row's separator hairline) 0.75rem
   past the content column on both sides. Instead the row just takes 0.75rem
   of right padding: the right-aligned price/pill slots move off the column
   edge, while the photo, title and separator keep their normal width. */
.rows[data-view="list"] .row.row-cars { padding-right: 0.75rem; }
.rows[data-view="list"] .row.row-cars:hover { background: var(--surface-hover); }

/* Grid view: .rows becomes a card grid and every row turns into a card.
   site.js flips data-view. B45: the frame is the HOME card's frame,
   copied exactly (see .card above): the page shows through the card
   (transparent, so the card reads as dark as the page), 1rem of padding
   all around, and the photo bleeds to the frame's edges with its own
   hairline directly under the shot – the dot row sits under that line,
   in the card's body, exactly like the home cards. The old surface-fill
   made grid cards a step lighter than the home strip; the hairline used
   to close the whole thumb (dots included). */
.rows[data-view="grid"] {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
  gap: 1.5rem; border-top: 0;
}
.rows[data-view="grid"] .row {
  display: flex; flex-direction: column; gap: 0; padding: 0;
  background: transparent; border: 1px solid var(--border);
}
.rows[data-view="grid"] .row > .row-thumb {
  width: auto; align-self: stretch;
  margin: 0 0 0.375rem;      /* B46: the home 0.75rem rides above the dots */
  border: 0;
}
/* The hairline rides the photo itself, not the thumb's bottom edge: the
   dot band hangs below it (inside the thumb), and the home card draws
   the line directly under the shot too. First child is the <img> with
   no JS, the .car-thumb-track span once site.js builds the slides. */
.rows[data-view="grid"] .row-thumb > :first-child {
  border-bottom: 1px solid var(--separator);
}
/* The body carries the home card's side and bottom padding (the frame
   has none, so the photo can bleed edge to edge); a photo-less row –
   the parts samples – keeps the full 1rem ring of breath. */
.rows[data-view="grid"] .row-body { padding: 0 1rem 1rem; }
.rows[data-view="grid"] .row:not(:has(.row-thumb)) > .row-body { padding: 1rem; }
/* Same bottom-pinned specs as the post cards: the body fills the framed
   row and the spec block rides to the bottom, so a one-line title card
   still has its price and icon grid on the same height as its neighbours.
   (Car rows bake no row-foot, so nothing trails the specs.) */
.rows[data-view="grid"] .row-body { display: flex; flex-direction: column; flex: 1 1 auto; }
.rows[data-view="grid"] .row-body > .row-specs { margin-top: auto; }
/* B50 (test report: grid cards misaligned): three jitter sources lined
   the same wall badly. (1) A multi-photo card grows a dot band under
   the photo, a single-photo one never does, so their text starts 1.75rem
   apart – the slot is now reserved on every thumb (band = the B46 0.75rem
   above the dots + 0.375 padding + 0.375 dots + 0.25 padding), empty on
   cards without one. (2) One-line vs three-line titles moved the price
   line – two lines are reserved (3em = exactly two 1.5 line-heights of
   the 1.125rem title), longer titles still stack. (3) A long part code
   blew its spec column wide – inside .car-ispec it now breaks anywhere. */
.rows[data-view="grid"] .row > .row-thumb:not(:has(.car-thumb-dots))::after {
  content: ""; display: block; flex: none; height: 1.75rem;
}
.rows[data-view="grid"] .row-title h2 { min-height: 3em; }
.car-ispec { overflow-wrap: anywhere; }
/* Car cards keep a fixed breath between the title and the price/pill
   line even on full-height cards (where the auto margin collapses to
   nothing): it equals the 0.5rem the specs' own column gap puts under
   that line, so the title, pill and icon grid read evenly spaced. */
.rows[data-view="grid"] .row.row-cars .row-body { gap: 0.5rem; }
.rows[data-view="grid"] .row-foot { margin-top: 0.75rem; }
.rows[data-view="grid"] .row:hover { background: var(--surface); }

/* ---------- Row photo dots + touch photo carousels (js/site.js) --------
   Listing and home thumbs backed by several photos grow a dot per shot,
   on both input kinds: GRID cards and home cards wear the dot row under
   the photo (js puts a thumb's dots inside its link, and the link stacks
   photo over dots; on home cards the dots are the thumb span's next
   sibling), LIST rows overlay them on the photo as a translucent chip –
   the lightbox counter's look (#0c0d0e99 plate, hairline border), so
   the dots stay legible over any shot without banding the photo.
   Desktop paints the dots only: the cursor sweep is that device's
   seeking gesture, and it lights the dot of the photo on screen. Touch
   screens get an actual carousel in grid view and on the home cards:
   the shots ride a scroll-snap track (the .car-thumb-track span js
   builds inside the thumb link), dragged by the browser's own fling and
   snap, the scroll marking the dots. A touch list row never scrubs and
   shows no dots at all – js still builds its track and dots because the
   view toggle can flip the same DOM to grid at any time, and main.css
   switches the look: track scrollable and dots live in grid view, track
   a plain box and dots hidden in list. None of this exists without JS
   (site.js builds the dots, slides and track), so the no-JS thumb is
   the plain single-photo box it always was and .car-thumb-dots hides. */
.car-thumb-dots { display: none; }
.car-thumb-dots i {
  display: block; width: 0.375rem; height: 0.375rem;
  background: var(--border);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.55);
}
.car-thumb-dots i.is-on { background: var(--accent); }
/* The dot band's frame of reference, and the grid card's stack. */
.rows .row > .row-thumb { position: relative; }
.rows[data-view="grid"] .row > .row-thumb { display: flex; flex-direction: column; }
.rows[data-view="grid"] .row-thumb > .car-thumb-dots {
  display: flex; justify-content: center; gap: 0.375rem;
  padding: 0.375rem 0 0.25rem;            /* the home cards' dot rhythm (B45) */
  /* B46: and the home cards' position too. There the band hangs 12px
     below the photo's hairline (the thumb's own bottom margin) with 6px
     of breath under it before the text; here the band sat flush on the
     hairline, so the dots read pinned to the photo. The 0.75rem rides
     ABOVE the dots now – they sit midway between the shot and the body,
     centred like on the home card. The thumb keeps 0.375rem of breath
     below the band (its old 0.75rem moves up here), and a photo-less
     JS thumb – or the no-JS single shot, where this band hides – still
     keeps that 6px under its hairline. */
  margin-top: 0.75rem;
}
.rows[data-view="list"] .row-thumb > .car-thumb-dots {
  /* The lightbox chip recipe (.lightbox-count): a plate with a light
     hairline, hugging the dots and floating on the photo – not a band
     across it. B46: the plate is opaque now – the same page colour the
     grid cards' frames sit in, at full strength – so over a light shot
     it reads as the dark card chip it matches instead of washing out. */
  display: flex; position: absolute; left: 50%; transform: translateX(-50%);
  bottom: 0.375rem; width: max-content;
  justify-content: center; gap: 0.375rem;
  padding: 0.1875rem 0.5rem;
  background: var(--bg); border: 1px solid #f2f2f233;
}
/* Home cards: dots under the thumb, the shape the touch strips always
   had – and js now builds them on hover devices too, next sibling of
   the thumb inside the card link, so this paints identically with and
   without a mouse. */
.grid .car-thumb-dots {
  display: flex; justify-content: center; gap: 0.375rem;
  padding: 0.375rem 0 0.25rem;
}
@media (hover: none) {
  /* A touch list row's photo never scrubs, so its dots don't paint –
     js still built them (the view toggle can flip this DOM to grid),
     they just stay invisible here. */
  .rows[data-view="list"] .row-thumb > .car-thumb-dots { display: none; }
  /* The track is a plain box in list view – the extra slides behind the
     preview exist but never paint or scroll. */
  .row-thumb .car-thumb-track { display: block; }
  .row-thumb .car-thumb-slide:not(:first-child),
  .card-thumb .car-thumb-slide:not(:first-child) { display: none; }
  .rows[data-view="grid"] .row > .row-thumb .car-thumb-track,
  .grid .card-thumb.car-thumb-carousel {
    display: flex; overflow-x: auto;
    scroll-snap-type: x mandatory; overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .rows[data-view="grid"] .row > .row-thumb .car-thumb-track::-webkit-scrollbar,
  .grid .card-thumb.car-thumb-carousel::-webkit-scrollbar { display: none; }
  .rows[data-view="grid"] .row > .row-thumb .car-thumb-track .car-thumb-slide,
  .grid .card-thumb.car-thumb-carousel .car-thumb-slide {
    display: block; flex: 0 0 100%; width: 100%;
    scroll-snap-align: start; scroll-snap-stop: always;
  }
}

/* ---------- Facet filter panel on /cars/ (car_filters component) ----------
   Lives in the .sidebar slot, which is widened on this page (see
   .layout-list:has(.car-filters) next to the layout rules) so the group
   titles and the "Від"/"До" boxes fit on one line. Every value inside one
   group is OR-ed with its siblings and AND-ed with the other groups
   (site.js). Checkboxes are custom square chips – empty square off,
   accent-filled square on – and the slider is a bare square-ended line
   with a square accent thumb; appearance:none keeps the same keyboard
   behaviour as the native controls. */
/* Desktop panel layout: one column – the form is a flex column and the
   two DOM column wrappers (car_filters) dissolve into it with
   display: contents, so the per-group order numbers below set the
   sequence: "Стан автомобіля", "Марка", "Ціна", "Пробіг до", "Рік випуску",
   "Привід", "Вид пального", "Коробка передач", "Тип кузова" ("Тип" slots
   between "Стан" and "Марка" on parts). The numbers leave gaps in the tens
   so a new facet can slot between any two neighbours without renumbering.
   On phones the wrappers stop dissolving and become the panel's two
   columns instead (see the ≤48rem block). */
.car-filters .filter-form { display: flex; flex-direction: column; }
.car-filters .filter-col { display: contents; }
/* "Адмін" (the draft facet the admin build adds, see car_filters): leads
   the stack above "Стан" – the tens leave that slot free on purpose. */
.filter-group[data-facet="draft"]    { order: 5; }
.filter-group[data-facet="status"]   { order: 10; }
.filter-group[data-facet="type"]     { order: 15; }
.filter-group[data-facet="brand"]    { order: 20; }
.filter-group[data-facet="price"]    { order: 30; }
.filter-group[data-facet="mileage"]  { order: 40; }
.filter-group[data-facet="year"]     { order: 50; }
.filter-group[data-facet="drive"]    { order: 60; }
.filter-group[data-facet="fuel"]     { order: 70; }
.filter-group[data-facet="gearbox"]  { order: 80; }
.filter-group[data-facet="features"] { order: 90; }
.car-filters .filter-reset { order: 100; }
.filter-group { border: 0; padding: 0; margin: 0 0 1.375rem; min-width: 0; }
.filter-title {
  padding: 0; margin-bottom: 0.5rem;
  font-size: 0.75rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--fg);
}
.filter-check {
  display: flex; align-items: center; gap: 0.5rem;
  font-size: 0.875rem; color: var(--muted); cursor: pointer;
}
.filter-check:hover { color: var(--fg); }
.filter-check + .filter-check { margin-top: 0.375rem; }
.filter-check input[type="checkbox"] {
  flex: 0 0 auto; width: 0.875rem; height: 0.875rem; margin: 0;
  -webkit-appearance: none; appearance: none; border-radius: 0;
  background: transparent; border: 1px solid var(--muted); cursor: pointer;
}
.filter-check input[type="checkbox"]:hover { border-color: var(--fg); }
.filter-check input[type="checkbox"]:checked { background: var(--accent); border-color: var(--accent); }
/* Checked = this value is part of the active query: lift the label to ink.
   site.js needs no help here, :has() reads the state straight off the DOM. */
.filter-check:has(input:checked) { color: var(--fg); font-weight: 700; }

/* Square-ended track line + square "zipper" thumb, both squared off to
   match the checkboxes (border-radius is 0 site-wide anyway). Track and
   thumb art has to be written per engine (WebKit vs Gecko). */
.filter-slider {
  width: 100%; height: 1rem; margin: 0.5rem 0 0.375rem;
  -webkit-appearance: none; appearance: none;
  background: transparent; cursor: pointer;
}
.filter-slider::-webkit-slider-runnable-track {
  height: 0.1875rem;
  background: linear-gradient(var(--accent), var(--accent)) left / var(--cfp, 0%) 100% no-repeat, var(--border);
}
.filter-slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 0.8125rem; height: 0.8125rem; margin-top: -0.3125rem;
  background: var(--accent); border: 0;
}
.filter-slider::-moz-range-track { height: 0.1875rem; background: var(--border); }
.filter-slider::-moz-range-thumb { width: 0.8125rem; height: 0.8125rem; background: var(--accent); border: 0; border-radius: 0; }
.filter-out { display: block; font-size: 0.75rem; color: var(--muted); }
/* B50: applied filters get the accent (user request + report: a typed
   price bound and a moved odometer cap looked identical to empty
   controls). --cfp carries the thumb's share of the scale, set by
   cfApply; is-capped marks an active cap (a slider parked at max means
   "any mileage" – the readout stays muted, but the track still fills).
   The webkit track has no progress
   pseudo, so the fill is a gradient layer sized by --cfp (100% at max:
   the track is fully orange even parked at "any mileage" – a bare grey
   track read as the filter being off); Firefox paints its own
   ::-moz-range-progress. */
.filter-out.is-set { color: var(--accent); }
.filter-num.is-set { color: var(--accent); border-color: var(--accent); }
.filter-slider::-moz-range-progress { height: 0.1875rem; background: var(--accent); }

/* "Від"/"До" number rows (price, year). The pair is justified apart, so the
   second ("До") box closes exactly on the panel's content edge: its right
   edge is then the shared line every other control – the mileage slider,
   the "Усі марки" button and "Скинути фільтри" alike – already spans to. */
.filter-nums { display: flex; gap: 0.625rem; justify-content: space-between; }
.filter-num-label {
  display: flex; align-items: center; gap: 0.375rem;
  font-size: 0.8125rem; color: var(--muted);
}
/* The "Від"/"До" words sit in a fixed box as wide as the wider of the two
   ("Від" = 3 glyphs; the font is monospace throughout, so 3ch is exactly
   that) instead of their natural text width. Their number inputs then
   start at the same x: above 40rem both inputs are the fixed 4.75rem and
   the boxes line up in columns; below it the inputs stretch to the rest
   of the column, and with the words equal the leftovers are equal too.
   The mobile stack (see the ≤40rem block) makes all four boxes – price
   and year alike – the same length. */
.filter-num-word { flex: none; min-width: 3ch; }
.filter-num {
  width: 4.75rem; padding: 0.25rem 0.375rem;
  font: inherit; font-size: 0.8125rem; color: var(--fg);
  /* Field fill = the page background (near-black on the dark theme, white
     on the light one) rather than --surface, so the box reads as a hole cut
     in the page and the border alone carries its edge. */
  background: var(--bg); border: 1px solid var(--border); border-radius: 0;
}

.filter-reset {
  display: inline-flex; align-items: center; justify-content: center;
  width: 100%; height: 2rem; padding: 0 0.75rem;
  font: inherit; font-size: 0.8125rem; color: var(--muted);
  background: transparent; border: 1px solid var(--border);
  border-radius: 0; cursor: pointer;
}
.filter-reset:hover { color: var(--fg); border-color: var(--muted); }
/* B50: disabled while the panel is pristine (cfApply owns the flag) –
   the button steps back instead of advertising an action with nothing
   to undo. Disabled buttons take no pointer events, so the hover rule
   never competes here. */
.filter-reset:disabled { color: var(--border); border-color: var(--separator); cursor: default; }

/* "Марка" dropdown (car_filters): the make list (45 curated makes on
   /cars/, 15 selling ones on /parts/) never
   fits the panel as stacked checkboxes, so the group shows one "Усі марки"
   button and opens its checkbox list as an overlay under it – the sidebar
   column and the rows behind never shift. The open switch is a bare
   checkbox living OUTSIDE
   the fieldset (form-child sibling before it): site.js collects facet
   checkboxes from inside .filter-group[data-facet], so the toggle is
   invisible to it, and :checked ~ fieldset drives the panel with no JS.
   The panel is sized to its content but never narrower than the group, and
   capped at min(20rem, 88vw) so it stays inside the viewport on the phone
   column, where the group itself is only half the width. */
.brand-dd-switch {
  position: absolute; width: 1px; height: 1px;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}
/* No align-self fiddling: the phone panel's column wrappers are flex
   stacks, where a group always keeps its own height (main axis is
   vertical, stretch only ever means full width) – and the dropdown is
   anchored at the group's bottom edge, so it lands right under the
   button. */
.filter-group-brand { position: relative; }
.brand-dd-label {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  height: 2rem; padding: 0 0.75rem;
  font-size: 0.8125rem; color: var(--muted);
  /* Field fill = page colour (--bg) – see .filter-num. */
  background: var(--bg); border: 1px solid var(--border); cursor: pointer;
}
.brand-dd-label:hover { color: var(--fg); border-color: var(--muted); }
.brand-dd-label::after {
  content: ""; flex: none;
  width: 0.4375rem; height: 0.4375rem;
  border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted);
  transform: rotate(45deg); transition: transform 0.15s ease;
}
.brand-dd-switch:checked ~ .filter-group-brand .brand-dd-label::after { transform: rotate(-135deg); }
/* Group has an active selection: lift the closed button to ink, like a
   checked label elsewhere in the panel. */
.filter-group-brand:has(.brand-dd-panel input:checked) .brand-dd-label {
  color: var(--fg); border-color: var(--muted);
}
.brand-dd-panel {
  display: none; position: absolute; left: 0; top: calc(100% + 0.3125rem);
  z-index: 20;
  width: max-content; min-width: 100%; max-width: min(20rem, 88vw);
  max-height: 16rem; overflow-y: auto; overscroll-behavior: contain;
  padding: 0.625rem 0.75rem;
  /* Same page-colour fill as the fields above; it has to stay opaque since
     the panel floats over the listing rows. */
  background: var(--bg); border: 1px solid var(--border);
}
.brand-dd-switch:checked ~ .filter-group-brand .brand-dd-panel { display: block; }
/* Monochrome make badge (see the car_filters component): the SVG's alpha
   is the shape, currentColor paints it – the mark inherits the label's
   colour, so it fades muted, lights to ink on hover and bolds with the
   checked row without any per-brand colour. -webkit- prefixes for older
   WebKit; url() comes inline per option from get_url. */
.brand-mark {
  flex: none; width: 1rem; height: 1rem; background: currentColor;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain; mask-size: contain;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}
/* Phone-only collapse switch (car_filters): invisible here, and its label
   is display:none, so desktop shows the panel open with no control. The
   ≤48rem block paints the label as a "Фільтри" button; the input stays in
   the a11y tree (clipped, not display:none) for keyboard/screen-reader use. */
.filter-switch {
  position: absolute; width: 1px; height: 1px;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}
.filter-switch-label { display: none; }

/* Listing page right rail (car.html toc slot): a framed panel – "Ціна"
   header section, then price + contact buttons in the body – floating in
   layout-doc's 16rem third column, centred between the article and the
   column's right edge (same sticky offset as the .toc it replaces). Below
   64rem that column disappears and the rail becomes a bottom dock
   (responsive block further down); the article itself has no price
   headline anymore, so no dot after a price anywhere.
   Both toc-slot panels (price, and the admin build's second panel) sit
   inside one .car-rail wrapper; display:contents makes the wrapper
   vanish from the layout entirely, so on the public site the asides are
   still direct grid items and behave exactly as before. admin.css gives
   the wrapper a real box only in the admin build. */
.car-rail { display: contents; }
.car-buy {
  /* The panel's pinned top edge mirrors the leading photo's top: the
     content block starts 1.5rem under the header, the return button is
     36px tall and carries a matching 1.5rem bottom margin – the photo
     (and the rail beside it) starts 5.25rem below the bar. */
  position: sticky; top: calc(var(--header-height) + 5.25rem);
  display: flex; flex-direction: column; align-items: stretch;
  /* Right edge flush with the header's theme-switch button (both sit
     one gutter inside the frame); the panel hugs that edge in its
     column instead of centring in it. */
  width: calc(100% - 1.5rem); margin-left: auto; margin-right: 1.5rem;
  border: 1px solid var(--border);
}
/* Rail buttons and the price line read centred in the panel. */
.car-buy .btn { justify-content: center; }
.car-buy-head {
  margin: 0; padding: 0.625rem 1.25rem;
  border-bottom: 1px solid var(--separator);
  font-size: 0.75rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.04em;
}
.car-buy-body {
  display: flex; flex-direction: column; align-items: stretch;
  gap: 0.625rem; padding: 1.25rem;
}
.car-buy-price { margin: 0; font-size: 1.4rem; font-weight: 700; color: var(--accent); text-align: center; }
/* Sale status rides under the price (car.html): the pill component is the
   same outline chip the list rows carry, centred in the rail here and
   parked beside the price in the mobile dock (see the 64rem block). */
.car-buy-status { margin: 0; text-align: center; }
/* The pill is sized like the contact buttons it sits among: the rail's
   buttons are 2.25rem tall stretch children with the site's 0.875rem
   text, so the pill grows to the same height and full width and takes
   their size. The chip's own colours and hairline stay untouched. */
.car-buy .car-status {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 2.25rem; font-size: 0.875rem;
}
/* "Ціна:" introduces the price in the dock (see the responsive block);
   the desktop rail needs no label. */
.car-buy-label { display: none; }
/* Spec sheet: framed grid, one cell per declared front-matter field. The
   1px grid gap shows the separator-coloured grid background through, so
   the hairlines between cells come for free (column and row rules) while
   the outer frame stays the same 1px border every other framed element
   uses; cells paint --bg back over their own area. */
.car-specs-grid {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1px; background: var(--separator);
  border: 1px solid var(--border); margin: 0 0 1.5rem;
}
/* Parts sheets declare exactly five fields ("Рік випуску" / "Стан" /
   "Марка" / "Тип" / "Код запчастини"), where the four-column track would
   leave a
   grey-hole second row. The section opts into a fifth column
   ([extra].spec_cols = 5, printed as spec-cols-5 by car.html): one row,
   exactly five cells, no padding anywhere. The narrow steps below drop
   the fifth track back to two/one columns like every other sheet. */
.car-specs-grid.spec-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.car-spec-cell { padding: 0.625rem 0.875rem; background: var(--bg); }
/* No filler cells anywhere: when the declared cell count doesn't fill the
   last row, the cell that starts it stretches across the columns it would
   otherwise leave as grey separator holes – the offsets below read off the
   remainder, so every sheet ends flush. They live behind the wide-width
   gate because the offsets are written for the wide track counts (4 and
   the parts sheets' 5); the 2-column step restates its own pair, and a
   single column needs nothing. The :not() guard keeps the four-track
   offsets off the five-track sheets and back. */
@media (min-width: 64.0625rem) {
  .car-specs-grid:not(.spec-cols-5) > .car-spec-cell:last-child:nth-child(4n+1) { grid-column: span 4; }
  .car-specs-grid:not(.spec-cols-5) > .car-spec-cell:last-child:nth-child(4n+2) { grid-column: span 3; }
  .car-specs-grid:not(.spec-cols-5) > .car-spec-cell:last-child:nth-child(4n+3) { grid-column: span 2; }
  .car-specs-grid.spec-cols-5 > .car-spec-cell:last-child:nth-child(5n+1) { grid-column: span 5; }
  .car-specs-grid.spec-cols-5 > .car-spec-cell:last-child:nth-child(5n+2) { grid-column: span 4; }
  .car-specs-grid.spec-cols-5 > .car-spec-cell:last-child:nth-child(5n+3) { grid-column: span 3; }
  .car-specs-grid.spec-cols-5 > .car-spec-cell:last-child:nth-child(5n+4) { grid-column: span 2; }
}
.car-spec-label { display: block; margin-bottom: 0.25rem; font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
.car-spec-value { display: block; font-size: 0.875rem; color: var(--fg); }

/* ---------- Car listings – photos on top (car.html) -----------------------
   One large slot, then a strip of four thumbs in a fixed 4-column grid (the
   +N expander slot sits inside it, the still-hidden extras wrap into further
   rows of the same grid once the strip opens). Square corners and the hairline
   border frame every photo, matching the site's control recipe. */
.car-media { margin: 0 0 2rem; }
.car-photo { position: relative; display: block; border: 1px solid var(--border); }
.car-photo img { display: block; width: 100%; height: auto; }
.car-photo:hover { border-color: var(--muted); }
.car-photo-main { margin-bottom: 0.75rem; }
.car-thumbs { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.75rem; }
/* The +N plate: a dark veil over the 4th thumb's own shot with the count
   centred on it. Fixed near-black under both themes – it darkens a photo,
   not the page, so it never follows --bg. */
.car-photo-count {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  background: #0c0d0e99; color: #fff;
  font-size: 1.125rem; font-weight: 700;
}
.car-photo-more:hover .car-photo-count { background: #0c0d0eb3; }
/* Photos past the 4th slot wait for the strip to open (site.js adds
   is-open to .car-media on the +N click); the plate drops out with them. */
.car-photo-extra { display: none; }
.car-media.is-open .car-photo-extra { display: block; }
.car-media.is-open .car-photo-count { display: none; }
/* The collapse tile (site.js removes is-open): photo-shaped – same grid
   slot, same hairline frame, but a 4:3 box (aspect-ratio) with a centred
   minus instead of a shot. Listed after .car-photo so its display:none
   wins the specificity tie; is-open's display:flex outranks it again. */
.car-photo-collapse {
  display: none; aspect-ratio: 4 / 3;
  align-items: center; justify-content: center;
  font: inherit; color: var(--muted); background: transparent; cursor: pointer;
}
.car-media.is-open .car-photo-collapse { display: flex; }
.car-photo-collapse:hover { color: var(--fg); }
.car-photo-sign { font-size: 1.5rem; line-height: 1; }

/* ---------- Photo lightbox (site.js builds it on first open) --------------
   Full-bleed dark wash over everything (z-index 95: header 50, search 90);
   the photo keeps its ratio inside the frame, the controls reuse the site's
   2.25rem square-button recipe. Clicking the wash outside the photo closes;
   Esc closes, arrows step through the car's photos (see site.js). */
.lightbox { position: fixed; inset: 0; z-index: 95; display: none; }
.lightbox.open { display: flex; align-items: center; justify-content: center; touch-action: none; }
.lightbox-wash { position: absolute; inset: 0; background: #060708e0; }
.lightbox-fig {
  position: relative; margin: 0;
  max-width: calc(100vw - 8.5rem); max-width: calc(100dvw - 8.5rem);
  max-height: calc(100vh - 4rem); max-height: calc(100dvh - 4rem);
  /* Panning a zoomed photo is native scrolling: site.js enlarges the img
     past this frame and the overflow becomes scrollable – the browser
     clamps the pan on its own (grab-drag, trackpad wheel and touch all
     pan the frame). The scrollbar is hidden chrome; overscroll stays
     contained so the page behind never shifts. */
  overflow: auto; overscroll-behavior: contain; scrollbar-width: none;
}
.lightbox-fig::-webkit-scrollbar { width: 0; height: 0; }
/* site.js parks the initial focus here (dialog entry without an orange
   ring on the close button); Tab still moves to the controls. */
.lightbox-fig:focus { outline: none; }
.lightbox-fig img {
  display: block; max-width: 100%; max-height: calc(100vh - 4rem);
  max-height: calc(100dvh - 4rem); width: auto; height: auto;
  border: 1px solid var(--border); background: var(--bg); cursor: grab;
  user-select: none; -webkit-user-drag: none;
}
.lightbox-fig:active img { cursor: grabbing; }
.lightbox button {
  position: absolute; z-index: 1;
  display: flex; align-items: center; justify-content: center;
  width: 2.25rem; height: 2.25rem; padding: 0;
  font: inherit; font-size: 1.125rem; color: #f2f2f2;
  background: #0c0d0e99; border: 1px solid #f2f2f233; border-radius: 0;
  cursor: pointer;
}
.lightbox button:hover { color: #fff; background: #0c0d0ecc; border-color: #f2f2f266; }
.lightbox-x { top: 1rem; right: 1rem; }
.lightbox-prev { top: 50%; left: 1rem; margin-top: -1.125rem; }
.lightbox-next { top: 50%; right: 1rem; margin-top: -1.125rem; }
/* Bottom bar: the counter pill plus, on desktop only, the zoom control.
   Touch devices get no slider – there pinch (site.js pointer-event pinch)
   is the zoom gesture, so the phone keeps the photo uncluttered. */
.lightbox-bar {
  position: absolute; bottom: 1rem; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 0.75rem; z-index: 1;
}
.lightbox-count {
  margin: 0; padding: 0.125rem 0.5rem;
  font-size: 0.8125rem; color: #f2f2f2; background: #0c0d0e99;
  border: 1px solid #f2f2f233;
}
/* The zoom control is a button-style chip like its counter neighbour: the
   zoom-in glyph (Remix Icon zoom-in-line, remixicon.com, MIT, path from
   remixicon@4.5.0 – inlined here because the lightbox is built in JS)
   and the slider share one framed plate. The slider itself carries the
   accessible name ("Масштаб фото"). */
.lightbox-zoom {
  display: none; align-items: center; gap: 0.625rem;
  padding: 0.125rem 0.625rem;
  background: #0c0d0e99; border: 1px solid #f2f2f233;
}
@media (hover: hover) and (pointer: fine) { .lightbox-zoom { display: flex; } }
.lightbox-zoom-label { display: flex; color: #f2f2f2; }
/* Same square slider art as the mileage filter (see .filter-slider): a
   square accent thumb riding a hairline track, per-engine pseudo-element
   rules. The plate is always the dark chip, so the track carries the
   plate family's light hairline (#f2f2f24d) rather than --border, which
   would vanish under the light theme. */
.lightbox-zoom input[type="range"] {
  width: 7rem; height: 0.8125rem; margin: 0;
  -webkit-appearance: none; appearance: none;
  background: transparent; cursor: ew-resize;
}
.lightbox-zoom input[type="range"]::-webkit-slider-runnable-track { height: 0.1875rem; background: #f2f2f24d; }
.lightbox-zoom input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 0.8125rem; height: 0.8125rem; margin-top: -0.3125rem;
  background: var(--accent); border: 0;
}
.lightbox-zoom input[type="range"]::-moz-range-track { height: 0.1875rem; background: #f2f2f24d; }
.lightbox-zoom input[type="range"]::-moz-range-thumb { width: 0.8125rem; height: 0.8125rem; background: var(--accent); border: 0; border-radius: 0; }

/* ---------- Buttons – same 2.25rem control square as the header ---------- */
.btn {
  display: inline-flex; align-items: center; gap: 0.4rem;
  height: 2.25rem; padding: 0 0.875rem;
  border: 1px solid var(--border); border-radius: 0;
  background: transparent; color: var(--fg); cursor: pointer;
}
.btn:hover { border-color: var(--muted); text-decoration: none; }
.btn-primary {
  background: var(--accent); border-color: transparent;
  color: var(--on-accent); font-weight: 700;
}
.btn-primary:hover { background: color-mix(in srgb, var(--accent) 85%, var(--fg)); border-color: transparent; }
/* Buttons written in Markdown sit inside .prose, whose link colour would
   otherwise win on specificity – re-assert the button colours there. */
.prose a.btn, .prose a.btn:hover { color: var(--fg); }
.prose a.btn-primary, .prose a.btn-primary:hover { color: var(--on-accent); }

/* ---------- Pills – buttons' compact siblings ----------
   Right angles and the 1px hairline border come from the buttons; the
   fill and border colour follow the tone, mixed from the palette colour
   the same way the alert boxes' tinted borders/fills read. Only the
   type shrinks (they live inside meta rows, where the 2.25rem control
   square would never fit). Component: <pill text="" color=""> in
   templates/components.html. */
.pill {
  display: inline-block; padding: 0.25rem 0.5rem; font-size: 0.75rem; line-height: 1;
  border: 1px solid var(--border); border-radius: 0;
  background: transparent; color: var(--fg); white-space: nowrap;
}
.pill-green  { color: var(--green);  border-color: color-mix(in srgb, var(--green)  45%, transparent); background: color-mix(in srgb, var(--green)  12%, transparent); }
.pill-blue   { color: var(--blue);   border-color: color-mix(in srgb, var(--blue)   45%, transparent); background: color-mix(in srgb, var(--blue)   12%, transparent); }
.pill-yellow { color: var(--yellow); border-color: color-mix(in srgb, var(--yellow) 45%, transparent); background: color-mix(in srgb, var(--yellow) 12%, transparent); }
.pill-red    { color: var(--red);    border-color: color-mix(in srgb, var(--red)    45%, transparent); background: color-mix(in srgb, var(--red)    12%, transparent); }

/* ---------- Home ---------- */
.hero-kicker { margin: 0 0 0.75rem; font-size: 0.8125rem; color: var(--muted); }
.hero-name {
  margin: 0; font-size: clamp(2.25rem, 6vw, 3.75rem);
  line-height: 1.1; letter-spacing: -0.03em; font-weight: 700;
}
.hero-name .accent { color: var(--accent); }
.hero-tagline { margin: 1rem 0 0; color: var(--muted); max-width: 38rem; }
.hero-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; margin-top: 1.5rem; }
.hero-social { display: inline-flex; gap: 0.5rem; margin-left: 0.25rem; }
.hero-social a { color: var(--muted); }
.hero-social a:hover { color: var(--fg); }

.home-sec { margin-top: 4rem; }
.sec-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; }
.sec-head h2 { margin: 0; font-size: 1.125rem; letter-spacing: -0.01em; }
.more { font-size: 0.75rem; color: var(--muted); }
.more:hover { color: var(--fg); }

/* Tag filters: any list item can be filtered out (section rows – see
   section.html). Two attribute selectors outrank .row's display: grid, so
   [hidden] sticks. */
[data-tags][hidden] { display: none; }
/* Same guarantee for the cars grid view: the grid rules above are
   (0,3,0)/(0,4,0) and would otherwise flip filtered-out rows back to
   display:flex – the facet filters hide rows exactly like the tag filter. */
.rows[data-view] .row[data-tags][hidden] { display: none; }
.filter-empty { color: var(--muted); padding: 1.25rem 0; }

/* ---------- Search ---------- */
/* Header trigger: see .search-trigger above (same square as .theme-toggle). */

/* The popup – docs-site .search-dialog metrics */
html.no-scroll body { overflow: hidden; }
.search-modal { position: fixed; inset: 0; z-index: 90; display: none; }
.search-modal.open { display: block; }
.search-backdrop { position: absolute; inset: 0; background: var(--backdrop); }
.search-panel {
  position: absolute; top: 10vh; left: 50%; transform: translateX(-50%);
  width: min(42rem, 100% - 2rem); max-height: min(36rem, 100vh - 4rem);
  max-height: min(36rem, 100dvh - 4rem);
  display: flex; flex-direction: column;
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
  border: 1px solid var(--border); border-radius: 0;
  color: var(--fg); padding: 0; overflow: hidden;
}
/* Input bar: docs-site .search-bar – ">" prompt, bare input, "Esc" button */
.search-bar {
  display: flex; align-items: center; gap: 0.75rem;
  padding: 0.75rem 1rem; border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.search-prompt { flex-shrink: 0; }
/* appearance:none strips iOS' native search-field chrome (inner rounding,
   inset padding); the transparent borderless docs-site look is ours.
   1:1 with the v2 docs site: its .search-bar input only ever gets
   outline:0 – the popup shows no focus ring anywhere, the caret is the
   keyboard cursor. */
.search-input {
  flex: 1; min-width: 0; background: transparent; border: 0; outline: 0;
  -webkit-appearance: none; appearance: none;
}
.search-input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
.search-bar button, .search-x {
  color: var(--muted); background: transparent; border: 0; padding: 0; cursor: pointer;
}
.search-bar button:hover, .search-x:hover { color: var(--fg); }
/* Results: docs-site .search-results – one link per row (strong title +
   muted excerpt, the whole row clickable); matches in an excerpt are bold
   foreground marks, never highlight blocks. */
.search-status { margin: 0; color: var(--muted); padding: 0.75rem 1.25rem 0; }
.search-status:empty { display: none; }
.search-results { margin: 0; padding: 0.5rem; max-height: 28rem; overflow-y: auto; }
.search-results > p { margin: 0; padding: 0.75rem; color: var(--muted); }
.search-results a { display: block; padding: 0.75rem; color: var(--fg); }
.search-results a:hover { background: var(--surface-hover); text-decoration: none; }
/* Arrow-key cursor (docs-site behaviour): the selected row gets the same
   light-grey hover wash – no outline ring at all, since Chrome's default
   :focus-visible indicator would draw its blue border on the row. */
.search-results a:focus { outline: 0; background: var(--surface-hover); text-decoration: none; }
.search-results strong, .search-results span { display: block; }
.search-results strong { font-weight: 700; }
.search-results span { margin-top: 0.25rem; font-size: 0.8125rem; color: var(--muted); }
.search-results mark { background: transparent; color: var(--fg); font-weight: 700; }
/* Listing hits get their preview image (search.js pulls it from the
   #search-thumbs block baked in base.html): thumbnail left, title+excerpt
   stacked right. The wrap div only exists when a thumb is present. */
.search-results a:has(.search-thumb) { display: flex; align-items: center; gap: 0.75rem; }
.search-thumb { flex: none; width: 5.5rem; height: 4rem; object-fit: cover; }
/* The thumb's title+excerpt stack takes the middle; the price (car rows
   only) rides the row's right edge, accent-coloured like the listings'
   prices. The selector is scoped through .search-results on purpose: the
   generic "muted small excerpt span" rule above (.search-results span)
   would otherwise out-specify a bare class and grey the price out. */
.search-results a:has(.search-thumb) > div { flex: 1 1 auto; min-width: 0; }
.search-results .search-price {
  margin: 0 0 0 auto; padding-left: 0.75rem;
  color: var(--accent); font-weight: 700; white-space: nowrap;
  /* A step above the excerpt's 0.8125rem: the price is the one fact a
     scan should never have to lean in for. */
  font-size: 0.875rem;
}
/* Phone widths: a 5.5rem thumb plus the nowrap price leaves the excerpt
   column barely 60px wide – the rows balloon. Below 25rem the teaser is
   cut and the thumb row carries just the year-prefixed title + price;
   the full excerpt lives on the listing itself. */
@media (max-width: 25rem) {
  .search-results a:has(.search-thumb) > div > span { display: none; }
}
/* Header nav at laptop/tablet widths: the four entries fit comfortably
   down to ~900px, but below that the last link starts to compress
   (measured at ~880px), so the long labels swap to the short ones baked
   into the markup ([extra].nav short, see zola.toml) and the whole row
   fits one line again down to where the header nav hides (48rem). */
@media (max-width: 56rem) {
  .site-nav .nav-long { display: none; }
  .site-nav .nav-short { display: inline; }
}

/* Inline search page (/search/): same parts, no dialog chrome. Scoped to
   .search-page so these overrides never touch the popup panel, which is a
   .search-ui too. */
.search-page { max-width: var(--content-width); }
.search-page .search-bar { border: 1px solid var(--border); margin-bottom: 1rem; }
.search-page .search-status { padding: 0.5rem 0.5rem 0; }
.search-page .search-results { padding: 0; max-height: none; max-width: 44rem; }
.search-page .search-results a { padding: 0.75rem 0.5rem; }

/* ---------- Footer ---------- */
.site-footer { border-top: 1px solid var(--separator); }
.footer-inner {
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.75rem;
  align-items: center; padding: 1.25rem var(--gutter);
  font-size: 0.8125rem; color: var(--muted);
}
.footer-inner p { margin: 0; }
.footer-links { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; }
.footer-links a { display: inline-flex; align-items: center; gap: 0.35rem; color: var(--muted); }
.footer-links a:hover { color: var(--fg); text-decoration: none; }

/* ---------- Responsive (docs-site breakpoints: 64rem / 48rem) ---------- */
@media (max-width: 64rem) {
  .layout-list, .layout-doc { grid-template-columns: 14rem minmax(0, 1fr); }
  /* …but the facet panel still gets a wider column than plain tag lists. */
  .layout-list:has(.car-filters) { grid-template-columns: 16.5rem minmax(0, 1fr); }
  /* .toc is hidden below, so a ToC-only page is just the article. */
  .layout-toc { grid-template-columns: minmax(0, 1fr); }
  .toc { display: none; }
  /* The spec sheet drops from four columns to two once the listing loses
     its right rail: the article column is far too narrow for four cells
     (never three – the column counts step 4 → 2 → 1, and the parts
     sheets' five tracks step down with it). An odd cell count now leaves
     the last row one cell short, so that cell takes the whole row rather
     than leaving a grey hole beside it. */
  .car-specs-grid, .car-specs-grid.spec-cols-5 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .car-specs-grid > .car-spec-cell:last-child:nth-child(odd) { grid-column: 1 / -1; }
  /* List-view car rows between 48rem and 64rem: the facet sidebar still
     stands, and the fixed 17.6rem photo track left as little as 134px of
     text column at the worst widths – the title wrapped and the specs
     squashed. A proportional pair (the same 37fr/63fr split the phone
     rows use, where the photo stacks beside the body) keeps photo-left
     text-right with a body that can hold the row. */
  .rows:has(.row-cars) .row.row-cars {
    grid-template-columns: minmax(0, 37fr) minmax(0, 63fr);
  }
  /* The listing page's third column is gone, so the buy rail no longer
     fits: it docks to the bottom of the screen instead – framed panel
     chrome stripped (border-top only, header hidden, the body's
     display:contents lifts price + status + buttons into the dock's own
     rows). Fixed, so the body gets bottom padding to let the footer's
     last line clear it. Price and status pill share the top row – the
     price sits left (introduced by its white "Ціна:" riding at exactly
     half the number's size, back in the inline flow now that nothing is
     centred), the pill closes the row on the right; the two contact
     buttons share the full width of the row below, equally wide. */
  .car-buy {
    position: fixed; left: 0; right: 0; bottom: 0; top: auto; z-index: 45;
    width: auto; margin-inline: 0;
    flex-direction: row; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.75rem;
    padding: 0.625rem var(--gutter);
    border: 0; border-top: 1px solid var(--separator);
    background: color-mix(in srgb, var(--bg) 82%, transparent);
    backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
  }
  .car-buy-head { display: none; }
  .car-buy-body { display: contents; }
  /* Price + pill row: the price line keeps its own row (basis 100%) but
     is left-justified and grows; the pill rides the row's right edge. It
     is taken out of the flow on purpose – at tablet widths the two
     basis-0 buttons would otherwise join the price row and leave no room
     to wrap. It hangs off the dock's fixed position, centred on the
     price line's height (padding-top + half the 1.75rem/1.3 line). */
  .car-buy-price { margin: 0; flex: 1 0 100%; text-align: left; font-size: 1.75rem; line-height: 1.3; white-space: nowrap; }
  /* "Ціна:" rides in the flow now – no absolute positioning to centre it
     under a centred number; it just leads the price line. */
  .car-buy-label {
    display: inline;
    color: var(--fg); font-weight: 400; font-size: 0.875rem;
  }
  .car-buy-status {
    position: absolute; right: var(--gutter); margin: 0;
    top: calc(0.625rem + 1.75rem * 1.3 / 2); transform: translateY(-50%);
  }
  /* The pill keeps its regular size here: the same 0.8125rem chip every
     list row carries, hugging its own text. The dock's buttons grew a
     fifth taller for thumbs; matching the pill to them put two heavy
     blocks beside the price, so the chip stays the small one it is
     everywhere else and the price keeps the row. width: auto is still
     needed – the desktop rail stretches the pill to the rail's full
     width, and here it hangs off the price row's right edge. */
  .car-buy .car-status { width: auto; height: auto; font-size: 0.8125rem; }
  /* Contact buttons: a fifth taller than the site's 2.25rem control
     square (easier thumbs), text one step up to match, and the pair
     swapped from the markup order via order: – Telegram leads, the
     orange Viber button closes the row. */
  .car-buy .btn {
    flex: 1 1 0; justify-content: center; white-space: nowrap;
    height: 2.7rem; font-size: 1rem; order: 0;
  }
  .car-buy .btn-primary { order: 1; }
  body:has(.car-buy) { padding-bottom: 7rem; }
}
@media (max-width: 48rem) {
  :root { --gutter: 1rem; }
  .site-nav { display: none; }
  /* docs-site trick: collapsed trigger shows a ">" glyph instead of the label.
     The header gap is NOT tightened to 0.75rem as the old rule here did –
     the docs site keeps .header-inner at 1.5rem at every width, so the
     trigger → toggle spacing is identical on mobile and desktop. */
  .search-trigger { min-width: 2.25rem; padding: 0; justify-content: center; }
  .search-trigger .search-trigger-label, .search-trigger .kbd { display: none; }
  /* The collapsed trigger shows the header's own search glyph (Remix
     search-line, components.html) – a square button the same 2.25rem as
     the theme toggle beside it, icon centred, nothing else. */
  .search-trigger .search-ic { display: inline-flex; align-items: center; }
  .search-ic .icon { display: block; }
  /* Nav links sit inline in the strip (no dropdown): one centered wrapping
     row. Same glass recipe as .site-header – see-through, not a solid block
     (deliberate deviation from the docs site, which uses a solid bg here).
     0.5rem + the links' own 0.25rem padding-block keeps the strip the exact
     height the closed "Menu" summary had (~2.9rem incl. border), so the
     scroll-margin math below still holds. */
  .mobile-navigation {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
    gap: 0.25rem 1.25rem;
    position: sticky; top: var(--header-height); z-index: 10;
    padding: 0.5rem var(--gutter);
    background: color-mix(in srgb, var(--bg) 82%, transparent);
    backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
    border-bottom: 1px solid var(--separator);
  }
  /* :has(.car-filters) listed explicitly: it outranks the plain selector,
     so phones must repeat it to keep the single-column stack. */
  .layout-list, .layout-doc, .layout-list:has(.car-filters) { grid-template-columns: minmax(0, 1fr); }
  .sidebar {
    position: static; height: auto;
    border-right: 0; border-bottom: 1px solid var(--separator);
    padding: 1rem var(--gutter);
  }
  /* Article pages (layout-doc): the sibling-posts sidebar lands AFTER the
     article on phones – content first. List layouts (.layout-list) keep the
     tag filter bar on top, where it belongs. */
  .layout-doc .content { order: -1; }
  /* …and the sidebar separator moves to the top with it: the line sits
     between article and sibling posts instead of stacking against the
     footer's own border-top right below the sidebar. */
  .layout-doc .sidebar { border-top: 1px solid var(--separator); border-bottom: 0; }
  /* The car-page heading alignment is a desktop affair; phones stack the
     panel with the plain 1rem padding all around. */
  body:has(.car-crumbs) .sidebar { padding-top: 1rem; }
  /* The footer stacks here (the copyright line and the socials no longer
     share a row) – both lines then centre. */
  .footer-inner { justify-content: center; text-align: center; }
  /* The list turns into wrapping rows here, so the vertical-list li spacing
     would push every item but the first 0.375rem down within a row and
     still glue wrapped rows together – row/column gap owns the spacing. */
  .side-list { display: flex; flex-wrap: wrap; gap: 0.375rem 1rem; }
  .side-list li + li { margin-top: 0; }
  .side-list a { flex-direction: row; align-items: baseline; gap: 0.4rem; }
  /* …but a previewed list (the car page's "Інші Авто") never flattens:
     the one-column card stack keeps its shape at every width. */
  .side-list:has(.side-thumb) { display: block; }
  /* …and it stops at five listings on phones: the stacked cards are
     discovery, not a second catalog page – the full set lives under the
     section's own nav entry, and the stack no longer pushes the footer
     off reach. */
  .side-list:has(.side-thumb) > li:nth-child(n+6) { display: none; }
  .side-title { display: none; }
  /* Cars toolbar on phones: the sort field drops under the view toggle
     and stretches full width, instead of floating right-aligned on a
     wrapped row of their own (the open switch is out of flow, so the
     wrapper carries the flex sizing). */
  .cars-bar .sort-dd { margin-left: 0; flex: 1 1 100%; }
  /* Facet panel on phones: two independent columns. The two DOM column
     wrappers (car_filters) stop dissolving (display: contents is undone
     here) and each becomes a flex stack placed side by side in a hard
     two-column grid – one long scroll would bury the list. Column 1 stacks
     the checkbox groups ("Стан", "Привід", "Вид пального", "Коробка
     передач"), column 2 the numeric "Від"/"До" groups around the "Марка"
     dropdown ("Ціна", "Марка", "Рік випуску") and closes with "Тип кузова".
     The shorter column
     simply ends higher – no stretching, no backfilling, no aligned
     headings between columns. Within a group the checks still stack –
     tap rows want width. */
  .car-filters .filter-form {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1rem 1.5rem;
  }
  .car-filters .filter-col {
    display: flex; flex-direction: column;
    gap: 1rem; min-width: 0;
  }
  .car-filters .filter-group { margin-bottom: 0; }
  /* The "Пробіг" slider is the one control that needs the full width, so the
     component keeps it out of the column wrappers: here it is a grid item
     and takes the whole first row, above both columns. The desktop order
     numbers reach a grid just as far as they reach a flex line, and there
     "Пробіг" sits fourth – so the top-level group leads the panel here
     instead, and the groups inside each column fall back to the DOM order
     the component writes the phone layout in (column 2 reads "Ціна",
     "Марка", "Рік випуску", "Тип кузова", not the desktop sequence). */
  .car-filters .filter-form > .filter-group { order: -1; grid-column: 1 / -1; }
  .car-filters .filter-col > .filter-group { order: 0; }
  /* "Скинути фільтри" spans both columns: on the phone grid a content-width
     button would sit alone in one half-column, and a full-width row is the
     natural end of a panel the visitor has just worked down. */
  .car-filters .filter-reset { grid-column: 1 / -1; }
  /* Facet panel collapses behind a "Фільтри" button on phones to keep the
     top of the page short: bare checkbox hack (see .filter-switch above),
     so it opens without JS. Open state flows the groups into the same
     compact grid as before. */
  .car-filters .filter-switch-label {
    display: flex; align-items: center; justify-content: space-between;
    width: 100%; padding: 0.5rem 0;
    font-weight: 700; color: var(--fg); cursor: pointer;
  }
  .car-filters .filter-switch-label::after {
    content: ""; flex: none;
    width: 0.4375rem; height: 0.4375rem;
    border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted);
    transform: rotate(45deg); transition: transform 0.15s ease;
  }
  .car-filters .filter-switch:checked ~ .filter-switch-label::after { transform: rotate(-135deg); }
  /* (0,3,0) beats the grid rule above's (0,2,0); :checked (0,4,0) wins back. */
  .car-filters .filter-switch ~ .filter-form { display: none; }
  .car-filters .filter-switch:checked ~ .filter-form {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1rem 1.5rem; margin-top: 0.75rem;
  }
  /* Phone-width dock: a tighter rhythm – smaller buttons, everything
     closer together; the two-row layout comes from 64rem. */
  .car-buy { gap: 0.5rem; }
  /* Phone widths are the tight case for the dock row: the site's monospace
     puts the phone number's 16 glyphs at 0.75em each, so at the 64rem block's
     1rem font the pair's nowrap text alone passes a 360px screen's inner
     width. One step down keeps the buttons on one equal row there (and the
     font still stands a third above the 0.75rem the dock used to carry). */
  .car-buy .btn { padding: 0 0.375rem; font-size: 0.875rem; }
  .content { padding: 2rem var(--gutter) 4rem; }
  /* Same halving on phones: 1rem above the crumb, 1rem below it. */
  .content:has(.car-crumbs) { padding-top: 1rem; }
  .car-crumbs { margin-bottom: 1rem; }
  .grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* Mobile usability: anchor jumps must clear the sticky header AND the
     inline menu strip (~2.9rem) sitting below it at this width; the 16px
     social icons get a padded hit box. Header, menu, and search-bar paddings
     are left at the docs-site densities on purpose – the docs site keeps
     .search-bar button at padding:0 at every width, so "Esc" sits exactly
     1rem from the panel edge, mirroring the ">" prompt on the left. */
  .prose h1, .prose h2, .prose h3, .prose h4,
  .prose .footnote-reference, .prose .footnotes li {
    scroll-margin-top: calc(var(--header-height) + 3.75rem);
  }
  .hero-social a { padding: 0.5rem; }
}
@media (max-width: 40rem) {
  .grid-3 { grid-template-columns: minmax(0, 1fr); }
  .row { grid-template-columns: minmax(0, 1fr); gap: 0.25rem; }
  .rows:has(.row-thumb) .row { grid-template-columns: minmax(0, 1fr); }
  .rows:has(.row-thumb) .row > .row-body { grid-column: auto; }
  /* The cars LIST is the exception: the photo stays beside the copy on a
     phone, which is what makes it read as a list rather than a stack of
     cards. This re-pins the car module's desktop rule (17.6rem photo column)
     to a fraction of the row instead, and out-specifies both collapses above
     (0,6,0 vs 0,4,0); the row-body keeps the column-2 pin from the car
     module, which needs no mobile copy. Grid view is display:flex, so the
     columns never reach it and its cards stay stacked.
     The phone list is also a two-band row: the photo and the title header
     (headline, then the pill and price in one row under it) share the top
     band, and the icon-spec grid moves out of the narrow text column to
     span the whole row underneath – there it has the room to spread into
     its comfortable 2-column grid instead of squeezing one spec per line.
     display:contents takes the row body out of the layout, which promotes
     its two children (the title header and the spec block) to items of the
     row grid, and each then places itself: the header into column 2 beside
     the photo, the spec block spanning both columns below. The header's
     lines (a wrapped headline plus one price row) fit within the photo's
     height, so the spec band starts right at the photo's bottom edge –
     top-justified under the image rather than pushed down a taller text
     column would leave.
     The photo column is 37fr where the desktop row splits 46fr (−20% of its
     width, the text column taking the rest). */
  .rows[data-view="list"]:has(.row-thumb) .row.row-cars {
    grid-template-columns: minmax(0, 37fr) minmax(0, 63fr);
    gap: 0.75rem;
  }
  .rows[data-view="list"] .row.row-cars > .row-body { display: contents; }
  /* The phone header stacks its lines rather than pairing title and slots
     on one baseline row: the headline on top, the price row under it. */
  .rows[data-view="list"] .row.row-cars .row-title {
    grid-column: 2; flex-direction: column; flex-wrap: nowrap;
    align-items: flex-start;
    /* The header stretches the row's full height (grid items stretch by
       default), and space-between then pins the price row to its bottom
       edge – which matches the photo's bottom edge in every row. */
    justify-content: space-between;
  }
  .rows[data-view="list"] .row.row-cars .row-specs { grid-column: 1 / -1; }
  /* The price and pill share one full-width line under the headline:
     space-between pins the status pill flush left and keeps the price
     flush right. row-reverse because the slots bake price before pill in
     the markup, while here the pill leads – as it does on the grid cards. */
  .rows[data-view="list"] .row.row-cars .car-title-slots {
    flex-direction: row-reverse; justify-content: space-between;
    align-items: center; margin-left: 0;
    width: 100%; gap: 0.75rem;
  }
  .row-thumb { max-width: 24rem; margin-bottom: 0.5rem; }
  .row-meta { padding-top: 0; }
  /* Grid view keeps the thumb full width of its card; the list thumb fills
     its (now narrow) grid column, so the 24rem cap has to go. B46: the
     grid thumb's bottom breath drops to 0.375rem – the home card's 12px
     rides above the dot band now, and the band's own 6px below closes
     the photo band (see the grid block above). */
  .rows[data-view="grid"] .row > .row-thumb { max-width: none; margin-bottom: 0.375rem; }
  .rows[data-view="list"] .row.row-cars > .row-thumb { max-width: none; margin-bottom: 0; }
  /* Phone list type sits a step under the desktop list: headline 1rem
     against 1.125rem, price 1.125rem against 1.4rem, pill 0.65rem against
     0.8125rem – enough smaller to keep the row tight, enough bigger than
     the 0.8125rem spec line to keep the title on top.
     The headline then rides 0.385em out of its grid cell: the line box
     starts a pixel above the photo, but OpenTUI Mono keeps 7.16px of
     leading plus accent room above its cap/digit height at 1rem, so the
     text itself would start that far below the image's top edge. The
     negative margin lifts the line box by just that much, leaving the
     digits' top edge flush with the photo. */
  .rows[data-view="list"] .row.row-cars .row-body h2 {
    font-size: 1rem; margin-top: -0.385em;
  }
  .rows[data-view="list"] .row.row-cars .car-price {
    font-size: 1.125rem; height: 1.5738rem;
  }
  /* The pill keeps the desktop recipe (line-height 1.8, inherited) and its
     vertical padding is tuned so the box lands on the price box exactly:
     1.17 + 0.2788 + 0.125 border = 1.5738rem (1.125rem × the price's 1.3989
     line-height), so price and pill stay height-matched as on a wide screen. */
  .rows[data-view="list"] .row.row-cars .car-status {
    font-size: 0.65rem; padding: 0.1394rem 0.4375rem;
  }
  /* Price and year fit a single filter column on a narrow phone: their
     "Від"/"До" boxes stack one over the other and stretch to the column – at
     this width a half-column is too tight to carry two number boxes side
     by side. (The boxes used to be moved between the grid's columns by
     facet here; the column wrappers in car_filters place them now.) */
  .car-filters .filter-nums { flex-direction: column; gap: 0.5rem; }
  .car-filters .filter-num { flex: 1 1 auto; }
  /* On a phone the make dropdown is exactly as wide as its field: the panel
     opens over the button rather than beside it, and the surrounding filter
     grid gives the field only a fraction of the row, so letting the panel
     grow to its labels' width would swing it out over the neighbouring
     column and off the screen. Longest makes (Mercedes-Benz) wrap inside the
     option instead of pushing the panel wider. */
  .brand-dd-panel { width: 100%; max-width: none; }
  /* The icon-spec grid stays a fixed two-column grid on phones too – the
     spec line spans the whole row width, enough for two of even the
     longest words side by side (auto-fit tracks used to bounce it to
     three columns at some widths, which the column count rules forbid). */
  .brand-logo { height: calc(1.25rem * 1.02); }
  /* Lightbox on phones: the photo nearly fills the width and the controls
     pull in to a 0.5rem inset, hovering over the photo's edges (their
     translucent plates sit on top of the image, the way phone galleries
     do); the arrows stay vertically centred. */
  .lightbox-fig { max-width: calc(100vw - 1rem); max-width: calc(100dvw - 1rem); }
  .lightbox-x { top: 0.5rem; right: 0.5rem; }
  /* The step arrows sit a touch off the screen edges – a little inward
     from the very border (the phone's swipe/hold gestures live there),
     but still close enough to read as edge controls. */
  .lightbox-prev { left: 0.875rem; }
  .lightbox-next { right: 0.875rem; }
  .lightbox-bar { bottom: 0.5rem; }
}
/* Smallest widths: the spec sheet takes its single column – the last step
   of the 4 → 2 → 1 sequence (never three). It lives at the very end of
   the sheet because media queries cascade by source order, and the two-
   column step above (the 64rem block) otherwise wins over this one at
   phone widths, where both blocks match. */
@media (max-width: 25rem) {
  .car-specs-grid, .car-specs-grid.spec-cols-5 { grid-template-columns: minmax(0, 1fr); }
}
