/* Компоненты веб-клиента поверх токенов (фаза 12).
 *
 * Цвет берётся ТОЛЬКО из tokens.css. Ни одного шестнадцатеричного значения
 * ниже нет и быть не должно: новый цвет сначала появляется в docs/themes.md,
 * потом в токенах, и только потом здесь. Иначе «фирменный бирюзовый»
 * становится тремя разными бирюзовыми в вебе, кабинете и приложении.
 *
 * ПОД НАМИ ЛЕЖИТ BOOTSTRAP 5.3.3 (Р-192), локально и без CDN: reboot, сетка
 * и раскладочные утилиты. Цвет он берёт наш — мост и гашение чужих имён
 * ниже. Скрипт Bootstrap не подключён: ни одного его компонента с поведением
 * на сайте нет.
 */

/* --- Мост к Bootstrap (Р-192) ---------------------------------------- */

/* Все `--bs-*` — из наших токенов, поэтому «синего Bootstrap» на сайте нет
   нигде: ни в фокусе поля, ни в ссылке, ни в кнопке. Тёмная тема идёт через
   `data-bs-theme`, который ставится на <html> тем же выражением, что и наш
   `data-theme`. */
:root {
  --bs-body-bg: var(--bg);
  --bs-body-color: var(--text);
  --bs-body-font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --bs-body-font-size: 15px;
  --bs-body-line-height: 1.55;
  --bs-emphasis-color: var(--text);
  --bs-secondary-color: var(--text2);
  --bs-tertiary-color: var(--text3);
  --bs-secondary-bg: var(--surface);
  --bs-tertiary-bg: var(--surface3);
  --bs-border-color: var(--border);
  --bs-border-radius: var(--radius);
  --bs-border-radius-sm: var(--radius-sm);
  --bs-link-color: var(--brand-ink);
  --bs-link-hover-color: var(--brand);
  --bs-link-color-rgb: 184, 67, 31;
  --bs-primary: var(--brand);
  --bs-primary-rgb: 226, 88, 47;
  --bs-focus-ring-color: rgba(226, 88, 47, 0.28);
  --bs-focus-ring-width: 3px;
  --bs-heading-color: var(--text);
  --bs-code-color: var(--brand-ink);
}

/* Bootstrap называет своими те же слова, что и мы: `.row`, `.col`, `.card`,
   `.btn`, `.small`. Наши правила лежат ниже и по порядку выигрывают — но
   только там, где мы что-то объявили. Всё, чего мы НЕ переопределяем,
   продолжает действовать: отрицательные поля сетки и `width: 100%` у каждого
   потомка `.row`, который ставит каждую кнопку ряда на свою строку. Гасим
   это здесь, один раз и явно. */
.row,
.col {
  --bs-gutter-x: 0;
  --bs-gutter-y: 0;
  margin: 0;
  padding: 0;
}

.row > * {
  flex-shrink: initial;
  width: auto;
  max-width: none;
  padding-right: 0;
  padding-left: 0;
  margin-top: 0;
}

.col { flex: initial; }
.card { display: block; min-width: 0; }

/* `flex-direction` отдельной строкой и не зря. Bootstrap объявляет
   `.card { display: flex; flex-direction: column }`; `display` мы гасим, а
   направление оставалось бутстраповским — и элемент с классами `card row`
   (карточка заказа, строка объекта) превращался в СТОЛБИК, выстроенный по
   центру: фотография сверху, цена под ней, полтора метра пустоты по бокам.
   Ряд обязан оставаться рядом, чем бы он ещё ни был */
.row { flex-direction: row; }
.alert, .badge, table { margin-bottom: 0; }

/* `.modal` — ещё одно слово, занятое обеими системами, и самое коварное из
   них. У Bootstrap это НЕ окно, а его подложка: `display: none` плюс
   `position: fixed` во весь экран, потому что показывает окно его скрипт,
   которого мы не подключаем. У нас `.modal` — родной `<dialog>`, и он
   показывается сам.

   Итог был такой: `showModal()` отрабатывал, атрибут `open` появлялся, а окно
   оставалось `display: none` — то есть кнопка отмены брони и подсказка о
   пересчёте не открывали ничего и молчали об этом. Возвращаем браузерное
   поведение целиком: `revert` — это ровно «как решил бы браузер сам». */
dialog.modal {
  display: revert;
  position: revert;
  inset: revert;
  width: revert;
  height: revert;
  overflow: revert;
  z-index: revert;
  outline: revert;
}

/* --- Основа ---------------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;

  /* Место под полосу прокрутки резервируется ВСЕГДА. Иначе страница, у
     которой содержимое дорисовывается скриптом, в момент появления полосы
     сужается на её ширину — и весь макет дёргается вбок. Заметнее всего это
     в ленте поиска, где карточки приезжают после первой отрисовки */
  scrollbar-gutter: stable;
}

/* Плавные переходы там, где меняется состояние, и нигде больше: анимировать
   всё подряд — значит получить «кисель» вместо отзывчивости */
@media (prefers-reduced-motion: no-preference) {
  a, button, summary, .tile, .card, .chip, .tab {
    transition: background-color .15s ease, border-color .15s ease,
                color .15s ease, box-shadow .15s ease, transform .15s ease;
  }
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  font-size: 15px;
  line-height: 1.5;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

a {
  color: var(--brand-ink);
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

img {
  max-width: 100%;
  display: block;
}

h1, h2, h3, h4 {
  margin: 0 0 12px;
  line-height: 1.25;
  font-weight: 650;
}

h1 { font-size: 30px; }
h2 { font-size: 22px; }
h3 { font-size: 18px; }

p { margin: 0 0 12px; }

/* Фокус виден всегда: клавиатурная навигация — это доступность, а не
   украшение. Убирать обводку ради «чистоты» значит выключить сайт для тех,
   кто не пользуется мышью */
:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.wrap {
  width: 100%;
  max-width: 1180px;
  margin: 0 auto;
  padding: 0 20px;
}

.wrap--wide { max-width: 1440px; }

main {
  flex: 1 0 auto;
  padding: 24px 0 48px;
}

/* ЦВЕТНОЙ БЛОК ПРИЛЕГАЕТ К ШАПКЕ. Отступ страницы рассчитан на обычное
   содержимое, а `.hero` — это полоса во всю ширину со своим фоном: между нею
   и шапкой оставалась полоса фона страницы, в тёмной теме — чёрная. Убираем
   отступ ровно там, где первым идёт цветной блок, а не везде */
main:has(> .hero:first-child) { padding-top: 0; }

.muted { color: var(--text2); }
.small { font-size: 13px; }
.tiny { font-size: 12px; }
.nowrap { white-space: nowrap; }
.center { text-align: center; }
.row { display: flex; gap: 12px; align-items: center; }
.row--between { justify-content: space-between; }
.row--center { justify-content: center; }
.row--wrap { flex-wrap: wrap; }

/* Ряд, в котором часть полей подписана сверху, а часть — нет.
 *
 * Стоит ПОСЛЕ `.row`: у них одинаковая специфичность, и объявленный выше
 * модификатор проигрывал базовому `align-items: center` — поля выстраивались
 * по центру, подписанные съезжали вниз, и ряд выглядел развалившимся.
 *
 * По нижнему краю, а не по центру: на одной линии должны стоять сами поля,
 * а подписи — подниматься над ней. */
.row--bottom { align-items: flex-end; }
.col { display: flex; flex-direction: column; gap: 12px; }
.grow { flex: 1; }
.mt { margin-top: 16px; }
.mt-lg { margin-top: 32px; }
.mb { margin-bottom: 16px; }
.hidden { display: none !important; }

/* --- Шапка ----------------------------------------------------------- */

/* Шапка. Разделителей ровно один — закатная линия из `::after`.
   Раньше их было три сразу: `border-bottom`, `box-shadow` и градиент. Под
   градиентом оставалась серая полоска, а в тёмной теме она читалась как
   чёрный шов во всю ширину экрана между шапкой и первым блоком. */
.header {
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--bg);
  border-bottom: 0;
}

/* Линия лежит ПОВЕРХ нижнего края шапки, а не под ним: отдельным блоком она
   добавляла шапке высоты, и между нею и содержимым появлялся зазор с цветом
   фона страницы */
.header::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  background: var(--gradient-brand);
  background-origin: border-box;
  background-repeat: no-repeat;
}

.header__inner {
  display: flex;
  align-items: center;
  gap: 16px;
  height: 64px;
}

.logo {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 700;
  font-size: 19px;
  color: var(--text);
  text-decoration: none;
}

.logo:hover { text-decoration: none; }

/* Знак сервиса — файл `promofiles/icon/bounty-icon-square.svg`, тот же, что
   во вкладке браузера и на телефоне. Квадрат с градиентом был заглушкой:
   узнают сервис по знаку, а не по цветному прямоугольнику */
.logo__mark {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background: url("/static/img/icon.svg") center / cover no-repeat;
  flex: none;
}

.nav {
  display: flex;
  gap: 4px;
  margin-left: 8px;
}

.nav__link {
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  color: var(--text2);
  font-weight: 500;
}

.nav__link:hover {
  background: var(--surface);
  color: var(--text);
  text-decoration: none;
}

.nav__link--active {
  color: var(--text);
  background: var(--surface3);
}

.header__right {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 8px;
  /* Личные настройки и вход не сжимаются и не уезжают за экран: это
     единственный путь к языку, теме и профилю */
  flex: none;
}

/* ШАПКА НЕ ЕДЕТ ВБОК НИ НА ОДНОЙ ШИРИНЕ.

   Разделы — единственное, что здесь можно сжать, поэтому вся нехватка места
   достаётся им: список прокручивается внутри себя, а не выталкивает язык,
   валюту и профиль за край экрана. Без `min-width: 0` элемент сетки не
   сжимается ниже своего содержимого — и вместо прокрутки внутри списка
   вбок ехала вся страница (проверено на 320 и 768). */
.header__inner > * { min-width: 0; }
.logo { flex: none; }

.nav {
  overflow-x: auto;
  scrollbar-width: none;
}

.nav::-webkit-scrollbar { display: none; }
.nav__link { white-space: nowrap; }

.avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--surface3);
  object-fit: cover;
  display: grid;
  place-items: center;
  font-size: 13px;
  font-weight: 600;
  color: var(--text2);
  flex: none;
}

.badge-count {
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 9px;
  background: var(--error);
  color: #fff;
  font-size: 11px;
  font-weight: 600;
  display: inline-grid;
  place-items: center;
}

/* --- Кнопки ---------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 10px 18px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background: var(--surface3);
  color: var(--text);
  font: inherit;
  font-weight: 550;
  cursor: pointer;
  text-decoration: none;
  transition: filter .15s ease, background .15s ease;
}

.btn:hover { text-decoration: none; filter: brightness(.97); }
.btn:disabled { opacity: .5; cursor: not-allowed; }

/* ГРАДИЕНТ РИСУЕТСЯ ОТ КРАЯ РАМКИ.
 *
 * По умолчанию картинка фона раскладывается от края ОТСТУПА
 * (`background-origin: padding-box`), а красится по краю РАМКИ. У элемента с
 * прозрачной рамкой в 1 px эта рамка показывает завёрнутый край картинки —
 * и у кнопки появляются светлые полоски слева и справа, а при наведении они
 * ещё и переползают. Лечится не подбором размеров, а origin и no-repeat. */
.btn--brand,
.tab--active,
.hero,
.profile-head,
.header::after,
.logo__mark {
  background-origin: border-box;
  background-repeat: no-repeat;
}

/* Главная кнопка — закат целиком, а не плоская заливка. Ровно та поверхность,
   ради которой градиент в проекте и заведён: небольшая, важная, одна на экран */
.btn--brand {
  background: var(--gradient-brand);
  background-size: 200% 200%;
  background-position: 0% 50%;
  color: #fff;
  border-color: transparent;
  box-shadow: var(--shadow-brand);
  transition: background-position .35s ease, filter .15s ease, box-shadow .15s ease;
}

.btn--brand:hover { background-position: 100% 50%; }

.btn--ghost {
  background: transparent;
  border-color: var(--border2);
  color: var(--text);
}

.btn--quiet {
  background: transparent;
  color: var(--text2);
  padding: 8px 10px;
}

.btn--danger { background: var(--error); color: #fff; }
.btn--sm { padding: 6px 12px; font-size: 13px; }
.btn--block { width: 100%; }

/* --- Карточки и плашки ----------------------------------------------- */

.card {
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}

.card--pad { padding: 18px; }

.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 11px;
  border-radius: 999px;
  background: var(--surface3);
  color: var(--text2);
  font-size: 13px;
  white-space: nowrap;
}

.chip--brand { background: var(--brand-bg); color: var(--brand-ink); }

.tone {
  display: inline-flex;
  align-items: center;
  padding: 4px 10px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
}

.tone--success { background: var(--success-bg); color: var(--success-ink); }
.tone--warning { background: var(--warning-bg); color: var(--warning-ink); }
.tone--error   { background: var(--error-bg);   color: var(--error-ink); }
.tone--info    { background: var(--info-bg);    color: var(--info-ink); }
.tone--muted   { background: var(--surface3);   color: var(--text2); }

.notice {
  padding: 12px 16px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface);
  margin-bottom: 16px;
}

.notice--error   { background: var(--error-bg);   border-color: transparent; color: var(--error-ink); }
.notice--success { background: var(--success-bg); border-color: transparent; color: var(--success-ink); }
.notice--info    { background: var(--info-bg);    border-color: transparent; color: var(--info-ink); }
/* Предупреждение: срок на исходе, заказ оборвался, договор отменён. Токены
   были заведены давно, а модификатора не существовало вовсе — и две полосы
   в карточке договора (`rentals/detail.html`) молча рисовались обычным
   серым, то есть выглядели как обычная подпись */
.notice--warning { background: var(--warning-bg); border-color: transparent; color: var(--warning-ink); }

/* --- Формы ----------------------------------------------------------- */

.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
.field > label { font-size: 13px; color: var(--text2); font-weight: 500; }

/* Подпись над полем — БЕЗ ЗАДАНИЯ РАЗМЕРОВ САМОГО ПОЛЯ.
 *
 * Тем и отличается от `.search-bar__field`, который вместе с подписью
 * навязывает высоту 40 px: в панели раздела поля мельче, на главной — крупнее.
 * Применённый на главной класс панели развалил ряд — строка поиска и кнопка
 * остались одной высоты, даты и счётчик стали другой.
 *
 * Здесь только сетка «подпись сверху, поле снизу»; высоту поле приносит своё,
 * а цвет подписи наследуется от места: на герое он светлый, в обычном блоке
 * приглушённый. */
.field-top { display: grid; gap: 4px; min-width: 0; }
.field-top > span { font-size: 12px; opacity: 0.85; }

/* ВСЕ ОРГАНЫ УПРАВЛЕНИЯ РЯДА — ОДНОЙ ВЫСОТЫ, И ОНА НАЗВАНА ЧИСЛОМ.
 *
 * У поля высота складывалась из отступов и строки (44,5 px), у счётчика была
 * задана числом (40 px). В панели раздела они сходятся — там полям тоже
 * назначено 40, — а в ряду главной разница в четыре с половиной пикселя
 * бросалась в глаза: счётчик гостей выглядел вдавленным.
 *
 * Здесь высота задана прямо и обоим сразу. Вычислять её из отступов и
 * строки нельзя: у поля и у счётчика строки разной высоты (22,5 против 20),
 * и «одинаковые отступы» дают разный итог — именно так и получилось с
 * первого раза. */
.row--bottom .input,
.row--bottom .stepper { height: 44px; }

/* Ответ поля человеку: «почта занята», «почта свободна».
 *
 * Размером и местом — как подпись-подсказка под полем, цветом — как ответ:
 * своих цветов не заводит, берёт готовые `--error-ink` и `--success-ink`
 * (docs/themes.md). Пустая подсказка скрыта атрибутом `hidden`, а не нулевой
 * высотой: скрытое высотой читают вслух программы чтения с экрана. */
.field__note { font-size: 12px; color: var(--text2); }
.field__note--taken { color: var(--error-ink); }
.field__note--free { color: var(--success-ink); }

.input,
.select,
.textarea {
  width: 100%;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border2);
  background: var(--surface2);
  color: var(--text);
  font: inherit;
}

.input:focus,
.select:focus,
.textarea:focus {
  border-color: var(--brand);
  outline: none;
}

