/* =========================================================
   СЕКЦИИ

   Два ритма:
   .is-beat — «кадр»: во весь экран, одна мысль, фильм светлый и в движении
   .is-read — «текст»: плотная вёрстка, фильм уходит в тень и ЗАМИРАЕТ

   Правила языка: ни одной коробки и ни одной заливки — только
   волосяные линии; между крупным и мелким кеглем нет «средних»
   размеров; блоки садятся со сдвигом по 12-колоночной сетке,
   а не по центру.
   ========================================================= */

.page { position: relative; z-index: var(--z-page); }

.chapter { position: relative; }

/* ПЕРЕХОД ПО ССЫЛКЕ ОСТАНАВЛИВАЕТСЯ НИЖЕ ШАПКИ.

   Шапка прибита к верху и непрозрачна. Пока поправки не было, нажатие на
   «Забронировать» приводило ровно к верху цели — и цель оказывалась ПОД
   шапкой. Замер на 390x844: верх первого поля формы вставал на 107.9 px,
   шапка занимала до 111 px.

   Полтора рема сверх высоты шапки — чтобы у блока было видно, что он
   начался, а не обрезан. Значение читает и браузер (при переходе по
   адресу с решёткой), и js/scroll.js — он везёт страницу сам и берёт
   отсюда же, чтобы не разъехаться. */
.chapter[id] { scroll-margin-top: calc(var(--head-h) + 1.5rem); }

/* Блок брони — цель ВСЕХ кнопок страницы, и подводить к нему надо иначе.

   У «кадров» сверху нарочно много воздуха: пятая часть экрана пустого
   кадра перед первой строкой. Когда человек долистывает до блока сам,
   это дыхание. Когда он нажал «Забронировать место» — это двести
   пикселей пустоты вместо ответа.

   Поэтому у брони переход съедает её собственный верхний отступ. Тогда в
   экран попадает всё сразу: заголовок, обещание и оба поля. Значение
   может выйти отрицательным — так и задумано, это «проехать ниже верха
   секции». */
#booking { scroll-margin-top: calc(var(--head-h) + 3.5rem - var(--beat-top)); }

/* Контент «кадра» прижат к низу: сверху кадр дышит, внизу
   тип садится на плотную подложку. Это композиция киноплаката,
   а не «текст по центру поверх картинки». */
.chapter.is-beat {
  --beat-top: clamp(8rem, calc(var(--vh, 1svh) * 19), 13rem);
  min-height: calc(var(--vh, 1svh) * 100);
  display: grid;
  align-content: end;
  padding-block: var(--beat-top) clamp(4.5rem, calc(var(--vh, 1svh) * 11), 8rem);
}

/* Первый экран начинается НИЖЕ переключателя языка.

   Переключатель висит в правом верхнем углу с первой секунды, и на низком
   экране (360x640 — обычный старый андроид) первая строка подписи заезжала
   под него. На нормальном экране отступ и так вдвое больше, и max() здесь
   ничего не меняет: плата берётся только там, где иначе была бы накладка. */
.chapter.is-beat.hero { padding-top: max(var(--beat-top), calc(var(--lang-bottom) + 0.9rem)); }

.chapter.is-read { padding-block: var(--pad); }

/* Секции с угловым маркером получают запас сверху.

   Маркер («VII · Ночь перед решением») лежит абсолютом на высоте
   clamp(5.5rem, 13vh, 8.5rem), а содержимое начиналось от var(--pad). Когда
   я сократил вертикальный ритм, нижняя граница маркера оказалась НИЖЕ
   верха заголовка — римская цифра наехала на слова. Видно на телефоне,
   где оба значения берут свой минимум.

   Запас считаем от той же величины, что и позиция маркера, — тогда они не
   разъедутся ни при каком размере экрана. */
.chapter.is-read:has(.mark) {
  padding-top: calc(clamp(5.5rem, calc(var(--vh, 1svh) * 13), 8.5rem) + 3.4rem);
}

/* Переход — чистая плёнка. Здесь проигрывается ровно один клип,
   от шва до шва. Текста нет намеренно: все блоки стоят на швах,
   а между ними идёт только кино. */
.chapter.is-move {
  min-height: calc(var(--vh, 1svh) * 105);
}

/* Донный скрим живёт на фиксированном слое фильма (film__scrim):
   секционный псевдоэлемент давал видимый стык между двумя «кадрами». */

.chapter.is-beat h1,
.chapter.is-beat h2,
.chapter.is-beat p { text-shadow: 0 2px 24px rgba(2, 5, 11, 0.95); }

.chapter.is-beat .lead,
.chapter.is-beat p { color: rgba(240, 232, 218, 0.94); }

/* ---- Угловой маркер главы: римская цифра + имя ---- */
.mark {
  position: absolute;
  top: clamp(5.5rem, calc(var(--vh, 1svh) * 13), 8.5rem);
  left: var(--gut);
  right: var(--gut);
  display: flex;
  align-items: center;
  gap: 1.1rem;
  pointer-events: none;
}
.mark__line { flex: 1; height: 1px; background: var(--hair); }
.mark .slate { white-space: nowrap; }

/* Маркер ПЕРВОЙ главы не доходит до правого края: там с первой секунды
   висит переключатель языка (.langbar), и судовая подпись читалась бы
   сквозь него. Остальным главам это не нужно — их маркеры проезжают
   верх страницы на прокрутке и уходят под шапку, как и задумано. */
.hero .mark { padding-right: calc(var(--lang-cell) * 2 + 0.9rem); }

/* ============ I. Пролог ============ */

.hero__top { display: flex; align-items: baseline; gap: 1.2rem; margin-bottom: clamp(1.6rem, calc(var(--vh, 1svh) * 4), 3rem); }
.hero__top .hair { flex: 1; min-width: 2.5rem; }

/* Слот надписи рассчитан на три слова («ОДИН ЗАХОД В ГОДУ»), а здесь
   целое предложение. С разрядкой 0.38em оно расползлось бы на четыре
   строки и вытеснило линейку: убавляем разрядку и разрешаем перенос. */
.label--long {
  letter-spacing: 0.16em;
  max-width: 56ch;
  line-height: 1.75;
}

.hero h1 { max-width: 15ch; }

.hero__lower {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: clamp(1.6rem, 4vw, 4rem);
  margin-top: clamp(2.2rem, calc(var(--vh, 1svh) * 5), 3.6rem);
  padding-top: clamp(1.6rem, calc(var(--vh, 1svh) * 3), 2.4rem);
  border-top: 1px solid var(--hair);
}
.hero__sub { max-width: 40ch; }

.hero__offer { display: flex; flex-direction: column; align-items: flex-end; gap: 0.5rem; }
.hero__cta { margin-top: 1.6rem; }

/* подсказка уходит вправо: слева весь низ занят типом */
.hint {
  position: absolute;
  bottom: clamp(1.5rem, calc(var(--vh, 1svh) * 3.5), 2.6rem);
  right: var(--gut);
  display: flex;
  align-items: center;
  gap: 0.9rem;
  font-size: var(--t-label);
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: var(--bone-dim);
  transition: opacity 0.6s var(--ease);
}
.is-past-hero .hint { opacity: 0; }
.hint__line {
  width: 56px; height: 1px;
  background: linear-gradient(90deg, transparent, var(--champ));
  transform-origin: right;
  animation: hint 2.6s var(--ease) infinite;
}
@keyframes hint {
  0%   { transform: scaleX(0); opacity: 0; }
  40%  { transform: scaleX(1); opacity: 1; }
  100% { transform: scaleX(1); opacity: 0; }
}

/* ============ II. Манифест ============ */

.manifest__lines {
  font-family: var(--display);
  font-size: var(--t-beat);
  line-height: 1.04;
  letter-spacing: -0.02em;
}
/* строгий потомок: внутрь этих строк скрипт кладёт ещё span'ы на слово */
.manifest__lines > span { display: block; }
.manifest__lines > span + span { color: var(--champ-hi); padding-left: clamp(2rem, 11vw, 11rem); }

.manifest__text {
  grid-column: 6 / -1;
  margin-top: clamp(2.4rem, calc(var(--vh, 1svh) * 6), 4.5rem);
  padding-top: 1.6rem;
  border-top: 1px solid var(--hair);
}

/* ============ III. Глубина ============ */

.depth p {
  font-family: var(--display);
  font-size: var(--t-beat);
  line-height: 1.06;
  letter-spacing: -0.02em;
  max-width: 20ch;
}
.depth em { display: block; padding-left: clamp(2rem, 9vw, 9rem); }

/* ============ IV. Мастер ============

   Переделано целиком. Что было не так, по скриншоту с большого экрана:
   заголовок в узкой колонке ломался на шесть рваных строк, фото висело
   маленьким прямоугольником с пустотой под ним на пол-экрана, число и
   подпись в фактах слипались, а правая половина экрана пустовала.

   Стало: заголовок во всю ширину над блоком и в две ровные строки, фото
   крупное и в раме со сдвигом — она даёт глубину, которой не хватало,
   имя лежит прямо на кадре. Факты — волосяная таблица с крупными
   числами дидоном, каждое на своей строке. */

