/* Части, из которых собраны экраны.
 *
 * Каждая — из макета, но описана классом, а не инлайном: макет генерируется
 * заново при любой правке, а это место должно переживать её.
 *
 * У всего интерактивного есть наведение и фокус. Состояние без стиля —
 * недоделанный элемент, а не мелочь.
 */

/* --- кнопки ---------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  border: 1px solid transparent;
  border-radius: var(--r2);
  padding: var(--s3) var(--s6);
  background: var(--fill);
  color: var(--ink-2);
  font: 600 var(--t-13) / 1 var(--sans);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease);
}
.btn:hover {
  background: var(--fill-hover);
}
.btn:disabled {
  opacity: 0.45;
  cursor: default;
}

/* Тёмная — главное действие блока. При наведении уходит в акцент: так видно,
 * что она ведёт дальше, а не просто нажимается. */
.btn.is-dark {
  background: var(--ink);
  color: var(--card);
}
.btn.is-dark:hover {
  background: var(--accent);
}

.btn.is-accent {
  background: var(--accent);
  color: var(--card);
}
.btn.is-accent:hover {
  background: var(--accent-hover);
}

/* Обведённая — для того, что рядом с полем поиска и в верхней панели. */
.btn.is-quiet {
  height: var(--control);
  padding: 0 var(--s5);
  border-color: var(--edge);
  background: transparent;
  color: var(--ink-2);
  font-weight: 500;
  font-size: var(--t-14);
}
.btn.is-quiet:hover {
  background: var(--fill);
}

.btn.is-bare {
  padding: var(--s2) var(--s3);
  background: transparent;
  color: var(--ink-4);
  font-weight: 500;
}
.btn.is-bare:hover {
  background: var(--fill);
  color: var(--ink);
}

/* Крестик и стрелки: одинаковый квадрат под все мелкие управления. */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  flex: none;
  border: 1px solid var(--edge);
  border-radius: var(--r1);
  background: transparent;
  color: var(--ink-3);
  font: 500 var(--t-13) / 1 var(--mono);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease);
}
.icon-btn:hover {
  background: var(--fill);
  color: var(--ink);
}
.icon-btn.is-bare {
  border-color: transparent;
}

/* --- фильтры и чипы -------------------------------------------------------- */

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}

.chip {
  padding: var(--s2) var(--s4);
  border: 1px solid var(--edge);
  border-radius: var(--r1);
  background: transparent;
  color: var(--ink-3);
  font: 500 var(--t-13) / 1 var(--sans);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.chip:hover {
  border-color: var(--accent-border);
}
.chip[aria-pressed="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--card);
  font-weight: 600;
}

/* Капсула — для «идей рядом» и выбора этапа: их много, и они не выбираются
 * навсегда, поэтому форма мягче, чем у фильтра. */
.chip.is-pill {
  border-radius: var(--r-pill);
  padding: var(--s2) var(--s5);
  font-weight: 400;
}
/* Наведение подсказывает, а не прячет. Правило цвета сильнее нажатого
   состояния (`.chip.is-pill:hover` против `.chip[aria-pressed]`), и на нажатой
   капсуле тёмная подпись оказывалась на тёмном фоне: слово пропадало ровно
   в тот момент, когда на него навели. Нажатую не трогаем — у неё свой цвет. */
.chip.is-pill:hover:not([aria-pressed="true"]) {
  border-color: var(--accent-border);
  color: var(--ink-2);
}
.chip.is-pill[aria-pressed="true"]:hover {
  border-color: var(--ink);
}
/* Фокус виден отдельно от наведения: рядом стоит десяток одинаковых капсул,
   и без кольца непонятно, какая из них сейчас под клавиатурой. */
/* Номер города в чипе: тем же знаком, что на карте и в рельсе. */
.chip-num {
  display: inline-block;
  min-width: 15px;
  margin-right: var(--s2);
  border-radius: var(--r1);
  background: var(--fill);
  color: var(--ink-4);
  font: 500 var(--t-12) / 15px var(--mono);
  text-align: center;
}
.chip[aria-pressed="true"] .chip-num {
  background: var(--rail-line-2);
  color: var(--rail-ink-3);
}
.chip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.chip.is-pill[aria-pressed="true"] {
  font-weight: 500;
}

/* Число ПОСЛЕ слова: «прошёл 34», «Фильтры 2». Тот же знак, что и у номера
 * города перед словом, — отличается только тем, с какой стороны отступ. */
.chip-num.is-tail {
  margin-right: 0;
  margin-left: var(--s2);
}

/* --- чип-состояние --------------------------------------------------------
 *
 * Выбранное красится **смыслом, а не чернилами**. Обычный чип в нажатом виде
 * заливается графитом — это верно, пока чипы говорят о разном («Отели»,
 * «Районы»). Но там, где четыре чипа подряд означают итог суждения — прошёл,
 * с оговоркой, не смотрели, нет, — четыре чёрные плашки отвечают только
 * «выбрано», а какой именно итог выбран, приходится вычитывать.
 *
 * Цвета взяты у состояний целиком (`--ok-*`, `--warn-*`, `--alarm-*`): те же,
 * что у меток `.badge` и у знаков вердиктов, — итог обозначен одним цветом
 * во всём проекте. Новых значений не заведено ни одного.
 *
 * Форма — прямоугольник во всю ячейку, а не капсула: чипы стоят сеткой, и
 * попадать в них надо, а не любоваться. Число справа — сколько за состоянием
 * останется. */
.chip.is-state {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: var(--s2) var(--s3);
  border-radius: var(--r2);
  text-align: left;
}

/* Пусто — не запрет: состояние с нулём остаётся нажимаемым (сито могло увести
 * всех соседним условием), но гаснет, чтобы по нему не целились. */
.chip.is-state.is-empty:not([aria-pressed="true"]) {
  border-color: var(--line);
  color: var(--ink-5);
}

.chip.is-state[aria-pressed="true"] {
  font-weight: 500;
}
.chip.is-state.is-ok[aria-pressed="true"] {
  background: var(--ok-bg);
  border-color: var(--ok-edge);
  color: var(--ok-ink);
}
.chip.is-state.is-warn[aria-pressed="true"] {
  background: var(--warn-bg);
  border-color: var(--warn-edge);
  color: var(--warn-ink);
}
.chip.is-state.is-no[aria-pressed="true"] {
  background: var(--alarm-bg);
  border-color: var(--alarm-edge);
  color: var(--alarm-deep);
}
/* «Не смотрели» — не итог, а его отсутствие: серым, тем же, что заливка полей. */
.chip.is-state.is-none[aria-pressed="true"] {
  background: var(--fill);
  border-color: var(--edge-dash);
  color: var(--ink-2);
}
/* Число на крашеном чипе — белой плашкой: тёмная от `.chip[aria-pressed]`
 * рассчитана на графитовую заливку и на зелёном читается как дыра. */
.chip.is-state[aria-pressed="true"] .chip-num {
  background: var(--card);
  color: inherit;
}

/* --- метки ---------------------------------------------------------------- */

/* Капсула на Plex Sans с тонкой рамкой — как во всех артбордах. Прежде это была
 * моноширинная плашка в прямоугольнике 6 px с трекингом: она читалась ярлыком,
 * а состояние — это слово, которое читают, а не код, который сверяют.
 *
 * Рамка не украшение: на бумаге и на карточке фон плашки почти одного веса,
 * и без рамки «в планах» на белой строке терялось.
 *
 * Слово не переносится: «не оплачено» в две строки читалось как две метки,
 * и строка списка разъезжалась по высоте. Не влезло — виновата ширина колонки. */
.badge {
  flex: none;
  white-space: nowrap;
  padding: 5px var(--s3);
  border: 1px solid var(--edge);
  border-radius: var(--r-pill);
  background: var(--fill);
  color: var(--ink-2);
  font: 500 var(--t-12) / 1 var(--sans);
}
.badge.is-ok {
  background: var(--ok-bg);
  color: var(--ok-ink);
  border-color: var(--ok-edge);
}
.badge.is-warn {
  background: var(--warn-bg);
  color: var(--warn-ink);
  border-color: var(--warn-edge);
}
.badge.is-accent {
  background: var(--accent);
  color: var(--card);
  border-color: var(--accent);
}
.badge.is-tint {
  background: var(--accent-soft);
  color: var(--accent);
  border-color: var(--accent-border);
}
/* Отказ: «пропустили» на «Всех местах». Терракота здесь значит то же, что
 * и везде в проекте, — решение не в пользу записи; зелёного «готово» у места
 * нет намеренно, посещённое отмечается синим, как галка и точка на карте. */
.badge.is-alarm {
  background: var(--alarm-bg);
  color: var(--alarm);
  border-color: var(--alarm-edge);
}

/* Счётчик в круге — номер этапа, номер точки на карте, номер места в списке. */
.num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 22px;
  height: 22px;
  border-radius: var(--r-pill);
  background: var(--ink);
  color: var(--card);
  font: 600 var(--t-12) / 1 var(--mono);
}
.num.is-square {
  width: 26px;
  height: 26px;
  border-radius: var(--r2);
  background: var(--fill);
  color: var(--ink-3);
  font-size: var(--t-13);
}
.num.is-now {
  background: var(--accent);
  color: var(--card);
}
.num.is-done {
  background: var(--faint);
}
/* Идея в списке — пустое кольцо на месте номера: той же формы, что её точка
 * на карте, и без числа, потому что в маршрут она ещё не взята. */