.textarea { min-height: 96px; resize: vertical; }

/* ЧИСЛОВОЕ ПОЛЕ БЕЗ СИСТЕМНЫХ СТРЕЛОК.
 *
 * Стрелки браузера рисуются его собственным стилем: белые, крошечные,
 * поверх нашего фона — в тёмной теме они выглядят как артефакт отрисовки.
 * Число гостей вводят не ими, а степпером ниже. */
.input[type="number"] {
  appearance: textfield;
  -moz-appearance: textfield;
}

.input[type="number"]::-webkit-outer-spin-button,
.input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* Степпер: «− 2 +» одним блоком. Число гостей меняют на один-двух, и для
   этого нужны две большие кнопки, а не попадание в стрелку в три пикселя */
.stepper {
  display: inline-flex;
  align-items: center;
  height: 40px;
  border: 1px solid var(--border2);
  border-radius: var(--radius-sm);
  background: var(--surface2);
  overflow: hidden;
}

.stepper__btn {
  width: 34px;
  height: 100%;
  border: 0;
  background: transparent;
  color: var(--text2);
  font: inherit;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}

.stepper__btn:hover { background: var(--surface3); color: var(--text); }
.stepper__btn:disabled { opacity: .35; cursor: not-allowed; }

.stepper .input {
  width: 46px;
  height: 100%;
  border: 0;
  border-radius: 0;
  background: transparent;
  text-align: center;
  padding: 0;
  font-weight: 600;
}

.stepper .input:focus { box-shadow: none; }

/* Поле выбора файла: системная кнопка «Обзор» встаёт вплотную к рамке и
   ломает высоту поля */
.input[type="file"] {
  padding: 8px 10px;
  line-height: 1.2;
}

.form-narrow {
  max-width: 420px;
  margin: 32px auto;
}

/* --- Витрина --------------------------------------------------------- */

.hero {
  padding: 44px 0 32px;
  background: var(--gradient-brand);
  color: #fff;
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  margin-bottom: 28px;
}

.hero h1 { color: #fff; font-size: 34px; }
.hero p { color: rgba(255, 255, 255, .88); max-width: 620px; }

.tabs { display: flex; gap: 6px; flex-wrap: wrap; }

.tab {
  padding: 9px 16px;
  border-radius: 999px;
  background: var(--surface3);
  color: var(--text2);
  font-weight: 550;
}

.tab:hover { text-decoration: none; }
.tab--active { background: var(--gradient-brand); color: #fff; }

.hero .tab { background: rgba(255, 255, 255, .18); color: #fff; }
.hero .tab--active { background: #fff; color: var(--gradient-ink); }

/* ГЛАВНАЯ КНОПКА НА ГРАДИЕНТЕ БЕЛАЯ.
 *
 * Градиентная кнопка на градиентной подложке сливается: одинаковый цвет,
 * одинаковое направление, разница только в тени. Поверх заката главной
 * становится белая плашка — на этом фоне она заметнее всего, что бывает.
 *
 * Чернила — `--gradient-ink`, а не `--brand-ink`: подложка одинакова в обеих
 * темах, значит и текст на ней обязан быть одинаковым. Тёмный коралл темы
 * на белой кнопке поверх заката давал бы контраст 2,4:1 — нечитаемо. */
.hero .btn--brand,
.profile-head .btn--brand,
.on-gradient .btn--brand {
  background: #fff;
  background-size: auto;
  color: var(--gradient-ink);
  box-shadow: var(--shadow-md);
}

.hero .btn--brand:hover,
.profile-head .btn--brand:hover,
.on-gradient .btn--brand:hover { filter: brightness(.94); }

/* Второстепенные кнопки и плашки поверх заката: полупрозрачное белое стекло.
   Обычные `--surface`/`--text` на градиенте не читаются вовсе — именно так
   пропадали «Личные данные» и «Выслать письмо ещё раз» в шапке профиля */
.hero .btn--ghost,
.hero .btn--quiet,
.profile-head .btn--ghost,
.profile-head .btn--quiet,
.on-gradient .btn--ghost,
.on-gradient .btn--quiet {
  background: rgba(255, 255, 255, .18);
  border-color: rgba(255, 255, 255, .45);
  color: #fff;
}

.hero .btn--ghost:hover,
.hero .btn--quiet:hover,
.profile-head .btn--ghost:hover,
.profile-head .btn--quiet:hover,
.on-gradient .btn--ghost:hover,
.on-gradient .btn--quiet:hover { background: rgba(255, 255, 255, .3); }

.profile-head .tone,
.on-gradient .tone {
  background: rgba(255, 255, 255, .22);
  color: #fff;
}

/* ЗАГОЛОВОК БЛОКА И ССЫЛКА «СМОТРЕТЬ ВСЕ» — РЯДОМ, А НЕ ПО КРАЯМ.
 *
 * Раньше их разводило `row--between`, и ссылка уезжала к правому краю
 * страницы. Когда карточек в ряду меньше, чем помещается, справа остаётся
 * пустота — и ссылка висит над ней, оторванная от того, к чему относится.
 * Рядом с заголовком она читается как его продолжение. */
.section-head {
  display: flex;
  align-items: baseline;
  gap: 14px;
  flex-wrap: wrap;
  margin: 32px 0 16px;
}

.section-head h1,
.section-head h2 { margin: 0; }

.section-head__more {
  font-size: 13px;
  font-weight: 600;
  color: var(--brand-ink);
  white-space: nowrap;
}

/* Правый край заголовка — для того, что относится к странице целиком
   (состояние брони, счётчик), а не для ссылки «дальше» */
.section-head__aside { margin-left: auto; }

.grid {
  display: grid;
  gap: 20px;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}

.tile { position: relative; }

.tile__photo {
  aspect-ratio: 4 / 3;
  width: 100%;
  object-fit: cover;
  background: var(--surface3);
}

.tile__body { padding: 12px 14px 14px; }

/* Чип ВНУТРИ плитки переносится по словам. У отдельно стоящего чипа
   `white-space: nowrap` правильный — перенос ломает ряд отбора; но
   «Мгновенное бронирование» это 189 px, а плитка на телефоне 167, и нерушимый
   чип вылезал за карточку вместе со своей рамкой */
.tile .chip { white-space: normal; }
.tile__title { font-weight: 600; margin: 0 0 4px; font-size: 15px; }
.tile__meta { color: var(--text2); font-size: 13px; margin-bottom: 8px; }
.tile__price { font-weight: 650; }

/* ИЗБРАННОЕ.
 *
 * Символ «♥» из шрифта рисуется по-разному в каждой системе и в половине
 * из них выглядит как опечатка. Здесь настоящая иконка: контур, пока не
 * отложено, и залитая закатом — когда отложено. Разница видна с расстояния,
 * а не только по оттенку фона. */
.tile__fav {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 36px;
  height: 36px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, .5);
  background: var(--overlay);
  color: #fff;
  cursor: pointer;
  display: grid;
  place-items: center;
  backdrop-filter: blur(4px);
  transition: transform .15s ease, background .15s ease, border-color .15s ease;
}

.tile__fav svg {
  width: 19px;
  height: 19px;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linejoin: round;
}

.tile__fav:hover { transform: scale(1.08); }
.tile__fav:active { transform: scale(.94); }

.tile__fav[aria-pressed="true"] {
  background: var(--gradient-brand);
  background-origin: border-box;
  background-repeat: no-repeat;
  border-color: transparent;
}

.tile__fav[aria-pressed="true"] svg { fill: currentColor; }

/* Избранное на карточке объекта (находка №103). Не круг поверх снимка, как у
   плитки, а кнопка со словом: здесь она стоит в строке фактов, и сердечко без
   подписи там читалось бы украшением. Слово меняется по состоянию */
.fav-btn {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 34px;
  padding: 0 12px;
  border: 1px solid var(--border2);
  border-radius: 999px;
  background: var(--surface2);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: border-color .15s ease, color .15s ease;
}
.fav-btn:hover { border-color: var(--brand); color: var(--brand-ink); }
.fav-btn svg {
  width: 17px;
  height: 17px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linejoin: round;
}
.fav-btn__on { display: none; }
.fav-btn[aria-pressed="true"] { color: var(--brand-ink); border-color: var(--brand); }
.fav-btn[aria-pressed="true"] svg { fill: currentColor; }
.fav-btn[aria-pressed="true"] .fav-btn__on { display: inline; }
.fav-btn[aria-pressed="true"] .fav-btn__off { display: none; }

.rating { display: inline-flex; align-items: center; gap: 4px; font-weight: 600; }
.rating__star { color: var(--gold); }

/* --- Поиск ----------------------------------------------------------- */

.search-layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: 20px;
}

.search-layout--map {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}

/* ФИЛЬТРЫ ЛЕНТЫ.
 *
 * Сетка, а не ряд `flex` с одинаковой шириной: поле запроса должно быть
 * широким, дата — ровно под дату, число гостей — узким. При одинаковой
 * ширине панель растягивается на две строки там, где хватило бы одной. */
.filters {
  display: grid;
  grid-template-columns: minmax(180px, 2fr) repeat(auto-fit, minmax(130px, 1fr));
  gap: 10px 12px;
  align-items: end;
  padding: 14px 16px;
  background: var(--surface2);
  border: 1px solid transparent;
  background-image:
    linear-gradient(var(--surface2), var(--surface2)),
    var(--gradient-border);
  background-origin: border-box;
  background-clip: padding-box, border-box;
  border-radius: var(--radius);
  margin-bottom: 18px;
  box-shadow: var(--shadow-sm);
}

.filters .field { margin: 0; min-width: 0; }
.filters .field > label { font-size: 12px; }

/* Кнопки прижаты к концу последней строки и не растягиваются на всю ячейку */
.filters__actions {
  grid-column: 1 / -1;
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
}

/* «Сбросить» уходит к правому краю: это отмена, а не действие в ряду */
.filters__actions .btn--quiet { margin-left: auto; }

/* Карта карточки объекта. Своим классом, а не по `#map`: общее правило с
   `min-height: 420px` и `position: sticky` растягивало её на карточке и
   уводило вверх — она вылезала поверх текста и выглядела чужой на странице */
.map-box {
  height: 320px;
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--border);
  margin-bottom: 12px;
}

/* Карта целым экраном (W25).

   Панель фильтров лежит ПОВЕРХ полотна, а не над ним. Полоса управления
   сверху съедала треть экрана и отодвигала карту вниз — на странице, где
   карта и есть ответ, это самая дорогая трата места. */
.map-page {
  position: relative;
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--border);
  background: var(--surface3);
}

.map-page:fullscreen { border-radius: 0; border: 0; }

.map-page__canvas {
  height: calc(100vh - 160px);
  min-height: 460px;
}

.map-page:fullscreen .map-page__canvas { height: 100vh; }

/* ПАНЕЛЬ ФИЛЬТРОВ НА КАРТЕ — ОДНА СТРОКА.
 *
 * Прошлая версия растягивалась во всю ширину и раскладывала поля в столбик:
 * панель занимала треть карты, то есть треть ответа на вопрос «что здесь
 * рядом». Теперь на виду ровно то, что меняют каждый раз, — раздел и место;
 * даты, гости и цена лежат в выпадающем списке и открываются по нажатию.
 *
 * Панель не тянется до правого края: справа у карты живут карточки объектов,
 * и накрывать их полупрозрачным стеклом незачем. */
.map-bar {
  position: absolute;
  z-index: 5;
  top: 12px;
  left: 12px;
  right: 12px;
  max-width: 780px;
  display: flex;
  align-items: center;
  gap: 8px;
  /* ПАНЕЛЬ НЕ ПЕРЕНОСИТСЯ НИКОГДА. «Таблетка» со скруглением 999px в две
     строки выглядит поломкой, а не переносом: скругление рисует торцы у
     каждой строки, и панель раздувается по высоте, съедая карту. Место под
     всё отдаёт то, что умеет ужиматься, — вкладки прокруткой и поле поиска
     шириной; кнопки справа не жмутся вовсе, они и так по размеру подписи. */
  flex-wrap: nowrap;
  padding: 8px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--surface2) 92%, transparent);
  border: 1px solid var(--border2);
  box-shadow: var(--shadow-md);
  backdrop-filter: blur(8px);
}

/* Разделов пять (Р-123), и вкладками они в строку не помещаются: последняя
   уезжала под поле поиска. Список занимает место одного слова вместо пяти —
   и на телефоне остаётся на экране, тогда как вкладки приходилось прятать
   целиком, оставляя карту без переключения раздела вовсе. */
.map-bar__kind {
  flex: none;
  height: 34px;
  max-width: 140px;
  padding: 0 30px 0 13px;
  border-radius: 999px;
  border: 1px solid var(--border2);
  background: var(--surface2);
  color: var(--text);
  font: inherit;
  font-size: 13.5px;
  font-weight: 550;
  cursor: pointer;
  /* Своя стрелка: системная у `select` в каждом браузере своя и рядом с
     остальными кнопками панели выглядит чужой */
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--text2) 50%),
                    linear-gradient(135deg, var(--text2) 50%, transparent 50%);
  background-position: calc(100% - 16px) 15px, calc(100% - 11px) 15px;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

.map-bar__kind:hover { background-color: var(--surface3); }

.map-bar__search {
  flex: 1 1 120px;
  /* Поле сжимается до узкого, но не исчезает: пустая полоска на месте
     поиска непонятна, а 64 px хватает, чтобы увидеть начало набранного */
  min-width: 64px;
  height: 34px;
  border: 0;
  background: transparent;
  padding: 0 6px;
  font: inherit;
  color: var(--text);
}

.map-bar__search:focus { outline: none; }

/* Разделитель между «где искать» и «чем уточнить»: без него панель читается
   как один длинный ряд одинаковых кнопок */
.map-bar__sep {
  width: 1px;
  height: 22px;
  background: var(--border2);
  flex: none;
}

/* Выпадающие уточнения. На `<details>`, как язык и валюта в шапке: список,
   который не открывается без JavaScript, — это фильтр, которым нельзя
   воспользоваться при сбое скрипта */
.map-bar__more { position: relative; flex: none; }

.map-bar__more > summary {
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 34px;
  padding: 0 13px;
  border-radius: 999px;
  border: 1px solid var(--border2);
  background: var(--surface2);
  color: var(--text2);
  font-size: 13.5px;
  font-weight: 550;
  cursor: pointer;
  white-space: nowrap;
}

.map-bar__more > summary::-webkit-details-marker { display: none; }
.map-bar__more[open] > summary,
.map-bar__more > summary:hover { background: var(--surface3); color: var(--text); }

/* Точка на кнопке, когда уточнения заданы: иначе человек не помнит, что
   фильтр включён, и не понимает, почему на карте пусто */
.map-bar__more--set > summary::after {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--brand);
}

.map-bar__panel {
  position: absolute;
  z-index: 6;
  top: calc(100% + 8px);
  left: 0;
  width: 300px;
  padding: 14px;
  display: grid;
  gap: 10px;
  border-radius: var(--radius);
  background: var(--surface2);
  border: 1px solid var(--border2);
  box-shadow: var(--shadow-lg);
}

/* Содержимое панели обязано уметь сжиматься: без `min-width: 0` пара полей
   «от» и «до» просила 300 px при 298 доступных, и панель подтекала на два
   пикселя на любой ширине */
.map-bar__panel > * { min-width: 0; }

.map-bar__panel .field { margin: 0; }
.map-bar__panel .field > label { font-size: 12px; }
/* `min-width: 0` до самого поля: без него «от» и «до» держат свою
   «естественную» ширину в двадцать символов, и пара просит 286 px там, где
   их 270 */
.map-bar__pair { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 10px; }
.map-bar__pair > *,
.map-bar__panel input,
.map-bar__panel select { min-width: 0; }

/* Действия справа — одной группой, и группа не жмётся. Так давление
   переносится на вкладки и поиск, а кнопки сохраняют читаемый размер:
   ужатая до многоточия кнопка хуже прокрутки вкладок. */
.map-bar__actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
}

.map-bar__go,
.map-bar__list { flex: none; white-space: nowrap; }

.input--sm { height: 34px; padding: 0 10px; font-size: 14px; }
.input--num { width: 96px; flex: none; }

/* Своё управление: правый нижний угол — туда дотягивается большой палец, а
   левый верхний у карты занят панелью фильтров */
.map-tools {
  position: absolute;
  z-index: 5;
  right: 12px;
  bottom: 12px;
  display: grid;
  gap: 8px;
  justify-items: center;
}