.master {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  column-gap: clamp(2.5rem, 6vw, 6rem);
  row-gap: clamp(2.5rem, calc(var(--vh, 1svh) * 6), 4.5rem);
  align-items: start;
}

.master__intro { grid-column: 1 / -1; }
.master__intro h2 {
  margin-top: 1.2rem;
  /* мера в знаках — чтобы строка ломалась там, где задумано, а не как выйдет */
  max-width: 24ch;
}

/* ---- Фото в раме со сдвигом ----
   Рамка не обводит кадр, а стоит за ним со смещением: приём из печатной
   вёрстки, который даёт слой и воздух там, где раньше был плоский
   прямоугольник на плоском фоне. */
.master__photo {
  position: relative;
  align-self: start;
}
.master__photo::before {
  content: "";
  position: absolute;
  inset: clamp(0.9rem, 2vw, 1.6rem) calc(-1 * clamp(0.9rem, 2vw, 1.6rem))
          calc(-1 * clamp(0.9rem, 2vw, 1.6rem)) clamp(0.9rem, 2vw, 1.6rem);
  border: 1px solid var(--hair-warm);
  pointer-events: none;
  transition: transform 0.8s var(--ease);
}
.master__photo:hover::before { transform: translate(4px, 4px); }

/* Ни aspect-ratio, ни object-fit тут не нужны: файл и так ровно 1000×1250,
   то есть искомые 4:5. А связка «aspect-ratio + height: auto» на картинке —
   как раз то место, где сафари на айфонах роняет высоту в ноль и картинка
   пропадает целиком. Убираем лишнее звено: соотношение берётся из самого
   файла, и ломаться нечему. */
.master__photo img {
  position: relative;
  display: block;
  width: 100%;
  height: auto;
  filter: saturate(0.94);
}

/* имя лежит на кадре, а не висит под ним отдельной строкой */
.master__caption {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 1;
  display: grid;
  gap: 0.3rem;
  padding: clamp(3rem, 8vw, 5rem) clamp(1rem, 2.5vw, 1.6rem) clamp(1rem, 2.2vw, 1.4rem);
  background: linear-gradient(180deg, transparent 0%, rgba(2, 5, 11, 0.86) 62%);
}
.master__name {
  font-family: var(--display);
  font-size: clamp(1.15rem, 1rem + 0.7vw, 1.6rem);
  line-height: 1.1;
  color: var(--bone);
}
.master__role {
  font-family: var(--ui);
  font-size: var(--t-label);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--champ);
}

.master__photo.is-empty img { display: none; }
.master__photo.is-empty::after {
  content: "Павел Андреев";
  display: grid;
  place-content: center;
  aspect-ratio: 4 / 5;
  border: 1px solid var(--hair);
  color: var(--bone-dim);
  font-size: var(--t-label);
  letter-spacing: 0.24em;
  text-transform: uppercase;
}

.master__body > .lead { max-width: 44ch; }

/* факты — волосяная таблица, а не карточки.
   Число и подпись раньше слипались: колонка под число была задана в
   процентах и на широком экране схлопывалась. Теперь она по содержимому. */
.facts {
  list-style: none;
  margin-top: clamp(2.2rem, calc(var(--vh, 1svh) * 5), 3.4rem);
}
.facts li {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: clamp(1.2rem, 3vw, 2.6rem);
  align-items: baseline;
  padding: clamp(1.1rem, 2.4vw, 1.6rem) 0;
  border-top: 1px solid var(--hair);
  transition: border-color 0.6s var(--ease);
}
.facts li:last-child { border-bottom: 1px solid var(--hair); }
.facts li:hover { border-color: var(--hair-warm); }
/* Иерархия чисел была перевёрнута: «13 человек экипажа» набиралось вдвое
   крупнее, чем «100 000 ₽ за один разбор» — то есть характеристика судна
   заглушала главный денежный довод страницы. Меняем местами по весу:
   доводы о цене крупнее, характеристики яхты спокойнее. */
.facts b {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(2rem, 1.2rem + 2.2vw, 3.4rem);
  color: var(--champ-hi);
  white-space: nowrap;
  letter-spacing: -0.01em;
  line-height: 1;
}
.facts span { font-size: var(--t-small); color: var(--bone-mid); max-width: 32ch; }

@media (max-width: 900px) {
  .master { grid-template-columns: 1fr; }
  .master__photo { max-width: 24rem; }
  .master__photo::before { inset: 0.7rem -0.7rem -0.7rem 0.7rem; }
}

/* ============ V. Метод — лестница вглубь ============ */

.method__head { grid-column: 1 / 8; margin-bottom: clamp(2.6rem, calc(var(--vh, 1svh) * 7), 4.5rem); }
.method__head h2 { margin-top: 1.2rem; max-width: 16ch; }
.levels { grid-column: 1 / -1; }

.level {
  display: grid;
  grid-template-columns: 3.2rem 1fr;
  gap: clamp(1rem, 3vw, 2.6rem);
  padding: clamp(1.6rem, calc(var(--vh, 1svh) * 3.4), 2.4rem) 0;
  border-top: 1px solid var(--hair);
  transition: border-color 0.6s var(--ease);
}
.level:last-child { border-bottom: 1px solid var(--hair); }

/* каждый следующий уровень глубже — форма повторяет смысл */
.level:nth-child(2) { padding-left: clamp(0rem, 3vw, 3.5rem); }
.level:nth-child(3) { padding-left: clamp(0rem, 6vw, 7rem); }
.level:nth-child(4) { padding-left: clamp(0rem, 9vw, 10.5rem); }

.level h3 { margin-bottom: 0.5rem; }
.level p { color: var(--bone-mid); font-size: var(--t-small); max-width: 52ch; }

.method__note { grid-column: 6 / -1; margin-top: clamp(2.4rem, calc(var(--vh, 1svh) * 6), 4rem); }

/* ============ VI. Сферы ============ */

.spheres__head { grid-column: 1 / 9; margin-bottom: clamp(2.6rem, calc(var(--vh, 1svh) * 7), 4.5rem); }
.spheres__head h2 { margin-top: 1.2rem; }
.spheres__list { grid-column: 1 / -1; }

.sphere {
  display: grid;
  grid-template-columns: minmax(9rem, 2fr) 6fr;
  gap: clamp(1rem, 3vw, 3rem);
  padding: clamp(1.9rem, calc(var(--vh, 1svh) * 4), 2.8rem) 0;
  border-top: 1px solid var(--hair);
}
.sphere:last-child { border-bottom: 1px solid var(--hair); }
.sphere h3 {
  font-size: clamp(1.4rem, 1rem + 1.3vw, 2.05rem);
  color: var(--champ-hi);
}
.sphere__now, .sphere__after { font-size: var(--t-small); }
.sphere__now { color: var(--bone-dim); }
.sphere__after { color: var(--bone); }
.sphere__now .label, .sphere__after .label { margin-bottom: 0.7rem; }

/* Итог сферы читают в последнюю очередь и часто на телефоне,
   поэтому набран не «примечанием», а полноценным текстом. */
.spheres__note {
  grid-column: 5 / -1;
  margin-top: clamp(2rem, calc(var(--vh, 1svh) * 5), 3rem);
  font-size: clamp(1.02rem, 0.95rem + 0.35vw, 1.22rem);
  line-height: 1.55;
  color: var(--bone);
}

/* =========================================================
   КАРУСЕЛЬ
   Листается пальцем, колесом и стрелками. Никакой автопрокрутки:
   она уводила страницу и её нельзя было остановить.
   ========================================================= */

/* min-width: 0 обязателен: без него ячейка сетки растягивается под
   суммарную ширину слайдов, и страница уезжает вбок */
.carousel { position: relative; min-width: 0; }

.carousel__track {
  display: flex;
  gap: clamp(1rem, 2.5vw, 2rem);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  /* дойдя до края, не отдаём жест странице — иначе прокрутка прыгает */
  overscroll-behavior-x: contain;
  /* карусель не должна участвовать в привязке прокрутки страницы */
  overflow-anchor: none;
  scrollbar-width: none;
  -ms-overflow-style: none;
  padding-bottom: 0.4rem;
}
.carousel__track::-webkit-scrollbar { display: none; }

.carousel .slide {
  flex: 0 0 100%;
  scroll-snap-align: start;
  min-width: 0;
}

/* пара «сейчас → после» внутри слайда */
.sphere .slide {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(1rem, 3vw, 3rem);
  align-content: start;
}

/* ---- точки и стрелки ---- */

.carousel__bar {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  margin-top: 1.1rem;
}

/* min-width и overflow — страховка: указатель не должен уметь
   раздвинуть раскладку, сколько бы отзывов туда ни положили */
.carousel__dots {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex: 1;
  min-width: 0;
  overflow: hidden;
}

