/* Feed page (market listing) — the offer detail's card language applied
   to the list: same tokens (theme.css, linked first), one 680px column,
   one card per Offer. Filter bar and subscribe control share the card
   chrome so the page reads as one system, not a bare list. */

* { box-sizing: border-box; }
body {
  margin: 0; background: var(--bg); color: var(--ink);
  font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.wrap { max-width: 680px; margin: 0 auto; padding: 20px 16px 80px; }

h1 { font-size: 20px; margin: 0 0 14px; font-weight: 650; letter-spacing: -0.01em; }
/* Header row (issue 00047): h1 left, display-currency picker right. */
.page-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.cur-picker {
  font: inherit; font-size: 13px; color: var(--ink-soft);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 8px; padding: 4px 8px;
}
.cur-picker:disabled { opacity: 0.5; } /* no JS → nothing to rewrite */

/* The setup chipbar (Ф3 prototype): «Фильтры» collapse · split · dial
   counters and toggles. SSR bakes it [hidden] (dials are a JS feature);
   dials.js unhides and drives it. */
.chipbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 10px; }
/* Desktop: the bar centers over the centered 520px card column (user
   direction 2026-07-27) — same footing as the cards below it. */
@media (min-width: 600px) {
  .chipbar { justify-content: center; }
}
.chipbar[hidden] { display: none; }
.chip {
  display: inline-flex; align-items: center; gap: 6px; height: 36px; padding: 0 12px;
  font-size: 13.5px; color: var(--ink); background: var(--card);
  border: 1px solid var(--line); border-radius: 999px; cursor: pointer; user-select: none;
  font-family: inherit;
}
.chip.on { border-color: var(--accent); color: var(--accent); background: color-mix(in srgb, var(--accent) 9%, var(--card)); }
/* «Фильтры» while any filter is applied: orange says «something is
   narrowing this list» even when the form is collapsed. After .chip.on so
   the accent wins while the form is open too. */
.chip.warn { border-color: var(--warn); color: var(--warn); background: color-mix(in srgb, var(--warn) 9%, var(--card)); }
/* Chevron rides the chip's text color (accent/warn included) and sits
   1.5px down with a hair more air before it (user-dialed in the workbench,
   2026-07-27). The open-state rotate must restate the translate — transform
   is one property, not composable across rules. */
.chip .chev { margin-left: 1px; transform: translateY(1.5px); transition: transform 0.15s; }
.chip.on .chev { transform: translateY(1.5px) rotate(180deg); }
.chip.count { padding: 0 6px; }
/* −/+ sized well above the label (user-dialed in the workbench,
   2026-07-27): the signs are the tap targets, not decoration. Flex-centered
   with line-height 1 — the inherited baseline sat the big glyphs visibly
   low in the chip; --ink-soft, not faint (user direction 2026-07-27). */
.chip.count button {
  font: inherit; font-size: 18px; font-weight: 300; line-height: 1; color: var(--ink-soft);
  background: none; border: none; padding: 0 10px; height: 100%; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  transform: translateY(-1px);
}
.chip.count .lab { display: inline-flex; align-items: center; gap: 5px; }
.chip.count .lab .unit { color: var(--ink-faint); }
.chip .n { font-variant-numeric: tabular-nums; min-width: 12px; text-align: center; }
.chip.count.zero .lab { color: var(--ink-faint); }
.chipbar .split { width: 1px; height: 20px; background: var(--line); margin: 0 2px; }

/* Filter bar: one card row; labels above their controls, button aligned
   to the control baseline. */
form.filters {
  display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end;
  background: var(--card); border: 1px solid var(--line);
  border-radius: 14px; padding: 14px; margin-bottom: 10px;
}
/* Desktop: the filter card takes EXACTLY the cards' 520px footing,
   centered like them (user direction 2026-07-27) — full-width it trailed
   empty space to the right of the price inputs. */
@media (min-width: 600px) {
  form.filters { max-width: 520px; margin-left: auto; margin-right: auto; }
}
.filters label { display: block; }
/* Sort + reset only exist while a search is in action (user direction
   2026-07-26): SSR bakes them [hidden] and search.js unhides them when a
   filter engages. The explicit displays above beat the UA's [hidden]
   default, so restate it. */