/* «Во весь экран» — в правом ВЕРХНЕМ углу карты объекта (Р-231): это не
   масштаб, а другой режим просмотра, и в одном столбике с «плюсом» и
   «минусом» его нажимали бы вместо них */
.map-tools--top { top: 12px; bottom: auto; }

.map-tools__zoom {
  display: grid;
  border-radius: var(--radius-sm);
  overflow: hidden;
  box-shadow: var(--shadow-md);
}

.map-tool {
  width: 38px;
  height: 38px;
  display: grid;
  place-items: center;
  border: 1px solid var(--border2);
  background: var(--surface2);
  color: var(--text);
  font: inherit;
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
}

.map-tools__zoom .map-tool { border-radius: 0; margin-top: -1px; }
.map-tools__zoom .map-tool:first-child { border-radius: var(--radius-sm) var(--radius-sm) 0 0; margin-top: 0; }
.map-tools__zoom .map-tool:last-child { border-radius: 0 0 var(--radius-sm) var(--radius-sm); }
.map-tools > .map-tool { border-radius: var(--radius-sm); box-shadow: var(--shadow-md); }
.map-tool:hover { background: var(--surface3); }

/* Указание на источник карты обязательно по лицензии OpenStreetMap. Убрать
   его нельзя, но можно свернуть: маленькая кнопка в левом нижнем углу,
   подальше от карточек и от управления */
.maplibregl-ctrl-bottom-left { z-index: 4; }

.maplibregl-ctrl-bottom-left .maplibregl-ctrl-attrib {
  font-size: 10px;
  background: color-mix(in srgb, var(--surface2) 80%, transparent);
  border-radius: var(--radius-sm);
  margin: 0 0 6px 6px;
  opacity: .65;
}

.maplibregl-ctrl-bottom-left .maplibregl-ctrl-attrib:hover { opacity: 1; }

.map-page__note {
  position: absolute;
  z-index: 5;
  left: 50%;
  bottom: 16px;
  transform: translateX(-50%);
  max-width: min(420px, calc(100% - 120px));
  padding: 10px 14px;
  border-radius: var(--radius-sm);
  background: var(--surface2);
  border: 1px solid var(--border2);
  box-shadow: var(--shadow-md);
  font-size: 13px;
  color: var(--text2);
  text-align: center;
}

/* Метка — маленькая карточка, а не булавка и не голый ценник: на карте
   выбирают глазами, и по одной цене выбрать нельзя */
.map-mark {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 190px;
  padding: 5px;
  border-radius: 12px;
  border: 1px solid var(--border2);
  background: var(--surface2);
  box-shadow: var(--shadow-md);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.map-mark:hover { border-color: var(--brand); box-shadow: var(--shadow-lg); transform: translateY(-1px); }

.map-mark__photo {
  width: 44px;
  height: 44px;
  flex: none;
  object-fit: cover;
  border-radius: 8px;
  background: var(--surface3);
}

.map-mark__body { min-width: 0; display: grid; gap: 2px; }

.map-mark__title {
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.25;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.map-mark__meta { display: flex; align-items: center; gap: 8px; font-size: 12px; }
.map-mark__price { font-weight: 700; color: var(--brand-ink); }
.map-mark__rating { color: var(--text3); }

/* Раскрытая карточка поверх метки */
.map-popup .maplibregl-popup-content {
  padding: 0;
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--surface2);
  box-shadow: var(--shadow-lg);
}

.map-popup .maplibregl-popup-tip { border-top-color: var(--surface2); }

.map-card { width: 240px; }

.map-card__photo {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  background: var(--surface3);
}

.map-card__body { padding: 10px 12px 12px; display: grid; gap: 8px; }

.map-card__title {
  font-weight: 650;
  font-size: 15px;
  line-height: 1.3;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.map-card__meta { display: flex; align-items: center; gap: 10px; font-size: 14px; }
.map-card__price { font-weight: 700; color: var(--brand-ink); }
.map-card__rating { color: var(--text2); }


@media (max-width: 900px) {
  .map-page__canvas { height: calc(100vh - 200px); }
  .map-bar { top: 8px; left: 8px; right: 8px; padding: 6px; border-radius: var(--radius); }
  /* Разделитель уходит — на телефоне панель и так короткая. Список
     разделов остаётся: он занимает место одного слова, а без него сменить
     раздел с карты было нельзя вовсе */
  .map-bar__sep { display: none; }
  .map-bar__kind { max-width: 104px; }
  /* «Найти» уходит: на 360 px три подписи рядом с полем поиска не
     помещаются, а эта кнопка — единственная лишняя из трёх. Такая же есть
     внутри уточнений, и форма отправляется вводом с клавиатуры. Остаются
     «Фильтры» и «Списком» — то, чего больше нигде нет */
  .map-bar__go { display: none; }
  .map-bar__more > summary,
  .map-bar__list { padding: 0 11px; }
  /* Панель цепляется к ПРАВОМУ краю кнопки, а не к левому: кнопка
     «Фильтры» стоит в правой половине полосы, и 300 px влево от неё
     помещаются, а вправо — уезжают за экран вместе с полями */
  .map-bar__panel {
    left: auto;
    right: 0;
    width: min(300px, calc(100vw - 40px));
  }
  .map-mark { width: 150px; }
  .map-mark__photo { width: 36px; height: 36px; }
}

/* Полоса карты не переносится никогда — значит на телефоне она обязана
   помещаться. На 390 px ей не хватало семи пикселей: отдаём их списку
   разделов и зазорам, а не прокрутке всей карты */
@media (max-width: 575.98px) {
  .map-bar { gap: 6px; }
  .map-bar__kind { max-width: 88px; }
  .map-bar__more > summary,
  .map-bar__list { padding: 0 9px; }
}

/* --- Карточка объекта ------------------------------------------------ */

/* Галерея подстраивается под ЧИСЛО снимков. Сетка «2fr 1fr 1fr в два ряда»
   красива ровно на пяти фотографиях; на двух она оставляет три пустые ячейки,
   и карточка выглядит недогруженной, а не лаконичной */
.gallery {
  display: grid;
  gap: 6px;
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: 24px;
  grid-template-columns: 2fr 1fr 1fr;
  /* `minmax(0, 1fr)`, А НЕ `1fr`. Голое `1fr` — это `minmax(auto, 1fr)`, и
     ряд не может стать ниже собственного размера снимка: квадратный кадр в
     колонке 350 px требовал 350 px высоты, верхний ряд забирал её себе, а
     нижний оставался обрезком. Нулевой минимум разрешает делить поровну. */
  grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
  /* ВЫСОТА — ПРОПОРЦИЕЙ, А НЕ ПОТОЛКОМ.

     Здесь стоял `max-height: 460px`, и он не ограничивал сетку, а РЕЗАЛ её:
     ряды `1fr` в контейнере без заданной высоты набирают её из снимков,
     переваливают за потолок, и `overflow: hidden` срезает нижний ряд
     наполовину. Подпись «смотреть все фотографии» оказывалась ровно в этой
     срезанной половине. Пропорция задаёт высоту точно, и `1fr 1fr` делит
     именно её. */
  aspect-ratio: 3 / 1;
}

.gallery img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  cursor: zoom-in;
  background: var(--surface3);
}

.gallery--1 { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr); aspect-ratio: 16 / 7; }
.gallery--2 { grid-template-columns: 1fr 1fr; grid-template-rows: minmax(0, 1fr); aspect-ratio: 16 / 7; }
.gallery--3 { grid-template-columns: 2fr 1fr;
  grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); aspect-ratio: 7 / 3; }
.gallery--4 { grid-template-columns: 2fr 1fr 1fr;
  grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); }

.gallery--3 > *:first-child,
.gallery--4 > *:first-child,
.gallery--5 > *:first-child { grid-row: span 2; }

.gallery--4 > *:nth-child(2) { grid-column: span 2; }

/* ПОСЛЕДНЯЯ ПЛИТКА — ДВЕРЬ В АЛЬБОМ (Р-225).

   Снимок под затемнением остаётся виден: пустой прямоугольник со словом
   «ещё» ничего не обещает, а «под этим есть ещё двадцать» — обещает.
   Затемнение снизу, а не сплошное: верх кадра, по которому снимок и узнают,
   остаётся светлым. */
.gallery__more {
  position: relative;
  display: block;
  overflow: hidden;
  color: #fff;
  text-decoration: none;
}

.gallery__more img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Не «приблизить», а «перейти»: плитка ведёт в альбом, а не открывает
     снимок на весь экран, как её соседи */
  cursor: pointer;
  transition: transform var(--motion, .18s) ease;
}

.gallery__more::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgb(0 0 0 / .42);
}

.gallery__more:hover img { transform: scale(1.04); }

/* Метка стоит В СЕРЕДИНЕ плитки — и по ширине, и по высоте. Прижатая к
   низу, она читалась как подпись к снимку, а не как «здесь есть ещё» */
.gallery__more-box {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 2px;
  padding: 0 10px;
  text-align: center;
}

.gallery__more-box svg { fill: currentColor; opacity: .9; }
.gallery__more-text { font-weight: 600; font-size: 14px; line-height: 1.2; }
.gallery__more-count { font-size: 12px; opacity: .8; }

/* ЗНАЧОК «ВСЕ ФОТО» — поверх галереи, в правом нижнем углу.

   На узком экране галерея превращается в ленту, и дверь в альбом уезжает за
   правый край: тот, кто ленту не пролистал, про альбом бы не узнал вовсе.
   На широком значок не нужен — там дверь видна плиткой, — и показывается он
   только у объекта, где плитки-двери нет (снимков пять или меньше). */
.gallery-wrap { position: relative; }

.gallery__badge {
  position: absolute;
  right: 12px;
  bottom: 12px;
  display: none;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border-radius: 999px;
  background: rgb(0 0 0 / .62);
  color: #fff;
  font-size: 13px;
  text-decoration: none;
  backdrop-filter: blur(4px);
}

.gallery__badge svg { fill: currentColor; }
.gallery__badge:hover { background: rgb(0 0 0 / .78); color: #fff; }

.gallery-wrap--nodoor .gallery__badge { display: inline-flex; }

/* АЛЬБОМ (W28) — КАМЕННАЯ КЛАДКА.

   `columns` вместо grid именно потому, что высота у снимков разная: сетка
   выравнивала бы ряды по самому высокому и оставляла полосы пустоты, а
   кладка их не оставляет вовсе. Порядок при этом идёт по колонкам сверху
   вниз — для альбома это допустимо: у снимков нет обязательной
   последовательности, в отличие от списка. */
.album {
  columns: 4;
  column-gap: 14px;
  margin-bottom: 24px;
}

/* СНИМОК И ПОДПИСЬ — ОДНОЙ КЛЕТКОЙ СЕТКИ, А НЕ `position: absolute` (Р-226).

   Подпись стояла абсолютом внутри колонок (`columns`), и в Safari на iPhone
   её не было видно вовсе: у абсолютного потомка внутри разбитого на колонки
   потока Safari считает содержащий блок по-своему, и элемент уезжает мимо
   снимка. Обе части лежат в одной клетке сетки и накладываются друг на
   друга — тот же вид, но без фрагментированного контекста. */
.album__cell {
  display: grid;
  margin: 0 0 14px;
  /* Разрыв колонки посреди снимка разрезал бы его пополам */
  break-inside: avoid;
  border-radius: var(--radius);
  overflow: hidden;
  cursor: zoom-in;
  background: var(--surface3);
}

.album__cell > img,
.album__caption { grid-area: 1 / 1; }

.album__cell img {
  display: block;
  width: 100%;
  height: auto;
  transition: transform var(--motion, .18s) ease;
}

.album__cell:hover img { transform: scale(1.03); }

/* Подпись — на самом снимке, слева внизу: под снимком она разрывала бы кладку.

   ЗАТЕМНЯЮЩЕЙ ПОЛОСЫ ПОД НЕЙ НЕТ (Р-229). Она перечёркивала низ каждого
   кадра, а кадров в кладке два десятка: подпись есть у трёх, а тёмная полоса
   была у всех. Читаемость держит тень самих букв — она обводит только их и
   снимка не трогает.

   `position: relative` и `z-index` обязательны: под курсором снимок
   увеличивается, а преобразование создаёт контекст наложения и поднимает
   картинку НАД обычным соседом. Подпись из-за этого пропадала ровно тогда,
   когда на неё наводили. */
.album__caption {
  position: relative;
  z-index: 1;
  align-self: end;
  /* Поднята от нижнего края кадра (Р-231): прижатая к нему вплотную, подпись
     читалась как обрезанная краем, а не как подпись */
  padding: 10px 12px 14px;
  color: #fff;
  font-size: 13px;
  line-height: 1.35;
}

/* ПРОСМОТР ВО ВЕСЬ ЭКРАН.

   `hidden` убирает окно из потока полностью — без этого невидимый слой
   перехватывал бы нажатия по странице. Появление и уход — прозрачностью и
   лёгким приближением: мгновенная подмена всего экрана читается как переход
   на другую страницу, а это не переход. */
.viewer {
  position: fixed;
  inset: 0;
  z-index: 120;
  display: grid;
  /* ВЫСОТА СТРОКИ ЗАДАНА ЯВНО, И ЭТО НЕ ПРИДИРКА.

     Строка была неявной (`auto`), то есть неопределённой по высоте на момент
     раскладки её содержимого, — и `max-height: 100%` у снимка вырождался в
     `none`: высокий кадр вылезал на три десятка пикселей за нижний край
     экрана, там же срезались его нижние закругления. `minmax(0, 1fr)` в
     контейнере с определённой высотой даёт строке точный размер, и снимок
     вписывается в отведённое место с одинаковыми полями со всех сторон. */
  grid-template-rows: minmax(0, 1fr);
  background: rgb(0 0 0 / .92);
  opacity: 0;
  transition: opacity .2s ease;
}

/* Свой `display: none` для спрятанного окна.

   `display: grid` выше — правило автора, и оно перебивает браузерное
   `[hidden] { display: none }`: без этой строки спрятанное окно оставалось бы
   видимым. Сейчас его прячет Bootstrap своим `!important`, но полагаться на
   чужую таблицу в таком месте нельзя — уберём Bootstrap, и на каждой странице
   альбома появится чёрный слой во весь экран. */
.viewer[hidden] { display: none; }

.viewer--on { opacity: 1; }

.viewer__stage {
  overflow: hidden;
  display: grid;
  /* СТРОКА И КОЛОНКА ЗАДАНЫ ЯВНО — ИНАЧЕ СНИМОК НЕ ВПИСЫВАЕТСЯ.

     Высоту снимку задаёт не окно, а вот эта строка: `max-height: 100%` у
     картинки считается от её клетки сетки. Пока строка была неявной
     (`auto`), клетка оставалась неопределённой по высоте на момент раскладки
     содержимого, процент вырождался в `none`, и кадр 3:2 вылезал за нижний
     край экрана на восемь десятков пикселей — вместе со своими нижними
     закруглениями. `minmax(0, 1fr)` в контейнере с определённой высотой
     даёт клетке точный размер. */
  grid-template-rows: minmax(0, 1fr);
  grid-template-columns: minmax(0, 1fr);
  place-items: center;
  padding: 24px;
  /* Жесты забирает скрипт: без этого браузер сам листает страницу и сам
     приближает щипком, а снимок под пальцами остаётся неподвижным */
  touch-action: none;
}

/* У СНИМКА НЕТ `transition` ВОВСЕ.

   Он здесь был — и каждое движение мыши доезжало до места за 0,22 с: снимок
   тянулся за указателем с задержкой и «примагничивался» к нему после того,
   как кнопку отпустили. Перетаскивание обязано идти один к одному, а
   появление рисует отдельная анимация ниже: она заканчивается раньше, чем
   человек успевает тронуть снимок. */
.viewer__image {
  max-width: 100%;
  max-height: 100%;
  border-radius: var(--radius);
  cursor: zoom-out;
  /* Отдельный слой: перерисовка при перетаскивании не задевает остальное */
  will-change: transform;
  /* Перетаскивание картинки браузером мешает таскать её нам */
  user-select: none;
  -webkit-user-drag: none;
}

.viewer--on .viewer__image { animation: viewer-in .22s ease; }

/* Конечный кадр не задан намеренно: им становится встроенный стиль, который
   выставил скрипт. Так анимация появления не спорит с приближением */
@keyframes viewer-in {
  from { transform: scale(.94); }
}

/* Подпись — внизу по центру: снимок стоит посередине экрана, и подпись под
   ним читается как его подпись, а не как надпись в углу страницы.

   ЗАТЕМНЕНИЯ ПОД НЕЙ НЕТ (Р-231): полоса лежала на нижнем крае каждого
   открытого снимка, а фон под ним и так почти чёрный — затемнять его нечем
   и незачем. Подпись поднята от края: прижатая к самому низу, она читалась
   как строка состояния браузера, а не как часть окна. */
.viewer__caption {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 34px;
  padding: 0 22px;
  color: #fff;
  font-size: 14px;
  line-height: 1.4;
  text-align: center;
  pointer-events: none;
}

/* УПРАВЛЕНИЕ — ТЕМИ ЖЕ КНОПКАМИ, ЧТО НА КАРТЕ (Р-226).

   Классы `map-tools` и `map-tool` взяты как есть: человек, нажимавший «плюс»
   на карте, узнаёт их сразу, а вторая пара кнопок того же назначения в другом
   виде читалась бы как другой элемент. Здесь они столбиком и в углу,
   который назван модификатором: в альбоме сверху справа, на карточке объекта
   снизу справа — там же, где на карте. */
.viewer__tools {
  position: absolute;
  z-index: 2;
  /* Отодвинуты от самого угла внутрь: снимок стоит в 24 px от края экрана, и
     кнопки, прижатые к углу, ложились ровно на его границу */
  right: 42px;
  bottom: 42px;
  top: auto;
}

/* СТРЕЛКИ ЛИСТАНИЯ (Р-227) — НА ЭКРАНЕ, А НЕ НА СНИМКЕ.

   Приближение двигает только сам снимок: будь стрелки его частью, на четырёх
   кратах они уехали бы за край вместе с кадром, и листать стало бы нечем.
   Полупрозрачные и небольшие: в альбом приходят смотреть фотографии, а не
   кнопки. */
.viewer__nav {
  position: absolute;
  z-index: 2;
  top: 50%;
  transform: translateY(-50%);
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 999px;
  background: rgb(255 255 255 / .12);
  color: #fff;
  cursor: pointer;
  opacity: .55;
  transition: opacity var(--motion, .18s) ease, background var(--motion, .18s) ease;
}

.viewer__nav:hover { opacity: 1; background: rgb(255 255 255 / .24); }
.viewer__nav--prev { left: 18px; }
.viewer__nav--next { right: 18px; }

/* На телефоне стрелки жмут пальцем и мимо снимка их не поставить: там они
   прижимаются к краям экрана вплотную */
@media (max-width: 640px) {
  .viewer__nav--prev { left: 8px; }
  .viewer__nav--next { right: 8px; }
  .viewer__tools { right: 20px; bottom: 20px; }
}

/* Пока окно открыто, страница под ним не прокручивается: иначе колесо
   уводит фон, и закрытое окно оставляет человека в другом месте страницы */
.no-scroll { overflow: hidden; }

/* Отбор отзывов (W29). Поля в строку на широком, в столбик на узком */
.review-filter {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 12px;
  align-items: end;
  padding: 14px;
  margin-bottom: 18px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface2);
}