.carousel__count {
  font-family: var(--ui);
  font-size: var(--t-label);
  letter-spacing: 0.22em;
  color: var(--bone-dim);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Точки были 22x2 px цветом rgba(240,232,218,0.14) — на тёмном фоне их
   попросту не видно, и человек не догадывался, что под «Деньгами» есть
   ещё четыре истории. Теперь линия заметная, а нажимать можно по площади
   44 px: сама точка тонкая, поле вокруг — нет. */
.carousel__dot {
  /* Ширина под палец — 34, видимая черта осталась 26: поле разведено
     внутрь псевдоэлементом. Точки стоят вплотную, так что мёртвых зон
     между ними нет — промахнуться можно только мимо всего ряда. */
  width: 34px; height: 44px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  position: relative;
}
.carousel__dot::before {
  content: "";
  position: absolute;
  left: 4px; right: 4px; top: 50%;
  height: 3px;
  margin-top: -1.5px;
  background: rgba(240, 232, 218, 0.38);
  transition: background 0.4s var(--ease), height 0.3s var(--ease);
}
.carousel__dot[aria-current="true"]::before {
  background: var(--champ);
  height: 4px;
  margin-top: -2px;
}

/* 34x34 меньше нормы касания: палец мажет. */
.carousel__arrow {
  width: 44px; height: 44px;
  display: grid;
  place-content: center;
  border: 1px solid var(--hair-warm);
  background: transparent;
  color: var(--champ-hi);
  cursor: pointer;
  font-size: 0.9rem;
  line-height: 1;
  transition: background 0.4s var(--ease), opacity 0.4s var(--ease);
}
.carousel__arrow:hover { background: rgba(216, 185, 143, 0.14); }
/* 0.25 читалось как украшение, а не как «дальше некуда» */
.carousel__arrow[disabled] { opacity: 0.4; cursor: default; }

/* ---- лента отзывов ---- */

/* Отзывы показываем скриншотами, как в актуальных в инстаграме:
   так видно, что это живая переписка, а не набранный нами текст.
   Высота у карточек разная — выравниваем по верху. */
.carousel--reviews .carousel__track { align-items: start; }

/* Ширину задаём через .carousel--reviews .slide, а не через .shot:
   у общего правила .carousel .slide специфичность выше, и одиночный
   класс им перебивался — скрины растягивались на всю ленту. */
.carousel--reviews .slide {
  flex: 0 0 clamp(240px, 26vw, 320px);
}

.shot {
  overflow: hidden;
  border: 1px solid var(--hair);
  background: var(--deep);
  transition: border-color 0.45s var(--ease);
}
.shot:hover { border-color: var(--hair-warm); }

/* Все карточки отзывов приведены к одному формату 4:5 ещё при сборке:
   скриншоты пришли с соотношением от 2.33 до 0.46, и лента из-за этого
   выглядела рваной. Теперь каждая вписана в общее поле на подложке цвета
   сайта и прогрета в тёплый тон, поэтому здесь достаточно держать формат —
   ни обрезки, ни ограничения высоты больше не нужно. */
.shot img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
}
.carousel--reviews cite {
  display: block;
  margin-top: 1rem;
  font-style: normal;
  font-size: var(--t-label);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--bone-dim);
}

/* ============ VII. Сканирование ============ */

.scan__body { grid-column: 4 / 11; }
.scan h2 { margin: 1.4rem 0 1.8rem; }
.scan p { max-width: 50ch; }

/* ============ VIII. Семь дней ============ */

.week {
  overflow: hidden;
  min-height: calc(var(--vh, 1svh) * 175);
  display: grid;
  align-content: center;
}
.week__head { margin-bottom: clamp(2.6rem, calc(var(--vh, 1svh) * 7), 4.5rem); }
.week__head h2 { margin-top: 1.2rem; }

.week__viewport {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 4%, #000 94%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 4%, #000 94%, transparent 100%);
}
.week__track {
  display: flex;
  gap: clamp(1.6rem, 3.5vw, 3.2rem);
  width: max-content;
  padding-inline: var(--gut);
  will-change: transform;
}

/* карточка без рамки: только линия сверху — как строка в расписании */
.day {
  width: clamp(230px, 24vw, 300px);
  flex: 0 0 auto;
  padding-top: 1.1rem;
  border-top: 1px solid var(--hair);
  transition: border-color 0.5s var(--ease);
}
.day__num {
  font-family: var(--display);
  font-size: clamp(2.2rem, 1.6rem + 1.4vw, 3.1rem);
  line-height: 1;
  color: var(--champ);
  letter-spacing: -0.02em;
}
.day h3 { margin: 0.9rem 0 0.6rem; }
.day p { font-size: var(--t-small); color: var(--bone-mid); }

.week__progress { position: relative; height: 1px; margin: clamp(2rem, calc(var(--vh, 1svh) * 5), 3rem) var(--gut) 0; background: var(--hair); }
.week__progress i { position: absolute; inset: 0 auto 0 0; width: 14%; background: var(--champ); transform-origin: left; }

/* ============ IX. Яхта ============ */

.yacht__head { grid-column: 1 / 9; margin-bottom: clamp(2.6rem, calc(var(--vh, 1svh) * 7), 4.5rem); }
.yacht__head h2 { margin-top: 1.2rem; }
.decks { grid-column: 1 / -1; }

.deck {
  display: grid;
  grid-template-columns: 4rem 1fr;
  gap: clamp(1rem, 3vw, 2.4rem);
  padding: clamp(1.5rem, calc(var(--vh, 1svh) * 3.2), 2.1rem) 0;
  border-top: 1px solid var(--hair);
  align-items: start;
}
.deck:last-child { border-bottom: 1px solid var(--hair); }
.deck p { color: var(--bone-mid); font-size: var(--t-small); margin-top: 0.4rem; max-width: 54ch; }

/* цифры как графика */
.stats {
  grid-column: 1 / -1;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: clamp(1.6rem, 3.5vw, 3rem);
  margin-top: clamp(3rem, calc(var(--vh, 1svh) * 7), 4.5rem);
}
.stats b {
  display: block;
  font-family: var(--display);
  font-weight: 400;
  /* см. .facts b: характеристики яхты уступают доводам о цене */
  font-size: clamp(1.9rem, 1.2rem + 1.7vw, 2.9rem);
  line-height: 0.95;
  color: var(--champ-hi);
  letter-spacing: -0.03em;
  margin-bottom: 0.7rem;
}
.stats span { font-size: var(--t-label); letter-spacing: 0.26em; text-transform: uppercase; color: var(--bone-dim); }

/* ============ X. Сессии ============ */

.sessions__body { grid-column: 3 / 11; }
.sessions h2 { margin: 1.4rem 0 1.8rem; }
.sessions p { max-width: 52ch; }

/* ============ XI. Цена — свёрстана как счёт ============ */

.price__head { margin-bottom: clamp(2.4rem, calc(var(--vh, 1svh) * 6), 4rem); }
.price__head h2 { margin-top: 1.2rem; }

.bill__row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: 1rem;
  padding: 1.15rem 0;
  border-top: 1px solid var(--hair);
}
.bill__row span { font-size: var(--t-small); color: var(--bone-dim); }
.bill__row b {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.15rem, 1rem + 0.7vw, 1.5rem);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* ---- Итог сметы: он же зачёркнутая цена ----
   Раньше блок оставлял человека с вопросом «так миллион или девять
   восемьсот?»: девять восемьсот стояли и здесь, и ещё раз зачёркнутыми
   над ценой. Одна и та же сумма дважды на экране, и обе выглядят как
   ответ на вопрос «сколько платить».

   Теперь сумма одна и зачёркнута прямо здесь. Дальше по странице
   остаётся ровно одно незачёркнутое число — то, которое и надо платить.
   Тем же приёмом работает ценник в магазине, и читается он без
   объяснений. */
.bill__row.is-total { border-top-color: var(--hair-warm); }
.bill__row.is-total span { color: var(--bone); }
.bill__row.is-total b {
  color: var(--bone-mid);
  text-decoration: line-through;
  /* линия идёт золотом: она здесь не «ошибка», а обещание скидки */
  text-decoration-color: rgba(216, 185, 143, 0.85);
  text-decoration-thickness: 2px;
  text-underline-offset: 0;
}

/* Итоговая цифра — единственное место на странице, где свет исходит
   от самого типа. Ореол лежит под цифрой отдельным слоем, чтобы не
   размывать её контур тенью.

   Отступ сверху небольшой: весь смысл блока — обрыв от зачёркнутых девяти
   восьмисот к миллиону, и оба числа обязаны попадать в один взгляд. */
.price__final { position: relative; margin-top: clamp(2.2rem, calc(var(--vh, 1svh) * 5), 3.4rem); }

/* ---- Зачёркнутая ценность ----
   Она должна весить заметно: это половина всего приёма. Но остаться явно
   меньше цены — иначе снова непонятно, какое из двух чисел платить.
   Держим примерно половину роста миллиона: разница в размере читается
   мгновенно и без подписей. */