.num.is-idea {
  width: 14px;
  height: 14px;
  margin: var(--s1);
  background: var(--card);
  border: 2px solid var(--ink-5);
}

/* Ночи числом и месяцем. Цвет наследуется: в рельсе он светлый, в колонке —
 * серый подписи, и знак обязан совпасть с числом рядом. */
.nights {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  vertical-align: baseline;
}

/* --- поля ------------------------------------------------------------------ */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  min-width: 0;
}
.field > .caps {
  letter-spacing: var(--track-caps);
}
.field input,
.field textarea,
.field select {
  width: 100%;
  padding: var(--s4) var(--s5);
  border: 1px solid var(--edge);
  border-radius: var(--r2);
  background: var(--field);
  font-size: var(--t-15);
  transition: border-color var(--dur-fast) var(--ease);
}
.field input:hover,
.field textarea:hover {
  border-color: var(--faint);
}
.field input:focus,
.field textarea:focus {
  border-color: var(--accent);
  outline: none;
}
.field textarea {
  min-height: 90px;
  resize: vertical;
  line-height: 1.5;
}
.field input::placeholder,
.field textarea::placeholder {
  color: var(--ink-4);
}

/* Поле, у которого разбор не уверен в значении. Не ошибка — просьба взглянуть:
 * разбор ошибается, и вопрос лишь в том, увидит ли человек это до поездки. */
.field.is-unsure input {
  border-color: var(--warn-edge);
  background: var(--warn-bg);
}

/* Подпись под полем: не ошибка и не подсказка ввода, а то, чего по самому полю
 * не видно. Заведена ради выбора системы координат — там цена молчания
 * измеряется сотнями метров, а по числам ошибку не отличить от правильного
 * ответа. Место занимает всегда: подпись, появляющаяся по условию, дёргает
 * форму на каждом переключении. */
.field-note {
  color: var(--ink-3);
  font-size: var(--t-13);
  line-height: 1.4;
}

/* --- галка «был» ---------------------------------------------------------- */

/* Круг 22 px, а не квадрат 16: так она нарисована во всех артбордах, и круглая
 * галка не спорит со скруглением строки, в которой стоит. Отступ сверху снят —
 * в строке-таблице она стоит по центру, а не подпирает первую строку текста. */
.tick {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 22px;
  height: 22px;
  border: 1.5px solid var(--edge-dash);
  border-radius: var(--r-pill);
  background: var(--card);
  color: var(--card);
  font: 600 var(--t-12) / 1 var(--sans);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.tick:hover {
  border-color: var(--accent);
}
.tick[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
}

/* --- тумблер -------------------------------------------------------------- */

.toggle {
  position: relative;
  flex: none;
  width: 40px;
  height: 22px;
  border: 0;
  border-radius: var(--r-pill);
  background: var(--edge);
  cursor: pointer;
  transition: background var(--dur) var(--ease);
}
.toggle::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 16px;
  height: 16px;
  border-radius: var(--r-pill);
  background: var(--card);
  transition: left var(--dur) var(--ease);
}
.toggle[aria-pressed="true"] {
  background: var(--accent);
}
.toggle[aria-pressed="true"]::after {
  left: 21px;
}

/* --- карточки ------------------------------------------------------------- */

.card {
  background: var(--card);
  border: 1px solid var(--edge-card);
  border-radius: var(--r5);
}
.card.is-pad {
  padding: var(--s8);
}

/* Пунктирная — «добавить ещё одно». Форма говорит, что внутри пока ничего нет. */
.card.is-dashed {
  border-style: dashed;
  border-color: var(--edge-dash);
  background: transparent;
  color: var(--ink-4);
  cursor: pointer;
}
.card.is-dashed:hover {
  border-color: var(--accent);
  color: var(--accent);
}