.review-filter__field { display: grid; gap: 4px; min-width: 0; }
.review-filter__field--wide { grid-column: span 2; }
.review-filter__actions { display: flex; gap: 8px; }

@media (max-width: 640px) {
  .review-filter__field--wide { grid-column: span 1; }
}

.listing-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  gap: 32px;
  align-items: start;
}

.booking-box {
  position: sticky;
  top: 84px;
  padding: 20px;
  border: 1px solid var(--border2);
  border-radius: var(--radius);
  background: var(--surface2);
  box-shadow: var(--shadow-md);
}

/* ГОРИЗОНТАЛЬНАЯ ПАНЕЛЬ БРОНИРОВАНИЯ на карточке объекта (Р-222).

   Та же карточка — рамка, тень, подложка те же, — но лежит поперёк и стоит
   в потоке страницы. Липкой она быть перестала намеренно: на невысоком
   экране длинный чек не помещался в высоту целиком, и низ панели, то есть
   саму кнопку, человек не видел вовсе. Прокрутить липкий блок внутри себя
   нельзя — он прибит к окну.

   Колонки: выбор дат — цена и настройки — чек с кнопкой. Ширина у панели
   больше высоты, и от числа ночей в высоту она почти не растёт. */
.booking-box--row {
  position: static;
  margin-bottom: 24px;
}

/* ГОРИЗОНТАЛЬНЫЙ БЛОК ОПЛАТЫ на карточке заказа (Р-224).

   Та же карточка, что панель бронирования на объекте, и по той же причине:
   липкой колонкой справа она не помещалась в высоту на невысоком экране, и
   кнопку внизу человек не видел. Колонки две: условия отмены — они и есть
   объём блока — и само действие. */
.pay-box__grid {
  display: grid;
  gap: 20px 28px;
  grid-template-columns: minmax(0, 1.7fr) minmax(260px, 0.8fr);
  align-items: start;
}

.pay-box__terms,
.pay-box__action {
  min-width: 0;
  display: grid;
  gap: 12px;
  align-content: start;
}

.pay-box__head {
  margin: 0;
  font-size: 15px;
}

/* Условий бывает два — основные и сезонные. Одно под другим они удлиняли
   блок ровно там, где он должен быть широким */
.pay-box__policies {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
  gap: 12px;
  align-items: start;
}

.pay-box__policies .policy-view { margin-bottom: 0; }

/* Планшет и телефон: условия сверху, действие под ними */
@media (max-width: 1080px) {
  .pay-box__grid { grid-template-columns: minmax(0, 1fr); }
}

/* Подпись договора под лесенкой стадий (Р-224): кнопка и пояснение рядом,
   а не кнопкой во всю ширину колонки, которой больше нет */
.rental-sign {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
}

/* Предложения долгосрочной аренды — по два в строку (Р-224) */
.contract-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr));
  gap: 12px;
}

.contract-offer {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  background: var(--surface2);
  align-items: start;
  gap: 12px;
}

/* СПЛОШНОЙ ТЕКСТ НЕ РАСТЯГИВАЕТСЯ ВО ВСЮ ШИРИНУ (Р-222).

   Блоки карточки заняли всю страницу, и это правильно: сетка удобств, карта,
   отзывы и панель бронирования от ширины только выигрывают. А описание —
   абзац: на 1400 px в его строке помещается больше двухсот знаков, и на
   переносе глаз не находит начало следующей строки. Ограничение задано в
   `ch` — это ширина цифры текущего шрифта, то есть мера самой строки, а не
   экрана: сменится шрифт — мера останется верной. */
.prose {
  max-width: 78ch;
  white-space: pre-line;
}

/* Текст без меры строки: описание объекта короткое, и мера оставляла справа
   от него пустой половину страницы (Р-231) */
.prose--wide { max-width: none; }

/* ОРИГИНАЛ МАШИННОГО ПЕРЕВОДА (находка №86). Раскрывается под описанием и
   выглядит ссылкой, а не кнопкой: это справка, а не действие. Тело — на
   утопленной поверхности, чтобы оригинал не читался продолжением перевода */
.original > summary {
  cursor: pointer;
  color: var(--brand-ink);
  font-size: 13px;
}
.original > summary:hover { color: var(--brand); }
.original__body {
  margin-top: 10px;
  padding: 14px 16px;
  background: var(--surface);
  border-radius: var(--radius);
}
.original__body > :last-child { margin-bottom: 0; }

/* НАЖАТИЕ ПАЛЬЦЕМ НЕ ДОЛЖНО ОБРУБАТЬ УГЛЫ (Р-231).

   На долгом нажатии мобильный браузер рисует поверх элемента свою подсветку
   прямоугольником — скруглённые углы снимка на телефоне превращались в
   прямые, и выглядело это поломкой. `touch-callout` заодно убирает окно
   «сохранить изображение»: снимок здесь открывают, а не выдёргивают. */
.album__cell,
.gallery > * {
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
}

.booking-box__grid {
  display: grid;
  gap: 20px 28px;
  /* Календарь не тянется: клетка у него квадратная (`aspect-ratio: 1`), и на
     колонке в 700 px дни превращаются в плитку размером с палец ноги */
  grid-template-columns: minmax(260px, 400px) minmax(190px, 0.8fr) minmax(240px, 1fr);
  align-items: start;
}

/* `min-width: 0` — иначе длинная строка чека распирает колонку грида:
   у элемента грида минимальная ширина по умолчанию равна ширине содержимого */
.booking-box__col {
  min-width: 0;
  display: grid;
  gap: 14px;
  align-content: start;
}

.booking-box__col--calendar { max-width: 400px; }

/* Подпись и счётчик в одну строку (Р-223). В вертикальной колонке подпись
   сверху экономила ширину, которой не было; в горизонтальной панели ширина
   есть, а высота общая на все три колонки — и лишняя строка удлиняет всю
   панель */
.field--inline {
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.field--inline > label { margin: 0; }
.field--inline .stepper { flex: none; }

/* Условия отмены выбранных дат (Р-223). Заполняют место под настройками —
   раньше там была пустота, а вопрос «что если планы поменяются» человек
   задаёт до брони, а не после */
.booking-policy {
  display: grid;
  gap: 6px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

.booking-policy__head {
  font-weight: 650;
  font-size: 14px;
}

/* Карточка условий внутри панели живёт без нижнего отступа: расстояния здесь
   держит грид колонки, и `mb` макроса добавлял бы к ним второй */
.booking-policy .policy-view { margin-bottom: 0; }

/* Планшет: календарь ложится сверху И РАСТЯГИВАЕТСЯ НА ВСЮ СТРОКУ.
   Три колонки здесь уже тесны: календарю нужно 260 px, и остальным двум
   остаётся по 200 — чек в такой колонке переносится на каждой строке.
   Ограничение ширины при этом снимается: календарь в строке один, и справа
   от него оставалась пустота в половину панели */
@media (max-width: 1080px) {
  .booking-box__grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .booking-box__col--calendar { grid-column: 1 / -1; max-width: none; }

  /* Клетка перестаёт быть квадратной. У квадрата при ширине строки в 800 px
     сторона выходит под сто двадцать пикселей — день размером с ладонь, и
     месяц занимает экран целиком. Высота задаётся числом: широкая клетка
     остаётся клеткой, а календарь — календарём */
  .booking-box__col--calendar .cal__day {
    aspect-ratio: auto;
    height: 44px;
  }
}

/* Телефон: всё в столбик, в порядке действий — выбрал даты, проверил
   условия, увидел сумму, нажал */
@media (max-width: 720px) {
  .booking-box__grid { grid-template-columns: minmax(0, 1fr); }
  .booking-box__col--calendar { grid-column: auto; }
}

.facts {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 12px;
  margin: 18px 0;
}

/* Лесенка стадий (BntSteps, docs/design.md). Долгосрочная сделка идёт
   неделями, и гость возвращается на страницу через дни: он должен узнать
   своё место, не читая. Пройденное обычным цветом, текущее выделено,
   будущее видно, но бледно — знать, что впереди, важно до согласия */
.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.steps__item {
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: 12px;
  color: var(--text3);
  background: var(--surface);
  opacity: 0.55;
}

.steps__item--done { opacity: 1; color: var(--text2); }

.steps__item--now {
  opacity: 1;
  color: var(--text);
  font-weight: 600;
  border-color: var(--brand);
}

.fact { padding: 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); }
.fact__label { font-size: 12px; color: var(--text3); }
.fact__value { font-weight: 600; }

/* УДОБСТВА — ПО ШИРИНЕ СЛОВА, А НЕ ПО КЛЕТКЕ (Р-230).

   Здесь была сетка с колонками не уже 190 px, и «Wi-Fi» занимало ровно
   столько же места, сколько «Стиральная машина»: за коротким словом
   тянулась пустота в полторы сотни пикселей. На телефоне колонка была одна,
   и каждое удобство растягивалось на всю ширину строки.

   Обычный поток строкой: каждое удобство шириной со своё слово, следующее
   встаёт рядом, а не влезающее переносится на новую строку. */
.amenities {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* Сетка оценок по критериям в форме отзыва (Р-230). Шесть коротких полей
   подряд в столбик — это шесть экранов прокрутки на телефоне; в ряд по
   ширине они укладываются в две строки */
.score-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 10px 12px;
}

.score-grid__item { display: grid; gap: 4px; min-width: 0; }

.review { padding: 16px 0; border-top: 1px solid var(--border); }
.review__head { display: flex; gap: 10px; align-items: center; margin-bottom: 8px; }
.review__reply {
  margin-top: 10px;
  padding: 10px 12px;
  background: var(--surface);
  border-radius: var(--radius-sm);
  border-left: 3px solid var(--brand);
  /* Та же мера строки, что у самого отзыва (Р-222): полосой во всю ширину
     ответ хозяина выглядел шире, чем отзыв, на который он отвечает */
  max-width: 78ch;
}

/* СТРАНИЦА ВСЕХ ОТЗЫВОВ — БЕЗ МЕРЫ СТРОКИ (Р-230).

   Мера в 78 знаков нужна там, где сплошной текст идёт среди других блоков:
   под объявлением он один такой, и растянутый на 1400 px глаз теряет начало
   следующей строки. Здесь же вся страница состоит из отзывов, и мера
   оставляла пустой ровно половину её ширины. */
.page-reviews .review .prose,
.page-reviews .review__reply { max-width: none; }

/* --- Чат ------------------------------------------------------------- */

.chat-layout {
  display: grid;
  grid-template-columns: 300px minmax(0, 1fr);
  gap: 20px;
  align-items: start;
}

.dialog-list { display: flex; flex-direction: column; }

.dialog-item {
  display: flex;
  gap: 10px;
  padding: 12px;
  border-bottom: 1px solid var(--border);
  color: var(--text);
}

.dialog-item:hover { background: var(--surface); text-decoration: none; }
.dialog-item--active { background: var(--surface3); }

/* Пузыри, лента и поле ввода — в разделе «Переписка» ниже: одна
   реализация вместо двух наборов правил с одинаковыми именами. */

/* --- Подвал ---------------------------------------------------------- */

.footer {
  flex: none;
  border-top: 1px solid var(--border);
  background: var(--surface);
  padding: 28px 0;
  color: var(--text2);
  font-size: 14px;
}

.footer__cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 24px;
  margin-bottom: 20px;
}

.footer a { color: var(--text2); display: block; padding: 3px 0; }
.footer a:hover { color: var(--text); }

/* --- Адаптив: от 360 px ---------------------------------------------- */

@media (max-width: 980px) {
  .listing-layout,
  .chat-layout,
  .search-layout--map { grid-template-columns: 1fr; }
  .booking-box { position: static; }
  .map-page__canvas { height: 60vh; }
  /* ГАЛЕРЕЯ НА УЗКОМ — ЛЕНТА, А НЕ МОЗАИКА (Р-225).

     Мозаика, сложенная в две колонки, занимала на телефоне два экрана: до
     названия объекта человек долистывал, а первый вопрос у него всё-таки
     «что это и почём». Лента показывает один кадр целиком, следующий
     выглядывает справа — этим и сказано, что её листают. Высота задана
     пропорцией кадра и от числа снимков не зависит вовсе. */
  .gallery {
    display: flex;
    gap: 8px;
    aspect-ratio: auto;
    max-height: none;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    /* Полосу прокрутки прячем: лента и так листается пальцем, а под кадрами
       она съедала бы часть высоты */
    scrollbar-width: none;
  }

  .gallery::-webkit-scrollbar { display: none; }

  .gallery > * {
    flex: 0 0 86%;
    scroll-snap-align: center;
    aspect-ratio: 4 / 3;
    border-radius: var(--radius);
    overflow: hidden;
  }

  .gallery > * img { height: 100%; object-fit: cover; }

  .gallery__badge { display: inline-flex; }
  /* В ленте край галереи совпадает с краем экрана — значку нужен отступ
     побольше, иначе он липнет к самой границе */
  .gallery__badge { right: 14px; bottom: 14px; }

  .album { columns: 3; }
}

/* Ниже 380 слово «Bounty» рядом со знаком не помещается вместе с настройками:
   знак сервиса узнают и без подписи, а выбор языка спрятать нельзя */
/* На планшете в ленту помещаются два кадра сразу: экран для этого достаточно
   широк, а один кадр на 900 px выглядел бы неоправданно крупным */