.price__was {
  font-family: var(--display);
  font-size: clamp(1.9rem, 1rem + 3.4vw, 3.4rem);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--bone-mid);
  font-variant-numeric: tabular-nums;
  text-decoration: line-through;
  /* линия идёт золотом: это обещание скидки, а не помарка */
  text-decoration-color: rgba(216, 185, 143, 0.9);
  text-decoration-thickness: 3px;
  margin: 0.9rem 0 0.55rem;
}
.price__final::before {
  content: "";
  position: absolute;
  left: -12%; top: 10%;
  width: 90%; aspect-ratio: 2 / 1;
  background: radial-gradient(50% 50% at 50% 50%,
    rgba(216, 185, 143, 0.20) 0%,
    rgba(216, 185, 143, 0.07) 45%,
    transparent 72%);
  opacity: 0;
  transition: opacity 1.6s var(--slow) 0.4s;
  pointer-events: none;
}
.price__final.is-in::before { opacity: 1; }

/* Кегль убавлен: на 8rem миллион занимал экран целиком и упирался в края,
   а рядом с зачёркнутой ценностью такая громада забирала весь блок себе.
   Число и так самое крупное на странице — кричать ему больше не нужно. */
.price__big {
  position: relative;
  font-family: var(--display);
  font-size: clamp(2.9rem, 1.4rem + 5.6vw, 5.6rem);
  line-height: 0.94;
  letter-spacing: -0.035em;
  color: var(--champ-hi);
  margin: 0 0 1.4rem;
  font-variant-numeric: tabular-nums;
}
/* Единственное объяснение, почему миллион — это недорого, было набрано
   самым мелким и тусклым кеглем на экране. Ставим его в полный голос. */
.price__final .note {
  max-width: 34ch;
  margin-bottom: 2.6rem;
  font-size: clamp(1.08rem, 1rem + 0.5vw, 1.32rem);
  color: var(--bone-mid);
}

/* ---- Счётчик: цифра растёт, коробка стоит ----
   Двойник с финальной суммой задаёт размер и остаётся невидимым; живое
   значение лежит в той же ячейке сетки поверх него. Раскладка страницы
   при этом не пересчитывается ни разу за анимацию. */
.count { display: inline-grid; }
.count > * { grid-area: 1 / 1; white-space: nowrap; }
.count__sizer { visibility: hidden; }

/* цифра не должна тянуть за собой пересчёт соседей */
.price__big { contain: layout; }

/* ============ XII. Включено ============ */

.included__head { grid-column: 1 / 6; }
.included__head h2 { margin-top: 1.2rem; }
.included__cols {
  grid-column: 6 / -1;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 0 clamp(1.6rem, 4vw, 3.5rem);
}
.included__cols ul { list-style: none; }
.included__cols li {
  font-size: var(--t-small);
  color: var(--bone-mid);
  padding: 0.85rem 0;
  border-top: 1px solid var(--hair);
}

/* ============ XIII. Отзывы ============ */

.reviews__head h2 { margin-top: 1.2rem; }
.reviews__flag {
  display: inline-block;
  margin-top: 1.1rem;
  padding-bottom: 0.3rem;
  border-bottom: 1px dashed var(--hair-warm);
  font-size: var(--t-label);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--bone-dim);
}

.reviews .carousel { margin-top: clamp(2.4rem, calc(var(--vh, 1svh) * 6), 3.5rem); }

/* ============ XIV. Вопросы ============ */

.faq__head { grid-column: 1 / 5; }
.faq__head h2 { margin-top: 1.2rem; }
.faq__list { grid-column: 6 / -1; }

.faq details { border-top: 1px solid var(--hair); }
.faq details:last-of-type { border-bottom: 1px solid var(--hair); }
.faq summary {
  position: relative;
  padding: 1.35rem 2.6rem 1.35rem 0;
  font-family: var(--display);
  font-size: clamp(1.05rem, 0.98rem + 0.42vw, 1.3rem);
  cursor: pointer;
  list-style: none;
  transition: color 0.4s var(--ease);
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after,
.faq summary::before {
  content: "";
  position: absolute;
  right: 0.3rem; top: 50%;
  width: 12px; height: 1px;
  background: var(--champ);
}
.faq summary::before { transform: rotate(90deg); transition: opacity 0.4s var(--ease), transform 0.4s var(--ease); }
.faq details[open] summary::before { opacity: 0; transform: rotate(0); }
.faq summary:hover { color: var(--champ-hi); }
.faq details p { padding-bottom: 1.6rem; max-width: 58ch; font-size: var(--t-small); color: var(--bone-mid); }

/* ============ XV. Бронь ============ */

.booking__body { grid-column: 3 / 11; }
.booking h2 { margin: 1.4rem 0 clamp(2.2rem, calc(var(--vh, 1svh) * 5), 3.2rem); }

/* Зазор нужен с тех пор, как у полей появилась рамка по кругу: вплотную
   две рамки давали двойную линию между полями. Отступ у блока мессенджеров
   свой, он задан ниже и этот зазор перекрывает. */
.book-form { display: grid; gap: 0.7rem; max-width: 560px; }
.book-form input {
  padding: 1.1rem 0;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--hair);
  color: var(--bone);
  font-family: var(--ui);
  font-size: 1.05rem;
  font-weight: 300;
  transition: border-color 0.4s var(--ease);
}
.book-form input::placeholder { color: var(--bone-dim); }
.book-form input:focus { outline: none; border-bottom-color: var(--champ); }

/* ---- Поля ----
   Поле должно быть видно, что оно поле. Волосяная линия в 14 % задумывалась
   как сдержанность, но под ней идёт фильм: на светлом кадре линия исчезает
   совсем, и форма — единственный способ связи на всей странице — перестаёт
   читаться как форма.

   Первая попытка дала серую заливку без рамки, и поля повисли на фотографии
   чужими прямоугольниками. Правильный образец стоял рядом всё это время:
   чипы мессенджеров в .choice — волосяная рамка по кругу и лёгкая подложка.
   Поля повторяют их один в один, только крупнее. Тогда форма выглядит
   набором из одной семьи, а не деталями от разных страниц. */
.book-form input:not([type="radio"]) {
  box-sizing: border-box;
  padding: 1.15rem 1.15rem;
  background: rgba(3, 7, 15, 0.34);
  border: 1px solid var(--hair-warm);
  /* низ чуть плотнее: строка ввода читается как строка, а не как рамка */
  border-bottom-color: rgba(240, 232, 218, 0.3);
  backdrop-filter: blur(6px);
  transition: border-color 0.4s var(--ease), background 0.4s var(--ease);
}
/* Подсказка обязана отличаться от введённого текста, но остаться читаемой:
   на 0.5 она тонула в подложке, на полной яркости её путали с готовым
   ответом и не понимали, что поле пустое. */
.book-form input:not([type="radio"])::placeholder { color: rgba(240, 232, 218, 0.58); }
.book-form input:not([type="radio"]):hover { border-color: rgba(240, 232, 218, 0.3); }
.book-form input:not([type="radio"]):focus {
  background: rgba(3, 7, 15, 0.58);
  border-color: var(--champ);
}

/* Автозаполнение браузера заливает поле собственным белым фоном и красит
   текст в чёрный — на тёмной странице это выглядит как вставка с чужого
   сайта. Свой цвет фона браузер не отдаёт, зато внутренняя тень его
   перекрывает целиком; текст перекрашивается своим свойством.
   Бесконечная задержка перехода держит фон тёмным и в момент подстановки. */
.book-form input:-webkit-autofill,
.book-form input:-webkit-autofill:hover,
.book-form input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--bone);
  -webkit-box-shadow: inset 0 0 0 100vw rgba(5, 10, 19, 0.92);
  caret-color: var(--bone);
  transition: background-color 100000s, color 100000s;
}

.book-form .btn { margin-top: 2.2rem; justify-self: start; }
.book-form.is-sent .btn { pointer-events: none; }

.booking__note { margin-top: 1.6rem; }

.booking__urge {
  margin-bottom: clamp(2rem, calc(var(--vh, 1svh) * 5), 3rem);
  font-size: clamp(1.05rem, 0.97rem + 0.4vw, 1.28rem);
  color: var(--champ-hi);
}

/* ---- выбор мессенджера ---- */

.book-form__where {
  margin-top: 1.8rem;
  padding: 0;
  border: 0;
}
.book-form__where .label { margin-bottom: 0.9rem; }

.choice { display: flex; flex-wrap: wrap; gap: 0.7rem; }

.choice label {
  position: relative;
  cursor: pointer;
}
/* сам радиопереключатель убираем, но оставляем доступным с клавиатуры */
.choice input {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}
.choice span {
  display: block;
  padding: 0.75rem 1.4rem;
  border: 1px solid var(--hair);
  font-size: var(--t-small);
  color: var(--bone-mid);
  transition: border-color 0.4s var(--ease), color 0.4s var(--ease), background 0.4s var(--ease);
}
.choice label:hover span { border-color: var(--hair-warm); color: var(--bone); }
.choice input:checked + span {
  border-color: var(--champ);
  color: var(--champ-hi);
  background: rgba(216, 185, 143, 0.12);
}
.choice input:focus-visible + span { outline: 2px solid var(--champ-hi); outline-offset: 3px; }

.footer {
  position: relative;
  z-index: var(--z-page);
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 1rem;
  padding: 2.8rem var(--gut);
  background: var(--abyss);
  border-top: 1px solid var(--hair);
}