.filters label[hidden], .filters button.reset[hidden] { display: none; }
/* The chipbar's «Фильтры» collapse (dials.js) hides the whole form; the
   explicit display:flex above beats the UA's [hidden] default. */
form.filters[hidden] { display: none; }
.filters .k {
  display: block; font-size: 11px; color: var(--ink-faint);
  margin-bottom: 4px;
}
/* One shared control height (issue 00099): select, price inputs and the
   submit button all sit on the same 36px line. */
.filters select, .filters input {
  font: inherit; font-size: 14px; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 8px; padding: 0 8px; height: 36px;
}
/* Wide enough for a grouped 9-digit VND amount («30 000 000»). Text inputs
   (search.js renders thousand separators live), so no spinner to house.
   The em-dash between the bounds says «range»; inline-flex centers it on
   the control line since the label is a block. */
/* The dropdown chevron: the UA-native arrow hugs the select's right edge
   (padding can't move it — it ignores box padding), so draw our own and
   inset it (user direction 2026-07-26). One mid-gray works on both themes'
   surfaces; padding-right keeps long option text off the glyph. */
.filters select {
  appearance: none;
  padding-right: 28px;
  background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="10" height="6"><path d="M1 1l4 4 4-4" fill="none" stroke="%238a8a8a" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>');
  background-repeat: no-repeat;
  /* One px below geometric center: the options are lowercase Cyrillic, and
     the x-height's optical middle sits under the box midline — dead-center
     read as floating above the text (user-caught 2026-07-26). */
  background-position: right 8px top calc(50% + 1px);
}
.filters label.price input { width: 118px; font-variant-numeric: tabular-nums; }
.filters .range-dash { color: var(--ink-faint); margin: 0 6px; vertical-align: middle; }

/* Rooms: segmented multi-select toggles (user direction 2026-07-27) —
   «студия» plus «1» together is a real search. They share the 36px
   control line; a pressed option fills accent like an active chip. */
.filters .rooms-opts { display: inline-flex; gap: 6px; }
.filters .room-opt {
  font: inherit; font-size: 14px; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 8px; padding: 0 12px; height: 36px; cursor: pointer;
}
.filters .room-opt:disabled { color: var(--ink-faint); cursor: default; }
.filters .room-opt:not(:disabled):hover { border-color: var(--accent); }
.filters .room-opt[aria-pressed="true"] {
  background: var(--accent); border-color: var(--accent); color: var(--bg);
}

/* Locality combobox: search.js hides the no-JS select and drops in a text
   input + suggestion dropdown (fuzzy match with RU→LAT transliteration).
   The label anchors the absolutely-positioned list under the input. */