/* Кликабельная — вся карточка ведёт куда-то. */
.card.is-link {
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.card.is-link:hover {
  border-color: var(--accent-border);
  box-shadow: var(--sh-card);
}

/* Заметная — «здесь и сейчас». Единственное место, где карточка обведена
 * акцентом: если так выделить две, не выделена ни одна. */
.card.is-now {
  border-color: var(--accent);
  box-shadow: var(--sh-card);
}

.card.is-past {
  background: var(--field);
}
.card.is-past .card-title {
  color: var(--ink-4);
}

/* Предупреждающая — сроки отмены и то, что горит. */
.card.is-warn {
  background: var(--warn-bg);
  border-color: var(--warn-edge);
  color: var(--warn-deep);
}

/* --- схематичная карта ---------------------------------------------------- */

/* Штриховка вместо тайлов — прямо из макета. Она честно говорит «это схема»:
 * ровная заливка выглядела бы недогруженной картой. */
.map {
  position: relative;
  flex: 1;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  background: repeating-linear-gradient(135deg, #e6e9ec 0 8px, #edf0f2 8px 16px);
  color: var(--ink);
}
/* Подложку можно тащить. Штриховка при этом остаётся фоном `.map` намеренно:
 * она проступает сквозь ещё не загруженный и сквозь прозрачный тайл, то есть
 * режим отказа подложки — это ровно тот вид, что был до неё. */
.map.is-live {
  cursor: grab;
  touch-action: none;
  user-select: none;
}
.map.is-live:active {
  cursor: grabbing;
}

/* Найденное поиском: список, из которого выбирают. Не выпадающий — он живёт
 * внутри формы, а всплывающий над ней перекрывал бы поля дат. */
.plan-found {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--edge);
  border-radius: var(--r2);
  overflow: hidden;
}
.plan-found-row {
  display: flex;
  align-items: center;
  gap: var(--s4);
  padding: var(--s4) var(--s5);
  border: none;
  border-bottom: 1px solid var(--edge);
  background: var(--field);
  font: inherit;
  color: var(--ink-3);
  font-size: var(--t-13);
  text-align: left;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease);
}
/* Длинный список найденного не выпихивает поля правки за край панели:
 * семь результатов двуязычного запроса — обычное дело, и без ограничения
 * панель обрезала их молча, будто дальше ничего нет. */
.plan-found.is-tall {
  max-height: 232px;
  overflow-y: auto;
}
.plan-found-row:last-child {
  border-bottom: none;
}
.plan-found-row:hover,
.plan-found-row:focus-visible {
  background: var(--accent-tint);
}
.plan-found-name {
  color: var(--ink);
  font-size: var(--t-15);
}

/* Холст MapLibre. Лежит под всем остальным содержимым карты: пины, линейка
 * и подпись — наши, они рисуются поверх и позиционируются от того же вида.
 *
 * **Селектор из двух классов обязателен.** MapLibre вешает на этот же элемент
 * свой `.maplibregl-map` с `position: relative`, а его стили грузятся позже
 * наших — библиотека подтягивается по требованию. При равной специфичности
 * побеждает поздний, и холст схлопывался в нулевую высоту: карта оставалась
 * штриховкой, без единой ошибки в консоли. Одного класса здесь мало. */
.map > .map-canvas {
  position: absolute;
  inset: 0;
}
/* До `load` холст не показывается: метки в это время стоят по нашей подгонке,
 * а кадр MapLibre ещё свой, и два слоя расходились на глазах. Под скрытым
 * холстом проступает штриховка — тот самый вид «подложка не загружена».
 * `visibility`, а не `display`: библиотека меряет контейнер и без размера
 * не поднимется. */
.map:not(.is-vector) > .map-canvas {
  visibility: hidden;
}
/* Своё поверх чужого: библиотека ставит свои цвета фону и логотипу, а карта
 * обязана выглядеть частью интерфейса, а не вставленным окном. */
.map > .map-canvas.maplibregl-map {
  font: inherit;
  background: transparent;
}
.map .maplibregl-ctrl-bottom-left,
.map .maplibregl-ctrl-bottom-right {
  display: none;
}

