/* [PV-LAYOUT 05.08] РАСКЛАДКА ПРОСМОТРЩИКА ФОТО — НАПИСАНА С НУЛЯ. ФАЗА 1.
   ═══════════════════════════════════════════════════════════════════════════════════════════
   Файл НИКУДА НЕ ПОДКЛЮЧЁН. Живой просмотрщик по-прежнему живёт на css/dr-photoviewer.css —
   этот лежит рядом и проверяется отдельно (_qa/pv-layout-05-08.html). Подключать только после
   того, как владелец посмотрит пальцем и скажет «да».

   ЗАЧЕМ ЗАНОВО. За 04–05.08 было четыре захода посадить хром (крестик, счётчик, «поделиться»)
   НА снимок в режиме главной, и все четыре провалились по одной причине: кадр лежит внутри
   `.rvp__vw-stage`, который на 66px выше самой фотографии. Хром, привязанный к этому боксу,
   физически не может попасть на снимок — он привязан к пустому воздуху вокруг него. Латали
   снаружи (обёртка inline-block, специфичность, флексовый обхват, грид с place-items) —
   бесполезно: подпирали чужую раскладку вместо того, чтобы завести свою.

   ЧТО ЗДЕСЬ ДРУГОГО — ТРИ ВЕЩИ, И ОНИ ЖЕ ВСЯ СУТЬ ФАЗЫ 1.

   1. СВОЙ КОНТЕЙНЕР — грид из ТРЁХ ПОЛОС: верхняя (хром вне кадра) · кадр · нижняя.
      Высоты крайних полос — переменные `--pv-top` / `--pv-bot`, и ровно ОНИ ЖЕ вычитаются из
      потолка снимка. Одна пара чисел управляет и полосами, и размером кадра, поэтому кадр не
      может заехать под полосу: это исключено арифметикой, а не подобранным отступом.

   2. СВОЯ РАМКА — `.pv__frame` не задаёт себе размер вообще. Её размер = размер снимка,
      потому что она обычный грид-элемент по содержимому. Значит рамка и снимок — один и тот же
      прямоугольник, всегда, при любой пропорции фотографии.
      ⚠ ПОТОЛКИ СНИМКА ТОЛЬКО В ЕДИНИЦАХ ЭКРАНА, НИКОГДА В ПРОЦЕНТАХ. Процент внутри
      обёртки-по-содержимому цикличен: браузер не может посчитать процент от коробки, размер
      которой сам зависит от этого процента, — он молча берёт натуральный размер файла, и
      снимок 1280×1600 раздувается до 576×768 на экране 390. Это провал №4 от 05.08.

   3. СВОИ МЕСТА ПОД ХРОМ — два разных дома, а не один:
      · `.pv__on--*` лежит ВНУТРИ рамки (углы снимка) — режим «просто глянуть фотку»;
      · `.pv__bar` / `.pv__foot` занимают крайние полосы грида — режим «полный экран с полосой».
      Одно и то же место больше не обслуживает оба случая: они физически разные узлы.

   ЧЕГО ЗДЕСЬ НЕТ НАМЕРЕННО: вида кнопок, цветов хрома, поведения по тапу, зума и свайпа.
   Это фазы 2–5. Здесь только геометрия, и проверяется она замером, а не глазом.

   КОНТРАКТ С ТЕМ, КТО БУДЕТ ПОДКЛЮЧАТЬ:
     .pv.is-ready — ставится, когда снимок загружен (`img.complete`). До этого хром на снимке
     скрыт: у незагруженной картинки нулевой размер, рамка схлопывается, и хром на миг
     собирается в точку в центре экрана. Одна строчка на стороне скрипта, зато без вспышки. */

