/* Дизайн мини-аппа.

   Ориентир — экраны Aviasales, Trip.com и Skyscanner: карточка билета там
   это прежде всего два времени и линия перелёта между ними, а всё
   остальное — подписи мелким. Прошлая версия рисовала коды городов и
   пунктирную стрелку с самолётиком, то есть украшение на месте данных:
   часы вылета источник отдаёт в каждой котировке, просто до экрана они
   не доходили.

   Палитра своя, а не из Telegram: тема оболочки даёт два фона и один
   акцент, на них тревел-интерфейс не собрать. У Telegram остаётся цвет
   шапки — она стоит вплотную над вебвью, и разъезжаться с ней нельзя. */

/* ═══ ТОКЕНЫ (v9 «направление v3» — design/flightdeals-v3-prompt.md +
   design/flightdeals-v3-reference.html, волна A) ═══════════════════════
   Полная замена палитры v8 (фиолет/светлый-дефолт) на эталонную:
   графит #0e0f13, тёплый белый #f4f1ea, лиловый только у самолётика
   маршрута. Тёмная тема теперь ОСНОВНАЯ — обычный body БЕЗ класса это и
   есть тёмная тема; body.light (волна D) описан отдельно ниже одним
   блоком, как просит задание, инверсия по формуле промпта.
   Приём с волны v8 сохранён: канонические имена определены тут, старые
   (--page/--line/--muted/--faint/--brand/--link/--good/--warn…), которыми
   пользуются десятки правил экранов волн B/C, остаются алиасами через
   var() — значение меняется в одном месте. */
:root {
  color-scheme: dark;

  --bg: #0e0f13;
  --ink: #f4f1ea;
  /* Роадмап §5б (readability-аудит): .6 давало 6.53:1 на --bg (с запасом),
     но ЭТОТ ЖЕ токен (через --text-2/--muted) красит текст и поверх
     --surface/--surface-2 (карточки/чипы) — там разница светлее фона
     меньше, а в светлой теме .6 держал текст-2 буквально впритык, 4.57:1
     (см. body.light ниже) — один шаг рендера от провала. .65 (посчитано
     WCAG-скриптом, tests/js/test-tabs-contrast.js) даёт 7.5:1/6.9:1 тёмная
     на --bg/--surface-2, 5.39:1/5.01:1 светлая — везде запас, не впритык. */
  --mute: rgba(244, 241, 234, .65);
  /* Ревью v12, P1: строго декоративное — пунктир/точки/затухающий
     превью-текст соседей карусели (.dl-switch-side, .sc-dash-dim). Любой
     текст, который пользователь должен ПРОЧЕСТЬ, ходит через --text-3
     ниже (body{}) — тот больше не алиас на это значение, он даёт 2.7:1,
     ниже порога даже «крупного» текста. */
  --dim: rgba(244, 241, 234, .32);

  /* Единственный цветной акцент во всём приложении — и то только у
     самолётика на пунктирном маршруте (.rh-path svg, компонент
     RouteDash). НЕ путать с --accent ниже: тот красит унаследованные с
     v8 интерактивные заливки (активный чип, выбранный день календаря,
     sheet-cta) и лиловым не пользуется — промпт запрещает цветной акцент
     где-либо ещё («Фон и цвета»). */
  --lilac: #b7b0f2;

  /* Семантика цены (промпт, «Фон и цвета»): зелёный — дешевле/упала,
     красный — дороже/выросла. Только бейджи и цифры, никогда фоны. */
  --price-good: #8fe0bf;
  --price-high: #f09590;

  /* Формула затемнения фото (промпт, «Фото») + зерно (SVG feTurbulence,
     дословно из эталона) + виньетка — общие для настоящих фото и
     процедурных сцен городов (см. секцию «сцены» ниже и PhotoCard/
     sceneHtml в app.js). */
  --photo-scrim: linear-gradient(180deg, transparent 40%, rgba(14, 15, 19, .75) 78%, rgba(14, 15, 19, .95) 100%);
  --photo-scrim-light: linear-gradient(180deg, rgba(14, 15, 19, .42) 0%, transparent 26%, transparent 40%, rgba(14, 15, 19, .78) 78%, rgba(14, 15, 19, .96) 100%);
  --vignette: radial-gradient(120% 90% at 50% 35%, transparent 40%, rgba(0, 0, 0, .55) 100%);
  --grain-svg: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 .9 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");

  /* Радиусы (промпт, «Кнопки и элементы»): карточки/шит — 28, пилюля —
     25. --r-sm/--r-md (12, мелкие элементы/инпуты/строки-списков) волной
     A не тронуты — вне периметра «карточки/пилюля» промпта. */
  --r-sm: 12px;
  --r-md: 12px;
  --r-card: 28px;
  --r-pill: 25px;
  --r-lg: var(--r-card);
  --r-xl: var(--r-card);

  /* Шкала кегля унаследованных экранов (волны B/C по месту). */
  --f-xs: 11px;
  --f-sm: 12px;
  --f-md: 13px;
  --f-base: 15px;
  --f-lg: 17px;
  --f-xl: 21px;
  --f-2xl: 26px;
  --f-3xl: 34px;
  --f-title: 27px;

  /* Именованная шкала v8 — она каскадирует на все 4 таба через .head h1
     (--type-display-*) и .list-group-title (--type-micro-*), так
     что вес/трекинг обновлены тут же по правилу промпта («никакого веса
     600-700 в заголовках», «капс-подписи 11px, letter-spacing .16em»).
     Остальные весовые литералы (600-800) внутри отдельных компонентов —
     не через эти токены, волны B/C правят по месту (см. DESIGN_PLAN.md,
     раздел v9 волна A — список того, что сознательно не тронуто). */
  --type-display-size: 32px; --type-display-lh: 38px; --type-display-w: 300;
  --type-title-size: 22px;   --type-title-lh: 28px;   --type-title-w: 400;
  --type-headline-size: 17px; --type-headline-lh: 22px; --type-headline-w: 400;
  --type-body-size: 15px;    --type-body-lh: 20px;    --type-body-w: 400;
  --type-caption-size: 13px; --type-caption-lh: 16px; --type-caption-w: 400;
  --type-micro-size: 11px;   --type-micro-lh: 14px;   --type-micro-w: 400;
  --type-micro-tracking: .16em;

  /* Новые именованные размеры промпта — для экранов волн B/C (01/02/03/
     06/07): заголовок/город 42-46 вес 300, город капсом в «Куда угодно»
     40-44 трекинг .2em, цена 40 вес 300, капс-подпись 11 трекинг .16em
     (то же значение, что --type-micro-tracking — два имени, потому что
     роли разные: micro красит существующие служебные ярлыки, cap — новые
     герой-подписи экрана из промпта). */
  --type-hero-size: 44px; --type-hero-w: 300;
  --type-citycaps-size: 42px; --type-citycaps-tracking: .2em; --type-citycaps-w: 400;
  --type-price-size: 40px; --type-price-w: 300;
  --type-cap-size: 11px; --type-cap-tracking: .16em;

  --pad: 16px;
  --safe-b: env(safe-area-inset-bottom, 0px);
  /* Плавающая стеклянная пилюля таббара (промпт, «Кнопки и элементы»):
     64 высотой (было 60 — роадмап §5а вернул подписи под иконками, бар
     подрос на те же 4px, что и .tab, см. её блок ниже) + 28 от низа +
     запас, чтобы последняя карточка ленты не утыкалась прямо в бар
     (задание волны A, п.4 — «паддинги контента снизу учитывают плавающий
     бар»). Единственные потребители токена — main{}/#home-view{} ниже по
     файлу (grep подтверждает) — травел/карта своих паддингов от него не
     считают, им трогать нечего. */
  --tabbar-h: 120px;

  --icon-stroke: 1.75;

  /* Высоты: primary-пилюля 50 (промпт), secondary-иконка/чипы — как в v8. */
  --btn-h: 50px;
  --btn-h-sm: 44px;
  --chip-h: 36px;

  /* Текст/иконки поверх фото (PhotoCard) и поверх сплошных тёмных
     подложек (blur-кружки действий) — не меняются между темами. */
  --on-scrim: #fff;
  --on-scrim-2: rgba(255, 255, 255, .82);
  --on-scrim-3: rgba(255, 255, 255, .65);
  /* Ревью v12, P1: токен был объявлен, но ничем не потреблялся —
     .dl-badge/.dl-fav/.rd-badge/.rd-radar жили только на .glass
     (rgba(255,255,255,.12), белое стекло), и в светлых верхних 12%
     фото, где --photo-scrim ещё прозрачен (transparent 40%), бейдж/
     сердечко мерили 2.41-3.38:1. .45 не хватало даже на пересвеченном
     небе (2.72:1 посчитано) — поднято до .62 (≥4.5:1 на белом фоне,
     формула WCAG), задействовано ниже у самих компонентов. */
  --scrim-btn-bg: rgba(10, 16, 26, .62);
  /* Аудит 2026-09-05 — --radar-scrim/--radar-scrim-ink изначально были
     задуманы как единый источник И для OFF, И для ON радара НАД фото
     (.rt-radar в .rt-cta-row, .rd-radar). ON-роль у них забрана 06.09
     (см. --radar-on/--radar-on-ink чуть ниже) — здесь остаётся только
     OFF: кремовое стекло над фото, не меняется между темами, та же
     логика, что и --on-scrim* строкой выше. */
  --radar-scrim: #f4f1ea;
  --radar-scrim-ink: #0e0f13;
  /* Аудит 2026-09-06 (владелец сравнил обе кнопочные семьи «Следить» —
     над фото, .rt-radar/.rd-radar, и в плоском подвале шита, #fd-radar/
     #ds-radar — рядом и увидел, что даже после фикса 05.09 они красятся
     по-разному: над фото ON держался тем-инвариантного --radar-scrim
     (кремовый), в подвале — тем-следящего var(--ink)/var(--bg), в
     светлой теме выглядят буквально противоположными цветами). Отныне
     ON — ОДИН источник на ВСЕ поверхности и ОБЕ темы: заливка тем же
     зелёным, что означает «дешевле»/«Покупать» (--price-good), но сам
     --price-good тем-следящий (#8fe0bf тёмная/#166b4a светлая — см.
     body{}/body.light ниже) и для инварианта не подходит, отсюда
     отдельный литерал средней яркости. Объявлен только тут, в :root, —
     НЕ переопределять в body.light, инвариантность нужна буквально.
     Иконка поверх — currentColor (.rt-radar svg/.rd-radar svg ниже по
     файлу), --radar-on-ink просто красит currentColor в белый. */
  --radar-on: #2f8f4e;
  --radar-on-ink: #ffffff;
  --overlay: rgba(6, 10, 18, .4);
  --shadow-modal: 0 -8px 40px rgba(0, 0, 0, .3);
  --shadow-pin: 0 1px 4px rgba(0, 0, 0, .35);
  --shadow-glow: 0 4px 18px rgba(0, 0, 0, .4);
}

/* Поверхности (--bg/--surface) — СВОИ, курированные (applyTelegramSurfaces()
   в app.js прокидывает themeParams.bg_color/secondary_bg_color сюда как
   приоритетный источник, курированный хекс ниже — фолбэк). Текст, бордер
   и семантические цвета остаются курированными: контраст — обязательство
   приложения, не темы стороннего бота. Три ступени --surface/--surface-2/
   --border остаются от v8 (унаследованные экраны волн B/C ещё держатся на
   сплошных карточках, не на стекле/фото/воздухе промпта) — просто
   пересчитаны под новый графитовый --bg вместо угольного v8. */