/* ============ Волосяная линия, которая загорается от руки ============

   Все перечисления на странице свёрстаны строками с линией сверху.
   Красить саму границу при наведении — резко: цвет щёлкает целиком.
   Вместо этого поверх границы кладём золотую линию и раскрываем её
   от левого края. Получается жест, а не подсветка. */

.facts li,
.level,
.sphere,
.deck,
.day,
.included__cols li,
.bill__row,
.faq details { position: relative; }

.facts li::before,
.level::before,
.sphere::before,
.deck::before,
.day::before,
.included__cols li::before,
.bill__row::before,
.faq details::before {
  content: "";
  position: absolute;
  top: -1px; left: 0; right: 0;
  height: 1px;
  background: linear-gradient(90deg, var(--champ) 0%, var(--hair-warm) 100%);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.7s var(--ease);
  pointer-events: none;
}

.facts li:hover::before,
.level:hover::before,
.sphere:hover::before,
.deck:hover::before,
.day:hover::before,
.included__cols li:hover::before,
.bill__row:hover::before,
.faq details[open]::before,
.faq details:hover::before { transform: scaleX(1); }

/* ============ Появление ============ */

.reveal { opacity: 0; transform: translateY(24px); transition: opacity 1.1s var(--ease), transform 1.1s var(--ease); }
.reveal.is-in { opacity: 1; transform: none; }

/* если внутри блока заголовок и так поднимается по словам, второе
   движение в ту же сторону читается как задержка — оставляем только свет */
.reveal:has(.rise) { transform: none; }

.stagger > * { opacity: 0; transform: translateY(18px); transition: opacity 0.9s var(--ease), transform 0.9s var(--ease); }
.stagger.is-in > * { opacity: 1; transform: none; }
.stagger.is-in > *:nth-child(1) { transition-delay: 0.05s; }
.stagger.is-in > *:nth-child(2) { transition-delay: 0.14s; }
.stagger.is-in > *:nth-child(3) { transition-delay: 0.23s; }
.stagger.is-in > *:nth-child(4) { transition-delay: 0.32s; }
.stagger.is-in > *:nth-child(5) { transition-delay: 0.41s; }
.stagger.is-in > *:nth-child(6) { transition-delay: 0.50s; }

/* ============ Адаптив ============ */

@media (max-width: 1000px) {
  .master__photo, .master__body,
  .method__head, .levels, .method__note,
  .spheres__head, .spheres__list, .spheres__note,
  .yacht__head, .decks, .stats,
  .included__head, .included__cols,
  .faq__head, .faq__list,
  .scan__body, .sessions__body, .booking__body,
  .manifest__text { grid-column: 1 / -1; }

  .master__photo { max-width: 330px; }
  .sphere { grid-template-columns: 1fr; gap: 1.1rem; }
  .sphere .slide { grid-template-columns: 1fr; gap: 1.1rem; }
  .sphere__after { padding-top: 1.1rem; border-top: 1px solid var(--hair); }
  .level:nth-child(2), .level:nth-child(3), .level:nth-child(4) { padding-left: 0; }
  .hero__lower { grid-template-columns: 1fr; }
  .hero__offer { align-items: flex-start; }
  .included__cols { margin-top: 2rem; }
  .faq__list, .method__note, .spheres__note { margin-top: 2rem; }
}

/* На узком экране горизонтальная лента не работает: карточка шире окна
   не помещается, текст обрезается по краю, а листать вбок внутри
   вертикальной страницы неудобно. Разворачиваем дни в обычный столбец —
   всё читается сразу, ничего не срезано. */
@media (max-width: 900px) {
  .week { min-height: 0; }
  .week__viewport {
    overflow: visible;
    -webkit-mask-image: none;
    mask-image: none;
  }
  .week__track {
    display: grid;
    grid-template-columns: 1fr;
    gap: 0;
    width: auto;
    transform: none !important;
    /* трансформа здесь нет — снимаем и заявку на отдельный слой,
       иначе композитор до конца жизни страницы держит текстуру под
       колонкой из семи карточек, которая никогда не двигается */
    will-change: auto;
    /* лента лежит вне .wrap, поэтому поля держит сама */
    padding-inline: var(--gut);
  }
  .day {
    width: auto;
    padding: 1.3rem 0 1.5rem;
  }
  .week__progress { display: none; }
}

/* Переходы на телефоне вдвое короче.

   Переходная секция — чистый кадр без единой буквы. На большом экране
   это пауза между главами, и она читается как замысел. На телефоне шесть
   таких секций по calc(var(--vh, 1svh) * 105) давали шесть с лишним экранов подряд, где
   листаешь — и текста просто нет: почти четверть всей страницы. Именно
   это заказчик и увидел как «текста вообще нету».

   Фильм за это время проходит ровно столько же — просто быстрее, а
   кадровому движку скорость ничего не стоит. */
@media (max-width: 700px) {
  .chapter.is-move {
    min-height: calc(var(--move-h, 140) * var(--vh, 1svh));
  }
}

@media (max-width: 620px) {
  .deck { grid-template-columns: 2.6rem 1fr; }
  .facts li { grid-template-columns: 1fr; gap: 0.35rem; }
  .bill__row { grid-template-columns: 1fr; gap: 0.25rem; }
  .manifest__lines span + span { padding-left: 1.2rem; }
  .depth em { padding-left: 1.2rem; }
  .mark { top: clamp(4.5rem, calc(var(--vh, 1svh) * 10), 6rem); }
  /* судовая подпись длиннее экрана — разрешаем перенос */
  .mark .slate { white-space: normal; }

  /* На ПЕРВОМ экране подписи у маркера нет: справа от неё переключатель
     языка, слева — начало первой строки текста, и в оставшуюся ширину она
     переносится на три строки и наезжает на подпись под заголовком.
     Терять нечего: «Мальдивы · 41 м» стоит и на заставке, и в первом же
     предложении под заголовком. Остаётся цифра главы и линейка. */
  .hero .mark .slate { display: none; }
}

/* ============ Появление на телефоне ============

   Весь текст страницы по умолчанию невидим (opacity: 0) и проявляется
   больше секунды: .reveal — 1.1 с, слова заголовка — 1.15 с плюс до 0.5 с
   построчной задержки. На мониторе это работает: страницу листают
   постепенно, блок успевает проявиться, пока доходишь до него глазами.

   На телефоне листают броском. Секция влетает в экран и вылетает из него
   быстрее, чем заканчивается проявление, — и человек видит пустое место
   там, где должен быть текст. Ровно это и означает «листаю, а текста
   вообще нету»: текст на странице есть, но он не успевает появиться.

   Поэтому на пальцевых экранах проявление короткое, ступенчатых задержек
   нет, а заголовки не набираются по словам — они просто есть. */
@media (pointer: coarse) {
  .reveal {
    transform: none;
    transition: opacity 0.32s var(--ease);
  }
  .stagger > * {
    transform: none;
    transition: opacity 0.28s var(--ease);
  }
  .stagger.is-in > * { transition-delay: 0s; }

  .is-split .rise .w > i { transform: none; transition: none; }
  .is-split .rise .w { overflow: visible; }
}

@media (prefers-reduced-motion: reduce) {
  .reveal, .stagger > * { opacity: 1; transform: none; transition: none; }
  .hint__line { animation: none; }
}

/* ГОЛЫЙ РЕЖИМ — что видно, если скрипты не поднялись.

   Ставится сторожем из <head> (см. index.html) через восемь секунд, если
   наблюдатель за появлением блоков так и не встал. Без него страница
   осталась бы пустой: весь текст невидим по умолчанию.

   Важнее восклицательных знаков здесь ничего нет — эти правила обязаны
   победить и появление, и маску слов заголовка, в каком бы состоянии их
   ни застала поломка. */
.is-bare .reveal,
.is-bare .stagger > *,
.is-bare .rise,
.is-bare .rise .w > i {
  opacity: 1 !important;
  transform: none !important;
  transition: none !important;
}
.is-bare .rise .w { overflow: visible !important; }

/* ============ Промо-ролик ============

   Переделано целиком. Было: огромный заголовок и маленькое видео в узкой
   колонке, вокруг пустой экран, кадр ни с чем не связан.

   Стало: заголовок и хронометраж стоят на одной линейке, как титр перед
   показом, а сам кадр занимает всю ширину полосы. По углам — метки
   видоискателя: тот же волосяной язык, что и во всей вёрстке, но здесь он
   объясняет, что перед тобой именно плёнка.

   Ролик не качается, пока на него не нажали: в разметке лежит только
   кадр-постер, <video> создаётся по клику. */

.promo__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 2rem;
  padding-bottom: clamp(1.2rem, 2.5vw, 1.8rem);
  border-bottom: 1px solid var(--hair);
}
.promo__head h2 { margin-top: 1.1rem; max-width: 18ch; }
.promo__meta { white-space: nowrap; color: var(--champ); }

.promo__frame {
  position: relative;
  margin-top: clamp(1.8rem, calc(var(--vh, 1svh) * 4), 3rem);
  background: var(--deep);
  overflow: hidden;
  cursor: pointer;
}