.map-tiles {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.tile {
  position: absolute;
  opacity: 0;
  /* Приглушены намеренно: на полноцветной карте пины и подписи тонут, а карта
   * здесь — фон для своих точек, а не сама по себе. */
  filter: grayscale(0.45) contrast(0.92);
  transition: opacity var(--dur) var(--ease);
}
.tile.is-on {
  opacity: 0.9;
}

/* Как едем между городами: знак вида транспорта посередине перегона.
 * Тип читается значком, а не цветом, — как и у пинов: цвет в этой системе
 * занят состоянием. Перелёт вдобавок отличается дугой самой линии.
 *
 * Двадцать пикселей и без каймы: на 22 с каймой знак весил больше места (14 px),
 * то есть «чем едем» читалось важнее, чем «куда».
 *
 * **Залит цветом вида, значок белый.** Прежний белый кружок с тёмным значком
 * читался дыркой в карте и требовал разглядывания: фигура в 13 px — единственное,
 * что отличало поезд от парома. Цвет отвечает на «чем едем» до того, как глаз
 * дойдёт до фигуры, а фигура остаётся и различает виды внутри одного цвета
 * (авто, автобус и трансфер — все земля).
 *
 * Времени цвет заливки не мешает: время живёт на линии и в кольце пина,
 * знак его не несёт. */
.leg-mark {
  position: absolute;
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  margin: -10px 0 0 -10px;
  border: 0;
  border-radius: var(--r-pill);
  background: var(--type-air);
  color: var(--card);
  box-shadow: 0 0 0 1.5px var(--card), var(--sh-mark);
  pointer-events: none;
}
.leg-mark.is-car,
.leg-mark.is-bus,
.leg-mark.is-transfer {
  background: var(--type-road);
}
.leg-mark.is-train {
  background: var(--type-rail);
}
.leg-mark.is-ferry {
  background: var(--type-water);
}
/* Текущий перегон обведён акцентом: заливка занята видом, поэтому «сейчас»
 * говорит кольцо — тем же цветом, что и у текущего города. */
.leg-mark.is-now {
  box-shadow: 0 0 0 1.5px var(--card), 0 0 0 3.5px var(--accent), var(--sh-mark);
}

/* Кнопки зума — справа сверху, где их не закрывают ни подпись, ни линейка. */
.map-zoom {
  position: absolute;
  top: var(--s5);
  right: var(--s7);
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding: var(--s2);
  border-radius: var(--r1);
  background: rgba(255, 255, 255, 0.86);
}

/* Слой маршрута накрывает карту целиком, поэтому он обязан быть прозрачным
 * для мыши: иначе он перехватывает перетаскивание, и карта не двигается нигде,
 * где нарисована линия. Дефект был именно такой и вычиткой не находился —
 * SVG невидим, а жест до холста не доходит. */
.map-line {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
/* Линия перегона. Полной силы, без прозрачности: отделяет её от подложки
 * свечение, а не бледность. Пока линия была бледной и голой, поверх
 * магистрали `#fea` она пропадала — а маршрут идёт ровно по магистралям.
 *
 * **Толщина складывается из двух осей, а не спорит за одно свойство.** Вид
 * задаёт базу, фаза — множитель. Пока и то и другое писало `stroke-width`
 * напрямую, правила выходили равной специфичности (`.leg.is-past .leg-line`
 * и `.leg[data-type=…] .leg-line` — оба 0,3,0), и побеждало то, что стоит
 * ниже в файле: пройденный перелёт получал 1,4 px и рассыпался в пыль.
 * Порядком строк это лечить нельзя — оси независимы, значит и записаны
 * они должны быть независимо. */
.leg {
  --w-type: 1.6;
  --w-phase: 1;
}
.leg-line {
  stroke: var(--ink-2);
  stroke-width: calc(var(--w-type) * var(--w-phase) * 1px);
}
.leg-line.is-road {
  --w-type: 1.8;
}
/* Точка весит меньше штриха той же толщины: у неё нет длины. Поэтому дуга
 * перелёта идёт толще — иначе от неё остаётся пыль. */
.leg[data-type="flight"] {
  --w-type: 2.8;
}

/* Отделяет линию от подложки **свечение, а не кант**.
 *
 * Кант рисовался тем же пунктиром, только шире, — то есть обводил каждый штрих
 * по кругу, включая торцы. Линия читалась цепочкой белых капсул, а на перелёте
 * со штрихом 3 px от неё оставалась строчка бусин. Дефект жил в трёх пикселях
 * и на масштабе карты в глаза не бросался; увидели его на увеличенном образце.
 *
 * У свечения торцов нет по определению, поэтому штрих остаётся штрихом.
 * Двумя тенями, а не одной: одна на таком радиусе почти не отделяет.
 * Сплошную светлую подкладку под пунктиром не берём — она закрывает карту
 * в промежутках между штрихами, ровно за это её когда-то и убрали. */
.leg {
  filter: drop-shadow(0 0 1.5px var(--card)) drop-shadow(0 0 1.5px var(--card));
}
/* Без подложки спорить не с чем: свечение снимается, линия тише — как в макете,
 * где карта была схемой и фон под ней был свой. */
.map:not(.is-live) .leg {
  filter: none;
}
.map:not(.is-live) .leg-line {
  opacity: 0.45;
}
.map:not(.is-live) .leg-line.is-road {
  opacity: 0.6;
}
/* Ломаная запасного случая: когда перегоны неизвестны, а точки есть. Перегоны
 * лежат внутри `g.leg`, поэтому прямым потомком остаётся только она. */
.map-line > polyline {
  opacity: 0.35;
}

/* --- нить знает время ------------------------------------------------------ */

/* Пройденное истончается, текущее горит, будущее ждёт. Классы приходят
 * из `RouteMap.js` и **только когда на карте есть «сейчас»**: у завершённой
 * поездки и у ненаступившей выделять нечего, а приглушить всё значит просто
 * ослабить карту.
 *
 * Будущее — это обычная линия, поэтому правила `.is-next` нет: состояние
 * по умолчанию не нуждается в объявлении, а объявленное разошлось бы
 * с базовым при первой же правке толщины. */
.leg.is-past {
  --w-phase: 0.85;
}
.leg.is-past .leg-line {
  stroke: var(--time-past);
}
.leg.is-next .leg-line {
  stroke: var(--time-next);
}
/* Текущий перегон — сам акцент, толще и со свечением того же цвета. Свечение
 * добавляется к светлому: `filter` не складывается каскадом, поэтому оба
 * перечислены здесь заново. */
.leg.is-now {
  filter: drop-shadow(0 0 1.5px var(--card)) drop-shadow(0 0 1.5px var(--card))
    drop-shadow(0 0 5px var(--accent-border));
}
.leg.is-now {
  --w-phase: 1.5;
}
.leg.is-now .leg-line {
  stroke: var(--accent);
}

/* Город во времени. Заливка и кольцо, а **не размер**: пин остаётся мишенью
 * в 28 px — по нему уходят на экран города, и это главное действие «Маршрута».
 * Отыгрывать читаемость времени за счёт попадания мышью нечестно. */
.map-point.is-past .pin {
  background: var(--ink-5);
}
.map-point.is-past .pin-name {
  color: var(--ink-4);
}
/* Текущий город обведён акцентом. Кольцо — на обёртке, а не тенью пина:
 * у выбранного пина тень уже занята, и два правила спорили бы за неё,
 * а «сейчас» и «выбрано» — разные вещи и встречаются вместе. */
.map-point.is-now::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 44px;
  height: 44px;
  margin: -22px 0 0 -22px;
  border: 2px solid var(--accent);
  border-radius: var(--r-pill);
  pointer-events: none;
}
.map-point.is-now .pin-name {
  color: var(--ink);
  font-weight: 600;
}

/* Точка — это пара «кружок + подпись». Обёртка нулевого размера стоит ровно
 * в спроецированной координате, а кружок и подпись отстраиваются от неё:
 * иначе центрируется группа, и кружок съезжает с места, которое обозначает. */
.map-point {
  position: absolute;
  width: 0;
  height: 0;
  z-index: 2;
}
/* Порядок перекрытия задан явно, а не порядком в разметке. Отель и место
 * бывают в одной точке — тогда верхний пин глотает клик по нижнему, и нижний
 * становится недоступен молча. Ночёвка уступает: до неё есть якорь над картой,
 * а до места из списка — только пин. Выбранный поднимается над всеми, иначе
 * его собственная подпись оказывается под соседом. */
.map-point:has(.pin.is-stay) {
  z-index: 1;
}
/* Место — нижним слоем: их девятнадцать вокруг пятнадцати городов, и номер
 * города обязан читаться поверх точек, а не выглядывать из-под них. */
.map-point:has(.pin.is-spot),
.map-point:has(.pin.is-idea) {
  z-index: 0;
}
/* Наведение на строку списка. Не заливка, а кольцо: заливкой на карте
   помечено выбранное, и второе такое же читалось бы как второй выбор. */
.pin.is-hot {
  box-shadow: 0 0 0 3px var(--accent-border);
}

.map-point:has(.pin.is-on) {
  z-index: 3;
}
/* Всё, что лежит **поверх** карты, а не на ней. Порядок внутри `.map`:
 * подложка 0, перегоны 1, пины 1–3, панели и кнопки 4.
 *
 * Без явного слоя панель проигрывает пину при любом наложении: у пина есть
 * `z-index`, у панели он `auto`, и порядок в разметке тут уже ничего не решает.
 * Так номер города вылезал поверх формы этапа. */
.map > .map-zoom,
.map > .map-credit,
.map > .map-detail,
.map > .plan-card,
.map > .cal,
.map > .plan-found {
  z-index: 4;
}
/* Подпись — обводкой по буквам, а не плашкой под ними.
 *
 * Плашка отделяла подпись не хуже, но закрывала карту прямоугольником шире
 * самого слова: пятнадцать таких прямоугольников выедали половину кадра.
 * Обводка отделяет ровно буквы. У HTML-текста `paint-order` нет — её роль
 * играют несколько теней одного цвета с бумагой.
 *
 * Отступ от пина — радиус (14) плюс кант (2,5) плюс воздух. */
.pin-name {
  pointer-events: none;
  position: absolute;
  top: 50%;
  left: calc(var(--s7) + var(--s2));
  transform: translateY(-50%);
  max-width: 22ch;
  overflow: hidden;
  font: 500 var(--t-13) / 1.2 var(--sans);
  color: var(--ink-2);
  text-shadow: 0 0 3px var(--card), 0 0 3px var(--card), 0 0 3px var(--card),
    0 0 6px var(--card);
  white-space: nowrap;
  text-overflow: ellipsis;
}
.map-point.is-flipped .pin-name {
  left: auto;
  right: calc(var(--s7) + var(--s2));
}

.pin {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  /* Без явного нуля у кнопки остаётся отступ браузера `1px 6px`: точка места
   * 14 px с рамкой 3 px не могла стать уже 18 px и рисовалась овалом 18×14. */
  padding: 0;
  border: 0;
  border-radius: var(--r-pill);
  background: var(--ink);
  color: var(--card);
  font: 600 var(--t-13) / 1 var(--mono);
  cursor: pointer;
  transform: translate(-50%, -50%);
  /* Кант той же бумагой, что и у линий: без него пин теряется в застройке
   * и подписях подложки — они того же тона, что и он сам. */
  box-shadow: 0 0 0 2.5px var(--card), 0 3px 10px -4px rgba(23, 24, 26, 0.5);
  transition: background var(--dur-fast) var(--ease), width var(--dur-fast) var(--ease),
    height var(--dur-fast) var(--ease);
}
/* Тесная карта: точка вместо пина. Полторы сотни кругов по 28 px в одном
   городе сливаются в чёрное пятно, и значок вида, ради которого пин и рисуется,
   в этом пятне никто не читает — читают форму скопления. Точка отвечает
   на «где их много», а выбранная и та, на которую навели, вырастают обратно
   в полноценный пин и снова показывают значок. */
.map.is-dense .pin {
  width: 18px;
  height: 18px;
  box-shadow: 0 0 0 2px var(--card), 0 2px 6px -3px rgba(23, 24, 26, 0.5);
}
/* Значок остаётся: голая точка не отвечает «что это за место», а разница
   между стоянкой, аэропортом и вокзалом — первое, что ищут глазами. Мельче
   он читается хуже, но читается. */
.map.is-dense .pin > svg {
  width: 11px;
  height: 11px;
}
.map.is-dense .pin:hover,
.map.is-dense .pin.is-on {
  width: 28px;
  height: 28px;
  box-shadow: 0 0 0 2.5px var(--card), 0 3px 10px -4px rgba(23, 24, 26, 0.5);
}
.map.is-dense .pin:hover > svg,
.map.is-dense .pin.is-on > svg {
  width: 14px;
  height: 14px;
}

.pin:hover {
  background: var(--accent);
}
.pin.is-done {
  background: var(--ink-5);
}
.pin.is-on {
  width: 34px;
  height: var(--control);
  background: var(--accent);
  box-shadow: 0 0 0 2.5px var(--card), var(--sh-float);
}

/* Засечка «был» — в углу пина, поверх канта. Цвет тот же приглушённый, что
 * и у заливки отмеченного: засечка отвечает за «видно сразу», оттенок — за
 * «одно с другим не спутать». */
.pin-tick {
  position: absolute;
  right: -4px;
  bottom: -4px;
  display: grid;
  place-items: center;
  width: 14px;
  height: 14px;
  border-radius: var(--r-pill);
  background: var(--card);
  color: var(--ink-5);
}
/* Ночёвка на карте этапа отличается материалом, а не номером: она не место
 * в списке и номера не имеет, но без неё карта не отвечает на главный вопрос
 * экрана — далеко ли это от того, где я сплю. */
.pin.is-stay {
  background: var(--card);
  border: 1px solid var(--ink);
  color: var(--ink);
}

/* Место поездки на общей карте — точкой, а не номером: их девятнадцать
 * против пятнадцати городов, и номера двух рядов вперемешку читались бы
 * как один сбитый счёт. Отмеченное гаснет тем же способом, что везде.
 *
 * Кольцо тона `--ink-2`, а не `--ink-3`: третья ступень — ровно тон подписей
 * OpenFreeMap, и место было неотличимо от слова «Гоби» на подложке. Кант
 * у точки тоньше, чем у пина: она сама вдвое меньше. */
.pin.is-spot {
  width: 14px;
  height: 14px;
  background: var(--card);
  border: 3px solid var(--ink-2);
  box-shadow: 0 0 0 2px var(--card), var(--sh-mark);
}
.pin.is-spot.is-done {
  background: var(--fill);
  border-color: var(--ink-5);
}

/* Идея на карте этапа — бледнее места: та же точка без номера, что у мест
 * на общей карте, но кольцо на две ступени светлее. Номер на карте этапа
 * принадлежит маршруту, и идея не должна читаться его пунктом. */
.pin.is-idea {
  width: 14px;
  height: 14px;
  background: var(--card);
  border: 3px solid var(--ink-5);
  box-shadow: 0 0 0 2px var(--card), var(--sh-mark);
}
.pin.is-idea:hover {
  border-color: var(--accent);
  background: var(--card);
}
.pin.is-idea.is-on {
  width: 18px;
  height: 18px;
  border-color: var(--accent);
  background: var(--accent);
}

/* Найденное поиском — **кандидат, а не этап**: пунктирное кольцо акцентом,
 * заливка на просвет. Пока правила не было, `kind: "found"` падал в стиль
 * города — чёрный пин с номером, со своим счётом с единицы поверх маршрута,
 * и «Дали» в Юньнани выглядела принятым решением, хотя ею ещё не была. */
.pin.is-found {
  width: 22px;
  height: 22px;
  background: var(--accent-tint);
  border: 1.5px dashed var(--accent);
  color: var(--accent);
  box-shadow: 0 0 0 2px var(--card);
}
.pin.is-found:hover {
  background: var(--accent);
  border-style: solid;
  color: var(--card);
}

/* Атрибуция источника подложки. Не украшение и не подпись экрана: показывать её
 * требует лицензия OpenStreetMap. На живой подложке это делает сам MapLibre
 * компактным значком, здесь — только для растрового запасного пути, у которого
 * своего значка нет.
 *
 * Прежняя `.map-note` несла ещё поперечник, «схема, не навигация» и причину
 * выключенной подложки; вместе с масштабной линейкой убраны решением владельца
 * 26.08.2026 — они лежали полосами поверх карты и мешали на неё смотреть. */
.map-credit {
  position: absolute;
  left: var(--s7);
  bottom: var(--s5);
  padding: var(--hair) var(--s3);
  border-radius: var(--r1);
  background: rgba(255, 255, 255, 0.72);
  font: 400 var(--t-11) / 1.3 var(--mono);
  color: var(--ink-4);
  pointer-events: none;
}

.map-empty {
  position: absolute;
  inset: 50% auto auto 50%;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  gap: var(--s3);
  align-items: center;
  padding: var(--s7) var(--s8);
  border-radius: var(--r4);
  background: rgba(255, 255, 255, 0.9);
  color: var(--ink-3);
  font-size: var(--t-14);
  text-align: center;
}

/* Плавающие поверх карты: переключатель слоёв, карточка выбранного, нижняя полоса. */
.map-layers {
  position: absolute;
  left: var(--s7);
  top: var(--s7);
  display: flex;
  gap: var(--s2);
}
.map-layers .chip {
  background: rgba(255, 255, 255, 0.78);
  border-color: transparent;
  box-shadow: var(--sh-row);
}
.map-layers .chip[aria-pressed="true"] {
  background: var(--card);
  border-color: transparent;
  color: var(--ink);
}

/* Панель выбранного **выезжает** справа, а не возникает: мгновенное появление
 * поверх карты читается как подмена экрана, а выезд говорит «это подробности
 * к тому, что ты нажал». Кадры — в motion.css, здесь только положение. */
.map-detail {
  animation: detail-in var(--dur) var(--ease) both;
  position: absolute;
  right: var(--s7);
  top: var(--s7);
  width: 320px;
  max-height: calc(100% - var(--s7) * 2 - 90px);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--s5);
  padding: var(--s7);
  border-radius: var(--r5);
  background: var(--card);
  box-shadow: var(--sh-panel);
}
/* Ничто внутри не ужимается: панель прокручивается сама, а флексовое сжатие
 * «по месту» отдаёт высоту тому, у кого своя прокрутка, — список найденного
 * схлопывался в ноль и на экране просто исчезал. Видно это только когда
 * содержимое выше панели, то есть не всегда. */