body {
  --bg: var(--tg-bg, #0e0f13);
  --surface: var(--tg-surface, #17181d);
  --surface-2: #1d1f26;
  /* «Разделители-волосы» — промпт, экран Профиль. */
  --border: rgba(255, 255, 255, .08);

  --text: var(--ink);
  --text-2: var(--mute);
  /* Ревью v12, P1 (§0): было var(--dim) — 2.67:1 тёмная/2.04:1 светлая,
     третий тир текста (крошки/шевроны «›»/«Реклама · erid»/подписи под
     барами) читался в 2-3 раза хуже норматива. .48/.6 — минимум,
     который держит ≥4.5:1 на обеих --bg (посчитано формулой WCAG, см.
     /tmp/aud-ux/notes — 4.56:1 тёмная тут же, светлая — ниже в
     body.light: фон там уже настолько светлый, что --text-2 (.6) сам
     держит только 4.57:1, так что третий тир физически не может быть
     заметно тусклее второго и остаться читаемым — не то же самое
     значение случайно, эта светлая тема просто не даёт зазора. */
  --text-3: rgba(244, 241, 234, .48);

  --price-good: #8fe0bf;
  --price-neutral: var(--text-2);
  --price-high: #f09590;

  /* --accent* красит унаследованные интерактивные заливки — промпт
     запрещает цветной акцент где-либо, кроме самолётика маршрута, так
     что «акцент» тут буквально --ink: заливка белым по тёмному, тот же
     язык, что у .pill/.sheet-cta ниже. */
  --accent: var(--ink);
  --accent-text: var(--ink);
  --on-accent: var(--bg);
  --accent-soft: rgba(244, 241, 234, .12);

  --shadow: 0 2px 14px rgba(0, 0, 0, .38);
  --shadow-sm: var(--shadow);
  --skeleton: linear-gradient(90deg, #1e212b 25%, #282c39 37%, #1e212b 63%);
  /* Светлое стекло (эталон, .glass) — фон стеклянных иконок/пилюль/
     бейджей поверх фото. --nav-bg — отдельно, таббар в эталоне темнее и
     непрозрачнее generic .glass (нужно оставаться читаемым над любым
     фоном ленты, не только над фото). */
  --glass: rgba(255, 255, 255, .12);
  --nav-bg: rgba(20, 21, 26, .55);

  --sale: var(--price-high);
  --danger: var(--price-high);

  /* ── старые имена → алиасы на канонические ── */
  --page: var(--bg);
  --line: var(--border);
  --muted: var(--text-2);
  --faint: var(--text-3);
  --brand: var(--accent);
  --link: var(--accent-text);
  --on-brand: var(--on-accent);
  --brand-soft: var(--accent-soft);
  --good: var(--price-good);
  --warn: var(--price-high);
}

/* Волна D: светлая тема — инверсия по формуле промпта (#f3f1ec фон,
   #14151c текст), переключается по Telegram.WebApp.colorScheme
   (applyTheme(), app.js — уже вешает класс .light на body, когда тема
   светлая; менять там было нечего — v9 уже привела схему к «тёмная =
   дефолт без класса, светлая = body.light», см. комментарий в самом
   applyTheme()). Механизм — не копия всех правил файла, а ПЕРЕЗАПИСЬ
   горстки базовых custom properties: --text/--accent/--sale и десятки
   унаследованных алиасов уже объявлены через var(--ink)/var(--price-*)
   ВНУТРИ обычного body{} выше (не body.light) — переопределив тут только
   источники (--bg/--ink/--mute/--dim/--price-good/--price-high/
   --surface/--surface-2/--border/--nav-bg/--accent-soft/--skeleton), вся цепочка
   пересчитывается сама (тот же приём, что уже даёт --tg-bg/--tg-surface
   из applyTelegramSurfaces()). «Второстепенные — те же альфы» — дословно:
   --mute/--dim/--border/--accent-soft/.rh-dot/.switch(on) держат ТЕ ЖЕ
   значения альфы, что и в тёмной теме, только от новой светлой пары
   ink/bg, а не пересобраны на глаз. */
body.light {
  --bg: #f3f1ec;
  --ink: #14151c;
  /* Роадмап §5б: та же правка и по той же причине, что у --mute в :root
     выше (тёмная тема) — держим ОДНУ альфу в обеих темах (принцип файла,
     см. комментарий над body.light). Раньше .6 тут был одновременно и
     минимумом 4.5:1, и потолком — --text-3 ниже был вынужден повторить
     то же число (не мог быть темнее без провала). .65 даёт 5.39:1 —
     теперь у --text-3 наконец есть куда быть темнее --mute, не трогая
     сам факт, что он ниже порога не уходит (не меняем его значение). */
  --mute: rgba(20, 21, 28, .65);
  --dim: rgba(20, 21, 28, .32);
  --text-3: rgba(20, 21, 28, .6);
  /* Дневные сцены светлые сверху, а бейдж/сердечко живут в верхних
     углах — без верхнего затемнения контраст падал до 1.89 (ревью
     v10, P1-4). */
  --photo-scrim: var(--photo-scrim-light);

  /* Ревью v12, P1: #1d8a5f/#c94f43 (промпт) держат только 3.83:1/3.97:1
     на #f3f1ec — «Сейчас дёшево»/«−82%» это обычный некрупный текст
     (13-21px/300-600), порог 4.5:1. Затемнено на глаз до ≈5.5-5.7:1
     (посчитано формулой WCAG) — тот же оттенок, просто на 1-2 ступени
     темнее; бейдж/иконка НАД фото сюда не входят — те намеренно
     остаются пастельными в обеих темах (см. блок «Стекло/иконки/бейджи
     НАД фото» ниже, тут их не трогаем). */
  --price-good: #166b4a;
  --price-high: #a93a2f;

  /* Курированный фолбэк поверхностей — themeParams Telegram (applyTelegramSurfaces())
     по-прежнему приоритетнее через var(--tg-bg, …)/var(--tg-surface, …)
     в обычном body{} выше, тут только курированные значения САМОЙ
     светлой темы (были #17181d/#1d1f26 — тёмный графит, тут — тёплый
     сероватый оффвайт той же СЕМЬИ, что и --bg, на 1-2 ступени темнее). */
  --surface: #eae7e0;
  --surface-2: #e2ded4;
  --border: rgba(0, 0, 0, .08);
  --accent-soft: rgba(20, 21, 28, .12);
  /* Таббар — тоже стекло (blur+полупрозрачность), просто непрозрачнее
     generic .glass (комментарий у --nav-bg выше) — в светлой теме тот же
     принцип, светлое стекло вместо тёмного. */
  --nav-bg: rgba(255, 255, 255, .65);
  /* Skeleton-шиммер (план v8, §3) — был тёмно-графитовым фиксированно;
     на светлом фоне тёмный шиммер читался бы дырой, не «загрузкой». */
  --skeleton: linear-gradient(90deg, #e8e5dc 25%, #f0eee6 37%, #e8e5dc 63%);
}

/* ── Стекло/иконки/бейджи НАД фото — не зависят от темы тела ──────────
   Промпт: «сцены городов оставь тёмными как есть» — сами фото/сцены не
   меняются между темами (см. .scene* выше, .ex-photo — им ничего тут не
   нужно), поэтому всё, что лежит ПРЯМО НА фото, а не на --bg, остаётся
   таким же, каким было в тёмной теме, даже когда body.light перекрашивает
   --ink/--price-good/--price-high для остального интерфейса: назад/
   поделиться/радар «Маршрута» и «Сделки» (.rt-full-actions, .rt-radar),
   сердечко карусели (.dl-fav) и билета (.rt-fav), радар-toggle/«Подождать»
   «Радара» (.rd-radar/.rd-pill-wait), бейджи скидки/
   тренда (.dl-badge/.rd-badge), скидка в факт-строке «Сделки»
   (.rt-fact.is-good) и статус мёртвого билета (.rt-blurb.is-dead — «Сделка»,
   openDealCard). Значения — буквально те же литералы, что у .glass/
   --price-good/--price-high тёмной темы (см. токены в начале файла),
   продублированы тут явно: сам токен теперь другой в body.light, а этим
   элементам нужно СТАРОЕ значение независимо от темы.
   Аудит светлой темы Главной (04.09): `.hm-fav` (сердечко «На выходные»/
   «Горящего») — БУКВАЛЬНО тот же `.ic.glass` приём, что и `.dl-fav`
   (карточка «переиспользована», см. её комментарий в app.js), но в этот
   список не попала — свой `stroke: var(--ink)` (.ic svg, выше по файлу)
   молча темнел в body.light поверх фото/hue-карточки, как и .dl-fav до
   этого фикса. `.hm-hot-badge` раньше несла ту же history (`--ink` вместо
   буквального значения) — 11.09.2026 её правило снято вместе с остальным
   `.hm-hot-*` (карточка «Горящего» переиспользует `.hm-badge`, ниже по
   каскаду, а он и так красит текст буквальным `#0e0f13`, не токеном —
   свой override для body.light ему не нужен вовсе, см. .hm-badge). */
body.light .rt-full-actions .ic svg,
body.light .dl-fav svg,
body.light .hm-fav svg {
  stroke: #f4f1ea;
}
/* .rt-radar/.rd-radar ушли из списка выше (аудит 2026-09-05, радар меняет
   цвет по темам) — у обеих кнопок теперь свой `color: var(--radar-scrim)`
   в базовом (не body.light) правиле рядом с их классом ниже по файлу, и
   иконка берёт его через `stroke: currentColor`, а не хардкод тут; токен
   theme-инвариантен сам по себе (объявлен вне body.light, см. начало
   файла), отдельный override для body.light ему не нужен вовсе. */
body.light .dl-fav.is-fav svg,
body.light .rt-fav.is-fav svg {
  fill: #f4f1ea;
}
body.light .dl-badge,
body.light .rd-pill-wait,
body.light .rd-pill-watch,
body.light .rd-goal-chip {
  color: #f4f1ea;
}
body.light .rd-badge.is-good,
body.light .rt-fact.is-good {
  color: #8fe0bf;
}
body.light .rd-badge.is-high,
body.light .rt-blurb.is-dead {
  color: #f09590;
}
/* Приёмка v9, P1-5: `.pill { background: var(--ink); color: var(--bg) }`
   переворачивается в body.light вместе с --ink/--bg — правильно для
   пилюль НА --bg (шторки, профиль), но именно поверх фото это красит
   «Смотреть»/«Купить за …»/«Покупать» тёмным на тёмном фото. Правило —
   не по имени конкретной кнопки (`.dl-cta`), а по фото-контексту: любая
   `.pill` внутри контентной обёртки над фото (`.dl-info` карусели,
   `.rt-full-content` «Маршрута»/«Сделки», `.rd-card-content` «Радара» —
   те же классы, что закреплены выше в этом блоке) получает промптовую
   белую пилюлю независимо от темы тела, так же как иконки/бейджи рядом. */
body.light .dl-info .pill,
body.light .rt-full-content .pill,
body.light .rd-card-content .pill {
  background: #f4f1ea; color: #0e0f13;
}

/* ── Стекло НЕ над фото — обычные плашки на --bg, инвертируем (промпт:
   «стекло инвертируй rgba(0,0,0,.06-.08) + бордер rgba(0,0,0,.1)»).
   Меньшинство .glass-потребителей: остальные (см. блок выше) — над фото
   и туда не входят. Текст/иконки внутри уже читают --text/--text-2/--ink
   токенами и переворачиваются сами — тут только фон/обводка.
   Аудит светлой темы Главной (04.09): `#hm-search` (лупа топбара) и
   `.hm-hero-chip` («N сделок» на hero-карточке «Весь мир») — тот же
   голый `.glass` (rgba(255,255,255,.12)) на плоской `--bg`/`--surface-2`,
   что уже дал 1.34:1 у `#dl-mode-toggle`/`.preset` (см. её комментарий
   ниже) — тот же приём, замер PIL: без инверсии грань кружка/пилюли
   отличалась от страницы на 1-3 из 255 (не видна физически). */
body.light #dl-mode-toggle,
body.light .preset,
body.light #hm-search,
body.light .hm-hero-chip,
body.light .hm-pd-card,
body.light #profile-gear {
  background: rgba(0, 0, 0, .07);
  border-color: rgba(0, 0, 0, .1);
}
/* Аудит светлой темы Главной (04.09), баг №1: `.hm-chip` (чипы месяцев
   «Горящего сейчас») красил фон `var(--scrim-2)` — токен нигде не
   объявлен (тот же класс guaranteed-invalid custom property, что уже
   чинили у `.hm-card.hm-skel`/`.hm-cheaper-row` в этом файле, см. их
   комментарии), background молча резолвился в transparent, а текст —
   буквально `--on-scrim` (#fff, тем-инвариантный). В тёмной теме баг был
   не виден (белый текст на прозрачном поверх тёмного `--bg` читается
   нормально), в светлой — белый текст на прозрачном поверх кремового
   `--bg` (#f3f1ec) визуально исчезает (замер PIL: ~1:1). Базовое правило
   `.hm-chip` не трогаем (менять его — красить дефолтную тёмную тему,
   которая сейчас случайно выглядит верно), фикс — только тут, теми же
   переменными, что уже красят соседний `.filterbar .chip` (см. её
   блок, «фильтры» выше по файлу): `--surface-2`/`--text-2` — готовая,
   уже подобранная под обе темы пара поверхность/текст неактивной
   пилюли, `.hm-chip.is-active` (--ink/--bg) не трогаем — активная
   пилюля уже тем-корректна (те же алиасы, что --accent/--on-accent).
   `:not(.is-active)` тут ОБЯЗАТЕЛЕН, не декоративный уточнитель: голый
   `body.light .hm-chip` (тег `body` + 2 класса, 0,0,2,1) специфичнее
   `.hm-chip.is-active` (2 класса, 0,0,2,0) и перекрашивал бы активную
   пилюлю тоже, невзирая на порядок правил в файле — живая проверка
   поймала именно это (PIL-замер после первого прохода). */
body.light .hm-chip:not(.is-active) {
  background: var(--surface-2);
  color: var(--text-2);
}
/* Ревью v12, P1 (§1): .sc-field лежит ровно на силуэте гор (.sc-scene),
   который на этой высоте проступает через маску на ~36% — прозрачное
   стекло (та же rgba(0,0,0,.07), что у остальных) не даёт устойчивой
   границы, плейсхолдер местами теряется на сером силуэте (тёмная тема
   так не страдает — фон там однороднее). Непрозрачная поверхность
   вместо стекла — только тут и только в светлой теме. */
body.light .sc-field {
  background: var(--surface); border: 1px solid var(--border);
  backdrop-filter: none; -webkit-backdrop-filter: none;
}
body.light .tabbar { border-color: rgba(0, 0, 0, .1); }
body.light .tab.is-active { background: rgba(0, 0, 0, .08); }

/* RouteDash вне фото (.rh-dot/.rh-path — «Выдача»/«Билет», ref-06: код—код
   с пунктиром; на «Маршруте»/«Поиске» тот же язык лежит на сцене и сюда
   не входит) — точка/пунктир были белыми фиксированно, на светлом фоне
   становятся ink-based тем же приёмом, что --mute/--dim выше. Самолётик
   (--lilac) — НЕ трогаем: единственный цветной акцент одинаков в обеих
   темах (промпт, «Фон и цвета»). */
body.light .rh-dot { background: rgba(20, 21, 28, .5); }
body.light .rh-path::before { border-top-color: rgba(0, 0, 0, .35); }
body.light .sc-dash-line { border-top-color: rgba(0, 0, 0, .28); }
/* Диагностика v12 этап 2 п.6: тот же .28 на светлом фоне читается хуже
   (ink-текст низкой прозрачности против светлой поверхности) — чуть
   плотнее, чем в тёмной теме. Ревью v16, №11: .32 всё ещё держал только
   2.04:1 в центре окна (см. основной комментарий у .sc-airports,
   app.css) — поднято до .58 (~4.28:1, формула WCAG к #f3f1ec), тот же
   принцип, что и AW_FADE_LIGHT в app.js — светлой теме нужна более
   высокая ступень непрозрачности для того же ощущаемого контраста. */
body.light .sc-airports-track span { opacity: .58; }
/* Тумблер «Уведомления» (Профиль) во «включённом» состоянии — та же
   ink-based альфа .28, что и .rh-dot выше. */
body.light .list-row.is-on .switch { background: rgba(20, 21, 28, .28); }

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
[hidden] { display: none !important; }
html, body { overscroll-behavior-y: none; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 400 var(--f-base)/1.4 -apple-system, BlinkMacSystemFont, "SF Pro Text",
        "Segoe UI", Roboto, system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
}

button { font: inherit; color: inherit; border: 0; background: none; cursor: pointer;
  touch-action: manipulation; }
/* Единый стиль иконок проекта: stroke 1.75 (план v8, §1) — раньше 1.9. */
svg { fill: none; stroke: currentColor; stroke-width: var(--icon-stroke); stroke-linecap: round; stroke-linejoin: round; }

main { padding: 18px var(--pad) calc(var(--tabbar-h) + var(--safe-b) + 20px); }
/* Переходы между экранами — 220ms ease-out (план v8, §4). */
.panel { display: none; animation: fade .22s ease-out both; }
.panel.is-active { display: block; }
@keyframes fade { from { opacity: 0; transform: translateY(5px); } }

/* Заголовок экрана (план v8, §1): именованная шкала брифа — display
   32/38/700, крупнее и контрастнее подписи под ним. Больше воздуха
   снизу — старые 14px впритык к контенту читались как «топорно». */
.head { margin: 0 2px 20px; }
.head h1 {
  font-size: var(--type-display-size); line-height: var(--type-display-lh);
  font-weight: var(--type-display-w); letter-spacing: -.5px; margin: 0 0 4px;
}
.head p { color: var(--text-2); font-size: var(--f-md); margin: 0; }
/* «Жирный + светлый» запрещён во всём приложении (план v8, §1: «§1
   прямо запрещает» — заголовок целиком одним весом). Раньше .lt красил
   половину заголовка приглушённым весом/цветом; сами <span class="lt">
   убраны из шаблонов §1/§2.1/§2.2/§2.7 этой волны (index.html, app.js:
   paintGreeting/recentRoutesHtml), .lt тут обнулён на inherit как
   защита для мест, куда эта волна не заходит (§2.3-2.6). */
.lt { font-weight: inherit; color: inherit; }

/* ═══ v3 базовые компоненты (design/flightdeals-v3-reference.html,
   классы перенесены дословно) ══════════════════════════════════════════
   .glass/.pill/.ic — общий стеклянный/пилюльный язык эталона: стеклянная
   круглая иконка (.ic, обычно вместе с .glass — `<span class="ic glass">`),
   белая пилюля основного действия (.pill), полупрозрачная стеклянная
   подложка бейджей/полей (.glass сама по себе). */
.glass {
  background: rgba(255, 255, 255, .12);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid rgba(255, 255, 255, .14);
}
/* Ревью v12, P2: .glass на плоском --bg/--surface (не над фото) держит
   1.34:1 — размывать нечего, грань кнопки/пилюли читается как невидимый
   прямоугольник (замерено на #dl-mode-toggle/.preset). Радар-toggle в
   подвале ОБЫЧНОГО шита (.sheet-foot .rt-radar — «Рейс»/«Даты и цены») —
   та же дыра; тот же класс НАД фото («Маршрут»/«Сделка») не задет, эти
   контексты живут в другом месте DOM (.rt-cta-row, не .sheet-foot). */
#dl-mode-toggle, .preset, .sheet-foot .rt-radar, #profile-gear {
  border-color: rgba(255, 255, 255, .22);
}
/* Аудит 2026-09-05 (владелец: радар меняет цвет неправильно по темам) —
   комментарий двумя правилами выше сам называл `.sheet-foot .rt-radar`
   («Рейс»/«Даты и цены») жертвой «той же дыры», что у #dl-mode-toggle/
   .preset, но в их body.light-инверсию (см. блок «Стекло НЕ над фото»
   выше по файлу) кнопку так и не добавили — офф-состояние оставалось
   белым стеклом (`.glass`, rgba(255,255,255,.12)) НА светлом `--surface`
   подвала шита: рамка выше это чинит, а вот заливка — нет, живой
   Playwright-скрин (R-light-fd-radar-off.png) поймал полностью пропавшую
   иконку. `:not(.is-on)` обязателен — `.rt-radar.is-on` красит свою
   заливку токеном ниже по файлу (var(--ink), тем-корректно САМ по себе,
   его трогать не нужно), а `body.light .sheet-foot .rt-radar` (0,0,3,1)
   специфичнее `.sheet-foot .rt-radar.is-on` (0,0,3,0) и молча забил бы
   заливку ON-состояния тоже — тот же приём, что уже документирован у
   `.hm-chip:not(.is-active)` (см. её комментарий выше по файлу). */
body.light .sheet-foot .rt-radar:not(.is-on) {
  background: rgba(0, 0, 0, .07);
  border-color: rgba(0, 0, 0, .1);
}
.pill {
  display: flex; align-items: center; justify-content: center;
  height: var(--btn-h); border-radius: var(--r-pill);
  background: var(--ink); color: var(--bg);
  font-size: 15px; font-weight: 500;
  transition: transform .12s ease-out;
}
.pill:active { transform: scale(.98); }
.pill[disabled] { opacity: .4; }
/* Ревью v12, P1: было 42×42, порог хит-зоны 44 — тут разница
   несущественная визуально, проще поднять сам размер, чем городить
   ::after (см. компактные бейджи/кружки ниже по файлу, где визуальный
   размер трогать нельзя и hit-área расширена невидимым слоем). */
.ic {
  width: 44px; height: 44px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
}
.ic svg { width: 18px; height: 18px; stroke: var(--ink); fill: none; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }

/* ═══ v3 процедурные сцены городов (эталон, дословно) ═══════════════════
   Заглушка вместо серого прямоугольника, пока нет своего фото направления
   (промпт, «Фото»): градиент неба + силуэт хребта/крыш + зерно + виньетка.
   4 сцены эталона — kgd (Калининград, закат)/sea (побережье)/mtn (горы,
   ночь)/ist (Стамбул, минареты) — выбор детерминирован по hash(IATA)
   (sceneForCity() в app.js), сама сцена рисуется генератором sceneHtml(). */
.scene { position: absolute; inset: 0; overflow: hidden; }
.scene .sky { position: absolute; inset: 0; }
.scene .sun { position: absolute; border-radius: 50%; filter: blur(22px); }
.scene .hill { position: absolute; left: -10%; right: -10%; bottom: 0; }
.scene .grain { position: absolute; inset: 0; opacity: .14; mix-blend-mode: overlay; background-image: var(--grain-svg); }
.scene .vig { position: absolute; inset: 0; background: var(--vignette); }
.scene .fade { position: absolute; left: 0; right: 0; bottom: 0; height: 62%; background: linear-gradient(180deg, rgba(14, 15, 19, 0), rgba(14, 15, 19, .75) 55%, rgba(14, 15, 19, .98)); }
/* Калининград, закат */
.kgd .sky { background: linear-gradient(180deg, #1b1f2c 0%, #3b3a48 38%, #8a6a5a 68%, #c9906a 84%, #5a4a44 100%); }
.kgd .sun { width: 220px; height: 220px; left: 45%; bottom: 22%; background: rgba(255, 190, 130, .55); }
.kgd .h1 { height: 44%; background: #1a1a20; clip-path: polygon(0 100%, 0 62%, 8% 60%, 14% 64%, 22% 58%, 30% 60%, 36% 56%, 40% 58%, 44% 18%, 46% 10%, 48% 18%, 50% 56%, 58% 54%, 64% 50%, 66% 52%, 70% 46%, 72% 48%, 78% 52%, 86% 50%, 94% 56%, 100% 54%, 100% 100%); }
.kgd .h2 { height: 24%; background: #101014; clip-path: polygon(0 100%, 0 70%, 10% 72%, 18% 60%, 26% 64%, 34% 55%, 42% 62%, 55% 58%, 60% 40%, 62% 44%, 66% 60%, 78% 56%, 90% 62%, 100% 58%, 100% 100%); }
/* Сочи, море */
.sea .sky { background: linear-gradient(180deg, #0f1a2a 0%, #213b55 40%, #5d7a86 60%, #d8a06a 72%, #1c3340 73%, #0d1c26 100%); }
.sea .sun { width: 180px; height: 180px; left: 52%; bottom: 27%; background: rgba(255, 200, 140, .5); }
.sea .h1 { height: 18%; background: #0a1319; clip-path: polygon(0 100%, 0 40%, 12% 44%, 22% 30%, 30% 38%, 40% 34%, 48% 42%, 60% 36%, 70% 44%, 82% 38%, 92% 46%, 100% 42%, 100% 100%); }
.sea .h2 { height: 30%; background: linear-gradient(180deg, rgba(255, 200, 140, .12), transparent 60%); clip-path: polygon(40% 0, 60% 0, 70% 100%, 30% 100%); }
/* Горы, ночь */
.mtn .sky { background: linear-gradient(180deg, #070a14 0%, #141c33 50%, #2b3450 80%, #3c4258 100%); }
.mtn .sun { width: 56px; height: 56px; left: 70%; top: 15%; background: rgba(225, 232, 255, .7); filter: blur(4px); box-shadow: 0 0 60px 30px rgba(180, 195, 240, .18); }
.mtn .h1 { height: 52%; background: #1d2338; clip-path: polygon(0 100%, 0 80%, 12% 62%, 22% 70%, 34% 48%, 42% 56%, 50% 30%, 56% 40%, 62% 34%, 72% 58%, 80% 50%, 90% 66%, 100% 60%, 100% 100%); }
.mtn .h2 { height: 36%; background: #0d111d; clip-path: polygon(0 100%, 0 60%, 10% 66%, 20% 52%, 30% 60%, 38% 50%, 48% 62%, 58% 56%, 66% 64%, 74% 54%, 86% 62%, 100% 56%, 100% 100%); }
.mtn .snow { position: absolute; left: -10%; right: -10%; bottom: 0; height: 52%; background: rgba(220, 228, 245, .35); clip-path: polygon(46% 42%, 50% 30%, 54% 36%, 56% 40%, 58% 46%, 52% 43%, 48% 46%, 44% 44%); }
/* Стамбул */
.ist .sky { background: linear-gradient(180deg, #1a1230 0%, #3d2a4e 40%, #8a4a52 66%, #d9855a 82%, #3a2a2a 100%); }
.ist .sun { width: 240px; height: 240px; left: 20%; bottom: 16%; background: rgba(255, 170, 110, .5); }
.ist .h1 { height: 40%; background: #141016; clip-path: polygon(0 100%, 0 70%, 6% 70%, 7% 20%, 8% 20%, 9% 70%, 20% 72%, 26% 62%, 30% 54%, 36% 50%, 42% 52%, 48% 60%, 52% 62%, 60% 62%, 61% 24%, 62% 24%, 63% 62%, 76% 64%, 84% 60%, 92% 66%, 100% 62%, 100% 100%); }
.ist .h2 { height: 22%; background: #0b090c; clip-path: polygon(0 100%, 0 60%, 15% 64%, 25% 56%, 40% 62%, 55% 58%, 70% 64%, 85% 58%, 100% 62%, 100% 100%); }

/* ═══ v10 план §4/§2: дневная палитра сцен (body.light) ═══════════════
   Каждая из 4 сцен выше получала только ночную/сумеречную палитру
   (промпт v9 — «фото тёмные, атмосферные»); v10 добавляет вторую,
   дневную, для body.light — тот же силуэт (clip-path не трогаем, форма
   гор/крыш/минаретов не меняется), только небо/солнце светлее и холоднее
   у зенита, силуэты на 2-3 ступени светлее графита, чтобы не читались
   как «та же ночь с фильтром». sceneForCity()/sceneHtml() (app.js)
   по-прежнему выбирают ТОЛЬКО силуэт (kind) по hash(IATA) — тема тут ни
   при чём, палитру дня/ночи применяет каскад body.light, поэтому смена
   темы Telegram (themeChanged → applyTheme() → classList.toggle('light'))
   перекрашивает уже нарисованные сцены сама, без повторного рендера
   разметки. .fade/.vig (нижнее затемнение для читаемости текста) НЕ
   трогаем — тот же принцип, что и «стекло/бейджи над фото не
   инвертируются» ниже по файлу: текст поверх фото лежит на тёмной
   подложке независимо от темы приложения. */
body.light .kgd .sky { background: linear-gradient(180deg, #6fb8e8 0%, #a7d6ee 35%, #dcedf0 62%, #f7e0b6 84%, #eccf98 100%); }
body.light .kgd .sun { background: rgba(255, 246, 210, .85); }
body.light .kgd .h1 { background: #48566a; }
body.light .kgd .h2 { background: #313e50; }

body.light .sea .sky { background: linear-gradient(180deg, #4fa7dc 0%, #74c3e2 32%, #a6dad9 58%, #d3ecd8 78%, #eef6e4 100%); }
body.light .sea .sun { background: rgba(255, 250, 220, .8); }
body.light .sea .h1 { background: #1c3a4a; }
body.light .sea .h2 { background: linear-gradient(180deg, rgba(255, 246, 214, .4), transparent 60%); }

body.light .mtn .sky { background: linear-gradient(180deg, #4f8fdb 0%, #7db4e8 45%, #bfddf0 78%, #e6f1f6 100%); }
body.light .mtn .sun { background: rgba(255, 255, 255, .95); box-shadow: 0 0 60px 30px rgba(255, 255, 255, .3); }
body.light .mtn .h1 { background: #3c4a63; }
body.light .mtn .h2 { background: #26314a; }
body.light .mtn .snow { background: rgba(255, 255, 255, .8); }

body.light .ist .sky { background: linear-gradient(180deg, #5fa6da 0%, #8bc2e0 38%, #cfe4e2 66%, #f2ddac 86%, #e4c384 100%); }
body.light .ist .sun { background: rgba(255, 248, 220, .85); }
body.light .ist .h1 { background: #423a4d; }
body.light .ist .h2 { background: #2d2836; }

/* PhotoCard (промпт, «Фото»): формула затемнения+зерно+виньетка — общая
   для НАСТОЯЩЕГО фото (банк /photos/v1/..., план fix-2026-08-28-photos-
   diag.md) и процедурной сцены выше.
   .ex-scrim уже красится --photo-scrim (унаследовано с v8) — тут та же
   подложка дополняется зерном и виньеткой через псевдоэлементы, разметку
   карточек трогать не нужно. v9 волна B: карусель «Сделки» (.dl-card) и
   экран «Маршрут» (.rt-full) переиспользуют ровно .ex-scrim в разметке —
   отдельных классов для них тут заводить не нужно (волна D: «Сделка»/
   openDealCard тоже — .deal-hero-shade, второй класс этого дуэта, снят
   вместе со старым .ticket, см. ниже). */
.ex-scrim { pointer-events: none; }
.ex-scrim::before {
  content: ""; position: absolute; inset: 0; background: var(--vignette);
}
.ex-scrim::after {
  content: ""; position: absolute; inset: 0;
  opacity: .13; mix-blend-mode: overlay; background-image: var(--grain-svg);
}

/* ─── фильтры ──────────────────────────────────────────────────────── */
/* Горизонтальный скролл с fade-маской справа (план v8, §2.2) — иначе
   непонятно, что чипов больше, чем помещается в экран. -webkit-mask-
   image обязателен: Telegram на iOS — WKWebView, без префикса маска не
   применится вовсе, а не просто выглядит иначе. */
.filterbar {
  display: flex; gap: 8px; overflow-x: auto; scrollbar-width: none;
  margin: 0 calc(var(--pad) * -1) 10px; padding: 2px var(--pad) 6px;
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
  mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
}
.filterbar::-webkit-scrollbar { display: none; }
/* Чипы 36px (план v8, §1): активный = акцентный фон + белый текст,
   неактивный = аутлайн (роадмап §3г, readability-аудит 2026-09-20: было
   заливкой --surface-2, той же, что у десятка других плашек в файле, —
   выключенный фильтр визуально не отличался от статичной карточки, «не
   читается как переключатель»; заливка снята, остаётся только видимая
   обводка — «пусто внутри» vs «сплошная заливка» читается однозначно
   как «выключен» vs «включён», без обучения). border-color — --text-3,
   не --border: тот 0.08 альфы — hairline для разделителей строк списка,
   для контура самостоятельной кнопки этого мало; --text-3 даёт ~4.4-4.5:1
   к --surface в обеих темах (WCAG-формула, tests/js/test-tabs-contrast.js)
   — контур виден, не домысливается. */
.chip {
  /* Ревью v12, P1: --chip-h (36px) ниже порога 44 — min-height поверх
     фиксированной height не меняет токен (он же красит другую
     геометрию по файлу), просто клампит фактическую высоту вверх. */
  flex: none; height: var(--chip-h); min-height: 44px; display: inline-flex; align-items: center;
  padding: 0 14px; border-radius: 999px; white-space: nowrap;
  font-size: var(--f-md); font-weight: 600; color: var(--text-2);
  background: var(--surface); border: 1px solid var(--text-3);
  transition: color .15s ease, background .15s ease, border-color .15s ease, transform .12s ease-out;
}
.chip.is-active {
  color: var(--on-accent); background: var(--accent); border-color: var(--accent);
  /* Аудит UX, P3 (радар-цель, openRadarGoalSheet): живой замер (Playwright,
     computed style + WCAG-формула) на реальных токенах обеих тем даёт
     16-17:1 текст/фон у самого is-active чипа — цветовая инверсия
     (--accent/--on-accent = --ink/--bg, тот же язык, что и у .hm-chip.
     is-active «Все даты», см. её блок выше по файлу) уже отличная, не
     переизобретаем. Жирность — ВТОРОЙ, независимый от цвета канал
     (WCAG 1.4.1 «не только цветом»): «Все даты» полагается на то же
     цветовое правило в одиночку — тут не трогаем её, а чипам цели
     радара (4 похожих по форме пилюли подряд, в отличие от одиночной
     «Все даты» на фоне обычных чипов месяцев) лишний, не завязанный на
     оттенок сигнал не помешает.
  */
  font-weight: 700;
}
.chip:active { transform: scale(.98); }
/* P1.5 («Выдача прогрессивно»): чипы-заглушки, пока не пришли рейсы —
   тот же приём, что .day.nodata/.day.past в календаре (муторно и
   некликабельно, но геометрия на месте, чтобы её не «перевёрстывало»,
   когда придут настоящие интерактивные чипы). */
.chip:disabled { opacity: .45; pointer-events: none; }

.status { color: var(--muted); font-size: var(--f-sm); margin: 0 2px 10px; }
.status:empty { display: none; }
.status.error { color: var(--sale); }
.status a { color: var(--link); font-weight: 600; }
/* P1.5: «Ищем лучшие цены…» — лёгкая анимация вместо голого текста, три
   точки пульсируют по очереди (тот же язык, что «печатает…» в мессенджерах).
   currentColor наследует --muted от .status — отдельный токен не нужен. */
.rs-dots { display: inline-flex; gap: 2px; margin-left: 3px; vertical-align: middle; }
.rs-dots i { width: 4px; height: 4px; border-radius: 50%; background: currentColor; opacity: .25; animation: rs-dot-pulse 1.1s ease-in-out infinite; }
.rs-dots i:nth-child(2) { animation-delay: .15s; }
.rs-dots i:nth-child(3) { animation-delay: .3s; }
@keyframes rs-dot-pulse { 0%, 80%, 100% { opacity: .25; } 40% { opacity: 1; } }

/* ═══ inline-баннер сетевой ошибки (план v8, §4) ═══════════════════════
   Заменяет tg.showAlert для сетевых сбоев (не для подтверждений
   действий, те остаются alert): фиксированная плашка сверху с кнопкой
   «Повторить», не модальный alert поверх приложения. showNetError()/
   hideNetError() в app.js — общий примитив на весь проект; volна A
   подключает его в своей зоне (карта «Куда угодно»), остальные
   showAlert(`Не вышло: …`) в §2.3-2.6 переведёт волна B/C (список в
   DESIGN_PLAN.md). */
.net-banner {
  position: fixed; left: 10px; right: 10px; z-index: 50;
  top: max(env(safe-area-inset-top, 0px), 10px);
  display: flex; align-items: center; gap: 10px;
  padding: 12px 12px 12px 14px; border-radius: var(--r-lg);
  background: var(--surface); box-shadow: var(--shadow-modal);
  border: 1px solid var(--border);
  animation: fade .22s ease-out both;
}
.net-banner-msg { flex: 1; min-width: 0; font-size: var(--f-md); color: var(--text); }
.net-banner button {
  flex: none; height: 32px; padding: 0 14px; border-radius: 999px;
  background: var(--accent); color: var(--on-accent);
  font-size: var(--f-sm); font-weight: 650;
  transition: transform .12s ease-out;
}
.net-banner button:active { transform: scale(.96); }

/* ─── секции: «row-head» ────────────────────────────────────────────
   Приёмка v9, P1-6: единственные потребители — «Минимум по дням»/«Лучшие
   даты» в шторке «Даты и цены» (app.js, openDatesSheet) — промпт §6
   просит капс-подпись (11px/.16em), не заголовок весом 750. Тот же
   стиль, что и .list-group-title (Профиль), text-transform делает капс
   из обычного текста в разметке — сам текст трогать не пришлось. */
.row-head { display: flex; align-items: baseline; gap: 10px; margin: 0 2px 10px; }
.row-head h2 {
  flex: 1; margin: 0; font-size: var(--type-micro-size); line-height: var(--type-micro-lh);
  font-weight: var(--type-micro-w); text-transform: uppercase;
  letter-spacing: var(--type-micro-tracking); color: var(--text-2);
}
.row-head .sub { color: var(--faint); font-size: var(--f-sm); font-weight: 500; }

/* ─── пусто и скелеты ──────────────────────────────────────────────── */
/* EmptyState (приёмка v9, P1-7): промпт запрещает обводки/плашки везде —
   раньше только .rd-empty (Радар) снимал заливку/бордер модификатором,
   остальные .empty (первый запуск без города, «Куда угодно» без
   результатов, сетевые ошибки) оставались обычной карточкой с обводкой.
   Действие — не всегда ghost «Повторить»: у первого запуска это
   единственный путь внутрь приложения, ему язык главной кнопки
   (.empty-cta, пилюля — см. emptyState() в app.js). */
.empty {
  padding: 34px 22px; text-align: center; color: var(--text-2);
  background: none; border: 0; border-radius: var(--r-lg);
}
.empty svg { width: 34px; height: 34px; color: var(--text-3); margin-bottom: 10px; }
.empty b { display: block; font-size: var(--f-lg); font-weight: 300; color: var(--text); margin-bottom: 4px; letter-spacing: -.02em; }
.empty p { margin: 0; font-size: var(--f-md); line-height: 1.45; }
.empty a { color: var(--accent-text); }
.empty-cta { width: 200px; margin: 18px auto 0; }
.empty-retry {
  display: inline-flex; align-items: center; justify-content: center;
  margin-top: 14px; height: var(--btn-h-sm); padding: 0 20px; border-radius: 999px;
  background: none; border: 1.5px solid var(--border); color: var(--accent-text);
  font-size: var(--f-md); font-weight: 650;
  transition: transform .12s ease-out, border-color .12s ease-out;
}
.empty-retry:active { transform: scale(.98); border-color: var(--accent); }

.skeleton {
  height: 132px; border-radius: var(--r-lg);
  background: var(--skeleton); background-size: 400% 100%;
  animation: shimmer 1.3s ease-in-out infinite;
}
.skeleton.tall { height: auto; aspect-ratio: 4 / 5; }
@keyframes shimmer { from { background-position: 100% 0; } to { background-position: -100% 0; } }
/* Скелетон фото города (план v8, §3): не отдельный оверлей-блок, а
   background САМОГО <img> — пока файл не декодирован, браузер рисует
   фон элемента вместо картинки, так что шиммер виден без лишней разметки;
   снимается классом при onload (app.js, все фото-цепочки). */
.img-loading { background: var(--skeleton); background-size: 400% 100%; animation: shimmer 1.3s ease-in-out infinite; }

/* ─── нижняя навигация: FloatingTabBar (промпт, «Кнопки и элементы» +
   эталон .nav) ══════════════════════════════════════════════════════════
   Плавающая стеклянная пилюля, 5 кнопок — главная/поиск/находки/радар/
   профиль (v10 план §5: Поиск первый и стартовый, было молния/лупа в v9 —
   index.html меняет порядок кнопок).
   Роадмап §5а (readability-аудит, 2026-09-20): подписи вкладок раньше
   уезжали в sr-only (эталон рисовал таббар вовсе без подписей) — «иконки
   без слов не считываются с одного взгляда», владелец попросил подписи
   ГЛАЗАМИ, не только скринридеру. Бар подрос 60→64px, .tab 44→52px (запас
   под вторую строку текста под иконкой) — --tabbar-h (:root выше) сдвинут
   на ту же дельту (116→120), иначе паддинги контента (main{}/#home-view
   ниже по файлу) начали бы утыкаться в подросший бар. */
.tabbar {
  /* max(--safe-b, --tg-content-safe-area-inset-bottom) — унаследовано с
     v8: обычный env() safe-area устройства ИЛИ, если больше, инсет,
     который для клиентов Bot API 8+ прокидывает сам telegram-web-app.js. */
  position: fixed; left: 50%; z-index: 30;
  bottom: calc(28px + max(var(--safe-b), var(--tg-content-safe-area-inset-bottom, 0px)));
  transform: translateX(-50%);
  display: flex; align-items: center; gap: 6px;
  height: 64px; padding: 0 10px; border-radius: 30px;
  background: var(--nav-bg); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
  border: 1px solid rgba(255, 255, 255, .14);
  /* Приёмка v9, P2-2: эталонный .nav — без тени, separation держат только
     стекло/обводка. */
}
.tab {
  width: 52px; height: 52px; flex: none;
  display: flex; align-items: center; justify-content: center;
  border-radius: 22px; color: var(--mute);
  transition: transform .12s ease-out, background .16s ease, color .16s ease;
}
.tab:active { transform: scale(.94); }
/* Активная — светлый кружок за иконкой+подписью (промпт), не акцентная
   заливка текста: лиловый акцент вне самолётика маршрута запрещён («Фон
   и цвета») — весь сигнал «выбрано» несёт инверсия --mute→--ink, общая
   для иконки И подписи (наследование, см. .tab-stack span ниже). */
.tab.is-active { background: rgba(255, 255, 255, .14); color: var(--ink); }
.tab-stack { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; }
.tab svg { width: 22px; height: 22px; stroke-width: 1.5; transition: transform .18s cubic-bezier(.32, .72, 0, 1); }
.tab.is-active svg { transform: scale(1.06); }
/* Роадмап §5а: подпись под иконкой, 10px — верх диапазона брифа (10-11px)
   отдан самой иконке, снизу подпись мельче, чтобы обе строки не спорили
   за внимание. Цвет не задаём явно — наследует currentColor от .tab/
   .tab.is-active выше: тот же переключатель --mute/--ink, что и у иконки,
   одним каналом, а не второй параллельной парой правил, которая могла бы
   рассинхронизироваться с ней при следующей правке. */
.tab-stack span {
  font-size: 10px; line-height: 12px; font-weight: 400; white-space: nowrap;
}
/* Ревью v8, P1-8: role=tablist явно перекрывал неявный landmark
   navigation у <nav class="tabbar"> (axe: region, moderate ×4) — роль
   переехала на обёртку внутри; display:contents убирает её из потока
   раскладки, .tabbar остаётся тем же flex-контейнером для кнопок. */
.tabbar-tabs { display: contents; }

/* ─── боттом-шит ───────────────────────────────────────────────────── */
/* v10 план §4: blur скрима — затемнение (.4 альфы в --overlay) не менялось,
   добавлен только backdrop-filter, чтобы контент ПОД шитом размывался, не
   просто темнел. */
.scrim {
  position: fixed; inset: 0; z-index: 40; background: var(--overlay);
  backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);
  animation: fade .22s ease-out both;
}
/* Открытие bottom sheet — 220ms ease-out (план v8, §4). */
.sheet {
  position: fixed; left: 0; right: 0; bottom: 0; top: 8vh; z-index: 41;
  display: flex; flex-direction: column;
  background: var(--bg); border-radius: var(--r-xl) var(--r-xl) 0 0;
  box-shadow: var(--shadow-modal);
  animation: slide-up .22s cubic-bezier(.32, .72, 0, 1) both;
}
.sheet.closing { animation: slide-down .2s ease-out both; }
@keyframes slide-up { from { transform: translateY(100%); } }
@keyframes slide-down { to { transform: translateY(100%); } }

.sheet-head { position: relative; flex: none; padding: 17px 52px 10px var(--pad); }
.grabber {
  position: absolute; left: 50%; top: 7px; transform: translateX(-50%);
  width: 36px; height: 4px; border-radius: 2px; background: var(--line);
}
/* Приёмка v9, P1-6: 700 — жирный заголовок, промпт запрещает 600-700 во
   всех заголовках без исключения («Даты и цены», «Выдача», «Фильтры»,
   «Города вылета» — все проходят через один .sheet-head h3). */
.sheet-head h3 { margin: 0; font-size: var(--f-lg); font-weight: 400; letter-spacing: -.02em; }
/* Ревью v12, P1: 32×32 визуально — язык компактного крестика в шапке
   шита эталонный, трогать нельзя; ::after — невидимый инсет -6px даёт
   44×44 хит-зоны, не увеличивая сам кружок (тот же приём — у всех
   компактных круглых кнопок ниже по файлу). */
.sheet-close {
  position: absolute; right: 10px; top: 13px; width: 32px; height: 32px;
  border-radius: 50%; background: var(--surface-2); color: var(--muted);
  display: grid; place-items: center;
}
.sheet-close::after { content: ''; position: absolute; inset: -6px; }
.sheet-close svg { width: 15px; height: 15px; }
/* Отступ снизу = safe-area inset + 16px (план v8, §2.6), а не плоские
   18px — на устройстве с home-indicator последняя строка списка иначе
   утыкается прямо в него. Не конфликтует со sticky .sheet-foot ниже:
   это независимый flex-сиблинг, а не оверлей поверх body, лишний
   паддинг просто чуть увеличивает воздух в конце скролла. */
.sheet-body { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: 0 var(--pad) calc(var(--safe-b) + 16px);
  overscroll-behavior: contain; }
.sheet-foot {
  /* Ревью v8, P2-3: было safe+10, §2.6 просит safe+16 (как .sheet-body). */
  flex: none; padding: 10px var(--pad) calc(16px + var(--safe-b));
  background: var(--surface); box-shadow: 0 -1px 0 var(--line);
}
/* CTA внутри .sheet-foot (план v8, §2.3/§2.4: «Следить за маршрутом»,
   выбор дня в календаре) — sticky и safe-area тут уже даёт сам подвал
   (флекс-сиблинг .sheet-body, а не оверлей — контент под ним никогда не
   перекрыт), тут только сама кнопка. primary — акцент, как .search-submit;
   secondary — состояние «уже подписан», не альтернативное действие. */
/* Ревью v9, P2-6: 750 расходился с языком главной кнопки (.pill — 15px/
   500 дословно по эталону) — тот же CTA-язык, просто квадратные, а не
   круглые углы у .sheet-cta оправдания под другой вес не дают. */
.sheet-cta {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: var(--btn-h); border-radius: 999px;
  background: var(--accent); color: var(--on-accent);
  font-size: var(--f-base); font-weight: 500; letter-spacing: -.01em;
  transition: transform .12s ease-out, opacity .12s ease-out;
}
.sheet-cta:active { transform: scale(.98); }
.sheet-cta[disabled] { background: var(--surface-2); color: var(--text-2); opacity: .4; }
.sheet-cta-secondary { background: var(--surface-2); color: var(--text); }
.sheet-cta-secondary[disabled] { opacity: 1; }
/* Ревью v12, P1: h37 (padding 12+текст) ниже 44 — display:flex вместо
   block, иначе min-height добавил бы воздух только СНИЗУ строки текста
   (block не центрирует контент по высоте), а не поровну. */
.ghost {
  display: flex; align-items: center; justify-content: center;
  width: 100%; min-height: 44px; padding: 12px; margin-top: 2px; text-align: center;
  color: var(--muted); font-size: var(--f-md); font-weight: 600;
}

/* ─── поиск города ─────────────────────────────────────────────────── */
.searchbox {
  position: relative; display: flex; align-items: center; gap: 8px;
  background: var(--surface); border-radius: var(--r-md); box-shadow: var(--shadow-sm);
  padding: 0 13px; margin-bottom: 4px;
}
.searchbox svg { width: 18px; height: 18px; color: var(--faint); flex: none; }
.searchbox input {
  flex: 1; min-width: 0; border: 0; background: none; outline: none; color: inherit;
  font: inherit; font-size: 16px; padding: 13px 0;
  /* iOS WebKit рисует у type=search СВОЮ прямоугольную рамку при фокусе
     (скрин владельца 03.09: «белый квадрат» вокруг поля, в светлой теме
     чёрный) — outline:none её не снимает, только сброс appearance. */
  -webkit-appearance: none; appearance: none;
  -webkit-tap-highlight-color: transparent;
  caret-color: var(--accent);
}
/* Страховка от любых фокус-колец на самом инпуте (box-shadow тоже!):
   рамку рисовало глобальное :focus-visible-кольцо, см. блок в конце файла.
   Отдельного кольца на капсуле НЕ надо: --accent==--ink (чёрный) — второй
   чёрный контур и был «рамкой» на скрине 02:01. */
.searchbox input:focus, .searchbox input:focus-visible { outline: none; box-shadow: none; }
.city-group {
  font-size: var(--f-sm); font-weight: 700; color: var(--faint);
  text-transform: uppercase; letter-spacing: .05em; margin: 14px 4px 7px;
}
.city-list { background: var(--surface); border-radius: var(--r-md); overflow: hidden; box-shadow: var(--shadow-sm); }
.city { display: flex; align-items: center; gap: 11px; width: 100%; padding: 12px 13px; text-align: left; }
.city + .city { box-shadow: inset 0 1px 0 var(--line); }
.city:active { background: var(--surface-2); }
.city .pin { width: 24px; height: 24px; color: var(--faint); flex: none; } /* план v8 §1, волна C: 24px как остальные иконки */
.city .txt { flex: 1; min-width: 0; }
.city .name {
  display: block; font-size: var(--f-base); font-weight: 600; letter-spacing: -.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* P0.2 §г: совпавший префикс жирнее самого имени (600 → 800) — только
   вес, без цвета/капса (KISS с остальной ч/б иерархией .code рядом). */
.city .name b { font-weight: 800; }
.city .country { display: block; font-size: var(--f-sm); color: var(--muted); }
.city .code {
  flex: none; font-size: var(--f-sm); font-weight: 800; letter-spacing: .05em; color: var(--faint);
  background: var(--surface-2); border-radius: 6px; padding: 4px 7px;
}
/* Крестик удаления «Недавних» + свайп влево (задача владельца п.6,
   11.09.2026 → переправлено тем же днём: «Недавние» пилюлями с зазорами
   отдельно от «Популярных» — владелец прямо просит ОДИН групповой
   контейнер на оба списка, тот же стиль строки). cityRows() (app.js) с
   removeKind оборачивает КАЖДУЮ строку в свой .swipe-wrap — это остаётся
   (нужен независимый transform при свайпе ОДНОЙ строки), но сам
   .swipe-wrap больше не несёт свою заливку/тень/радиус/отступ — та
   версия и давала вид «отдельных пилюль». Теперь `.city-list.is-
   removable` красится ровно как база `.city-list` (правило выше,
   ничего не переопределяем — заливка/радиус/тень общие на «Недавние» и
   «Популярные»), а внутри — плоские строки с divider'ом от соседа снизу,
   тот же приём, что `.list-row + .list-row` (Профиль, её блок ниже по
   файлу) и `.city + .city` у Популярных чуть выше. .city внутри не
   теряет свой облик (флаг/имя/код, hover) — просто делит строку с
   крестиком (flex:1), а не занимает её целиком. */
.city-list.is-removable .swipe-wrap { border-radius: 0; }
.city-list.is-removable .swipe-wrap + .swipe-wrap { margin-top: 0; }
.city-list.is-removable .swipe-wrap + .swipe-wrap .city-recent {
  box-shadow: inset 0 1px 0 var(--line);
}
.city-recent { display: flex; align-items: center; background: var(--surface); }
.city-recent .city { flex: 1; min-width: 0; }
.city-recent .lr-viewed-close { margin: 0 10px; }

/* ─── шит-выбор из списка: «Бюджет»/«География»/«Когда» ──────────────── */
.opt-list { background: var(--surface); border-radius: var(--r-md); overflow: hidden; box-shadow: var(--shadow-sm); }
.opt { display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; padding: 13px 14px; text-align: left; font-size: var(--f-base); font-weight: 600; }
.opt + .opt { box-shadow: inset 0 1px 0 var(--line); }
.opt:active { background: var(--surface-2); }
.opt.is-active { color: var(--link); }
.opt-check { color: var(--link); font-weight: 800; }

/* ─── календарь ────────────────────────────────────────────────────── */
/* v10 план §3а → P2 «календарь-диапазон»: подпись шага (.cal-step —
   .scr-cap как есть, тут только flex под соседство) + вторичная
   «Пропустить» — только у нового потока поиска (openSearchDatesFlow,
   app.js передаёт их через extra.step/extra.skip), режим просмотра
   календаря их не рисует вовсе. Вопрос «Обратный билет нужен?» (v11 план
   §1, .cal-return-*) снесён вместе со вторым календарём «Обратно» — CTA
   после тапа теперь панель .cal-cta-panel (ниже), решает диапазон в этом
   же календаре. */
.cal-flow-top { display: flex; align-items: center; gap: 10px; margin: 0 0 14px; }
.cal-flow-top .cal-step { flex: 1; }
/* Ревью v12: h33 → min-height 44 (родитель .cal-flow-top уже
   align-items:center, добавка просто делает строку выше). Цвет/вес —
   P2: --accent-text (=--ink, чистый белый/чёрный) весом 600 читался
   заметно сильнее подписи шага рядом, хотя промпт зовёт «Пропустить»
   вторичным действием. */
.cal-skip { flex: none; min-height: 44px; padding: 8px 4px; font-size: var(--f-sm); font-weight: 400; color: var(--text-2); display: flex; align-items: center; }
/* Подвал календаря после тапа №1 (P2 «календарь-диапазон», задача §3,
   заменяет снесённый .cal-return-ask): primary «Найти на …» (одна
   сторона, работает сразу) + мелкий hint про вторую дату — тап №2
   заменяет всю панель на одну кнопку-диапазон (обычный .sheet-cta,
   отдельного класса не требует). */
.cal-cta-panel { display: flex; flex-direction: column; gap: 8px; }
.cal-cta-hint {
  margin: 0; font-size: var(--type-caption-size); line-height: var(--type-caption-lh);
  color: var(--faint); text-align: center;
}
/* Caption, не по центру (план v8, §2.4) — было text-align:center/f-xs. */
.cal-hint {
  font-size: var(--type-caption-size); line-height: var(--type-caption-lh);
  color: var(--faint); text-align: left; margin: 0 0 8px;
}
.cal-week {
  position: sticky; top: 0; z-index: 2;
  display: grid; grid-template-columns: repeat(7, 1fr);
  padding: 7px 0; background: var(--page);
  font-size: var(--f-xs); font-weight: 700; color: var(--faint); text-align: center;
}
/* Ревью v8, P1-2: было var(--sale) — второй, «не про цену» красный на
   экране, где красный уже занят легендой «дороже» (--price-high). Разница
   сб/вс — весом, не цветом. */
.cal-week span:nth-child(n+6) { color: var(--text-2); font-weight: 700; }
/* Роадмап §3б (search-аудит 2026-09-20): подпись «что значит цена в
   клетке» — до этой правки heat-map красил цветом дешевле/дороже, но
   нигде не пояснял, ЗА ЧТО именно эта цена (за одного? туда-обратно или
   в одну сторону?). Текст ставит openCalendar (app.js, ~4680) по
   askReturn — та же пара, что и у тумблера формы поиска (§3а). Отдельный
   класс от .cal-legend ниже: эта строка про СМЫСЛ числа, легенда — про
   ЦВЕТ, но обе — часть одного визуального блока (тот же кегль/цвет). */
.cal-legend-caption {
  margin: 0; padding-top: 4px; font-size: var(--f-xs); color: var(--text-2); text-align: center;
}
/* Легенда heat-map (план v8, §2.4) — одной строкой под шапкой дней
   недели, те же три семантических тона, что красят ячейки ниже. */
.cal-legend {
  display: flex; align-items: center; justify-content: center; gap: 14px;
  padding: 6px 0 10px; font-size: var(--f-xs); color: var(--text-2);
}
.cal-legend-dot {
  display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  margin-right: 4px; background: var(--price-neutral);
}
.cal-legend-dot.cheap { background: var(--price-good); }
.cal-legend-dot.dear { background: var(--price-high); }
/* Заголовки месяцев — sticky при скролле (план v8, §2.4), докованные
   сразу под .cal-week (top: высота шапки дней недели). margin→padding:
   иначе сквозь margin сверху при скролле «просвечивают» дни соседнего
   месяца — sticky-элемент красит фон только там, где у него box, не
   там, где раньше был margin. */
.cal-month h4 {
  position: sticky; top: 30px; z-index: 1;
  margin: 0; padding: 12px 4px 6px; background: var(--page);
  font-size: var(--f-base); font-weight: 700;
  letter-spacing: -.01em; text-transform: capitalize;
}
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px 0; }
.cal-grid > span { height: 48px; }

/* День с ценой: число сверху, цена мелким под ним. Ради этого календарь
   и открывают — иначе даты выбираются вслепую. */
.day {
  position: relative; height: 48px; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 2px;
  font-size: var(--f-base); font-weight: 600; border-radius: 11px;
}
/* Сб/вс в heat-map календаре (аудит UX, P3) — только жирнее число, цвет
   heat-шкалы (cheap/neutral/dear ниже) не трогаем, он несёт смысл цены,
   не дня недели. .day .p (цена) держит свой явный font-weight:600 ниже —
   этот бы всё равно её не тронул. 800 — то же значение, что уже стоит
   на паре других крупных цифр приложения (не изобретаем свой вес). */
.day.is-weekend { font-weight: 800; }
/* Heat-map ячеек (план v8, §2.4), приёмка v9 P2-11: промпт разрешает
   семантику цветом «только в бейджах и цифрах, не в фонах» — красный/
   зелёный фон ячейки как раз то, что запрещено. Фон теперь один и тот
   же нейтральный тинт у всех трёх состояний (тот же токен, что раньше
   был только у .day.neutral); дёшево/дорого видно по цвету цифр — и
   числа дня, и цены под ним (.p), обе уже разрешённые промптом «цифры».
   Порядок ПЕРЕД .day.edge ниже по-прежнему обязателен: акцентный фон/
   цвет выбранного дня должен побеждать тинт и цвет цифр дешёвой/дорогой
   даты, а не наоборот. */
.day.cheap, .day.neutral, .day.dear {
  background: color-mix(in srgb, var(--price-neutral) 12%, transparent);
}
.day.cheap { color: var(--price-good); }
.day.dear { color: var(--price-high); }
/* P2 «календарь-диапазон» (задача §3): подсветка дней МЕЖДУ .start и
   .end (app.js paintRangeShading) — своя, более сильная заливка ПОСЛЕ
   heat-тинта выше (побеждает cheap/neutral/dear по фону), но ДО .day.p
   цвета цифр ниже не участвует — cheap/dear текст остаётся читаемым
   поверх подсветки диапазона (два разных свойства, background/color, не
   конфликтуют). */
.day.in-range { background: color-mix(in srgb, var(--brand) 16%, transparent); }
.day .p { font-size: 10px; font-weight: 600; color: var(--price-neutral); line-height: 1; }
.day.cheap .p { color: var(--price-good); }
.day.today::after {
  content: ""; position: absolute; bottom: 4px; left: 50%; transform: translateX(-50%);
  width: 4px; height: 4px; border-radius: 50%; background: var(--brand);
}
/* Ревью v12, P1 (§1): задел под «день A отмечен в календаре дня B» —
   тонкое кольцо, не заливка (заливка — язык .day.edge/.start/.end ниже,
   «выбрано ПРЯМО СЕЙЧАС»). Порядок ДО .day.edge обязателен: активный
   выбор должен визуально победить — то же правило, что уже
   документировано для .day.cheap/.day.dear выше. */
.day.is-depart { box-shadow: inset 0 0 0 1.5px var(--text-2); }
/* P2 «календарь-диапазон» (задача §3): .start/.end — оба конца
   диапазона, тот же сильный акцент, что раньше был только у .edge
   (тап №1 навешивает и .edge, и .start вместе — .start сам по себе был
   «мёртв» в CSS, теперь несёт то же правило явно, не только по имени
   соседа). */
.day.edge, .day.start, .day.end { background: var(--brand); color: var(--on-brand); z-index: 1; }
.day.edge .p, .day.edge.cheap .p,
.day.start .p, .day.start.cheap .p,
.day.end .p, .day.end.cheap .p { color: color-mix(in srgb, var(--on-brand) 82%, transparent); }
.day.edge.today::after, .day.start.today::after, .day.end.today::after { background: var(--on-brand); }
.day.dear .p { color: var(--price-high); }
/* Дни ДО выбранного .start (P2 «календарь-диапазон», задача §2/§3) —
   визуально гаснут КАК .past ниже, но НЕ pointer-events:none: тап по
   любому из них должен пройти (переносит .start на новый день, см.
   app.js calendarDayHandler/setStart) — только .past и .nodata по-
   настоящему недоступны, .before-start — «пока не выбран, но можно». */
.day.before-start { opacity: .32; }
/* Дни дальше 30 суток от выбранного .start (P2, задача §2): источник не
   считает такую пару вылет/возврат (server.py rt_calendar) — честно
   гаснут КАК .past, тут уже по-настоящему недоступны (title/aria с
   объяснением — см. applyRtColors в app.js). */
.day.out-of-window { opacity: .32; pointer-events: none; }
/* «Обратно» в РТ: дни раньше даты «Туда» не продаются — гасим и убираем
   цену/data-price ещё в JS, тут только визуальный «неактивный» вид
   (план v6, §2 — календарь обратного плеча). */
.day.past { opacity: .32; pointer-events: none; }
/* Дни без данных вовсе — --text-3, не кликабельны (план v8, §2.4): клик
   и так гасит делегированный слушатель (.day[data-price] в JS), тут
   только визуальный некликабельный вид, тот же приём, что у .past. */
.day.nodata { color: var(--text-3); pointer-events: none; }
/* P1.4 (ревью v12, P0-2 — не откатывать): баннер отказа календаря —
   ПОВЕРХ уже нарисованной сетки (opener() в app.js), не вместо неё:
   мутная сетка без единой цены сама по себе неотличима от честного «на
   эти даты данных нет», без баннера человек не узнал бы, что источник
   просто не ответил. Не .net-banner (showNetError) — тот гасит себя сам
   через 6с таймером, тут отказ не «мигнуло и прошло»: сетка остаётся без
   цен, пока не «Повторить», баннер обязан жить всё это время. */
.cal-banner {
  display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
  padding: 14px 16px; margin: 0 0 12px; border-radius: var(--r-lg);
  background: var(--surface); box-shadow: var(--shadow-sm); border: 1px solid var(--border);
}
.cal-banner b { font-size: var(--f-md); font-weight: 650; color: var(--text); }
.cal-banner p { margin: 0; font-size: var(--f-sm); color: var(--text-2); }
.cal-banner button {
  align-self: flex-start; margin-top: 2px; height: 32px; padding: 0 16px; border-radius: 999px;
  background: var(--accent); color: var(--on-accent); font-size: var(--f-sm); font-weight: 650;
  transition: transform .12s ease-out;
}
.cal-banner button:active { transform: scale(.96); }

/* ─── профиль ──────────────────────────────────────────────────────── */
/* addToHomeScreen тизер (TOP-12 №4) — тот же язык, что .cal-banner/
   .net-banner выше по файлу (surface-карточка, border+shadow-sm, ОДНА
   акцентная пилюля), только с добавленным вторым, текстовым действием
   «Не сейчас» (hsRenderPrompt(), app.js). #hs-prompt — просто контейнер
   (index.html), сама карточка — .hs-card, которую вставляет/убирает JS. */
.hs-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
  padding: 14px 16px; margin: 0 0 20px; border-radius: var(--r-lg);
  background: var(--surface); box-shadow: var(--shadow-sm); border: 1px solid var(--border);
}
.hs-card b { font-size: var(--f-md); font-weight: 650; color: var(--text); }
.hs-card p { margin: 0; font-size: var(--f-sm); color: var(--text-2); }
.hs-card-actions { display: flex; align-items: center; gap: 16px; margin-top: 4px; }
.hs-add {
  height: 32px; padding: 0 16px; border-radius: 999px;
  background: var(--accent); color: var(--on-accent); font-size: var(--f-sm); font-weight: 650;
  transition: transform .12s ease-out;
}
.hs-add:active { transform: scale(.96); }
.hs-dismiss { height: 32px; padding: 0 4px; color: var(--muted); font-size: var(--f-sm); font-weight: 600; }
/* Благодарность (hsOnAdded) переиспользует .hs-card как контейнер —
   короткая пауза перед скрытием, никаких кнопок/gap уже не нужно. */
.hs-card-thanks { gap: 0; }
.row-item {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  padding: 14px; border-radius: var(--r-lg); background: var(--surface);
  box-shadow: var(--shadow-sm); color: inherit;
}
.row-item + .row-item { margin-top: 8px; }
.row-icon {
  flex: none; width: 40px; height: 40px; border-radius: var(--r-sm);
  display: grid; place-items: center; font-size: 19px; background: var(--surface-2);
}
.row-icon svg { width: 24px; height: 24px; fill: none; stroke: var(--muted); }
.row-txt { flex: 1; min-width: 0; }
.row-txt b { display: block; font-size: var(--f-base); font-weight: 650; letter-spacing: -.01em; }
.row-txt span { display: block; font-size: var(--f-sm); color: var(--muted); margin-top: 1px; }
.row-go { flex: none; color: var(--faint); font-size: 20px; }
/* Feature 3 (cta_label подборок, P2-4): текст перед шевроном в шите «Все
   подборки» (hmCollectionsAllRowHtml, app.js) — рисуется, только когда у
   записи ЕСТЬ cta_label; row-go рядом остаётся как есть (та же строка,
   тот же шеврон, что у любого другого .list-row файла). */
.row-cta { flex: none; font-size: var(--f-sm); font-weight: 600; color: var(--muted); }
a.row-item { text-decoration: none; }

/* ─── ListRow сгруппированный (план v8, §2.3/§2.5) ─────────────────────
   Один контейнер, бордер вместо тени, строки разделены inset-линией — та
   же идея, что и .recent-routes (план v8, §2.1), заведена отдельным
   именем вместо переиспользования: разные волны/зоны файла, у обеих
   компонент один и тот же паттерн «карточка iOS Settings». Внутренние
   .row-icon/.row-txt/.row-go — те же, что у одиночного .row-item выше. */
.list-group {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); overflow: hidden;
}
.list-row {
  display: flex; align-items: center; gap: 12px; width: 100%; padding: 14px;
  text-align: left; color: inherit; transition: background .12s ease-out;
}
.list-row + .list-row { box-shadow: inset 0 1px 0 var(--border); }
.list-row:active { background: var(--surface-2); }
a.list-row { text-decoration: none; }
.list-group-title {
  margin: 20px 2px 8px; font-size: var(--type-micro-size); line-height: var(--type-micro-lh);
  font-weight: var(--type-micro-w); text-transform: uppercase;
  letter-spacing: var(--type-micro-tracking); color: var(--text-2);
}
.list-group-title:first-child { margin-top: 0; }

/* «Недавно смотрел»/«Избранное» — тот же .list-row, но без иконки: два
   конца строки (план v8, §2.5: маршрут слева, цена справа жирно, дата
   --text-3). v10 план §2: строка обёрнута в .swipe-wrap (удаление свайпом
   влево, тот же механизм, что у подписок «Радара») и содержит ДВА
   настоящих <button> — .lr-viewed-open (открыть) и .lr-viewed-close
   (крестик, прямое удаление) — .list-row сам больше не кнопка (не
   nested-interactive). background: var(--bg) — .swipe-item обязан быть
   непрозрачным, иначе красная подложка .swipe-under видна и без свайпа. */
.list-row.lr-viewed { background: var(--bg); }
/* Ревью v12, P1: h41 — уже flex+align-items:center, min-height просто
   поднимает итог до 44. */
.lr-viewed-open {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  flex: 1; min-width: 0; min-height: 44px; text-align: left; color: inherit;
}
.lr-viewed-open:active { opacity: .65; }
.lr-viewed-route {
  flex: 1; min-width: 0; font-size: var(--f-base); font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lr-viewed-price {
  flex: none; text-align: right; font-weight: 700; font-variant-numeric: tabular-nums;
}
.lr-viewed-price i {
  display: block; font-style: normal; font-weight: 400;
  font-size: var(--f-sm); color: var(--text-3); margin-top: 1px;
}
/* Ревью v12, P1: 32×32 визуальный крестик остаётся компактным
   (та же логика, что у .sheet-close выше) — ::after расширяет хит-зону
   до 44×44 не трогая размер кружка; position:relative нужен явно, тут
   его раньше не было (не absolute-позиционирован сам по себе). */
.lr-viewed-close {
  position: relative;
  flex: none; width: 32px; height: 32px; margin-left: -2px;
  display: grid; place-items: center; border-radius: 50%; color: var(--text-3);
}
.lr-viewed-close::after { content: ''; position: absolute; inset: -6px; }
.lr-viewed-close svg { width: 12px; height: 12px; }
.lr-viewed-close:active { color: var(--price-high); background: var(--surface-2); }

/* ─── пресеты подписок ─────────────────────────────────────────────── */
/* Пилюли-подсказки (промпт §5, пустое состояние «Радара»): стеклянный
   язык эталона (те же значения, что .glass), не заливка --surface —
   волна C переводит их с «обычной карточки» v8 на стекло. */
.presets { display: grid; gap: 10px; margin-top: 22px; }
.preset {
  text-align: center; padding: 15px 16px; border-radius: 20px;
  background: rgba(255, 255, 255, .12);
  backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
  border: 1px solid rgba(255, 255, 255, .14); color: inherit;
}
.preset b { display: block; font-size: var(--f-base); font-weight: 500; }
.preset span { display: block; font-size: var(--f-sm); color: var(--text-2); margin-top: 2px; }

/* ─── профиль: «Города вылета» — мультивыбор чипами (план v7, §6б) ────
   Первый чип — основной: тонкое кольцо --brand + подпись «сюда
   уведомления», остальные — просто города, которые видит поиск.
   Иконка переиспользует ICON.pin — тот же аккуратный location-pin, что
   заменил кривой самолётик в строке профиля. */
.home-chips { display: flex; flex-direction: column; gap: 8px; }
/* Бордер вместо тени (план v8: обычная карточка — --border, не тень) —
   основной город: акцентный бордер 1.5px (план v8, §2.5, было: box-shadow-
   кольцо той же толщины, тот же вид, но буквально «бордер» по тексту
   брифа). */
.home-chip {
  display: flex; align-items: center; gap: 12px; padding: 12px 14px;
  border-radius: var(--r-lg); background: var(--surface); border: 1px solid var(--border);
}
.home-chip.is-primary { border: 1.5px solid var(--accent); }
.hc-icon {
  flex: none; width: 36px; height: 36px; border-radius: 11px;
  display: grid; place-items: center; background: var(--brand-soft); color: var(--link);
}
.hc-icon svg { width: 18px; height: 18px; }
.hc-txt { flex: 1; min-width: 0; }
.hc-txt b { display: block; font-size: var(--f-base); font-weight: 650; letter-spacing: -.01em; }
.hc-txt span { display: block; font-size: var(--f-sm); color: var(--muted); margin-top: 1px; }
/* Бейдж «Основной» (план v8, §2.5), не двухстрочная подпись. Селектор
   .hc-txt .hc-badge (0,2,0), не голый .hc-badge (0,1,0): бейдж — тоже
   <span> внутри .hc-txt (просто ещё и внутри <b>), и .hc-txt span выше
   (0,1,1: класс+тег) иначе выигрывал бы display/color/font-size —
   именно так бейдж и растягивался на всю ширину строки серым текстом,
   пока это не поймали на скрине. */
.hc-txt .hc-badge {
  display: inline-block; margin-left: 6px; padding: 2px 7px; border-radius: 999px;
  background: var(--accent-soft); color: var(--accent-text);
  font-size: var(--f-xs); font-weight: 700; vertical-align: middle;
}
/* Ревью v12, P1: 30×30 — самый маленький хит-таргет в файле; -7px даёт
   ровно 44×44 (30+14), а не «примерно», как у остальных 32/36px кружков
   с -6px. */
.hc-drop { position: relative; flex: none; width: 30px; height: 30px; display: grid; place-items: center; border-radius: 50%; color: var(--faint); }
.hc-drop::after { content: ''; position: absolute; inset: -7px; }
.hc-drop svg { width: 13px; height: 13px; }
.hc-drop:active { color: var(--sale); }
/* «+ Добавить город» — высота 52 (план v8, §2.5); модификатор, не правка
   базового .seg-add (пунктирная рамка-кнопка — см. ниже, «форма поиска»;
   мультисити снесено волной v9 A, но сам класс остался общим примитивом
   «добавить» и трогать его высоту здесь по-прежнему нельзя). */
.hc-add { height: 52px; display: flex; align-items: center; justify-content: center; }

/* «Проверено N мин назад» — строка caption с иконкой часов (план v8,
   §2.3), не карточка. v9 волна B: тот же класс переиспользован шторкой
   «Даты и цены» (openDatesSheet, app.js) — «карточка маршрута: фото-
   шапка»/StatTile этой же волны, что раньше тут стояли (.rt-hero/
   .stat-row/.stat-tile), сняты целиком: экран «Маршрут» v9 — immersive-
   шит на всю фотографию (.rt-full/.rt-cap/.rt-city, ниже по файлу), а
   три цифры маршрута переехали в шторку БЕЗ карточек-плашек (.ds-stat,
   тоже ниже) — промпт §6 прямо просит «без плашек», .stat-tile для этого
   не годился уже архитектурно, не только по разметке. */
.route-checked {
  display: flex; align-items: center; gap: 5px; margin: 10px 2px 0;
  font-size: var(--f-sm); color: var(--text-3);
}
.route-checked svg { width: 14px; height: 14px; flex: none; }

.card-block {
  margin-top: 14px; padding: 13px 14px; border-radius: var(--r-lg);
  background: var(--surface-2);
}
.block-title { font-size: var(--f-sm); font-weight: 700; color: var(--muted);
  text-transform: uppercase; letter-spacing: .04em; margin-bottom: 8px; }
.block-text { margin: 0; font-size: var(--f-md); line-height: 1.5; }
.block-text a { color: var(--link); font-weight: 600; }

.deal-actions { display: flex; gap: 6px; margin-top: 6px; }
.deal-actions .ghost { flex: 1; padding: 10px 6px; border-radius: var(--r-md); box-shadow: inset 0 0 0 1px var(--line);
  background: var(--surface-2); color: var(--text); font-size: var(--f-sm);
  text-align: center; white-space: nowrap; }

.best-row {
  display: flex; align-items: center; gap: 10px; padding: 12px 4px;
  border-bottom: 1px solid var(--line); color: inherit; text-decoration: none;
}
.best-row:last-child { border-bottom: 0; }
.best-txt { flex: 1; min-width: 0; }
.best-txt b { display: block; font-size: var(--f-base); font-weight: 650; }
.best-txt span { display: block; font-size: var(--f-sm); color: var(--muted); }
.best-price { font-weight: 700; font-variant-numeric: tabular-nums; }
.best-go { color: var(--faint); font-size: 18px; }


/* ═══ крошки и одна нижняя панель ═══════════════════════════════════ */
.sheet-titles { flex: 1; min-width: 0; }
.crumbs { display: block; font-size: var(--f-xs, 11px); color: var(--faint);
  margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
body.sheet-open .tabbar { display: none; }

/* ═══ свайп-удаление подписки ═══════════════════════════════════════ */
.swipe-wrap { position: relative; border-radius: var(--r-lg); overflow: hidden; }
.swipe-wrap + .swipe-wrap { margin-top: 8px; }
.swipe-under {
  position: absolute; inset: 0; display: flex; justify-content: flex-end;
  align-items: center; padding-right: 26px; background: var(--danger);
  opacity: 0; transition: opacity .12s ease;
}
.swipe-wrap.swiping .swipe-under { opacity: 1; }
.swipe-under svg { width: 22px; height: 22px; fill: none; stroke: var(--on-scrim); stroke-width: 2; }
.swipe-item { position: relative; width: 100%; text-align: left;
  transition: transform .22s cubic-bezier(.32, .72, 0, 1); }
/* ═══ разное ═════════════════════════════════════════════════════════ */
.city-flag { flex: none; font-size: 22px; width: 30px; text-align: center; }
/* flag() отдаёт '' на устройствах без поддержки эмодзи-флагов (app.js) —
   фикс. ширина+gap родителя иначе оставляли бы пустую дыру перед именем. */
.city-flag:empty { display: none; }
.geo-btn {
  display: flex; align-items: center; gap: 10px; width: 100%; padding: 12px 13px;
  margin: 6px 0 2px; border-radius: var(--r-md); background: var(--surface);
  box-shadow: var(--shadow-sm); color: var(--link); font-size: var(--f-md); font-weight: 600;
}
.geo-btn svg { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 1.8; }
/* Полноширинный паддинг-бар v8 тут больше не нужен: плавающая пилюля
   (см. «нижняя навигация» выше) сама учитывает safe-area в своём bottom. */

/* pull-to-refresh */
.ptr {
  height: 0; overflow: hidden; display: grid; place-items: center;
  color: var(--faint); font-size: var(--f-sm);
  transition: height .15s ease;
}

/* ═══ route-hero: шапка «код—код» с пунктирным путём (ref-06, NYC–ESB) ═
   план v6, §4: коды крупнее (34px) и приглушённо-серым — не сами коды,
   а линия между ними с точками теперь несёт акцент фиолетовым.
   Точечная карта мира за кодами (план v7, §9) убрана целиком (ревью v8,
   P1-1): бриф §6 запрещает клипарт-иллюстрации без оговорок, а точки за
   MOW⟶AER не несли данных — оставались только на «Выдаче» и «Билете»
   (маршрут ушёл на фото-шапку .rt-hero волной B). Сам файл
   /static/img/worldmap.svg и генератор tools/gen_dotted_map.py не
   удаляем — просто больше ничего их не рисует. */
.route-hero { display: flex; align-items: flex-start; gap: 6px; margin: 2px 0 14px; }
.rh-end { flex: 1 1 0; min-width: 0; }
.rh-end:last-child { text-align: right; }
.rh-end b {
  display: block; font-size: var(--f-3xl); font-weight: 800; letter-spacing: -.02em;
  color: var(--muted); font-variant-numeric: tabular-nums;
}
.rh-end .rh-city {
  display: flex; align-items: flex-start; gap: 4px; min-width: 0; margin-top: 3px;
  font-size: var(--f-sm); color: var(--muted);
}
/* Ревью v8, P2-7: «Санкт-Петербург» обрезался многоточием на билете —
   те же 135px, что и у «Москва». Перенос на 2 строки вместо ellipsis,
   тем же приёмом, что .fc-value.fc-placeholder выше по файлу. */
/* Ревью v12, P2 (§1): «Набережные Челны» переполняет колонку на 320px
   (scrollWidth 82 > clientWidth 79, замерено) — та же защита, что уже
   есть у .aw-row-city. */
.rh-end .rh-city span { min-width: 0; overflow-wrap: anywhere; }
.rh-end:last-child .rh-city { justify-content: flex-end; }
/* v9, RouteDash (промпт: «MOW ---✈--- KGD»): точка у города — нейтральный
   белый, не лиловый; пунктир — rgba(255,255,255,.35) буквально по
   промпту; лиловый достаётся ТОЛЬКО самолётику — единственное место во
   всём приложении, где вообще используется --lilac. */
.rh-dot { flex: none; width: 5px; height: 5px; border-radius: 50%; background: rgba(244, 241, 234, .5); }
.rh-path { flex: 0 0 auto; min-width: 88px; max-width: 42%; position: relative; display: flex;
  flex-direction: column; align-items: center; padding-top: 12px; }
.rh-path::before {
  content: ""; position: absolute; left: 0; right: 0; top: 11px;
  border-top: 1.5px dashed rgba(255, 255, 255, .35);
}
.rh-path svg { position: relative; width: 16px; height: 16px; color: var(--lilac); margin-top: -8px; }
.rh-cap {
  display: block; margin-top: 6px; font-size: var(--f-xs); color: var(--faint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
}

/* «+ Добавить …» — пунктирная рамка-кнопка (план v7, §5), общий примитив:
   раньше «+ Добавить перелёт» сложного маршрута (снесено волной v9 A),
   сейчас — только «+ Добавить город» в Профиле (.hc-add — модификатор
   высоты, см. выше). */
.seg-add {
  display: block; width: 100%; margin-top: 10px; padding: 12px; border-radius: var(--r-md);
  background: none; border: 1.5px dashed var(--line); color: var(--muted);
  font-weight: 700; font-size: var(--f-md); text-align: center;
  transition: border-color .15s ease, color .15s ease, transform .18s cubic-bezier(.32, .72, 0, 1);
}
.seg-add:active { transform: scale(.98); border-color: var(--brand); color: var(--link); }

/* ═══ PhotoCard: фото + фолбэк-сцена (общая инфраструктура) ════════════
   Сетка «Куда угодно» (.ex-card/.ex-grid) и рельс «Популярные» сняты
   волной C вместе со старым «Куда угодно»/формой поиска (DESIGN_PLAN) —
   .ex-photo/.ex-scrim остаются: их по-прежнему используют .dl-card
   (карусель «Сделок»), .rt-full (шапка «Маршрута» И «Сделки» — волна D
   перевела «Билет» на тот же immersive-компонент, .deal-hero снят),
   .aw-bg-layer (фон «Куда угодно», план v11 §3 — заменил .aw-scene
   волны C) и .rd-card (карточка «Радара», волна C). */
/* v10 план §4: тёмная тема притемняет настоящие фото банка чуть сильнее
   (brightness .78 поверх --photo-scrim) — сцены рядом в той же карусели
   тёмные/сумеречные сами по себе, а дневные фото банка без фильтра были
   заметно светлее соседних процедурных карточек. Светлая
   тема — как есть, без фильтра (промпт волны D: «сцены/фото оставь
   тёмными как есть» — тут используем тот же принцип в обратную сторону,
   не затемняем сильнее в светлой теме). */
/* Ревью v12, P2: банк фото — горизонтальные кадры (1080×608/810), карточки
   портретные (0.72) — cover резал до центральной вертикальной полосы,
   часто теряя то, что делает город узнаваемым (крыши/горизонт обычно
   чуть ниже геометрического центра кадра). object-position — грубый
   компромисс без правок пайплайна фото. */
.ex-photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 35%; filter: brightness(.78); }
body.light .ex-photo { filter: none; }
/* Своего фото нет или оно не загрузилось → cardImgErr() (app.js) добавляет
   .noimg классу-контейнеру этого фото — сцена под ним (sceneHtml())
   остаётся видна как есть. */
.dl-card.noimg .ex-photo, .rt-full.noimg .ex-photo,
.aw-bg-layer.noimg .ex-photo, .rd-card.noimg .ex-photo,
.amb-layer.noimg .ex-photo, .rail-card.noimg .ex-photo,
/* .story-photo — общий класс guide/vintage <img> «Историй» (app.js,
   storyPhotoHtml) — одна строка скрывает оба варианта на onerror. */
.story-card.noimg .story-photo { display: none; }
/* Кредит фото (.rt-credit, «Маршрут») — вложен в тот же .rt-full, что и
   .ex-photo выше: при отвале картинки (cardImgErr) прячем чужую подпись
   под уже не показанным кадром тем же .noimg, без правки разметки/JS —
   единственное место, где app.js вообще рисует видимый credit. */
.rt-full.noimg .rt-credit { display: none; }
/* Градиент снизу (план v8, §3 — формула брифа, токен --photo-scrim):
   гарантирует читаемость текста-оверлея на любом фото. */
.ex-scrim { position: absolute; inset: 0; z-index: 1; background: var(--photo-scrim); pointer-events: none; }
/* Ревью v12, P1 (§4): .rt-full-content растёт вверх от bottom (нет
   верхнего предела) — на «Маршруте»/«Сделке»/«Подписке» капс/город часто
   оказываются выше нижних ~22%, где --photo-scrim ещё почти прозрачен
   (замерено по пикселям: .rt-cap 1.38:1, .rt-city 1.0:1 в светлых
   участках). Второй слой ПОВЕРХ обычного скрима — два background через
   запятую, не замена: первый в списке рисуется сверху, оба
   полупрозрачны и складывают затемнение — специфичность класса та же,
   `.rt-full .ex-scrim` просто конкретнее одиночного `.ex-scrim` и
   выигрывает у карусели/радара (короче контентом, их не трогаем). Не
   зависит от темы явно — var(--photo-scrim) сам уже light/dark, второй
   слой того же тона добавляется поверх в обеих. */
.rt-full .ex-scrim {
  background: var(--photo-scrim),
    linear-gradient(180deg, transparent 28%, rgba(14, 15, 19, .58) 54%, rgba(14, 15, 19, .92) 100%);
}

/* ═══ свайп между вкладками: входящая панель translateX(±18px)→0 +
   opacity (план v5, «Свайпы») ══════════════════════════════════════ */
@keyframes slide-in-r { from { transform: translateX(18px); opacity: 0; } }
@keyframes slide-in-l { from { transform: translateX(-18px); opacity: 0; } }
.slide-in-r { animation: slide-in-r .16s ease both; }
.slide-in-l { animation: slide-in-l .16s ease both; }

/* ═══ карточка рейса в выдаче (ref-06, план v6 §3) ════════════════════
   белая карточка: верх — «Вылет {время}» | перевозчик+длительность по
   центру | «Прилёт {время}»(+1); середина — сетка дат/пересадок (третья
   колонка — дата обратно, только в RT); низ — цена-пилюля на
   --brand-soft слева, свежесть находки мелким справа. Вся карточка —
   ссылка на Авиасейлс (как раньше). Бордер вместо тени — план v8, §1,
   волна C. */
.flight-row {
  display: block; padding: 14px; border-radius: var(--r-lg);
  background: var(--surface); border: 1px solid var(--border);
  color: inherit; text-decoration: none;
}
.fr-top { display: flex; align-items: flex-start; gap: 8px; }
.fr-pt { flex: 1 1 0; min-width: 0; }
.fr-pt-r { text-align: right; }
.fr-cap { display: block; font-size: var(--f-xs); color: var(--muted); font-weight: 600; margin-bottom: 2px; }
.fr-pt b {
  display: flex; align-items: center; gap: 4px;
  font-size: var(--f-lg); font-weight: 750; letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
}
.fr-pt-r b { justify-content: flex-end; }
.fr-pt > span:last-child { display: block; font-size: var(--f-xs); color: var(--faint); margin-top: 1px; }
.fr-mid { flex: 1 1 0; min-width: 0; text-align: center; padding-top: 15px; }
.fr-carrier {
  display: block; font-size: var(--f-sm); font-weight: 650;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.fr-durline { display: block; font-size: var(--f-xs); color: var(--faint); margin-top: 2px; }
.fr-plus {
  display: inline-block; padding: 0 5px; border-radius: 999px;
  background: var(--surface-2); font-size: 10px; font-weight: 700; color: var(--text);
}
.fr-grid {
  display: flex; justify-content: space-between; gap: 8px; margin-top: 10px; padding-top: 10px;
  border-top: 1px dashed var(--line); font-size: var(--f-sm); color: var(--muted);
}
.fr-grid span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fr-bottom { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 10px; }
.fr-price-pill {
  background: var(--brand-soft); color: var(--link); font-weight: 800; font-size: var(--f-base);
  padding: 6px 12px; border-radius: 999px; font-variant-numeric: tabular-nums; letter-spacing: -.01em;
}
.fr-age { font-size: var(--f-xs); color: var(--faint); }

/* ═══ v9 волна B: экран 01 «Сделки» — переключатель городов + карусель
   (design/flightdeals-v3-prompt.md, «Экраны» §1; эталон, слот 01) ══════
   Обычный поток main-скроллера, не абсолютное позиционирование 390×844-
   мокапа эталона — пропорции/типографика перенесены буквально, отступы
   адаптированы под реальный скролл вкладки. */
.dl-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 2px 2px 18px; }
/* Приёмка v9, P2-3: капс-подписи экранов — второстепенный текст промпта
   (--mute, .6 альфы), не третьестепенный --dim (.32); `.mono` эталона
   красит их именно var(--mute). Тот же фикс — у .scr-cap/.ds-stat-label
   ниже по файлу. */
/* Ревью v12, P1 (§2): единственный вход в фильтры/смену города, зона
   тапа была 302×17 и не отличалась от обычной капс-подписи (тот же цвет/
   кегль). min-height — порог 44; шеврон (.dl-cap-chev, разметка —
   index.html) — первый видимый признак «это кнопка» во всём приложении
   для этой роли. */
.dl-cap {
  display: flex; align-items: center; gap: 5px; min-height: 44px;
  flex: 1; min-width: 0; text-align: left;
  font-size: var(--type-cap-size); letter-spacing: var(--type-cap-tracking);
  text-transform: uppercase; color: var(--mute); font-weight: 400;
}
.dl-cap-chev { flex: none; font-size: 9px; color: var(--text-3); transform: translateY(1px); }
.dl-mode-toggle { flex: none; }

.dl-fallback { margin: -8px 2px 16px; font-size: var(--f-sm); color: var(--text-2); }
.dl-fallback button { color: var(--accent-text); font-weight: 650; text-decoration: underline; }

/* Переключатель городов НАЗНАЧЕНИЯ (промпт: «текущий крупно 32-34px вес
   300, соседи по бокам приглушённо») — --type-display-size/-w уже РОВНО
   32px/300 токеном волны A, отдельного именованного токена под это не
   заводим (единственный потребитель). */
.dl-switch {
  display: flex; align-items: baseline; justify-content: center; gap: 26px;
  margin: 0 0 18px;
}
/* Приёмка v9, P1-1: `flex: 0 1 auto` на ПУСТОМ #dl-prev (idx=0, ширина 0)
   всё равно резервирует `gap`, и группа съезжает влево на половину
   ширины соседа справа — центр текущего города оказывается левее центра
   экрана. `flex: 1 1 0` — оба соседа получают РАВНУЮ долю пространства
   по обе стороны от `.dl-switch-cur` (у которого `flex: none`), пустой
   и непустой сосед весят одинаково, и текущий город остаётся по центру
   в любом кадре, не только после первого свайпа. */
.dl-switch-side {
  flex: 1 1 0; min-width: 0; max-width: 25vw; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font-size: 15px; font-weight: 300; color: var(--dim);
}
/* Каждый сосед растёт К центру — иначе при `flex:1` текст залипает у
   внешнего края своей половины, а не рядом с текущим городом. */
#dl-prev { text-align: right; }
#dl-next { text-align: left; }
.dl-switch-cur {
  flex: none; max-width: 60vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--type-display-size); font-weight: var(--type-display-w); letter-spacing: -.01em;
}
/* Ревью v12, P1 (§2): на 390px реальные названия обрезались до
   «Минеральн…»/«Тб…»/«Ю…», на 320px — уже сам текущий город («Калинин…»).
   .is-long (app.js, paintDealsSwitch — по длине destination_city > 10)
   снижает кегль текущего города, а не безусловно — короткие имена
   («Сочи», «Ялта») остаются 32px. Узкие экраны (≤400px) — соседи вовсе
   не помещаются рядом с длинным именем, гасим их целиком, а не режем
   до 2 букв. */
.dl-switch-cur.is-long { font-size: 26px; }
/* Добивка verify-v12, §2 п.3: на ≤400px соседи прячутся (ниже), и голое
   «Калининград» в #dl-cur повисало ПРЯМО над «Калининград» в .dl-city
   карточки — тот же город дважды, переключатель при этом не тапается
   (нет обработчика, только читает скролл карусели, см. app.js). Вместо
   второй копии имени — счётчик позиции «N из M» (.dl-switch-count,
   paintDealsSwitch в app.js): другая, содержательная информация вместо
   дубля, а не два конкурирующих варианта чинки (спрятать целиком/менять
   назначение) — счётчик передаёт ТО ЖЕ, что раньше давали видимые соседи
   (где ты в списке), просто не именами. */
/* --text-3 (не --dim, см. комментарий у токена в начале файла): счётчик
   должен ПРОЧИТАТЬСЯ (4.56/4.57:1), не просто угадываться, как соседи
   .dl-switch-side, которых он тут заменяет. */
.dl-switch-count { display: none; font-size: 15px; font-weight: 300; color: var(--text-3); }
@media (max-width: 400px) {
  .dl-switch-side, .dl-switch-cur { display: none; }
  .dl-switch-count { display: block; }
  .dl-switch { gap: 0; }
}

/* ═══ v10 план §4: карусель ВЕРТИКАЛЬНАЯ (была горизонтальной с v9,
   пикселя-точная история центрирования/peek по X — ниже, для памяти) ═══
   scroll-snap по Y, align:start: активная карточка занимает верх
   контейнера целиком, следующая выглядывает СНИЗУ на --peek (только
   один сосед виден за раз — не оба, как было при боковом peek).
   Локальные custom properties на самом .dl-carousel (не глобальные
   токены — потребители ровно два вложенных класса, .dl-slot/.dl-card,
   им и наследуются). touch-action:pan-y — жест тут однозначно
   вертикальный, браузеру не нужно гадать направление скролла.
   Контейнер УБРАН из IGNORE_SEL свайпа табов (app.js): раньше
   горизонтальный жест по карусели забирала сама карусель (боковой
   peek), и IGNORE_SEL не давал ему ещё и переключить вкладку; теперь
   карусель поглощает только Y (dx у вертикального жеста маленький — под
   порог свайпа табов сам по себе, см. app.js), а горизонтальный свайп
   по карточке освободился и переключает вкладку так же, как и по
   остальной части экрана (.dl-top/.dl-switch/.dl-foot туда и так не
   входили). overscroll-behavior-y:none — драг вниз на первой карточке
   не даёт браузеру самому "отскочить"/растянуть карусель, это делает
   pull-to-refresh (app.js, свой жест поверх). */
#tab-explore { --crumb-h: 0px; }
/* Крошка (#ex-crumb, «Крошка уровня 2» выше) выкупает свою высоту у
   --card-h, а не добавляется поверх неё — план §5: без компенсации высота
   крошки без амортизации сдвигает низ карусели вниз и на 320×568 заезжает
   под #dl-foot (запас там и так ~0). .lvl2 ставит paintExploreCrumb()
   (app.js) СИНХРОННО с #ex-crumb.hidden — тем же вызовом, до рендера
   списка (риск R3), так что --card-h ниже никогда не считается по
   «средней» геометрии между показом и скрытием.
   Живая верификация (P1): реально занятая высота — 50px, не 34
   (min-height:44 + margin 12+12, схлопывание вертикальных margin даёт
   -18 против соседей, не полные 24) — с 34px карточка на 320×568 уезжала
   на ~45px и заезжала под #dl-foot (запас там был всего 2px). */
#tab-explore.lvl2 { --crumb-h: 50px; }
.dl-carousel {
  /* Ревью v12, P1 (§2): 494px фиксированной высоты на 320×568 сажал
     пилюлю «Смотреть»/цену под сгиб первого кадра (`cta.bottom 611 > vh
     568`, замерено) — min() с долей вьюпорта отдаёт карточке ровно
     столько высоты, сколько реально есть, вместо жёсткого пикселя.
     62vh — запас на обычной высоте (390×844 и выше: 62vh ≥ 494px, min()
     остаётся на исходных 494 без изменений), медиа-запрос ниже сужает
     долю ТОЛЬКО на низких вьюпортах — 62vh мало (карточка 352px всё
     ещё заезжала на #dl-foot/таббар, замерено footOverCard:true). */
  /* P1 (живая верификация): min(494px, 62vh - var(--crumb-h)) вычитал
     --crumb-h ВНУТРИ min() — комментарий выше уже фиксирует, что на
     390×844 и выше 62vh переваливает за константу 494px САМ ПО СЕБЕ; если
     на ещё более высоком вьюпорте 62vh-crumb-h всё равно останется ≥494px,
     min() как и раньше отдаёт голую константу 494 — крошка тогда вообще не
     выкупает у карточки высоту, и низ карусели уезжает на весь --crumb-h.
     calc(min(...) - ...) вычитает ПОСЛЕ min() — компенсация действует
     независимо от того, какая ветка min() победила. */
  --card-h: calc(min(494px, 62vh) - var(--crumb-h)); --peek: 20px; --gap: 14px;
  /* Ревью Opus v17, P1: без position:relative offsetParent слотов —
     ближайший позиционированный предок ВЫШЕ по дереву (#tab-explore/body),
     не сама карусель. offsetTop слота тогда включал ~103-135px её отступа
     в документе, а rAF-слушатель ниже сравнивает его с box.scrollTop
     (локальным for карусели) — детектор «какая карточка у верха» уже был
     сдвинут на весь этот оффсет, цок/подпись приходили на ~77-94% хода
     свайпа. position:relative делает саму карусель offsetParent —
     offsetTop(слот 0) становится ровно top-padding, формула JS верна без
     правок. Внутренние абсолюты (.dl-badge/.dl-fav/.dl-info/.ex-scrim и
     т.д.) уже позиционированы от .dl-card (у него свой position:relative
     ниже) — не от карусели, конфликта нет; fixed-элементов внутри нет. */
  position: relative;
  display: flex; flex-direction: column; gap: var(--gap);
  overflow-y: auto; overflow-x: hidden; scroll-snap-type: y mandatory;
  -webkit-overflow-scrolling: touch; scrollbar-width: none;
  overscroll-behavior-y: none; touch-action: pan-y;
  padding: 2px 0 0;
  height: calc(var(--card-h) + var(--gap) + var(--peek));
}
.dl-carousel::-webkit-scrollbar { display: none; }
/* Фикс владельца v17 (недокрут последней карточки), ревизия по замечанию
   Opus-ревью П2: padding-bottom как источник докрута ненадёжен на части
   Android WebView (не всегда учитывается в scrollHeight скролл-флекс-
   контейнера) — вместо него отдельный flex-элемент ::after (padding-bottom
   контейнера вернули к исходным 2px 0 0, докрут больше НЕ на паддинге).
   Расчёт (offsetTop локален от .dl-carousel после фикса position:relative
   выше): offsetTop(слот N-1) = 2 + (N-1)(card-h+gap). scrollHeight = 2
   (pad-top) + N·card-h + N·gap (N слотов + спейсер = N+1 флекс-элементов,
   между ними N зазоров gap) + S (высота спейсера). clientHeight = card-h
   + gap + peek. maxScrollTop = scrollHeight - clientHeight = 2 +
   (N-1)·card-h + (N-1)·gap + S - peek. Равенство offsetTop(last) ==
   maxScrollTop даёт S - peek = 0 → S = peek (ровно var(--peek), БЕЗ
   добавки gap: зазор между последней карточкой и спейсером уже учтён в
   «N·gap» выше как отдельный элемент gap-сетки; S = gap+peek считал бы
   этот gap дважды и давал бы maxScrollTop на +gap БОЛЬШЕ offsetTop(last) —
   не недокрут, а лишние 14px мёртвой зоны после последней карточки).
   card-h/N сокращаются — формула одна на оба media-таргета --card-h. */
.dl-carousel::after { content: ''; flex: 0 0 var(--peek); }
/* Ревью v12, P1 (§2): на низких вьюпортах (320×568 и т.п.) шапка+карточка
   при 62vh всё ещё перекрывали #dl-foot/таббар (фиксированный отступ
   108px от низа не масштабируется вместе с картой) — 51vh на этом
   диапазоне высот освобождает те же ~108+34px снизу, что резервирует
   подпись карусели. */
@media (max-height: 680px) {
  .dl-carousel { --card-h: calc(min(494px, 51vh) - var(--crumb-h)); }
  /* P1 (живая верификация, 320×568 + видимая крошка уровня 2): с базовыми
     --crumb-h:50px карточка на этом диапазоне худеет до ~240px — .dl-info
     (bottom:26px, свой контент ~194px) тогда стартует практически вплотную
     к .dl-badge (top:16px, ~50px высоты вместе с паддингом), город наезжает
     на бейдж (замерено ~29px перекрытия, .dl-city.top 191 vs .dl-badge.bottom
     220, screenshots/v16/h4-region-cards-320.png). Только на ЭТОМ диапазоне
     высот у крошки и инфоблока карточки — компактнее: крошка ниже (--crumb-h
     здесь меньше базовых 50px, отдаёт карточке разницу), инфоблок ближе к
     низу карточки и его внутренние отступы (.dl-meta/.dl-cta) туже — суммарно
     отдаёт городу назад >20px. 390×844 не задет: там своя, БАЗОВАЯ (вне этого
     media) геометрия крошки/инфоблока, этот блок только под max-height:680px.
     #tab-explore.lvl2 (не голый .lvl2) — та же специфичность, что у базового
     правила выше по файлу, иначе более специфичный ID-селектор победил бы
     этот оверрайд независимо от порядка/media. */
  #tab-explore.lvl2 { --crumb-h: 38px; }
  .lvl2 .aw-back { min-height: 34px; margin: -4px 2px 8px; padding: 4px 0; }
  .lvl2 .dl-info { bottom: 16px; }
  .lvl2 .dl-city { font-size: clamp(20px, 7vw, 34px); }
  .lvl2 .dl-meta { margin: 6px 0 8px; }
  .lvl2 .dl-cta { margin-top: 8px; }
  /* Риск R18 (план §12): «БЛИЖНИЙ ВОСТОК»/«СЕВ. АМЕРИКА» на капсе 38px
     двухстрочные — растят .dl-info вверх; на карточке 290px (51vh здесь)
     это уже опасно (та же арифметика вертикального бюджета, что и у
     .lvl2 выше). Меньше кегль + прячем .dl-scope-top целиком — на 320×568
     вертикального бюджета под топ-3 города нет вовсе (шаг 16). Уровня 2
     не касается — .dl-card-scope только на уровне 1 (карточки скоупов). */
  .dl-card-scope .dl-city { font-size: clamp(18px, 6.5vw, 28px); }
  .dl-scope-top { display: none; }
  /* Добивка verify-v12, §2 п.1: --photo-scrim затемняет по ПРОЦЕНТУ высоты
     (transparent 40%) — на карточке 290px (51vh здесь) инфоблок (.dl-info,
     дно ~26px + контент вверх) оказывается в верхних ~25% кадра, где скрим
     ещё почти прозрачен (замерено: .dl-city 3.04-4.45:1 вместо 4.5,
     .dl-meta 2.84-3.85:1 — ровно тот дефект, что чинили волной 2 для
     .rt-full, вернулся тут как побочный эффект фикса CTA §12). Тот же
     приём: второй слой ПОВЕРХ обычного скрима (.rt-full .ex-scrim выше —
     прецедент), но с остановками в PX от НИЖНЕГО края (calc(100% - Npx)),
     а не в процентах — абсолютная зона не «худеет» вместе с card-h,
     только этот media-диапазон её и получает (обычный card-h 494px — той же
     .ex-scrim, без изменений, полный визуальный регресс-риск снят самой
     областью действия media-запроса). */
  .dl-card .ex-scrim {
    background: var(--photo-scrim),
      linear-gradient(180deg, transparent 0, transparent calc(100% - 230px),
        rgba(14, 15, 19, .82) calc(100% - 150px), rgba(14, 15, 19, .98) 100%);
  }
}
/* Ревью v12, P1 (§2): пустая лента (0 сделок) держала эти же 528px
   высоты — #dl-empty уезжал под таббар (y≈730, скрин E01-feed.png).
   renderDealsCarousel уже пишет innerHTML='' на пустом списке — :empty
   тут абсолютно надёжен, без доп. класса из JS. Докрут v17 (::after ниже)
   не задевает эту строку: :empty матчится по DOM-детям, генерируемый
   ::after в них не считается (спека Selectors) — спейсер и на пустой
   ленте продолжает существовать, но padding-bottom тут 0 (докрут v17
   перенесён с padding на ::after), а height:0 не даёт борду вырасти
   заметно: overflow:auto просто обрезает спейсер внутри, наружу видна
   только исходная 2px padding-top (как и до этой правки). */
.dl-carousel:empty { height: 0; }
/* Ревью v12, P1 (§0): content-visibility:auto пропускает layout/paint у
   слотов вне вьюпорта (карусель снап-скроллит по одной карточке за раз —
   соседи почти всегда за кадром); contain-intrinsic-size — оценка
   высоты для скроллбара ДО первого замера, чтобы браузер не прыгал. */
.dl-slot {
  flex: 0 0 var(--card-h); scroll-snap-align: start;
  /* Фикс владельца v17 (проскальзывание свайпа): без stop:always резкий
     фling проезжал мимо промежуточных карточек к дальней снэп-точке —
     always заставляет инерцию гасить на КАЖДОМ слоте, свайп листает по
     одной карточке за раз. */
  scroll-snap-stop: always;
  content-visibility: auto; contain-intrinsic-size: 0 var(--card-h);
}
.dl-card {
  position: relative; height: var(--card-h); border-radius: var(--r-card); overflow: hidden;
  background: var(--surface-2); opacity: .5; transition: opacity .2s ease;
}
.dl-slot.is-current .dl-card { opacity: 1; }
/* История v9 (для контекста, геометрия по X больше не применяется):
   карусель была `display:flex` по строке, `margin:0 calc(var(--pad)*-1)`
   full-bleed + `padding:2px 32px 4px` — паддинг 32 + слот 100% (326px)
   давало центр экрана точно при scrollLeft:0, видимая часть соседа
   `32 − 12(gap) = 20px` (приёмка v9, P1-2). Вертикальная версия не
   центрирует — только align:start сверху + peek снизу, поэтому этот
   расчёт под X был снят, а не адаптирован. */
.dl-card.skeleton { opacity: 1; }
/* Ревью v12, P1: бейдж/сердечко живут в верхних ~12% карточки, где
   --photo-scrim ещё прозрачен (transparent 40%) — на светлом фото
   белое .glass-стекло само по себе там не защита (2.41-3.38:1,
   замерено по пикселям). --scrim-btn-bg — тёмная подложка ПОД тем же
   блюром, а не поверх (см. токен, app.css верх файла); .glass задаёт
   background первым по каскаду (общий класс, раньше в файле) — тут
   просто более позднее правило того же свойства побеждает. */
.dl-badge { position: absolute; left: 16px; top: 16px; z-index: 2; padding: 7px 12px; border-radius: 14px; font-size: 12px; font-weight: 500; color: var(--ink); background: var(--scrim-btn-bg); }
.dl-fav { position: absolute; right: 12px; top: 12px; z-index: 2; width: 36px; height: 36px; background: var(--scrim-btn-bg); }
/* Ревью v12, P1: 36×36 сердечко остаётся компактным над фото, ::after
   расширяет хит-зону до 48×48 (запас над порогом 44). */
.dl-fav::after { content: ''; position: absolute; inset: -6px; }
.dl-fav svg { width: 17px; height: 17px; color: var(--ink); }
.dl-fav.is-fav svg { fill: var(--ink); }
/* Двойной тап по карточке — сердце-анимация избранного (план v11 §4,
   восстановлено из истории v8 — .ex-card/.heart-burst, снесено волной
   v9 C вместе со старой сеткой). z-index 3 — выше .dl-info(2)/.dl-fav(2). */
.heart-burst { position: absolute; inset: 0; z-index: 3; display: grid; place-items: center; pointer-events: none; }
.heart-burst svg {
  width: 96px; height: 96px; fill: var(--on-scrim); stroke: var(--on-scrim);
  filter: drop-shadow(var(--shadow-glow));
  animation: heart-pop .7s cubic-bezier(.2, 1.1, .4, 1) both;
}
@keyframes heart-pop {
  0% { transform: scale(.3); opacity: 0; }
  35% { transform: scale(1.15); opacity: 1; }
  55% { transform: scale(.94); }
  100% { transform: scale(1); opacity: 0; }
}
.dl-info { position: absolute; left: 24px; right: 24px; bottom: 26px; z-index: 2; text-align: center; color: var(--on-scrim); }
/* Приёмка v9, P2-1: эталон не кладёт text-shadow ни на один слой текста
   над фото — читаемость там даёт только --photo-scrim (затемнение снизу)
   плюс зерно/виньетка; тень была лишним слоем без опоры в референсе.
   Тот же приём применён ниже у .rt-… / .rd-card-… (§2/§5). Ревью v12, P0:
   раньше здесь стояли те же два селектора БЕЗ пробела вокруг слэша — та
   пара символов, которой закрывается этот самый комментарий, случайно
   нашлась прямо в его тексте и оборвала блок на середине фразы; хвост
   превращался в невалидный селектор, и следующее ниже правило `.dl-city`
   парсер выбрасывал целиком. Пробел вокруг слэша ниже — не стиль, а
   защита от повтора той же ловушки. */
/* Добивка verify-v12, §2 п.4: «Калининград» 38px переполнял карточку на
   320px (scrollWidth 263 > clientWidth 240, замерено) — тот же приём, что
   уже есть у .rd-card-city (клапан §5 выше): clamp вместо жёсткого px,
   overflow-wrap как страховка от ещё более длинных имён. */
.dl-city { font-size: clamp(24px, 8vw, 38px); font-weight: 300; letter-spacing: -.01em; margin: 0; overflow-wrap: anywhere; }
.dl-meta { font-size: var(--f-md); color: var(--on-scrim-2); margin: 12px 0 20px; }
.dl-price { font-size: 40px; font-weight: 300; line-height: 1; margin: 0; }
.dl-usual { font-size: 14px; font-weight: 400; color: var(--on-scrim-2); text-decoration: line-through; vertical-align: middle; margin-left: 6px; }
.dl-cta { height: 46px; width: 180px; margin: 22px auto 0; }

/* Карточка скоупа уровня 1 (план v18.1 §9.1, §10 шаг 15) — свой билдер
   (dealScopeCardHtml, app.js), не dealCardHtml (риск R17): капс-заголовок
   скоупа той же типографики, что у капс-городов списка «Куда угодно»
   (--type-citycaps-tracking), hyphens:manual — урок v16 №5, против
   словарного переноса «БЛИЖ-НИЙ ВОСТОК»/«СЕВ. АМЕРИКА» на 38px (риск R18
   ниже — media 680 обязателен именно из-за этих трёх лейблов).
   .dl-scope-top — топ-3 города скоупа мелким текстом («что внутри?» без
   тапа). .is-soon (план §9.3) — собственное приглушение текста поверх
   обычного .dl-slot:not(.is-current) opacity — заглушке нечего показать,
   даже когда её слот листанием стал активным. */
.dl-card-scope .dl-city { text-transform: uppercase; letter-spacing: var(--type-citycaps-tracking); -webkit-hyphens: manual; hyphens: manual; }
.dl-scope-top { font-size: var(--f-sm); color: var(--on-scrim-2); margin: 6px 0 0; }
.dl-card-scope.is-soon .dl-info { opacity: .7; }
/* 03.09.2026 (разделитель перед регионами, app.js/dealScopeCardHtml):
   бейдж-подпись над заголовком ПЕРВОЙ карточки-региона — та же роль,
   что играл бы отдельный служебный слот карусели, но без риска сломать
   индексацию .dl-slot↔dealsRows (счётчик «N из M», paintDealsSwitch —
   см. комментарий у isFirstRegion в JS). Мелкая приглушённая капс-
   строка — не спорит с .dl-city под собой, opacity поверх --on-scrim-2
   (уже rgba с альфой) — тот же приём двойного приглушения, что и у
   .aw-row.aw-soon в списке (эта же правка). */
.dl-region-badge {
  font-size: var(--f-sm); font-weight: 600; letter-spacing: .14em;
  text-transform: uppercase; color: var(--on-scrim-2); opacity: .75; margin: 0 0 8px;
}

/* v11 план §2 п.4: подпись карусели «Сделок» поднята НАД плавающим
   таббаром — position:fixed (вынесена из #deals-mode/<main> в index.html,
   containing-block причина в комментарии там), заякорена от низа ТОЙ ЖЕ
   safe-area-формулой, что и .tabbar ниже, со сдвигом на высоту пилюли
   (60px) + её собственный отступ от низа (28px) + зазор (20px, задание
   просит ≥8px, взят с запасом на глаз) = 108px. Раньше подпись была
   обычным потоковым абзацем сразу под каруселью — видна, только когда
   пользователь докрутил страницу целиком до конца, а `.dl-carousel` со
   своим `overscroll-behavior-y:none` (ниже по файлу) не даёт жесту
   зацепить внешний скролл страницы — на невысоких вьюпортах подпись
   оказывалась физически недостижима тем же жестом, что листает саму
   карусель. pointer-events:none — декоративный текст, не перехватывает
   тап у того, что может оказаться рядом/под ним. */
.dl-foot {
  margin: 0; text-align: center;
  font-size: var(--f-sm); color: var(--text-2); pointer-events: none;
}
/* Ревью v12, P2 (§5): #ds-week-hint (шторка «Даты и цены») раньше делил
   класс .dl-foot с #dl-foot — тот id-селектор (position:fixed, ниже)
   его саму не задевал (разные id), но связь хрупкая на будущее (ревью
   v11 уже ловило регресс от похожего совпадения) — свой класс с той же
   текстовой геометрией. */
.ds-hint { margin: 0; text-align: center; font-size: var(--f-sm); color: var(--text-2); }
/* Ревью v12, P1 (§2): `left:50%` без `right` ограничивает shrink-to-fit
   ПОЛОВИНОЙ экрана независимо от max-width — фактическая ширина была
   195px при заявленных `calc(100% - 32px)` (замерено), подпись рвалась
   на 2 строки всегда. left:0/right:0 — центрирует по всей ширине шита,
   transform больше не нужен. */
/* Добивка verify-v12, §2 п.6: геометрию (одну строку/позицию) починили,
   контраст — нет: 12px var(--text-2) поверх ambient-фото на глазок
   держит 3.0-4.0:1 вместо 4.5 в обеих темах (текст меняет цвет с темой,
   а фон под ним — фото, которое НЕ меняется, тот же класс несовпадения,
   что уже решён для .dl-badge/.dl-fav/.rd-badge токеном --scrim-btn-bg +
   постоянным --on-scrim-2 — тут ровно тот же приём: тёмная стеклянная
   пилюля-подложка ПОД текстом (не над, порядок фона будет "снизу вверх"
   тот же, что и у --scrim-btn-bg — сплошной непрозрачный слой, не смесь
   с фото), текст всегда светлый, фон подложки от темы не зависит.
   width:fit-content вместо left/right:16px — теперь пилюля обтягивает
   текст, а не растягивается на всю ширину шита (та же ловушка
   shrink-to-fit с `left:50%`, что описана в комментарии выше, тут не
   грозит: left:0/right:0 симметричны, margin:auto — стандартное
   центрирование блока известной ширины в раме). */
#dl-foot {
  position: fixed; left: 0; right: 0; z-index: 29;
  bottom: calc(108px + max(var(--safe-b), var(--tg-content-safe-area-inset-bottom, 0px)));
  width: fit-content; max-width: calc(100% - 32px); margin: 0 auto;
  padding: 7px 14px; border-radius: 999px;
  background: var(--scrim-btn-bg); color: var(--on-scrim-2);
  backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
  white-space: nowrap;
}
.dl-foot:empty { display: none; }
/* На 320 «проверено N мин назад · свайп — следующая» рвётся на 2 строки
   (замерено) — на ≤374px (320/360; не задевает 390/430, там и так одна
   строка) прячем ХВОСТ подсказки, оставляя короткую форму «проверено
   N мин назад». Хвост — второй <span class="dl-foot-swipe"> в разметке,
   которую строит paintDealsSwitch (app.js), не CSS ::after: текст должен
   остаться настоящим DOM-текстом (тот же принцип, что и у остального
   контента), а не декоративным generated content. */
@media (max-width: 374px) {
  .dl-foot-swipe { display: none; }
}

/* ═══ v10 план §3/§4: ambient-фон карусели «Сделок» (v11: «Куда угодно»
   больше не делит этот слой — свой независимый фон #aw-bg, план §3,
   ниже по файлу) ═══════════════════════════════════════════════════════
   За каруселью, во весь экран — размытая копия активной сцены или фото
   (два слоя #amb-a/#amb-b, кроссфейд .4s при смене активной карточки —
   paintAmbient() в app.js чередует их тем же приёмом, что
   .dl-slot.is-current), плёночное зерно СВЕРХУ без блюра (иначе теряет
   текстуру и превращается в ровный шум — блюрится только сама
   сцена/фото), и вертикальный градиент в --bg сверху/снизу, чтобы верх
   (шапка/переключатель городов) и низ (таббар) экрана не спорили с
   ambient-подсветкой. Разметка — вне <main> (index.html), та же
   containing-block причина, что и у #aw-bg (см. комментарий там):
   .panel.is-active держит `animation: fade … both` постоянно.
   z-index:-1 — красится НАД фоном <body>, но ПОД обычным (не
   спозиционированным) содержимым <main> — так ambient виден только в
   «пустых» местах экрана (паддинги вокруг карусели), а не поверх самих
   карточек. Видимость — paintExploreLayers() (app.js): показан, только
   когда активен таб «Сделки» И режим внутри него — deals. */
.dl-ambient { position: fixed; inset: 0; z-index: -1; overflow: hidden; pointer-events: none; }
.amb-layer {
  position: absolute; inset: 0; opacity: 0;
  transform: scale(1.2); filter: blur(48px);
  transition: opacity .4s ease;
}
.amb-layer.is-on { opacity: 1; }
.amb-grain { position: absolute; inset: 0; opacity: .12; mix-blend-mode: overlay; }
.amb-shade {
  position: absolute; inset: 0;
  background: linear-gradient(180deg, var(--bg) 0%, transparent 26%, transparent 74%, var(--bg) 100%);
}

/* ═══ v9 волна B: экран 02 «Маршрут» — immersive-шит (design/
   flightdeals-v3-prompt.md, «Экраны» §2; эталон, слот 02) ══════════════
   .sheet.rt-immersive: фото/сцена на ВЕСЬ шит, контент в нижней трети.
   Стандартная шапка шита (grabber/заголовок/крестик) визуально прячется —
   вместо неё стеклянные назад/поделиться поверх фото; сама .sheet-head
   остаётся в DOM (не display:none) — свайп-вниз «потянуть шапку, чтобы
   закрыть» (app.js, слушатель на #sheet-head) продолжает работать, просто
   ловит жест на невидимой узкой полосе. mountSheet() (app.js) снимает этот
   класс на КАЖДОМ открытии шита — сам openRouteScreen ставит его обратно
   сразу после mountSheet, чтобы вложенные экраны (Даты и цены, Радар) не
   унаследовали чужой стиль. */
/* Обычный .sheet начинается на top:8vh (92% экрана) — под этой полоской
   просвечивает таб, с которого открыли шит. Для immersive-экрана это
   оставляло чужую шапку «Сделок» видной НАД фото (поймано скрином) —
   промпт прямо требует «фото на ВЕСЬ экран шита». top:0 + прямые верхние
   углы — единственное место в приложении, где шит выходит за 8vh. */
.sheet.rt-immersive { top: 0; border-radius: 0; }
.sheet.rt-immersive .sheet-head { padding: 0; min-height: 0; }
.sheet.rt-immersive .sheet-titles,
.sheet.rt-immersive .sheet-close,
.sheet.rt-immersive .grabber { display: none; }
.sheet.rt-immersive .sheet-body { padding: 0; overflow: hidden; }

.rt-full { position: relative; height: 100%; min-height: 100%; }
.rt-full-actions {
  position: absolute; left: 20px; right: 20px; z-index: 2;
  top: calc(env(safe-area-inset-top, 0px) + 14px);
  display: flex; justify-content: space-between;
}
/* «Сделка» (openDealCard, волна D) добавляет избранное рядом с
   «поделиться» — «Маршрут» этой кнопки не показывает (общий маршрут не
   избранное, а конкретная находка — да), поэтому это отдельная обёртка
   внутри .rt-full-actions, а не третий flex-child с space-between. */
.rt-full-actions-right { display: flex; gap: 10px; }
.rt-full-content {
  position: absolute; left: 28px; right: 28px; z-index: 2; color: var(--on-scrim);
  bottom: calc(env(safe-area-inset-bottom, 0px) + 30px);
}
.rt-cap {
  font-size: var(--type-cap-size); letter-spacing: var(--type-cap-tracking); text-transform: uppercase;
  color: var(--on-scrim-2); margin: 0 0 14px;}
/* Добивка verify-v12, §2 п.4: тот же clamp/wrap, что у .dl-city выше —
   «Калининград» 44px обрезался и на .rt-city (инсет .rt-full-content
   28/28, чуть теснее dl-info 24/24, поэтому верхняя граница clamp свой,
   не буквально скопированный). */
.rt-city {
  font-size: clamp(24px, 9vw, var(--type-hero-size)); font-weight: var(--type-hero-w); letter-spacing: -.01em;
  line-height: 1.05; margin: 0; overflow-wrap: anywhere;}
.rt-blurb {
  font-size: var(--f-md); line-height: 1.5; color: var(--on-scrim-2);
  margin: 16px 0 22px; max-width: 320px;}
/* Мёртвый билет (sold_out/flown/overpriced) — статус вместо описания
   города, промпт «Логика»: красный = дороже/плохие новости о цене. */
.rt-blurb.is-dead { color: var(--price-high); }
.rt-facts { display: flex; flex-wrap: wrap; gap: 20px; align-items: center; margin-bottom: 24px; font-size: var(--f-md); }
.rt-fact { display: flex; align-items: center; gap: 7px; }
.rt-fact svg { width: 15px; height: 15px; color: var(--on-scrim-2); flex: none; }
/* Скидка находки — зелёный (промпт: семантика «только в бейджах и
   цифрах» — ровно этот случай, факт-строка «Сделки», не «Маршрута»). */
.rt-fact.is-good { color: var(--price-good); }
.rt-cta-row { display: flex; gap: 10px; align-items: center; }
.rt-cta-row .pill { flex: 1; }
.rt-radar { flex: none; width: 50px; height: 50px; }
/* Аудит 2026-09-05 (владелец: радар меняет цвет неправильно после тапа в
   зависимости от темы) — currentColor вместо хардкода: `.ic svg` (~строка
   484) задаёт `stroke: var(--ink)` напрямую и специфичнее голого `color`
   на кнопке (та самая ловушка, из-за которой предыдущий проход красил
   `stroke` в обход `color` вовсе). Этот селектор — тот же один класс, но
   позже в файле → та же специфичность, порядок решает, побеждает. Дальше
   OFF ниже всё ещё ставит РАЗНЫЙ `color` по контексту (тем-инвариантная
   над фото и тем-следящая в подвале, см. блок ниже) — currentColor
   подхватывает любую. ON с 06.09 везде одинаковый (--radar-on). */
.rt-radar svg { width: 20px; height: 20px; stroke: currentColor; }
/* ── OFF остаётся «один класс — два контекста — две стратегии темы»,
   ON — ОДИН источник на оба контекста (фикс 06.09.2026) ──
   .rt-radar живёт (1) НАД фото — `.rt-cta-row` «Маршрута»/«Сделки»/«Шита
   подписки» (#rt-radar/#deal-watch/#ws-radar), и (2) в подвале ОБЫЧНОГО
   шита без фото — `.sheet-foot` «Рейса»/«Дат и цен» (#fd-radar/#ds-radar).
   OFF законно разный по конструкции экрана: (1) кремовое стекло,
   тем-инвариантное — лежит на неизменной фотографии, как сосед
   `.rd-radar` (та же роль над тем же типом фото); (2) плоская обводка,
   тем-следящая (`var(--ink)`) — лежит на --bg подвала шита, как соседний
   `.glass` там же. 05.09 ON тоже расходился по этому принципу
   (`--radar-scrim` над фото / `var(--ink)`+`var(--bg)` в подвале) — итог
   был «то кремовый кружок, то тёмно-синий» на одном и том же ON между
   разными темами Telegram (жалоба владельца, live-Playwright подтвердил
   разъезжающийся bg). 06.09 владелец сравнил обе семьи РЯДОМ и заметил,
   что даже так они выглядят как противоположные цвета в светлой теме —
   ON обязан быть байт-в-байт одинаковым всегда, независимо от контекста
   и темы: заливка `--radar-on` (см. её комментарий в :root — тот же
   зелёный смысл, что «Покупать»/скидка, но тем-инвариантный литерал),
   иконка `--radar-on-ink` (белый) поверх через уже объявленный
   `currentColor`. */
.rt-cta-row .rt-radar {
  color: var(--radar-scrim); /* OFF — было в body.light (см. блок «Стекло
    над фото» выше), тут тем-инвариантно по построению, без дубля там. */
}
.rt-cta-row .rt-radar.is-on { background: var(--radar-on); color: var(--radar-on-ink); }
.sheet-foot .rt-radar {
  color: var(--ink); /* OFF — подвал шита плоский и тем-следящий (см.
    body.light-фикс `.glass` выше по файлу), иконка обязана следовать
    теме так же — раньше молча наследовала на этот `--ink`, работало
    только пока `.ic svg` был единственным источником stroke. */
}
.sheet-foot .rt-radar.is-on { background: var(--radar-on); color: var(--radar-on-ink); }
/* Сердечко «Сделки» — тот же принцип заливки при активном состоянии,
   что и у .dl-fav карусели (промпт §1), просто без absolute-позиции:
   тут оно уже флекс-ребёнок .rt-full-actions-right. */
.rt-fav.is-fav svg { fill: var(--ink); }
/* Полная ширина шита (пересиливает инсет .rt-full-content), как в
   эталоне: подсказка центрирована независимо от левого края абзаца. */
/* Ревью v12, P1: теперь <button> (app.js) — display:block взамен
   inline-block по умолчанию у кнопок (width осознанно НЕ задан: авто-
   ширина блочного элемента с margin слева/справа -28px сама решает
   уравнение и раздвигает бокс на полную ширину шита — та же механика,
   что раньше давал блочный <p> по умолчанию; явный width:100% тут
   пересчитал бы margin-right и снял правый вылет). */
/* 06.09: transition только для transform — драг-фоллоу (app.js,
   sheetSwipeUpHandler-делегат) снимает её на время активного жеста
   (1:1 с пальцем) и возвращает на touchend/срабатывании, чтобы возврат
   к 0 был плавным, а не рывком. */
.rt-hint { display: block; margin: 18px -28px 0; padding: 6px 12px; text-align: center; color: var(--on-scrim-2); font-size: var(--f-sm); transition: transform .15s ease; }

/* Атрибуция кадра банка (план fix-2026-08-28-photos-diag.md, «Риски»:
   CC BY/BY-SA требуют видимую подпись) — самый третьестепенный текст
   шита, поэтому --on-scrim-3, не --on-scrim-2 у соседей выше: тот
   заведён именно под текст поверх фото (не путать с --text-3 — тот
   калиброван под плоский --bg, тут задник — тёмный градиент .rt-full
   .ex-scrim, который НЕ меняется между темами, см. комментарий там же,
   так что --on-scrim-3 тоже не нужно переопределять на body.light). */
.rt-credit { margin: 8px 0 0; font-size: var(--f-xs); color: var(--on-scrim-3); }

/* ═══ «Истории» (ограниченный релиз за SHOW_STORIES, app.js) ═══════════
   Шит openStoriesSheet() — вертикальная snap-лента ВО ВЕСЬ экран, не
   карусель с peek, как .dl-carousel выше: тут каждая карточка = 100%
   высоты ленты (задача владельца «во весь экран»), соседей не видно.
   v17 (тот же приём, что и у .dl-carousel — см. её комментарий «Ревью
   Opus v17, P1» выше по файлу): position:relative на .story-lane —
   иначе offsetParent слотов уходит выше по дереву, и rAF-детектор в
   app.js (openStoriesSheet) мерил бы scrollTop не от того контейнера.
   Спейсер-докрут ::after у .dl-carousel («Фикс владельца v17, недокрут
   последней карточки») тут НЕ нужен: по формуле того же комментария
   S=peek, а peek (высота выглядывающего соседа) тут архитектурно 0. */
.story-lane {
  position: relative; height: 100%;
  display: flex; flex-direction: column;
  overflow-y: auto; overflow-x: hidden; scroll-snap-type: y mandatory;
  -webkit-overflow-scrolling: touch; scrollbar-width: none;
  overscroll-behavior-y: none; touch-action: pan-y;
}
.story-lane::-webkit-scrollbar { display: none; }
.story-slot {
  flex: 0 0 100%; scroll-snap-align: start;
  /* Тот же фикс владельца v17, что и .dl-slot выше (проскальзывание
     свайпа) — без stop:always резкий фling проезжает мимо промежуточных
     карточек к дальней снэп-точке. */
  scroll-snap-stop: always;
  content-visibility: auto; contain-intrinsic-size: 0 600px;
}
.story-card { position: relative; height: 100%; overflow: hidden; background: var(--surface-2); }
/* Честная пустота фильтра (масштабирование 07.09.2026, задача владельца
   §3) — emptyState() (app.js) рисует .empty ПРЯМО в #story-lane, когда
   комбинация kind+decade не даёт ни одной карточки: у .empty (см. её
   блок выше по файлу) нет своего фото/фона, показывает то, что лежит
   под .story-lane (плоский фон шита) — обычные --text/--text-2 токены
   .empty тут читаются нормально (не над фото, --on-scrim не нужен).
   padding-top отодвигает блок НИЖЕ строки переключателя/шапки (.rt-full-
   actions + .story-seg, обе position:absolute поверх .story-lane) — без
   этого текст пустого состояния рисовался бы В НАЧАЛЕ потока и лежал
   бы визуально ПОД чипами, а не под ними. */
.story-lane .empty { padding-top: calc(env(safe-area-inset-top, 0px) + 150px); min-height: 100%; }
/* Второй слой поверх --photo-scrim — тот же приём двух background через
   запятую, что и .rt-full .ex-scrim выше (эталон значений: блок эйброу+
   тайтл+текст+CTA карточки истории такой же высокий, как у «Маршрута»/
   «Сделки», обычного --photo-scrim не хватает под двухстрочным
   заголовком — тот же класс проблемы, см. комментарий у .rt-full
   .ex-scrim). */
.story-card .ex-scrim {
  background: var(--photo-scrim),
    linear-gradient(180deg, transparent 28%, rgba(14, 15, 19, .58) 54%, rgba(14, 15, 19, .92) 100%);
}
.story-photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* Винтажный скан — CSS-фильтр поверх файла как есть (просьба владельца
   дословно, не переобработка самого JPEG); НЕ .ex-photo — та несёт
   brightness(.78) тёмной темы и live-рефреш темы (refreshVisiblePhotos,
   ищет [data-iata]), архивный скан 1924 года не подписан ни на то, ни на
   другое — остаётся визуально одинаковым в обеих темах, так и задумано. */
/* opus-ревью, «желательно» §в: скан снят без учёта текстового оверлея
   снизу карточки (см. .story-card .ex-scrim выше) — базовый center/center
   .story-photo резал ровно ту часть кадра (яхту), которую и полагалось
   показать; сдвигаем «окно» cover выше, к 30% от верха. */
.story-photo-vintage { filter: sepia(.25) grayscale(.2); object-position: center 30%; }
.story-eyebrow-wrap {
  position: absolute; left: 20px; right: 20px; z-index: 2;
  /* Было 76px (44px кнопка + 14px её top-отступ + запас) — 07.09.2026
     (масштабирование, задача владельца §3) добавила строку фильтров
     (.story-seg ниже, 11.09.2026 — переключатель вместо чипов, та же
     высота) МЕЖДУ .rt-full-actions и эйброу: +44px высота (тот же
     min-height:44px, что и у общего .chip, см. её блок «фильтры» выше по
     файлу) + 10px зазор = ещё 54px, тот же запас в конце сохранён, иначе
     пилюля лейбла карточки перекрывает переключатель. */
  top: calc(env(safe-area-inset-top, 0px) + 130px);
}
.story-eyebrow {
  display: inline-flex; align-items: center; max-width: 100%;
  padding: 8px 13px; border-radius: 14px;
  background: var(--scrim-btn-bg); color: var(--on-scrim);
  font-size: var(--f-xs); font-weight: 600; letter-spacing: .14em; text-transform: uppercase;
  line-height: 1.45; white-space: normal;
}
/* .story-content — маркер-класс поверх .rt-full-content (позиционирование/
   z-index/цвет уже даёт она, см. её правило выше) — своя типографика
   заголовка/текста ниже. */
.story-title {
  margin: 0; color: var(--on-scrim);
  font-size: clamp(24px, 8vw, 34px); line-height: 1.12; font-weight: 300;
  letter-spacing: -.02em; overflow-wrap: anywhere;
}
.story-text { margin: 14px 0 0; color: var(--on-scrim-2); font-size: var(--f-base); line-height: 1.4; max-width: 340px; }
.story-cta-row { display: flex; gap: 10px; margin-top: 20px; }
.story-cta { width: 100%; }
.story-swipe {
  margin: 16px 0 0; text-align: center; pointer-events: none;
  font-size: var(--f-xs); font-weight: 500; letter-spacing: .1em; text-transform: uppercase;
  color: var(--on-scrim-3);
}
/* Счётчик «N из 8» — стеклянная пилюля-подложка над фото, тот же приём,
   что и .dl-badge (не .dl-switch-count карусели — та калибрована под
   плоский --bg, тут задник — фото/сцена). Сидит в .rt-full-actions
   (переиспользована как есть — back слева/счётчик справа тем же
   justify-content:space-between, что уже даёт «Маршруту»/«Сделке» back+
   поделиться/избранное) — фиксирована над .story-lane, не скроллится
   вместе с карточками. */
.story-count {
  padding: 8px 12px; border-radius: 14px; background: var(--scrim-btn-bg);
  color: var(--on-scrim-2); font-size: var(--f-sm); font-weight: 500;
  pointer-events: none;
}

/* Переключатель периода читалки (задача владельца п.7, 11.09.2026) —
   своя строка МЕЖДУ .rt-full-actions (back+счётчик) и .story-eyebrow-wrap
   (та сдвинута ниже, см. её комментарий выше). Было 3 чипа kind + рельс
   чипов-десятилетий (07.09.2026, «chips, v9 glass style», §3) — с виду
   чужеродно рядом с остальным языком приложения, а на 320px рельс декад
   срезался маской. Теперь один сегментированный переключатель — тот же
   язык «пилюля-в-пилюле», что и .ds-seg шторки «Даты и цены» (см. её
   комментарий выше по файлу), но inline-flex, не на всю ширину (задача:
   «компактный, без рельса» — 3 подписи сами по себе короткие, тянуть их
   в разные концы экрана незачем) и стеклянный фон вместо плоского
   --surface-2 — контрол лежит ПОВЕРХ фото карточки, тот же язык, что и
   у .story-eyebrow/.story-count рядом (var(--scrim-btn-bg)/var(--on-scrim)).
   Класс `rail` в разметке (app.js) сохранён не про раскладку (та своя,
   ниже) — чтобы остаться в GESTURE_EXCLUDE_SEL (app.js, свайп-вниз-
   закрыть шторки), та же причина, что и была у .story-filters. */
.story-seg {
  position: absolute; left: 20px; z-index: 2;
  top: calc(env(safe-area-inset-top, 0px) + 68px);
  display: inline-flex; gap: 2px; padding: 3px;
  background: var(--scrim-btn-bg); border-radius: 999px;
}
/* Та же формула «визуально 32px / хит-зона 44px», что уже держит общий
   .chip (см. её ревью-комментарий v12, P1, «фильтры» выше по файлу) —
   min-height поверх фиксированной height клампит фактическую высоту
   вверх, не трогая видимый размер пилюли. Ширину .story-seg не задаём —
   3 подписи с паддингами укладываются в ≈218px (посчитано вручную: нет
   headless-браузера в песочнице), запас даже на 320px-экране (320 - 20
   left - 20 условный правый отступ = 280px свободно). */
.story-seg button {
  flex: none; height: 32px; min-height: 44px; display: inline-flex; align-items: center;
  padding: 0 13px; border-radius: 999px; white-space: nowrap;
  font-size: var(--f-sm); font-weight: 600; color: var(--on-scrim-2);
  transition: color .15s ease, background .15s ease, transform .12s ease-out;
}
.story-seg button.is-active { color: var(--on-accent); background: var(--accent); font-weight: 700; }
.story-seg button:active { transform: scale(.96); }

/* ═══ ридер полной статьи (03.09.2026, openArticleSheet, app.js) ═══════
   Обычный (НЕ rt-immersive) шит — задача владельца прямо просит
   «бумажный» фон светлой темы, не фото на весь экран: .sheet уже красит
   var(--bg), body уже красит var(--text) поверх него (app.css, самое
   начало файла) — «бумага в light / тёмный фон+тёплый белый текст в
   dark» получается БЕСПЛАТНО, просто не навешивая rt-immersive и не
   переопределяя ни цвет фона, ни цвет текста тут ни разу. Журнальная
   типографика — только размеры/интервалы/приглушение, ни одного
   хардкод-цвета: --text (наследуется), --text-2 (лид, как .story-text/
   .rt-blurb у соседних экранов), --text-3 (подпись/кредит — тот же
   аудированный WCAG-минимум ≥4.5:1 на ОБЕИХ --bg, что у .crumbs). */
.art-shell { padding-top: 6px; }
/* Лейбл-пилюля — тот же язык, что у .hc-txt .hc-badge (Профиль,
   «Основной» бейдж): --accent-soft/--accent-text уже дают контрастную
   пару на плоском --bg в ОБЕИХ темах (в отличие от .story-eyebrow —
   тот стеклянный, калиброван под фото, тут фото под лейблом нет). */
.art-label {
  display: inline-block; padding: 6px 14px; border-radius: 999px;
  background: var(--accent-soft); color: var(--accent-text);
  font-size: var(--f-xs); font-weight: 700; letter-spacing: .06em;
}
/* Тот же клэмп/вес/трекинг, что у .story-title карточки — тот же
   визуальный язык «Историй», просто на бумаге, не на фото (цвет тут не
   переопределяем — --on-scrim там был ради контраста НАД фото, тут
   обычный var(--text) от body). */
.art-title { margin: 14px 0 0; font-size: clamp(26px, 8vw, 34px); line-height: 1.12; font-weight: 300; letter-spacing: -.02em; overflow-wrap: anywhere; }
.art-lead { margin: 14px 0 0; font-size: var(--f-base); line-height: 1.5; font-style: italic; color: var(--text-2); }
/* Секционные заголовки блоков — вес 500 (промпт «никакого 600-700 в
   заголовках» держится и тут, тот же принцип, что у .sheet-head h3). */
.art-h3 { margin: 32px 0 10px; font-size: var(--f-xl); line-height: 1.3; font-weight: 500; letter-spacing: -.01em; }
.art-h3:first-child { margin-top: 24px; }
/* max-width ~34em/line-height 1.55 — дословно ТЗ владельца («журнальная
   мера строки»); на мобильном экране (реальный viewport мини-аппа) сам
   .sheet-body уже уже 34em, ограничение — подстраховка на широких
   экранах (Telegram Desktop), не рабочая мера тут. */
.art-p { margin: 12px 0 0; max-width: 34em; line-height: 1.55; }
.art-figure { margin: 20px 0 0; }
/* Фото во всю ширину со скруглением (--r-md — «мелкие/средние элементы»,
   не --r-card: та калибрована под герой-карточки во весь экран, тут фото
   внутри читаемой колонки). Сепия — ТЕ ЖЕ значения, что у .story-photo-
   vintage карточки (задача владельца «как на карточке»); отдельное
   правило, не общий класс — .story-photo-vintage несёт position:absolute
   под карточку-фон, тут обычное блочное изображение в потоке текста. */
.art-img { display: block; width: 100%; border-radius: var(--r-md); filter: sepia(.25) grayscale(.2); }
.art-caption { margin: 8px 2px 0; font-size: var(--f-xs); line-height: 1.4; color: var(--text-3); }
.art-end { margin: 32px 0 4px; padding-top: 20px; border-top: 1px solid var(--border); }
.art-credit { margin: 0 0 14px; font-size: var(--f-xs); color: var(--text-3); }
.art-cta { width: 100%; }

/* «Похожие направления» (план v11 §4) — горизонтальный рельс мини-карточек
   под кнопками «Маршрута»: тот же PhotoCard-язык (.ex-photo/.ex-scrim),
   что и у остальных карточек банка фото, просто 120×160 вместо во весь
   экран. Скроллбар спрятан — рельс в узком immersive-содержимом, не
   отдельный экран со своим местом для полосы прокрутки. */
.rail { display: flex; gap: 10px; overflow-x: auto; padding: 2px 2px 4px; scrollbar-width: none; }
.rail::-webkit-scrollbar { display: none; }
.rail-card {
  position: relative; flex: 0 0 120px; width: 120px; height: 160px;
  border-radius: 16px; overflow: hidden; text-align: left;
}
/* Ревью v12, P2: карточки рельса 120×160 режут cover ещё жёстче, чем
   основная карточка — яркие дневные кадры банка ломают «тёмный
   атмосферный» язык (видно рядом с более тёмными соседями рельса).
   Притемняем в обеих темах — .ex-photo сам уже снимает фильтр в
   body.light (промпт: «сцены/фото оставь тёмными как есть», см.
   комментарий у .ex-photo), тут своя, чуть мягче основной (.78). */
.rail-card .ex-photo { filter: brightness(.7); }
/* body.light .ex-photo{filter:none} (выше по файлу) специфичнее одной
   .rail-card .ex-photo — без явного повтора тут светлая тема снимала бы
   и это притемнение тоже. */
body.light .rail-card .ex-photo { filter: brightness(.7); }
.rail-city {
  position: absolute; left: 10px; right: 10px; bottom: 30px; z-index: 2;
  font-size: 13px; font-weight: 500; color: var(--on-scrim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rail-price { position: absolute; left: 10px; right: 10px; bottom: 10px; z-index: 2; font-size: 11px; color: var(--on-scrim-2); }

/* ═══ Главная: «Истории» (волна 1.3) ═══════════════════════════════════
   Разметка — renderHmStories()/hmStoryCardHtml() (app.js, тот же
   render*Html-контракт, что у соседнего renderHmWeekend): `.hm-head`/
   `.hm-all` — общая шапка секции («Заголовок + Все N →»), делят имя с
   соседним рельсом «На выходные» (пока нигде не оформлены — стилуем тут
   минимально/нейтрально, без магических чисел под конкретный рельс,
   чтобы не разъехаться с ним визуально). `.rail.hm-rail` — тот же приём:
   `.rail` (выше по файлу, §v11) уже даёт flex/overflow-x/скрытый
   скроллбар (и уже в IGNORE_SEL свайпа табов, app.js #scroller) — тут
   только добавляем gap/padding под задачу владельца.
   `.hm-story-card` — 120×160, СВОЯ фоновая логика (не `.hm-card` без
   модификатора — тот размер/контент задаёт каждый рельс сам, тут не
   трогаем): у истории нет единого кадра-героя (гайд города vs винтажный
   скан), фон — сплошной цвет (hash по IATA, cityHue()/hmStoryCardHtml,
   app.js). --hm-hue пишет inline-style сама карточка, CSS его только
   читает — тот же приём переменной на элементе, что и filter:hue-rotate()
   у процедурных сцен (.scene, ниже по файлу).
   Редизайн 04.09 (мокап владельца): градиент поднят с 55%/34→18%
   lightness до 60%/44→26% — прежние цифры на большинстве оттенков
   читались почти чёрными (жалоба «карточки слишком тёмные»), теперь
   разные IATA дают явно различимые синий/коричневый/зелёный, а не один
   и тот же графит. Крупная полупрозрачная буква (`.hm-story-face`,
   наезжала на заголовок — аудит 04.09) заменена тонким кольцом
   `.hm-story-ring` сверху-слева (тот же язык, что сторис-ринг Telegram,
   просто декоративный кружок без анимации/непрочитанного состояния —
   этот UI ничего подобного не хранит). */
.hm-head {
  display: flex; align-items: baseline; justify-content: space-between;
  padding: 0 2px; margin-bottom: 2px;
}
.hm-head h2 { font-size: 17px; font-weight: 700; margin: 0; }
.hm-all {
  background: none; border: 0; padding: 4px; margin: 0;
  font-size: 13px; color: var(--muted); cursor: pointer;
}
.hm-rail { gap: 12px; padding: 16px 0; align-items: center; }
/* Составной селектор .hm-card.hm-story-card (специфичность 0,0,2,0) —
   финальный аудит 04.09: разметка карточки несёт ОБА класса, а голый
   .hm-card объявлен НИЖЕ по файлу и при равной специфичности затирал
   весь этот блок (и размер 120×160 → 140×180, и приглушённый градиент →
   кислотный 60%-ный). Составной селектор побеждает независимо от порядка
   объявлений — тот же приём, что у .hm-card.hm-skel ниже. */
.hm-card.hm-story-card {
  position: relative; flex: 0 0 120px; width: 120px; height: 160px;
  border-radius: var(--r-md); overflow: hidden; text-align: left;
  padding: 10px; cursor: pointer;
  display: flex; flex-direction: column; justify-content: flex-end; gap: 2px;
  color: var(--on-scrim); box-shadow: var(--shadow-sm);
  /* Приглушённая тёмная палитра (мокап 04.09: тёмно-синий/коричневый/
     тёмно-зелёный, не кислотные): sat 42-46%, lightness 36→20 — белый
     текст читается без отдельного скрима. */
  background: linear-gradient(160deg,
    hsl(var(--hm-hue, 230) 42% 36%), hsl(calc(var(--hm-hue, 230) + 26) 46% 20%));
  transition: transform .15s ease;
}
.hm-card.hm-story-card:hover, .hm-card.hm-story-card:active { transform: scale(1.05); }
/* Фото-обложка (задача владельца 04.09) — hmStoryCoverHtml()/app.js кладёт
   <img class="ex-photo hm-story-cover"> ВТОРЫМ слоем НАД hue-градиентом
   выше (сам градиент в разметке не участвует, background статичен на
   .hm-card.hm-story-card): бэйр-рул `.ex-photo` (выше по файлу) уже даёт
   position:absolute;inset:0;object-fit:cover — своего позиционирования
   `.hm-story-cover` не требует, класс тут только адресный хук на карточку
   рельса, а не сама раскладка. Нет фото/сбой загрузки — <img> либо не
   рисуется вовсе (hmStoryCoverHtml вернула ''), либо onerror убирает её
   (`this.remove()`) — hue-градиент под ней ничем не перекрыт, тот же
   принцип двух слоёв «без единой строчки JS-фолбэка сверх удаления
   картинки», что и у .hm-hot-card ниже по файлу (см. её комментарий).
   Скрим снизу — `var(--photo-scrim)`, ТОТ ЖЕ токен, что уже держит
   `#hm-weekend .hm-card::after` (задача владельца «как у карточек На
   выходные — посмотри их ::after»); рисуем его ВСЕГДА (не только при
   фото) — на голом hue-градиенте скрим почти не виден (градиент и так
   тёмный понизу), а при фото не нужно второе правило под условие «есть
   ли кадр». Кольцо/заголовок/подпись — z-index:2 ЯВНО: `.hm-story-ring`
   и без того позиционирован (z-index уже имел смысл), а
   `.hm-story-title`/`.hm-story-cap` — flex-элементы .hm-card.hm-story-card
   (display:flex) без своего position; спека Flexbox делает z-index
   значимым для flex-детей и без position:relative (тот же факт, на
   который полагается остальной файл — .hm-hot-info и соседи просто
   дублируют position:relative явно, тут не стали множить свойство без
   нужды, поведение идентично). */
.hm-card.hm-story-card::after {
  content: ''; position: absolute; inset: 0; z-index: 1;
  background: var(--photo-scrim); pointer-events: none;
}
.hm-story-ring {
  position: absolute; top: 10px; left: 10px; width: 22px; height: 22px;
  border: 2px solid rgba(255, 255, 255, .55); border-radius: 50%;
  z-index: 2;
}
.hm-story-title {
  z-index: 2;
  font-size: 14px; font-weight: 700; line-height: 1.2;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.hm-story-cap { z-index: 2; font-size: 12px; color: var(--on-scrim); opacity: .6; }
/* Трейлинг-карточка «Все истории →» (масштабирование 07.09.2026, задача
   владельца §4, hmStoryAllCardHtml/app.js) — та же геометрия/поведение,
   что .hm-card.hm-story-card (120×160, hover/active), но своя палитра:
   нет фото/hue под ней (не выпуск, служебная плитка), поэтому --on-scrim
   (тем-инвариантный белый, калиброван под фото/скрим) на её плоском
   --surface-2 в светлой теме читался бы почти невидимым — тот же класс
   бага, что уже чинили у .hm-chip (см. её комментарий выше по файлу),
   тут просто не даём ему случиться: свои, тем-следящие --text/--text-2.
   Три класса (0,0,3,0) специфичнее .hm-card.hm-story-card (0,0,2,0) —
   побеждает независимо от порядка объявлений, тот же приём, что и у
   составного селектора выше. */
.hm-card.hm-story-card.hm-story-all {
  background: var(--surface-2); color: var(--text); justify-content: center;
}
.hm-card.hm-story-card.hm-story-all::after { content: none; }
.hm-story-all .hm-story-cap { color: var(--text-2); opacity: 1; }

/* ─── Главная: контейнер (аудит 04.09, P1) ────────────────────────────
   Тот же зазор снизу, что main{} (выше по файлу) уже считает для ВСЕХ
   табов — calc(--tabbar-h + --safe-b + 20px) — закреплён явно и на
   #home-view, не только унаследован от main{}: тот же узел, что уже
   чистит свой инлайн-паддинг homeRecalcLayout() перед каждым рендером
   (app.js, у объявления функции комментарий про будущий JS-спейсер волны
   2 — этот статичный CSS-паддинг не конфликтует, сбрасывается только
   инлайн-стиль).
   Живая проверка ректами (Playwright, 390×844): main{} и ДО этой правки
   честно докручивал последнюю секцию («Дешевле обычного») выше таббара
   на любом городе — цифры не изменились. Эта строка не про то: она даёт
   #home-view тот же явный, самодокументированный контракт, что и у узла,
   который homeRecalcLayout() уже держит на примете под будущий спейсер.
   Что эта строка НЕ чинит и физически не может (padding-bottom двигает
   пространство ПОСЛЕ контента, не сам контент): на первом входе, пока
   сайт не проскроллен, карточки «Историй» всё равно частично уходят под
   .tabbar (y:756-816 на 844-высоком вьюпорте) — контента над ними просто
   больше одного экрана. .tabbar — стеклянная полупрозрачная пилюля
   (backdrop-filter, --nav-bg с альфой .55/.65, см. её блок ниже по файлу)
   и ВЕЗДЕ в приложении рассчитана на то, что контент проходит ПОД ней при
   скролле — тем же образом, что и на любом другом табе с контентом длиннее
   экрана, не регрессия именно Главной. */
#home-view { padding-bottom: calc(var(--tabbar-h) + var(--safe-b) + 20px); }

/* ─── Главная: топбар (редизайн под мокап владельца, 04.09) ────────────
   Разметка — renderHmTop() (app.js, рядом с renderHmHero). FD слева —
   ГЛОБАЛЬНЫЙ `.dl-logo` таба «Куда угодно» (его блок ниже по файлу не
   трогаем, просто ссылаемся). Капс-капсула — своя `.hm-cap` (не
   `.dl-cap`: та калибрована под пару со свитчем режима, тут нужен
   настоящий центр между FD и лупой) — flex:1 с text-align:center вместо
   left/right даёт капсуле центр независимо от того, что она чуть шире
   слева (FD) или справа (лупа). Лупа — `.ic.glass` как есть (та же
   стеклянная иконка-кружок, что и везде в файле), `.hm-top .ic{flex:none}`
   страхует её от сжатия на узких экранах рядом с flex:1 капсулой (тот же
   приём, что `.dl-mode-toggle{flex:none}` — таб 2, не трогаем). */
#hm-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 2px 2px 18px; }
#hm-top .ic { flex: none; }
.hm-cap {
  flex: 1; min-width: 0; display: flex; align-items: center; justify-content: center; gap: 5px;
  min-height: 44px; font-size: var(--type-cap-size); letter-spacing: var(--type-cap-tracking);
  text-transform: uppercase; color: var(--mute); font-weight: 400;
}
.hm-cap-chev { flex: none; font-size: 9px; color: var(--text-3); transform: translateY(1px); }

/* ─── Главная: hero ALL (волна 1.1; редизайн под мокап владельца 04.09) ──
   Тёмная карточка (`--surface-2`/`--r-card` — тот же радиус-калибр, что у
   шитов) вместо голого текста на фоне таба. Чип «N сделок» — `.glass`
   утилита как есть (та же, что `.ic.glass`/`#dl-mode-toggle` — контраст
   на плоском `--bg`/`--surface-2` уже проверен ревью v12, см. её блок
   выше по файлу), `align-self:flex-start` держит его слева, пока всё
   остальное в карточке центровано. Заголовок теперь статичный «ВЕСЬ МИР»
   (не имя города — hero целиком ведёт в скоуп ALL, см. heroCta в app.js,
   заголовок называет ЕГО), `.hm-hero-cta` — та же `.pill` (белая, тёмный
   текст, радиус-пилюля — все три уже даёт класс `.pill` выше по файлу),
   тут только сузили до контентной ширины и убрали конкурирующие
   background/color/radius, которые раньше её перекрашивали в узкий
   `--r-md` прямоугольник на всю ширину.
   Курсивная редакционная строка (`.hm-editorial`, волна 2.2) снята
   вместе с блоком её комментария — единственным потребителем класса был
   именно этот hero (проверено грепом), решение аудита 04.09: редакция
   переехала в «Дешевле обычного» (другая зона правок). */
#hm-hero { padding: 24px 16px 0; }
.hm-hero-skel { width: 100%; height: 240px; margin-bottom: 16px; border-radius: var(--r-card); }
.hm-hero-card {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  padding: 20px 20px 24px; background: var(--surface-2); border-radius: var(--r-card);
  /* Фото (app.js renderHmHero, `.hm-hero-photo`) — bare `.ex-photo` (выше
     по файлу) уже кладёт его absolute;inset:0 внутри карточки, тут только
     контейнер: position/overflow нужны заранее — без фото они ни на что
     не влияют, с фото не дают кадру/скриму вылезти за скруглённый угол
     радиусом --r-card. */
  position: relative; overflow: hidden;
}
/* Скрим фото-фона (задача владельца: «плотный тёмный скрим, ~55-70%, по
   краям сильнее») — СВОЙ градиент, не `var(--photo-scrim)`: тот калиброван
   под текст ТОЛЬКО в нижней трети карточки (обычные photo-card файла),
   тут же фото — атмосфера ПОД центрированным текстом на всю высоту карточки
   (заголовок вверху, кнопка внизу) — нужна равномерная плотная дымка, не
   только нижний растяг. radial здесь и даёт «плотнее по краям» (задача
   дословно) при более светлой сердцевине под самим кадром. Рисуем ТОЛЬКО
   при `.hm-has-photo` (app.js — ставит класс, только когда есть фото И оно
   реально загрузилось; onerror снимает класс разом с `<img>`, см. её
   комментарий) — без фото карточка остаётся `--surface-2`, без скрима,
   как и раньше. */
.hm-hero-photo { z-index: 0; }
.hm-hero-card.hm-has-photo::after {
  content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: radial-gradient(140% 110% at 50% 22%,
    rgba(14, 15, 19, .58) 0%, rgba(14, 15, 19, .74) 55%, rgba(14, 15, 19, .9) 100%);
}
/* Тём-инвариантный текст/пилюля НАД фото (задача владельца: карточка с
   фото остаётся тёмной в ОБЕИХ темах — см. комментарий renderHmHero,
   app.js, почему: инверсия под body.light тут дала бы кремовую подложку
   под фото направления). `--on-scrim`/`--on-scrim-2` — те же тем-
   инвариантные токены, что держат любую другую фото-карточку файла (dl-
   info/rt-full-content и соседи), не новый цвет. Специфичность класса
   `.hm-hero-card.hm-has-photo .hm-hero-chip`/`…-cta` (0,0,3,0) выше, чем у
   `body.light .hm-hero-chip`/`.pill` (0,0,2,0 и 0,0,1,0) — выигрывает
   независимо от порядка объявления в каскаде, ни одно из тех правил не
   трогаем. */
.hm-hero-card.hm-has-photo { color: var(--on-scrim); }
.hm-hero-card.hm-has-photo .hm-hero-stat { color: var(--on-scrim-2); }
/* Титул и цена — тот же пин (финальный аудит 04.09): без него в светлой
   теме оба наследовали body.light --text (тёмный) и терялись на фото. */
.hm-hero-card.hm-has-photo .hm-hero-title,
.hm-hero-card.hm-has-photo .hm-hero-price { color: var(--on-scrim); }
.hm-hero-card.hm-has-photo .hm-hero-chip {
  background: rgba(255, 255, 255, .16); border-color: rgba(255, 255, 255, .24); color: var(--on-scrim);
}
.hm-hero-card.hm-has-photo .hm-hero-cta { background: #f4f1ea; color: #0e0f13; }
.hm-hero-chip, .hm-hero-title, .hm-hero-stat, .hm-hero-price, .hm-hero-cta {
  position: relative; z-index: 2;
}
.hm-hero-chip {
  align-self: flex-start; padding: 5px 12px; border-radius: 999px;
  font-size: 12px; margin-bottom: 14px;
}
.hm-hero-title {
  font-size: 28px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  margin: 0 0 6px;
}
.hm-hero-stat { font-size: 14px; color: var(--muted); margin: 0 0 14px; }
.hm-hero-price {
  font-size: 38px; font-weight: 700; letter-spacing: -.01em; margin: 0 0 18px;
  font-variant-numeric: tabular-nums;
}
.hm-hero-cta { width: 60%; min-width: 160px; }

/* ─── Главная: горящее сейчас (волна 1.1 → 1.5; 11.09.2026 — рельс
   визуально СЛИТ с «На выходные», задание владельца: «выглядит скомкано»,
   сделать секцию единой с соседом, docs/research/ux-fixes-map-2026-09-11.md) ──
   Шапка — общие .hm-head/.hm-all/h2 (стилуются в блоке «На выходные»
   ниже по каскаду, задача владельца «переиспользуй .hm-head»), но левый
   инсет теперь пришпилен ЯВНО в `#hm-hot .hm-head` ниже: раньше секция
   получала padding:0 16px 8px ТОЛЬКО потому, что второе объявление
   голого `.hm-head` (§«На выходные», ниже по каскаду) побеждает первое
   при равной специфичности — хрупко (переставь блоки местами или заведи
   третье `.hm-head`, и шапка тихо съедет к padding:0 2px первого
   объявления). Своя копия тут не полагается на порядок остальных.
   .hm-chips — тот же контейнер-рельс, что и раньше (уже в IGNORE_SEL
   свайпа табов, app.js #scroller — новых точек свайпа тут не
   прибавилось), 16px слева — тот же инсет, что рельс карточек ниже
   (задача владельца «выровненные отступы»).

   Карточки (11.09.2026, п.1/14 → правка «единый вид»): рельс `.rail.hm-rail`
   БЕЗ своего `#hm-hot .rail.hm-rail`-переопределения — тот же голый
   `.rail`/`.hm-rail` (§v11 выше по файлу), что и у «На выходные» ниже по
   каскаду, тот же left-inset 16px (даёт `main{ padding: 18px var(--pad) }`
   контейнеру, `--pad:16px`, а не сам рельс — «На выходные» и так всегда
   полагался на этот инсет, свой mandatory-снап `#hm-hot` раньше добавлял
   ВТОРОЙ 16px горизонтальный padding поверх (32px итого) — тот самый
   «разъехавшийся» рельс, который правка убирает). Сама карточка — БУКВАЛЬНО
   `.hm-card` (см. её блок у «На выходные» ниже по каскаду: ширина 140,
   высота 180, фон-двухслойник hue+`--hm-photo`, .hm-badge/.hm-fav/.hm-city/
   .hm-meta/.hm-price) — свой параллельный набор `.hm-hot-card`/`.hm-hot-skel`/
   `.hm-hot-badge`/`.hm-hot-info`/`.hm-hot-row`/`.hm-hot-city`/`.hm-hot-price`/
   `.hm-hot-meta` (280×208 landscape, текст в absolute-блоке снизу) снят
   этой правкой целиком — hmHotCardHtml (app.js) рисует ту же разметку, что
   и hmCardHtml-блок в renderHmWeekend, класс `hm-hot-card` на элементе
   остался только JS-маркером для делегата (siblings/index рельса, своя
   ветка .hm-fav), CSS на нём больше нет. Вечерняя правка того же дня
   (живой скрин: подпись переносилась на 2 строки и наезжала на название,
   зачёркнутая «обычная» цена добавляла 4-ю строку текста) убрала и
   `.hm-hot-usual` — карточка «Горящего» теперь СТРОГО `.hm-card` без
   единого довеска сверх него, .hm-meta несёт только дату (hmHotDateRange,
   app.js), не дату+тип поездки. */
#hm-hot { padding: 20px 0; }
#hm-hot .hm-head { padding: 0 16px 8px; }
.hm-chips {
  display: flex; gap: 8px; overflow-x: auto; -webkit-overflow-scrolling: touch;
  padding: 0 16px; margin: 0 0 4px; list-style: none;
}
.hm-chip {
  /* Аудит 2026-09-05 (home-explore-stories.md, P2-2): min-height:44px, тот
     же порог, что .chip выше по файлу — не меняет видимый размер пилюли
     (padding остаётся тем же), просто увеличивает кликабельную область;
     display:flex центрирует текст, если min-height превысит контент. */
  flex: 0 0 auto; padding: 6px 12px; background: var(--scrim-2);
  color: var(--on-scrim); border: 1px solid transparent; border-radius: 20px;
  font-size: 12px; white-space: nowrap; cursor: pointer;
  display: inline-flex; align-items: center; min-height: 44px;
}
.hm-chip.is-active { background: var(--ink); border-color: var(--ink); color: var(--bg); }
.hm-chip-skel { flex: 0 0 80px; height: 24px; border-radius: 12px; }

/* ─── Главная: на выходные (волна 1.2; редизайн под мокап владельца 04.09) ──
   Карточка — фото города (`--hm-photo`, инлайн custom property из
   cityPhotoOrCoverAttrs(), app.js) ВТОРЫМ слоем НАД hue-градиентом
   (`--hm-hue`, та же формула, что подняла `.hm-story-card` выше по
   файлу) — не общая `<img class="ex-photo">`/`cardImgErr` машинерия:
   та живёт за пределами этой зоны правок (её closest-список не знает
   `.hm-card`), а два background-слоя на самом элементе дают тот же
   «фото, а нет фото — свой цвет» без единой строчки JS-обработчика
   ошибок — 404/нет кадра просто не рисует верхний слой, градиент
   (заданный ПРЯМО в этом правиле, не только у `.hm-has-photo`) остаётся
   виден как есть. `#hm-weekend .hm-card::after` — тот же `--photo-scrim`
   токен, что у `.ex-scrim` (общий градиент-затемнение снизу, см. его
   блок выше по файлу) — держит текст читаемым и на светлых кадрах, и на
   пустом hue-фоне; СКОПИРОВАН на `#hm-weekend` (не голый `.hm-card`) —
   живая проверка 04.09 поймала регрессию: `.hm-story-card` тоже несёт
   класс `hm-card` (`hmStoryCardHtml`, app.js), а её `.hm-story-title`/
   `.hm-story-cap` — обычные statically-позиционированные span'ы, не
   `position:relative`; голый `.hm-card::after` (position:absolute,
   z-index:1) красился ПОВЕРХ них независимо от DOM-порядка (позиционный
   элемент всегда красится над непозиционированным соседом), заголовки
   историй гасли до серого. Скоуп по контейнеру — тот же приём, что уже
   держит `#hm-cheaper .hm-head`/`#hm-collections .hm-head` в файле.
   Бейдж и сердечко поменялись углами (мокап: скидка слева, избранное
   справа) — цвета тоже переведены на РЕАЛЬНО существующие токены:
   `--price-warn`/`--scrim`/`--scrim-2` нигде не объявлены (guaranteed-
   invalid custom property — тот же класс бага, что чинили в «Дешевле
   обычного» выше по файлу, см. её комментарий), background/color молча
   резолвились в transparent/inherit. `#f4f1ea`/`#0e0f13` — те же
   литералы, что `--ink`/`--bg` тёмной темы (см. токены в начале файла),
   продублированы явно НАМЕРЕННО: бейдж живёт НАД фото, а `--ink`/`--bg`
   переворачиваются в body.light — светлый чип должен остаться светлым в
   обеих темах (тот же принцип, что уже держит секция «Стекло/иконки/
   бейджи НАД фото», просто без правки её самой). is-good (зелёный чип)
   снят — мокап показывает единый светлый чип на всех уровнях скидки,
   is-huge (пульс) остался единственным сигналом «выгоднее соседних».
   `.hm-card.hm-skel` — компонентная специфичность (0,0,2,0) вместо
   мёртвого `--scrim-2` гарантирует, что шиммер `.skeleton` (объявлен
   выше по файлу) побеждает background-градиент `.hm-card` независимо от
   порядка правил в каскаде. */
#hm-weekend { padding: 20px 0; }
.hm-head {
  display: flex; justify-content: space-between; align-items: center;
  padding: 0 16px 8px;
}
.hm-card {
  position: relative; flex: 0 0 auto; width: 140px; height: 180px;
  border-radius: var(--r-md); overflow: hidden; padding: 10px;
  display: flex; flex-direction: column; justify-content: flex-end;
  text-align: left; color: var(--on-scrim);
  background: linear-gradient(160deg,
    hsl(var(--hm-hue, 230) 60% 44%), hsl(calc(var(--hm-hue, 230) + 26) 55% 26%));
  background-size: cover; background-position: center;
  box-shadow: var(--shadow-sm); cursor: pointer; transition: transform .15s ease;
}
/* Аудит 04.09 (фото-фолбэк, найдено вживую: карточка MRV в одном прогоне
   плоско-чёрная, в следующем — с фото): `background-image: var(--hm-photo)`
   в один слой ЗАМЕЩАЛ background-image, который короткая запись `background`
   у .hm-card выше тоже проставляет — упавший/заблокированный Unsplash-URL
   резолвился в невалидный image-слой, и весь hue-градиент пропадал вместе
   с ним (CSS background не умеет onerror). Второй слой ниже по списку —
   тот же градиент, что и у голого .hm-card (буквально те же hsl()/углы) —
   чинит это без JS: первый слой в background-image — верхний (спека CSS),
   так что фото рисуется поверх, а промах просто оставляет верхний слой
   прозрачным и открывает градиент под ним, как и было задумано в комментарии
   выше по файлу (§«На выходные»). */
.hm-card.hm-has-photo {
  background-image: var(--hm-photo), linear-gradient(160deg,
    hsl(var(--hm-hue, 230) 60% 44%), hsl(calc(var(--hm-hue, 230) + 26) 55% 26%));
}
#hm-weekend .hm-card::after {
  content: ''; position: absolute; inset: 0; z-index: 1;
  background: var(--photo-scrim); pointer-events: none;
}
.hm-card:hover, .hm-card:active { transform: scale(1.02); }
.hm-badge {
  position: absolute; top: 10px; left: 10px; z-index: 2; padding: 4px 8px;
  background: #f4f1ea; color: #0e0f13; font-size: 11px;
  font-weight: 700; border-radius: 14px;
}
/* Волна 3.1: пульс для «огромной» скидки (>=50%, HUGE_DISCOUNT в app.js) —
   привлекает взгляд к самым выгодным карточкам ряда. Гашение под
   prefers-reduced-motion — в общем блоке ниже по файлу (§
   «prefers-reduced-motion»), не тут, тем же приёмом, что
   .rd-card.rd-card-anim/.rs-dots и остальные анимации файла. */
.hm-badge.is-huge { animation: pulse 1.5s infinite; }
@keyframes pulse {
  0% { opacity: 1; }
  50% { opacity: .6; }
  100% { opacity: 1; }
}
.hm-fav {
  position: absolute; top: 10px; right: 10px; z-index: 2; width: 28px; height: 28px;
  padding: 4px; background: rgba(0, 0, 0, 0.1); border-radius: 50%;
  cursor: pointer; border: 0; transition: background .15s ease;
  display: flex; align-items: center; justify-content: center;
}
/* Аудит 2026-09-05 (home-explore-stories.md, P2-1): тот же хит-слоп приём,
   что у .dl-fav (карусель «Куда угодно», app.css выше) — 28+8+8=44,
   эффективная зона тапа дотягивает до порога без изменения видимого
   размера сердечка. */
.hm-fav::after { content: ''; position: absolute; inset: -8px; }
/* --price-warn(-rgb) — тот же guaranteed-invalid токен, что чинили у
   .hm-badge выше (нигде не объявлен) — is-fav молча резолвился в
   transparent/inherit, сердечко «в избранном» ничем не отличалось от
   обычного. --price-high реально объявлен (см. токены в начале файла) и
   уже несёт нужный смысл «акцент/внимание» в остальном файле. */
.hm-fav.is-fav { background: rgba(240, 149, 144, .25); color: var(--price-high); }
/* Заливка глифа в избранном — тот же приём, что .dl-fav.is-fav svg выше;
   литерал (не var(--ink)) — сердечко живёт на фото/hue-карточке, где вся
   иконография тем-инвариантно светлая (см. пин-лист body.light в начале
   файла: stroke уже закреплён как #f4f1ea в светлой теме — заливка обязана
   совпадать с ним в ОБЕИХ темах, а --ink в светлой стал бы тёмным). */
.hm-fav.is-fav svg { fill: currentColor; }
.hm-city { position: relative; z-index: 2; display: block; font-size: 15px; font-weight: 700; }
.hm-meta { position: relative; z-index: 2; display: block; font-size: 11px; color: var(--on-scrim-2); margin: 2px 0 4px; }
.hm-price { position: relative; z-index: 2; display: block; font-size: 18px; font-weight: 700; color: var(--on-scrim); }
.hm-card.hm-skel { background: var(--skeleton); background-size: 400% 100%; }
.hm-empty { text-align: center; padding: 20px 16px; color: var(--muted); font-size: 13px; }

/* ─── Главная: «Дешевле обычного» (волна 2.3 + редизайн 04.09) ───────
   Вертикальный список (не рельс, полная ширина) — тот же приём заголовка
   .hm-head/h2, что у #hm-weekend выше, подпись — общий .status. Ряд — своя
   разметка (не .hm-card, тому карточка-квадрат под рельс не подходит),
   строки разделены border-bottom. Задача владельца (мокап): без «Город →»
   слева (origin и так ясен из контекста Главной) — город назначения и
   редакционная/страновая подпись слева, цена и мелкий процент справа;
   .hm-cheaper-left/.hm-cheaper-right — те же две колонки строки, что и до
   редизайна, просто каждая теперь несёт два ряда текста, не один.
   border-bottom красился var(--scrim) — токен нигде в файле не объявлен
   (guaranteed-invalid custom property, см. :root — есть только --on-scrim/
   --photo-scrim), правило молча теряло цвет границы; var(--border) тут —
   тот самый токен-разделитель, что и везде в файле. */
#hm-cheaper { padding: 20px 0; }
#hm-cheaper .hm-head { padding: 0 16px 4px; }
#hm-cheaper > .status { padding: 0 16px; margin-bottom: 8px; }
.hm-cheaper-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px 16px; border-bottom: 1px solid var(--border); cursor: pointer;
}
.hm-cheaper-left { min-width: 0; }
.hm-cheaper-right { flex: none; text-align: right; }
.hm-cheaper-to {
  margin: 0; font-size: 15px; font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.hm-cheaper-sub {
  margin: 2px 0 0; font-size: 12px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.hm-cheaper-price { margin: 0; font-size: 16px; font-weight: 700; }
.hm-cheaper-discount { margin: 2px 0 0; font-size: 12px; font-weight: 600; color: var(--price-good); }

/* ─── Главная: «Куда поехать» (волна 2.1, редизайн 04.09 — задача
   владельца, мокап «большая фото-карта подборки») ─────────────────────
   Разметка — renderHmCollections()/hmCollectionCardHtml() (app.js): тот
   же .rail.hm-rail рельс, что и у соседей (.hm-rail даёт gap/padding,
   §выше по файлу — правило общее с «На выходные»/«Истории», НЕ трогаем
   его тут, см. их же комментарии); свой скролл-скоуп — #hm-collections
   .rail, без правки самого .rail/.hm-rail. Карточка растёт с 160×120
   витрины (волна 2.1) до почти полноэкранной, ~200px высотой (задача
   владельца: ~88% ширины рельса, соседняя карточка чуть выглядывает) —
   по-прежнему СВОЯ, полностью самостоятельная, НЕ .hm-card (та же
   причина, что и раньше: `.hm-card` объявлен ниже по каскаду и при
   равной специфичности перетёр бы position/width/height общих свойств).
   Фон — <img>/hue-псевдослой, которые кладёт hmCollectionCardHtml() (её
   комментарий в app.js — cover подборки → банк фото первого города →
   hue-градиент); scrim — тот же ::after приём, что и раньше, просто гуще
   (карточка выше, снизу три строки текста, не две). */
#hm-collections { padding: 20px 0; }
#hm-collections .hm-head { padding: 0 16px 4px; }
#hm-collections .hm-sub { margin: 0 16px 12px; font-size: 13px; color: var(--muted); }
#hm-collections .rail { scroll-snap-type: x proximity; }
.hm-collection-card {
  position: relative; flex: 0 0 88%; width: 88%; height: 200px;
  border-radius: 18px; overflow: hidden; text-align: left;
  padding: 16px; cursor: pointer; scroll-snap-align: start;
  display: flex; flex-direction: column; justify-content: flex-end; gap: 3px;
  color: var(--on-scrim); background: var(--surface-2);
  box-shadow: var(--shadow-sm); transition: transform .15s ease;
}
.hm-collection-card:hover, .hm-collection-card:active { transform: scale(1.02); }
.hm-collection-cover {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; z-index: 0; filter: brightness(.72);
}
body.light .hm-collection-cover { filter: brightness(.72); } /* тот же явный повтор, что .rail-card .ex-photo — снимает светлую тему */
.hm-collection-hue {
  position: absolute; inset: 0; z-index: 0;
  background: linear-gradient(160deg, hsl(var(--hm-hue) 55% 32%), hsl(calc(var(--hm-hue) + 24) 55% 16%));
}
.hm-collection-card::after {
  content: ''; position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(180deg, transparent 30%, rgba(0, 0, 0, .58) 65%, rgba(0, 0, 0, .82) 100%);
}
.hm-collection-badge {
  position: absolute; top: 14px; left: 14px; z-index: 2;
  padding: 7px 14px; color: var(--on-scrim); background: var(--scrim-btn-bg);
  font-size: 12px; font-weight: 600; border-radius: var(--r-pill);
}
.hm-collection-cap {
  position: relative; z-index: 2; margin: 0;
  font-size: var(--type-cap-size); font-weight: 600; letter-spacing: var(--type-cap-tracking);
  text-transform: uppercase; color: var(--on-scrim-3);
}
.hm-collection-title {
  position: relative; z-index: 2; margin: 3px 0 0; font-size: 20px; font-weight: 700; line-height: 1.22;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.hm-collection-meta {
  position: relative; z-index: 2; margin: 8px 0 0;
  display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
}
.hm-collection-months { font-size: 13px; color: var(--on-scrim-2); }
.hm-collection-price { font-size: 16px; font-weight: 700; }
/* Feature 3 (cta_label подборок, P2-4/hmCollectionCardHtml): карточка
   рельса раньше не несла своего текста действия вовсе — вся карточка и
   так кликабельна, эта строка просто называет действие словами автора
   подборки (или общим фолбэком «Смотреть», как у соседних .dl-cta/
   .hm-hero-cta пилюль файла). */
.hm-collection-cta {
  position: relative; z-index: 2; margin: 6px 0 0;
  font-size: 13px; font-weight: 650; color: var(--on-scrim);
}

/* ─── Главная: «Помоги стать лучше» (06.09.2026, задание владельца) ────
   Второй вход в фото-дуэль pd* (тот же шит, что у #pd-entry Профиля) —
   компактная стеклянная карточка, не .list-row: задача явно просит
   «карточку», плюс `.list-row` живёт на плоском `--surface` Профиля, а
   тут карточка лежит прямо на `--bg` Главной, как и соседние секции
   (глубина 32px = 16px `main{}` + собственные 16px контейнера, тот же
   расчёт, что у `.hm-cheaper-row`/`#hm-collections .hm-head` рядом).
   Вся карточка — один `<button>` (как `.list-row`), `.hm-pd-cta` внутри —
   `<span>`, оформленный пилюлей, а не вложенная кнопка (тот же приём, что
   `.row-cta` у hmCollectionsAllRowHtml — invalid nested `<button>` не
   заводим). v9 язык: заголовок весом 300 (та же шкала, что `.pd-city`/
   `.hm-hero-title`), --accent — ТОЛЬКО у пилюли действия (задача
   дословно: «--accent only for the CTA»), иконка/подпись остаются
   нейтральными var(--text)/var(--text-2). */
#hm-pd { padding: 20px 16px; }
.hm-pd-card {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 14px 14px 14px 16px; border-radius: var(--r-lg); text-align: left;
}
.hm-pd-card:active { transform: scale(.99); }
.hm-pd-icon { flex: none; width: 22px; height: 22px; color: var(--text-2); }
.hm-pd-icon svg { width: 100%; height: 100%; }
.hm-pd-text { flex: 1; min-width: 0; }
.hm-pd-text b { display: block; font-size: 16px; font-weight: 300; letter-spacing: -.01em; }
.hm-pd-text span { display: block; font-size: 12.5px; color: var(--text-2); margin-top: 2px; }
.hm-pd-cta {
  flex: none; height: 34px; padding: 0 16px; border-radius: var(--r-pill);
  display: flex; align-items: center; justify-content: center;
  background: var(--accent); color: var(--on-accent);
  font-size: 13px; font-weight: 600;
}

/* ═══ v9 волна B: шторка «Даты и цены» (промпт §6) — обычный шит (радиус
   уже --r-xl/28 токеном волны A), три числа БЕЗ карточек-плашек, бары
   7 дней со свайпом недель, «Лучшие даты» сегментами. Второй шаг
   («Сообщить, когда будет ниже») — .rg-*, отдельный пуш-шит того же
   стиля (openRadarGoalSheet, app.js). .best-row/.best-txt/.best-price/
   .best-go (карточка маршрута, выше по файлу) переиспользованы для
   «Лучших дат» как есть — тот же язык «строка со стрелкой», новых
   классов под список не заводили. */
.ds-stats { display: flex; gap: 18px; margin: 2px 0 8px; }
.ds-stat { flex: 1 1 0; min-width: 0; }
.ds-stat-label {
  display: block; font-size: var(--type-cap-size); letter-spacing: var(--type-cap-tracking);
  text-transform: uppercase; color: var(--text-2); margin-bottom: 6px;
}
.ds-stat-val { display: block; font-size: 21px; font-weight: 300; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }
.ds-stat-val.ds-strike { color: var(--text-2); text-decoration: line-through; }
.ds-stat-val.ds-good { color: var(--price-good); }

/* Бары «Минимум по дням» — своя разметка (.ds-bar*, НЕ .bar/.bar-col
   маршрута/билета выше): та пара красит только «дешевле всего»/«тап»,
   тут КАЖДЫЙ столбик несёт свой цвет от отношения к обычной цене (промпт,
   «Логика»: <85% зелёный, 85-115% нейтральный, >115% красный) — общая
   геометрия дорожки (BAR_MIN/BAR_TRACK, app.js) та же самая. */
.ds-bars-plot { position: relative; display: flex; align-items: flex-end; justify-content: space-between; gap: 6px; height: 84px; overflow: visible; }
/* Ревью v12, P1 (§5): «обычная цена» — пунктир поверх дорожки (app.js,
   weekBarsHtml), не часть шкалы самих баров — bottom считает JS, клампит
   в разумных пределах. z-index:-1 держит его ЗА барами (сами столбики —
   обычные <button> без позиционирования, красятся позже в DOM = поверх
   по умолчанию; пунктир не должен перекрывать/спорить с активным
   выделением .is-selected). */
.ds-baseline {
  position: absolute; left: 0; right: 0; z-index: -1;
  border-top: 1.5px dashed var(--text-2);
}
.ds-bar { position: relative; flex: 1 1 0; min-width: 0; height: 100%; display: flex; align-items: flex-end; justify-content: center; }
.ds-bar-col { position: relative; width: 60%; border-radius: 5px 5px 2px 2px; background: var(--price-neutral); }
.ds-bar.is-good .ds-bar-col { background: var(--price-good); }
.ds-bar.is-high .ds-bar-col { background: var(--price-high); }
/* Тап по бару — выделение дня (план v11 §4): кольцо --ink вокруг
   столбика, цвет самой заливки (нейтральный/дешёво/дорого) не трогаем —
   это отдельная семантика (§«Логика»), выбор — поверх неё, не вместо. */
.ds-bar.is-selected .ds-bar-col { box-shadow: 0 0 0 2px var(--ink); }
.ds-bars-under { display: flex; justify-content: space-between; gap: 6px; margin-top: 6px; }
.ds-bars-under span {
  flex: 1 1 0; min-width: 0; text-align: center; font-size: 10px; color: var(--text-2);
  font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ds-bars-labels { display: flex; justify-content: space-between; gap: 6px; margin-top: 2px; }
.ds-bars-labels span {
  flex: 1 1 0; min-width: 0; text-align: center; font-size: var(--f-xs); color: var(--text-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Сегменты «В одну»/«Туда-обратно» — пилюля-в-пилюле, не .chip (тот —
   для скроллящегося ряда независимых фильтров, этот — ровно два
   взаимоисключающих состояния на всю ширину). */
.ds-seg { display: flex; gap: 6px; margin: 18px 0 10px; padding: 3px; background: var(--surface-2); border-radius: 999px; }
/* Ревью v12, P1: h34 → min-height 44 (.ds-seg-контейнер сам по себе не
   ограничивает высоту детей паддингом/overflow, безопасно расти). */
.ds-seg button { flex: 1; height: 34px; min-height: 44px; border-radius: 999px; font-size: var(--f-sm); font-weight: 600; color: var(--text-2); }
.ds-seg button.is-active { background: var(--ink); color: var(--bg); }

.ds-foot { display: flex; gap: 10px; align-items: center; }
.ds-foot .sheet-cta { flex: 1; }
/* .ds-foot-radar (сплошной --surface-2 кружок) снят — ревью v12, P2:
   та же роль на «Маршруте»/«Рейсе» — .ic.glass.rt-radar (стекло), один
   и тот же тумблер держал три разных облика. #ds-radar в openDatesSheet
   (app.js) теперь несёт эти же классы, отдельного правила не нужно. */

/* Радар — второй шаг шторки: цель со стрелками ± и чипами скидки (.chip —
   переиспользован как есть, та же пилюля, что и у фильтров). */
.rg-price-row { display: flex; align-items: center; justify-content: center; gap: 22px; margin: 26px 0 6px; }
/* Ревью v12, P1: 42×42 → 44×44 (тот же приём, что у .ic — разница
   несущественная визуально, увеличивать проще, чем невидимый инсет). */
.rg-step { flex: none; width: 44px; height: 44px; border-radius: 50%; background: var(--surface-2); display: grid; place-items: center; color: var(--text); }
.rg-step svg { width: 18px; height: 18px; }
.rg-price { font-size: 40px; font-weight: 300; font-variant-numeric: tabular-nums; min-width: 150px; text-align: center; }
.rg-hint { text-align: center; font-size: var(--f-sm); color: var(--text-2); margin: 0 0 22px; }
/* Ревью v12, P2: 3+1 переносило «Любое снижение» отдельной строкой по
   центру (эталон-скрин, было криво) — сетка 2×2 держит все четыре чипа
   ровно, независимо от длины подписи третьего/четвёртого. */
.rg-chips { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 26px; }
.rg-chips .chip { width: 100%; }

/* ═══ v9 волна C: капс-шапка «Радара»/«Профиля» (общий компонент, эталон
   05 — капс 11/.16em + число/имя 28px вес 300) ══════════════════════════ */
.scr-cap {
  margin: 0 2px 8px; font-size: var(--type-cap-size); letter-spacing: var(--type-cap-tracking);
  text-transform: uppercase; color: var(--mute); font-weight: 400;
}
.scr-title { margin: 0 2px 20px; font-size: 28px; font-weight: 300; letter-spacing: -.01em; }
/* Приёмка v9, P2-7: §8 просит имя в Профиле 30px — .scr-title (28px)
   переиспользован и у капса «N маршрутов» Радара (та роль остаётся 28,
   как в эталоне 05), так что имя правится точечно по #profile-name, не
   базовым правилом. */
#profile-name { font-size: 30px; }
.scr-sub { margin: -14px 2px 20px; font-size: var(--f-md); color: var(--text-2); }
.scr-sub:empty { display: none; }
/* Роадмап п.12 (восстановлено 23.09) — ⚙︎ (#profile-gear) top-right рядом
   с капсом/именем/подписью, тот же `.ic.glass` 44px круглый стеклянный
   icon-button, что уже даёт #dl-mode-toggle (см. её комментарий выше по
   файлу) — переиспользуем класс целиком, тут только layout самой строки. */
.profile-headrow { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.profile-headrow .profile-head-text { flex: 1; min-width: 0; }
.profile-headrow .profile-gear { flex: none; margin-top: 2px; }
/* Имя в режиме карты («Моя карта Иван») — тот же <h1>#profile-name (30px,
   правило выше), только добавочный <small> мельче и приглушённее, как у
   #profile-sub. Пустой по умолчанию (фолбэк) — не занимает места. */
#profile-name-sm { font-size: 15px; font-weight: 400; color: var(--mute); }

/* ═══ v9 волна C: экран 04 «Поиск» (design/flightdeals-v3-reference.html,
   эталон 04) ════════════════════════════════════════════════════════════
   Старая форма Откуда/Куда/Туда/Обратно снесена (см. app.js) — одно поле,
   чипы, RouteDash-заглушка, недавние. Сцена «mtn» статична (не зависит от
   города — промпт буквально «ночная сцена сверху»), растворяется маской
   к середине, контент лежит поверх без абсолютного позиционирования
   мокапа — обычный поток, отступы адаптированы под реальный скролл. */
.sc-scene {
  /* Отменяет padding самого <main> (18px сверху, --pad по бокам) — сцена
     всегда full-bleed, независимо от того, чему сейчас равен --pad.
     Приёмка v9: высота 300px обрезала силуэт заметно выше эталонных
     520px (маска — `180deg,#000 45%,transparent` — уже была дословно
     эталонной, расходилась только высота). */
  position: relative; height: 520px; margin: -18px calc(var(--pad) * -1) 0;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(180deg, #000 45%, transparent);
  mask-image: linear-gradient(180deg, #000 45%, transparent);
}
/* Заголовок «Куда летим?» садится там же, где в эталоне (top:230 из 520
   высоты сцены — у самого начала маски-затухания, 45%): подъём тела
   пересчитан под новую высоту сцены (было -76 при 300px).
   Роадмап 23.09 («масштаб»): -290 → -318px. Компакция .sc-sub/.sc-trip
   выше отыграла БОЛЬШУЮ часть 62px, что волна 1 добавила между героем и
   #sc-field (7462552: 44px пилюля без рамки, margin 14/0/28 → сейчас
   44px с рамкой, 12 + новый тумблер 78px = было 148 против старых 86,
   разница 62), но не всю — компактные сумма (14+36+10)+(0+40+14)=114
   всё ещё на 28px больше исходных 86. Эти 28 долистывает подъём тела —
   заголовок поднимается на те же 28px (не коллизия с шапкой Telegram:
   на 390×844 .sc-hero.top было 230, стало 202, порог readability-аудита
   «не выше ~120px» — запас 82px). Проверено Playwright: T_old (390×844,
   до волны 1, 7462552^) = 411px, после этой правки #sc-field.top = 411px
   день-в-день, тот же расчёт для 375×667 (media-ветка ниже) и 430×932/
   390×740 (не задеты media, тот же -318). */
.sc-body { position: relative; margin-top: -318px; padding: 0 6px; text-align: center; }
.sc-hero {
  margin: 0; font-size: var(--type-hero-size); font-weight: var(--type-hero-w);
  line-height: 1.08; letter-spacing: -.01em;
}
/* v10 план §3б: подпись — теперь <button> (пикер города вылета), не
   голый <p> — базовые сбросы button{} (app.css, верх файла) уже снимают
   бордер/фон/шрифт, тут только сохранённая геометрия. */
/* Ревью v12, P1 (§1): единственный вход в смену города вылета, зона тапа
   была 346×20 без единого признака кнопки (тот же цвет/кегль, что у
   обычной подписи). min-height — порог 44; ::after-шеврон — тот же
   язык, что и у .dl-cap-chev (Сделки).
   Роадмап §5в (readability-аудит 2026-09-20): min-height/шеврон выше уже
   были — не хватало именно фона/контура («не читается как кнопка» даже
   с шевроном, тот слишком мелкий сигнал в одиночку). background/border
   — тот же язык, что и у .chip выше по файлу (--surface + --text-3),
   border-radius — --r-pill (токен пилюли, :root), не число на глаз.
   Нижний отступ ужат 28→12: под кнопкой теперь тумблер «Туда-обратно/
   В одну сторону» (.sc-trip ниже, роадмап §3а, вставляется ensureTripToggleEl()
   в app.js) — весь снятый отступ переехал под НЕГО, до .sc-field-wrap
   геометрия не сдвинулась ни на пиксель.
   Роадмап 23.09 («масштаб» — владелец: экран раздулся, #sc-field съехал
   с силуэта гор ~60px, инвариант v12 «.sc-field лежит ровно на силуэте
   гор» нарушен): пилюля стала full-width/44px после волны 1 — вернули
   компактной, content-width (inline-flex внутри .sc-body с text-align:
   center — тот же приём центровки, что уже держит .sc-hero), фон/бордер
   оставлены (readability-аудит 2026-09-20 требовал именно их — «не
   читается как кнопка» без заливки/контура), min-height 44→36,
   padding-x 16→14, шрифт на ступень мельче (var(--f-sm), было --f-md). */
.sc-sub {
  display: inline-flex; align-items: center; justify-content: center; gap: 5px;
  min-height: 36px; width: auto; margin: 14px 0 10px; padding: 0 14px;
  font-size: var(--f-sm); color: var(--text-2);
  background: var(--surface); border: 1px solid var(--text-3); border-radius: var(--r-pill);
}
.sc-sub::after { content: '▾'; font-size: 9px; color: var(--text-3); }
/* Роадмап §3а: тумблер режима поездки — та же пилюля-в-пилюле, что и
   .ds-seg (шторка радар-цели, см. её блок выше по файлу) — переиспользуем
   класс(ы) как есть (ensureTripToggleEl() в app.js ставит "sc-trip ds-seg"
   вместе), тут только своя вставка в поток .sc-body (после #sc-sub) и
   свой отступ вместо ds-seg-шного: тот калиброван под шторку (18px сверху
   — перед ним заголовок шага), этот тумблер идёт ВПЛОТНУЮ под #sc-sub
   (margin-top:0 — .ds-seg's 18px тут не нужен, кнопки и так рядом).
   Роадмап 23.09 («масштаб»): max-width 280→272 (не 240 — живая проверка
   Playwright: .ds-seg button держит собственный `padding: 1px 6px`,
   которого не видно в исходном правиле выше по файлу, при 240 контентная
   ширина кнопки падает до 102px против 106.8px, которые реально занимает
   «В одну стороны» на var(--f-sm) — текст переносился на 2 строки и
   вылезал за пилюлю; 272 держит ~11px запаса на обе подписи в обеих
   темах) и кнопки компактнее (34px вместо унаследованных 44 от .ds-seg
   button — override только тут, под .sc-trip, шторка радар-цели
   (.ds-seg сама по себе) не задета), низ ужат 28→14 — снятая высота
   вернула #sc-field на исходное место (см. арифметику T_old/T_now в
   PROGRESS.md). */
.sc-trip { margin: 0 auto 14px; max-width: 272px; }
.sc-trip button { height: 34px; min-height: 34px; }
.sc-field-wrap { position: relative; }
.sc-field {
  display: flex; align-items: center; gap: 12px; width: 100%; height: 56px;
  border-radius: 28px; padding: 0 22px; text-align: left; color: var(--text-2); font-size: var(--f-base);
}
.sc-field svg { flex: none; width: 18px; height: 18px; }
/* v10 план §3в → v11 план §1 → диагностика v12 (fix-2026-08-28-carousel-
   diag.md) этап 2: декор под полем справа — вертикальная авто-карусель
   IATA-кодов направлений, теперь буквально СЛОТ назначения на линии
   маршрута (.sc-dash), а не отдельная плашка сама по себе — ширина/отступ
   (44px/30px) равны .sc-dash-slot (app.js searchDashHtml), который держит
   это место в потоке пунктира. Список продублирован в разметке (app.js)
   — переезд ровно на 50% высоты трека визуально неотличим от начала (тот
   же набор кодов), это и даёт бесшовную петлю; шаг steps() и duration
   пересчитывает JS от длины массива (app.js renderAirportsCarousel) —
   здесь только фолбэк на случай, если тот код не выполнился. Окно — 3
   строки (90px), маска симметричная (прозрачно → .20 → #000 в центре →
   .20 → прозрачно) — активный код читается в центре, соседи угадываются
   по краям, не обрываются жёстким краем.
   Ревью v16, №11: активная (средняя) строка держится ТОЛЬКО на маске —
   отдельного CSS-класса «средней» строки нет, DOM-строки едут сквозь
   окно, и в центре просто оказывается mask-alpha=1 (#000 в стопе 50%),
   то есть span показывается на своей ЖЕ opacity (.28 тёмная тема). Формула
   WCAG к фону сцены: rgb(244,241,234)/.28 на #0e0f13 — 2.32:1, а на
   светлом #f3f1ec (текст --ink тёмный, opacity .32 из body.light ниже) —
   2.04:1, оба ниже порога «уверенно читается» (3:1). Подняли span-opacity
   до .46 тёмная/.58 светлая (см. правило ниже и body.light) — центр
   считает ~4.28:1 в ОБЕИХ темах (одна и та же формула, тот же приём
   разной альфы под тему, что у AW_FADE/AW_FADE_LIGHT). Боковые строки при
   такой альфе стали бы заметно ярче старого (mask .32 × новая opacity), а
   не «как сейчас» — поэтому края маски ослаблены с .32 до .20: боковая
   яркость (opacity × mask) держится у исходных ~0.09 (тёмная)/~0.10
   (светлая), почти не отличить на глаз от того, что было до правки. */
.sc-airports {
  position: absolute; top: calc(100% + 83px); right: 30px; width: 44px; height: 90px;
  overflow: hidden; pointer-events: none;
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, .20) 33%, #000 50%, rgba(0, 0, 0, .20) 67%, transparent 100%);
  mask-image: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, .20) 33%, #000 50%, rgba(0, 0, 0, .20) 67%, transparent 100%);
}
.sc-airports-track {
  display: flex; flex-direction: column; will-change: transform;
  animation: sc-airports-scroll 23.65s steps(11) infinite;
}
.sc-airports-track span {
  display: block; height: 30px; line-height: 30px;
  font-size: var(--f-xs); font-weight: 400; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink); opacity: .46; text-align: right;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* -50%: ровно один полный набор AIRPORT_CODES (app.js) — трек держит его
   дважды подряд, так что конец первого прохода = начало второго.
   steps(11) — длина AIRPORT_CODES сейчас 11 (диагностика v12: было
   steps(10) при фактических 14 кодах — рассинхрон, шаг съезжал на 1.4
   строки вместо ровно одной, отсюда «некрасиво вращается»). Если
   справочник изменится числом записей, ЭТОТ фолбэк надо пересчитать
   вручную вместе с ним — не то же самое значение, что и длина массива,
   автомат тут не считает (JS всегда переопределяет оба значения инлайн-
   стилем от фактической длины, см. renderAirportsCarousel). */
@keyframes sc-airports-scroll { from { transform: translateY(0); } to { transform: translateY(-50%); } }
.sc-dash { display: flex; align-items: center; gap: 14px; margin: 120px 36px 0; }
.sc-dash-code { font-size: var(--f-sm); letter-spacing: .24em; text-transform: uppercase; color: var(--ink); }
.sc-dash-code.sc-dash-dim { color: var(--dim); }
/* Пустой слот под барабан направлений (app.js searchDashHtml, диагностика
   v12 этап 2 п.5) — раньше тут был текстовый плейсхолдер «···», который
   конкурировал с барабаном за один и тот же угол. flex:none + ширина ==
   ширине .sc-airports, чтобы пунктир (.sc-dash-line, flex:1) сам
   останавливался ровно там, где начинается окно барабана. */
.sc-dash-slot { width: 44px; flex: none; }
.sc-dash-line {
  flex: 1; position: relative; height: 1px;
  border-top: 1px dashed rgba(255, 255, 255, .28);
}
.sc-dash-line svg {
  position: absolute; left: 50%; top: -8px; transform: translateX(-50%);
  width: 16px; height: 16px; color: var(--lilac);
  background: var(--bg); padding: 0 5px; box-sizing: content-box;
}
/* P3.10 (docs/research/search-ux-2026-09-02.md): чип «Повторить последний
   поиск» — узел .sc-repeat вставляется app.js (ensureLastSearchChipEl)
   СРАЗУ ПОСЛЕ #sc-dash, не внутрь .sc-body — та зависимость хрупкая
   (см. комментарий у .sc-airports выше: #sc-dash висит на margin-top от
   высоты .sc-body, а карусель кодов спозиционирована абсолютно от
   .sc-field-wrap и от потока не зависит), поэтому свой блок живёт снаружи
   и ничьей геометрии не трогает. :empty прячет контейнер целиком, когда
   fd.lastSearch нет/протух — тот же приём, что у .status:empty выше по
   файлу. Сам .chip — существующий язык (см. §chip выше), тут только
   центрирование и защита от длинной пары городов (у чипа white-space:
   nowrap, а тут, в отличие от .filterbar, нет горизонтального скролла). */
.sc-repeat { display: flex; justify-content: center; margin: 18px 6px 0; }
.sc-repeat:empty { display: none; }
.sc-repeat .chip { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
/* Диагностика v12 этап 3: 320×568 и подобные низкие экраны — геометрия
   выше в px рассчитана под 390×844, на низком экране барабан целиком
   уезжает под сгиб/таббар. Числа стартовые (см. план — сверить
   скриншотами при следующей правке): сцена и подъём тела ужаты
   пропорционально, .sc-dash ближе к полю, окно барабана — одна строка. */
@media (max-height: 680px) {
  .sc-scene { height: 380px; }
  /* Роадмап 23.09 («масштаб»): та же поправка -28px, что и у базового
     правила выше (см. её комментарий) — низкий экран (375×667 из
     обязательного набора вьюпортов) тоже идёт через эту ветку. */
  .sc-body { margin-top: -258px; }
  .sc-dash { margin-top: 28px; }
  .sc-airports { top: calc(100% + 21px); height: 30px; }
  .sc-repeat { margin-top: 12px; }
}

/* ═══ v9 волна C: экран 03 «Куда угодно» (эталон 03) ═══════════════════
   Капс-список городов вместо сетки карточек v8 (снесена целиком — см.
   DESIGN_PLAN): затухание AW_FADE (app.js) — верхний видимый ярче,
   опрокидывает по мере скролла. Фото справа — фиксировано во viewport,
   растворяется в фон горизонтальным градиентом (не .ex-scrim — тот
   вертикальный, для текста поверх фото, тут фото декоративное). */
.dl-logo { flex: none; font-size: 15px; font-weight: 600; letter-spacing: .1em; color: var(--ink); }
/* «FD» — первый ребёнок .dl-top (index.html); когда виден — капс справа
   выравнивается по правому краю (эталон 03: «FD слева + капс справа»).
   justify-content — ревью v12: .dl-cap стал flex (шеврон-кнопка), одного
   text-align для перестановки строки-с-иконкой уже недостаточно. */
#dl-logo:not([hidden]) ~ .dl-cap { text-align: right; justify-content: flex-end; }
/* ═══ v11 план §3: «Куда угодно» — фото активного города фоном ВСЕГО
   экрана (замена карточки 200×310 справа — история волны v9 C ниже) ═══
   #aw-bg-a/#aw-bg-b — кроссфейд (paintAwBg(), app.js), object-fit:cover,
   БЕЗ блюра на самом слое (в отличие от .amb-layer выше — тут фото
   должно остаться резким справа). #aw-bg-blur — ОДИН статичный слой
   поверх обоих: backdrop-filter даёт блюр, background — затемнение (тот
   же color-mix приём, что уже стоит в файле, например у .day.edge), а
   mask-image — общая маска для ОБОИХ эффектов разом: там, где маска
   прозрачна (правая половина), пропадает и блюр, и затемнение вместе —
   переход остаётся одним градиентом, не двумя рассинхронными. Значения
   маски (38%/58%) держат блюр+тёмный целиком в левых ~40% (там список),
   к правой половине эффект уже полностью сошёл на нет — она читаема, как
   просит план. #aw-bg-shade — лёгкий вертикальный скрим сверху/снизу
   (капс/таббар), СЛАБЕЕ и уже, чем .amb-shade у ambient «Сделок» выше:
   там фон блюрен целиком, тут справа остаётся читаемое резкое фото, и
   сильный вертикальный скрим спорил бы с ним. z-index:-1 — та же
   containing-block/paint-order логика, что у .dl-ambient (см. комментарий
   там): красится НАД <body>, но ПОД обычным статическим содержимым
   <main> (.dl-top в том числе — он тоже непозиционирован, поэтому
   автоматически поверх) и ПОД .aw-list (z-index:2 ниже — список остаётся
   поверх блюра, план §3 буквально). */
.aw-bg { position: fixed; inset: 0; z-index: -1; overflow: hidden; pointer-events: none; }
.aw-bg-layer { position: absolute; inset: 0; opacity: 0; transition: opacity .4s ease; }
.aw-bg-layer.is-on { opacity: 1; }
.aw-bg-blur {
  position: absolute; inset: 0;
  backdrop-filter: blur(28px); -webkit-backdrop-filter: blur(28px);
  background: color-mix(in srgb, var(--bg) 55%, transparent);
  /* Ревью v12, P1 (§3): маска гасла с 84% ширины, а .aw-list (ниже) ведёт
     текст до `right:40px` (≈86-90% на 390-430px) — хвосты длинных
     названий («КАЛИНИНГРАД») ложились на нерасплывшуюся/незатемнённую
     часть фото (замерено: right:334 при vw:390), контраст там падал до
     ~1.3:1. Затемнение сдвинуто дальше вправо — под весь текстовый блок
     с запасом. */
  -webkit-mask-image: linear-gradient(to right, black 0%, black 78%, transparent 96%);
  mask-image: linear-gradient(to right, black 0%, black 78%, transparent 96%);
}
.aw-bg-shade {
  position: absolute; inset: 0;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--bg) 60%, transparent) 0%, transparent 16%,
    transparent 82%, color-mix(in srgb, var(--bg) 65%, transparent) 100%);
}
/* Приёмка v9, P1-3 (история — фон тогда был карточкой 200px справа, план
   v11 §3 растянул его на весь экран, но вывод остаётся тем же): текстовый
   блок списка ведётся до `right: 0`, без отдельного «коридора» под фото —
   само фото декоративно и ниже по z-index (.aw-list z:2 выше .aw-bg z:-1),
   так что текст может уходить поверх любой части фона без коллизии;
   переносится только честно длинное «КАЛИНИНГРАД». */
.aw-list { position: relative; z-index: 2; padding: 6px 40px 10px 0; }
/* v14 (диагностика fix-2026-08-28-anywhere-diag.md, (б)): content-visibility
   /contain-intrinsic-size (Ревью v12, P1 §0) сняты — реальная высота строки
   гуляет 88-198px (1-3 строки капса на длинных названиях), а фиксированная
   оценка 90px держала scrollHeight заниженным, пока строки ниже вьюпорта не
   измерены по-настоящему: дно списка «убегало» при скролле/догрузке
   (loadMoreAwRows), последняя страница не доезжала одним фликом. AW_PAGE в
   app.js и так режет число узлов разом в DOM — content-visibility тут была
   вторым, менее надёжным слоем экономии поверх уже достаточного среза. */
.aw-row {
  display: block; width: 100%; text-align: left; padding: 11px 0; transition: opacity .18s ease;
}
/* Живая верификация Opus-диагноста (B7): тап по строке не давал никакой
   обратной связи — глобальный `* { -webkit-tap-highlight-color: transparent }`
   снимает нативную подсветку, а своей не было, в отличие от всех прочих
   списков проекта (.city/.opt/.list-row/.rs-row — тот же приём). .aw-soon
   (заглушка «скоро», не кликабельна) сознательно исключена — не глушить
   ей ложную "нажимаемость". */
.aw-row:not(.aw-soon):active { background: var(--surface-2); }
/* Ревью v12, P1 (§3): overflow-wrap:anywhere при 40px/.2em трекинга на
   350px доступной ширины рвал длинные названия ПОСЕРЕДИНЕ слова без
   дефиса («КАЛИНИНГ/РАД», «ЕКАТЕРИНБ/УРГ», «ПЕТРОПАВЛОВСК-КАМЧАТ/СКИЙ»).
   hyphens:auto добавляло сам дефис на переносе — но на капсе стало
   отдельной проблемой (ревью v16, №5): словарный перенос капслока читался
   коряво («НАПРАВЛЕ-НИЯ», «ЮГО-ВОС-ТО-ЧНАЯ» — ярлык рвался на 3-4 строки).
   manual переносит только по мягкому переносу (&shy;), которого в разметке
   нет, — де-факто выключает автоперенос; безопасно только вместе с
   остальной частью фикса (короткие REGION_LABEL, срез скобок у городов —
   app.js) — она убирает почти все случаи, где вообще было что переносить,
   overflow-wrap:anywhere остаётся страховкой на то, что всё же не влезет. */
.aw-row-city {
  display: block; font-size: clamp(26px, 9vw, 40px); line-height: 1.05; font-weight: var(--type-citycaps-w);
  letter-spacing: var(--type-citycaps-tracking); text-transform: uppercase;
  overflow-wrap: anywhere; hyphens: manual; -webkit-hyphens: manual;
}
.aw-row-meta { display: block; margin-top: 10px; font-size: var(--f-sm); color: var(--text-2); }
/* v14: заглушка пустого региона уровня 1 (awSoonRowHtml, app.js) — тот же
   `.aw-row` бокс (паддинг/сетка не меняются), но фиксированно приглушённая
   — НЕ участвует в fade-тирах paintAwFade (тот берёт `.aw-row:not(.aw-soon)`)
   и не в клик-делегате (обычный <div>, не <button> — не кликабельна).
   Ревью v16, №10: --text-2 (альфа .6, одинаковая в обеих темах) поверх ЕЩЁ
   одного opacity:.4 на самой строке — двойное приглушение мерило 2.1:1
   тёмная/1.6:1 светлая (формула WCAG к --bg темы: #0e0f13/#f3f1ec) — ниже
   порога даже некликабельного текста. .6 держит ~3.07:1 к #0e0f13. Тёмный
   текст на светлом фоне размывается иначе на той же альфе (тот же эффект,
   что развёл AW_FADE/AW_FADE_LIGHT в app.js) — светлой теме для тех же
   ~3:1 к #f3f1ec нужна более высокая ступень (.78 → ~3.05:1), отсюда
   отдельное правило ниже. Сами токены --text-2/--text-3 не трогаем — они
   продолжают переворачиваться под тему для всех прочих потребителей. */
.aw-row.aw-soon { opacity: .6; }
.aw-row.aw-soon .aw-row-city { color: var(--text-2); }
.aw-row.aw-soon .aw-row-meta { color: var(--text-3); }
body.light .aw-row.aw-soon { opacity: .78; }
/* Сводная строка «скоро» (аудит 2026-09-05, P2, см. awSoonSummaryHtml в
   app.js) — один компактный ряд на ВСЕ пустые регионы разом вместо
   прежних N полноразмерных .aw-row-city блоков (26-40px капс) по одному
   на регион. Обычный текстовый кегль, .aw-row задаёт паддинг/сетку,
   .aw-soon — приглушение (обе строки выше) — тут только typography. */
.aw-soon-summary { font-size: var(--f-sm); color: var(--text-3); line-height: 1.4; }

/* Крошка уровня 2 (план-регион-первый 29.08, §1/§3 A6) — единственный путь
   назад + контекст скоупа, ОДНА на оба вида (карточки/список), живёт МЕЖДУ
   .dl-top и #deals-mode/#anywhere-mode (index.html, id=ex-crumb), не первой
   строкой #aw-list, как раньше (v14: «‹ Все направления», awBackRowHtml —
   удалена вместе с этим комментарием о ней). Класс `.aw-back` сохранён
   буквально ради живого прогона /tmp/aw_list_check.py (page.click(".aw-back")
   продолжает работать без правок) — сменилась роль (шапка, не строка
   списка): justify-content:space-between разводит фиксированный текст
   назад и приглушённый контекст региона по краям одной строки. 44px и
   --text у самой кнопки — то же обоснование, что и раньше (полноценный
   тап-таргет, не подпись). Высота ФИКСИРОВАНА и не зависит от длины
   текста контекста — план §7 R3: список ниже меряет свою позицию ДО и
   ПОСЛЕ показа крошки, скачок высоты увёл бы весь кэш (см. --crumb-h
   ниже и paintExploreCrumb, app.js). */
.aw-back {
  display: flex; justify-content: space-between; align-items: center; width: 100%; text-align: left;
  min-height: 44px; margin: -6px 2px 12px; padding: 6px 0; font-size: var(--f-md);
  font-weight: 600; color: var(--text); gap: 12px;
}
/* P1 (07.09.2026, живая проверка): длинные заголовки коллекций («Осень
   на Волге: Казань, Самара, Нижний Новгород», collections.json) не
   умещались в одну строку — .ex-crumb-ctx ничем не запрещал перенос, а
   высота крошки ФИКСИРОВАНА (--crumb-h, см. комментарий у .aw-back
   выше) — вторая строка вылезала за неё и наезжала на список.
   .ex-crumb-back: flex-shrink:0 — держит «‹ Регионы» ровно в её
   собственную ширину при любом дефиците места (без этого оба ребёнка
   .aw-back ужимаются по умолчанию поровну — при длинном контексте
   подрезало бы и стрелку назад тоже). .ex-crumb-ctx: min-width:0 снимает
   автоматический флекс-минимум по контенту (иначе ужаться теснее
   собственного текста нельзя — переполнение вместо усечения), дальше
   обычная связка nowrap+overflow+ellipsis. Короткий контекст («ЕВРОПА ·
   14») это не трогает: дефицита нет, фаза сжатия не включается,
   justify-content:space-between по-прежнему прижимает его к правому
   краю, как раньше. gap — минимальный зазор, который не съедает фаза
   сжатия (иначе «‹ Регионы» и хвост усечённого «…НОВГОРОД · 5» касались
   бы впритык при самом длинном заголовке). */
.ex-crumb-back { flex: none; }
.ex-crumb-ctx {
  font-size: var(--type-cap-size); letter-spacing: var(--type-cap-tracking);
  text-transform: uppercase; color: var(--mute); font-weight: 400;
  min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}

/* ═══ v9 волна C: экран 05 «Радар» — стопка (эталон 05, бывш. «Слежу») ═
   Свёрнутые — узкие полоски .rd-strip (внутри .swipe-wrap — тот же
   свайп-механизм, что раньше держал .watch, промпт: «оставь существующий
   механизм… в новом стиле»); раскрытая — фото-карточка .rd-card, тот же
   PhotoCard-язык, что у .dl-card/.rt-full. */
/* Приёмка v9, P1-4: настоящая стопка, не колонка сплошных строк во всю
   ширину с зазорами. Каждый следующий слой (`.rd-strip-wrap`, порядок в
   разметке — от дальнего к ближнему, `renderWatchesList` в app.js) уже
   `26px` инсета с боков и тянет СЛЕДУЮЩИЙ элемент вверх отрицательным
   `margin-bottom` — тот же приём для flex-column, что и в обычном
   потоке: пересчитанный размер бокса просто сокращает место, которое
   резервирует лейаут. Слой перед самой раскрытой карточкой (`.is-near`,
   выставляется в JS — либо последняя настоящая полоска, либо счётчик
   «и ещё N») — уже инсет `14px`, шире предыдущих: тот же «карты веером»
   эффект, что в эталоне (`left:54/42/28`). `.rd-card` ничего не меняет —
   её без своего margin просто утягивает вверх margin последнего слоя
   перед ней; порядок покраски (позже в DOM — позже нарисован = сверху)
   даёт перекрытие без z-index. */
.rd-stack { display: flex; flex-direction: column; }
.rd-strip-wrap {
  position: relative; border-radius: 22px; overflow: hidden;
  margin: 0 26px -26px;
}
.rd-strip-wrap.is-near { margin-left: 14px; margin-right: 14px; }
.rd-strip {
  position: relative; overflow: hidden; display: flex;
  align-items: flex-start; justify-content: space-between; gap: 10px;
  width: 100%; height: 58px; padding: 8px 22px 0; text-align: left; color: var(--on-scrim);
}
/* Контент выровнен по ВЕРХУ, не по центру: у большинства слоёв виден
   только верхний срез (низ перекрыт следующим слоем стопки), центрированный
   текст там просто пропал бы за перекрытием. */
.rd-strip-shade { position: absolute; inset: 0; background: rgba(14, 15, 19, .5); }
/* Ревью v12, P1 (§6): двухстрочный маршрут («Южно-Сахалинск → Набережные
   Челны») рвался — вторая строка полностью пряталась под следующим
   слоем стопки (виден только верхний срез полоски, 58px высотой).
   min-width:0 обязателен рядом с flex:1 — без него ellipsis у flex-
   ребёнка не срабатывает (браузер не даёт сжаться меньше intrinsic-
   ширины текста). */
.rd-strip-route {
  position: relative; flex: 1 1 auto; min-width: 0; font-size: var(--f-sm); font-weight: 500;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.rd-strip-meta { position: relative; flex: none; font-size: 12px; color: var(--on-scrim-2); }
/* «и ещё N» — не своя сцена/фото (не выдумываем город, которого тут
   нет), нейтральное стекло вместо неё. Ревью v12, P1 (§6): align-
   items:center/padding:0 центрировали текст по всей высоте 58px, но
   видна только верхняя треть (низ перекрыт раскрытой карточкой,
   margin-bottom:-26px у .rd-strip-wrap) — подпись срезалась пополам
   (измерено txtCoveredPx=5). Вернули верхнее выравнивание обычных
   полосок вместо центрирования. */
.rd-more .rd-strip {
  align-items: flex-start; justify-content: center; padding-top: 8px;
  background: rgba(255, 255, 255, .06); color: var(--text-2);
}
/* flex:none — единственный ребёнок этого варианта (нет .rd-strip-meta
   рядом), сброс flex:1/min-width:0 обычной .rd-strip-route выше, иначе
   растянутый на всю ширину блок глушит justify-content:center родителя. */
.rd-more .rd-strip-route { flex: none; font-size: var(--f-sm); font-weight: 400; color: var(--text-2); }

/* Добивка verify-v12, §2 п.5: 440px фиксированной высоты плюс шапка
   («СЛЕЖУ ЗА ЦЕНОЙ»+заголовок) и стопка свёрнутых полосок НАД карточкой —
   на 320×568 итог топил пилюлю «Покупать»/цену под плавающий таббар
   (замерено: низ карточки уходил на ~130px глубже верхней границы
   таббара). Тот же приём, что уже есть у --card-h карусели «Сделок» выше
   (min() с долей вьюпорта, сильнее суженной в отдельном media-диапазоне
   для низких экранов) — здесь просто на самой карточке, не на переменной,
   единственный потребитель этого min-height. */
.rd-card { position: relative; min-height: min(440px, 62vh); border-radius: var(--r-card); overflow: hidden; }
/* §2 п.5: 62vh всё ещё давало 440px на 568-высоких экранах — этот
   диапазон сужает долю дальше, тем же приёмом, что и --card-h карусели
   «Сделок» (media-блок рядом с ней, выше по файлу). ВАЖНО: этот override
   должен идти ПОСЛЕ базового правила .rd-card выше — та же специфичность
   (0,0,1,0), при равенстве побеждает порядок в файле, не то, что́ внутри
   media-запроса (первая попытка добавить это правило рядом с --card-h
   выше по файлу молча проигрывала базовому .rd-card, потому что база
   физически ниже, — поймано повторным Playwright-замером, не осмотром
   CSS). Значение — компромисс двух замеров, оба Playwright, не на глаз:
   низ .rd-card (`top` карточки в потоке — 173px, от шапки+стопки НАД ней —
   не зависит от высоты самой карточки) должен остаться НАД таббаром
   (top 480 на 568), но верх .rd-card-content (растёт ВВЕРХ от
   `bottom:26px`, ~190px контента у обычной подписки) не должен наехать
   на .rd-badge/.rd-radar (`top:12-16px`, низ ~221px от верха экрана) —
   40vh (227px) чинил таббар, но карточка стала настолько низкой, что эти
   двое столкнулись друг с другом (см. скрин 14-radar-w320-*, где
   «Калининград» наезжает на «✓ цель достигнута»). 50vh на 568 ≈ 284px —
   единственный найденный диапазон без коллизии ни с одной из границ (для
   568: 173+284=457 < 480 таббара; 284-43=241 > 221 низа бейджа/радара). */
@media (max-height: 680px) {
  .rd-card { min-height: min(440px, 50vh); }
}
/* Ревью v12, P1 — тот же провал контраста в верхних 12%, что у
   .dl-badge/.dl-fav карусели «Сделок» (см. комментарий там), карточка
   «Радара» рисует бейдж/toggle в том же незащищённом поясе фото. */
.rd-badge { position: absolute; top: 16px; left: 16px; z-index: 2; padding: 7px 12px; border-radius: 14px; font-size: 12px; font-weight: 500; background: var(--scrim-btn-bg); }
.rd-badge.is-good { color: var(--price-good); }
.rd-badge.is-high { color: var(--price-high); }
/* v10 план §1г: одна toggle-кнопка радара (была стрелка+корзина
   отдельно) — единый визуальный язык toggle везде, не только классом
   wireRadarToggle (см. .is-on/--radar-on* ниже — общий с .rt-radar
   «Маршрута»/«Сделки»/подвала шита токен ON, тем-инвариантный везде). */
.rd-radar { position: absolute; top: 12px; right: 12px; z-index: 2; width: 36px; height: 36px; background: var(--scrim-btn-bg); }
/* Ревью v12, P1 — тот же приём, что у .dl-fav (карусель «Сделок»). */
.rd-radar::after { content: ''; position: absolute; inset: -6px; }
.rd-radar svg { width: 16px; height: 16px; stroke: currentColor; }
/* Ревью v12, P2: было var(--ink)/var(--bg) — переворачивалось вместе с
   темой (белый круг/тёмная иконка в тёмной теме, тёмный круг/светлая в
   светлой, замерено). Кнопка лежит НАД фото — по правилу файла «стекло/
   бейджи над фото не инвертируются» (см. блок выше, начало файла)
   должна выглядеть одинаково всегда.
   Аудит 2026-09-05: OFF получил общий `--radar-scrim`/`--radar-scrim-ink`
   с `.rt-radar` внутри `.rt-cta-row` (та же роль над тем же типом фото).
   Аудит 2026-09-06: ON у обеих кнопок (и у footer-семьи #fd-radar/
   #ds-radar, см. `.sheet-foot .rt-radar.is-on` выше по файлу) переведён
   на ЕЩЁ более общий тем-инвариантный `--radar-on`/`--radar-on-ink` (см.
   их комментарий в :root) — до этого `.rd-radar`/`.rt-cta-row .rt-radar`
   (общий `--radar-scrim`, кремовый) и `.sheet-foot .rt-radar` (свой
   `var(--ink)`/`var(--bg)`, тем-следящий) красили ON двумя разными
   путями — в светлой теме получались буквально противоположные цвета,
   ровно жалоба владельца. `stroke: currentColor` на svg выше подхватывает
   `color` без отдельной строки для is-on. */
.rd-radar { color: var(--radar-scrim); }
.rd-radar.is-on { background: var(--radar-on); color: var(--radar-on-ink); }
/* Лёгкая анимация ротации стопки (план §1а) — короткий фейд+сдвиг на
   смену раскрытой карточки свайпом, не более того («лёгкая»). */
@keyframes rd-card-swap { from { opacity: 0; transform: translateY(10px); } }
.rd-card.rd-card-anim { animation: rd-card-swap .18s ease-out both; }
.rd-card-content { position: absolute; left: 24px; right: 24px; bottom: 26px; z-index: 2; text-align: center; color: var(--on-scrim); }
/* Ревью v12, P1 (§6): «Калининград» на 320px переполняет колонку
   (scrollWidth 263 > clientWidth 240), «Петропавловск-Камчатский» — и на
   390 (314 > 310). overflow-wrap — та же защита, что уже есть у
   .aw-row-city; clamp снижает кегль у самых длинных названий, не трогая
   короткие («Сочи» остаётся 38px). */
.rd-card-city { margin: 0; font-size: clamp(24px, 8vw, 38px); font-weight: 300; letter-spacing: -.01em; overflow-wrap: anywhere; }
.rd-card-goal { margin: 10px 0 18px; font-size: var(--f-md); color: var(--on-scrim-2); }
.rd-card-price { margin: 0; font-size: 40px; font-weight: 300; line-height: 1; }
/* Роадмап п.2, item 2/4: пилюля вердикта + чип «Цель» бок о бок — раньше
   тут была одна .pill, центрированная своим собственным margin:auto;
   теперь центрирует .rd-verdict-row, а пилюля внутри неё просто одна из
   двух строк ряда (ширина по контенту, не фиксированные 180px — «Следим»/
   «Подождать»/«Покупать» разной длины). */
.rd-card-content .rd-verdict-row { display: flex; align-items: center; justify-content: center; gap: 8px; margin: 22px 0 0; }
.rd-card-content .rd-verdict-row .pill { height: 46px; width: auto; padding: 0 20px; margin: 0; }
/* §2 п.5, второй проход: 50vh (media-блок у .rd-card выше) чинит базовый
   кейс задания («Калининград», 1 строка, 190px контента), но у
   двухстрочных городов («Петропавловск-Камчатский» — тот же пример из
   verify-v12.md §2 п.4, где .rd-card-city переносится) контент
   раздувается до 226px и на 320×568 всё ещё на 16px наезжает на
   .rd-badge/.rd-radar сверху (замерено). Раз задание прямо просит «ужать
   высоту карточки/ОТСТУПЫ» — донабираем зазор внутренними margin: -26px
   суммарно, только в этом диапазоне высот. Тот же порядок-в-файле нюанс,
   что у .rd-card (см. комментарий там): override должен идти ПОСЛЕ
   базовых .rd-card-goal/.rd-card-content .pill выше — первая попытка
   рядом с --card-h выше по файлу так же молча проигрывала. */
@media (max-height: 680px) {
  .rd-card-goal { margin: 4px 0 8px; }
  .rd-card-content .rd-verdict-row { margin-top: 12px; }
}
/* «Подождать» — secondary/стекло, не белая пилюля (промпт §5). */
.rd-pill-wait {
  background: rgba(255, 255, 255, .12); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
  border: 1px solid rgba(255, 255, 255, .14); color: var(--ink);
}
/* «Следим» (роадмап п.2, item 2) — третий, самый нейтральный регистр:
   не filled/accent как «Покупать», не glass как «Подождать» — контур без
   заливки, рядом с .rd-pill-wait по смыслу и структуре объявления. */
.rd-pill-watch {
  background: transparent; border: 1px solid rgba(255, 255, 255, .4); color: var(--ink);
}
/* «Цель»-чип рядом с пилюлей вердикта (роадмап п.2, item 4) — тот же
   стеклянный язык, что и .rd-pill-wait выше, просто отдельный, меньший по
   смыслу элемент (ярлык-действие, не статус). */
.rd-goal-chip {
  height: 46px; padding: 0 18px; border-radius: var(--r-pill);
  background: rgba(255, 255, 255, .12); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
  border: 1px solid rgba(255, 255, 255, .14); color: var(--ink);
  font-size: var(--f-sm); font-weight: 600;
}
.rd-goal-chip:active { transform: scale(.96); }
/* verdict_why (роадмап п.2, item 2) — маленькая строка ПОД рядом пилюли,
   не рядом с ней: длинный текст с сервера сломал бы rd-verdict-row на
   узких экранах. */
.rd-verdict-why { margin: 10px 0 0; font-size: 12px; line-height: 1.35; color: var(--on-scrim-2); text-align: center; }
/* Шит-объяснение вердикта (openWatchWhySheet, роадмап п.2, item 3) — два
   равноценных действия рядом в подвале, зеркалит .pd-done-actions (панель
   фотодуэли: тот же flex-ряд из двух .sheet-cta), под своим именем — не
   значит фотодуэль по смыслу. */
.rd-why-actions { display: flex; gap: 10px; }
.rd-why-actions .sheet-cta { flex: 1; }
/* Числовой инпут цели (openGoalSheet, роадмап п.2, item 4: «a numeric
   input (16px font)») — 16px ОБЯЗАТЕЛЕН: меньший font-size на iOS Safari/
   Telegram WebView триггерит авто-zoom при фокусе на <input>, который
   дёргает весь шит. Вид — как .rg-price (те же tabular-nums), просто
   редактируемый инпут вместо статичного <span>. */
.gs-input {
  display: block; width: 150px; margin: 0 auto; padding: 4px 0; border: 0; border-radius: 0;
  font-family: inherit; font-size: 16px; font-weight: 300; text-align: center;
  color: var(--text); background: none; -moz-appearance: textfield;
}
.gs-input::-webkit-inner-spin-button, .gs-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
/* «Без цели» (item 4: «allow clearing») — текстовая кнопка, не .chip/
   .sheet-cta: это разрушительное действие («снять цель»), не альтернативный
   пресет и не основной CTA шита — визуально должна быть тише обоих. */
.rd-goal-clear { display: block; margin: 2px auto 26px; padding: 6px 12px; font-size: var(--f-sm); font-weight: 600; color: var(--text-2); }
/* Ревью v12, P1: h37 — тот же flex-центр-приём, что у .ghost выше
   (block не центрирует контент по высоте при min-height). */
.rd-add { display: flex; align-items: center; justify-content: center; width: 100%; min-height: 44px; margin-top: 16px; padding: 10px; text-align: center; font-size: var(--f-sm); color: var(--text-2); }
/* Пустое состояние — без плашки .empty (эталон: капс+иконка+текст прямо
   на фоне, не карточка). */
.rd-empty { background: none; border: 0; padding: 36px 20px 6px; }
.rd-empty svg { width: 40px; height: 40px; }
.rd-empty b { font-size: 26px; font-weight: 300; letter-spacing: -.01em; margin: 14px 0 10px; }
.rd-empty p { max-width: 280px; margin: 0 auto; }

/* ═══ v9 волна C: экран 07 «Выдача» (промпт §7, эталон текстовый) ══════
   RouteDash крупнее обычного (30/300, не 34/800 деф.) — .rh-results
   модификатор поверх .route-hero (ref-06), не новый компонент. */
.route-hero.rh-results { margin: 4px 0 22px; }
.rh-results .rh-end b { font-size: 30px; font-weight: 300; color: var(--text); }
.rh-results .rh-path { min-width: 100px; }

.rs-verdict { margin: 0 2px 18px; font-size: var(--f-md); color: var(--text-2); }
.rs-verdict b { font-weight: 600; color: var(--text); }
.rs-verdict b.is-good { color: var(--price-good); }
.rs-verdict b.is-high { color: var(--price-high); }

.rs-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%;
  padding: 13px 2px; text-align: left; color: inherit; text-decoration: none;
}
.rs-row + .rs-row { box-shadow: inset 0 1px 0 var(--border); }
.rs-row:active { background: var(--surface-2); }
.rs-left { flex: 1; min-width: 0; }
.rs-times { display: block; font-size: 19px; font-weight: 400; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }
.rs-plus { font-size: 11px; color: var(--text-2); vertical-align: super; font-style: normal; }
/* Ревью v12, P2 (§1): white-space:nowrap+ellipsis при длинной цене
   («128 400 ₽») обрезали строку до «8ч 19м ·…»/«1 перес…» — терялся
   главный дифференциатор (число пересадок, последним в строке). Перенос
   на 2 строки вместо обрезки. */
.rs-meta {
  display: -webkit-box; margin-top: 3px; font-size: var(--f-sm); color: var(--text-2);
  -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* Feature B: обёртка вокруг .rs-price — flex-колонка сама держит место
   в ряду .rs-row (flex:none, как раньше у .rs-price), а пилюля
   встаёт ВНУТРИ неё, соседом .rs-price, не портя textContent цены
   (см. blPatchRowPills). */
.rs-price-wrap { flex: none; display: flex; flex-direction: column; align-items: flex-end; }
.rs-price { font-size: var(--f-lg); font-weight: 500; font-variant-numeric: tabular-nums; }
.rs-row.is-best .rs-price { color: var(--price-good); }
/* Роадмап §4а: бейдж багажа — своя пилюля ПОД строкой meta, не ещё один
   пункт в её " · "-перечислении: это самостоятельный сигнал «повлияет на
   бюджет», должен цепляться взглядом быстрее ряда через точки.
   is-no (нет багажа) красит текст --price-high — тот же токен, что и
   «дороже» у вердикта цены, специально ретюненный под читаемый ТЕКСТ (не
   только заливку), см. его блок в body.light выше по файлу — уместно
   переиспользовать для «доплата за место в сумке» без нового токена.
   is-yes (багаж включён) — нейтральный --text-2, этому включать внимание
   не нужно. q.baggage===null (источник не знает) — resultRowHtml вообще
   не рисует бейдж, тут для него сознательно нет правила. */
.rs-baggage {
  display: inline-block; margin-top: 4px; padding: 1px 8px; border-radius: 999px;
  font-size: 11px; font-weight: 600; background: var(--surface-2);
}
.rs-baggage.is-no { color: var(--price-high); }
.rs-baggage.is-yes { color: var(--text-2); }
.rs-ad { margin: 10px 2px 0; font-size: var(--f-xs); color: var(--text-3); }

/* ═══ аудит 2026-09-05 (competitors-deep-research.md TOP-12 №1/№5,
   search-route.md «углубить» п.2): Feature A «Гибкие даты» + Feature B
   «дешевле/дороже обычного» на «Выдаче» (app.js: renderSearchResults/
   flexWireChips/blPatchSearchBadge). Токены — те же, что уже несут
   ценовую семантику по всему файлу (--price-good/--warn/--text-2/-3),
   ни одного нового цвета; чипы переиспользуют .filterbar/.chip как есть
   (см. их блок выше) — тут только то, чего у них ещё не было. ═══ */

/* ─── Feature B: бейдж «дешевле/дороже обычного» + пилюля строки ───── */
.bl-badge {
  display: block; margin: 0 0 14px; padding: 0; text-align: left;
  font-size: var(--f-sm); font-weight: 600; color: var(--text-2);
  text-decoration: underline; text-underline-offset: 2px; text-decoration-color: var(--border);
}
.bl-badge.is-good { color: var(--price-good); }
.bl-badge.is-high { color: var(--warn); }
/* Только текст/цифра, никогда фон (промпт v9, «Фон и цвета») — тот же
   принцип, что у .rs-verdict b.is-good/.rd-badge.is-good рядом по файлу. */
.bl-pill { display: block; margin-bottom: 2px; font-size: 11px; font-weight: 700; color: var(--price-good); text-align: right; }

/* ─── Feature A: чипы «±3 дня» / «Выходные» / «Весь месяц» ──────────── */
.flex-dates { margin: 0 0 16px; }
.flex-body { margin-top: 8px; }
.flex-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%;
  min-height: 44px; padding: 11px 2px; text-align: left; color: inherit;
}
.flex-row + .flex-row { box-shadow: inset 0 1px 0 var(--border); }
.flex-row:active { background: var(--surface-2); }
.flex-row-date { flex: 1; min-width: 0; font-size: var(--f-base); font-weight: 500; }
.flex-row-right { flex: none; text-align: right; }
.flex-row-price { display: block; font-size: var(--f-md); font-weight: 600; font-variant-numeric: tabular-nums; }
.flex-row.is-best .flex-row-price { color: var(--price-good); }
.flex-row-delta { display: block; margin-top: 2px; font-size: var(--f-xs); color: var(--text-3); }
.flex-row-delta.is-good { color: var(--price-good); }

/* ═══ v10 план §4/§6: мини-шит деталей рейса (тап по строке «Выдачи»,
   не эталон — собран из уже существующего языка). RouteDash сверху
   (.rh-results — тот же модификатор, что у самой «Выдачи»), время
   крупно, факт-строки в столбец (до 5 пунктов не влезли бы в ряд, как
   .rt-facts «Маршрута»), цена 40/300 (тот же язык, что .dl-price/
   .rd-card-price). Пилюля «Купить на Авиасейлс» (.sheet-cta, подвал) —
   единственный выход наружу отсюда. */
.fd-times { margin: 4px 2px 20px; font-size: var(--f-3xl); font-weight: 300; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }
.fd-rows { display: flex; flex-direction: column; gap: 13px; margin: 0 2px 22px; }
.fd-row { display: flex; align-items: center; gap: 10px; font-size: var(--f-base); color: var(--text-2); }
.fd-row svg { width: 17px; height: 17px; color: var(--text-3); flex: none; }
.fd-price { margin: 0 2px; font-size: 40px; font-weight: 300; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }

/* ═══ v9 волна C: экран 08 «Профиль» (промпт §8) ════════════════════════
   «Список без плашек» — .list-flat снимает заливку/бордер .list-group,
   разделители-волосы остаются (--border уже rgba(255,255,255,.08)
   дословно из промпта, трогать не пришлось). */
.list-group.list-flat { background: none; border: 0; border-radius: 0; }
/* Отступ секции ≥28px между группой действий и «Недавно смотрел» (план
   v10 §2) — margin-top на самом #profile-viewed: margin схлопывается с
   margin-top:0 первого .list-group-title внутри (:first-child ниже), но
   при схлопывании побеждает БОЛЬШЕЕ значение — эффективный отступ и есть
   эти 28px, не сумма с нулём. */
#profile-viewed { margin-top: 28px; }
/* Тумблер «Уведомления» — своя маленькая пилюля-переключатель, не нативный
   checkbox (визуально свой язык проекта); role=switch на самой строке
   .list-row (не вложенный контрол — тот же принцип, что убрал
   nested-interactive у .ex-card, ревью v8 P0-4). */
.switch {
  flex: none; width: 46px; height: 28px; border-radius: 14px; position: relative;
  background: var(--surface-2); transition: background .18s ease;
}
.switch-thumb {
  position: absolute; top: 3px; left: 3px; width: 22px; height: 22px; border-radius: 50%;
  background: var(--text-2); transition: transform .18s cubic-bezier(.32, .72, 0, 1), background .18s ease;
}
.list-row.is-on .switch { background: rgba(244, 241, 234, .28); }
.list-row.is-on .switch-thumb { transform: translateX(18px); background: var(--ink); }

/* ═══ доступность: видимый фокус для клавиатуры ═══════════════════════
   :focus-visible, а не :focus — не рисует кольцо тапу пальцем/мышью,
   только клавиатурной/программной навигации. */
/* Ревью v12, P2: --brand = --ink — в тёмной теме кольцо белое, вокруг
   белой .pill над светлым фото («Смотреть»/«Купить»/«Покупать») не
   читается вовсе. Двойное кольцо — внутренний в цвет --bg (зазор от
   самой кнопки, читается на любом фоне пилюли), внешний снова --ink
   (виден на любом фоне позади него, включая фото). */
/* Только интерактивные НЕ-текстовые элементы: у текстового ввода кольцо
   давало «чёрную рамку с обрезанным верхом» (клип .sheet-body) и «полосу
   в поле» — скрины владельца 03.09, попиксельный разбор. У инпута фокус
   виден и так: клавиатура + каретка. */
:where(a, button, [tabindex], summary):focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--ink);
}

/* ═══ «Помоги стать лучше» — фото-дуэль обложек городов (Профиль,
   задание владельца: «по принципу тиндера листание фоток для
   определения веса»). pdOpen/pdRenderStack/pdBindDrag — app.js. Вход
   — обычный .list-row (index.html, #pd-entry), тут только сам шит.
   .pd-photo/.pd-scrim переиспользуют .ex-photo/.ex-scrim (PhotoCard,
   см. блок выше по файлу) — тот же тёмный фильтр/градиент, что и у
   остальных фото банка; .rt-credit — та же атрибуция, что у «Маршрута». */
.pd-progress { text-align: center; color: var(--text-2); font-size: var(--f-sm); margin: 0 0 14px; font-variant-numeric: tabular-nums; }
/* min(60vh,…) — задание: карточка обязана влезать на 320×568 вместе с
   прогрессом/кнопками/safe-area таббара под шитом. */
.pd-stage { position: relative; height: min(60vh, 460px); }
.pd-card {
  position: absolute; inset: 0; overflow: hidden; border-radius: 24px;
  background: var(--surface-2); touch-action: none;
}
/* Следующая карточка уже в DOM — картинка грузится заранее (задание:
   «следующая карточка пререндерена, фото прелоадится»), сама чуть
   уменьшена и не кликабельна — целиком под верхней. */
.pd-card-next { z-index: 1; transform: scale(.94) translateY(10px); pointer-events: none; }
.pd-card-top { z-index: 2; transition: transform .28s cubic-bezier(.32, .72, 0, 1); }
.pd-chip {
  position: absolute; left: 14px; top: 14px; z-index: 2;
  padding: 6px 12px; border-radius: 14px;
  background: var(--scrim-btn-bg); color: var(--on-scrim);
  font-size: var(--f-xs); font-weight: 600; text-transform: uppercase; letter-spacing: .08em;
}
.pd-info { position: absolute; left: 18px; right: 18px; bottom: 18px; z-index: 2; color: var(--on-scrim); }
.pd-city { margin: 0; font-size: clamp(20px, 7vw, 28px); font-weight: 300; letter-spacing: -.01em; overflow-wrap: anywhere; }
/* «LIKE»/«NOPE» штампы — вылезают только за порогом драга (см.
   .pd-will-like/.pd-will-nope, pdBindDrag в app.js), тот же язык
   стеклянных бейджей поверх фото, что и .pd-chip выше. */
.pd-stamp {
  position: absolute; top: 20px; z-index: 3; padding: 4px 10px; border-radius: 10px;
  border: 2px solid var(--on-scrim); color: var(--on-scrim); font-size: 22px; line-height: 1.3;
  opacity: 0; transition: opacity .12s ease, transform .12s ease;
}
.pd-stamp-like { right: 20px; transform: scale(.7) rotate(-12deg); }
.pd-stamp-nope { left: 20px; transform: scale(.7) rotate(12deg); }
.pd-card-top.pd-will-like .pd-stamp-like,
.pd-card-top.pd-will-nope .pd-stamp-nope { opacity: 1; transform: scale(1) rotate(0); }
.pd-actions { display: flex; align-items: center; justify-content: center; gap: 22px; margin-top: 18px; }
/* 56px — над порогом 44 из задания «кнопки под стопкой 44px+». */
.pd-act {
  flex: none; width: 56px; height: 56px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface); box-shadow: var(--shadow-sm); border: 1px solid var(--border);
  font-size: 26px; transition: transform .12s ease-out;
}
.pd-act:active { transform: scale(.92); }
.pd-wrong { flex: none; min-height: 44px; padding: 0 14px; color: var(--text-2); font-size: var(--f-sm); font-weight: 600; }
.pd-undo { display: block; margin: 14px auto 0; min-height: 44px; padding: 0 12px; color: var(--accent-text); font-size: var(--f-sm); font-weight: 650; }
.pd-done { padding: 40px 12px 10px; text-align: center; }
.pd-done-title { margin: 0 0 4px; font-size: var(--f-lg); font-weight: 300; color: var(--text); }
.pd-done-sub { margin: 0 0 22px; color: var(--text-2); font-size: var(--f-md); }
.pd-done-actions { display: flex; gap: 10px; }
.pd-done-actions .sheet-cta { flex: 1; }

/* ═══ prefers-reduced-motion: входные анимации карточек/панелей — при
   reduce всё превращаем в мгновенный переход, а не просто ускоряем. */
@media (prefers-reduced-motion: reduce) {
  .panel, .slide-in-l, .slide-in-r,
  .empty, .city, .preset, .swipe-wrap,
  /* v10: карусель аэропортов «Поиска» (план §3в, «reduced-motion —
     статична») и лёгкая анимация ротации стопки «Радара» (план §1а). */
  .sc-airports-track, .rd-card.rd-card-anim,
  /* v11 §4: сердце-burst двойного тапа «Сделок». */
  .heart-burst svg,
  /* P1.5: пульсация точек статус-строки выдачи. */
  .rs-dots i,
  /* Волна 3.1: пульс бейджа огромной скидки (Главная, hm-badge.is-huge). */
  .hm-badge.is-huge {
    animation: none !important;
  }
  /* Диагностика v12: анимация уже гасится (.sc-airports-track выше) —
     без движения статичный столбик кодов выглядит как сломанный
     элемент, а не декор. Притушить барабан целиком. */
  .sc-airports { opacity: .5; }
  /* .adm-act-sheet (поповер действий кадра, «Обложки городов») — тот же
     slide-up …both, что и .sheet, по той же причине рядом с ним, не в
     группе animation:none выше: both держит transform из keyframes как
     статичное состояние, голое отключение анимации вернуло бы базовый
     (нетрансформированный) стиль вместо конечного кадра слайда. */
  .sheet, .sheet.closing, .scrim, .adm-act-sheet { animation-duration: .001s !important; }
  * { transition-duration: .001s !important; }
}

/* ═══ «Обложки городов» — админ-панель курации (задача владельца
   2026-09-08: «настройка обложек через кнопку, её должен видеть только
   я»). Видимость строки входа решает admApplyProfile() (app.js) —
   #adm-entry-slot в index.html пуст для всех, кроме владельца, классы
   ниже всё равно на отдельном префиксе adm-, чтобы не путать с
   остальным Профилем. Экраны — обычные кадры pushSheet внутри общего
   #sheet (как и весь остальной апп); «bottom action sheet» кадра —
   свой мини-поповер position:absolute ВНУТРИ #sheet (тот сам
   position:fixed — готовый containing block), не второй модальный слой.
   --accent (=var(--ink), см. body{} выше — тут НЕ цвет, а монохромная
   ink/bg-инверсия) в новых классах используется только там, где задание
   прямо просит акцент — HERO-бейдж и праймари-действие поповера;
   сегментированный День/Ночь и фильтр-чипы красятся тем же приёмом, что
   .chip.is-active/.opt.is-active везде в файле — это язык «выбрано»
   всего приложения, не новый цветной акцент. */

/* Экран 1 «Обложки»: превью день/ночь в строке списка — тот же .list-row/
   .row-txt/.row-go скелет, что у всего остального Профиля, .row-icon
   (одна SVG-иконка) заменён на две узкие фото-плашки. */
.adm-thumbs { flex: none; display: flex; gap: 3px; }
.adm-thumb {
  /* v2 (09.09.2026): было 28×40 — задание просит миниатюры «выше, ~56px»,
     ширина держит ту же пропорцию, чтобы кадр не выглядел сплющенным. */
  width: 40px; height: 56px; border-radius: 6px; overflow: hidden;
  background: var(--surface-2);
}
.adm-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Маркер «слабое покрытие» (<4 кадров/дизлайки/жалобы — ADMIN_WEAK_*,
   server.py) — точка, не текстовый бейдж: список и так уже отсортирован
   слабыми вперёд, тут только быстрый визуальный якорь на фильтре «Все». */
.row-txt .adm-weak-dot {
  /* Селектор с .row-txt — иначе общий `.row-txt span { display:block }`
     (0,1,1) перебивал одиночный класс (0,1,0), и точка падала на свою
     строку под названием города (заметно на приёмке 09.09). */
  display: inline-block; width: 6px; height: 6px; border-radius: 50%;
  background: var(--danger); margin-left: 6px; vertical-align: middle;
}

/* Экран 2 «{Город}» (v2, 09.09.2026): полоса текущих обложек вместо
   сегмента День/Ночь — та же роль (тап переключает adm.variant, грид
   ниже перерисовывается), но теперь виден САМ кадр, который стоит
   сейчас, а не только текстовая подпись. */
.adm-hero-strip { display: flex; gap: 10px; margin: 14px 0 4px; }
.adm-hero-tile {
  position: relative; flex: 1; height: 120px; border-radius: var(--r-md);
  overflow: hidden; background: var(--surface-2); text-align: left;
  border: 2px solid transparent;
}
/* Акцент только на активной плитке — единственный явный запрос задания
   на цветной --accent тут, тот же язык, что HERO-бейдж грида ниже. */
.adm-hero-tile.is-active { border-color: var(--accent); }
.adm-hero-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
.adm-hero-tile-empty-txt {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  padding: 0 10px; font-size: var(--f-xs); color: var(--text-2); text-align: center;
}
.adm-hero-tile-label {
  position: absolute; left: 8px; bottom: 8px; z-index: 2;
  font-size: 10px; font-weight: 700; letter-spacing: .01em; line-height: 1;
  padding: 3px 6px; border-radius: 6px;
  background: var(--glass); color: var(--on-scrim);
}

.adm-live-link {
  display: block; margin: 2px 2px 4px; font-size: var(--f-sm);
  font-weight: 600; color: var(--link);
}

/* Грид кадров — 3 в ряд (задание, п.3). */
.adm-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 12px; }
.adm-frame {
  position: relative; aspect-ratio: 3 / 4; border-radius: var(--r-sm);
  overflow: hidden; background: var(--surface-2); text-align: left;
}
.adm-frame:active { transform: scale(.97); }
.adm-frame-img { width: 100%; height: 100%; object-fit: cover; display: block; }
.adm-frame-img.adm-frame-broken { opacity: 0; }
/* BANNED — притушенный, но виден (задание: «BANNED (dimmed)») — админ
   должен уметь разбанить, скрывать кадр целиком было бы тупиком. */
.adm-frame-banned .adm-frame-img { filter: grayscale(1) brightness(.5); }
.adm-frame-top {
  position: absolute; top: 4px; left: 4px; right: 4px; z-index: 2;
  display: flex; flex-wrap: wrap; gap: 3px;
}
.adm-badge {
  font-size: 10px; font-weight: 700; letter-spacing: .01em; line-height: 1;
  padding: 3px 6px; border-radius: 6px;
  background: var(--glass); color: var(--on-scrim);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
}
/* Ровно один активный HERO на город×вариант (server.py:_admin_set_hero) —
   единственный бейдж этого экрана, окрашенный --accent (задание, п.5). */
.adm-badge-hero { background: var(--accent); color: var(--on-accent); }
.adm-badge-ban { background: rgba(240, 149, 144, .85); color: #2a0f0d; }
.adm-frame-src {
  position: absolute; left: 4px; bottom: 4px; z-index: 2;
  font-size: 9px; font-weight: 600; text-transform: uppercase; letter-spacing: .02em;
  padding: 2px 6px; border-radius: 6px;
  background: var(--glass); color: var(--on-scrim-2);
}
.adm-frame-lum {
  position: absolute; right: 4px; bottom: 4px; z-index: 2;
  font-size: 9px; font-variant-numeric: tabular-nums;
  padding: 2px 6px; border-radius: 6px;
  background: var(--glass); color: var(--on-scrim-2);
}
/* «★ Обложка» (задание v2, п.3б) — оверлей во всю ширину низа кадра;
   рендерится ТОЛЬКО на кадрах, где действие вообще имеет смысл (не hero,
   не забанен, банк/пул — см. admFrameHtml, класс .adm-frame-setable на
   самом кадре). Бейджи источника/яркости на таких кадрах поднимаются над
   плашкой (приёмка 09.09: «БАНК»/«40» просвечивали сквозь стекло кнопки —
   одна и та же нижняя полоса, разный z-index). <span>, не <button> — тег
   вложен ВНУТРИ .adm-frame (тот уже <button>), а button внутри button —
   невалидный HTML.
   */
.adm-frame-setable .adm-frame-src,
.adm-frame-setable .adm-frame-lum { bottom: 30px; }
.adm-frame-set {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 3;
  padding: 6px 4px; text-align: center;
  font-size: 10px; font-weight: 700; letter-spacing: .01em;
  background: var(--glass); color: var(--on-scrim);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
}
.adm-frame-set:active { background: var(--accent); color: var(--on-accent); }

/* Пред./След. (задание v2, п.3в) — обход текущего отфильтрованного
   списка без выхода на экран 1: sticky к низу #sheet-body (тот сам
   overflow-y:auto — ближайший скроллящийся предок, sticky считается от
   НЕГО), отрицательные поля — тот же приём полноширинной подложки, что у
   .filterbar выше по файлу, только у нижнего края. */
.adm-nav {
  position: sticky; bottom: 0; z-index: 4;
  display: flex; align-items: center; gap: 8px;
  margin: 18px calc(var(--pad) * -1) calc(-1 * (var(--safe-b) + 16px));
  padding: 10px var(--pad) calc(var(--safe-b) + 16px);
  background: var(--surface); box-shadow: 0 -1px 0 var(--line);
}
.adm-nav-btn {
  flex: 1; min-width: 0; height: 44px; padding: 0 12px;
  border-radius: var(--r-md); background: var(--surface-2);
  font-size: var(--f-sm); font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.adm-nav-btn:active { transform: scale(.98); }
.adm-nav-btn:disabled { opacity: .35; pointer-events: none; }
.adm-nav-count {
  flex: none; font-size: var(--f-xs); color: var(--text-2);
  font-variant-numeric: tabular-nums;
}

/* Поповер действий кадра — «bottom action sheet» (задание, п.3): свой
   мини-лист ВНУТРИ #sheet, см. заголовок блока выше. */
.adm-act-backdrop { position: absolute; inset: 0; z-index: 5; background: var(--overlay); }
.adm-act-sheet {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 6;
  background: var(--surface); border-radius: var(--r-xl) var(--r-xl) 0 0;
  box-shadow: var(--shadow-modal);
  padding: 8px var(--pad) calc(var(--safe-b) + 10px);
  animation: slide-up .18s cubic-bezier(.32, .72, 0, 1) both;
}
.adm-act-title {
  margin: 6px 2px 10px; font-size: var(--f-xs); color: var(--text-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.adm-act-row {
  display: block; width: 100%; padding: 14px; margin-top: 6px;
  border-radius: var(--r-md); background: var(--surface-2);
  font-size: var(--f-base); font-weight: 600; text-align: center;
}
.adm-act-row:active { transform: scale(.98); }
.adm-act-row.adm-act-primary { background: var(--accent); color: var(--on-accent); }
.adm-act-row.adm-act-danger { color: var(--danger); }
.adm-act-row.adm-act-cancel { color: var(--text-2); }

/* «Открыть крупно» — иммерсивный просмотр (задание, п.3): переиспользует
   .rt-immersive/.rt-full/.rt-full-actions «Маршрута» целиком (формат
   «фото во весь экран без шапки» + свайп-вниз-закрыть, уже gated строго
   на класс .rt-immersive — ничего не пишем заново), КРОМЕ .ex-photo — та
   даёт brightness(.78) и object-fit:cover ради читаемости текста поверх
   фото, а тут наоборот нужен неискажённый кадр целиком для оценки
   качества/яркости оригинала. */
.adm-viewer-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; background: #000; }
.adm-viewer-credit {
  position: absolute; left: 28px; right: 28px; z-index: 2; text-align: center;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 30px);
}

/* Тост 409 (задание, п.3) — свой транзиентный поповер: #net-banner
   (план v8 §4) — другая роль, persistent retry banner для сетевых
   сбоев, не переиспользуем. Крепится к body, не к #sheet — переживает
   admHideActionSheet()/переход между экранами админки. */
.adm-toast {
  position: fixed; left: 50%; bottom: calc(var(--safe-b) + 84px); z-index: 60;
  max-width: calc(100% - 48px); padding: 10px 18px; border-radius: 999px;
  background: var(--ink); color: var(--bg); font-size: var(--f-sm); font-weight: 600;
  text-align: center; opacity: 0; pointer-events: none;
  transform: translate(-50%, 10px);
  transition: opacity .18s ease-out, transform .18s ease-out;
}
.adm-toast.is-shown { opacity: 1; transform: translate(-50%, 0); }

/* Скелетон строки списка городов — базовый .skeleton (132px) слишком
   высок для .list-row (~84px: миниатюры 40×56 + подпись в 3 строки —
   IATA/страна/цена, кадры/голоса — v2, 09.09.2026, было 68px/28×40/
   2 строки), тот же приём модификатора, что и .skeleton.tall выше. */
.skeleton.adm-row { height: 84px; margin-bottom: 8px; }