@media (min-width: 641px) and (max-width: 980px) {
  .gallery > * { flex: 0 0 48%; }
}

@media (max-width: 700px) {
  .album { columns: 2; }
}

@media (max-width: 379px) {
  .logo__word { display: none; }
}

@media (max-width: 640px) {
  .wrap { padding: 0 14px; }
  h1 { font-size: 24px; }
  .hero { padding: 28px 0 22px; }
  .hero h1 { font-size: 26px; }
  .nav { display: none; }
  .header__inner { height: 56px; }
  /* Гостю на телефоне — одна кнопка входа вместо двух: обе вместе с языком,
     валютой и темой шире экрана. Регистрация со страницы входа в одно
     нажатие, и это лучше, чем шапка, уезжающая вбок */
  .header__register { display: none; }
  .grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; }
}

/* Печать: человек уносит с собой бронь, а не интерфейс */
@media print {
  .header, .footer, .booking-box, .btn { display: none !important; }
}

/* Хлебные крошки и сетка направлений (W25, W26).
   Крошки переносятся: «Таиланд › Пхукет › Патонг» на узком экране обязано
   переноситься, а не уезжать за край страницы. */
.crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.crumbs a { color: var(--text2); }
.crumbs a:hover { color: var(--brand); }

.place-grid {
  display: grid;
  gap: 12px;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}

@keyframes bnt-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .35; }
}

/* --- Шапка: значки, выпадающие меню, кнопка профиля ------------------ */

/* Кнопка-значок с рамкой. До этого переключатель темы был голым символом и
   в шапке его попросту не было видно — люди не догадывались, что он там */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  height: 36px;
  min-width: 36px;
  padding: 0 10px;
  border: 1px solid var(--border2);
  border-radius: var(--radius-sm);
  background: var(--surface2);
  color: var(--text2);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  list-style: none;
  user-select: none;
}

.icon-btn:hover { background: var(--surface3); color: var(--text); text-decoration: none; }
.icon-btn::-webkit-details-marker { display: none; }
.icon-btn--wide { min-width: 56px; }

.menu__caret { font-size: 9px; opacity: .6; }

/* Меню на <details>: оно открывается и без скрипта. Список, который не
   открывается при сбое JavaScript, — единственный способ сменить язык на
   странице, где скрипт не загрузился */
.menu { position: relative; }

.menu[open] > .icon-btn { background: var(--surface3); color: var(--text); }

.menu__body {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  z-index: 50;
  min-width: 210px;
  padding: 6px;
  border: 1px solid var(--border2);
  border-radius: var(--radius);
  background: var(--surface2);
  box-shadow: var(--shadow-lg);
}

.menu__title {
  padding: 6px 10px 8px;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--text3);
}

.menu__item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.menu__item:hover { background: var(--surface3); text-decoration: none; }
.menu__item--active { background: var(--brand-bg); color: var(--brand-ink); font-weight: 600; }
.menu__code { font-size: 12px; color: var(--text3); }

.menu__note {
  padding: 8px 10px 4px;
  font-size: 12px;
  line-height: 1.4;
  color: var(--text3);
  border-top: 1px solid var(--border);
  margin-top: 4px;
}

.profile-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 36px;
  padding: 0 12px 0 4px;
  border: 1px solid var(--border2);
  border-radius: 999px;
  background: var(--surface2);
  color: var(--text);
  font-weight: 600;
  font-size: 14px;
}

.profile-btn:hover { background: var(--surface3); text-decoration: none; }
.profile-btn .avatar { width: 28px; height: 28px; font-size: 13px; }

.profile-btn__name {
  max-width: 130px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- Профиль: сводка вместо списка ссылок ---------------------------- */

.profile-head {
  display: flex;
  align-items: center;
  gap: 18px;
  flex-wrap: wrap;
  padding: 22px;
  border-radius: var(--radius-lg);
  background: var(--gradient-brand);
  color: #fff;
  margin-bottom: 22px;
}

.profile-head .avatar {
  width: 64px;
  height: 64px;
  font-size: 24px;
  background: rgba(255, 255, 255, .22);
  color: #fff;
  border: 2px solid rgba(255, 255, 255, .5);
}

.profile-head h1 { color: #fff; margin: 0; font-size: 26px; }
.profile-head .muted { color: rgba(255, 255, 255, .82); }

.stats {
  display: grid;
  gap: 12px;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  margin-bottom: 22px;
}

.stat {
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface2);
}

.stat:hover { border-color: var(--border2); box-shadow: var(--shadow-sm); text-decoration: none; }
.stat__value { font-size: 24px; font-weight: 700; line-height: 1.1; color: var(--text); }
.stat__label { font-size: 13px; color: var(--text3); margin-top: 4px; }

.profile-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
  gap: 24px;
  align-items: start;
}

.upcoming {
  display: grid;
  gap: 12px;
}

/* Ближайшая поездка — крупной строкой с фотографией: на экране профиля это
   единственное, что человек действительно ищет глазами */
.upcoming__item {
  display: grid;
  grid-template-columns: 96px minmax(0, 1fr) auto;
  gap: 14px;
  align-items: center;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface2);
  color: var(--text);
}

.upcoming__item:hover { border-color: var(--brand); text-decoration: none; }

.upcoming__photo {
  width: 96px;
  height: 72px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  background: var(--surface3);
}

.side-menu { display: grid; gap: 6px; }

.side-menu a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 11px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface2);
  color: var(--text);
  font-weight: 500;
}

.side-menu a:hover { border-color: var(--brand); background: var(--surface); text-decoration: none; }
.side-menu__badge {
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--brand);
  color: #fff;
  font-size: 12px;
  font-weight: 700;
  display: grid;
  place-items: center;
}

@media (max-width: 900px) {
  .profile-layout { grid-template-columns: 1fr; }
  .profile-btn__name { display: none; }
  .profile-btn { padding: 0 4px; }
}

/* Группы разделов профиля */
.side-group { margin-bottom: 18px; }

.side-group__title {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--text3);
  margin: 0 0 8px 2px;
}

.side-menu__hint {
  display: block;
  font-size: 12px;
  font-weight: 400;
  color: var(--text3);
  margin-top: 2px;
}

.side-menu--active {
  border-color: var(--brand) !important;
  background: var(--brand-bg) !important;
}

/* Кружок с числом объектов на мелком зуме */
.map-cluster {
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  min-width: 40px;
  height: 40px;
  padding: 0 8px;
  border-radius: 999px;
  border: 2px solid var(--surface2);
  background: var(--brand);
  color: #fff;
  box-shadow: var(--shadow-md);
  cursor: pointer;
}

.map-cluster:hover { background: var(--brand-ink); }


/* --- Градиентные обводки (docs/themes.md §4) -------------------------
 *
 * Градиент не принимается в `border-color`, поэтому обводка рисуется двумя
 * фонами: цвет поверхности в `padding-box` и градиент в `border-box`. Фон
 * внутри остаётся обычным цветом темы — текст на нём читается, чего не было
 * бы, залей мы градиентом всю карточку.
 *
 * Обводка — акцент, а не оформление всего подряд: она стоит там, где взгляд
 * должен остановиться (панель брони, карточка под курсором, аватар), и не
 * стоит на длинных списках. */
.gradient-edge {
  border: 1px solid transparent;
  background:
    linear-gradient(var(--surface2), var(--surface2)) padding-box,
    var(--gradient-border) border-box;
}

/* Панель бронирования — главное, ради чего человек открыл карточку */
.booking-box {
  border: 1px solid transparent;
  background:
    linear-gradient(var(--surface2), var(--surface2)) padding-box,
    var(--gradient-border) border-box;
}

/* Карточка объекта под курсором: обводка вместо смены цвета рамки —
   так видно, что карточка «живая», и при этом ничего не сдвигается */
.tile { border: 1px solid var(--border); }

.tile:hover {
  border-color: transparent;
  background:
    linear-gradient(var(--surface2), var(--surface2)) padding-box,
    var(--gradient-border) border-box;
  box-shadow: var(--shadow-md);
}

.stat:hover,
.side-menu a:hover,
.upcoming__item:hover,
.map-mark:hover {
  border-color: transparent;
  background:
    linear-gradient(var(--surface2), var(--surface2)) padding-box,
    var(--gradient-border) border-box;
}

/* Аватар в шапке профиля — 2 px по документу тем */
.profile-btn .avatar,
.profile-head .avatar {
  border: 2px solid transparent;
  background:
    linear-gradient(var(--surface3), var(--surface3)) padding-box,
    var(--gradient-border) border-box;
}

.profile-head .avatar {
  background:
    linear-gradient(rgba(255, 255, 255, .22), rgba(255, 255, 255, .22)) padding-box,
    linear-gradient(#fff, rgba(255, 255, 255, .6)) border-box;
}

/* Мягкая подложка под блоком-приглашением */
.panel--soft {
  background: var(--gradient-soft);
  border-color: transparent;
}

/* ======================================================================
 * Компоненты второго захода по вебу: календарь, переписка, боковое меню
 * разделов, страница бизнеса, билет заселения, капча.
 *
 * Цвет по-прежнему только из токенов. Белый (#fff) и полупрозрачный белый —
 * единственное исключение и только поверх фирменного градиента: подложка там
 * одинакова в обеих темах, значит и то, что на ней лежит, обязано быть
 * одинаковым.
 * ====================================================================== */

/* --- Заголовок страницы с возвратом ---------------------------------- */

/* «← К профилю» стоит СЛЕВА от заголовка, а не у правого края.
   У правого края возврат оторван от того, к чему относится, и на широком
   экране до него полстраницы пустоты. Слева он читается как путь: откуда
   пришли — куда пришли. */
.page-head {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: 18px;
}

.page-head h1 { margin: 0; }

.page-head__back {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px 6px 9px;
  border-radius: 999px;
  border: 1px solid var(--border2);
  background: var(--surface2);
  color: var(--text2);
  font-size: 13px;
  font-weight: 550;
  white-space: nowrap;
}

.page-head__back:hover {
  background: var(--surface3);
  color: var(--text);
  text-decoration: none;
}

.page-head__aside { margin-left: auto; display: flex; gap: 8px; align-items: center; }

/* --- Раздел с боковым меню ------------------------------------------- */

/* Тот же каркас, что у профиля, — и это не дублирование, а одно и то же
   место. «Избранное», «Поездки», «Сообщения» и «Уведомления» живут внутри
   профиля: попав в них, человек не должен терять из виду остальные разделы
   и возвращаться в профиль ради каждого перехода. */
.side-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--sidebar-width);
  gap: 28px;
  align-items: start;
}

@media (max-width: 900px) {
  .side-layout { grid-template-columns: 1fr; }
  /* На телефоне меню разделов уходит ВНИЗ: сверху человеку нужен ответ, а не
     оглавление, через которое надо пролистать весь экран */
  .side-layout > aside { order: 2; }
}

/* --- Календарь занятости (W02) --------------------------------------- */

/* СВОЙ КАЛЕНДАРЬ, А НЕ ДВА `<input type=date>`.
 *
 * Системное поле даты не знает о занятости: человек выбирает в нём число,
 * жмёт «забронировать» и получает отказ «эти дни заняты» — и так до тех пор,
 * пока не угадает. Свободные даты обязаны быть видны ДО выбора, иначе выбор
 * превращается в перебор.
 *
 * Поле `type=date` при этом остаётся в разметке скрытым: оно и уходит на
 * сервер, и работает без скрипта. Календарь — надстройка над ним. */
.cal {
  border: 1px solid transparent;
  border-radius: var(--radius);
  background:
    linear-gradient(var(--surface2), var(--surface2)) padding-box,
    var(--gradient-border) border-box;
  padding: 12px;
}

.cal__head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}

.cal__title {
  flex: 1;
  text-align: center;
  font-weight: 650;
  /* `capitalize` поднимает КАЖДОЕ слово, и русское «сентябрь 2026 г.»
     становилось «Сентябрь 2026 Г.». Заглавная нужна только первой букве —
     её ставит `::first-letter` */
}

.cal__title::first-letter {
  text-transform: uppercase;
}

.cal__nav {
  width: 30px;
  height: 30px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--border2);
  background: var(--surface2);
  color: var(--text2);
  font: inherit;
  cursor: pointer;
}

.cal__nav:hover:not(:disabled) { background: var(--surface3); color: var(--text); }
.cal__nav:disabled { opacity: .3; cursor: not-allowed; }

.cal__grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
}

.cal__dow {
  text-align: center;
  font-size: 11px;
  color: var(--text3);
  padding-bottom: 4px;
  text-transform: uppercase;
}

.cal__day {
  aspect-ratio: 1;
  border: 0;
  background: transparent;
  border-radius: var(--radius-sm);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  display: grid;
  place-items: center;
  position: relative;
}

.cal__day:hover:not(:disabled) { background: var(--surface3); }
.cal__day--empty { visibility: hidden; }

/* Занято и прошедшее — разными способами, а не одним «серым».
   Прошедшее просто выключено, занятое перечёркнуто: человеку важно понять,
   что дом свободен «вообще», но не в эти числа */
.cal__day:disabled { color: var(--text3); cursor: not-allowed; }

.cal__day--busy {
  color: var(--text3);
  text-decoration: line-through;
  background: var(--surface);
}

.cal__day--busy::after {
  content: "";
  position: absolute;
  bottom: 4px;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--error);
}

/* Выбранный отрезок: концы закрашены закатом, середина — мягкой подложкой.
   Так видно и границы, и длину: одинаковая заливка на всём отрезке не
   показывает, где заезд, а где выезд */
.cal__day--in {
  background: var(--brand-bg);
  border-radius: 0;
  color: var(--brand-ink);
}

.cal__day--from,
.cal__day--to {
  background: var(--gradient-brand);
  background-origin: border-box;
  background-repeat: no-repeat;
  color: #fff;
  font-weight: 650;
}

.cal__day--from { border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
.cal__day--to { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
.cal__day--from.cal__day--to { border-radius: var(--radius-sm); }

/* Выбран один день, отрезка ещё нет. Квадратный правый край обещает
   продолжение, которого пока нет, и читается как недоделка */
.cal__day--only { border-radius: var(--radius-sm); }

/* Мало свободных номеров (Р-121). Не «занято» и не «свободно», а третье
   состояние: забронировать ещё можно, но откладывать не стоит. Цвет —
   предупреждающий из токенов, потому что это ровно предупреждение, а не
   ошибка и не успех */
.cal__day--few { color: var(--warning-ink); font-weight: 650; }

/* ДЕНЬ С ПРАВИЛОМ ЦЕНЫ (`BntRuledDay`, docs/design.md).
 *
 * Пунктирная черта под числом цветом бренда. Цветом заливки не отмечаем
 * намеренно: цвета в календаре уже заняты состояниями дня — свободно,
 * занято, выбрано, мало номеров, — и пятая заливка читалась бы как шестое
 * состояние. Пунктир означает ровно то, что нужно: «здесь есть, что
 * прочитать», а читается это в подсказке и в строке под календарём.
 *
 * Курсор оставляем `pointer`, хотя в документе стоял `help`: клетка — это
 * кнопка выбора даты, и «вопросительный» курсор обещал бы, что нажатие
 * ничего не сделает. */
.cal__day--ruled::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 5px;
  width: 14px;
  transform: translateX(-50%);
  border-bottom: 2px dotted var(--brand);
}

/* На выбранных сутках подложка своя — фирменный градиент или его мягкая
   заливка, — и оранжевый пунктир на ней теряется. Отметка остаётся, но
   берёт цвет текста клетки */
.cal__day--from.cal__day--ruled::after,
.cal__day--to.cal__day--ruled::after,
.cal__day--in.cal__day--ruled::after { border-bottom-color: currentColor; }

.cal__left {
  position: absolute;
  right: 3px;
  top: 2px;
  font-size: 9px;
  font-style: normal;
  line-height: 1;
  color: var(--warning-ink);
}

/* Что добавится к цене — списком под заголовком карточки. Маркеры убраны:
   это не перечень возможностей, а сноска, и точки делают её объявлением */
.price-note {
  list-style: none;
  padding: 0;
  margin: -4px 0 14px;
  display: grid;
  gap: 2px;
}

.cal__legend {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  margin-top: 10px;
  font-size: 12px;
  color: var(--text3);
}

.cal__legend span { display: inline-flex; align-items: center; gap: 5px; }

.cal__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: none;
}