.map-detail > * {
  flex: none;
}
.map-detail .photo {
  height: 132px;
  border-radius: var(--r3);
  flex: none;
}

.map-bar {
  position: absolute;
  left: var(--s7);
  right: var(--s7);
  bottom: var(--s7);
  display: flex;
  align-items: center;
  gap: var(--s8);
  padding: var(--s6) var(--s7);
  border-radius: var(--r4);
  background: var(--card);
  box-shadow: var(--sh-float);
}
.map-bar .sep {
  width: 1px;
  align-self: stretch;
  background: var(--line);
}

/* --- слот под фото -------------------------------------------------------- */

.photo {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  border-radius: inherit;
  background: var(--fill);
  transition: box-shadow var(--dur-fast) var(--ease);
}
.photo img,
.photo svg {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Подсказка поверх сцены, а не вместо неё: сцена показывает, что тут будет
 * картинка, подпись — что её можно принести. */
.photo-hint {
  position: absolute;
  inset: auto var(--s3) var(--s3) var(--s3);
  font: 500 var(--t-11) / 1.3 var(--sans);
  color: var(--card);
  text-shadow: 0 1px 3px rgba(23, 24, 26, 0.65);
  pointer-events: none;
}
.photo.is-over {
  box-shadow: inset 0 0 0 2px var(--accent);
}

/* --- карточка брони -------------------------------------------------------- */

/* Отступы уезжают внутрь: шапка со сценой должна доходить до краёв карточки,
 * иначе она читается вставленной картинкой, а не обложкой. */
.detail-card {
  overflow: hidden;
}
.detail-card > .is-pad {
  padding: var(--s7);
}
/* Ящик брони шире колонки чтения, и текст, растянутый на всю панель, читается
 * плохо: строка в тысячу пикселей теряет начало следующей. Но мера чтения нужна
 * **тексту**, а не сетке пар и не ленте кадров — им она оставляла треть панели
 * пустой (карточка 1028 px, содержимое 720). Поэтому мера точечная. */
.detail-card .detail-note,
.detail-card .detail-line,
.detail-card .detail-value {
  max-width: 720px;
}

.detail-cover {
  position: relative;
  height: 168px;
  flex: none;
}
/* У жилья обложка выше: там кадр номера, ради которого бронь и открывают,
 * а на 168 px он обрезается по кроватям. У билета и документа обложка —
 * рисованная сцена, ей высота ни к чему. */
.detail-cover.is-tall {
  height: 240px;
}
/* Затемнение снизу — под текстом, а не по всей высоте: сцена светлая сверху,
 * и глухая вуаль убила бы её целиком ради двух строк. */
.detail-cover::after {
  content: "";
  position: absolute;
  inset: 20% 0 0 0;
  /* Три остановки, а не две: линейная растяжка даёт светлую полосу ровно там,
     где идут иероглифы, и они пропадают.

     Плотность поднята под кадры продавца: у рисованной сцены низ тёмный
     сам по себе, а снимок номера бывает белой стеной в дневном свете —
     на «Atour Hotel Shanghai Qingpu Industrial Park» белый заголовок на ней
     не читался вовсе. */
  background: linear-gradient(
    to bottom,
    rgba(23, 24, 26, 0),
    rgba(23, 24, 26, 0.5) 55%,
    rgba(23, 24, 26, 0.85)
  );
  pointer-events: none;
}
/* Вуаль существует ради текста поверх обложки. У перелёта его нет — маршрут
 * написан на самой сцене, — и затемнение там просто съедает нижнюю треть
 * картинки, ничего не давая взамен. */
.detail-cover.is-bare::after {
  content: none;
}

/* Подсказка о фото уезжает наверх: внизу стоит название, и они накладывались
   друг на друга. Здесь она не мешает и по-прежнему видна. */
.detail-cover .photo-hint {
  inset: var(--s3) var(--s3) auto auto;
  text-align: right;
}
/* Подписи городов на шапке перелёта (`FlightScene`). Гарнитура и кегль —
 * правилом, а не атрибутами SVG: `var()` в презентационном атрибуте не
 * раскрывается, и шрифт уехал бы в браузерный по умолчанию. Кегль в единицах
 * кадра сцены (1240×200), то есть на экране это те же ~15 px. Кант тенью —
 * тот же приём, что у текста обложки: небо дневного рейса светлое, и белая
 * подпись без тени на нём тонет. */
.flight-scene-city {
  font: 500 15px/1 var(--sans);
  letter-spacing: 0.01em;
  text-shadow: 0 1px 4px rgba(23, 24, 26, 0.55);
}

/* Подписи шапки аренды (`RentalScene`): даты выдачи и возврата, над ними тихая
 * подпись, над машиной срок. Гарнитуры — правилом, а не атрибутами SVG, по той
 * же причине, что у шапки рейса; кегли в единицах кадра 1240×200. */
.rental-scene-label {
  font: 500 17px/1 var(--mono);
  text-shadow: 0 1px 4px rgba(23, 24, 26, 0.55);
}
.rental-scene-caps {
  font: 600 11px/1 var(--sans);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  opacity: 0.8;
  text-shadow: 0 1px 3px rgba(23, 24, 26, 0.5);
}
.rental-scene-days {
  font: 600 22px/1 var(--sans);
  letter-spacing: var(--track-tight);
  text-shadow: 0 1px 6px rgba(23, 24, 26, 0.55);
}
/* Модель машины — крупно, как местное название отеля: по ней ищут машину
 * на стоянке и сверяют с договором. */
.rental-model {
  font: 600 var(--t-22) / 1.2 var(--sans);
  letter-spacing: var(--track-tight);
  color: var(--ink);
}

.detail-cover-text {
  position: absolute;
  /* Выше вуали. `::after` красится последним среди детей, и без этой строки
     0,96 чёрного ложилось поверх белых букв: заголовок читался серым по серому
     на любом кадре. С рисованной сценой дефект не был виден — она тёмная. */
  z-index: 1;
  inset: auto var(--s7) var(--s5) var(--s7);
  display: flex;
  flex-direction: column;
  gap: var(--hair);
  color: var(--card);
  text-shadow: 0 2px 10px rgba(23, 24, 26, 0.85);
  pointer-events: none;
}
.detail-cover-text .caps {
  color: rgba(255, 255, 255, 0.78);
}
.detail-cover-text .detail-title {
  color: var(--card);
}
/* Местное название — то, что показывают на стойке и таксисту. Здесь ему
 * и место: в момент заселения оно важнее всего остального на карточке. */
.detail-cover-local {
  font: 500 var(--t-16) / 1.3 var(--sans);
  color: rgba(255, 255, 255, 0.92);
}

/* --- пустые состояния ----------------------------------------------------- */

/* Макет их не рисует, а они случаются постоянно: город без жилья, место без
 * координат, бронь без файла. Пустой экран без объяснения читается как поломка. */
.empty {
  display: flex;
  flex-direction: column;
  gap: var(--s4);
  align-items: flex-start;
  padding: var(--s8) var(--s7);
  color: var(--ink-4);
  font-size: var(--t-14);
  line-height: 1.5;
}

/* --- выезжающая панель ---------------------------------------------------- */

/* Правка живёт в панели, а не посреди таблицы: форма на пятнадцать полей,
 * раскрытая внутри списка, разрывает его пополам и уносит соседние строки
 * с экрана — то есть отбирает ровно тот контекст, ради которого правку
 * и держали рядом. Панель оставляет список видимым за собой. */

.scrim {
  position: fixed;
  inset: 0;
  z-index: 55;
  background: var(--scrim);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur) var(--ease);
}
.scrim.is-open {
  opacity: 1;
  pointer-events: auto;
}

.drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 56;
  width: min(560px, 92vw);
  display: flex;
  flex-direction: column;
  background: var(--card);
  box-shadow: var(--sh-panel);
  transform: translateX(100%);
  transition: transform var(--dur) var(--ease);
}
/* Узел смонтирован всегда, класс переключает видимость: иначе анимация
 * закрытия не успевает проиграться — элемент исчезает из дерева раньше. */
.drawer.is-open {
  transform: translateX(0);
}
/* Пока открыт кадр во весь экран, ящик снимает `transform`. Слой `position:
 * fixed`, но живёт в дереве ящика, а элемент с `transform` становится для
 * такого потомка содержащим блоком: вуаль отмерялась не от окна, а от ящика
 * и накрывала только его две трети. Открытый ящик и так стоит
 * на `translateX(0)`, так что видимой цены у отмены нет. Портала
 * в вендоренном preact нет, иначе слой уехал бы в конец `body`. */
.drawer.is-open:has(.lightbox) {
  /* Гасится и анимация: у ящика висит доигравшая `rise` с `fill: both`,
     а заполняющая анимация `transform` бьёт объявление и держит содержащий
     блок дальше. Ровно тот же случай, что у `.hs-root` ниже, — и найден он
     тоже замером: слой выходил 950 px при окне 1440. */
  animation: none;
  transform: none;
  transition: none;
}
/* Широкий — для подробностей, которые читают, а не правят. Две трети экрана:
 * карточка с обложкой, ссылками и вложениями в колонку 360 px не помещалась
 * и вырождалась в список пар «подпись — значение». Потолок нужен, иначе
 * на широком мониторе строка уезжает за предел читаемой длины. */
