/* ---- Radar fonts (OFL, local — Golos Text + Michroma, no external CDN) ---- */
@font-face {
  font-family: "Golos Text"; font-style: normal; font-weight: 400; font-display: swap;
  src: url("./fonts/golos-text-400-cyrillic.woff2") format("woff2");
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: "Golos Text"; font-style: normal; font-weight: 400; font-display: swap;
  src: url("./fonts/golos-text-400-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122;
}
@font-face {
  font-family: "Golos Text"; font-style: normal; font-weight: 500; font-display: swap;
  src: url("./fonts/golos-text-500-cyrillic.woff2") format("woff2");
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: "Golos Text"; font-style: normal; font-weight: 500; font-display: swap;
  src: url("./fonts/golos-text-500-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122;
}
@font-face {
  font-family: "Golos Text"; font-style: normal; font-weight: 600; font-display: swap;
  src: url("./fonts/golos-text-600-cyrillic.woff2") format("woff2");
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: "Golos Text"; font-style: normal; font-weight: 600; font-display: swap;
  src: url("./fonts/golos-text-600-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122;
}
@font-face {
  font-family: "Golos Text"; font-style: normal; font-weight: 700; font-display: swap;
  src: url("./fonts/golos-text-700-cyrillic.woff2") format("woff2");
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: "Golos Text"; font-style: normal; font-weight: 700; font-display: swap;
  src: url("./fonts/golos-text-700-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122;
}
@font-face {
  font-family: "Michroma"; font-style: normal; font-weight: 400; font-display: swap;
  src: url("./fonts/michroma-400-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+2000-206F;
}

:root {
  /* Radar palette — OUR colors always, regardless of the user's Telegram theme (blue, purple, …).
     Telegram only ever tells us light/dark (see data-theme in JS); it never tints these values. */
  --bg: #F6F3EC;
  --surface: #FFFFFF;
  /* A second, slightly-tinted surface for nested fields/chips inside a card (search card, filter
     group icons, subscription actions) - same color-mix every .pill--surface2 already used,
     named once here so Этап 5's new CSS doesn't repeat that mix expression everywhere. */
  --surface-2: color-mix(in srgb, var(--text) 6%, var(--surface));
  --text: #13251D;
  --muted: #6E7D75;
  --accent: #0E3B2C;
  --accent-text: #EFE9DE;
  --line: color-mix(in srgb, var(--text) 12%, transparent);
  --card: var(--surface);
  /* Amber — the one signal accent (price drop / cheaper / new), independent of Telegram's theme */
  --amber: #E8A33D;
  --amber-tint: color-mix(in srgb, var(--amber) 16%, transparent);
  /* --amber-ink: amber-as-TEXT on a neutral bg (readable on both themes — dark ink in light mode,
     the amber itself in dark mode). --on-amber: fixed dark ink for text ON a solid amber bg —
     never equals --amber itself, or the text disappears into its own background. */
  --amber-ink: #8A5A17;
  --on-amber: #1A1410;
  --like: #FF5D7A;
  --good: var(--amber-ink);
  --warn: #B07A2E;
  --danger: #C64B4B;
  /* Text-safe accent: --accent itself is tuned as a BUTTON background (readable with --accent-text
     on top), not as text color on the page background — on dark backgrounds a dark-green accent
     would read as barely visible. --accent-ink is for text/icons that sit directly on --bg/--surface. */
  --accent-ink: var(--accent);
  --radius: 18px;
  --font-ui: "Golos Text", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-logo: "Michroma", -apple-system, sans-serif;
  color-scheme: dark light;
  /* Motion tokens: fast = instant tap feedback, base = state/color changes, slow = sheets/entrances. */
  --dur-fast: 120ms;
  --dur-base: 200ms;
  --dur-slow: 280ms;
  --ease: cubic-bezier(.4, 0, .2, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);
  /* Этап 6 (motion-spec.md): a slight overshoot-and-settle for things the user just summoned
     (sheets opening, the segment thumb landing) - never used on the way OUT, see .sheet.closing. */
  --spring: cubic-bezier(.34, 1.56, .64, 1);
}

/* One blanket rule instead of annotating every animation/transition below - also catches
   anything future CSS adds here. JS-driven gestures (photoviewer drag, swipe-to-delete) check
   window.matchMedia("(prefers-reduced-motion: reduce)") themselves, since they are not CSS. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important; animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important; scroll-behavior: auto !important;
  }
}

/* prefers-color-scheme is the fallback for a plain browser; inside Telegram, JS sets
   data-theme from Telegram.WebApp.colorScheme and that always wins (see :root[data-theme] below). */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #0B1813;
    --surface: #132620;
    --text: #EFE9DE;
    --muted: #9BAFA5;
    --accent: #13432F;
    --accent-ink: var(--accent-text);
    --amber-ink: var(--amber);
    --price-down: #3ECF86;
    --price-up: #FF6B6B;
  }
}

:root[data-theme="dark"] {
  --bg: #0B1813;
  --surface: #132620;
  --text: #EFE9DE;
  --muted: #9BAFA5;
  --accent: #13432F;
  --accent-ink: var(--accent-text);
  --amber-ink: var(--amber);
  --price-down: #3ECF86;
  --price-up: #FF6B6B;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

/* pan-x/pan-y (no "pinch-zoom") keeps normal scrolling but drops pinch- and double-tap-zoom
   app-wide; belt-and-suspenders with the viewport meta + gesturestart guard in main.js. */
html { scroll-behavior: smooth; touch-action: pan-x pan-y; }
/* п.3: no visible scrollbar anywhere (main page + every sheet + the detail screen) - scrollbar-width:
   none removes the reserved gutter entirely (no width jump when the bar would otherwise appear). */
html, .sheet-scroll, .detail { scrollbar-width: none; }
html::-webkit-scrollbar, .sheet-scroll::-webkit-scrollbar, .detail::-webkit-scrollbar { display: none; }

/* ---- tap feedback + smooth state changes, shared by every tappable control ---- */
.btn, .chip, .seg button, .icon-btn, .tab-bar__item, .picker-field, .picker-option, .notif-actions__btn {
  transition: transform var(--dur-fast) var(--ease), background var(--dur-base) var(--ease),
    color var(--dur-base) var(--ease), border-color var(--dur-base) var(--ease);
}
.btn:active, .chip:active, .seg button:active, .icon-btn:active, .tab-bar__item:active,
.picker-field:active, .picker-option:active, .notif-actions__btn:active {
  transform: scale(0.96);
}

/* ♥ pulse when a listing is added to favorites (util.js pulse()) - never on removal. */
/* Heartbeat: two beats (the second a little softer). Only the icon moves - never the round backing. */
@keyframes heart-pulse { 0% { transform: scale(1); } 18% { transform: scale(1.42); } 36% { transform: scale(1); } 54% { transform: scale(1.24); } 78%, 100% { transform: scale(1); } }
.pulse svg { animation: heart-pulse 620ms var(--ease-out); }
/* The liked card gives one short push (util.js cardPulse). */
@keyframes card-pulse { 40% { transform: scale(1.03); } }
.card-pulse { animation: card-pulse 300ms var(--ease-out); }

/* Этап 7c, п.4 (util.js unlikeBurst()): the opposite of the pulse above - a short "let go" poof
   when a favorite is turned OFF from the notifications feed (feed.js row()), in the spirit of
   Telegram's own "message deleted" animation. The icon shrinks/fades while two small particles
   fly out from the button's centre and fade. Covered by the blanket prefers-reduced-motion rule
   above (the ::before/::after selector there matches these too) - no separate exception needed. */
@keyframes unlike-shrink { 0% { transform: scale(1); opacity: 1; } 60% { transform: scale(.72); opacity: .55; } 100% { transform: scale(.55); opacity: 0; } }
@keyframes unlike-particle { 0% { transform: translate(0, 0) scale(1); opacity: 1; } 100% { transform: translate(var(--px), var(--py)) scale(0); opacity: 0; } }
.unlike-burst { position: relative; }
.unlike-burst svg { animation: unlike-shrink 320ms var(--ease-out) both; }
.unlike-burst::before, .unlike-burst::after {
  content: ""; position: absolute; top: 50%; left: 50%; width: 5px; height: 5px; margin: -2.5px;
  border-radius: 50%; background: currentColor; opacity: 0; pointer-events: none;
  animation: unlike-particle 380ms var(--ease-out) both;
}
.unlike-burst::before { --px: -13px; --py: -12px; animation-delay: 20ms; }
.unlike-burst::after { --px: 12px; --py: -14px; animation-delay: 55ms; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.45 var(--font-ui);
  -webkit-font-smoothing: antialiased;
  touch-action: pan-x pan-y;
}
body.no-scroll { overflow: hidden; }

button, input, select { font: inherit; color: inherit; }
button { cursor: pointer; }
/* App-wide zoom is off (viewport maximum-scale=1/user-scalable=no + gesturestart guard in main.js);
   this still kills the double-tap/press-and-hold zoom delay on controls. */
button, a, select, .chip, .switch, .check { touch-action: manipulation; }
.hidden { display: none !important; }

/* ---- splash ---- */
.splash {
  position: fixed; inset: 0; z-index: 100; display: flex; flex-direction: column; align-items: center;
  background: var(--accent); color: var(--accent-text); transition: opacity .25s ease; pointer-events: none;
  padding-bottom: calc(28px + env(safe-area-inset-bottom));
}
.splash.hidden { opacity: 0; display: flex; } /* main.js adds this, then removes the node after the transition */
/* Mark + wordmark + progress bar stay centered; "by Verkus" is pinned to the bottom (its own flex
   item below the flex:1 center group, not part of it). */
.splash__center { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; }
.splash__word { font-family: var(--font-logo); font-size: 22px; letter-spacing: 4px; font-weight: 400; clip-path: inset(0 100% 0 0); animation: word-reveal .6s var(--ease) 1.65s both; }
.splash__bar { width: 120px; height: 3px; border-radius: 999px; background: color-mix(in srgb, var(--accent-text) 24%, transparent); overflow: hidden; opacity: 0; animation: bar-fade-in .3s ease-out .5s both; }
/* The bar appears right after the dot lands and creeps towards ~85% (slowing down) while the first
   screen loads; main.js then finishes it to 100% (SPLASH_FILL_MS) once that screen is ready. */
.splash__bar-fill { height: 100%; width: 0%; background: var(--amber); border-radius: 999px; transition: width 2500ms cubic-bezier(.15, .6, .3, 1); }
.splash__by { font-family: var(--font-ui); font-size: 12px; letter-spacing: 1px; font-weight: 500; opacity: .72; }

/* ---- splash build-in (approved as docs/design/splash-17.html v2) ----
   Точка хлопает (--spring, 0-.6s) -> оба клинка одновременно (.55-1.4s) -> дуга выдвигается из
   истинного центра сразу в обе стороны (1.05-1.8s, см. комментарий у mark-arc-half в index.html)
   -> RADAR открывается шторкой (1.65-2.25s); полоска загрузки появляется с 0.5s и идёт всю заставку
   (main.js, см. SPLASH_*). После сборки метка тихо
   дышит и точка держит кольцо-пульс, пока идёт загрузка - coder's hideSplash()/.hidden просто
   убирает #splash когда готово, когда бы это ни случилось (0.8s или 3s - см. e64e943), без
   отдельной остановки анимации. Reduced-motion: общее блок-правило выше в этом файле схлопывает
   все длительности - каждый keyframe оканчивается на уже собранном, спокойном кадре. */
.splash__mark { width: 88px; height: 123px; }
.mark-breathe { transform-box: fill-box; transform-origin: center; animation: mark-breathe 2.4s ease-in-out 2.25s infinite; }
.mark-blade { transform-box: fill-box; transform-origin: 50% 100%; opacity: 0; animation: mark-blade-grow .85s var(--ease-out) .55s both; }
.mark-arc-half { stroke-dasharray: 100; stroke-dashoffset: 100; animation: mark-arc-draw .75s var(--ease-out) 1.05s both; }
.mark-dot-ring { transform-box: fill-box; transform-origin: center; opacity: 0; animation: mark-dot-ring-pulse 2s ease-out 2.25s infinite; }
.mark-dot { transform-box: fill-box; transform-origin: center; opacity: 0; animation: mark-dot-pop .6s var(--spring) both; }
@keyframes word-reveal { 0% { clip-path: inset(0 100% 0 0); } 100% { clip-path: inset(0 0% 0 0); } }
@keyframes bar-fade-in { 0% { opacity: 0; } 100% { opacity: 1; } }
@keyframes mark-breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.02); } }
@keyframes mark-blade-grow { 0% { opacity: 0; transform: scaleY(.15) scaleX(.7); } 100% { opacity: 1; transform: scaleY(1) scaleX(1); } }
@keyframes mark-arc-draw { 0% { stroke-dashoffset: 100; } 100% { stroke-dashoffset: 0; } }
@keyframes mark-dot-ring-pulse { 0% { opacity: .6; transform: scale(1); } 100% { opacity: 0; transform: scale(1.6); } }
@keyframes mark-dot-pop { 0% { opacity: 0; transform: scale(0); } 55% { opacity: 1; transform: scale(1.25); } 100% { opacity: 1; transform: scale(1); } }

#app {
  max-width: 560px;
  margin: 0 auto;
  min-height: 100dvh;
  padding: 0 14px calc(84px + env(safe-area-inset-bottom));
}

.view { display: none; }
/* Этап 6 motion-spec.md §2.2: direction-aware crossfade between tabs - main.js's showTab() sets
   --tab-dir (1 = moving right, -1 = moving left) on the root right before switching .active, so
   the incoming view slides in from the side it conceptually came from. The outgoing view is just
   display:none'd instantly (no parallel exit) - a deliberate simplification, see showTab(). */
@keyframes view-fade-in { from { opacity: 0; transform: translateX(calc(var(--tab-dir, 0) * 12px)); } to { opacity: 1; transform: none; } }
.view.active { display: block; }
/* Tab switch (util.js cascadeIn): each block rises in with a short stagger; a backwards fill, so the
   class can be dropped afterwards without leaving a transform behind. */
@keyframes tab-rise { from { opacity: 0; transform: translateY(14px); } }
.tab-cascade { animation: tab-rise 280ms var(--ease-out) backwards; animation-delay: calc(var(--stagger-i, 0) * 35ms); }