.promo__frame img,
.promo__frame video {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

/* НАЕЗД ПРИ НАВЕДЕНИИ — ТОЛЬКО ТАМ, ГДЕ ЕСТЬ НАВЕДЕНИЕ.

   Раньше и переход, и сам эффект стояли безусловно. На тач-экране
   наведения не бывает, но браузер всё равно держит на трёх картинках
   1280x720 живой transition по transform и filter — и на первом же
   касании андроид засчитывает hover, запуская восьмисотмиллисекундную
   анимацию яркости сразу на всех. Ровно там прокрутка и спотыкалась:
   заказчик описал это как «два-три кадра лагает, а ниже опять плавно».

   Здесь картинка просто лежит и ничего не пересчитывает. */
@media (hover: hover) {
  .promo__frame img {
    transition: transform 1.2s var(--ease), filter 0.8s var(--ease);
  }
  .promo__frame:hover img { transform: scale(1.03); filter: brightness(1.06); }
}

/* Когда ролик запущен, постер обязан УЙТИ: иначе видео встаёт под ним и
   блок раздваивается — сверху неподвижный кадр, снизу играющее видео. */
.promo__frame.is-playing .promo__poster { display: none; }
.promo__frame.is-playing { cursor: default; }
.promo__frame.is-playing .promo__corner { opacity: 0; }

/* Тень собирает кадр к центру, чтобы кнопка читалась на любом сюжете.

   Было 30 % в центре и 62 % по краям — замер показал, что постеры теряли
   от этого половину яркости: дневной кадр с яхтой из 156 превращался в 79,
   то есть в мутное пятно. Кнопке столько и не нужно: у кольца своя заливка,
   золотая рамка и ореол, оно держится само. Оставлено ровно столько тени,
   чтобы кадр собирался к центру, а не гас. */
.promo__frame::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(58% 58% at 50% 50%,
    rgba(2, 5, 11, 0.14) 0%, rgba(2, 5, 11, 0.38) 100%);
  pointer-events: none;
  transition: opacity 0.6s var(--ease);
}
.promo__frame.is-playing::after { opacity: 0; }

/* ---- метки видоискателя по углам ---- */
.promo__corner {
  position: absolute;
  width: clamp(1.4rem, 3vw, 2.2rem);
  height: clamp(1.4rem, 3vw, 2.2rem);
  pointer-events: none;
  opacity: 0.55;
  transition: opacity 0.6s var(--ease), inset 0.6s var(--ease);
}
.promo__corner--tl { top: 1rem; left: 1rem; border-top: 1px solid var(--champ); border-left: 1px solid var(--champ); }
.promo__corner--tr { top: 1rem; right: 1rem; border-top: 1px solid var(--champ); border-right: 1px solid var(--champ); }
.promo__corner--bl { bottom: 1rem; left: 1rem; border-bottom: 1px solid var(--champ); border-left: 1px solid var(--champ); }
.promo__corner--br { bottom: 1rem; right: 1rem; border-bottom: 1px solid var(--champ); border-right: 1px solid var(--champ); }
.promo__frame:hover .promo__corner { opacity: 1; }

.promo__play {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  border: 0;
  background: none;
  cursor: pointer;
  padding: 0;
}
.promo__frame.is-playing .promo__play { display: none; }

/* Кольцо — единственная подсказка «сюда можно нажать», поэтому оно
   заметнее прочих волосяных линий: своя заливка и мягкий ореол. */
.promo__ring {
  grid-area: 1 / 1;
  width: clamp(5rem, 10vw, 7.2rem);
  aspect-ratio: 1;
  border: 1px solid var(--champ);
  border-radius: 50%;
  background: rgba(3, 7, 15, 0.44);
  box-shadow: 0 0 0 1px rgba(216, 185, 143, 0.16),
              0 0 0 14px rgba(216, 185, 143, 0.05),
              0 12px 48px rgba(2, 5, 11, 0.55);
  transition: transform 0.7s var(--ease), background 0.5s var(--ease),
              box-shadow 0.6s var(--ease);
}
.promo__frame:hover .promo__ring,
.promo__play:focus-visible .promo__ring {
  transform: scale(1.09);
  background: rgba(216, 185, 143, 0.22);
  box-shadow: 0 0 0 1px rgba(216, 185, 143, 0.4),
              0 0 0 22px rgba(216, 185, 143, 0.07),
              0 12px 52px rgba(2, 5, 11, 0.6);
}

/* треугольник собран из бордюров: ни картинки, ни шрифта не нужно */
.promo__tri {
  grid-area: 1 / 1;
  width: 0; height: 0;
  margin-left: 0.36em;
  border-left: 1.05rem solid var(--champ-hi);
  border-top: 0.68rem solid transparent;
  border-bottom: 0.68rem solid transparent;
}

.promo__note { margin-top: 1.1rem; }

/* ---- Карусель роликов ----

   Кадр внутри карусели уже не первый элемент блока, а слайд: верхний
   отступ ему больше не нужен, иначе он оттолкнёт сам себя от заголовка
   дважды. Тень видоискателя и углы остаются как были. */
.carousel--promo { margin-top: clamp(1.8rem, calc(var(--vh, 1svh) * 4), 3rem); }
.carousel--promo .promo__frame { margin-top: 0; }

/* Полоса с точками и стрелками у роликов прижата вправо: слева под ней
   идёт подпись «со звуком», и по центру они сталкивались. */
.carousel--promo .carousel__bar { justify-content: flex-end; }

/* ---- Подкаст ----

   Второй ярус блока: сверху короткие ролики, ниже часовой разговор.
   Отделён воздухом и волосяной линией — это другое обещание по времени,
   и человек должен видеть, что переключился жанр. */
.podcast {
  margin-top: clamp(3rem, calc(var(--vh, 1svh) * 8), 5.5rem);
  padding-top: clamp(2.2rem, calc(var(--vh, 1svh) * 5), 3.4rem);
  border-top: 1px solid var(--hair);
}

.podcast__head { margin-bottom: clamp(1.4rem, calc(var(--vh, 1svh) * 3), 2.2rem); }

.podcast__title {
  margin-top: 0.9rem;
  font-family: var(--display);
  font-size: var(--t-h3);
  line-height: 1.25;
  max-width: 24ch;
  color: var(--bone);
}

.podcast__frame { margin-top: 0; }

/* ОБЛОЖКА ПОДКАСТА НЕ ЗАТЕМНЯЕТСЯ ПО ВСЕМУ КАДРУ.

   У .promo__frame поверх картинки лежит виньетка от 30 % в центре до 62 %
   по краям. Для фотографии это правильно: кадр случайный, и кнопка обязана
   читаться на любом сюжете. Но обложка подкаста — не случайный кадр. Она
   нарисована, чтобы её рассматривали: там заголовок, лица и цвет, и
   виньетка съедает ровно то, ради чего её делали. На странице это выглядит
   как выцветшая картинка.

   Поэтому здесь виньетка превращается в пятно под самой кнопкой: тень
   остаётся там, где она работает, а обложка остаётся обложкой. */
.podcast__frame::after {
  background: radial-gradient(42% 42% at 50% 50%,
    rgba(2, 5, 11, 0.38) 0%,
    rgba(2, 5, 11, 0.22) 46%,
    rgba(2, 5, 11, 0) 72%);
}

/* Кольцу добавлена плотность взамен ушедшего затемнения — оно теперь
   держится на светлом кадре само. */
.podcast__frame .promo__ring { background: rgba(3, 7, 15, 0.58); }

.podcast__caption {
  margin-top: 1.2rem;
  max-width: 46ch;
  font-size: var(--t-small);
  color: var(--bone-mid);
}

/* Плеер ютуба приезжает по нажатию и занимает весь кадр. */
.podcast__frame iframe {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 0;
}

@media (max-width: 700px) {
  .promo__head { display: block; }
  .promo__meta { display: block; margin-top: 0.9rem; }
  .promo__frame { margin-top: 1.4rem; }
  .promo__corner { width: 1.1rem; height: 1.1rem; }
  .promo__corner--tl, .promo__corner--tr { top: 0.6rem; }
  .promo__corner--bl, .promo__corner--br { bottom: 0.6rem; }
  .promo__corner--tl, .promo__corner--bl { left: 0.6rem; }
  .promo__corner--tr, .promo__corner--br { right: 0.6rem; }
}

/* ---- Ответ формы ----
   Строка под кнопкой говорит правду о том, что произошло с заявкой:
   отправляем, принято или не дошло. Ложного «отправлено» больше нет. */
.book-form__say {
  margin-top: 1rem;
  font-size: var(--t-small);
  line-height: 1.5;
  min-height: 1.5em;              /* место занято заранее — кнопка не прыгает */
  color: var(--bone-dim);
  transition: color 0.4s var(--ease);
}
/* Секция брони — «кадр», а там действует .chapter.is-beat p с цветом на
   весь текст. Поднимаем вес селектора, иначе строка ответа остаётся
   обычной и не отличает «принято» от «не дошло». */