.drawer.is-wide {
  width: min(66vw, 1080px);
}
.drawer-head {
  display: flex;
  align-items: flex-start;
  gap: var(--s5);
  flex: none;
  padding: var(--s8) var(--s9);
  border-bottom: 1px solid var(--line);
}
.drawer-title {
  font: 600 var(--t-19) / 1.2 var(--sans);
  letter-spacing: var(--track-tight);
}
.drawer-body {
  min-height: 0;
  overflow-y: auto;
  padding: var(--s8) var(--s9) var(--s10);
}

/* --- сообщение ------------------------------------------------------------ */

.toast {
  position: fixed;
  left: 50%;
  bottom: var(--s8);
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: var(--s4);
  max-width: 560px;
  padding: var(--s5) var(--s7);
  border-radius: var(--r4);
  background: var(--ink);
  color: var(--rail-ink);
  font-size: var(--t-14);
  box-shadow: var(--sh-panel);
  z-index: 60;
}
.toast.is-error {
  background: var(--alarm);
}

/* --- Полоса фотографий ------------------------------------------------------
 *
 * Обложка показывает одну, а у отеля их две-три: продавец отдаёт несколько,
 * и вторая обычно про номер, а не про фасад. Полоской, а не сеткой — это
 * дополнение к обложке, и оно не должно спорить с ней за внимание.
 */
/* Одиннадцать пар подряд читались как стена одинаковых подписей. Группа
 * даёт три взгляда вместо одиннадцати: «когда», «деньги», «на стойке». */
.detail-part {
  display: flex;
  flex-direction: column;
  gap: var(--s3);
}
.detail-part > .caps {
  color: var(--ink-4);
}

.photo-strip {
  display: flex;
  gap: var(--s2);
  overflow-x: auto;
  /* Полоса живёт внутри карточки фиксированной ширины: без этого длинный
     ряд растягивает карточку, а не прокручивается внутри неё. */
  min-width: 0;
}
/* Значение, которое копируют по клику: кнопка, но выглядит как текст —
 * иначе три номера подряд читаются как ряд кнопок, а не как номера. */
.copyable {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.copyable svg {
  opacity: 0;
  color: var(--ink-4);
  transition: opacity var(--dur-fast) var(--ease);
}
.copyable:hover svg,
.copyable:focus-visible svg {
  opacity: 1;
}
/* Кнопка без текста — только значок: прятать его не во что. */
.copyable:not(:has(span)) svg {
  opacity: 0.55;
}
.copyable:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r1);
}

/* Кадр во весь экран. Общий на весь проект: им листают и альбом отеля
 * в подборе, и кадры брони. Переехал из раздела целиком — вместе
 * с плёнкой превью и счётчиком.
 *
 * Текст поверх вуали набран бумагой: на тёмном ей же и быть чернилами,
 * а своего цвета «светлый текст» в шкале нет. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 41;
  display: flex;
  flex-direction: column;
  background: var(--scrim-deep);
  animation: fade var(--dur) var(--ease);
}

.lightbox-head {
  display: flex;
  align-items: center;
  gap: var(--s5);
  flex: none;
  padding: var(--s5) var(--s6);
  color: var(--paper);
}

.lightbox-head .icon-btn {
  color: var(--paper);
}

.lightbox-name {
  flex: 1;
  min-width: 0;
  font: 500 var(--t-16) / 1.2 var(--sans);
}

.lightbox-count {
  font-size: var(--t-14);
  opacity: 0.8;
}

.lightbox-body {
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s5);
  padding: 0 var(--s6);
}

/* `contain`, а не `cover`: в ленте плитка режется ради ровного ряда,
 * а в просмотре резать кадр незачем. */
.lightbox-frame {
  max-height: var(--hs-light-frame);
  max-width: min(1100px, 88vw);
  object-fit: contain;
  border-radius: var(--r3);
}

.lightbox-frame.is-gone {
  display: block;
  width: min(600px, 60vw);
  aspect-ratio: 3 / 2;
  background: rgba(255, 255, 255, 0.06);
  border-radius: var(--r3);
}