.cal__dot--free { background: var(--success); }
.cal__dot--busy { background: var(--error); }
.cal__dot--picked { background: var(--brand); }

/* В легенде отметка правил показана собой же, а не кружком: кружок означал
   бы ещё одно состояние дня, а это не состояние */
.cal__dot--ruled {
  width: 14px;
  height: 0;
  border-radius: 0;
  background: none;
  border-bottom: 2px dotted var(--brand);
}

.cal__picked {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  font-size: 13px;
}

/* --- Переписка -------------------------------------------------------- */

/* ПЕРЕПИСКА — ЭКРАН, А НЕ ПРЯМОУГОЛЬНИК С ТЕКСТОМ.
 *
 * Раньше диалог был карточкой со скруглёнными углами посреди страницы: у
 * него не было ни шапки, ни собеседника, ни вложений — только лента и поле
 * ввода. Теперь это отдельный экран: сверху — с кем разговор и о чём объект,
 * снизу — то, чем отвечают. */
.chat {
  display: flex;
  flex-direction: column;
  height: calc(100vh - var(--header-height) - 120px);
  min-height: 460px;
  border: 1px solid transparent;
  border-radius: var(--radius-lg);
  background:
    linear-gradient(var(--surface2), var(--surface2)) padding-box,
    var(--gradient-border) border-box;
  overflow: hidden;
}

.chat__head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
  background: var(--surface2);
  flex: none;
}

/* Название объекта и его снимок в шапке ведут НА КАРТОЧКУ ОБЪЕКТА.
   Человек, который спрашивает про дом, обязан иметь возможность на этот дом
   посмотреть, не выходя из переписки и не ища его заново */
.chat__peer {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  flex: 1;
  color: var(--text);
}

.chat__peer:hover { text-decoration: none; }
.chat__peer:hover .chat__name { color: var(--brand-ink); }

.chat__photo {
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: 10px;
  object-fit: cover;
  background: var(--surface3);
  display: grid;
  place-items: center;
  font-weight: 600;
  color: var(--text2);
}

/* Аватарка поддержки — знак сервиса: переписку с Bounty человек обязан
   узнавать по значку, а не по первой букве слова «поддержка» */
.chat__photo--support {
  background: url("/static/img/icon.svg") center / cover no-repeat;
}

.chat__name {
  font-weight: 650;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat__sub { font-size: 12px; color: var(--text3); }

.chat__scroll {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  background: var(--surface);
}

/* Разделитель дня. Без него переписка, растянутая на две недели, выглядит
   одним разговором, и «завтра к десяти» относится непонятно к какому дню */
.day-sep {
  align-self: center;
  margin: 12px 0 8px;
  padding: 3px 12px;
  border-radius: 999px;
  background: var(--surface3);
  color: var(--text3);
  font-size: 12px;
}

.bubble {
  position: relative;
  max-width: 72%;
  padding: 9px 13px;
  border-radius: var(--radius-lg);
  background: var(--surface2);
  border: 1px solid var(--border);
  align-self: flex-start;
  word-break: break-word;
  margin-bottom: 4px;
}

/* Хвостик у пузыря: он отличает «строку в списке» от «реплики в разговоре» */
.bubble--mine {
  align-self: flex-end;
  background: var(--brand-bg);
  border-color: transparent;
  border-bottom-right-radius: 4px;
}

.bubble:not(.bubble--mine):not(.bubble--system) { border-bottom-left-radius: 4px; }

/* Системная запись — не реплика, а событие: подтверждение брони, оплата,
   отмена. Обе стороны разбирают спор по этой ленте, поэтому события живут
   здесь же, а не в отдельном журнале */
.bubble--system {
  align-self: center;
  max-width: 92%;
  text-align: center;
  background: var(--gradient-soft);
  border: 1px solid transparent;
  color: var(--text2);
  font-size: 13px;
  padding: 7px 14px;
  border-radius: 999px;
}

.bubble--deleted { opacity: .6; font-style: italic; color: var(--text3); }

.bubble__time {
  font-size: 11px;
  color: var(--text3);
  margin-top: 3px;
  display: flex;
  align-items: center;
  gap: 5px;
  justify-content: flex-end;
}

.bubble__mask { font-size: 12px; color: var(--warning-ink); margin-top: 4px; }

.bubble__photo {
  max-width: 240px;
  border-radius: var(--radius-sm);
  display: block;
  cursor: zoom-in;
}

/* ИМЯ ФАЙЛА НЕ ЛОМАЕТСЯ ПО БУКВАМ. У пузыря стоит `word-break: break-word`, и
   он ровно для текста: длинную фразу перенести можно. С именем документа это
   давало «cont / ract.p / df» в три строки — вложение переставало
   узнаваться. Здесь перенос выключен, а слишком длинное имя обрезается
   многоточием: полностью его видно в подсказке и после скачивания */
.bubble__file {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  word-break: normal;
  color: var(--brand-ink);
  font-weight: 550;
}

/* Пузырь с документом не сжимается до ширины иконки: ему нужно место под имя */
.bubble:has(.bubble__file) { min-width: min(260px, 80%); }

/* Кнопка меню сообщения. Появляется при наведении, а на телефоне видна
   всегда: там наведения не бывает вовсе */
.bubble__menu {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 24px;
  height: 24px;
  border: 0;
  border-radius: 50%;
  background: var(--surface2);
  color: var(--text3);
  cursor: pointer;
  opacity: 0;
  display: grid;
  place-items: center;
  line-height: 1;
}

.bubble:hover .bubble__menu { opacity: 1; }
.bubble__menu:hover { background: var(--surface3); color: var(--text); }

@media (hover: none) {
  .bubble__menu { opacity: .55; }
}

/* «Печатает…»: три точки вместо текста. Строка «собеседник печатает»
   занимает место и прыгает; точки не сдвигают ленту */
.typing {
  align-self: flex-start;
  display: inline-flex;
  gap: 4px;
  padding: 12px 14px;
  border-radius: var(--radius-lg);
  background: var(--surface2);
  border: 1px solid var(--border);
}

.typing i {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--text3);
  animation: bnt-typing 1.2s infinite ease-in-out;
}

.typing i:nth-child(2) { animation-delay: .18s; }
.typing i:nth-child(3) { animation-delay: .36s; }

@keyframes bnt-typing {
  0%, 60%, 100% { transform: translateY(0); opacity: .4; }
  30% { transform: translateY(-4px); opacity: 1; }
}

/* Поле ввода: скрепка, текст, микрофон, отправка */
/* СТРОКА ВВОДА — ОДНА СТРОКА, А СПИСОК ВЛОЖЕНИЙ ПОД НЕЙ.
   `flex-wrap` здесь не украшение: список файлов и строка-заметка лежат
   внутри той же формы, и без переноса они вставали в ряд со скрепкой и
   кнопкой отправки — строка разъезжалась на три этажа. */
.composer {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 8px;
  padding: 10px 12px;
  border-top: 1px solid var(--border);
  background: var(--surface2);
  flex: none;
}

/* Всё, что не кнопки и не поле, — отдельной строкой под ними */
.composer > .attach__list,
.composer > .attach__note {
  order: 10;
  flex: 1 0 100%;
  margin-top: 4px;
}

.composer__text {
  flex: 1;
  min-height: 40px;
  max-height: 120px;
  padding: 9px 12px;
  border: 1px solid var(--border2);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  resize: none;
  line-height: 1.4;
}

.composer__text:focus {
  outline: none;
  border-color: var(--brand);
}

.composer__btn {
  width: 40px;
  height: 40px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--border2);
  background: var(--surface2);
  color: var(--text2);
  cursor: pointer;
  padding: 0;
}

.composer__btn:hover { background: var(--surface3); color: var(--text); }
.composer__btn svg { width: 20px; height: 20px; display: block; }

.composer__btn--send {
  background: var(--gradient-brand);
  background-origin: border-box;
  background-repeat: no-repeat;
  border-color: transparent;
  color: #fff;
}

.composer__btn--send:hover { filter: brightness(1.05); background: var(--gradient-brand); color: #fff; }
.composer__btn--danger { color: var(--error); }

/* ЗАПИСЬ ГОЛОСОВОГО: ИДЁТ ТАЙМЕР, ЕСТЬ ОТМЕНА И ЕСТЬ ОТПРАВКА.
 *
 * Точка, по которой надо нажать второй раз, — не кнопка записи, а загадка:
 * непонятно, идёт запись или нет, и передумать нельзя вовсе. Здесь видно
 * время, слева — «выбросить», справа — «отправить». */
.recorder {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1;
  padding: 0 12px;
  height: 40px;
  border-radius: var(--radius-lg);
  background: var(--error-bg);
  color: var(--error-ink);
  font-variant-numeric: tabular-nums;
}

.recorder__dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--error);
  animation: bnt-pulse 1.1s ease-in-out infinite;
  flex: none;
}

.recorder__hint { color: var(--text3); font-size: 13px; }

/* Список диалогов слева от переписки — на широком экране */
.dialog-item__photo {
  width: 44px;
  height: 44px;
  flex: none;
  border-radius: 10px;
  object-fit: cover;
  background: var(--surface3);
  display: grid;
  place-items: center;
  font-weight: 600;
  color: var(--text2);
}

.dialog-item__photo--support {
  background: url("/static/img/icon.svg") center / cover no-repeat;
}

/* --- Страница бизнеса (W27) ------------------------------------------ */

/* Шапка бизнеса на закате: это его лицо, и первое, что видит человек, —
   с кем он вообще имеет дело */
.biz-head {
  display: flex;
  align-items: center;
  gap: 18px;
  flex-wrap: wrap;
  padding: 24px;
  border-radius: var(--radius-lg);
  background: var(--gradient-brand);
  background-origin: border-box;
  background-repeat: no-repeat;
  color: #fff;
  margin-bottom: 22px;
}

.biz-head h1 { color: #fff; margin: 0; }
.biz-head .muted { color: rgba(255, 255, 255, .85); }

.biz-head__logo {
  width: 72px;
  height: 72px;
  flex: none;
  border-radius: var(--radius);
  object-fit: cover;
  background: rgba(255, 255, 255, .22);
  border: 2px solid rgba(255, 255, 255, .55);
  display: grid;
  place-items: center;
  font-size: 26px;
  font-weight: 700;
  color: #fff;
}

/* --- Билет заселения -------------------------------------------------- */

/* QR показывают на стойке — иногда через месяц после заезда, когда бронь
   нужно просто найти. Поэтому он крупный, всегда рядом с номером брони и
   не прячется после заселения */
.ticket {
  display: grid;
  gap: 14px;
  justify-items: center;
  padding: 22px;
  border: 1px solid transparent;
  border-radius: var(--radius-lg);
  background:
    linear-gradient(var(--surface2), var(--surface2)) padding-box,
    var(--gradient-border) border-box;
  text-align: center;
}

.ticket__code {
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 22px;
  font-weight: 700;
  letter-spacing: .12em;
}

/* Билет заселения: тот же расчёт, что и у кода СБП в симуляторе. Билет длиннее
   ссылки, модулей в нём больше, и на 220 px каждый занимал меньше пяти
   пикселей — на стойке отеля, где код показывают с руки, этого мало.

   280 px — ПОТОЛОК, А НЕ ЖЁСТКИЙ РАЗМЕР. Жёсткий вылезал за края блока на
   телефоне: до кода остаются ширина экрана минус поля страницы, колонки и
   самого билета — на 360 px это 248 px, и квадрат в 280 торчал наружу.
   `aspect-ratio` держит его квадратным без второй жёсткой величины. */
.ticket canvas,
.ticket svg {
  width: 100%;
  max-width: 280px;
  aspect-ratio: 1 / 1;
  height: auto;
  background: #fff;
  padding: 10px;
  border-radius: var(--radius-sm);
}

/* --- Лента событий заказа --------------------------------------------- */

.timeline { display: grid; gap: 0; margin: 0; padding: 0; list-style: none; }

.timeline__item {
  display: grid;
  grid-template-columns: 18px minmax(0, 1fr);
  gap: 12px;
  padding-bottom: 16px;
  position: relative;
}

.timeline__item::before {
  content: "";
  position: absolute;
  left: 8px;
  top: 16px;
  bottom: 0;
  width: 1px;
  background: var(--border);
}

.timeline__item:last-child { padding-bottom: 0; }
.timeline__item:last-child::before { display: none; }

.timeline__dot {
  width: 10px;
  height: 10px;
  margin-top: 6px;
  border-radius: 50%;
  background: var(--border2);
  justify-self: center;
  z-index: 1;
}

.timeline__dot--done { background: var(--success); }
.timeline__dot--now { background: var(--brand); box-shadow: 0 0 0 3px var(--brand-bg); }

/* --- Капча ------------------------------------------------------------ */

/* КАРТИНКА КАПЧИ НЕ РАСТЯГИВАЕТСЯ.
 *
 * Она приходит 340×120 и нарисована попиксельно. Растянутая на всю ширину
 * поля, она размывается ровно там, где важна чёткость, — на цифрах. Здесь
 * она в своём размере и по центру, а под ней слева — «обновить код»: код,
 * который не читается, обязано быть чем заменить, не перезагружая страницу
 * и не теряя введённые почту и пароль. */
.captcha {
  display: grid;
  gap: 6px;
  justify-items: center;
}

.captcha__image {
  width: 340px;
  max-width: 100%;
  height: auto;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface2);
}

.captcha__row {
  width: 340px;
  max-width: 100%;
  display: flex;
  justify-content: flex-start;
}

.captcha__refresh {
  background: none;
  border: 0;
  padding: 2px 0;
  color: var(--brand-ink);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}

.captcha__refresh:hover { text-decoration: underline; }

/* --- Мелочи ----------------------------------------------------------- */

/* Пустое место с объяснением, а не голое «ничего нет» */
.empty {
  display: grid;
  gap: 10px;
  justify-items: center;
  padding: 40px 20px;
  text-align: center;
  color: var(--text2);
}

.empty__mark {
  width: 56px;
  height: 56px;
  border-radius: var(--radius);
  background: var(--gradient-soft);
  display: grid;
  place-items: center;
  font-size: 24px;
}

@media (max-width: 640px) {
  .chat { height: calc(100vh - var(--header-height) - 60px); border-radius: var(--radius); }
  .bubble { max-width: 88%; }
  .biz-head { padding: 18px; }
}


/* Простой список: «что взять с собой» и подобные перечисления. Маркеры
   свои, потому что браузерные диски сидят вплотную к тексту */
.list {
  margin: 0;
  padding-left: 18px;
  list-style: none;
}

.list li {
  position: relative;
  padding: 2px 0;
}

.list li::before {
  content: "";
  position: absolute;
  left: -14px;
  top: 11px;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--text3);
}

.text-right { text-align: right; }

/* --- Выбор выхода (BntSlotPicker) -------------------------------------
 *
 * Список выходов вместо календаря дат у категорий, которые продаются
 * слотами (Р-133). В одном дне бывает три выхода, и на двух из них ещё
 * есть места — клетка дня такого не покажет никак.
 *
 * Своих цветов компонент не заводит: те же токены, что у кнопки и
 * календаря. Полных выходов в списке нет вовсе, поэтому состояния
 * «выключено» здесь тоже нет.
 */
.slots {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 8px;
  max-height: 320px;
  overflow-y: auto;
}