/* ═══ 1. КОНТЕЙНЕР ═══════════════════════════════════════════════════════════════════════ */
.pv {
  position: fixed;
  inset: 0;
  z-index: 1200;

  /* Единицы экрана. `dvh` — потому что на телефоне адресная строка съезжает и `vh` врёт на
     её высоту: кадр то не влезает, то оставляет дыру. Подмена в @supports ниже. */
  --pv-vw: 100vw;
  --pv-vh: 100vh;

  /* ПОЛОСЫ ПОД ХРОМ ВНЕ КАДРА — собираются из двух слагаемых, и это принципиально.
     ⚠ ГРАБЛИ, ПОЙМАННЫЕ ЗАМЕРОМ 05.08: сначала `--pv-top` переопределялся напрямую и в
     режиме, и в медиа-запросе. У обоих правил одинаковая специфичность (0,1,0), и в горизонте
     побеждало то, что НИЖЕ по файлу, — поле 8px вместо полосы 44px. Кадр молча заезжал под
     полосу на всех четырёх снимках (замер `подПолосой:false`, глазами это ловится плохо).
     Теперь `--pv-top` НИКТО не переопределяет: он вычисляется из высоты полосы и поля.
     Правило: режимы и медиа-запросы трогают ТОЛЬКО `--pv-barh` и `--pv-pad-*`. */
  --pv-barh: 0px;                /* высота верхней полосы; 0 = полосы нет */
  --pv-footh: 0px;               /* то же снизу */
  --pv-pad-top: max(12px, env(safe-area-inset-top, 0px));
  --pv-pad-bot: max(12px, env(safe-area-inset-bottom, 0px));
  --pv-top: calc(var(--pv-barh) + var(--pv-pad-top));
  --pv-bot: calc(var(--pv-footh) + var(--pv-pad-bot));
  --pv-gx: 12px;                 /* боковое поле */

  /* Потолок кадра сверх полей: по умолчанию потолка нет (кадр во весь доступный прямоугольник).
     Режим-окно ставит свои числа. */
  --pv-capw: 100vw;
  --pv-caph: 100vh;

  /* ИТОГОВЫЕ ПОТОЛКИ СНИМКА. Считаются один раз здесь и больше нигде не пересчитываются. */
  --pv-maxw: min(var(--pv-capw), calc(var(--pv-vw) - var(--pv-gx) * 2));
  --pv-maxh: min(var(--pv-caph), calc(var(--pv-vh) - var(--pv-top) - var(--pv-bot)));

  --pv-radius: 0px;
  --pv-inset: 10px;              /* отступ хрома от края снимка */
  --pv-arrow-gap: 12px;          /* зазор между кромкой снимка и стрелкой ЗА кадром */

  display: grid;
  grid-template-rows: var(--pv-top) 1fr var(--pv-bot);
  grid-template-areas: "top" "photo" "bot";
}

@supports (height: 100dvh) {
  .pv { --pv-vw: 100dvw; --pv-vh: 100dvh; }
}

.pv[hidden] { display: none; }

/* ═══ 2. ПОЛОСА КАДРА И САМА РАМКА ═══════════════════════════════════════════════════════ */
/* `min-height: 0` обязателен: без него грид-полоса `1fr` не даёт себя сжать ниже содержимого,
   и в горизонтальной ориентации кадр выдавливает полосы за экран. */
.pv__stage {
  grid-area: photo;
  min-height: 0;
  min-width: 0;
  display: grid;
  place-items: center;
}

/* СЕТКА ВСЕХ ФОТО (галерея квартиры). Занимает ту же полосу грида, что и кадр, и переключается
   с ним взаимоисключающе: одновременно они не живут. Прокрутка своя — у квартиры 25–43 снимка,
   в экран они не влезают никогда. `overscroll-behavior` не даёт докрутке пробить на страницу
   под слоем. */