/* Стрелки текстом: стрелок в спрайте Tripbox нет, а рисовать фигуру по месту
 * запрещено — знаки живут одним набором. */
.lightbox-nav {
  flex: none;
  width: var(--control);
  height: var(--control);
  border: 0;
  border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.12);
  color: var(--paper);
  font-size: var(--t-22);
  line-height: 1;
  cursor: pointer;
}

.lightbox-nav:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.22);
}

.lightbox-nav:disabled {
  opacity: 0.3;
  cursor: default;
}

.lightbox-film {
  flex: none;
  display: flex;
  gap: var(--s2);
  overflow-x: auto;
  padding: var(--s5) var(--s6);
}

.lightbox-thumb {
  flex: none;
  width: 72px;
  height: 48px;
  padding: 0;
  border: 2px solid transparent;
  border-radius: var(--r2);
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
  cursor: pointer;
}

.lightbox-thumb:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.lightbox-thumb.is-on {
  border-color: var(--accent);
}

/* --- содержащий блок ------------------------------------------------------- */

/* Окно и просмотр — `position: fixed`, но живут они в дереве панели, а `.work > *`
 * (то есть `.hs-root`) анимируется `rise` с `animation-fill-mode: both`.
 * Chromium считает элемент с **заполняющей** анимацией `transform` содержащим
 * блоком даже тогда, когда вычисленное значение уже `none`, — и `inset: 0`
 * отмеряется не от окна, а от раздела.
 *
 * Замер 07.09.2026: слой вышел 1440×836 при окне 1440×900 — ровно на высоту
 * верхней полосы (`--topbar: 64px`). В коде этого не видно никак, поэтому
 * сторожит UI-тест `test_plitka_vsego_otkryvaet_okno_alboma`.
 *
 * Гасим анимацию корня, **пока слой открыт**: к этому моменту она давно доиграла
 * (0,18 с), так что видимой цены нет, а содержащий блок исчезает. Портал
 * в вендоренном preact отсутствует, иначе слой просто уехал бы в конец `body`. */
.hs-root:has(.hs-album-win),
.hs-root:has(.lightbox) {
  animation: none;
}

/* Плитка плёнки: кадр обрезается ради ровного ряда. */
.lightbox-thumb-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.lightbox-thumb-img.is-gone {
  background: rgba(255, 255, 255, 0.08);
}

/* Прицел по кадру внутри ссылки, а не по любому вложенному: кадр во весь
 * экран рисуется внутри же ленты, и правило плитки било по нему — лайтбокс
 * открывал фотографию размером 168×120. */
.photo-strip.is-lg > a img {
  width: 168px;
  height: 120px;
}
.photo-strip > a img {
  width: 84px;
  height: 60px;
  flex: none;
  object-fit: cover;
  border-radius: var(--r2);
  border: 1px solid var(--edge-card);
  background: var(--field);
  transition: transform var(--dur-fast) var(--ease);
}
.photo-strip a:hover img {
  transform: scale(1.03);
}
.photo-strip a:focus-visible img {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* «Или» между отелями на одну ночь — связка, а не строка списка: тон приглушён,
 * линия справа отделяет варианты друг от друга. Живёт здесь, а не в стилях
 * экрана: одним словом говорят про выбор и лента городов, и план. */
.or {
  display: flex;
  align-items: center;
  gap: var(--s4);
  color: var(--ink-4);
  font: 400 var(--t-12) / 1 var(--mono);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}
.or::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line);
}

/* --- Перелёт ------------------------------------------------------------------
 *
 * Один состав полей на все экраны, глубина разная: `Flight.js`. Сетка сегмента —
 * две колонки, маршрут и время: их читают парой, и время обязано стоять рядом
 * с маршрутом, а не у правого края экрана. Разнесённые по углам широкого экрана,
 * они собирались глазами из трёх мест.
 */
.flight {
  display: flex;
  flex-direction: column;
  gap: var(--s3);
  min-width: 0;
  /* Ширина ограничена намеренно: время читают в паре с маршрутом, и на широком
     экране растянутая сетка разносила их на восемьсот пикселей. */
  max-width: 720px;
}
.flight-leg {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--hair) var(--s5);
  align-items: baseline;
  min-width: 0;
}
.flight-route {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font: 600 var(--t-16) / 1.3 var(--sans);
  letter-spacing: var(--track-tight);
  color: var(--ink);
}
/* Номер сегмента — у перелёта с пересадкой. Без него два жирных заголовка
   подряд читаются как два разных письма, а не как один перелёт. */
.flight-num {
  flex: none;
  min-width: 16px;
  height: 16px;
  border-radius: var(--r1);
  background: var(--fill);
  color: var(--ink-3);
  font: 500 var(--t-12) / 16px var(--mono);
  text-align: center;
}
/* Время — моноширинной, как все числа, которые сверяют глазами. */
.flight-when {
  font: 500 var(--t-15) / 1.3 var(--mono);
  color: var(--ink-2);
  white-space: nowrap;
  text-align: right;
}
.flight-when b {
  font-weight: 600;
  color: var(--ink);
}
.flight-when.is-empty {
  font-size: var(--t-13);
  color: var(--ink-4);
}
.flight-when small {
  display: block;
  font: 400 var(--t-12) / 1.4 var(--mono);
  color: var(--ink-4);
}
/* Сдвиг суток — надстрочным, как в расписаниях: «06:15⁺¹» читается одним
   взглядом, а «прилёт на следующий день» отдельной строкой — нет. */
.flight-shift {
  margin-left: 1px;
  font-size: var(--t-12);
  color: var(--alarm);
}
.flight-line {
  grid-column: 1 / -1;
  font-size: var(--t-14);
  color: var(--ink-3);
}
.flight-facts {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s6);
  margin-top: var(--s2);
}
.flight-fact {
  display: flex;
  flex-direction: column;
  gap: var(--hair);
  font: 500 var(--t-14) / 1.3 var(--mono);
  color: var(--ink-2);
}
/* Кто летит — колонка строк, а не такие же «факты» рядом.
   Факт отвечает одним значением, пассажир — тремя (имя, багаж, билет),
   и в общем ряду они разваливались бы на девять безымянных кусков. */
.flight-pax {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  gap: var(--hair);
  margin-top: var(--s2);
}
.flight-pax-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2) var(--s4);
  font: 400 var(--t-14) / 1.4 var(--mono);
  color: var(--ink-3);
}
/* Имя весит больше остального: с ним сверяют паспорт, а багаж и номер билета
   при нём — уточнения. */
.flight-pax-row b {
  font-weight: 600;
  color: var(--ink-2);
}

/* Пересадка — строка между сегментами, а не подпись к одному из них:
   она принадлежит обоим и читается как пауза в маршруте. */
.flight-layover {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2) var(--s4);
  padding: var(--s2) var(--s4);
  border-left: 2px solid var(--edge);
  margin-left: var(--s2);
  font-size: var(--t-14);
  color: var(--ink-3);
}
.flight-layover.is-tight {
  border-left-color: var(--warn-edge);
  background: var(--warn-bg);
  color: var(--warn-deep);
  border-radius: 0 var(--r1) var(--r1) 0;
}