.slot {
  display: grid;
  gap: 2px;
  padding: 8px;
  border: 1px solid var(--border2);
  border-radius: var(--radius-sm);
  background: var(--surface2);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.slot:hover { background: var(--surface3); }

/* Скрытый выход обязан исчезать. `hidden` ставит `display: none` в таблице
   браузера, а `display: grid` выше по весу — и выходы соседних дней
   оставались на экране целиком, ради чего календарь и заводился */
.slot[hidden] { display: none; }

/* Правила дня под календарём. То же, что в подсказке, но читать можно не
   наводя мышь — на телефоне подсказки нет вовсе, а объяснение цены нужно
   там ровно так же. Столбиком, потому что это цепочка: базовая цена, что с
   ней сделали, что получилось */
.cal__rules[hidden] { display: none; }

.cal__rules {
  display: grid;
  gap: 2px;
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
  font-size: 12px;
  color: var(--text2);
}

/* Первая строка — базовая цена, последняя — итог: их и читают, остальное
   между ними объясняет разницу */
.cal__rules span:last-child { color: var(--text); font-weight: 650; }

.slot__day { font-size: 11px; color: var(--text3); }
.slot__time { font-size: 16px; font-weight: 650; }
.slot__left { font-size: 11px; color: var(--text2); }

/* Выбранный — как выбранный чип: рамка бренда и его подложка. Обводка
   толще на пиксель, потому что цвет один и тот же на светлой и тёмной
   теме, а вес рамки читается в обеих */
.slot--picked {
  border-color: var(--brand);
  background: var(--brand-bg);
  color: var(--brand-ink);
}

.slot--picked .slot__day,
.slot--picked .slot__left { color: inherit; opacity: .75; }

/* В режиме дня список показывает выходы выбранного дня, и день без выходов
   бывает: скрытый список схлопывается, а не оставляет пустую рамку */
.slots--empty { display: none; }

/* День в календаре выходов подписан временем, а не ценой: цена у выходов
   одного дня разная, и одна цифра в клетке была бы неправдой для половины */
#slot-calendar .cal__day--from .cal__price { color: inherit; }

/* --- Крупный код брони (Р-157) ---------------------------------------- */

/* Код на карточке кликабелен: на стойке телефон протягивают через прилавок,
   и мелкий код там бесполезен — а страница брони при этом обязана оставаться
   страницей с подробностями, а не полем под QR во весь экран. */
.ticket__open {
  border: 0;
  padding: 0;
  background: none;
  cursor: zoom-in;
  border-radius: var(--radius-sm);
  display: block;
  /* Кнопка — клетка сетки билета: без ширины она берёт размер по содержимому
     и вместе с ним вылезает за края узкого экрана */
  width: 100%;
  max-width: 280px;
}

.ticket__open:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 3px;
}

/* Родное `<dialog>`: само перехватывает клавиатуру, закрывается по Escape и
   не требует ни библиотеки, ни ручного удержания фокуса (правило №15). */
.qr-modal {
  border: 0;
  padding: 0;
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-lg);
  max-width: min(92vw, 420px);
}

.qr-modal::backdrop {
  background: rgba(0, 0, 0, .55);
  backdrop-filter: blur(3px);
}

.qr-modal__body {
  display: grid;
  gap: 14px;
  justify-items: center;
  padding: 22px;
  text-align: center;
}

.qr-modal__code {
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 20px;
  font-weight: 700;
  letter-spacing: .12em;
}

/* Крупно, но не во весь экран: код должен читаться с руки, а кнопки —
   оставаться на виду без прокрутки. Ширина ограничена и высотой окна тоже:
   на телефоне, лежащем горизонтально, квадрат в 92vw уезжает за край. */
.qr-modal__box svg {
  width: min(78vw, 60vh, 320px);
  height: min(78vw, 60vh, 320px);
  background: #fff;
  padding: 10px;
  border-radius: var(--radius-sm);
  display: block;
}

.qr-modal__actions {
  display: flex;
  gap: 10px;
  width: 100%;
}

.qr-modal__actions .btn {
  flex: 1;
}

/* ======================================================================
   ОТДЕЛЬНЫЕ ЧЕКИ ЗАКАЗА (S36, Р-163 и Р-164)
   ====================================================================== */

/* Цвет полосы слева говорит, ЧЕРЕЗ КОГО прошли деньги, а не оплачен ли чек:
   состояние написано словами строкой ниже. Фирменный — через сервис,
   сиреневый — отдано партнёру в руки. Это разные вещи, и человек должен
   видеть, к кому идти с вопросом */
.charge {
  border-left: 3px solid var(--brand);
  padding: 10px 0 10px 12px;
  margin-bottom: 10px;
}

.charge--offline { border-left-color: var(--accent); }

/* Снятый чек не прячется: исчезнувшая строка читается как ошибка
   приложения, а не как отменённая доплата (правило №6) */
.charge--cancelled { opacity: 0.6; }
.charge--cancelled strong:first-child { text-decoration: line-through; }

/* ЧЕКИ ЗАКАЗА.
 *
 * Показываются прямо на странице, под хронологией: сначала что происходило,
 * потом за что заплачено — порядок тот же, в каком человек об этом думает.
 * Окно, в котором они жили раньше, отвечало на тот же вопрос, но требовало
 * нажатия и прятало от беглого взгляда самое частое: сколько всего вышло. */
.receipt {
  border: 1px solid var(--border);
  border-left: 3px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  margin-top: 12px;
}

/* ЧЕТЫРЕ ВИДА ЧЕКА, ДВЕ ОСИ.
 *
 * ТОЛЩИНА полосы говорит, главный это чек или добавленный. ЦВЕТ говорит,
 * через кого прошли деньги (Р-164): через сервис или напрямую хозяину.
 * Сложить обе разницы в один цвет нельзя — это ответы на разные вопросы, и
 * «зелёный» тогда означал бы то одно, то другое.
 *
 * Зелёный у главного через сервис означает не «хорошо», а «это и есть
 * заказ»: остальные чеки к нему добавлены. */
.receipt--main { border-left-width: 5px; }
.receipt--service { border-left-color: var(--brand); }
.receipt--offline { border-left-color: var(--accent); background: var(--accent-bg); }
.receipt--main.receipt--service {
  border-left-color: var(--success);
  background: var(--success-bg);
}
.receipt--cancelled { opacity: 0.6; }
.receipt--cancelled .receipt__head { text-decoration: line-through; }

.receipt__head {
  display: flex;
  justify-content: space-between;
  gap: 12px;
}

.receipt__what { color: var(--text2); font-size: 13px; margin-top: 2px; }

.receipt__lines {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  font-size: 13px;
}

.receipt__lines li {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 2px 0;
}

/* Подробности чека — парами «что : когда». Списком определений, а не
   таблицей: это и есть определения, и на узком экране они переносятся сами */
.receipt__facts {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 16px;
  margin: 10px 0 0;
  font-size: 12px;
  color: var(--text2);
}

.receipt__facts div { display: flex; gap: 6px; }
.receipt__facts dt::after { content: ":"; }
.receipt__facts dd { margin: 0; color: var(--text); }

/* ── Окно с последствиями (Р-180) ─────────────────────────────────────────
   Родное `<dialog>`: перехватывает клавиатуру, закрывается по Escape и не
   требует библиотеки (правило №15). Отличается от `qr-modal` тем, что здесь
   не показывают — здесь читают: текст выровнен по левому краю, список
   последствий отделён, галочки крупные под палец. */
.modal {
  border: 0;
  padding: 0;
  border-radius: var(--radius-lg);
  background: var(--surface2);
  color: var(--text);
  box-shadow: var(--shadow-lg);
  max-width: min(92vw, 480px);
}

.modal::backdrop {
  background: var(--overlay);
  backdrop-filter: blur(3px);
}

.modal__body {
  display: grid;
  gap: 14px;
  padding: 22px;
}

.modal__title { margin: 0; font-size: 18px; }
.modal__lead { margin: 0; color: var(--text2); }

.modal__list {
  margin: 0;
  padding-left: 20px;
  display: grid;
  gap: 8px;
  color: var(--text);
}

.modal__actions {
  display: flex;
  gap: 10px;
  margin-top: 4px;
}

.modal__actions .btn { flex: 1; }

/* Галочка целиком кликабельна: попасть в квадрат 13×13 пальцем на телефоне
   нельзя, а это подтверждение необратимого решения. */
.check {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
}

.check input { margin-top: 2px; flex: none; }
.check:hover { border-color: var(--border2); }

/* ── Лента заказа (Р-181, Р-184) ─────────────────────────────────────────

/* Одна лента, а не «журнал плюс чат» рядом: вопрос один — что было по этому
 * заказу и в каком порядке, — а у события и реплики одна ось, время.
 * Гость видит здесь ровно то же, что партнёр, и выглядеть это обязано
 * одинаково: блок повторён в `partnerpanel/app/static/css/own.css` слово в
 * слово. Разошлись — и один и тот же заказ в двух окнах выглядит двумя
 * разными сервисами.
 *
 * УСТРОЙСТВО: слева столбик времени, по нему проходит ось с точками, справа
 * записи. События — текстом на точке, реплики — баблами; своя прижата вправо
 * и залита фирменным. На длинной ленте это различает стороны быстрее, чем
 * подпись над каждой репликой.
 *
 * Ось времени ОДНА и всегда слева, в том числе у своих реплик. В мессенджере
 * время уезжает к своему баблу вправо, но здесь это не переписка, а история
 * заказа: столбик дат вдоль левого края читается одним взглядом (Р-184), а
 * разорванный пополам — не читается вовсе.
 */
.feed {
  /* Ширина столбца времени: «03.09.2026 21:55 Asia/Bangkok» переносится в
     две строки, и 118 px — та ширина, при которой перенос приходится на
     пробел перед поясом, а не в середину даты */
  --feed-time: 118px;
  --feed-gap: 10px;
  --feed-axis: calc(var(--feed-time) + var(--feed-gap) + 4px);

  position: relative;
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 16px 0 0;
  padding: 0;
  list-style: none;
}

/* Ось: одна линия через всю ленту, ЗА точками. Без неё записи выглядят
   несвязанным списком, а лента — это порядок событий */
.feed::before {
  content: "";
  position: absolute;
  top: 8px;
  bottom: 8px;
  left: var(--feed-axis);
  width: 2px;
  border-radius: 2px;
  background: var(--border);
}

.feed__event,
.feed__message {
  display: grid;
  grid-template-columns: var(--feed-time) 10px minmax(0, 1fr);
  gap: 0 var(--feed-gap);
}

/* Время по ПРАВОМУ краю своего столбца: дата и час выстраиваются друг под
   другом ровно у оси, а не пляшут по длине названия пояса */