.filters label.loc { position: relative; }
.filters .loc-input { width: 210px; }
ul.loc-suggest {
  position: absolute; z-index: 10; top: 100%; left: 0; margin: 4px 0 0;
  padding: 4px; list-style: none; min-width: 250px; max-height: 264px;
  overflow-y: auto; background: var(--card); border: 1px solid var(--line);
  border-radius: 10px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
ul.loc-suggest li {
  padding: 7px 10px; border-radius: 7px; font-size: 14px; cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
ul.loc-suggest li:hover, ul.loc-suggest li.active { background: var(--chip); }
ul.loc-suggest li.none { color: var(--ink-faint); cursor: default; }
.filters button {
  font: inherit; font-size: 14px; font-weight: 600;
  color: var(--bg); background: var(--accent);
  border: none; border-radius: 8px; padding: 0 16px; height: 36px;
  cursor: pointer;
}
.filters button:hover { filter: brightness(1.08); }
/* Reset is destructive-ish (user direction 2026-07-26): the theme's warn
   orange as an OUTLINE accent — never a SOLID fill, it undoes the search
   rather than performing one. It does carry a faint warn TINT (user
   direction 2026-07-27 — it was the last orange control left sitting on
   bare page, which read as unfinished next to the filled chips), at the
   same 8% strength the marks pills use. Sits on the shared 36px control
   line via the form's flex-end alignment. */
.filters button.reset {
  color: var(--warn); background: color-mix(in srgb, var(--warn) 8%, var(--card));
  border: 1px solid var(--warn); font-weight: 400;
}
/* Deepen the tint rather than `filter: brightness()` — brightening a wash
   this pale just walks it back toward the card colour. */
.filters button.reset:hover {
  background: color-mix(in srgb, var(--warn) 16%, var(--card));
}
.filters button.reset:disabled { opacity: 0.5; cursor: not-allowed; }
/* No-JS state (issue 00046): the island's controls render `disabled` and
   search.js enables them on boot. The controls set explicit colors, so lean
   on opacity (not the UA's default) to read unmistakably grayed-out; the
   `not-allowed` cursor confirms they are dead. */
.filters select:disabled, .filters input:disabled {
  opacity: 0.5; cursor: not-allowed;
}
/* The «search needs JavaScript» notice (issue 00046): lives in a <noscript>,
   so it exists ONLY when JS is off — no flash for the common case, and nothing
   for search.js to hide. A quiet caption under the grayed control card. */
.search-island .needs-js {
  margin: 8px 2px 0;
  font-size: 12px; color: var(--ink-faint);
}

/* The quiet outline pill, worn by «Получать уведомления» — one tap into the
   alert bot (issue rl-1ib7.1). The listing door «Разместить объявление» wore
   the same pill beside it until issue rl-buc6 took the door off the page. */
a.subscribe {
  display: inline-block; margin: 0 0 16px;
  font-size: 13.5px; font-weight: 600; color: var(--accent);
  border: 1px solid var(--accent); border-radius: 999px;
  padding: 5px 14px; text-decoration: none;
}
a.subscribe:hover { background: var(--chip); }

ul.offers {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 10px;
}
/* The Ф3 card (redesign 2026-07-27, prototype
   docs/extraction/renders/feed-card-photos-v1.html): photo swipe strip
   above a column of text lines. Direct child only: the card's WRAPPER
   anchor — a nested link (the failed-card placeholder's ref) must not
   inherit the card chrome. */
li.offer > a {
  display: flex; flex-direction: column; align-items: stretch; gap: 8px;
  background: var(--card); border: var(--card-edge) solid var(--line);
  border-radius: 14px; padding: 0 0 11px;
  text-decoration: none; color: inherit;
}
li.offer > a:hover { border-color: var(--accent); }
/* The swipe strip: all photos in a scroll-snapped row, 160px tall (locked
   2026-07-26; desktop 240px below), each image's width set by its conformed
   aspect ratio (inline style; the 4/3 fallback here covers rows without
   stored thumb dims). */
/* The media area's height is ONE constant, not the five copies it was: the
   swipe row, the solo stage, the photoless hatch, the pending tile and the
   desktop scroll arrows all have to agree or the card's top edge steps. Set
   here and raised once for desktop (below). */
/* The card's edge is likewise ONE constant. The wrapper anchor draws it, but
   the scroll arrows are positioned against li.offer — OUTSIDE that anchor —
   so they have to subtract it or they sit a pixel above and left of the media
   they belong to. A literal 1px in two places is the same bug as the five
   copies of the height above, just smaller and harder to see. */
li.offer { --strip-h: 160px; --card-edge: 1px; }
li.offer .strip {
  display: flex; gap: 4px; overflow-x: auto; width: 100%;
  border-radius: 13px 13px 0 0; scroll-snap-type: x mandatory;
  scrollbar-width: none;
}
li.offer .strip::-webkit-scrollbar { display: none; }
li.offer .strip img {
  flex: none; width: auto; height: var(--strip-h); aspect-ratio: 4 / 3;
  object-fit: cover; display: block; scroll-snap-align: start;
  background: var(--surface);
}
li.offer .strip img:first-child { border-radius: 13px 0 0 0; }
/* Solo media (user direction 2026-07-27): a lone strip item — a vertical
   video poster, a single collage photo — rarely fills the card width.
   Center it at its NATIVE ratio over a blurred copy of itself; the strip
   becomes a fixed full-width stage instead of a scroll row. */
/* `clip`, NOT `hidden` — they hide the same pixels but only `clip` stops the
   stage being a SCROLL CONTAINER, and the difference was a real bug (00143).
   The stage keeps the swipe row's `scroll-snap-type: x mandatory` and the
   photo keeps its `scroll-snap-align: start`; the backdrop's `scale(1.15)`
   below then hands that container ~39px of scrollable overflow off its end
   edge (518px desktop stage: scrollWidth 557 vs clientWidth 518). Under
   `hidden` there is no scrollbar to show it, but a sideways trackpad nudge
   still scrolls it, and mandatory snapping PARKS it at the far end — the
   centered photo sits 39px left of center until the page reloads. Note that
   `scroll-snap-type: none` alone does not fix this: it stops the snap holding
   the position, not the scrolling that gets there. */
li.offer .strip.solo {
  position: relative; justify-content: center;
  height: var(--strip-h); overflow: clip;
}
li.offer .strip.solo .blur-bg {
  position: absolute; inset: 0; width: 100%; height: 100%;
  aspect-ratio: auto; object-fit: cover; border-radius: 0;
  filter: blur(20px) saturate(1.15); transform: scale(1.15);
}
/* The item itself: native ratio (aspect-ratio fallback off), clamped to
   the card, painted above the absolutely-positioned backdrop. */
li.offer .strip.solo img:not(.blur-bg) {
  position: relative; aspect-ratio: auto;
  max-width: 100%; object-fit: contain; background: none;
  border-radius: 0;
}
/* In solo mode the STAGE is the media surface, so the tile stops being a
   positioning and clipping box. The poster is centered and full-height here,
   so the play glyph lands in exactly the same place whichever box it
   measures from — but the badge can be WIDER THAN THE POSTER (91px of badge
   on an 88px tile: a 176×320 clip at the 160px mobile strip height, iPhone
   SE, 2026-07-27) and was being sliced off at the left by the tile's
   overflow. Anchored to the stage it has the full card width to sit in.
   `overflow: visible` is the half that matters: an absolutely positioned
   box escapes a clipping ancestor only when its containing block is OUTSIDE
   that ancestor, which is what dropping `position` here buys. */
li.offer .strip.solo .video-thumb { position: static; overflow: visible; }
/* Photoless placeholder (issue 00099): a strip-height slot in the map hatch
   language — striped panel, faint house glyph — so text-only cards keep
   the rhythm without faking a photo. */
li.offer .thumb.ph {
  width: 100%; height: var(--strip-h); border-radius: 13px 13px 0 0;
  display: flex; align-items: center; justify-content: center;
  color: var(--ink-faint);
  background: repeating-linear-gradient(
    45deg, var(--surface) 0 10px, var(--chip) 10px 20px);
}
/* The card's text column; min-width lets long localities ellipsis
   instead of stretching the card. */
li.offer .txt {
  min-width: 0; display: flex; flex-direction: column; gap: 2px;
  padding: 0 11px;
}
/* L1: price + the conversion aside on one baseline. */
li.offer .l1 {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  line-height: 1.3;
}
li.offer .price {
  font-size: 16.5px; font-weight: 700; letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}
/* Currency suggestion (issue 00047, ADR-0008): the posted price is the
   price; this is a quiet aside beside it. The element exists on EVERY
   priced card (currency.js needs a rewrite target) and hides itself when
   empty (USD-posted baseline, or selection == posted currency). */
li.offer .conv {
  font-size: 14px; color: var(--ink-faint); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
li.offer .conv:empty { display: none; }
/* L2: the quiet money line — deposit / rent basis / add-ons — small and
   faint, clamped to two lines. Parts are spans only; the « · » separators
   are CSS (between VISIBLE siblings), so a part dials.js hides never
   strands a dot. A line with every part hidden collapses entirely. */
li.offer .l2 {
  font-size: 14px; color: var(--ink-faint); line-height: 1.4;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
li.offer .l2 span[hidden] { display: none; }
li.offer .l2 span:not([hidden]) ~ span:not([hidden])::before { content: " · "; }
li.offer .l2:not(:has(span:not([hidden]))) { display: none; }
/* L3: what · where, one line, ellipsized. */
li.offer .l3 {
  font-size: 14px; line-height: 1.4;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
li.offer .locality { color: var(--ink-soft); }
/* L4: chips left (wrapping), the freshness note (issue 00099) right. */
li.offer .l4 {
  display: flex; flex-wrap: wrap; align-items: center; gap: 5px;
  margin-top: 2px; min-height: 20px;
}
li.offer .l4 .fresh {
  margin-left: auto; font-size: 11.5px; color: var(--ink-faint); flex: none;
}
/* The chip language (Ф3 prototype, locked tier model 2026-07-27): green =
   good news, yellow = an assumption to check, red = a hard constraint. */
li.offer .mchip {
  font-size: 11px; line-height: 1; padding: 3px 7px; border-radius: 999px;
  background: var(--chip); color: var(--ink-soft); white-space: nowrap;
}
li.offer .mchip.good {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 11%, var(--card));
}
li.offer .mchip.warn {
  color: var(--warn);
  background: color-mix(in srgb, var(--warn) 11%, var(--card));
}
li.offer .mchip.bad {
  color: var(--miss);
  background: color-mix(in srgb, var(--miss) 11%, var(--card));
}
/* Desktop (locked 2026-07-27): the card caps at 520px and the strip grows
   half again. The capped card CENTERS under the full-width search panel
   (user direction 2026-07-27) — width:100% + auto margins, so narrower
   than-cap viewports keep edge-to-edge cards. */
@media (min-width: 600px) {
  li.offer { --strip-h: 240px; max-width: 520px; width: 100%; margin: 0 auto; }
  li.offer .strip .pending-tile { width: 190px; }
}

/* Media-assembling placeholder (issue 00118): the census-exact promise
   («3 фото · видео — …») lives IN the media area (user direction
   2026-07-27) — the pulsing hatch panel when nothing is captured yet, or a
   trailing .pending-tile inside the strip when some media already shows.
   Pure CSS — the card upgrades via fragment re-render, never JS. */
li.offer .thumb.ph.pending {
  flex-direction: column; gap: 8px;
  animation: assembling-pulse 1.6s ease-in-out infinite;
}
li.offer .assembling {
  font-size: 12px; color: var(--ink-faint); font-style: italic;
  text-align: center; padding: 0 14px;
}
li.offer .strip .pending-tile {
  flex: none; width: 150px; height: var(--strip-h); scroll-snap-align: start;
  display: flex; flex-direction: column; gap: 8px;
  align-items: center; justify-content: center;
  color: var(--ink-faint);
  background: repeating-linear-gradient(
    45deg, var(--surface) 0 10px, var(--chip) 10px 20px);
  animation: assembling-pulse 1.6s ease-in-out infinite;
}
@keyframes assembling-pulse {
  0%, 100% { opacity: 0.55; }
  50% { opacity: 1; }
}

/* Video poster strip item (issue 00110 re-spec): leads the strip, wrapped
   so the play-symbol overlay can center over it — a still with no `<video>`
   element has no native play affordance on any desktop browser (research
   2026-07-24), so this is drawn by hand. Pure CSS/inline-SVG, no image
   asset. Posters carry no stored dims, so the img's 4/3 fallback sizes the
   slot. */
li.offer .video-thumb {
  position: relative; flex: none; overflow: hidden;
  scroll-snap-align: start;
}
li.offer .strip .video-thumb:first-child img { border-radius: 13px 0 0 0; }
li.offer .play-overlay {
  position: absolute; inset: 0; margin: auto; width: 50px; height: 50px;
  display: flex; align-items: center; justify-content: center;
  color: #fff; background: rgba(0, 0, 0, 0.45); border-radius: 999px;
  padding: 9px;
}
li.offer .play-overlay svg { width: 100%; height: 100%; }
/* Runtime · weight on the card's poster tile (user direction 2026-07-27),
   mirroring the offer page's `.photos .shot.video-shot .video-badge` — same
   corner, same metrics, same words (both render `video_badge`). Bottom-right
   keeps it clear of the centered play glyph and of the tile's rounded top
   corner. Tabular numerals so «0:06 · 2.4 MB» and «14:32 · 118.0 MB» don't
   jitter against each other down a scrolling column.
   White on a black scrim, not tokens: this sits on a PHOTOGRAPH, which does
   not theme — the same call marks.css:36-39 documents. */
li.offer .video-badge {
  position: absolute; z-index: 1; right: 8px; bottom: 8px;
  font-size: 12px; font-variant-numeric: tabular-nums;
  color: #fff; background: rgba(0, 0, 0, 0.6);
  border-radius: 6px; padding: 2px 7px;
}

/* Scroll arrows over the swipe strip (user direction 2026-07-27). The strip
   hides its scrollbar, which is right on touch — you swipe — but leaves a
   mouse with no way to reach photo 5 of 9 and no hint one exists. So: fine
   pointers only, and only at the desktop width where the card is a fixed
   520px column. A coarse pointer never sees these; the swipe is unchanged.
   Anchored to the card — `li.offer` is already `position: relative` for the
   marks rail — and sized to --strip-h so the pair sits over the media and
   never over the text block. */
li.offer > .strip-nav { display: none; }
@media (pointer: fine) and (min-width: 600px) {
  li.offer > .strip-nav {
    /* Inset by the card's border: the nav hangs off li.offer, but the media
       it frames lives inside the wrapper anchor, one edge-width in. Without
       this the arrows sit a pixel high and a pixel left of the photos. */
    position: absolute; display: block; height: var(--strip-h);
    top: var(--card-edge); left: var(--card-edge); right: var(--card-edge);
    /* The bar spans the whole strip, so it must be INVISIBLE to the pointer
       everywhere except the two buttons — otherwise it swallows clicks meant
       for the card link underneath and the card stops opening. */
    pointer-events: none;
  }
  /* strip.js sets this when the tiles turn out to fit after all; the
     display:block above beats the UA's [hidden] rule, so restate it. */
  li.offer > .strip-nav[hidden] { display: none; }
  /* Deliberately the SAME OBJECT as the lightbox's ‹ › (offer.css `.lb-nav`,
     user direction 2026-07-27): same 46px circle, same scrim, same glyph
     size, same optical nudge, absolutely positioned against the media rather
     than laid out in a row. Two different circular arrows over two different
     photo surfaces in one product would read as two different products. */
  li.offer .strip-arrow {
    pointer-events: auto;
    position: absolute; top: 50%; transform: translateY(-50%);
    width: 46px; height: 46px; font-size: 30px;
    display: flex; align-items: center; justify-content: center;
    font-family: inherit; line-height: 1;
    /* Flex centers the LINE BOX, but ‹ › ink sits below its midpoint
       (baseline metrics) and reads as low-shifted. Zero the UA button
       padding, then nudge the optical center up — the same 5px `.lb-nav`
       uses, arrived at there the same way. */
    padding: 0 0 5px;
    color: #fff; background: rgba(0, 0, 0, 0.4);
    border: none; border-radius: 999px; cursor: pointer;
    opacity: 0; transition: opacity 120ms ease;
  }
  li.offer .strip-arrow.prev { left: 12px; }
  li.offer .strip-arrow.next { right: 12px; }
  /* BOTH arrows appear together on hover (user direction 2026-07-27) — a
     lone arrow reads as "this is the only direction that exists". The one
     with nowhere to go dims to 0.25 rather than vanishing, which is again
     exactly what the lightbox pair does at the ends of an album. */
  li.offer:hover .strip-arrow,
  li.offer .strip-arrow:focus-visible { opacity: 1; }
  li.offer:hover .strip-arrow:disabled { opacity: 0.25; }
  li.offer .strip-arrow:disabled { cursor: default; }
  li.offer .strip-arrow:not(:disabled):hover { background: rgba(0, 0, 0, 0.65); }
}

.empty { color: var(--ink-soft); font-size: 14px; margin: 24px 0; }

/* Search-island status line: the match count («Найдено 37 предложений»),
   the not-found / error notices, and the slow-search escalation driven by
   search.js: nothing for the first 3s, then a breathing h3-sized
   «дольше обычного» line (.loading), then at 8s the orange stuck notice
   (.stuck, steady — an alarm shouldn't breathe) whose tail refreshes the
   page. Lines fade in/out via the .line span's opacity transition. */
/* p.feed-count is the SSR feed's own count line (user direction 2026-07-27) —
   the same sentence in the same slot, so the two surfaces must share the type
   and the metrics exactly or switching between them twitches. Only the RESTING
   look is shared: the loading/stuck escalation below stays island-only, since
   the SSR count arrives with the document and can never be pending. */
p.island-status, p.feed-count {
  font-size: 13.5px; color: var(--ink-soft); margin: 4px 2px 12px;
}
p.island-status.loading, p.island-status.stuck { font-size: 18px; }
p.island-status.loading { animation: assembling-pulse 3.2s ease-in-out infinite; }
p.island-status.stuck { color: #cc7722; }
p.island-status.stuck a { color: inherit; font-weight: 600; }
p.island-status .line { transition: opacity 0.4s ease; }
p.island-status .line.fading { opacity: 0; }
/* Desktop: the count line belongs to the 520px card column, not the 680px
   wrap (user direction 2026-07-27) — left-flush under centered cards it read
   as a stray. Same cap and auto margins as li.offer, so its text starts on
   the card's left edge; the 2px inset goes, that IS the alignment. */
@media (min-width: 600px) {
  p.island-status, p.feed-count {
    max-width: 520px; margin-left: auto; margin-right: auto;
  }
}

/* In-flight skeleton cards: the offer card's chrome with blank bars where
   the text lands and the photoless hatch in the thumb slot, breathing on
   the assembling pulse (staggered so the list ripples, not blinks). They
   hold the result area's shape from search start until the first page of
   real cards swaps in. */
/* Skeletons enter softly (.warming on the results list while it holds
   them): transparent for the first 0.25s, then a 0.5s fade-in. A search
   that resolves inside the grace window swaps in real cards before
   anything painted — small price adjustments never flash the skeletons.
   List-level opacity multiplies the per-card pulse, so the breathing wave
   keeps its phase through the fade. */
ul.offers.warming { animation: skel-reveal 0.5s ease 0.25s both; }
@keyframes skel-reveal { from { opacity: 0; } to { opacity: 1; } }

li.offer.skel {
  display: flex; flex-direction: column; align-items: stretch; gap: 8px;
  background: var(--card); border: 1px solid var(--line);
  border-radius: 14px; padding: 0 0 11px;
  animation: assembling-pulse 3.2s ease-in-out infinite;
}
/* One wave, top to bottom: each card trails the one above by 0.4s of the
   3.2s cycle. Delays are the same phase shifted a full cycle NEGATIVE, so
   every card is already mid-breath at first paint — no frozen lead-in.
   Keyed on nth-LAST-child: mid-hydration the skeletons are the pending
   TAIL below rendered cards, so counting from the bottom addresses them
   wherever the list top has grown to. */
li.offer.skel:nth-last-child(6) { animation-delay: -3.2s; }
li.offer.skel:nth-last-child(5) { animation-delay: -2.8s; }
li.offer.skel:nth-last-child(4) { animation-delay: -2.4s; }
li.offer.skel:nth-last-child(3) { animation-delay: -2s; }
li.offer.skel:nth-last-child(2) { animation-delay: -1.6s; }
li.offer.skel:nth-last-child(1) { animation-delay: -1.2s; }
li.offer.skel .txt { gap: 7px; }
li.offer.skel .bar {
  display: block; height: 12px; border-radius: 6px; background: var(--chip);
}
li.offer.skel .bar.b1 { height: 15px; width: 45%; }
li.offer.skel .bar.b2 { width: 25%; }
li.offer.skel .bar.b3 { width: 60%; }

/* Failed-card placeholder: the card's chrome around an apology + the offer's
   real link, standing in while search.js retries the fragment on backoff
   (and staying if all attempts drain — the link still works). */
li.offer.card-fail {
  display: flex; gap: 12px; align-items: center;
  background: var(--card); border: 1px solid var(--line);
  border-radius: 14px; padding: 10px;
}
/* The apology card keeps the compact pre-Ф3 row shape — it has no photos
   to strip, and a full-height hatch would shout about a hiccup. */
li.offer.card-fail .thumb.ph { width: 96px; height: 72px; border-radius: 8px; }
li.offer.card-fail .txt { flex: 1; gap: 3px; padding: 0; }
li.offer.card-fail .fail-note { font-size: 13.5px; color: var(--ink-soft); }
li.offer.card-fail .fail-link { font-size: 13.5px; color: var(--ink-soft); }
li.offer.card-fail .fail-link a { color: var(--accent); font-weight: 600; }

/* The pager (user direction 2026-07-27, replacing both «Показать ещё» and
   the prev/next-only SSR pager of issue 00100): ONE pill language for the
   SSR feed's links and the search island's buttons — a bare «←», windowed
   page numbers with … gaps, a bare «→», centered under the list. NEUTRAL,
   no accent; the current page sits filled in inverse video (rule below). */
nav.pager {
  display: flex; justify-content: center; flex-wrap: wrap; gap: 6px;
  align-items: center; margin-top: 16px;
}
/* The copy ABOVE the list (user direction 2026-07-27) — same controls, mirrored
   spacing so it hangs off the list rather than the element before it. Rendered
   only past page 1 (SSR omits it, the island leaves it empty and hidden), so
   this never adds a gap at the top of a page-1 feed. */
nav.pager.top { margin-top: 4px; margin-bottom: 14px; }
/* Resting pills carry NO outline in either theme (user direction 2026-07-27) —
   bare numbers, with only the current page's fill and the hover wash drawn.
   `transparent` rather than `border: none`: the border box still occupies its
   1px, so pills keep the exact size and rhythm they had when outlined, and the
   filled current-page pill (background-clip defaults to border-box) paints
   right through it. */
nav.pager a,
nav.pager button,
nav.pager span.num {
  font: inherit; font-size: 13.5px; font-weight: 600; color: var(--ink);
  background: none; border: 1px solid transparent; border-radius: 999px;
  padding: 5px 14px; text-decoration: none; cursor: pointer;
}
/* Hover draws the chip wash AND an edge one token stronger than it (user
   direction 2026-07-27: the wash alone read smaller than the filled current
   page). It is not actually smaller — every pill is 32.25px tall and «1» is
   the NARROWEST of them — but a bright fill on a dark ground optically
   expands while a wash a hair off the page tone has no findable edge, so the
   two read as different sizes. The border is what gives the hover state its
   outline back; a border in the FILL's own colour would have changed nothing,
   since background-clip is border-box and the wash already paints under the
   transparent border. */
nav.pager a:hover,
nav.pager button:hover:not(:disabled) {
  background: var(--chip); border-color: var(--line);
}
/* The page you are ON is a pill in INVERSE VIDEO — the ink/bg pair swapped —
   because the chip tone it wore before was too close to the resting pills to
   find at a glance. Tokens, not the literal #000/#fff it shipped with in prod
   v80-84: black is theme-BLIND, and in dark mode #000 is darker than the page
   (--bg: #151412), so the filled pill read as a hole punched in the list while
   #fff was the brightest thing on screen. var(--ink) gives the light theme the
   near-black the user asked for and the dark theme a warm off-white
   (#e9e6e1 — "gray, not white but close", user direction 2026-07-27), from one
   rule with no media query.
   One rule also serves both pagers: the SSR feed's inert span and the island's
   disabled button. Both selectors carry a class or element to the LEFT of the
   attribute on purpose: a bare `nav.pager [aria-current="page"]` (0,2,1) loses
   to the resting-pill rule's `nav.pager span.num` (0,2,2) above, and the fill
   silently never lands — it didn't, until prod v80.
   The border is --bg, NOT the fill's own --ink (user direction 2026-07-27).
   Borders paint above the background, so a page-coloured ring insets the
   visible fill by 1px all round — the filled pill's ink block ends up 2px
   narrower and 2px shorter than a hovered pill's, while the boxes stay
   byte-identical and the row's rhythm never moves. That deliberate mismatch
   is the correction for irradiation: a bright fill on a dark ground reads
   LARGER than its true size, so the two pills only look equal once the
   filled one is drawn slightly small. */
nav.pager .num[aria-current="page"],
nav.pager button[aria-current="page"] {
  background: var(--ink); border-color: var(--bg); color: var(--bg);
  cursor: default;
}
nav.pager .gap { color: var(--ink-faint); }