/* First page of a list/feed only (util.js staggerIn) - up to the first 8 cards fade/slide in
   one after another instead of all popping in at once. */
@keyframes stagger-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.stagger-in { animation: stagger-in var(--dur-base) var(--ease-out) both; animation-delay: calc(var(--stagger-i, 0) * 30ms); }

.screen-title { margin: 18px 0 12px; font-size: 23px; font-weight: 700; letter-spacing: -0.01em; }

/* ---- app bar (brand header above each tab) ---- */
.app-bar { display: flex; align-items: center; gap: 6px; height: 28px; padding: 6px 0 2px; }
.app-bar__mark { display: inline-flex; color: var(--accent-ink); }
.app-bar__mark svg { height: 14px; width: auto; display: block; }
.app-bar .logo { font-family: var(--font-logo); font-weight: 400; font-size: 13px; letter-spacing: 2px; color: var(--accent-ink); }

/* ---- top bar ---- */
.topbar {
  padding: 10px 0 8px;
  display: grid;
  gap: 8px;
  border-bottom: 1px solid var(--line);
}
/* min-width:0 on the row and its flexed children - without it, a CSS Grid ancestor (.sheet-scroll)
   sizes its track to this row's un-shrunk min-content (e.g. a picker-field's full, un-ellipsized
   label), overflowing the sheet sideways instead of letting text-overflow:ellipsis do its job. */
.row { display: flex; gap: 8px; align-items: center; min-width: 0; }
.row > .seg:first-child, .row > .select, .row > .picker-field { flex: 1; min-width: 0; }
.row.between { justify-content: space-between; }
.filter-btn { min-height: 44px; }

.seg { position: relative; display: flex; padding: 3px; gap: 3px; border-radius: 12px; background: var(--surface); border: 1px solid var(--line); }
/* util.js syncSegThumb() positions this behind the active button instead of the button itself
   jumping to var(--accent) - call it any time a button gets/loses `.on`. */
.seg__thumb {
  position: absolute; top: 3px; bottom: 3px; left: 0; border-radius: 9px; background: var(--accent);
  /* Этап 6 motion-spec.md §3: spring easing on position/size (the same light overshoot as sheets
     opening) - opacity (fading the thumb in/out when nothing or everything is selected) stays on
     the plain --ease, a spring on a fade reads as a glitch, not a bounce. */
  transition: transform 260ms var(--ease-out), width 260ms var(--ease-out), opacity var(--dur-base) var(--ease);
  z-index: 0; pointer-events: none;
}
.seg button {
  position: relative; z-index: 1; flex: 1; min-height: 44px; display: flex; align-items: center; justify-content: center;
  border: 0; padding: 8px 12px; border-radius: 9px; background: transparent; color: var(--muted); font-weight: 600;
  font-family: var(--font-ui);
}
.seg button.on { color: var(--accent-text); }
.seg.small button { padding: 6px 10px; font-size: 13px; }
/* List/swipe toggle as two plain icons (no text) so it sits in the same row as deal+currency at 375px. */
.seg.icon-only { flex: none; }
.seg.icon-only button { flex: none; width: 38px; padding: 0; }
.seg.icon-only button svg { width: 20px; height: 20px; }

.select, .search-input {
  width: 100%; min-width: 0; height: 44px; padding: 0 12px; border-radius: 12px;
  border: 1px solid var(--line); background: var(--surface); outline: none; font-family: var(--font-ui);
}
.search-input:focus, .select:focus, .field input:focus { border-color: var(--accent); }

/* ---- picker (our own select — a field button + bottom sheet with a checkmark) ---- */
.picker-field {
  width: 100%; min-width: 0; height: 44px; padding: 0 12px; border-radius: 12px;
  border: 1px solid var(--line); background: var(--surface); color: var(--text); font-family: var(--font-ui); font-size: 15px;
  display: flex; align-items: center; justify-content: space-between; gap: 8px; text-align: left;
}
.picker-field__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.picker-field svg { width: 18px; height: 18px; color: var(--muted); flex: none; }
.picker-list { display: flex; flex-direction: column; }
.picker-option {
  min-height: 50px; padding: 0 2px; border: 0; border-bottom: 1px solid var(--line); background: transparent;
  color: var(--text); font-family: var(--font-ui); font-size: 15px; text-align: left;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
}
.picker-option:last-child { border-bottom: none; }
.picker-option.on { font-weight: 700; color: var(--accent-ink); }
.picker-option svg { width: 20px; height: 20px; color: var(--accent-ink); flex: none; }

.chips { display: flex; gap: 8px; flex-wrap: wrap; }
.chips.scroll { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; margin: 0 -14px; padding: 0 14px; }
.chips.scroll::-webkit-scrollbar { display: none; }
.chip {
  flex: none; min-height: 44px; display: inline-flex; align-items: center; justify-content: center;
  padding: 6px 14px; border-radius: 999px; border: 1px solid var(--line); background: var(--surface);
  color: var(--text); font-size: 13.5px; font-weight: 600;
}
.chip.on { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }

.count-line { min-height: 18px; font-size: 13px; color: var(--muted); }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: 44px; padding: 0 16px;
  border: 1px solid transparent; border-radius: 14px; text-decoration: none; font-weight: 700; white-space: nowrap;
  font-family: var(--font-ui);
}
/* An icon inline with text (e.g. a phone/bell/gear SVG before a label) needs an explicit size —
   svg has no usable intrinsic one. Same reasoning as .icon-btn svg above. */
.btn svg, .seg button svg, .chip svg { width: 16px; height: 16px; display: inline-block; vertical-align: -3px; flex: none; }
.btn.primary { background: var(--accent); color: var(--accent-text); }
.btn.ghost { background: var(--surface); border-color: var(--line); color: var(--text); font-weight: 600; }
.btn.wide { width: 100%; }
.filter-btn { position: relative; flex: none; }
.count-dot {
  min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; background: var(--amber); color: var(--on-amber);
  font-size: 12px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center;
}
.link-btn {
  min-height: 44px; display: inline-flex; align-items: center;
  border: 0; background: none; color: var(--accent-ink); padding: 4px 6px; font-weight: 700; font-family: var(--font-ui);
}

/* ---- cards ---- */
.cards { display: grid; gap: 14px; padding-top: 14px; }
.card {
  overflow: hidden; border-radius: var(--radius); background: var(--card); border: 1px solid var(--line);
  cursor: pointer; outline: none;
}
.card:focus-visible { border-color: var(--accent); }

/* Fade+collapse instead of an instant pop when util.js's animateRemove() removes a card/event row.
   max-height is a fixed large number (not "auto") because CSS can only transition to/from a length —
   650px comfortably clears any real card/notif-swipe height, so the collapse still reads as smooth. */
.card, .notif-swipe { transition: opacity .25s ease, transform .25s ease, max-height .25s ease; max-height: 650px; }
.card--removing {
  opacity: 0; transform: scale(0.97); max-height: 0 !important; pointer-events: none; overflow: hidden;
}
@media (prefers-reduced-motion: reduce) {
  .card, .notif-swipe { transition: none; }
}
.card.gone .cover, .card.gone .card-body { opacity: 0.55; }
.cover { position: relative; aspect-ratio: 16 / 10; background: var(--surface); }
.cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cover-empty, .gallery-empty { height: 100%; display: grid; place-items: center; font-size: 40px; opacity: 0.4; }
.gallery-empty { aspect-ratio: 16 / 10; background: var(--surface); }
/* Этап 20c (util.js coverImg()): a stored photo URL 404s - distinct from "no photo at all" above by
   its own icon, and (where there is room) the "Фото недоступно" text; a bare SVG icon() has no
   intrinsic size of its own, so this sizes it explicitly rather than relying on cover-empty's
   font-size (which only ever constrained an emoji, not the SVG icons used today). */
.cover-broken { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; opacity: 1; }
.cover-broken svg { width: 32px; height: 32px; }
.cover-broken span { font-size: 12px; text-align: center; padding: 0 10px; line-height: 1.3; }
.badge {
  display: inline-block; padding: 3px 9px; border-radius: 999px; font-size: 12px; font-weight: 700;
  background: color-mix(in srgb, var(--text) 10%, transparent); color: var(--text); white-space: nowrap;
}
.badge.photos, .badge.gallery-count {
  position: absolute; left: 10px; bottom: 10px; background: rgba(0,0,0,.62); color: #fff;
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); box-shadow: 0 0 0 1px rgba(255,255,255,.3);
}
.badge.gallery-count { left: auto; right: 10px; }
.badge.owner { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent-ink); }
.badge.agency { background: color-mix(in srgb, var(--warn) 18%, transparent); color: var(--warn); }
.badge.gone { background: color-mix(in srgb, var(--danger) 30%, transparent); color: var(--danger); }
/* Site plate on top of a photo: Kufar green, Onliner blue, Realt orange.
   Этап 8i task п.2: read smaller than the search-card's own source chips (15px now) - bumped. */