.feed__at {
  grid-column: 1;
  color: var(--text3);
  font-size: 11.5px;
  line-height: 1.3;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* Точка на оси. Кольцо цветом карточки — чтобы линия не просвечивала
   сквозь точку и та читалась как узел, а не как утолщение */
.feed__dot,
.feed__message::before {
  grid-column: 2;
  width: 10px;
  height: 10px;
  margin-top: 5px;
  border-radius: 50%;
  background: var(--border2);
  box-shadow: 0 0 0 3px var(--surface2);
}

.feed__message::before { content: ""; }
.feed__message--mine::before { background: var(--brand); }

.feed__body { grid-column: 3; min-width: 0; }

/* Событие: время и запись стоят на ОДНОЙ базовой линии. Выравнивание по
   верхнему краю давало разницу в три пикселя — ту самую, из-за которой
   запись выглядела чуть ниже своего времени */
.feed__event { align-items: baseline; }
.feed__event .feed__dot { align-self: start; }

.feed__event .feed__body {
  color: var(--text2);
  font-size: 13.5px;
}

.feed__message { align-items: start; }

/* У реплики время равняется на подпись внутри бабла, а не на его верхнюю
   рамку: рамка — это оформление, а читают строку */
.feed__message .feed__at { padding-top: 10px; }

.feed__message .feed__body {
  justify-self: start;
  max-width: min(88%, 560px);
  padding: 9px 13px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
}

/* СВОЯ РЕПЛИКА ОСТАЁТСЯ СЛЕВА (Р-206). В обычном чате свои сообщения прижаты
   вправо, и там это верно: разговор двоих. Здесь же лента — история заказа, в
   которой у событий своё время в левой колонке; реплика, уехавшая вправо,
   рвала эту колонку пополам и заставляла читать зигзагом. Отличается своё
   цветом и подписью «Вы» — этого достаточно, чтобы не перепутать. */
.feed__message--mine .feed__body {
  justify-self: start;
  border-color: transparent;
  background: var(--brand-bg);
}

/* Список приложенных документов внутри записи ленты */
.feed__files {
  margin: 6px 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.feed__files li {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
}

/* Имя файла не рвётся по буквам: «contract.pdf», сложенное в три строки по
   две буквы, — это ровно то, из-за чего вложение перестают узнавать */
.feed__files a {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

/* ---- Прикреплённые файлы под полем ввода (Р-205) ---- */

.feed__form { flex-wrap: wrap; }

.attach__list {
  order: 9;
  flex: 1 0 100%;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}

.attach__item {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  max-width: 100%;
  padding: 5px 8px 5px 11px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  font-size: 12.5px;
}

.attach__name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 220px;
}

.attach__size { color: var(--text3); font-size: 11.5px; }

.attach__drop {
  width: 20px;
  height: 20px;
  border: 0;
  border-radius: 50%;
  background: var(--surface3);
  color: var(--text2);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}

.attach__drop:hover { background: var(--error-bg); color: var(--error-ink); }

.attach__note { order: 10; flex: 1 0 100%; }

.feed__who {
  margin-bottom: 2px;
  color: var(--text3);
  font-size: 11px;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* `pre-wrap` обязателен: человек разбивает сообщение на строки, и склеенный
   в абзац адрес с кодом от сейфа читается как одна длинная строка */
.feed__text {
  font-size: 13.5px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.feed__when { color: var(--text3); font-size: 11px; }

.feed__form { display: flex; gap: 10px; margin-top: 16px; }
.feed__form .input { flex: 1 1 auto; }

/* На телефоне столбик времени съедал бы треть ширины, а «Asia/Bangkok» в нём
   рассыпался по одной букве в строку. Время встаёт строкой НАД записью, ось и
   точки уходят: на 390 px они уже не помогают читать, а место занимают.
   Своя реплика остаётся справа — цветом и краем стороны различаются и здесь */
@media (max-width: 575.98px) {
  .feed::before { display: none; }

  .feed__event,
  .feed__message {
    grid-template-columns: minmax(0, 1fr);
    gap: 3px 0;
  }

  .feed__at,
  .feed__body { grid-column: 1; }

  .feed__at { text-align: left; }
  .feed__message .feed__at { padding-top: 0; }
  .feed__dot,
  .feed__message::before { display: none; }

  .feed__message .feed__body { max-width: 92%; }
  .feed__form { flex-direction: column; align-items: stretch; }
}


/* ── Чек лентой (Р-183) ──────────────────────────────────────────────────
   Не «красивая версия для печати», а ровно то, что уйдёт на чековый
   принтер: 32 знака в строке, моноширинно. Поэтому — `pre`, а не таблица:
   выравнивание держат пробелы, и любая вёрстка поверх них его сломает. */
.tape {
  margin: 10px 0 0;
  padding: 12px 14px;
  border: 1px dashed var(--border2);
  border-radius: var(--radius-sm);
  background: var(--surface2);
  color: var(--text);
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 12px;
  line-height: 1.45;
  white-space: pre;
  overflow-x: auto;
  -webkit-user-select: all;
  user-select: all;
}

.receipt__foot {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 8px;
}

/* ── Лесенка этапов со стрелками (Р-184) ────────────────────────────────
   Поперёк, а не столбиком: тот же ответ «где мы сейчас» одной строкой
   вместо половины экрана. Стрелка говорит, что шаги идут по порядку и
   назад не возвращаются — без неё это просто набор плашек. */
.steps--flow {
  align-items: stretch;
  gap: 0;
}

.steps--flow .steps__item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1 1 120px;
  min-width: 110px;
  border-radius: 0;
  border-left: 0;
}

.steps--flow .steps__item:first-child {
  border-left: 1px solid var(--border);
  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
}

.steps--flow .steps__item:last-child {
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}

/* Стрелка — псевдоэлементом, а не символом в разметке: она украшение
   порядка, и читалке экрана её произносить незачем */
.steps--flow .steps__item + .steps__item::before {
  content: "";
  position: absolute;
  left: -6px;
  top: 50%;
  width: 10px;
  height: 10px;
  transform: translateY(-50%) rotate(45deg);
  border-top: 1px solid var(--border);
  border-right: 1px solid var(--border);
  background: var(--surface);
}

.steps--flow .steps__item { position: relative; }
.steps__title { font-weight: 600; }
.steps__when { color: var(--text3); font-size: 11px; }

@media (max-width: 640px) {
  /* На телефоне лесенка встаёт столбиком: пять плашек по 110 пикселей в
     ряд превращаются в нечитаемую полосу */
  .steps--flow { flex-direction: column; }
  .steps--flow .steps__item,
  .steps--flow .steps__item:first-child,
  .steps--flow .steps__item:last-child {
    border-radius: var(--radius-sm);
    border-left: 1px solid var(--border);
  }
  .steps--flow .steps__item + .steps__item::before { display: none; }
}


/* --- Списки заказов и шапка заказа ------------------------------------ */

/* СПИСОК ЗАКАЗОВ — ДВЕ КАРТОЧКИ В РЯД НА ШИРОКОМ ЭКРАНЕ.
   Одна карточка во всю ширину колонки — это пять слов и полметра пустоты
   справа: строка «Вилла · даты · номер» короткая по природе, и растягивать
   её незачем. Порог — 1200 px: на 992 колонка списка ужимается меню разделов
   до 640, и две карточки по 310 уже не вмещают фотографию с ценой. */
.order-list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 14px;
}

@media (min-width: 1200px) {
  .order-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* Карточка в две колонки уже, и правая группа (состояние и сумма) при
   нехватке места уходит на свою строку, а не жмёт название до многоточия */
.order-list > .card {
  flex-wrap: wrap;
  row-gap: 10px;
  align-items: flex-start;
}

/* ШАПКА ЗАКАЗА: объект, билет заселения и главные действия одним блоком.
   Билет и «Отменить» стояли в правой колонке, и на широком экране человек
   искал их глазами через полстраницы от того, к чему они относятся. */
/* ── Шапка заказа (Р-198) ─────────────────────────────────────────────────
 *
 * ЧТО БЫЛО НЕ ТАК. Три блока стояли в ряд тремя колонками `1fr auto auto`:
 * объект, билет, кнопка отмены. Колонки получались разной ширины и разной
 * высоты, кнопка висела вровень с верхом фотографии, подпись под ней уезжала
 * на три строки, а на 1200 px билет сжимался и переставал читаться. Блок
 * читался как три случайно поставленных рядом предмета.
 *
 * КАК СТАЛО. Колонок две. Слева сверху — что снято, слева снизу под тонкой
 * чертой — действия по заказу: они относятся к нему, и стоять должны под ним.
 * Справа во всю высоту — билет в своей рамке: это самостоятельный предмет,
 * его показывают на стойке, и в общем потоке он терялся.
 */
.order-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 236px;
  grid-template-rows: auto 1fr;
  align-items: start;
  gap: 16px 24px;
}

.order-head__what {
  grid-column: 1;
  grid-row: 1;
  display: flex;
  align-items: center;
  gap: 16px;
  min-width: 0;
  color: inherit;
}

.order-head__what:hover { text-decoration: none; }
.order-head__what:hover .order-head__title { text-decoration: underline; }

.order-head__photo {
  width: 116px;
  height: 88px;
  flex: none;
  object-fit: cover;
  border-radius: var(--radius-sm);
}

/* Три строки одна под другой и с разным весом: название — предмет, место и
   категория — уточнение, ссылка — приглашение уйти. Одинаковым кеглем это
   читалось бы как один абзац */
.order-head__text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.order-head__title { font-size: 17px; line-height: 1.25; }
.order-head__line { display: block; }
.order-head__link { color: var(--brand-ink); }

/* Билет во всю высоту шапки и по центру своей колонки: он показывается
   человеку за стойкой, и мельчить его нельзя. Крупный код живёт в окне по
   нажатию (Р-157) — здесь он один из двух предметов, а не всё содержимое */
.order-head__ticket {
  grid-column: 2;
  grid-row: 1 / span 2;
  align-self: stretch;
  /* Рамка и подложка остаются собственные, `.ticket`: фирменный градиент по
     краю — то, чем билет опознаётся во всех клиентах. Здесь задаётся только
     место в сетке и то, что содержимое стоит по центру своей высоты */
  align-content: center;
  gap: 10px;
  padding: 16px 14px;
}

.order-head .ticket svg { max-width: 168px; }
.order-head .ticket__code { font-size: 18px; }

/* Действия — под объектом и отделены чертой: это уже не описание заказа, а
   решение по нему, и слипшись с текстом они читаются как его продолжение */
.order-head__actions {
  grid-column: 1;
  grid-row: 2;
  align-self: end;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 14px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

.order-head__note { flex: 1 1 220px; min-width: 0; }

@media (max-width: 767.98px) {
  /* На телефоне блоки встают друг под друга: билет по центру остаётся тем,
     что показывают на стойке, а кнопка отмены — во всю ширину */
  .order-head { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto; }
  .order-head__what,
  .order-head__ticket,
  .order-head__actions { grid-column: 1; grid-row: auto; }
  .order-head__photo { width: 88px; height: 66px; }
  .order-head__actions .btn { width: 100%; }
}

/* Правой колонки нет — страница идёт одной. Пустая треть экрана читается как
   невыполненное обещание: человек ищет там что-то и не находит */
.listing-layout--single { grid-template-columns: minmax(0, 1fr); }

/* --- Пальцевые цели на телефоне -------------------------------------- */

/* В КОНЦЕ ФАЙЛА НАМЕРЕННО: `.cal__nav`, `.captcha__refresh` и `.bubble__menu`
   объявлены выше, и стоя раньше этот блок ими же и перебивался.

   Тридцать пикселей — меньше подушечки пальца. Промах по «‹» в календаре или
   по «обновить код» в капче стоит человеку лишнего шага там, где шагов и так
   хватает. */
@media (max-width: 575.98px) {
  .btn--sm { min-height: 32px; }
  .cal__nav { width: 32px; height: 32px; }
  .captcha__refresh { min-height: 32px; }
  .bubble__menu { width: 32px; height: 32px; }
}



/* ==================================================================== */
/* Чеки сеткой, условия отмены, проживающие                             */
/* ==================================================================== */

/* ЧЕК — УЗКИЙ ДОКУМЕНТ (Р-199). Тридцать два знака в строке, и в столбик на
 * широком экране он оставлял справа пустое место в три четверти ширины.
 * Колонок столько, сколько помещается: на мониторе четыре, на планшете две,
 * на телефоне одна. `auto-fill`, а не жёсткие четыре: в правой колонке
 * страницы место другое, и число там обязано быть другим само собой.
 */
.receipts-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(238px, 1fr));
  gap: 12px;
  align-items: start;
  margin-top: 12px;
}

/* Отступ сверху у чека внутри сетки лишний: его задаёт сама сетка */
.receipts-grid > .receipt { margin-top: 0; }

/* Лента чека внутри узкой карточки: тридцать два знака должны поместиться,
   а если ширины всё же не хватило — прокручиваться внутри себя, а не рвать
   страницу вбок */
.receipts-grid .tape {
  font-size: 11px;
  line-height: 1.35;
  overflow-x: auto;
}

/* ── Условия отмены (Р-193) ───────────────────────────────────────────────
 *
 * Пороги списком: слева срок, справа процент. Именно так их и читают —
 * «за тридцать дней вернут всё, ближе шестнадцати ничего», — и превращать
 * это в абзац текста значит заставить человека вычитывать числа из фразы.
 */
.policy-view {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  background: var(--surface);
}

.policy-view__name { font-weight: 650; margin-bottom: 6px; }

/* ПОМЕТКА СЕЗОНА У УСЛОВИЙ ОТМЕНЫ.

   Своего `.badge` у нас не было вовсе, и класс подхватывал бутстраповский:
   белый текст без подложки. На светлой карточке условий пометка «с 20 декабря
   по 10 января» была не видна ни в брони, ни на оплате, ни на карточке
   объекта — то есть сезонные условия ничем не отличались от основных.
   Цвет — акцент (docs/themes.md §3), тот же, которым помечены сезоны в
   кабинете партнёра. */
.badge {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  line-height: 1.6;
  white-space: nowrap;
  background: var(--surface3);
  color: var(--text2);
  vertical-align: middle;
}

.badge--accent { background: var(--accent-bg); color: var(--accent-ink); }

.policy-view__rules { list-style: none; margin: 0; padding: 0; font-size: 13px; }

.policy-view__rules li {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 3px 0;
}

.policy-view__rules li + li { border-top: 1px dashed var(--border); }
.policy-view__terms { margin: 8px 0 0; white-space: pre-line; }

/* ── Проживающие (Р-197) ──────────────────────────────────────────────────
 *
 * Строка на человека: номер, имя, возраст. Номер слева — по нему видно,
 * сколько всего строк и какую заполняешь; без него четыре одинаковых пары
 * полей читаются как повтор одного и того же.
 */
.stay-guest {
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr) 96px;
  align-items: end;
  gap: 10px;
  margin-bottom: 10px;
}

.stay-guest__no {
  height: 38px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text3);
  font-size: 13px;
}

.stay-guest .field { margin: 0; }

@media (max-width: 575.98px) {
  /* Возраст остаётся рядом с именем: разложенные в столбик, они перестают
     читаться как один человек */
  .stay-guest { grid-template-columns: 20px minmax(0, 1fr) 76px; gap: 8px; }
}


/* ==================================================================== */
/* Полоса прокрутки внутри блоков                                        */
/* ==================================================================== */

/* СВОЯ ПОЛОСА ВНУТРИ ПЕРЕПИСКИ И ТАБЛИЦ, А НЕ У СТРАНИЦЫ. Платформенная
   полоса в 17 px рисуется поверх содержимого серым прямоугольником из чужой
   темы — в тёмной особенно заметно. У окна браузера полоса остаётся
   системной: её человек знает, и подменять её ради красоты значит менять
   привычное поведение всей страницы.

   Firefox понимает `scrollbar-width`/`scrollbar-color`, остальные —
   псевдоэлементы. Оба объявления нужны: это не дубль. */
.chat__scroll,
.table-wrap,
.receipts-grid {
  scrollbar-width: thin;
  scrollbar-color: var(--border2) transparent;
}

.chat__scroll::-webkit-scrollbar,
.table-wrap::-webkit-scrollbar,
.receipts-grid::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

.chat__scroll::-webkit-scrollbar-track,
.table-wrap::-webkit-scrollbar-track,
.receipts-grid::-webkit-scrollbar-track {
  background: transparent;
}

.chat__scroll::-webkit-scrollbar-thumb,
.table-wrap::-webkit-scrollbar-thumb,
.receipts-grid::-webkit-scrollbar-thumb {
  border: 2px solid transparent;
  border-radius: 999px;
  background: var(--border2);
  background-clip: content-box;
}

.chat__scroll:hover::-webkit-scrollbar-thumb,
.table-wrap:hover::-webkit-scrollbar-thumb {
  background: var(--text3);
  background-clip: content-box;
}


/* ПУСТЫЕ БЛОКИ ВЛОЖЕНИЙ НЕ ЗАНИМАЮТ МЕСТА (Р-205).
   
   `display: flex` у списка сильнее браузерного `[hidden] { display: none }`:
   пустой список всё равно рисовался, а вместе со строкой-заметкой давал две
   пустые полосы во всю ширину внутри строки ввода. Со стороны это выглядело
   поломкой: скрепка сверху, поле посередине, кнопка отправки снизу. */
.attach__list[hidden] { display: none !important; }
.attach__note:empty { display: none; }




/* ==================================================================== */
/* Блок поиска раздела                                                   */
/* ==================================================================== */

/* ОБЫЧНАЯ КАРТОЧКА, А НЕ «ТАБЛЕТКА» С КАРТЫ. Панель карты плавает поверх
   холста, и скругление 999 px с полупрозрачным фоном там уместно. На
   странице та же панель выходила высокой полосой с огромными торцами, а поля
   внутри — разной высоты. Здесь скругление страницы, ровная сетка и одна
   высота у всех полей: их читают одним взглядом сверху вниз. */
.search-bar {
  display: grid;
  gap: 12px;
  padding: 16px;
  margin-bottom: 18px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface2);
}

/* Верхняя строка — то, чем пользуются каждый раз */
.search-bar__top {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
}

.search-bar__query { flex: 1 1 260px; min-width: 0; }

/* ВСЁ В БЛОКЕ ОДНОЙ ВЫСОТЫ. Поле поиска, кнопки, списки и счётчик гостей
   приходят из разных компонентов со своими отступами, и без общей высоты
   блок выглядел набранным из разных деталей */
.search-bar__top .input,
.search-bar__top .btn { height: 40px; }

/* Остальные условия — ровной сеткой. `auto-fit` вместо жёсткого числа
   колонок: у категорий разный набор полей (срока нет у слотовых), и число
   колонок обязано меняться само */
.search-bar__fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 10px 12px;
  align-items: end;
}

.search-bar__field {
  display: grid;
  gap: 4px;
  min-width: 0;
}

.search-bar__field > span {
  color: var(--text2);
  font-size: 12px;
}

.search-bar__field .input,
.search-bar__field .select,
.search-bar__field .stepper {
  height: 40px;
  width: 100%;
}

/* Внутри счётчика рамка своя, а число стоит между кнопками — по краям его
   не прочесть */
.search-bar__field .stepper .input { border: 0; height: 38px; text-align: center; }

@media (max-width: 575.98px) {
  .search-bar { padding: 12px; }
  .search-bar__top .btn { flex: 1 1 auto; }
  /* НА ТЕЛЕФОНЕ РОВНО ДВЕ КОЛОНКИ. `auto-fit` от 170 px на экране 360 px даёт
     одну, и шесть полей выстраивались в столбик выше самой ленты — ради
     фильтра приходилось прокручивать страницу до объявлений */
  .search-bar__fields { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}


/* ==================================================================== */
/* Примерная цена в валюте человека (Р-214)                              */
/* ==================================================================== */

/* ЭТО НЕ ВТОРАЯ ЦЕНА, И ВЫГЛЯДЕТЬ ОНА ДОЛЖНА ИНАЧЕ. Тем же начертанием и
   цветом, что настоящая цена, пересчёт читается как выбор: «а можно заплатить
   вот столько рублей?» — нельзя, платят в валюте объявления. Поэтому мельче,
   тише и пунктиром: пунктир говорит «здесь есть объяснение», и объяснение
   действительно открывается нажатием. */
.approx {
  display: inline;
  /* Отступ слева — здесь, а не пробелом в разметке. Кнопку рисуют два места:
     шаблон (`_tile.html`) и скрипт ленты поиска (`search.js`); в шаблоне между
     ценой и кнопкой стоял перенос строки, который браузер схлопывает в пробел,
     а скрипт склеивал строки встык — и в выдаче поиска выходило
     «от 6 500 THB≈ 198 USD». Отступ в стиле работает одинаково в обоих. */
  margin-left: 0.4em;
  padding: 0;
  border: 0;
  border-bottom: 1px dashed var(--border2);
  background: none;
  color: var(--text2);
  font: inherit;
  font-size: 0.86em;
  font-weight: 400;
  line-height: inherit;
  cursor: help;
  white-space: nowrap;
}

.approx:hover,
.approx:focus-visible { color: var(--text); border-bottom-color: var(--text2); }

/* Рядом с ценой, а не под ней: это уточнение к сумме слева */
.tile__price .approx,
.price__value .approx { margin-left: 6px; }

/* Курс в окне — моноширинным: числа сверяют глазами, и «2,5000» под «2,5100»
   должно стоять разряд в разряд */
.mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
  color: var(--text2);
}


/* Окно условий отмены шире обычного (Р-215): политик у бизнеса бывает
   четыре — основная и три сезонные, — и каждая это заголовок, список порогов
   и абзац оговорок. В 480 px они складываются в ленту на два экрана */
.modal--wide { max-width: min(94vw, 620px); }
.modal--wide .modal__body { max-height: 78vh; overflow-y: auto; }


/* Счёт, который уже не соответствует полям: пока идёт пересчёт, сумму видно,
   но видно и то, что она устарела. Отправить её в этот момент нельзя —
   кнопка погашена (booking.js) */
.is-stale {
  opacity: .45;
  transition: opacity .12s ease;
}

@media (prefers-reduced-motion: reduce) {
  .is-stale { transition: none; }
}


/* Подсказка «выберите даты», на которую только что нажали мимо: коротко
   подсвечивается, чтобы её заметили, — а не молча остаётся серой строкой */
.is-asking {
  color: var(--brand);
  font-weight: 600;
}

/* Календарь, в котором ждут даты (находка №99): обводка фирменным цветом и
   короткая пульсация. Держится, пока даты не выбраны, — иначе на широком
   экране, где календарь и так виден, нажатие «Забронировать» не меняло ничего */
.cal--asking {
  border-radius: var(--radius);
  box-shadow: 0 0 0 2px var(--brand);
  animation: cal-asking 0.9s ease-out 1;
}

@keyframes cal-asking {
  0% { box-shadow: 0 0 0 0 var(--brand); }
  50% { box-shadow: 0 0 0 6px var(--brand-bg); }
  100% { box-shadow: 0 0 0 2px var(--brand); }
}

@media (prefers-reduced-motion: reduce) {
  .cal--asking { animation: none; }
}