.pv__grid {
  grid-area: photo;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

/* РАМКА. Ни ширины, ни высоты, ни аспекта — размер приходит от снимка.
   `line-height: 0` — чтобы под картинкой не оставалась полоска строчного интерлиньяжа
   (иначе рамка была бы на 3–5px выше снимка, и весь смысл потерялся бы). */
.pv__frame {
  /* [ПОЧИНКА 10.08] Рамка занимает ПЕРВУЮ ячейку сцены явно. Утром соседи по ленте лежали
     сиблингами рамки со своим `grid-area: 1/1`, а у рамки ячейки не было — и автораскладка
     уносила её в следующий ряд, как только появлялся хоть один видимый сосед, то есть всегда.
     Замер на 390: ряды сцены «477px 477px», рамка на y=491 при экране 844, низ снимка и счётчик
     за нижней кромкой. Соседи с тех пор переехали ВНУТРЬ рамки, на дорожку, и сиблингов у неё
     больше нет — но явная ячейка остаётся сторожем: вернётся сосед снаружи, рамка не уедет. */
  grid-area: 1 / 1;
  justify-self: center;
  align-self: center;
  position: relative;
  display: block;
  line-height: 0;
  margin: 0;
  border-radius: var(--pv-radius);
}

.pv__img {
  display: block;
  width: auto;
  height: auto;
  max-width: var(--pv-maxw);
  max-height: var(--pv-maxh);
  border-radius: inherit;
  /* object-fit не нужен: снимок не растянут, он сам ужимается по пропорции под два потолка. */
}

/* ═══ ДОРОЖКА СО СНИМКАМИ ═══ [SWIPE-TRACK 10.08, пересобрано]
   Едет ДОРОЖКА, а рамка с хромом стоит. Сначала тянули всю сцену — и крестик со счётчиком,
   которые живут на углах рамки, уезжали вместе с фотографией за левый край, а через 265мс
   возвращались рывком. Замер одного свайпа по крестику: 318 → 156 → −72 → снова 318 за один
   кадр; владелец назвал это «будто на каждый свайп обновление происходит».
   Дорожка ничего не задаёт своим размером: она в потоке, высоту ей даёт снимок внутри, а через
   неё — рамке. Тяга не может жить на самом снимке: его `transform` занят увеличением. */
.pv__track {
  position: relative;
  display: block;
  line-height: 0;
}

/* Соседи по ленте. ВЫНУТЫ ИЗ ПОТОКА намеренно: иначе рамка считала бы высоту по самому высокому
   из трёх снимков, и у горизонтального кадра окно осталось бы высотой вертикального.
   Отодвинуты ровно на ширину экрана — потому `100vw`, а не `100%`: процент считался бы от ширины
   самого снимка, а она у каждого своя, и сосед вставал бы каждый раз в другое место.
   Потолки размера те же, что у снимка в рамке: сосед обязан въезжать той величины, какой станет,
   когда доедет. Иначе на доводке кадр прыгает в размере. */
.pv__peek {
  position: absolute;
  top: 50%;
  left: 50%;
  display: block;
  width: auto;
  height: auto;
  max-width: var(--pv-maxw);
  max-height: var(--pv-maxh);
  pointer-events: none;
}
.pv__peek[hidden] { display: none; }

/* [ЗАЗОР 12.08] СОСЕД СТОИТ ВПЛОТНУЮ К РАМКЕ, А НЕ У КРАЯ ЭКРАНА.
   Было `100vw` — сосед отодвигался на ширину ЭКРАНА, и между кадрами оставалась чёрная полоса
   шириной `экран − своя/2 − соседа/2`. Замер на 390: ширины кадров 338 и 366, полосы по всей
   ленте 38 · 24 · 24 · 24 · 24 · 24 · 24 · 24 · 24. На узком портретном снимке она была бы шире —
   это гарантировала сама формула.
   Довод в пользу `100vw` («процент считался бы от ширины самого снимка, а она у каждого своя»)
   верен и никуда не делся: поэтому половину СВОЕЙ ширины по-прежнему даёт процент, а половину
   ЧУЖОЙ — переменная `--pv-framew`, которую движок пишет по замеру рамки на каждом кадре.
   Сумма и есть расстояние между центрами двух соприкасающихся кадров.
   Запасное значение `100vw` оставлено намеренно: если движок почему-то не написал переменную,
   поведение откатывается к прежнему, а не рассыпается. */
.pv__peek--prev { transform: translate(-50%, -50%) translateX(calc(-50% - var(--pv-framew, 100vw) / 2)); }
.pv__peek--next { transform: translate(-50%, -50%) translateX(calc(50% + var(--pv-framew, 100vw) / 2)); }

/* ═══ ОКНО ПОСТОЯННОГО РАЗМЕРА ═══ [10.08, уровень «главная»]
   Окно больше не берёт размер у снимка — снимок вписывается в окно. Так делают площадки:
   живой замер карусели Airbnb на 390 (их страница нашей же квартиры) дал бокс 390×371 у ВСЕХ
   слайдов при исходниках разной пропорции (1.50 и 1.78) — то есть контейнер фиксирован всегда.
   До этого у нас было наоборот, и на переходе с вертикального кадра на горизонтальный окно
   схлопывалось с 477 до 269 и съезжало на 105px вниз за один кадр. Владелец: «разноразмерные
   фото выглядят ужасно на свайпе».

   ЧЕТЫРЕ ХОДА ПЕРЕБРАНЫ И ОТВЕРГНУТЫ ВЛАДЕЛЕМ 10.08, возвращать их не надо:
     · окно по снимку с мгновенной сменой размера — «дергается, будто обновление»;
     · постоянное окно 3:4, кадр вписан, поля тёмные — «не нравится»;
     · постоянное окно, кадр заполняет с обрезкой (в двух пропорциях: 3:4 и квадрат 1.05,
       как у карусели Airbnb) — «мне не нравится обрезка снимка»;
     · тёмный экран целиком, как в их фототуре — отменяет вердикт 05.08 и не выбран.
   Замер коллекции на всякий случай: десять снимков гостей, восемь ровно 0.75, один 1.33,
   один 1.78 — то есть однородная коллекция с двумя выбросами.

   ОСТАЁТСЯ ЕДИНСТВЕННОЕ, что не нарушает ни одного из этих условий: окно снова по размеру
   снимка (кадр цел, обрезки нет, полей нет), но размер меняется ПЛАВНО за то же время, что
   едет свайп. Рывок был не в том, что размер разный, а в том, что он менялся за один кадр.
   Ширину и высоту в пикселях считает движок (`подогнать` в `js/dr-pv.js`) — переход
   `transition` умеет работать только с числом, но не с `auto`. */
.pv--home {
  /* Зазор между кадрами ленты. С 10.08 равен нулю: тёмная полоса, проезжавшая между снимками
     на свайпе, — это он и был. Слово владельца: «можно ли убрать чёрную свайп-полосу между
     фото». Кадры едут встык. Шаг ленты движок меряет живьём, поэтому число здесь можно менять
     свободно — свайп подстроится сам. */
  --pv-gap: 0px;
}

/* ═══ ОКНО ПО РАЗМЕРУ СНИМКА, НО С ПЕРЕТЕКАНИЕМ ═══ [10.08]
   Кадр цел, обрезки нет, полей нет — окно просто принимает форму снимка. Рывок был не в том,
   что размеры разные, а в том, что смена занимала ОДИН кадр: 477 → 269 по высоте и 105px вниз
   мгновенно. Здесь та же смена идёт переходом.

   Что перебрано и отвергнуто владельцем 10.08 — не возвращать:
     · мгновенная смена размера — «дергается, будто обновление»;
     · постоянное окно, кадр вписан, поля тёмные — «не нравится»;
     · постоянное окно, кадр заполняет с обрезкой (3:4 и квадрат 1.05 как у Airbnb) —
       «мне не нравится обрезка снимка»;
     · постоянное окно, поля заполнены размытой копией снимка — «было лучше»;
     · тёмный экран целиком, как в фототуре Airbnb — отменяет вердикт 05.08, не выбран.

   ЦЕНА ПЕРЕХОДА И ЧЕМ ОНА СБИТА. Анимация ширины и высоты — единственный способ (переход не
   умеет работать с `auto`), но она заставляет браузер каждый кадр пересчитывать раскладку и
   заново растрировать снимок. На телефоне это давало подрагивание. Сбито тремя вещами:
     · `contain` запирает пересчёт внутри окна — страница снаружи в нём не участвует;
     · распаковку следующего снимка движок делает ДО подмены (`decode`), иначе первый кадр
       перехода тратится на неё.
   `will-change` здесь не помогает и снят: он готовит слой для трансформа, а высота считается
   раскладкой, до слоя дело не доходит. */
/* ⚠ ОБРЕЗКА ЖИВЁТ НА СВОЕЙ НЕПОДВИЖНОЙ КОРОБКЕ [11.08]. Две попытки до этого были неверны, обе
   стоили владельцу вечера, и обе поучительны:
     · ОБРЕЗКА НА РАМКЕ срезала боковые стрелки листания — они лежат В рамке, но СНАРУЖИ её
       коробки, за кромкой кадра. На телефоне не видно (стрелок там нет вовсе), на 1440 с мышью
       они пропали целиком. Замер: стрелка на 387px, рамка начинается с 435px.
     · ОБРЕЗКА НА ЛЕНТЕ срезала соседний снимок на подходе: лента едет за пальцем, и окно
       обрезки уезжает вместе с ней. Замер пальцем на середине свайпа: слева снимок, в центре
       и справа — заливка рамки. Владелец: «чёрный снимок на подложке».
   Обрезать должно то, что СТОИТ: коробка по кадру, лента внутри неё едет.
   ⚠ И `overflow: hidden`, и `contain: paint` обрезают одинаково — снять одно и оставить другое
   не починка. Рамке оставлен только `contain: layout`, ради которого всё и затевалось: пересчёт
   высоты на морфе не выходит за окно. */
.pv--home .pv__frame {
  contain: layout;
}
.pv__clip {                       /* на прочих уровнях — прозрачная обёртка в потоке */
  position: relative;
  display: block;
  line-height: 0;
}
/* ⚠ Здесь ТОЛЬКО обрезка, без `contain: paint`. Паинт-контейнмент обрезает ровно так же, но
   вдобавок каждый кадр морфа заново растрирует содержимое отдельным слоем — это лишняя работа
   на анимации, за которую владелец сказал «дёрганый очень». Изоляцию пересчёта держит
   `contain: layout` на рамке, и её достаточно. */
.pv--home .pv__clip {
  position: absolute;
  inset: 0;
  overflow: hidden;               /* лента едет ВНУТРИ окна, за его края не вылезает */
  border-radius: inherit;         /* обрезка обязана повторять скругление кадра */
}
/* [МОРФ 10.08, ВТОРАЯ РЕДАКЦИЯ] Морф идёт ВЫСОТОЙ, а не трансформом. Первая редакция масштабом
   отвергнута владельцем на телефоне — «всё растягивает и шакалится»:
     · `scaleY` рамки жал и соседей ленты, встречный масштаб стоял только у снимка в кадре;
     · масштаб трансформом тянет готовые пиксели вместо перерисовки — отсюда мыло.
   Высота дороже: каждый кадр раскладка и растрирование заново. Цена сбита `contain: layout paint`
   на рамке (пересчёт заперт внутри окна) и `decode` перед подменой снимка. Ширина не меняется
   никогда — анимируется одна высота. Метку `is-morph` вешает и снимает движок. */
/* ⚠ СЕЛЕКТОР НАРОЧНО С `.pv` ВПЕРЕДИ, а прозрачность перечислена вместе с размером.
   Ниже по файлу живёт `.pv.is-ready .pv__on { transition: opacity 140ms }` — ровно та же
   специфичность (три класса) и позже по файлу, поэтому оно побеждало, и хром прыгал в конечное
   положение вместо перехода (поймано замером: `transitionProperty` у крестика был `opacity`).
   Это записанное правило проекта: переопределяя переход в состоянии, перечисляй ВСЕ свойства. */
.pv.pv--home.is-morph .pv__frame,
.pv.pv--home.is-morph .pv__chrome {
  transition: width 220ms var(--ease-standard, cubic-bezier(.22,1,.36,1)),
              height 220ms var(--ease-standard, cubic-bezier(.22,1,.36,1));
}
.pv.pv--home.is-morph .pv__on {
  transition: opacity 140ms var(--ease-standard, cubic-bezier(.22,1,.36,1));
}
@media (prefers-reduced-motion: reduce) {
  .pv.pv--home.is-morph .pv__frame,
  .pv.pv--home.is-morph .pv__chrome,
  .pv.pv--home.is-morph .pv__on { transition: none; }
}

/* Слой хрома — коробка ровно по размеру кадра, лежит на нём. Сам не ловит нажатия, ловят дети:
   иначе прозрачный прямоугольник поверх снимка съедал бы свайп. */
.pv__chrome {
  grid-area: 1 / 1;
  justify-self: center;
  align-self: center;
  position: relative;
  pointer-events: none;
  z-index: 2;
}
.pv__chrome > * { pointer-events: auto; }
.pv__chrome .pv__count { pointer-events: none; }   /* счётчик — подпись, палец идёт сквозь */
.pv:not(.pv--home) .pv__chrome { display: none; }
.pv--home .pv__track { width: 100%; height: 100%; }
/* ⚠ `width/height: 100%`, а НЕ `inset: 0`. Пробовали inset — у заменяемого элемента (`img`)
   `width: auto` берёт СОБСТВЕННЫЙ размер файла, а не коробку: снимок вырос до 1024×768 и вылез
   из окна. Замер поймал сразу. Проценты здесь обязательны. */
.pv--home .pv__img,
.pv--home .pv__peek {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  object-fit: cover;              /* окно точно по пропорции снимка ⇒ ничего не обрезается */
}
.pv--home .pv__peek--prev { transform: translateX(calc(-100% - var(--pv-gap))); }
.pv--home .pv__peek--next { transform: translateX(calc(100% + var(--pv-gap))); }

/* Мерка. Пустой невидимый узел ростом ровно в потолки кадра: браузер сам считает из них
   пиксели, движку остаётся прочитать прямоугольник. Иначе никак — `--pv-maxw` это выражение
   `min(...)`, и из скрипта оно читается текстом, а не числом. */
.pv__probe {
  position: absolute;
  visibility: hidden;
  pointer-events: none;
  width: var(--pv-maxw);
  height: var(--pv-maxh);
}
@media (prefers-reduced-motion: reduce) {
  .pv--home.is-sized .pv__frame { transition: none; will-change: auto; }
}

/* Пока палец тянет — сцена не должна выпускать соседей за свои границы и подхватывать
   собственную прокрутку страницы. `touch-action` снимает у браузера горизонтальный жест;
   вертикальный оставляем, на нём держится закрытие свайпом вниз. */
.pv.is-swiping .pv__stage { overflow: hidden; }
.pv .pv__stage { touch-action: pan-y; }

/* ═══ 3. ХРОМ НА СНИМКЕ ══════════════════════════════════════════════════════════════════ */
/* Живёт ВНУТРИ рамки ⇒ его координаты — координаты снимка. Это и есть решение задачи,
   ради которой всё переписано. */
.pv__on {
  position: absolute;
  z-index: 2;
  line-height: normal;           /* гасим нулевой интерлиньяж рамки для содержимого кнопок */
}
/* [ЧИСТКА 15.08] Слоты --tl и --bl и полоса низа .pv__foot сняты: ничто их не ставило
   (подтверждено состязательной проверкой — grep по js/dr-pv.js и всем живым html = 0;
   про --tl в движке остался только комментарий). Верх и правые углы живые. */
.pv__on--tr { top: var(--pv-inset); right: var(--pv-inset); }
.pv__on--br { bottom: var(--pv-inset); right: var(--pv-inset); }
/* Боковые слоты — стрелки листания на десктопе, и они стоят ЗА КАДРОМ, на затемнении.
   ⚠ ЭТО ПРАВКА 09.08 ПО ПЕРЕЗАМЕРУ 08.08: у всех трёх площадок в полноэкранном кадре стрелки
   вне снимка (Airbnb кружки 48px, Booking 58px, Суточно рельсы). Раньше они лежали НА
   фотографии и закрывали её углы.
   `right: calc(100% + зазор)` считается от кромки САМОЙ рамки, а рамка = снимок (см. пункт 2
   наверху файла) ⇒ стрелка привязана к фотографии, а не к экрану: у портретного снимка она
   подъезжает вместе с ним, а не висит у края монитора. Место под неё резервирует поле `--pv-gx`
   в разделе 6 — там же объяснено, почему без резерва стрелка уезжала бы за экран.
   Свой `transform` здесь несёт центрирование, поэтому нажатие масштабирует кнопку отдельным
   правилом в файле вида: голый `scale()` стёр бы центровку и стрелка прыгнула бы вниз. */
.pv__on--left  { top: 50%; right: calc(100% + var(--pv-arrow-gap)); transform: translateY(-50%); }
.pv__on--right { top: 50%; left:  calc(100% + var(--pv-arrow-gap)); transform: translateY(-50%); }

/* ⚠ ГРАБЛИ, ПОЙМАННЫЕ СНИМКОМ ЭКРАНА 05.08 (замер их пропустил — он верил атрибуту `hidden`,
   а не тому, что видно): у слотов задан `display`, и он ПЕРЕБИВАЕТ штатное `[hidden]` браузера.
   В режиме с полосой на кадре оставались лишний крестик и «поделиться» — два хрома разом.
   Поэтому раскладка сама отвечает за скрытие СВОИХ слотов, и селектор нарочно (0,2,0):
   вид кнопок приходит из файла страницы, который лежит НИЖЕ по каскаду, и при равной
   специфичности победил бы он. Правило: слот прячется атрибутом `hidden`, а не классом. */
.pv .pv__on[hidden],
.pv .pv__bar[hidden],
.pv .pv__stage[hidden],
.pv .pv__grid[hidden] { display: none; }

/* До загрузки снимка размер рамки нулевой — хром прячем, иначе он мигает в центре экрана. */
.pv:not(.is-ready) .pv__on { opacity: 0; pointer-events: none; }
.pv.is-ready .pv__on { opacity: 1; transition: opacity 140ms var(--ease-standard, ease); }

/* ═══ 4. ХРОМ ВНЕ КАДРА: ПОЛОСЫ ══════════════════════════════════════════════════════════ */
/* Три колонки, а не флекс с `space-between`: середина (счётчик) обязана стоять по центру
   ЭКРАНА независимо от того, что лежит слева и справа. У флекса центр уезжает от длины соседей. */
.pv__bar {
  min-width: 0;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 8px;
  padding-inline: var(--pv-gx);
}
.pv__bar  { grid-area: top; padding-top: env(safe-area-inset-top); }

/* [ПОЧИНКА 08.08 · зум накрывал полосу] Увеличенный снимок растёт трансформацией и вылезает
   за свою полосу — он ложился ПОВЕРХ шеврона, счётчика и «поделиться», и кнопки переставали
   нажиматься: палец попадал в фотографию. Выходы из этого состояния были (двойной тап, свайп),
   но неочевидные, и человек с увеличенным кадром чувствовал себя запертым.
   Лечим двумя строками: полосы поднимаем над кадром, а сам кадр при увеличении подрезаем по
   своей полосе. Подрезка включается только в зуме — на «главной» зума нет вовсе, и хром там
   живёт в углах СНИМКА, ему подрезка навредила бы. */
.pv__bar, .pv__foot { position: relative; z-index: 3; }
.pv.is-zoomed .pv__stage { overflow: hidden; }

/* ⚠ МЕСТА РАЗДАЁМ ПО ИМЕНИ, А НЕ ПО ПОРЯДКУ ДЕТЕЙ. Первая редакция ставила края через
   `:first-child` / `:last-child` — и разъехалась, как только средний слот (счётчик) скрылся:
   `display:none` выкидывает узел из грида целиком, оставшиеся двое съезжают в колонки 1 и 2,
   и «поделиться» оказывается по центру полосы. Поймано глазом на кадре сетки 05.08.
   С явными колонками слот стоит на своём месте независимо от того, что скрыто. */
.pv__slot-a { grid-column: 1; justify-self: start; }
.pv__slot-b { grid-column: 2; justify-self: center; }
.pv__slot-c { grid-column: 3; justify-self: end; }

/* ═══ 5. РЕЖИМЫ РАСКЛАДКИ ════════════════════════════════════════════════════════════════ */
/* Только геометрия. Что именно лежит в слотах и как выглядит — фазы 2–4.

   [ОКНО] «просто глянуть фотку» — с главной. Полный экран здесь ОТВЕРГНУТ владельцем дословно:
   «человек хочет просто быстро глянуть фотку, красить весь экран незачем». Значит кадр обязан
   иметь СВОЙ потолок и не занимать всё: 560×680 — из замеров 04.08, при них снимок на экране
   390 встаёт 343×458 (проверено), а страница вокруг остаётся видна. */
.pv--window {
  --pv-capw: 560px;
  --pv-caph: 680px;
  --pv-radius: var(--radius-lg, 24px);
  --pv-pad-top: max(14px, env(safe-area-inset-top, 0px));
  --pv-pad-bot: max(14px, env(safe-area-inset-bottom, 0px));
  --pv-gx: 16px;
}

/* [ПОЛНЫЙ ЭКРАН] страница отзывов и кадр галереи квартиры: кадр во весь доступный
   прямоугольник, хром — в полосе сверху. Высота полосы задаётся ЗДЕСЬ и той же переменной,
   из которой вычитается потолок снимка ⇒ кадр под полосу не заедет. */
.pv--bar {
  --pv-barh: 56px;
  --pv-pad-top: env(safe-area-inset-top, 0px);   /* поле уже внутри полосы, второй раз не нужно */
}
.pv--bar .pv__bar { min-height: var(--pv-barh); }

/* ═══ 6. ШИРИНЫ И ОРИЕНТАЦИЯ ═════════════════════════════════════════════════════════════ */
/* Горизонт на телефоне: высоты 360–420px. Полоса 56px съедала бы шестую часть экрана, а кадр
   и так зажат по высоте — полосу режем, поля тоже. Правило про «кадр не должен ужиматься в
   горизонте» соблюдается тем, что по высоте у кадра остаётся максимум возможного. */
@media (orientation: landscape) and (max-height: 620px) {
  /* трогаем только слагаемые — `--pv-top` считается сам (см. грабли 05.08 наверху файла) */
  .pv { --pv-pad-top: max(8px, env(safe-area-inset-top, 0px)); --pv-pad-bot: 8px; }
  .pv--bar { --pv-barh: 44px; --pv-pad-top: env(safe-area-inset-top, 0px); }
  .pv--window { --pv-caph: 620px; }
}

/* Десктоп: поля крупнее, у полноэкранного кадра появляется потолок по ширине — снимок
   1920px во всю ширину монитора это не «крупно», это «нечитаемо». */
@media (min-width: 1024px) {
  .pv { --pv-gx: 28px; }
  .pv:not(.pv--window) { --pv-capw: 1180px; }
  .pv--bar { --pv-barh: 60px; }
  .pv--window { --pv-capw: 640px; --pv-caph: 760px; }
}

/* ═══ 7. ПОДПИСЬ ПОД СНИМКОМ (шторка отзыва) ═════════════════════════════════════════════ */
/* Живёт в НИЖНЕЙ полосе грида, и её высота задаётся ТОЙ ЖЕ переменной, из которой вычитается
   потолок снимка (`--pv-footh` → `--pv-bot` → `--pv-maxh`, раздел 1). Значит кадр под шторку
   не заедет — это исключено арифметикой, ровно как с верхней полосой.
   ⚠ ВЫСОТА СВЁРНУТОЙ ШТОРКИ — ЧИСЛО, А НЕ СОДЕРЖИМОЕ. Соблазн сделать полосу `auto` большой:
   тогда потолок снимка зависел бы от высоты текста, а высота текста — от ширины кадра. Это тот
   самый цикл, на котором раскладка провалилась 05.08 (пункт 2 наверху файла). Поэтому свёрнутая
   шторка ровно `--pv-noteh`, а текст в ней обрезается по слову скриптом.
   РАЗВЁРНУТАЯ шторка кадр НЕ ужимает: она ложится поверх него абсолютом. Иначе разворот дёргал
   бы фотографию, а гость в этот момент читает, а не смотрит. */
/* ВЫСОТА СЧИТАНА ПО СОДЕРЖИМОМУ, А НЕ НА ГЛАЗ: грабер 20 + оценка 23 + подпись 28 + две строки
   текста 46 + кнопка 52 ≈ 176. ⚠ Первая редакция 09.08 ставила 132 и резала строку посередине,
   а кнопка наезжала на текст — поймано СНИМКОМ ЭКРАНА, замер этого не видел (он проверял
   присутствие узлов, а не то, что кадр читается). Меняешь состав шторки — пересчитай число. */
.pv--note { --pv-noteh: 176px; --pv-footh: var(--pv-noteh); }
.pv__note {
  grid-area: bot;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding: 0 var(--pv-gx) calc(12px + env(safe-area-inset-bottom, 0px));
}
.pv__note.is-open {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  max-height: min(72vh, calc(var(--pv-vh) - 96px));
  z-index: 4;                      /* выше полос: развёрнутая шторка — верхний слой на экране */
}
.pv__note-scroll { min-height: 0; overflow: hidden; }
.pv__note.is-open .pv__note-scroll {
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
.pv .pv__note[hidden], .pv .pv__grab[hidden], .pv .pv__note-goto[hidden] { display: none; }

/* На широком экране шторка не растягивается во всю ширину монитора: строка в 1400px не
   читается. Держим её колонкой той же ширины, что и текст в остальном проекте. */
@media (min-width: 1024px) {
  .pv--note { --pv-noteh: 190px; }
  .pv__note { width: min(760px, 100%); margin-inline: auto; }
  .pv__note.is-open { left: 50%; right: auto; transform: translateX(-50%); }
}

/* ПОЛЕ ПОД СТРЕЛКИ ЗА КАДРОМ. Стрелки живут ровно там, где есть мышь (на таче их нет вовсе),
   поэтому и место под них резервируется по тому же признаку, а не по ширине экрана: узкое окно
   на компьютере — это тоже мышь, и там стрелке нужно куда встать.
   Арифметика поля: 44 кнопка + 12 зазор до снимка + 12 до края экрана = 68. Потолок снимка
   считается из `--pv-gx` (раздел 1), значит по бокам гарантированно остаётся 68px, и стрелка
   физически не может уехать за экран — это исключено расчётом, а не подобранным отступом.
   Сетке галереи поле не нужно: стрелок в ней нет, там листают прокруткой. */
@media (hover: hover) {
  .pv:not(.pv--grid) { --pv-gx: 68px; }
}

@media (prefers-reduced-motion: reduce) {
  .pv.is-ready .pv__on { transition: none; }
}