.src-plate {
  --site: #6b7280;
  position: absolute; top: 10px; left: 10px; z-index: 2; padding: 5px 10px; border-radius: 8px;
  background: var(--site); color: #fff; font-size: 13.5px; font-weight: 700; letter-spacing: 0.02em; line-height: 1.3;
  box-shadow: 0 1px 4px rgba(0,0,0,.35); pointer-events: none;
}
.src-plate.src-kufar, .chip.src-kufar { --site: #12a150; }
.src-plate.src-onliner, .chip.src-onliner { --site: #2f6df6; }
.src-plate.src-realt, .chip.src-realt { --site: #f26b1d; }
/* Этап 8h task п.2: map mini-card's own plate (map.js showMiniCard) - static/inline instead of
   .src-plate's absolute top-left, sized for sitting next to the price text, not on a 64px photo. */
.src-plate--inline {
  position: static; display: inline-flex; align-items: center; flex: none; padding: 2px 7px;
  border-radius: 6px; font-size: 10.5px; line-height: 1.3; box-shadow: none;
}
/* Muted pill + a colored dot for the site — quieter than a full-color fill, the dot still carries
   the Kufar/Onliner/Realt identity. .on/.off is the include/exclude toggle (every site is on by
   default; a tap excludes it, dimming the dot and striking the label). */
.chip.src-chip { display: inline-flex; align-items: center; gap: 7px; background: var(--surface); border-color: var(--line); color: var(--text); }
.chip__dot { width: 7px; height: 7px; border-radius: 50%; background: var(--site, var(--muted)); flex: none; }
/* ON: the site's own color (Kufar green / Onliner blue / Realt orange), not the app accent —
   overrides the generic `.chip.on` rule below via higher specificity. */
.chip.src-chip.on { background: var(--site); border-color: var(--site); color: #fff; }
.chip.src-chip.on .chip__dot { background: #fff; }
/* OFF: muted but still readable, no strikethrough. */
.chip.src-chip.off { color: var(--muted); opacity: 0.7; }
.chip.src-chip.off .chip__dot { background: var(--muted); }
.gallery-wrap { position: relative; }

/* Same flat on several sites (cloud/dedup.py) */
.cheaper { margin-top: 6px; font-size: 13.5px; font-weight: 700; color: var(--good); }
.cheaper.big { margin: 0 0 10px; padding: 10px 12px; border-radius: 12px; font-size: 15px;
  background: var(--amber-tint); }
.also { margin-top: 4px; font-size: 12.5px; color: var(--muted); }
.dupes h3 { margin-top: 0; }
.dupe-row { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 44px; margin-top: 6px; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: 12px; background: var(--card); text-align: left; }
.dupe-row.cheaper-row { border-color: var(--amber); }
.dupe-site { flex: 1; font-weight: 700; }
.dupe-price { font-variant-numeric: tabular-nums; }
.dupe-diff { color: var(--good); font-weight: 700; font-size: 13px; }
.dupe-go { color: var(--muted); }
.dupe-go svg { width: 16px; height: 16px; display: block; }

/* Hit area is 44x44 (finger-sized) but the visible round button stays 36x36 via ::before,
   so it doesn't grow into a bulkier blob on top of the photo. */
.icon-btn {
  position: absolute; top: 6px; width: 44px; height: 44px; border: 0; background: transparent;
  color: #fff; font-size: 18px; line-height: 1; display: grid; place-items: center;
}
/* SVG icons have no intrinsic small size of their own — without this an inline <svg> defaults
   to ~300x150 and blows up any button it's dropped into. Every icon-only control gets a fixed
   px size here instead of relying on font-size/em, which SVGs ignore unless told to use it. */
.icon-btn svg { width: 20px; height: 20px; display: block; position: relative; z-index: 1; }
.icon-btn.plain svg { width: 24px; height: 24px; }
/* Dark translucent + blur so ✕/♥ stay readable on a light/busy photo, not just a dark one —
   no white ring (looked out of place with the rest of the design). */
.icon-btn::before {
  content: ""; position: absolute; inset: 4px; border-radius: 50%; background: rgba(0,0,0,.6);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); box-shadow: 0 1px 4px rgba(0,0,0,.35);
}
.icon-btn:not(.plain) svg { stroke-width: 2.3; }
/* On a photo the round backing (blurred ::before) must stay perfectly still: press feedback moves
   only the icon, not the button - scaling the button made the circle jump on tap. */
.icon-btn:not(.plain):active { transform: none; }
.icon-btn:not(.plain) svg { transition: transform var(--dur-fast) var(--ease); }
.icon-btn:not(.plain):active svg { transform: scale(0.86); }
/* The heart glyph's optical centre sits ~0.5px low in its 24-unit box. */
/* The glyph is centred by absolute position + negative margins (not by the grid), so neither the
   filled/outline swap nor a scale animation can move it off the middle of the circle. The heart's
   shape sits ~0.5px low in its box, hence -10.5px. */
.icon-btn:not(.plain) svg { position: absolute; left: 50%; top: 50%; margin: -10px 0 0 -10px; transform-origin: 50% 50%; }
.icon-btn.fav svg { margin-top: -10.5px; }
.icon-btn.fav { right: 6px; }
.icon-btn.fav.on { color: var(--like); }
.icon-btn.hide { right: 50px; font-size: 14px; }
.icon-btn.plain { position: static; width: 44px; height: 44px; color: var(--text); font-size: 22px; }
.icon-btn.plain::before { content: none; }

.card-body { padding: 13px 14px 15px; display: grid; gap: 3px; }
.price-row { display: flex; align-items: baseline; gap: 10px; }
.price { font-size: 21px; font-weight: 800; letter-spacing: -0.01em; }
.price-row.big .price { font-size: 27px; }
.price-alt { color: var(--muted); font-size: 14px; }
.price-old { color: var(--muted); font-size: 14px; font-weight: 400; }
.params { font-weight: 600; }
.title { color: var(--text); overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.loc { color: var(--muted); font-size: 14px; }
.meta { margin-top: 0; display: flex; flex-wrap: wrap; gap: 8px 10px; align-items: center; color: var(--muted); font-size: 13px; }
.meta__date { flex: 1 0 100%; }

/* ---- pager (list mode) ---- */
.pager { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 14px 0 4px; }
.pager .btn { flex: 1; }
.pager .btn:disabled { opacity: 0.4; cursor: default; }
.pager-info { flex: none; color: var(--muted); font-size: 13.5px; }

/* ---- swipe deck ---- */
.deck {
  --deck-h: clamp(340px, calc(100dvh - 200px - env(safe-area-inset-bottom)), 640px);
  display: grid; gap: 10px; padding-top: 12px; align-content: start; min-height: calc(100dvh - 84px);
}
/* Fullscreen swipe mode (main.js syncSwipeFull): topbar hidden, more height for the card. */
.deck.full { --deck-h: clamp(340px, calc(100dvh - 120px - env(safe-area-inset-bottom)), 760px); }
.deck-stage { position: relative; min-height: var(--deck-h); }
/* Этап 6 motion-spec.md §1, §1.7: no blanket always-on transition/will-change any more - each
   gesture state is its own class, set/cleared by swipe.js (table in §1.7), so a resting card pays
   nothing for a transition it isn't using and will-change only applies while a drag is live. */
.swipe-card {
  position: absolute; inset: 0 0 auto 0; display: flex; flex-direction: column; overflow: hidden; height: var(--deck-h);
  border-radius: 20px; background: var(--card); border: 1px solid var(--line); box-shadow: 0 10px 28px rgba(0,0,0,.18);
  cursor: grab; outline: none; touch-action: none; user-select: none; -webkit-user-select: none;
}
.swipe-card:active { cursor: grabbing; }
.swipe-card.dragging { will-change: transform; }
.swipe-card.spring-back { transition: transform 320ms var(--spring); }
/* --fly-ms is set inline per swipe (§1.4: 180ms flick / 240ms drag-to-threshold). The opacity leg
   only starts at 60% of the trip (transition-delay) and covers the remaining 40% - the card should
   visibly leave before it fades, not fade in place. */
.swipe-card.flying-left, .swipe-card.flying-right {
  pointer-events: none;
  transition: transform var(--fly-ms, 240ms) cubic-bezier(.3, .1, .6, 1),
    opacity calc(var(--fly-ms, 240ms) * 0.4) cubic-bezier(.3, .1, .6, 1) calc(var(--fly-ms, 240ms) * 0.6);
}
.swipe-card.behind { pointer-events: none; } /* transform/opacity now set inline by applyBehind() (§1.6) */
.swipe-card.behind-settle { transition: transform 320ms var(--spring), opacity 320ms var(--spring); }
.swipe-card.behind-commit { transition: transform 200ms var(--ease-out), opacity 200ms var(--ease-out); }
.swipe-cover { position: relative; flex: 1 1 auto; min-height: 0; background: var(--surface); }
.swipe-cover img { width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; }
.swipe-body { flex: none; padding: 12px 16px 16px; display: grid; gap: 3px; }
.swipe-fx {
  position: absolute; inset: 0; z-index: 2; display: grid; place-items: center; opacity: 0; pointer-events: none;
  color: #fff; font-size: 110px; line-height: 1; transition: opacity .28s ease-out;
}
.swipe-fx.skip { background: rgba(10, 10, 8, .93); }
.swipe-fx.like { background: rgba(255, 93, 122, .88); }
.swipe-fx span { display: block; transition: transform .28s ease-out; filter: drop-shadow(0 4px 14px rgba(0,0,0,.4)); }
.swipe-fx svg { width: 96px; height: 96px; display: block; filter: drop-shadow(0 4px 14px rgba(0,0,0,.4)); }
/* §1.2 "Новое": a subtler whole-card wash under the icon badge above (opacity only, strength*0.22). */
.swipe-fx-tint { position: absolute; inset: 0; z-index: 1; opacity: 0; pointer-events: none; }
.swipe-fx-tint.skip { background: #0a0a08; } /* dark only: no red wash before the dark overlay */
.swipe-fx-tint.like { background: var(--like); }
.deck-actions { display: flex; justify-content: center; align-items: center; gap: 22px; }
.deck-btn {
  width: 60px; height: 60px; border-radius: 50%; border: 1px solid var(--line); background: var(--surface); font-size: 26px; line-height: 1;
  display: grid; place-items: center; box-shadow: 0 4px 14px rgba(0,0,0,.08);
}
.deck-btn svg { width: 26px; height: 26px; display: block; }
.deck-btn.like { color: var(--like); }
.deck-btn.nope { color: var(--danger); }
.deck-btn.undo { width: 46px; height: 46px; font-size: 20px; color: var(--muted); box-shadow: none; }
.deck-btn.undo svg { width: 20px; height: 20px; }
.deck-btn:disabled { opacity: 0.35; cursor: default; }
.deck-empty { display: grid; gap: 14px; justify-items: center; align-content: center; min-height: var(--deck-h); text-align: center; color: var(--muted); padding: 0 24px; }
.deck-empty__title { font-size: 17px; font-weight: 700; color: var(--text); }
/* min-height keeps this from collapsing to 0 while empty (deck-empty state, first load) - it must
   hold its real-content height even then, since swipe.js measures it to size the card above. */
.swipe-counter { min-height: 17px; text-align: center; font-size: 12px; color: var(--muted); margin: 10px 0 2px; font-variant-numeric: tabular-nums; }

.list-status { display: flex; justify-content: center; align-items: center; gap: 8px; padding: 18px 0; color: var(--muted); text-align: center; }
.list-status:empty { display: none; }
.sentinel { height: 1px; }
.hint { color: var(--muted); font-size: 13.5px; margin: 8px 0; }

.empty { text-align: center; padding: 40px 12px; color: var(--muted); }
.empty-ico { font-size: 42px; margin-bottom: 8px; opacity: .7; }
.empty-ico svg { width: 42px; height: 42px; display: block; margin: 0 auto; }

/* ---- tab bar ---- */
.tab-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 10; display: flex; justify-content: center; gap: 4px;
  padding: 6px 10px calc(6px + env(safe-area-inset-bottom));
  background: color-mix(in srgb, var(--surface) 94%, transparent); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  border-top: 1px solid var(--line);
}
.tab-bar__item {
  flex: 1; max-width: 180px; display: grid; justify-items: center; gap: 2px; padding: 6px 4px 8px; border: 0; border-radius: 12px;
  background: transparent; color: var(--muted); font-size: 10.5px; font-weight: 600; font-family: var(--font-ui);
}
.tab-bar__icon { width: 22px; height: 22px; display: inline-flex; position: relative; }
.tab-bar__icon svg { width: 100%; height: 100%; }
.tab-bar__item.active { color: var(--accent-ink); font-weight: 700; }
/* п.10b: amber dot on the "Поддержка" tab itself when something there is unread. */
.tab-bar__item.has-unread .tab-bar__icon::after {
  content: ""; position: absolute; top: -2px; right: -2px; width: 8px; height: 8px; border-radius: 50%;
  background: var(--amber); border: 2px solid var(--surface);
}

.toast {
  position: fixed; left: 50%; bottom: calc(84px + env(safe-area-inset-bottom)); transform: translateX(-50%); z-index: 40;
  max-width: calc(100% - 28px); padding: 10px 14px; border-radius: 12px; display: flex; gap: 12px; align-items: center;
  background: #221b12; color: #fff; box-shadow: 0 8px 24px rgba(0,0,0,.35); font-size: 14px;
}
.toast-action { min-height: 44px; display: inline-flex; align-items: center; border: 0; background: none; color: var(--amber); font-weight: 700; padding: 0; }

/* ---- detail ---- */
.detail { position: fixed; inset: 0; z-index: 30; background: var(--bg); overflow-y: auto; }
.detail-bar {
  position: sticky; top: 0; z-index: 3; display: flex; align-items: center; gap: 6px; padding: 8px 10px;
  background: color-mix(in srgb, var(--bg) 94%, transparent); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
}
.detail-bar-title { font-weight: 700; }
.detail-body { max-width: 560px; margin: 0 auto; padding-bottom: calc(24px + env(safe-area-inset-bottom)); }
.gallery { position: relative; }
.gallery-strip { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; }
.gallery-strip::-webkit-scrollbar { display: none; }
.gallery-strip img, .gallery-strip .cover-broken { flex: none; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; scroll-snap-align: center; background: var(--surface); }
.detail-main { padding: 14px; display: grid; gap: 6px; }
/* Этап 6 motion-spec.md §2.1 last paragraph: the rest of the card (price, title, pills...) fades
   in a beat after the photo, so the photo visibly "arrives" first. Toggled once per openDetail()
   call by adding .revealed to .detail-body - a later render() (the full ad data replacing the
   summary) leaves .revealed in place, so its freshly-built elements just render at opacity:1 with
   no re-animation (only the very first paint should do this). */
.detail-body:not(.revealed) .detail-main > * { opacity: 0; transform: translateY(6px); transition: none; }
.detail-body.revealed .detail-main > * { transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out); }
.detail-body.revealed .detail-main > *:nth-child(1) { transition-delay: 60ms; }
.detail-body.revealed .detail-main > *:nth-child(2) { transition-delay: 100ms; }
.detail-body.revealed .detail-main > *:nth-child(3) { transition-delay: 140ms; }
.detail-body.revealed .detail-main > *:nth-child(4) { transition-delay: 180ms; }
.detail-body.revealed .detail-main > *:nth-child(5) { transition-delay: 220ms; }
.detail-body.revealed .detail-main > *:nth-child(6) { transition-delay: 260ms; }
.detail-body.revealed .detail-main > *:nth-child(7) { transition-delay: 300ms; }
.detail-body.revealed .detail-main > *:nth-child(n+8) { transition-delay: 340ms; }
/* Pills and buttons inside those blocks pop in one by one (detail.js adds .pop to them on the very
   first render only). An animation with a backwards fill, not a transition: no delay is left
   behind on the buttons, so their press feedback stays instant. */
.detail-body:not(.revealed) .pop { opacity: 0; }
.detail-body.revealed .pop { animation: pop-in var(--dur-base) var(--ease-out) backwards; animation-delay: calc(220ms + var(--pop-i, 0) * 50ms); }
@keyframes pop-in { from { opacity: 0; transform: translateY(6px) scale(.96); } }
/* "Загружаем…" while phone / photos / description are still arriving: a thin sliding bar on the
   header's lower edge plus a capsule over the top of the photo; both fade out when everything is in. */
.detail-loader { position: absolute; left: 0; right: 0; top: 100%; pointer-events: none; transition: opacity var(--dur-base) var(--ease); }
.detail-loader.done { opacity: 0; }
.detail-loader__bar { position: absolute; left: 0; right: 0; top: -2px; height: 2px; overflow: hidden; background: var(--amber-tint); }
.detail-loader__bar::after { content: ""; position: absolute; inset: 0 auto 0 0; width: 40%; background: var(--amber); animation: loader-slide 1.1s var(--ease) infinite; }
@keyframes loader-slide { from { transform: translateX(-100%); } to { transform: translateX(250%); } }
.detail-loader__text {
  position: absolute; top: 12px; left: 50%; transform: translateX(-50%); white-space: nowrap; padding: 5px 12px; border-radius: 999px;
  background: rgba(0,0,0,.62); color: #fff; font-size: 12.5px; font-weight: 600; backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
}
/* Closing (§2.1 "Обратно", simplified to one symmetric exit regardless of whether a FLIP back to a
   source card was possible - see detail.js closeDetail()): fade + slide down, no shared element. */
.detail.closing { transition: opacity 220ms cubic-bezier(.4, 0, 1, 1); opacity: 0; }
.detail.closing .detail-body { transition: transform 220ms cubic-bezier(.4, 0, 1, 1); transform: translateY(16px); }
/* Only takes effect while a View Transition captured by detail.js is actually in flight (it sets
   view-transition-name:'ad-photo' on exactly one element at a time, then clears it) - a no-op
   style otherwise, safe to keep in the stylesheet unconditionally. */
::view-transition-group(ad-photo) { animation-duration: 300ms; animation-timing-function: var(--ease-out); }
.detail-title { margin: 6px 0 0; font-size: 18px; line-height: 1.3; }
.actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin: 10px 0 2px; }
.actions .btn { min-width: 0; white-space: normal; text-align: center; padding-top: 8px; padding-bottom: 8px; overflow-wrap: anywhere; }
.actions .btn:first-child:nth-last-child(odd) { grid-column: 1 / -1; }
.fav-btn.on { color: var(--like); }
.detail-section { margin-top: 12px; }
.detail-section h3 { margin: 0 0 8px; font-size: 16px; }
.desc { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.param-table { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr); gap: 6px 12px; margin: 0; }
.param-table dt { color: var(--muted); }
.param-table dd { margin: 0; overflow-wrap: anywhere; }

/* ---- filter sheet ---- */
.sheet { position: fixed; inset: 0; z-index: 50; display: flex; align-items: flex-end; justify-content: center; }
/* Runs automatically the moment the sheet is appended - no JS class toggling needed. */
@keyframes sheet-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes sheet-slide-up { from { transform: translateY(100%); } to { transform: translateY(0); } }
.sheet-backdrop {
  position: absolute; inset: 0; border: 0; background: rgba(10,10,8,.55);
  animation: sheet-fade-in var(--dur-base) var(--ease) backwards;
}
.sheet-grip { width: 38px; height: 4px; border-radius: 2px; background: var(--line); margin: 8px auto 0; flex: none; }
.sheet-drag-zone { touch-action: none; }
.sheet-panel {
  position: relative; width: 100%; max-width: 560px; max-height: 88dvh; display: flex; flex-direction: column;
  border-radius: 20px 20px 0 0; background: var(--bg); border: 1px solid var(--line); overflow-x: hidden;
  animation: sheet-slide-up 340ms var(--ease-out) backwards; /* smooth, no overshoot; no fill, so a drag can move it */
  /* Every sheet (with or without a .sheet-foot) keeps clear of the phone's rounded bottom corners. */
  padding-bottom: env(safe-area-inset-bottom);
}
/* Closing (Этап 6, motion-spec.md §3 - util.js animateRemove(sheet, onDone, "closing") adds this,
   waits for it, then actually removes the node): no spring overshoot on the way out - a bouncy
   entrance reads as "springing into view", the same bounce on exit would look like it's bouncing
   back open instead of leaving. Backdrop and panel close together, same easing/duration. */
.sheet.closing .sheet-backdrop { animation: none; transition: opacity 240ms cubic-bezier(.4, 0, 1, 1); opacity: 0; }
.sheet.closing .sheet-panel { animation: none; transition: transform 240ms cubic-bezier(.4, 0, 1, 1); transform: translateY(100%); }
.sheet-head { display: flex; justify-content: space-between; align-items: center; padding: 14px 16px 6px; }
.sheet-head h2 { margin: 0; font-size: 19px; }
/* grid-auto-rows:max-content - without it, once all rows together are taller than the sheet's
   own (flex-resolved) height, Grid's default "auto" row sizing shrinks rows using each item's
   automatic minimum size (0 for anything with overflow:hidden, like .group - see its comment)
   instead of their real content height, so later rows start where an earlier, now-too-short
   track said they should - items overlap instead of stacking. max-content pins every row to its
   item's own full height regardless of the container being too short to show it all at once;
   overflow-y above is what makes that remaining content reachable by scrolling, not clipped. */
/* -webkit-overflow-scrolling/overscroll-behavior (Этап 19e): with the keyboard open, a finger drag on
   this list must scroll the list itself - inertial (iOS without it can feel like it isn't scrolling
   at all) and contained, so it never hands the drag off to the document behind the fixed sheet (the
   "page scrolls under the keyboard" bug kb-open exists to prevent, Этап 7c/19d). */
.sheet-scroll {
  overflow-y: auto; overflow-x: hidden; padding: 6px 16px 12px; display: grid; grid-auto-rows: max-content; gap: 14px;
  -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
}
/* display:flex + gap so a second button (e.g. support.js's "Закрыть обращение" next to "Отправить",
   Этап 5b, п.8) doesn't sit flush against the first - plain block buttons had zero space between
   them. A single button (the common case) is unaffected either way. */
.sheet-foot { padding: 10px 16px 12px; border-top: 1px solid var(--line); display: flex; flex-direction: column; gap: 8px; }
/* Этап 7c, п.5 (support.js openThread/syncThreadHeight): while the on-screen keyboard is open,
   the thread sheet grows to fill whatever visualViewport reports as available (already excludes
   the keyboard) instead of staying its normal content-sized height - a couple of short messages
   otherwise left almost the whole screen as empty backdrop above a cramped sheet. --thread-h is
   set/cleared by JS; the .sheet-scroll flex-grow is what actually lets the message list claim
   that extra height (the head and foot stay their own natural size either way). */
.sheet-panel.kb-open { max-height: none; height: var(--thread-h, 88dvh); }
.sheet-panel.kb-open .sheet-scroll { flex: 1 1 auto; min-height: 0; }
.field { display: grid; gap: 8px; }
.field > label { font-weight: 600; }
.unit { color: var(--muted); font-weight: 400; }
.range { display: flex; align-items: center; gap: 8px; }
.range input {
  width: 100%; min-width: 0; height: 44px; padding: 0 12px; border-radius: 12px; border: 1px solid var(--line);
  background: var(--surface); outline: none;
}
.dash { color: var(--muted); }

/* ---- saved searches / notification settings ---- */
.muted { color: var(--muted); font-size: 14px; }
.saved-list { display: grid; gap: 12px; }
.requests-list { gap: 16px; }
.saved { padding: 14px; border-radius: var(--radius); background: var(--card); border: 1px solid var(--line); display: grid; gap: 10px; }
.saved-name { font-size: 16px; font-weight: 600; }
.saved-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.btn.small { min-height: 44px; padding: 0 12px; font-size: 14px; }
.btn.danger { color: var(--danger); }
.switch { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; margin-right: auto; cursor: pointer; }
.switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.switch-ui { width: 42px; height: 24px; border-radius: 12px; background: color-mix(in srgb, var(--text) 22%, transparent); position: relative; transition: background .15s; }
.switch-ui::after { content: ""; position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: transform .15s; }
.switch input:checked + .switch-ui { background: var(--accent); }
.switch input:checked + .switch-ui::after { transform: translateX(18px); }
.switch input:focus-visible + .switch-ui { outline: 2px solid var(--accent); outline-offset: 2px; }
.switch-text { font-size: 14px; }
.text-input { width: 100%; height: 44px; padding: 0 12px; border-radius: 12px; border: 1px solid var(--line); background: var(--surface); outline: none; }
.text-input:focus { border-color: var(--accent); }
/* Textareas (support.js) keep the fixed height set inline - no native resize handle, no
   horizontal scrollbar from a long unbroken word (a pasted link, say). */
textarea.text-input { resize: none; overflow-x: hidden; white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
.check { display: flex; align-items: center; gap: 10px; min-height: 44px; }
.check input { width: 20px; height: 20px; accent-color: var(--accent); }
.error { color: var(--danger); margin: 4px 0 0; font-size: 14px; }

/* ---- advanced filters ---- */
.section-title { margin: 20px 0 10px; font-size: 17px; font-weight: 700; letter-spacing: -0.005em; color: var(--text); }
.advanced { display: grid; gap: 8px; }
/* min-height:min-content - without it, an open/expanded .group (body visible) gets squeezed
   almost to 0 by .sheet-scroll's CSS Grid: once all the sheet's rows together are taller than
   the sheet's own (flex-resolved, so "definite") height, Grid shrinks "auto" row tracks toward
   each item's automatic minimum size, which is 0 for anything with overflow != visible (every
   .group has overflow:hidden, for its rounded corners) - so an always-open .group.flat (Этап 5b:
   static filters, see filters.js staticGroup()) collapsed to a near-empty strip, its body clipped
   away, while a normally-CLOSED dynamic group never showed the bug (little content to shrink). */
.group { border: 1px solid var(--line); border-radius: 14px; background: var(--card); overflow: hidden; min-height: min-content; }
.group-head { width: 100%; display: flex; align-items: center; gap: 8px; padding: 12px 14px; border: 0; background: transparent; text-align: left; font-weight: 600; }
.group-title { flex: 1; }
.group-head .arrow { color: var(--muted); font-size: 20px; line-height: 1; transition: transform .15s; }
.group.open .group-head .arrow { transform: rotate(90deg); }
.group-body { display: none; padding: 0 14px 14px; gap: 10px; }
.group.open .group-body { display: grid; }
.group-switch { justify-content: space-between; padding: 12px 14px; border: 1px solid var(--line); border-radius: 14px; background: var(--card); margin-right: 0; }
.group-switch .switch-text { order: 0; flex: 1; font-weight: 600; font-size: 15px; }
.group-switch .switch-ui { order: 1; }
.text-input.small { height: 44px; }
.muted.small { font-size: 12px; font-weight: 400; }
.chip.hidden { display: none; }

/* ---- FAQ answer text / privacy link (support.js) ---- */
.faq-answer { color: var(--text); font-size: 14px; line-height: 1.5; margin: 4px 0 0; }
.privacy-link { color: var(--muted); font-weight: 400; text-decoration: underline; }

/* ---- support request thread (support.js openThread) ---- */
.request-row { width: 100%; text-align: left; border: 1px solid var(--line); }
.request-row__title { display: flex; align-items: center; gap: 7px; }
.support-thread { display: grid; gap: 10px; margin-bottom: 4px; }
.support-msg { max-width: 86%; padding: 8px 12px; border-radius: 14px; background: var(--surface); border: 1px solid var(--line); }
.support-msg--mine { margin-left: auto; background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
/* The scripted support bot (supportbot.js): a chat in a sheet, answers as buttons in the foot. */
.sbot-list { display: grid; gap: 8px; align-content: start; }
.sbot-msg { max-width: 88%; padding: 9px 13px; border-radius: 16px; font-size: 14px; line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere; }
.sbot-msg--bot { background: var(--surface); border: 1px solid var(--line); border-bottom-left-radius: 5px; justify-self: start; }
.sbot-msg--user { background: var(--accent); color: var(--accent-text); border-bottom-right-radius: 5px; justify-self: end; }
.sbot-foot { max-height: 55vh; overflow-y: auto; }
.sbot-foot > * { flex: none; }
.sheet-panel.kb-open .sbot-foot { max-height: none; overflow: visible; }
.sbot-human { align-self: center; }
.support-status { margin: 0; padding: 0 16px 6px; }
.support-msg__meta { font-size: 11px; opacity: .7; margin-bottom: 2px; }
.support-msg__text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; font-size: 14px; }
/* п.1 (2026-10-10): the calm "Обращение закрыто" plaque replacing the reply field/buttons once a
   thread is status="done" (support.js syncClosed) - sits where .sheet-foot's buttons would be. */
.support-closed-notice { text-align: center; margin: 4px 0; }
/* п.9's "Отправить"/"Закрыть обращение" stack (support.js footButtons) - .sheet-foot is already a
   column, this nested wrapper just needs the same gap so it reads as one group. */
.sheet-foot-actions { display: flex; flex-direction: column; gap: 8px; }

/* ---- п.7/8/10b (2026-10-10): "Поддержка" tab visual sections, the "Мои обращения"/admin count
   cards, and the amber unread dot shared by the card, a row inside it, and the tab-bar item. ---- */
.support-section { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 14px; display: grid; gap: 10px; margin-bottom: 14px; }
.support-section > .section-title { margin: 0; }
.unread-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--amber); flex: none; }
.request-row--unread .saved-name { font-weight: 800; }
.support-count-card {
  width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 14px; border-radius: var(--radius); background: var(--surface-2); border: 1px solid var(--line);
  color: var(--text); font-family: var(--font-ui); font-weight: 700; font-size: 15px; text-align: left;
}
.support-count-card__title { display: flex; align-items: center; gap: 8px; }
.support-count-card__right { display: flex; align-items: center; gap: 8px; color: var(--muted); }
.support-count-card .arrow { color: var(--muted); font-size: 18px; }

/* ---- fullscreen photo viewer ---- */
.photo-viewer {
  position: fixed; inset: 0; z-index: 70; background: #000; display: flex; flex-direction: column;
  overflow: hidden; touch-action: none;
}
.pv-bar {
  position: relative; z-index: 2; display: flex; align-items: center; justify-content: center;
  min-height: 44px; padding: calc(6px + env(safe-area-inset-top)) 54px 6px;
}
.pv-count { color: #fff; font-size: 14px; font-variant-numeric: tabular-nums; opacity: 0.85; }
.pv-close {
  position: absolute; right: 6px; top: calc(6px + env(safe-area-inset-top));
  width: 44px; height: 44px; border: 0; background: transparent; color: #fff; font-size: 20px; display: grid; place-items: center;
}
.pv-close svg { width: 22px; height: 22px; display: block; }
.pv-track { flex: 1; display: flex; width: 100%; height: 100%; will-change: transform; }
.pv-slide { flex: 0 0 100%; width: 100%; height: 100%; position: relative; display: grid; place-items: center; overflow: hidden; }
.pv-slide img {
  max-width: 100%; max-height: 100%; object-fit: contain; touch-action: none; user-select: none;
  -webkit-user-select: none; opacity: 0; transition: opacity 150ms ease-out;
}
.pv-slide.loaded img { opacity: 1; }
.pv-slide.broken img { display: none; }
/* Этап 20c: a stored photo URL can 404 on its own (any source - first seen on a Realt listing
   whose images were gone from its CDN), not just when the ad truly has no photo. */
.pv-broken { display: flex; flex-direction: column; align-items: center; gap: 10px; color: #fff; opacity: 0.6; }
.pv-broken svg { width: 48px; height: 48px; }
.pv-broken span { font-size: 14px; }

/* ---- notifications feed (feed.js) ---- */
.notif-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 14px 0 2px; }
.notif-header h1 { margin: 0; }
.notif-header .row { gap: 6px; }
/* Gear/"Очистить всё" read as buttons here (ghost pill / boxed icon), not bare text next to the title. */
.notif-header .icon-btn.plain {
  position: static; width: 40px; height: 40px; border-radius: 12px; color: var(--text);
  background: var(--surface); border: 1px solid var(--line);
}
.notif-header .icon-btn.plain svg { width: 18px; height: 18px; }
.notif-header .link-btn { min-height: 40px; padding: 0 12px; border-radius: 12px; background: var(--surface); border: 1px solid var(--line); font-weight: 600; }
.support-banner {
  display: flex; align-items: center; gap: 12px; width: 100%; min-height: 56px; padding: 0 16px; margin: 10px 0 16px;
  border-radius: var(--radius); background: var(--amber-tint); border: 0; cursor: pointer; text-align: left;
}
.support-banner svg:first-child { width: 22px; height: 22px; color: var(--amber-ink); flex: none; }
.support-banner__text { flex: 1; font-size: 13.5px; font-weight: 700; color: var(--amber-ink); }
.support-banner svg:last-child { width: 18px; height: 18px; color: var(--amber-ink); flex: none; }

.notif-section-title, .notif-date { font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); margin: 18px 0 10px; }
.notif-section-title:first-of-type, .notif-date:first-of-type, .cards > .notif-date:first-child { margin-top: 0; }
/* Swipe-to-reveal (coder wires the drag/pointer logic): .notif-swipe is the clipping row, .notif-item
   is the front layer that slides left on drag, .notif-actions sits behind it, right-aligned. */
.notif-swipe { position: relative; overflow: hidden; border-bottom: 1px solid var(--line); touch-action: pan-y; }
.notif-swipe:last-child { border-bottom: none; }
.notif-item { position: relative; z-index: 1; display: flex; gap: 12px; padding: 12px 0; background: var(--bg); transition: transform .2s ease; touch-action: pan-y; }
/* п.4 (2026-10-10): back to the original edge-to-edge rectangles, full row height, no gap/padding -
   width here must match ACTIONS_WIDTH in feed.js (two 64px .notif-actions__btn = 128px). Colors are
   softened from the original bright pink/mustard, built from the project's own palette (--accent,
   --danger mixed with --surface for a calmer clay rather than a saturated red). */
.notif-actions { position: absolute; inset: 0 0 0 auto; z-index: 0; display: flex; }
.notif-actions__btn { width: 64px; border: 0; flex: none; display: flex; align-items: center; justify-content: center; color: #fff; }
.notif-actions__btn svg { width: 22px; height: 22px; }
.notif-actions__btn--fav { background: var(--accent); color: var(--accent-text); }
.notif-actions__btn--delete { background: color-mix(in srgb, var(--danger) 72%, var(--surface)); }

/* п.3 (2026-10-10): same swipe-to-reveal mechanism as .notif-swipe above, for "Админ: все
   обращения" — one 🗑 action, closed (status="done") requests only (support.js adminRequestRow).
   The front layer is the existing .saved.request-row button itself (keeps its own card look), so
   this wrapper only needs to clip + round to match it and sit behind with the same radius. */
.request-swipe { position: relative; overflow: hidden; border-radius: var(--radius); touch-action: pan-y; }
.request-row { position: relative; z-index: 1; transition: transform .2s ease; touch-action: pan-y; }
.request-actions { position: absolute; inset: 0 0 0 auto; z-index: 0; display: flex; border-radius: var(--radius); overflow: hidden; }
.request-actions__btn { width: 64px; border: 0; flex: none; display: flex; align-items: center; justify-content: center; color: #fff; background: color-mix(in srgb, var(--danger) 72%, var(--surface)); }
.request-actions__btn svg { width: 22px; height: 22px; }
.notif-item__thumb { width: 56px; height: 56px; border-radius: 12px; object-fit: cover; flex: none; background: var(--surface); }
.notif-item__body { flex: 1; min-width: 0; }
.notif-item__row { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
/* Этап 8i task п.2 (lead's phone report: "всё выглядит мелко/бледно") - title/meta/time each +1.5px;
   meta/time also picked up font-weight 600 (they had none, inheriting body's regular weight -
   "бледно" was weight as much as size, not just var(--muted)'s color, which this task didn't ask
   to touch). */
.notif-item__title { font-size: 15px; font-weight: 700; margin: 0 0 4px; }
.notif-item__time { font-size: 12.5px; font-weight: 600; color: var(--muted); flex: none; white-space: nowrap; }
.notif-item__meta { font-size: 13.5px; font-weight: 600; color: var(--muted); margin-bottom: 6px; overflow-wrap: break-word; }
.notif-item__row2 { display: flex; align-items: center; gap: 8px; margin-top: 2px; }
.notif-item__price { font-size: 13px; font-weight: 700; color: var(--text); }

.pill {
  display: inline-flex; align-items: center; gap: 6px; min-height: 30px; padding: 5px 10px; border-radius: 10px;
  font-size: 11.5px; font-weight: 700;
}
.pill svg { width: 14px; height: 14px; flex: none; }
.pill--accent { background: var(--amber); color: var(--on-amber); }
.pill--accent-tint { background: var(--amber-tint); color: var(--amber-ink); font-weight: 600; }
.pill--surface2 { background: color-mix(in srgb, var(--text) 6%, var(--surface)); color: var(--text); font-weight: 600; }

/* п.5 (2026-10-10): "Показать N новых" floating banner above the list (main.js newAdsBanner) -
   appears while new ads matching the current filters show up without disturbing the open list. */
.new-ads-banner {
  position: fixed; top: calc(env(safe-area-inset-top, 0px) + 6px); left: 50%; transform: translateX(-50%);
  z-index: 20; background: var(--amber); color: var(--on-amber); font-weight: 800; font-size: 13.5px;
  padding: 10px 18px; border: 0; border-radius: 999px; box-shadow: 0 8px 20px rgba(0,0,0,.2);
}
.new-ads-banner.hidden { display: none; }
/* Этап 13: soft endless "tap me" pulse while the pill is visible (a ring wave plus a slight scale). */
.new-ads-banner:not(.hidden) { animation: new-ads-pulse 2s ease-in-out infinite; }
@keyframes new-ads-pulse {
  0%   { transform: translateX(-50%) scale(1);    box-shadow: 0 8px 20px rgba(0,0,0,.2), 0 0 0 0 color-mix(in srgb, var(--amber) 55%, transparent); }
  50%  { transform: translateX(-50%) scale(1.05); box-shadow: 0 8px 20px rgba(0,0,0,.2), 0 0 0 10px color-mix(in srgb, var(--amber) 0%, transparent); }
  100% { transform: translateX(-50%) scale(1);    box-shadow: 0 8px 20px rgba(0,0,0,.2), 0 0 0 0 color-mix(in srgb, var(--amber) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) { .new-ads-banner:not(.hidden) { animation: none; } }

/* ---- detail: meta pills / price history / map ---- */
.meta-row { display: flex; flex-wrap: wrap; gap: 8px; margin: 6px 0 0; }
/* "История цены" (detail.js changedPriceChart): header with the change, smooth curve, a dot per change. */
.ph-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.ph-price { font-size: 22px; font-weight: 700; }
.ph-badge { font-size: 12.5px; font-weight: 700; padding: 3px 8px; border-radius: 999px; }
.ph-badge.down { color: var(--good); background: color-mix(in srgb, var(--good) 14%, transparent); }
.ph-badge.up { color: var(--danger); background: color-mix(in srgb, var(--danger) 14%, transparent); }
.ph-caption { font-size: 12px; color: var(--muted); margin-top: 3px; }
.ph-flat { display: flex; align-items: center; gap: 10px; padding: 6px 2px; }
.ph-flat svg { width: 20px; height: 20px; color: var(--muted); flex: none; }
.ph-flat-main { font-size: 14.5px; font-weight: 700; }
.ph-flat-sub { font-size: 12px; color: var(--muted); margin-top: 1px; }
.ph-chart-wrap { position: relative; margin-top: 34px; touch-action: pan-y; }
.ph-chart-svg { display: block; width: 100%; height: 128px; overflow: hidden; }
.ph-line { fill: none; stroke: var(--amber); stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
.ph-grad-top { stop-color: var(--amber); stop-opacity: .28; }
.ph-grad-bottom { stop-color: var(--amber); stop-opacity: 0; }
.ph-dot { fill: var(--amber); }
.ph-dot.minmax { fill: var(--card); stroke: var(--amber); stroke-width: 2; }
.ph-dot.cur { fill: var(--amber); }
.ph-cursor { stroke: var(--muted); stroke-width: 1; stroke-dasharray: 3 3; }
.ph-cursor-dot { fill: var(--amber); stroke: var(--card); stroke-width: 1.5; }
.ph-tip {
  position: absolute; top: -6px; left: 0; transform: translate(-50%, -100%); padding: 6px 9px; border-radius: 10px; z-index: 1;
  background: var(--text); color: var(--bg); font-size: 11.5px; font-weight: 700; white-space: nowrap; pointer-events: none;
  display: flex; flex-direction: column; gap: 1px; box-shadow: 0 4px 12px rgba(0,0,0,.2);
}
:root[data-theme="dark"] .ph-tip { background: var(--amber); color: #2B1B06; }
.ph-tip span { font-weight: 400; opacity: .75; font-size: 10px; }
.ph-tip.hidden { display: none; }
.ph-minmax { font-size: 11.5px; color: var(--muted); margin-top: 8px; }

/* ---- floating "search settings" button (fullscreen swipe mode, main.js) ---- */
.fab-settings {
  position: fixed; right: 16px; bottom: calc(96px + env(safe-area-inset-bottom)); z-index: 15;
  width: 52px; height: 52px; border-radius: 50%; border: 0; background: var(--accent); color: var(--accent-text);
  display: grid; place-items: center; box-shadow: 0 6px 18px rgba(0,0,0,.28);
}
.fab-settings svg { width: 24px; height: 24px; display: block; }

/* ---- empty state (favorites etc.) ---- */
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 90px 30px 40px; gap: 14px; }
.empty-state svg { width: 48px; height: 48px; color: var(--muted); }
.empty-state__title { font-size: 16px; font-weight: 700; color: var(--text); }
.empty-state__text { font-size: 13px; color: var(--muted); line-height: 1.5; max-width: 260px; }

/* ---- Этап 5: unified search card, location field, filter-group icons, notify confirm/subscriptions ---- */

.search-card {
  background: var(--surface); border-radius: var(--radius); padding: 12px 10px 10px;
  border: 1px solid var(--line); box-shadow: 0 10px 30px rgba(20,30,20,.06); display: grid; gap: 10px;
  min-width: 0;
}
.search-card > * { min-width: 0; }
.search-card__top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.search-card__title { font-size: 14.5px; font-weight: 800; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.search-card__count { flex: none; }
.search-card__count { font-size: 11px; font-weight: 700; color: var(--on-amber); background: var(--amber); padding: 3px 9px; border-radius: 999px; }
.search-card__count.hidden { display: none; }

/* The old free-text search-input, now also the entry point for the location picker (location.js) -
   see main.js updateLocField(). Looks like .picker-field/.search-input, just with a leading icon. */
.loc-field {
  width: 100%; display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 0 12px;
  border-radius: 12px; background: var(--surface-2); border: 0; color: var(--text); font-family: var(--font-ui);
  font-size: 13.5px; font-weight: 600; text-align: left;
}
.loc-field svg { width: 18px; height: 18px; color: var(--muted); flex: none; }
.loc-field__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.loc-field__label.muted { color: var(--muted); font-weight: 400; }
.loc-chip-row { display: flex; }
.loc-chip-row.hidden { display: none; }
.loc-chip {
  display: inline-flex; align-items: center; gap: 8px; min-height: 34px; padding: 0 6px 0 12px;
  border-radius: 999px; background: var(--accent); color: var(--accent-text); font-size: 12.5px; font-weight: 700;
}
.loc-chip svg:first-child { width: 14px; height: 14px; }
.loc-chip button { width: 24px; height: 24px; border-radius: 50%; background: rgba(255,255,255,.18); border: 0; color: inherit; display: flex; align-items: center; justify-content: center; }
.loc-chip button svg { width: 12px; height: 12px; }

/* Deal/currency/mode merged into one visual bar (three .seg pieces share a background instead of
   each drawing its own) - the segs themselves (dealSeg/currencySeg/modeSeg in main.js) are unchanged. */
.unified-bar { display: flex; align-items: stretch; gap: 3px; background: var(--surface-2); border-radius: 14px; padding: 2px; min-width: 0; }
/* Деал/валюта stretch to fill the bar (matches the plain .row these segs used to live in, which
   gave them this via ".row > .seg:first-child" - a rule that only applies inside .row, not here);
   .seg.icon-only keeps its own fixed width (base .seg.icon-only{flex:none}) so the row+mode icons
   don't stretch too - without this, the bar's own full width went unused as blank space to the
   right of the icons (Этап 5b, п.2). */
.unified-bar .seg { background: transparent; border: 0; padding: 0; min-width: 0; }
.unified-bar .seg:not(.icon-only) { flex: 1; }
/* Deal words ("Покупка") are longer than USD/BYN - give that seg the bigger share so it never clips. */
.unified-bar .seg:first-child { flex: 1.4; }
.unified-bar .seg:not(:last-child) { border-right: 1px solid var(--line); padding-right: 3px; }
.unified-bar .seg.small button { padding: 6px 4px; font-size: 12.5px; }
.unified-bar .seg.icon-only button { width: 30px; }
/* Addendum п.1: every text in the search card read too bold except the title - one weight step
   down each (base .seg button is 600, .chip is 600/700, .btn is 700), scoped here so the same
   classes elsewhere (filters sheet, tab bar, etc.) keep their own weight. */
.unified-bar .seg button { font-weight: 500; }

/* Этап 5b, п.3: period/source rows laid out as the Figma mock has them - one row, equal-width
   chips, no wrap (the generic .chips/.chip elsewhere - filters sheet, etc. - keep their own
   auto-width/wrap look, this only scopes the search-card's two rows). Source chips keep their
   .chip.src-chip colors/fill as-is (the user asked to keep them "as bright as now") - only the
   sizing/layout changes here. */
.search-card .period-row, .search-card .src-row { flex-wrap: nowrap; }
/* iPhone (~390px) fix: without white-space:nowrap a squeezed chip (flex:1, min-width:0) wraps its
   own text onto two lines instead of just staying narrow ("Всё время" was the one that showed it
   first) - ellipsis is the fallback for the rare case a label still doesn't fit. Same for src-row's
   site chips just below. */
.search-card .period-row .chip {
  flex: 1; min-width: 0; min-height: 34px; padding: 0 2px; font-size: 11.5px; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Этап 8g task п.4 (lead's phone report: site chip labels read "мелкие и тонкие") - was
   font-weight 500 (addendum п.1 above only meant to tone down the *segmented controls*, this rule
   got swept into the same blanket "everything reads too bold" pass by scope, not by intent) and no
   explicit size (inherited the base .chip's 13.5px). Bumped weight and size both; the ellipsis
   fallback above already covers the rare label that doesn't fit at the bigger size.
   Этап 8i task п.2: still read small for a button this size even after 8g's bump - +1.5px on top
   (font-weight already at 700, the family's own boldest cut - size was the only lever left). */
.search-card .src-row .chip.src-chip {
  flex: 1; min-width: 0; justify-content: center; padding: 0 6px; font-weight: 700; font-size: 15px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Extra headroom on genuinely narrow phones (iPhone SE/mini and similar ~375-390px) - a touch
   smaller font/padding so 5 period chips ("Всё время" included) and 3 source chips comfortably
   clear the ellipsis case above instead of routinely hitting it. Untouched above 400px. */
@media (max-width: 400px) {
  .search-card .period-row .chip { font-size: 10.5px; padding: 0 1px; }
  .search-card .src-row .chip.src-chip { font-size: 13.5px; font-weight: 700; padding: 0 4px; gap: 5px; }
}

/* Addendum (Этап 6, phone feedback п.1): used to be bellBtn+filterBtn pinned right behind an empty
   .count-line spacer, leaving blank space on the left. Dropped the spacer and stretched filterBtn
   so the row fills edge to edge. */
.bottom-row2 { display: flex; align-items: center; gap: 8px; padding-top: 8px; border-top: 1px solid var(--line); }
.bottom-row2 .filter-btn { flex: 1; font-weight: 600; }
.btn.primary.filter-btn .count-dot { position: absolute; top: -6px; right: -6px; }

/* ---- location autocomplete sheet (location.js) ---- */
.auto-drop { display: flex; flex-direction: column; }
.auto-row {
  display: flex; align-items: center; gap: 12px; padding: 11px 2px; border: 0; border-bottom: 1px solid var(--line);
  background: transparent; text-align: left; color: var(--text); font-family: var(--font-ui); width: 100%;
}
.auto-row:last-child { border-bottom: none; }
/* Suggestions arrive a bit slower than other lists. */
.auto-drop .stagger-in { animation-duration: 340ms; animation-delay: calc(90ms + var(--stagger-i, 0) * 45ms); }
.auto-row svg { width: 18px; height: 18px; color: var(--muted); flex: none; }
.auto-row__main { font-size: 14px; font-weight: 700; }
.auto-row__sub { font-size: 12px; color: var(--muted); }

/* ---- filter sheet: icon chip reused by every group header (dynamic and static alike) ---- */
.group-head__icon, .picker-field__icon {
  width: 30px; height: 30px; border-radius: 9px; background: var(--amber-tint); color: var(--amber-ink);
  display: flex; align-items: center; justify-content: center; flex: none;
}
.group-head__icon svg, .picker-field__icon svg { width: 16px; height: 16px; }
/* Static groups (rooms/price/area/floor/landlord in filters.js) look like the dynamic .group
   accordions but never collapse - no arrow, no onclick, body always shown. */
.group.flat .group-head { cursor: default; }
.group.flat .group-body { display: grid; }

/* ---- notify confirmation summary (searches.js openSaveSheet) ---- */
.confirm-summary { display: grid; }
.summary-line { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; padding: 6px 0; border-bottom: 1px solid var(--line); }
.summary-line:last-child { border-bottom: none; }
.summary-line svg { width: 15px; height: 15px; color: var(--accent-ink); flex: none; }
.freq-row { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-radius: 12px; background: var(--amber-tint); color: var(--amber-ink); font-size: 12px; font-weight: 600; }
.freq-row svg { width: 15px; height: 15px; flex: none; }

/* ---- subscriptions screen (searches.js createAlerts - "Мои подписки") ---- */
.saved.paused { opacity: .6; }
.saved-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.saved-meta { font-size: 11.5px; color: var(--muted); margin-top: 2px; }
.saved-toggle { margin-right: 0; flex: none; }
.saved-actions { display: flex; gap: 8px; padding-top: 10px; border-top: 1px solid var(--line); }
.sub-action {
  flex: 1; min-height: 36px; border-radius: 10px; background: var(--surface-2); border: 0;
  display: flex; align-items: center; justify-content: center; gap: 6px; font-size: 11.5px; font-weight: 700; color: var(--text);
}
.sub-action svg { width: 14px; height: 14px; }
.sub-action.danger { color: var(--danger); }

/* ---- Этап 8: map view (map.js, third search mode alongside ≡/⇄) ---- */
.map-view {
  --map-h: clamp(340px, calc(100dvh - 240px - env(safe-area-inset-bottom)), 720px);
  position: relative; height: var(--map-h); border-radius: var(--radius); overflow: hidden;
  border: 1px solid var(--line); margin-top: 4px;
}
/* Fullscreen map mode (main.js syncFullMode): same idea as .deck.full, --map-h is instead set
   inline by map.js's own syncHeight() (measured against the tab bar's real on-screen top, not a
   guessed px - see its comment), so no CSS override is needed here beyond dropping the radius/
   border/margin that only make sense for the small in-card preview. */
.map-view.full { border-radius: 0; border: 0; margin-top: 0; }
/* .map-view prefix: maplibre-gl.css loads after styles.css and its .maplibregl-map{position:relative}
   would otherwise win and collapse the canvas to 0 height. */
.map-view .map-canvas { position: absolute; inset: 0; }
/* MapLibre's own canvas - keep it from fighting the app's own touch-action/user-select resets. */
.map-canvas .maplibregl-canvas { touch-action: none; }
/* Top-right, not bottom-right: bottom is already claimed by the ⚙ fullscreen-mode FAB
   (.fab-settings, fixed near the tab bar) and by .map-minicard when a point is tapped - top stays
   clear in both the small in-card preview and fullscreen. .map-view .list-status (below) reserves
   the matching 54px on its own right edge so the two never collide either. */
.map-attribution {
  position: absolute; right: 10px; top: 10px; z-index: 5; width: 30px; height: 30px; border-radius: 50%;
  border: 0; background: color-mix(in srgb, var(--surface) 85%, transparent); color: var(--muted);
  display: grid; place-items: center; box-shadow: 0 2px 8px rgba(0,0,0,.18);
}
.map-attribution svg { width: 17px; height: 17px; }
/* Этап 12b: "Следить за этой областью" - top centre. A toggle: hollow amber ring = off, filled lit
   amber = on (the search holds an area); turning on plays a ring pulse + a short flash of the pill.
   prefers-reduced-motion: the blanket rule near the top of this file cuts the animations/transitions,
   the filled/hollow state itself stays. */
.map-watch {
  position: absolute; left: 50%; top: 10px; transform: translateX(-50%); z-index: 5; max-width: calc(100% - 100px);
  min-height: 36px; padding: 0 14px 0 12px; border: 1px solid var(--line); border-radius: 18px;
  background: color-mix(in srgb, var(--surface) 92%, transparent); color: var(--text); font-family: var(--font-ui);
  font-size: 13.5px; font-weight: 600; white-space: nowrap; box-shadow: 0 2px 8px rgba(0,0,0,.18);
  display: inline-flex; align-items: center; gap: 8px; touch-action: manipulation;
  transition: border-color var(--dur-base) var(--ease), background var(--dur-base) var(--ease), transform var(--dur-fast) var(--ease);
}
.map-watch:active { transform: translateX(-50%) scale(0.96); }
.map-watch__dot {
  width: 12px; height: 12px; box-sizing: border-box; border-radius: 50%; flex: none;
  border: 2px solid var(--accent-ink); background: transparent;
  transition: background var(--dur-base) var(--ease), box-shadow var(--dur-slow) var(--ease);
}
.map-watch.is-on { border-color: var(--amber); background: color-mix(in srgb, var(--amber) 14%, var(--surface)); }
.map-watch.is-on .map-watch__dot { border-color: var(--amber); background: var(--amber); box-shadow: 0 0 0 3px var(--amber-tint), 0 0 8px 1px color-mix(in srgb, var(--amber) 70%, transparent); }
@keyframes watch-ring { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--amber) 75%, transparent); } 100% { box-shadow: 0 0 0 16px transparent; } }
@keyframes watch-flash { 0% { background: color-mix(in srgb, var(--amber) 55%, var(--surface)); } 100% { background: color-mix(in srgb, var(--amber) 14%, var(--surface)); } }
.map-watch.just-on { animation: watch-flash 700ms var(--ease-out); }
.map-watch.just-on .map-watch__dot { animation: watch-ring 800ms var(--ease-out); }
.region-row { display: flex; align-items: center; gap: 8px; }
.region-row .picker-field { flex: 1; min-width: 0; }
.picker-field.is-area { border-color: var(--amber); background: var(--amber-tint); }
.area-reset { flex: none; position: static; }  /* with .icon-btn.plain: in the flex row, not a photo overlay */
.area-reset.hidden { display: none; }
/* Этап 8k: the caption ⓘ opens (map.js attributionPop) - right under the button, never leaves the app. */
.map-attribution-pop {
  position: absolute; right: 10px; top: 46px; z-index: 6; max-width: calc(100% - 20px); padding: 8px 12px;
  border-radius: 10px; background: var(--card); border: 1px solid var(--line); color: var(--text);
  font-size: 13px; line-height: 1.35; box-shadow: 0 4px 14px rgba(0,0,0,.2);
}
.map-attribution-pop.hidden { display: none; }
.map-attribution-pop__link {
  display: inline; padding: 0; border: 0; background: none; font: inherit; font-weight: 700;
  color: var(--accent-ink); text-decoration: underline; cursor: pointer;
}
.map-view .list-status {
  position: absolute; left: 10px; right: 54px; top: 54px; z-index: 5; padding: 6px 10px; border-radius: 10px;
  background: color-mix(in srgb, var(--surface) 88%, transparent); font-size: 12.5px; justify-content: flex-start;
}
.map-view .list-status:empty { display: none; }
.map-minicard {
  position: absolute; left: 10px; right: 10px; bottom: 10px; z-index: 6; display: flex; gap: 10px;
  padding: 10px; border-radius: var(--radius); background: var(--card); border: 1px solid var(--line);
  box-shadow: 0 10px 28px rgba(0,0,0,.2); cursor: pointer;
}
.map-minicard.hidden { display: none; }
.map-minicard__photo { position: relative; flex: none; width: 64px; height: 64px; border-radius: 12px; overflow: hidden; background: var(--surface); }
.map-minicard__photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.map-minicard__body { flex: 1; min-width: 0; display: grid; align-content: center; gap: 4px; }
/* Этап 8h task п.2: price + the moved-off-the-photo source plate, same row. */
.map-minicard__price-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
.map-minicard__price { font-size: 16px; font-weight: 800; }
.map-minicard__meta { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--muted); flex-wrap: wrap; }
.map-minicard__close {
  position: absolute; top: -10px; right: -10px; width: 28px; height: 28px; border-radius: 50%;
  background: var(--surface); border: 1px solid var(--line); box-shadow: 0 2px 6px rgba(0,0,0,.18);
}
.map-minicard__close svg { width: 14px; height: 14px; }
/* Этап 8b task п.5: in fullscreen the mini-card's own × used to land under the ⚙ FAB
   (.fab-settings, fixed bottom:calc(96px+safe-area)) - bumped the card up past it instead of
   touching main.js's fullSettingsBtn (lead's "hide it OR move it", moving the overlay is the
   one-file fix). Non-fullscreen (embedded search-card preview) has no FAB, so left untouched. */
.map-view.full .map-minicard, .map-view.full .map-carousel {
  bottom: 12px; /* map-view itself ends above the tab bar (syncHeight), which already clears the safe area */
}
/* Этап 8d: the FAB is hidden while a card/carousel is open (map.js syncOverlayFlag). */
body.map-overlay-open .fab-settings { display: none; }
/* Этап 8g: cluster bubbles moved to GL circle/symbol layers (map.js buildStyle) so a place-name
   label can paint on top of them - no more .map-cluster-label DOM element/CSS (was Этап 8f). */
.map-point-price {
  min-height: 24px; padding: 4px 9px; border-radius: 999px; cursor: pointer; white-space: nowrap;
  font: 700 12px/1 var(--font-ui); color: var(--on-amber); background: var(--amber);
  border: 1.5px solid var(--card); box-shadow: 0 3px 8px rgba(0,0,0,.18);
}
/* Already opened (item.v): the old green look, dimmed (see .map-point-price.is-viewed below). */
.map-point-price.is-viewed { color: var(--text); background: var(--card); border-color: var(--accent); }

/* Этап 8k: a house (several ads in one building) as a plate: filled, so it reads as "more than one". */
.map-point-price--house { background: var(--amber); color: var(--on-amber); border-color: var(--card); }
.map-point-price--house.is-viewed { background: var(--accent); color: var(--accent-text); }

/* ---- Этап 8b: cluster carousel (map.js showCarousel, task п.4) ---- */
.map-carousel {
  position: absolute; left: 10px; right: 10px; bottom: 12px; z-index: 7; padding: 10px 0;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0,0,0,.2); touch-action: none;
}
.map-carousel.hidden { display: none; }
.map-carousel__head { display: flex; align-items: center; justify-content: space-between; padding: 0 12px 8px; font-size: 12.5px; font-weight: 700; color: var(--muted); }
.map-carousel__close svg { width: 16px; height: 16px; }
.map-carousel__track {
  display: flex; gap: 10px; overflow-x: auto; padding: 0 12px 2px; scroll-snap-type: x proximity;
  touch-action: pan-x;
}
.map-carousel__item {
  flex: none; width: 168px; scroll-snap-align: start; display: flex; flex-direction: column; gap: 8px;
  padding: 8px; border-radius: 14px; background: var(--surface); border: 1px solid var(--line); text-align: left;
}
.map-carousel__photo { position: relative; width: 100%; height: 96px; border-radius: 10px; overflow: hidden; background: var(--surface-2); }
.map-carousel__photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.map-carousel__body { display: grid; gap: 3px; min-width: 0; }
.map-carousel__price { font-size: 14.5px; font-weight: 800; }
.map-carousel__meta { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--muted); flex-wrap: wrap; }

/* "Просмотрено": the user already opened this listing's card. Children, not the card itself:
   the swipe deck animates the card's own opacity. */
.card.is-viewed .cover img, .card.is-viewed .cover-empty, .card.is-viewed .card-body,
.swipe-card.is-viewed .swipe-cover > img, .swipe-card.is-viewed .swipe-cover > .cover-empty, .swipe-card.is-viewed .swipe-body,
.notif-item.is-viewed .notif-item__thumb, .notif-item.is-viewed .notif-item__title,
.notif-item.is-viewed .notif-item__meta, .notif-item.is-viewed .pill { opacity: 0.55; }
/* «Просмотрено» plate on the photo (under the site plate), same dark blurred look as the "N фото"
   badge; not dimmed with the card. Hidden unless an ancestor is .is-viewed. */
.viewed-plate, .viewed-tag { display: none; align-items: center; gap: 4px; color: #fff; font-weight: 600; line-height: 1.3; }
.is-viewed .viewed-plate, .is-viewed .viewed-tag { display: inline-flex; }
.viewed-plate {
  position: absolute; top: 42px; left: 10px; z-index: 2; padding: 3px 8px; border-radius: 8px; font-size: 12px;
  background: rgba(0,0,0,.62); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  box-shadow: 0 0 0 1px rgba(255,255,255,.3); pointer-events: none;
}
.viewed-plate svg, .viewed-tag svg { width: 13px; height: 13px; flex: none; }
/* Inline variant (feed row, map mini-card): no photo to sit on, so a quiet muted label. */
.viewed-tag { font-size: 11.5px; color: var(--muted); }
.map-point-price.is-viewed { opacity: 0.6; }

/* ---- mode switcher (список / свайпы / карта): floating above the tab bar (main.js modeBar) ---- */
.mode-bar {
  position: fixed; left: 50%; bottom: calc(76px + env(safe-area-inset-bottom)); transform: translateX(-50%); z-index: 12;
}
.mode-bar .seg {
  padding: 4px; gap: 4px; border-radius: 16px; border: 1px solid var(--line); box-shadow: 0 8px 24px rgba(0,0,0,.22);
  background: color-mix(in srgb, var(--surface) 92%, transparent); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
}
.mode-bar .seg.icon-only button { width: 56px; min-height: 44px; border-radius: 12px; }
.mode-bar .seg__thumb { top: 4px; bottom: 4px; border-radius: 12px; }
/* Out of the way of an open map card/carousel (same flag that hides the ⚙ FAB). */
body.map-overlay-open .mode-bar { display: none; }
/* Этап 20: a focused price field's keyboard covers these fixed bars without moving them (main.js
   focusin/focusout on `topbar`) - hidden outright while a field in the search card has focus. */
body.search-kb-open .tab-bar, body.search-kb-open .mode-bar { display: none; }
/* Room under the last card for the floating switcher. */
#view-search { padding-bottom: 64px; }
/* Incoming pane of a mode switch: short fade + slight slide/scale from the button's side. */
/* No scale here: the deck/map measure their height while this runs, and a scaled box would read short. */
@keyframes mode-enter { from { opacity: 0; transform: translateX(calc(var(--mode-dir, 1) * 10px)); } }
.mode-enter { animation: mode-enter 220ms var(--ease-out) backwards; }

/* ---- onboarding slides (onboarding.js) ---- */
@keyframes onb-in { from { opacity: 0; } }
.onb-overlay {
  position: fixed; inset: 0; z-index: 400; display: flex; flex-direction: column; background: var(--accent); color: var(--accent-text);
  animation: onb-in 240ms var(--ease) backwards;
}
.onb-overlay.closing { opacity: 0; transition: opacity 220ms var(--ease); pointer-events: none; }
.onb-top { display: flex; justify-content: flex-end; padding: calc(14px + env(safe-area-inset-top)) 14px 0; }
.onb-skip { background: none; border: 0; color: var(--accent-text); opacity: .75; font: 600 13px var(--font-ui); min-height: 44px; padding: 0 10px; }
.onb-skip:active { opacity: .5; }
.onb-track-wrap { flex: 1; overflow: hidden; touch-action: pan-y; }
.onb-track { display: flex; height: 100%; }
.onb-slide { flex: 0 0 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 0 28px 10px; gap: 16px; }
.onb-ill { width: min(220px, 70vw); height: auto; }
.onb-slide h3 { margin: 0; font-size: 21px; font-weight: 700; }
.onb-slide p { margin: 0; font-size: 14px; line-height: 1.5; opacity: .85; max-width: 300px; }
.onb-bottom { display: flex; flex-direction: column; align-items: center; gap: 18px; padding: 6px 20px calc(28px + env(safe-area-inset-bottom)); }
.onb-dots { display: flex; gap: 7px; }
.onb-dot { width: 7px; height: 7px; padding: 0; border: 0; border-radius: 999px; background: var(--accent-text); opacity: .3; transition: width .22s var(--ease-out), opacity .22s var(--ease); }
.onb-dot.active { width: 20px; opacity: 1; background: var(--amber); }
.onb-next { width: 100%; max-width: 280px; min-height: 48px; border-radius: 14px; border: 0; background: var(--amber); color: #2B1B06; font: 700 15px var(--font-ui); transition: transform var(--dur-fast) var(--ease); }
.onb-next:active { transform: scale(.97); }

/* ---- onboarding illustrations: endless loops (onboarding.js ILL) ----
   Each loop plays only while its own slide is .active (paused otherwise, restarted from 0% by
   playIllustration when it becomes active again) - see the note above ILL in onboarding.js for
   why every looping piece is transform/opacity-only on an element with no attribute transform="". */
.onb-slide:not(.active) .onb-loop { animation-play-state: paused; }
.onb-loop { transform-box: fill-box; transform-origin: center; }

/* Slide 1 "Всё в одном месте": the pill pulses like the real "Показать N новых" banner
   (.new-ads-banner / new-ads-pulse above) - box-shadow doesn't apply to <rect>, so the ring here
   is a second shape that expands and fades instead, same 2s rhythm. */
@keyframes onb-pill-scale { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } }
@keyframes onb-pill-ring { 0% { opacity: .55; transform: scale(1); } 50%, 100% { opacity: 0; transform: scale(1.35); } }
.onb-pulse-pill { animation: onb-pill-scale 2s ease-in-out infinite; }
.onb-pulse-ring { fill: none; stroke: var(--amber); stroke-width: 2; animation: onb-pill-ring 2s ease-in-out infinite; }

/* Slide 2 "Три способа смотреть": highlight travels список(0%) -> свайпы -> карта -> список(100%),
   each icon lighting up as the highlight reaches it; loop seam matches (100% state = 0% state). */
.onb-modes-icon { color: #EFE9DE; }
.onb-modes-thumb { animation: onb-modes-thumb 4.8s var(--ease-out) infinite; }
.onb-modes-icon1 { animation: onb-modes-icon1 4.8s linear infinite; }
.onb-modes-icon2 { animation: onb-modes-icon2 4.8s linear infinite; }
.onb-modes-icon3 { animation: onb-modes-icon3 4.8s linear infinite; }
@keyframes onb-modes-thumb { 0%, 27% { transform: translateX(0); } 33%, 60% { transform: translateX(58px); } 66%, 93% { transform: translateX(116px); } 100% { transform: translateX(0); } }
@keyframes onb-modes-icon1 { 0%, 27% { opacity: 1; } 33%, 93% { opacity: .5; } 100% { opacity: 1; } }
@keyframes onb-modes-icon2 { 0%, 27% { opacity: .5; } 33%, 60% { opacity: 1; } 66%, 100% { opacity: .5; } }
@keyframes onb-modes-icon3 { 0%, 60% { opacity: .5; } 66%, 93% { opacity: 1; } 100% { opacity: .5; } }

/* Slide 3 "Карта следит за районом": a fixed-position toggle (see the transform note above
   ILL) switches off (grey track, knob left) -> on (amber track, knob right, dashed ring brighter)
   -> off, with a comfortable hold on each side. */
.onb-toggle-track { fill: #EFE9DE; opacity: .25; }
.onb-toggle-track.onb-loop { animation: onb-toggle-track 4.5s var(--ease) infinite; }
.onb-toggle-knob.onb-loop { animation: onb-toggle-knob 4.5s var(--ease) infinite; }
.onb-area-ring.onb-loop { animation: onb-area-ring 4.5s var(--ease) infinite; }
@keyframes onb-toggle-track { 0%, 8% { fill: #EFE9DE; opacity: .25; } 22%, 60% { fill: var(--amber); opacity: 1; } 74%, 100% { fill: #EFE9DE; opacity: .25; } }
@keyframes onb-toggle-knob { 0%, 8% { transform: translateX(0); } 22%, 60% { transform: translateX(18px); } 74%, 100% { transform: translateX(0); } }
@keyframes onb-area-ring { 0%, 8% { opacity: .4; } 22%, 60% { opacity: 1; } 74%, 100% { opacity: .4; } }

/* Slide 4 "Хорошее уходит быстро": bell shakes, an amber ping appears, a dashed arrow draws to a
   speech bubble at the bell's own height, its outline fades in, then its three dots in turn and
   bounce like typing - everything settles back to rest before the loop repeats. */
.onb-bell-shake.onb-loop { transform-origin: 50% 0%; animation: onb-bell-shake 5.5s ease-in-out infinite; }
.onb-bell-dot.onb-loop { animation: onb-bell-dot 5.5s linear infinite; }
.onb-bell-arc1.onb-loop { animation: onb-bell-arc1 5.5s linear infinite; }
.onb-bell-arc2.onb-loop { animation: onb-bell-arc2 5.5s linear infinite; }
.onb-bell-arc3.onb-loop { animation: onb-bell-arc3 5.5s linear infinite; }
.onb-bell-arc4.onb-loop { animation: onb-bell-arc4 5.5s linear infinite; }
.onb-bell-arc5.onb-loop { animation: onb-bell-arc5 5.5s linear infinite; }
.onb-bell-arrowhead.onb-loop { animation: onb-bell-arrowhead 5.5s linear infinite; }
.onb-bell-bubble.onb-loop { animation: onb-bell-bubble-outline 5.5s linear infinite; }
.onb-bell-d1.onb-loop { animation: onb-bell-d-op1 5.5s linear infinite, onb-bell-bounce1 5.5s linear infinite; }
.onb-bell-d2.onb-loop { animation: onb-bell-d-op2 5.5s linear infinite, onb-bell-bounce2 5.5s linear infinite; }
.onb-bell-d3.onb-loop { animation: onb-bell-d-op3 5.5s linear infinite, onb-bell-bounce3 5.5s linear infinite; }
@keyframes onb-bell-shake { 0%, 4% { transform: rotate(0deg); } 6% { transform: rotate(-10deg); } 9% { transform: rotate(9deg); } 12% { transform: rotate(-7deg); } 15% { transform: rotate(5deg); } 18%, 100% { transform: rotate(0deg); } }
@keyframes onb-bell-dot { 0%, 20% { opacity: 0; transform: scale(.4); } 26% { opacity: 1; transform: scale(1.15); } 30%, 92% { opacity: 1; transform: scale(1); } 97%, 100% { opacity: 0; transform: scale(.4); } }
/* The arc beads fade in one by one, left to right, so the line reads as drawing itself smoothly
   instead of the old stroke-dasharray trick (interpolating "0 999" -> "3 5" keeps the gap huge
   until almost the end, so it stayed invisible then popped in whole). The arrowhead fades in last,
   right before the bubble - its rotation is baked into its transform="" attribute in onboarding.js
   (matches the curve's own end tangent), only opacity animates here. */
@keyframes onb-bell-arc1 { 0%, 24% { opacity: 0; } 28%, 90% { opacity: 1; } 95%, 100% { opacity: 0; } }
@keyframes onb-bell-arc2 { 0%, 27.5% { opacity: 0; } 31.5%, 90% { opacity: 1; } 95%, 100% { opacity: 0; } }
@keyframes onb-bell-arc3 { 0%, 31% { opacity: 0; } 35%, 90% { opacity: 1; } 95%, 100% { opacity: 0; } }
@keyframes onb-bell-arc4 { 0%, 34.5% { opacity: 0; } 38.5%, 90% { opacity: 1; } 95%, 100% { opacity: 0; } }
@keyframes onb-bell-arc5 { 0%, 38% { opacity: 0; } 42%, 90% { opacity: 1; } 95%, 100% { opacity: 0; } }
@keyframes onb-bell-arrowhead { 0%, 41.5% { opacity: 0; } 45.5%, 90% { opacity: 1; } 95%, 100% { opacity: 0; } }
@keyframes onb-bell-bubble-outline { 0%, 46% { opacity: 0; } 50%, 90% { opacity: 1; } 95%, 100% { opacity: 0; } }
@keyframes onb-bell-d-op1 { 0%, 54% { opacity: 0; } 58%, 90% { opacity: 1; } 95%, 100% { opacity: 0; } }
@keyframes onb-bell-d-op2 { 0%, 58% { opacity: 0; } 62%, 90% { opacity: 1; } 95%, 100% { opacity: 0; } }
@keyframes onb-bell-d-op3 { 0%, 62% { opacity: 0; } 66%, 90% { opacity: 1; } 95%, 100% { opacity: 0; } }
@keyframes onb-bell-bounce1 { 0%, 66% { transform: translateY(0); } 68% { transform: translateY(-3px); } 70%, 100% { transform: translateY(0); } }
@keyframes onb-bell-bounce2 { 0%, 70% { transform: translateY(0); } 72% { transform: translateY(-3px); } 74%, 100% { transform: translateY(0); } }
@keyframes onb-bell-bounce3 { 0%, 74% { transform: translateY(0); } 76% { transform: translateY(-3px); } 78%, 100% { transform: translateY(0); } }

/* Slide 5 "Уже смотрели — видно сразу": the middle card is pressed, a detail sheet opens
   full-screen over the list (like actually opening the ad), closes, THEN the real "Просмотрено"
   badge fades in on that card while its photo/title dim (same look and .55 opacity as .is-viewed
   above) - only after that does the list really scroll one slot (card height 37 + gap 8 = 45px):
   the viewed card slides up and off, the fresh one below slides up to take its place. Everything
   snaps back to rest in the last few % so the loop seam is invisible. */
.onb-seen-text { font-family: var(--font-ui); }
/* The opened card: the slide's own green, a step lighter, opaque so the list underneath stays hidden. */
.onb-detail-bg { fill: color-mix(in srgb, var(--accent-text) 9%, var(--accent)); }
.onb-seen-scroll.onb-loop { animation: onb-seen-scroll 7s var(--ease) infinite; }
.onb-card2.onb-loop { animation: onb-card2-press 7s var(--ease) infinite; }
.onb-card2-photo.onb-loop, .onb-card2-title.onb-loop { animation: onb-card2-dim 7s linear infinite; }
.onb-card2-badge.onb-loop { animation: onb-card2-badge 7s linear infinite; }
.onb-detail.onb-loop { animation: onb-detail 7s var(--ease) infinite; transform-origin: 50% 58%; }
@keyframes onb-seen-scroll { 0%, 62% { transform: translateY(0); } 78%, 96% { transform: translateY(-45px); } 100% { transform: translateY(0); } }
@keyframes onb-card2-press { 0%, 4% { transform: scale(1); } 7% { transform: scale(.96); } 10%, 100% { transform: scale(1); } }
@keyframes onb-detail { 0%, 8% { opacity: 0; transform: scale(.85); } 18%, 40% { opacity: 1; transform: scale(1); } 50%, 100% { opacity: 0; transform: scale(.85); } }
@keyframes onb-card2-dim { 0%, 50% { opacity: 1; } 56%, 96% { opacity: .55; } 100% { opacity: 1; } }
@keyframes onb-card2-badge { 0%, 50% { opacity: 0; } 56%, 96% { opacity: 1; } 100% { opacity: 0; } }

/* ---- guided tour over the real app (tour.js) ---- */
.tour-layer { position: fixed; inset: 0; z-index: 1000; pointer-events: none; }
.tour-layer.closing { opacity: 0; transition: opacity 220ms var(--ease); }
.tour-blocker { position: absolute; inset: 0; pointer-events: auto; }
/* The lit element is outlined by this box; its huge shadow dims everything else. Moving between
   steps animates its position/size - a rare, discrete move (once per step), not a per-frame loop. */
.tour-spot {
  position: absolute; left: 0; top: 0; border-radius: 14px; pointer-events: none;
  box-shadow: 0 0 0 9999px rgba(8, 18, 14, .5);
  transition: transform .32s var(--ease-out), width .32s var(--ease-out), height .32s var(--ease-out), box-shadow .3s var(--ease);
}
.tour-layer.lit .tour-spot { box-shadow: 0 0 0 9999px rgba(8, 18, 14, .78); }
.tour-ring {
  position: absolute; width: 44px; height: 44px; margin: -22px 0 0 -22px; border-radius: 50%; border: 2px solid var(--amber);
  pointer-events: none; opacity: 0; animation: tour-ring-pulse 1.3s var(--ease-out) infinite;
}
.tour-layer:not(.lit) .tour-ring { display: none; }
@keyframes tour-ring-pulse { 0% { transform: scale(.6); opacity: .9; } 70% { transform: scale(1.5); opacity: 0; } 100% { transform: scale(1.5); opacity: 0; } }
.tour-topbar {
  position: absolute; top: 0; left: 0; right: 0; display: flex; align-items: center; justify-content: space-between;
  padding: calc(10px + env(safe-area-inset-top)) 12px 10px; pointer-events: none;
}
.tour-topbar > * { pointer-events: auto; }
.tour-count, .tour-skip { background: rgba(0, 0, 0, .45); color: #fff; border-radius: 999px; font: 600 12px var(--font-ui); }
.tour-count { padding: 5px 11px; }
.tour-count:empty { display: none; }
.tour-skip { border: 0; min-height: 34px; padding: 0 12px; }
.tour-bubble {
  position: absolute; pointer-events: auto; padding: 12px 14px 10px; border-radius: 14px; background: var(--text); color: var(--bg);
  font-size: 13.5px; line-height: 1.45; box-shadow: 0 10px 26px rgba(0, 0, 0, .35);
  animation: tour-bubble-in var(--dur-base) var(--ease-out) backwards;
}
:root[data-theme="dark"] .tour-bubble { background: var(--surface-2); color: var(--text); }
@keyframes tour-bubble-in { from { opacity: 0; transform: translateY(6px) scale(.97); } }
.tour-bubble::after { content: ""; position: absolute; left: var(--arrow-x, 20px); width: 10px; height: 10px; background: inherit; transform: rotate(45deg); }
.tour-bubble.below::after { top: -5px; }
.tour-bubble.above::after { bottom: -5px; }
.tour-next { display: block; margin: 8px 0 0 auto; min-height: 34px; padding: 0 16px; border: 0; border-radius: 10px; background: var(--amber); color: #2B1B06; font: 700 13px var(--font-ui); }
.tour-final {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); pointer-events: auto; display: grid; gap: 14px; justify-items: center;
  padding: 22px 26px; border-radius: 18px; background: var(--card); color: var(--text); box-shadow: 0 14px 36px rgba(0, 0, 0, .35); text-align: center;
  animation: onb-in var(--dur-base) var(--ease) backwards;
}
.tour-final__text { font-size: 17px; font-weight: 700; }

/* ---- notifications feed redesign (Этап 18, docs/design/feed-18.html: strip + round icon) ----
   Every event kind has its own colour; the left strip and the round icon share it. */
:root { --price-down: #12994F; --price-up: #C93C3C; }
:root { --warn-tint: color-mix(in srgb, var(--warn) 15%, transparent); --danger-tint: color-mix(in srgb, var(--danger) 14%, transparent); }
.notif-swipe { border-radius: 16px; border-bottom: 0; margin-bottom: 10px; }
.notif-item { padding: 12px; background: var(--surface); border-radius: 16px; border-left: 3px solid transparent; align-items: flex-start; }
.notif-item.is-viewed .notif-item__thumb, .notif-item.is-viewed .notif-item__title { opacity: .55; }
.notif-actions { border-radius: 0 16px 16px 0; overflow: hidden; }
.notif-item__body { display: flex; flex-direction: column; gap: 5px; }
.notif-item__title { margin: 0; font-size: 14.5px; line-height: 1.35; }
.notif-item__addr { font-size: 12.5px; color: var(--muted); font-weight: 500; }
.notif-kind { display: flex; align-items: center; gap: 6px; font-size: 12.5px; font-weight: 700; color: var(--kind-c, var(--muted)); }
.notif-kind__chip { width: 20px; height: 20px; border-radius: 999px; display: flex; align-items: center; justify-content: center; flex: none; background: var(--kind-bg, transparent); }
.notif-kind__chip svg { width: 12px; height: 12px; }
.notif-kind__label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.notif-kind__time { margin-left: auto; padding-left: 6px; font-size: 12px; font-weight: 600; color: var(--muted); white-space: nowrap; flex: none; }
.notif-item__row2 { flex-wrap: wrap; margin-top: 1px; }
.price-now { font-size: 14.5px; font-weight: 700; }
.price-old { font-size: 12.5px; color: var(--muted); text-decoration: line-through; font-weight: 600; }
.price-delta { font-size: 12px; font-weight: 700; padding: 2px 7px; border-radius: 999px; }
.price-delta.down { background: color-mix(in srgb, var(--price-down) 16%, transparent); color: var(--price-down); }
.price-delta.up { background: color-mix(in srgb, var(--price-up) 15%, transparent); color: var(--price-up); }
.src-dot { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 600; color: var(--muted); }
.src-dot::before { content: ""; width: 7px; height: 7px; border-radius: 999px; background: var(--site, var(--muted)); }
.src-dot.src-kufar { --site: #12a150; } .src-dot.src-onliner { --site: #2f6df6; } .src-dot.src-realt { --site: #f26b1d; }
.status-tag { display: inline-flex; align-items: center; gap: 4px; font-size: 11.5px; font-weight: 700; padding: 2px 8px; border-radius: 999px; }
.status-tag.gone { background: var(--danger-tint); color: var(--danger); }
.status-tag svg { width: 12px; height: 12px; }
.notif-item .viewed-tag { padding: 0; }
.k-search { --kind-c: var(--accent-ink); --kind-bg: color-mix(in srgb, var(--accent) 14%, transparent); }
.k-cheaper { --kind-c: var(--amber-ink); --kind-bg: var(--amber-tint); }
.k-fav-down { --kind-c: var(--price-down); --kind-bg: color-mix(in srgb, var(--price-down) 16%, transparent); }
.k-fav-up { --kind-c: var(--price-up); --kind-bg: color-mix(in srgb, var(--price-up) 15%, transparent); }
.k-fav-gone { --kind-c: var(--danger); --kind-bg: var(--danger-tint); }
.notif-item.k-search { border-left-color: var(--accent); }
.notif-item.k-cheaper { border-left-color: var(--amber); }
.notif-item.k-fav-down { border-left-color: var(--price-down); }
.notif-item.k-fav-up { border-left-color: var(--price-up); }
.notif-item.k-fav-gone { border-left-color: var(--danger); }
/* "⋯" menu: the trash and "clear everything" */
.notif-menu-wrap { position: relative; }
.notif-menu-wrap .menu { position: absolute; top: 46px; right: 0; min-width: 190px; background: var(--surface); border-radius: 14px; box-shadow: 0 8px 24px rgba(0,0,0,.22); padding: 6px; z-index: 10; }
.notif-menu-wrap .menu.hidden { display: none; }
.notif-menu-wrap .menu button { width: 100%; display: flex; align-items: center; gap: 10px; padding: 10px; border: 0; background: none; color: var(--text); font: 600 14px var(--font-ui); border-radius: 10px; text-align: left; }
.notif-menu-wrap .menu button svg { width: 17px; height: 17px; color: var(--muted); flex: none; }
.notif-menu-wrap .menu button:active { background: var(--surface-2); }
.notif-menu-wrap .menu button:disabled { opacity: .4; }
.notif-menu-wrap .menu button.danger, .notif-menu-wrap .menu button.danger svg { color: var(--danger); }
.notif-menu-wrap .menu hr { border: 0; border-top: 1px solid var(--line); margin: 4px 2px; }
.notif-empty { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 12px; padding: 70px 24px 40px; color: var(--muted); }
.notif-empty.hidden { display: none; }
.notif-empty svg { width: 40px; height: 40px; opacity: .45; }
.notif-empty p { margin: 0; font-size: 14px; line-height: 1.55; max-width: 260px; }
.notif-empty b { color: var(--text); }

/* ---- Этап 20: price/rooms panel, ported from docs/design/filters-panel-20.html (user's choice:
   Variant C, "Подкарточки + кнопка Следить") - replaces the compact arrow-only row of 19b/19c/19d.
   Collapsed: a full summary line + chevron. Open: two subcards (Комнат / Цена, no system number
   spinners) stacked, then a "Следить за этим поиском" pill leading to "Уведомления" - the same place
   the old bare bell icon did, now the panel's only bell. The top block above it (region/deal/
   currency/period/sources) is untouched. */
.summary-row {
  width: 100%; display: flex; align-items: center; gap: 8px; min-height: 38px; padding: 8px 2px;
  border: 0; background: transparent; color: var(--text); font-family: inherit; text-align: left; border-radius: 12px;
}
.summary-row:active { background: var(--surface-2); }
.summary-row__text {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 13.5px; font-weight: 600; color: var(--muted);
}
.summary-row__text.filled { color: var(--text); }
.summary-row svg { width: 20px; height: 20px; color: var(--muted); flex: none; transition: transform 280ms var(--ease-out); }
.summary-row.open svg { transform: rotate(180deg); }
/* grid 0fr -> 1fr animates the height without measuring it; the inner node clips while closed (so its
   children, not the inner node itself, carry the paddings). The blanket reduced-motion rule cuts it. */
.more-section { display: grid; grid-template-rows: 0fr; opacity: 0; transition: grid-template-rows 280ms var(--ease-out), opacity 220ms var(--ease-out); }
.more-section.open { grid-template-rows: 1fr; opacity: 1; }
.more-section__inner { overflow: hidden; min-height: 0; }
.sub-card { background: var(--surface-2); border-radius: 14px; padding: 10px 10px 11px; margin: 10px 2px 0; }
.sub-card__head { display: flex; align-items: center; gap: 7px; font-weight: 700; font-size: 13.5px; margin-bottom: 8px; }
.sub-card__head svg { width: 16px; height: 16px; color: var(--muted); flex: none; }
.sub-rooms { display: flex; gap: 6px; }
.sub-rooms .chip { flex: 1; padding: 6px 4px; }
.sub-price { display: flex; align-items: center; gap: 7px; }
/* No system spinner arrows (docs/design/filters-panel-20.html's .num). */
.sub-price input {
  width: 100%; min-width: 0; height: 38px; border-radius: 10px; border: 1px solid var(--line); background: var(--surface);
  color: var(--text); font: 600 14px var(--font-ui); text-align: center; padding: 0 4px; -moz-appearance: textfield;
}
.sub-price input::-webkit-outer-spin-button, .sub-price input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.watch-pill {
  display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; margin: 12px 2px 0;
  border: 0; border-radius: 12px; background: var(--amber-tint); color: var(--amber-ink); font-size: 13.5px; font-weight: 700; padding: 11px 10px;
}
.watch-pill svg { width: 18px; height: 18px; flex: none; }
.search-card.editing .period-row, .search-card.editing .search-card__top { display: none; }

/* ---- Этап 19: "Уведомления" sheet - the big call to action of an empty list ---- */
.track-empty { display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 36px 12px 24px; text-align: center; }
.track-empty .hint { margin: 0; max-width: 280px; }
.track-cta {
  display: inline-flex; align-items: center; gap: 12px; padding: 18px 28px; border: 0; border-radius: 20px;
  background: var(--amber); color: var(--on-amber); font-size: 18px; font-weight: 800; box-shadow: 0 8px 20px rgba(0,0,0,.2);
  animation: track-pulse 2s ease-in-out infinite;
}
.track-cta svg { width: 24px; height: 24px; }
@keyframes track-pulse {
  0%   { transform: scale(1);    box-shadow: 0 8px 20px rgba(0,0,0,.2), 0 0 0 0 color-mix(in srgb, var(--amber) 55%, transparent); }
  50%  { transform: scale(1.04); box-shadow: 0 8px 20px rgba(0,0,0,.2), 0 0 0 10px color-mix(in srgb, var(--amber) 0%, transparent); }
  100% { transform: scale(1);    box-shadow: 0 8px 20px rgba(0,0,0,.2), 0 0 0 0 color-mix(in srgb, var(--amber) 0%, transparent); }
}
.add-alert { margin-top: 12px; }

/* ---- Этап 19b: the bot chat goes on with the operator ---- */
.sbot-who { display: block; font-size: 11.5px; font-weight: 700; color: var(--muted); margin-bottom: 2px; }
.sbot-note { justify-self: center; color: var(--muted); font-size: 13px; padding: 4px 0; }

/* ---- Этап 19d: keyboard debug line (util.js trackKeyboard) - admin / ?kbdebug=1 only ---- */
.kb-debug {
  position: absolute; top: 0; left: 0; right: 0; z-index: 5; padding: 2px 8px; pointer-events: none;
  font: 10px/1.3 ui-monospace, Menlo, monospace; color: #fff; background: rgba(0, 0, 0, .6); white-space: nowrap; overflow: hidden;
}
.kb-debug.hidden { display: none; }

/* ---- Этап 19d: the arrow row and its section are one grid item (.more-wrap, main.js), so no row gap
   is left under the arrow while the section is closed; the card's bottom padding is the only space. ---- */
.more-wrap { min-width: 0; }
.search-card { padding-bottom: 6px; }