.chapter .book-form__say { color: rgba(240, 232, 218, 0.7); }
.chapter .book-form.is-sent .book-form__say { color: #f2dcb8; }
.chapter .book-form.is-failed .book-form__say { color: #e8a794; }

/* ---- Отправлено ----
   Раньше подтверждение гасилось до 72% прозрачности, а сообщение под ним
   шло мелким и тусклым. Человек только что отдал номер телефона за
   миллион и не понимал, отправилось ли: заказчик описал это точно —
   «должна быть яркая, чтобы прямо вылезла, а не искать текст глазами».

   Теперь наоборот: кнопка наливается золотом в полную силу, а ответ
   становится отдельной панелью с рамкой. Гасим только промежуточное
   состояние «отправляем» — там приглушённость на месте, действие ещё
   не завершилось. */
.book-form.is-sending button[type=submit] { pointer-events: none; opacity: 0.72; }
.book-form.is-sent button[type=submit] {
  pointer-events: none;
  opacity: 1;
  background: var(--champ);
  border-color: var(--champ);
  color: var(--abyss);
}
.book-form.is-sent button[type=submit]::before { display: none; }

.book-form.is-sent .book-form__say,
.book-form.is-failed .book-form__say {
  margin-top: 1.4rem;
  padding: 1.1rem 1.2rem;
  font-size: clamp(1.02rem, 0.95rem + 0.4vw, 1.18rem);
  line-height: 1.45;
  border-left: 3px solid var(--champ);
  background: rgba(216, 185, 143, 0.1);
}
.book-form.is-failed .book-form__say { border-left-color: #e8a794; background: rgba(232, 167, 148, 0.1); }

/* =========================================================
   ТЕЛЕФОН: размеры, при которых страницей можно пользоваться
   Всё в этом блоке — по результатам замеров на живой странице.
   ========================================================= */

@media (max-width: 700px) {
  /* ОТЗЫВЫ. Замер: карточка выходила 240x300 px при исходнике 1000x1250 —
     уменьшение вчетверо. Семнадцать скриншотов переписки превращались в
     цветные пятна, а это единственное социальное доказательство на
     странице. Даём карточке почти всю ширину и оставляем край следующей,
     чтобы было видно: лента продолжается. */
  .carousel--reviews .slide { flex: 0 0 86%; }

  /* СФЕРЫ. Пять историй на блок, а видно было одну: следующая начиналась
     ровно за краем. Показываем её край — тогда понятно, что надо листать. */
  .carousel--spheres .slide { flex: 0 0 88%; }
}

/* Короткий экран: на 360x640 кнопка в герое уезжала на 54 px ниже сгиба, а
   подсказки «листай» не было видно вовсе — первый экран оставался без
   действия. Виноват верхний отступ: clamp брал минимум 8rem, это пятая
   часть окна. */
@media (max-height: 700px) {
  .chapter.is-beat {
    --beat-top: clamp(4.5rem, calc(var(--vh, 1svh) * 14), 8rem);
    padding-block: var(--beat-top) clamp(3rem, calc(var(--vh, 1svh) * 8), 5rem);
  }
  .mark { top: clamp(3.4rem, calc(var(--vh, 1svh) * 8), 5rem); }
  .hero__lower { margin-top: clamp(1.2rem, calc(var(--vh, 1svh) * 3), 2rem); }
  .hero__cta { margin-top: 1rem; }

  /* Первый экран здесь начинается ниже переключателя языка (см. выше), и
     эти два десятка пикселей надо у кого-то занять. Занимаем у мелкого
     набора: разрядка 1.75 на трёх строках подписи — это тридцать пикселей
     воздуха внутри одного абзаца, а внизу за них не влезает кнопка. */
  .hero .label--long { line-height: 1.45; }
  .hero .hero__top { margin-bottom: 1rem; }
}

/* Кнопка возврата была 29x39 — ниже нормы касания в 44 px */
.totop { min-height: 44px; }

/* ============ Отзывы текстом ============

   Раньше это были скриншоты переписки. На телефоне карточка выходила
   240x300 при исходнике 1000x1250 — уменьшение вчетверо, читать
   невозможно. А это единственное социальное доказательство на странице.

   Текст читается на любом экране, весит копейки вместо мегабайта картинок
   и переводится на английский. Карточка — не «коробка с тенью», а лист:
   волосяная линия слева, кавычка дидоном, имя внизу под чертой. */

.quote {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: clamp(1.6rem, 3vw, 2.4rem) 0 clamp(1.2rem, 2.4vw, 1.8rem);
  padding-left: clamp(1.4rem, 2.6vw, 2.2rem);
  border-left: 1px solid var(--hair);
  transition: border-color 0.6s var(--ease);
}
.quote:hover { border-left-color: var(--hair-warm); }

/* открывающая кавычка — знак, а не украшение: сразу видно, что это речь */
.quote::before {
  content: "«";
  position: absolute;
  top: clamp(0.2rem, 1vw, 0.6rem);
  left: clamp(1.1rem, 2.2vw, 1.8rem);
  font-family: var(--display);
  font-size: clamp(2.6rem, 5vw, 4rem);
  line-height: 1;
  color: var(--champ);
  opacity: 0.28;
  pointer-events: none;
}

.quote__text {
  margin: 0;
  font-size: var(--t-body);
  line-height: 1.7;
  color: var(--bone-mid);
  /* длинные отзывы свёрнуты: иначе одна карточка уезжает на три экрана */
  max-height: 15.3em;
  overflow: hidden;
  position: relative;
  transition: max-height 0.7s var(--ease);
}
.quote__text p + p { margin-top: 0.8em; }

/* Свёрнутый текст ГАСИТСЯ МАСКОЙ, а не накрывается плашкой.

   Сначала здесь лежал градиент из прозрачного в цвет фона. Но фон
   страницы — движущийся кадр, а цвет в градиенте непрозрачный, поэтому
   поверх фотографии повисал тёмный прямоугольник. На телефоне это было
   особенно грубо: заказчик так и написал — «странное затемнение».

   Маска гасит сам текст, ничего не рисуя сверху. Кадр под ним остаётся
   виден целиком, на любом фоне и при любом свете. */
/* Маска только там, где текст ДЕЙСТВИТЕЛЬНО не поместился: класс ставит
   скрипт по факту замера. Иначе короткий отзыв, который виден целиком,
   тоже гаснет внизу и выглядит обрезанным на ровном месте. */
.quote.is-clipped .quote__text {
  -webkit-mask-image: linear-gradient(180deg, #000 68%, transparent 97%);
  mask-image: linear-gradient(180deg, #000 68%, transparent 97%);
}
.quote.is-open .quote__text {
  max-height: 200em;
  -webkit-mask-image: none;
  mask-image: none;
}

/* Было 129x16 — вдвое ниже нормы касания. И промах здесь дороже обычного:
   мимо кнопки палец попадает в карточку, то есть листает ленту вбок.
   Человек хотел дочитать отзыв, а получил следующий.

   Площадь растим полями, а не кеглем, а подчёркивание переносим с рамки
   на само слово — иначе черта уехала бы на край поля и повисла в воздухе
   в сантиметре под надписью. */
.quote__more {
  align-self: flex-start;
  margin-top: 0;
  /* 1rem сверху и снизу поверх строки в 13.8 px — ровно 46, чуть выше
     нормы касания. На 0.9rem выходило 42.6, то есть всё ещё мимо. */
  padding: 1rem 0;
  border: 0;
  background: none;
  color: var(--champ);
  font-family: var(--ui);
  font-size: var(--t-label);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: var(--hair-warm);
  text-underline-offset: 0.42em;
  transition: color 0.4s var(--ease), text-decoration-color 0.4s var(--ease);
}
.quote__more:hover { color: var(--champ-hi); text-decoration-color: var(--champ); }
/* короткому отзыву кнопка не нужна — её убирает скрипт */
.quote__more[hidden] { display: none; }

.quote__who {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  margin-top: auto;
  padding-top: clamp(1rem, 2vw, 1.4rem);
}
.quote__mark {
  width: 1.6rem; height: 1px;
  background: var(--champ);
  flex: none;
}
.quote__who cite {
  font-style: normal;
  font-family: var(--ui);
  font-size: var(--t-small);
  letter-spacing: 0.04em;
  color: var(--bone);
}

/* карточки одной высоты, чтобы лента не выглядела лесенкой */
.carousel--reviews .carousel__track { align-items: stretch; }
.carousel--reviews .slide { flex: 0 0 clamp(20rem, 30%, 26rem); }

.reviews__source {
  margin-top: clamp(1.4rem, 3vw, 2.2rem);
  max-width: 46ch;
}
.link {
  color: var(--champ-hi);
  text-decoration: none;
  border-bottom: 1px solid var(--hair-warm);
  transition: border-color 0.4s var(--ease);
}
.link:hover { border-color: var(--champ); }

@media (max-width: 700px) {
  .carousel--reviews .slide { flex: 0 0 86%; }
  .quote__text { max-height: 13.6em; }
}

/* ============ Маршрут ============

   Карта из брошюры судна. На широком экране она слева и крупно, текст
   справа: заказчик просил, чтобы карта и надпись попадали в один экран,
   а раньше на них не хватало высоты.

   Подписи островов на карте мелкие по своей природе — это лист формата А4,
   ужатый до колонки. Читать их на странице невозможно ни при каком
   размере, поэтому карта открывается: лупой под курсором и во весь экран
   по нажатию. См. js/mapview.js. */

.chapter.is-read.route { padding-block: clamp(4rem, calc(var(--vh, 1svh) * 10), 7rem); }

.route__grid {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: clamp(2rem, 4vw, 4rem);
  align-items: center;
}

.route__head h2 { margin-top: 1.2rem; max-width: 16ch; }
.route__note { margin-top: 1.4rem; max-width: 44ch; color: var(--bone-dim); }

/* ---- Карта ---- */

.route__map { margin: 0; }

/* Высота карты ограничена экраном, а не колонкой.

   Заказчику нужно было, чтобы карта и заголовок попадали в один экран, а
   карта при этом была крупнее. Одно противоречит другому: карта
   вертикальная, и растёт она в ту же сторону, где кончается экран.
   Развели так — текст ушёл вбок, а у секции срезаны поля: освободившееся
   отдано карте. */

/* Вся карта — одна большая кнопка. Так её открывает и мышь, и палец, и
   клавиатура, и не нужно отдельной подписи «нажмите сюда». */
.route__open {
  position: relative;
  display: block;
  width: 100%;
  padding: clamp(0.5rem, 1.2vw, 1rem);
  border: 1px solid var(--hair-warm);
  /* Кнопка не наследует цвет страницы — у кнопок он свой, чёрный. Видно
     это будет ровно один раз: если карта не загрузится, вместо неё
     покажется подпись alt чёрным по тёмному, то есть ничем. */
  color: var(--bone);
  background: rgba(3, 7, 15, 0.55);
  cursor: zoom-in;
  overflow: hidden;
  transition: border-color 0.5s var(--ease);
}
.route__open:hover { border-color: rgba(216, 185, 143, 0.6); }
.route__open:focus-visible { outline: 2px solid var(--champ-hi); outline-offset: 3px; }
/* Одно правило на все размеры. Раньше их было два: одно ограничивало
   высоту и ставило width: auto, второе ниже возвращало width: 100%.
   Побеждало второе, и на мониторе карта выходила 684x378 вместо 344x378 —
   то есть сплющенной вдвое. Держим ширину и высоту в одном месте, чтобы
   спорить было нечему. */
/* КАРТА ДЕРЖИТ СВОЁ МЕСТО ДО ЗАГРУЗКИ.

   Было: width: auto плюс max-height: 70vh. Высотой карта ограничивалась
   правильно, но ширины у неё до загрузки не было НИКАКОЙ — ни в стилях,
   ни в природе: картинка отложенная, natural-размеры ещё нули, а из
   атрибутов width/height браузер выводит только пропорцию. Коробка
   схлопывалась в ноль и распрямлялась в 377 px ровно в тот момент, когда
   карта долетала. Замер: скачок 359 px прямо под глазами у человека,
   читающего описание маршрута рядом.

   Стало: ширина определённая (100 % колонки), высота выводится из
   пропорции — и место занято с первой отрисовки. Прежнее ограничение по
   высоте выражено через ширину: 70vh при пропорции 1500:1647 — это ровно
   вот столько ширины. Так у карты один ограничитель вместо двух, и спорить
   им не о чем: раньше именно спор двух правил давал на мониторе карту,
   сплющенную вдвое. */
.route__open img {
  display: block;
  width: 100%;
  max-width: calc(var(--vh, 1svh) * 70 * 1500 / 1647);
  height: auto;
  aspect-ratio: 1500 / 1647;
  margin-inline: auto;
}

/* Подсказка. Маленькая и не мигает: она нужна один раз — понять, что
   карта открывается, — и не должна спорить с самой картой. */
.route__hint {
  position: absolute;
  left: 50%;
  bottom: clamp(1rem, 2.4vw, 1.8rem);
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.55rem 0.95rem;
  background: rgba(3, 7, 15, 0.86);
  border: 1px solid var(--hair-warm);
  color: var(--champ-hi);
  font-family: var(--ui);
  font-size: clamp(0.68rem, 0.64rem + 0.2vw, 0.76rem);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  white-space: nowrap;
  pointer-events: none;
}
.route__lens {
  width: 11px; height: 11px;
  border: 1px solid currentColor;
  border-radius: 50%;
  position: relative;
  flex: 0 0 auto;
}
.route__lens::after {
  content: "";
  position: absolute;
  right: -3px; bottom: -3px;
  width: 5px; height: 1px;
  background: currentColor;
  transform: rotate(45deg);
}

/* ---- Лупа под курсором ---- */

.route__glass {
  position: absolute;
  width: clamp(210px, 21vw, 330px);
  aspect-ratio: 1;
  margin: 0;
  transform: translate(-50%, -50%);
  border: 1px solid rgba(216, 185, 143, 0.7);
  border-radius: 50%;
  box-shadow: 0 18px 50px rgba(2, 5, 11, 0.7);
  background-repeat: no-repeat;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.25s var(--ease), visibility 0.25s;
  pointer-events: none;
  z-index: 2;
}
.route__open.is-glassed .route__glass { opacity: 1; visibility: visible; }
.route__open.is-glassed .route__hint { opacity: 0; }

/* ---- Остановки маршрута ---- */

.route__legs {
  list-style: none;
  margin: clamp(1.8rem, 3vw, 2.6rem) 0 0;
  padding: 0;
}
.route__legs li {
  display: grid;
  grid-template-columns: 2.4rem 1fr;
  align-items: baseline;
  gap: 0 0.9rem;
  padding: 0.75rem 0;
  border-top: 1px solid var(--hair);
}
.route__legs li:last-child { border-bottom: 1px solid var(--hair); }
.route__no {
  font-family: var(--ui);
  font-size: var(--t-label);
  letter-spacing: 0.16em;
  color: var(--bone-dim);
}
.route__legs b {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.05rem, 1rem + 0.4vw, 1.3rem);
  color: var(--bone);
}
.route__legs li > span:last-child {
  grid-column: 2;
  font-size: var(--t-small);
  color: var(--bone-dim);
}

/* ---- Телефон: одна колонка, карта первой ---- */

@media (max-width: 900px) {
  .route__grid { grid-template-columns: 1fr; gap: clamp(1.8rem, 5vw, 2.6rem); }
  /* сначала заголовок, потом карта: без подписи картинка приходит без имени */
  .route__map { order: 2; }
  .route__side { order: 1; display: contents; }
  .route__head { order: 1; }
  .route__note { order: 3; }
  .route__legs { order: 4; margin-top: 0; }
}

/* ============ Карта во весь экран ============
   Отдельный слой поверх всего: страница под ним замирает, чтобы прокрутка
   не уезжала, пока человек таскает карту пальцем. */

.mapview {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  grid-template-rows: auto 1fr auto;
  background: #02050b;
}
.mapview[hidden] { display: none; }
body.is-mapview { overflow: hidden; }

.mapview__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: calc(env(safe-area-inset-top, 0px) + 1rem) var(--gut) 1rem;
  border-bottom: 1px solid var(--hair);
}
.mapview__title {
  margin: 0;
  font-family: var(--display);
  font-size: clamp(1.05rem, 0.95rem + 0.6vw, 1.5rem);
  color: var(--bone);
  line-height: 1.25;
}
.mapview__title span {
  display: block;
  font-family: var(--ui);
  font-size: var(--t-label);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--bone-dim);
  margin-top: 0.3rem;
}
.mapview__close {
  flex: 0 0 auto;
  min-height: 46px;
  padding: 0.6rem 1.2rem;
  border: 1px solid var(--hair-warm);
  background: rgba(216, 185, 143, 0.12);
  color: var(--champ-hi);
  font-family: var(--ui);
  font-size: var(--t-label);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  cursor: pointer;
}
.mapview__close:hover { border-color: var(--champ); }

.mapview__stage {
  position: relative;
  overflow: hidden;
  /* Без min-height: 0 грид-элемент не даёт себе сжаться меньше содержимого,
     а содержимое здесь — картинка в 2856 px высотой. Строка 1fr оставалась
     номинальной, картинка выпирала за сцену и обрезалась полосой. */
  min-height: 0;
  display: grid;
  place-items: center;
  touch-action: none;          /* жесты забираем себе, иначе страница уедет */
  cursor: grab;
}
.mapview.is-dragging .mapview__stage { cursor: grabbing; }
/* Картинка занимает ровно сцену, а вписывает её object-fit. Проценты в
   max-height внутри грида не разрешаются, и картинка выпирала за кадр —
   поэтому размер задан абсолютом, без процентов от неопределённой высоты. */
.mapview__stage img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  transform-origin: center;
  will-change: transform;
  user-select: none;
  -webkit-user-drag: none;
}

.mapview__tip {
  margin: 0;
  padding: 0.9rem var(--gut) calc(env(safe-area-inset-bottom, 0px) + 0.9rem);
  border-top: 1px solid var(--hair);
  font-size: var(--t-small);
  color: var(--bone-dim);
  text-align: center;
}
