/* ПОЛНОЭКРАННЫЙ ПРОСМОТР РОЛИКОВ.
 *
 * ТЁМНЫЙ ВСЕГДА, как и экран камеры: светлый интерфейс поверх видео слепит и мешает
 * смотреть. Это не «тема не поддержана» — это осознанное решение для полноэкранного
 * показа; светлая тема сайта под просмотрщиком остаётся нетронутой.
 *
 * Геометрия: один ролик на экран, прокрутка с привязкой. Ничего между роликами нет —
 * ни отступов, ни разделителей: пауза между кадрами и есть то, что читается как рывок.
 */

.rv{
  position:fixed;inset:0;z-index:2147483000;
  background:#000;color:#fff;
  display:block;overscroll-behavior:contain;
  -webkit-tap-highlight-color:transparent}

/* Пока открыт просмотрщик, страница под ним не прокручивается: иначе палец уводит
   ленту, а человек думает, что листает ролики. */
html.rv-open,html.rv-open body{overflow:hidden;overscroll-behavior:none}

.rv__track{
  position:absolute;inset:0;overflow-y:auto;overflow-x:hidden;
  scroll-snap-type:y mandatory;scroll-behavior:auto;
  overscroll-behavior-y:contain;
  scrollbar-width:none;-ms-overflow-style:none}
.rv__track::-webkit-scrollbar{display:none}

/* LAYER 1 — KEYBOARD-IMMUTABLE. Высота слайда = 100% от .rv__track (position:absolute;
   inset:0 внутри .rv position:fixed;inset:0 → определённая высота layout viewport). НЕ 100dvh:
   на реальном iPhone (замер) слой viewport при клавиатуре НЕ сжимается (clientHeight/fixed.h/
   dvh все стабильны), НО ключевое слово dvh заставляет браузер ПЕРЕ-ВЫЧИСЛЯТЬ dvh-боксы на
   каждое viewport-событие клавиатуры → relayout слайда → инвалидация compositing-слоя <video>
   → БЕЛЫЙ КАДР (поймано в burst 43fps, кадр 269/465). height:100% даёт ТОТ ЖЕ прямоугольник
   (равен fixed .rv), но без dvh-переоценки → ноль keyboard-driven relayout ролика. */
.rv__slide{
  position:relative;display:block;
  width:100%;height:100%;min-height:100%;
  scroll-snap-align:start;scroll-snap-stop:always;
  background:#000;overflow:hidden}

/* ПУСТАЯ ПОЛОСА ВМЕСТО НЕОТРИСОВАННЫХ СЛАЙДОВ.
   Держит высоту тех роликов, которых сейчас нет в разметке, чтобы место слайда на полосе
   задавал его номер в списке, а не состав окна. Точкой привязки быть НЕ должна: иначе
   палец залипал бы на пустоте между роликами. Высоту ставит render(). */
.rv__spacer{display:block;width:100%;flex:0 0 auto;height:0;
  scroll-snap-align:none;scroll-snap-stop:normal;
  pointer-events:none;background:#000}
.rv__stage{position:absolute;inset:0;display:block;background:#000;overflow:hidden}

/* ── ЧЕМ ЗАПОЛНЕН ЭКРАН ──
 *
 * По умолчанию кадр вписывается ЦЕЛИКОМ: обрезать сильно непохожую пропорцию (4:5 на
 * узком телефоне) значит выбросить почти половину кадра.
 *
 * Но у РОДНОЙ вертикали 9:16 на экране 19.5:9 «вписать целиком» даёт чёрные полосы
 * сверху и снизу — в полноэкранном разделе это читается как поломка, а не как забота о
 * кадре. Поэтому там, где обрезка мала (класс is-fill ставит скрипт, посчитав её по
 * настоящим размерам кадра), экран заполняется целиком.
 *
 * Полосы, если они всё же остаются, закрывает размытая подложка того же кадра —
 * чёрный прямоугольник рядом с видео выглядит дешевле всего. */
.rv__blur{
  position:absolute;left:0;top:0;width:100%;height:100%;
  object-fit:cover;transform:scale(1.15);
  filter:blur(30px) brightness(.45) saturate(1.1);
  opacity:.9;pointer-events:none;display:block}
.rv__slide.is-fill .rv__blur{display:none}

.rv__video,.rv__cover{
  position:absolute;left:0;top:0;width:100%;height:100%;
  object-fit:contain;object-position:center;display:block}
.rv__slide.is-fill .rv__video,
.rv__slide.is-fill .rv__cover{object-fit:cover}
/* ГОРИЗОНТ / КВАДРАТ (класс ставит buildSlide из размеров DTO): вписываем ЦЕЛИКОМ по центру на ЧЁРНОМ
   фоне (стейдж уже #000). Размытую подложку НЕ показываем — ТЗ: чёрный letterbox сверху/снизу. Явно
   форсим object-fit:contain (video И poster одинаково) — гарантия «не режется через cover», даже если
   is-fill где-то сработал; poster→video без скачка fit. Вертикаль (--portrait) не трогаем. */
.rv__slide--landscape .rv__blur,
.rv__slide--square .rv__blur{display:none}
.rv__slide--landscape .rv__video,.rv__slide--landscape .rv__cover,
.rv__slide--square .rv__video,.rv__slide--square .rv__cover{object-fit:contain}
.rv__video{background:transparent;z-index:1}
.rv__cover{
  z-index:2;pointer-events:none;opacity:1;transition:opacity .2s ease-out}
.rv__slide.is-vframe .rv__cover{opacity:0}
@media (prefers-reduced-motion:reduce){
  .rv__cover{transition:none}
}

/* Слой нажатия занимает кадр, но НЕ заходит под столбец действий и подпись:
   иначе нажатие на «Нравится» превращалось бы в паузу. */
.rv__tap{
  position:absolute;left:0;top:0;right:76px;bottom:132px;z-index:2;
  -webkit-appearance:none;appearance:none;border:0;padding:0;
  background:transparent;cursor:pointer}
.rv__tap:focus{outline:0}
.rv__tap:focus-visible{outline:2px solid #fff;outline-offset:-4px}

/* ЦЕНТРАЛЬНЫЙ PLAYBACK-OVERLAY (по тапу свободной области кадра). SOUND сверху (небольшой круг),
   PLAY/PAUSE снизу (крупнее). Каждая кнопка — отдельный мягкий полупрозрачный круг: без общей панели,
   без blur-прямоугольника, без рамок, без текста, без оранжевого. Скрыт по умолчанию; показ по классу
   rv--controls. В comments и во время свайпа — всегда скрыт. Контейнер событий не ловит (pointer-events
   none) — тап мимо кнопок проходит на слой кадра и прячет overlay; кликабельны только сами кнопки. */
.rv__controls{
  position:absolute;left:50%;top:50%;translate:-50% -50%;z-index:6;
  display:flex;flex-direction:column;align-items:center;gap:16px;
  opacity:0;pointer-events:none;transition:opacity .18s ease}
.rv--controls .rv__controls{opacity:1}
.rv--controls .rv__cbtn{pointer-events:auto}
.rv--comments .rv__controls,.rv--scrolling .rv__controls{opacity:0;pointer-events:none}
.rv__cbtn{
  -webkit-appearance:none;appearance:none;border:0;padding:0;cursor:pointer;
  display:flex;align-items:center;justify-content:center;border-radius:999px;
  background:rgba(0,0,0,.44);color:#fff;-webkit-tap-highlight-color:transparent}
.rv__cbtn:focus{outline:0}
.rv__cbtn:focus-visible{outline:2px solid #fff;outline-offset:2px}
.rv__cbtn--sound{width:44px;height:44px}
.rv__cbtn--play{width:64px;height:64px}
@media (prefers-reduced-motion:reduce){ .rv__controls{transition:none} }

/* PERSISTENT SLIDE UI (avatar/author/caption/meta, столбец like/comments/bookmark, «Добавить
   комментарий…») во время свайпа НЕ гаснет: эти элементы — ДЕТИ слайда (buildMeta/buildActions/
   buildAddComment внутри buildSlide) и едут ВМЕСТЕ с роликом. Прежний fade на .rv--scrolling ошибочно
   считал их root-оверлеями (артефакт: аватар/текст/иконки/поле «дособирались» через 140ms после
   settle — «сначала голое видео, потом UI»). Гасим на свайпе ТОЛЬКО transient .rv__controls (см. выше,
   строка 93) — центральные Sound + Play/Pause. */

/* ЕДИНЫЙ МЯГКИЙ НИЖНИЙ SCRIM (P0-3). Один плавно растворяющийся вверх градиент во всю ширину даёт
   читаемость автора/подписи/действий/поля БЕЗ отдельной прямоугольной панели и без жёсткой верхней
   границы, «режущей» видео. Живёт на слайде (z над видео/cover, под meta/actions), поэтому едет с
   роликом, а не висит отдельной полосой. В COMMENTS MODE выключен — там ролик в компактной полосе. */
.rv__slide::after{
  content:"";position:absolute;left:0;right:0;bottom:0;z-index:3;pointer-events:none;
  height:40%;max-height:300px;
  background:linear-gradient(0deg,rgba(0,0,0,.55) 0%,rgba(0,0,0,.26) 32%,rgba(0,0,0,0) 100%)}
/* ═══════════ COMMENTS = НЕЗАВИСИМЫЙ OVERLAY, REEL IMMUTABLE (VK-модель) ═══════════
 * Reel — НЕИЗМЕНЯЕМЫЙ fullscreen базовый слой. При открытии комментариев его геометрия НЕ меняется:
 * .rv/.rv__track/.rv__slide/video остаются во весь экран (тот же <video> node, играет). Comments —
 * независимый bottom-sheet (.fcm) ПОВЕРХ ролика (геометрия листа — в pub-feed.css: верх листа привязан
 * к экрану, низ — над клавиатурой; Reel лист не двигает). Клавиатура влияет ТОЛЬКО на лист/композер.
 * .rv--comments теперь ЛИШЬ прячет собственные оверлеи ролика, попадающие под лист (мета/действия/поле/
 * тап/скрим/центральные контролы) — display:none, БЕЗ resize/remount/rebuild. Верх ролика виден над
 * листом; «Ролики»/«закрыть» остаются сверху. Старой resize-полосы (rv__track height 42% от --dc-vv-*,
 * display:none соседних слайдов, object-fit:contain) больше НЕТ — это устраняет скачок Reel в корне. */
.rv--comments .rv__meta,
.rv--comments .rv__actions,
.rv--comments .rv__addcomment,
.rv--comments .rv__tap,
.rv--comments .rv__slide::after{display:none}

/* Нижнее поле «Добавить комментарий…» съедает низ ролика — автор/подпись и столбец действий
   поднимаем над ним ровно на его высоту. */
.rv{--rv-addc-h:calc(50px + env(safe-area-inset-bottom,0))}

/* Заголовок раздела сверху по центру — поверх ролика, safe-area, событий не ловит. */
/* Верхняя строка: chevron слева и заголовок «Ролики» имеют ОДИН вертикальный центр. Заголовок —
   полоса 44px (та же высота и тот же top, что у .rv__close), align-items:center → его центр совпадает
   с центром кнопки. left:0/right:0 + justify-content:center держат «Ролики» по центру ВЬЮПОРТА (а не
   «по центру места после стрелки»). Chevron лежит слева поверх этой полосы, текст по центру с ним не
   сталкивается. safe-area учтена один раз (в top). */
.rv__title{
  position:absolute;left:0;right:0;top:calc(10px + env(safe-area-inset-top,0));z-index:7;
  height:44px;display:flex;align-items:center;justify-content:center;
  text-align:center;pointer-events:none;
  color:#fff;font-size:15px;font-weight:600;letter-spacing:-.01em;
  text-shadow:0 1px 4px rgba(0,0,0,.55)}

/* ── автор и подпись ── */
.rv__meta{
  position:absolute;left:0;right:76px;bottom:var(--rv-addc-h);z-index:4;
  display:flex;flex-direction:column;gap:6px;
  padding:0 12px 12px calc(12px + env(safe-area-inset-left,0))}
  /* Свой прямоугольный градиент убран — читаемость даёт общий нижний scrim (.rv__slide::after). */
.rv__who{
  -webkit-appearance:none;appearance:none;border:0;padding:0;
  display:flex;align-items:center;gap:8px;
  background:transparent;color:inherit;cursor:pointer;text-align:left;
  min-height:44px}
/* Аватар автора — СЛОТ, а не картинка: внутри либо фотография, либо инициалы. Разметку
   строит buildMeta() тем же способом, что fillAvatar в ленте. Типографику инициалов задаёт
   общий компонент ds-avatar-fallback по --ds-av-size (см. список адоптеров в нём).
   ПОДЛОЖКА ЗДЕСЬ СВОЯ, И ЭТО ОСОЗНАННО: Ролики — единственная поверхность, где аватар лежит
   не на панели, а поверх произвольного движущегося кадра. Панельные цвета DS на видео теряют
   контраст, полупрозрачная белая — нет. Размер и кегль при этом общие, второго описания
   контракта не заводится. */
.rv__avatar{
  width:32px;height:32px;flex:0 0 auto;border-radius:999px;overflow:hidden;
  display:grid;place-items:center;
  --ds-av-size:32px;
  --ds-av-bg:rgba(255,255,255,.16);
  --ds-av-ink:rgba(255,255,255,.92);
  --ds-av-border:rgba(255,255,255,.24);
  background:var(--ds-av-bg)}
.rv__avatar img{width:100%;height:100%;object-fit:cover;display:block}
.rv__name{font-size:14px;font-weight:600;letter-spacing:-.01em}
.rv__official{
  width:14px;height:14px;flex:0 0 auto;border-radius:999px;
  background:var(--pub-brand,#e8651c)}
.rv__when{font-size:12px;color:rgba(255,255,255,.62)}
/* Строка «автор + Подписаться» на одной линии. who ужимается (имя — ellipsis), follow не сжимается и
   всегда виден. Приоритет при нехватке места: аватар → имя → «Подписаться» → время. Без h-overflow на
   320/375/390/430 (meta уже отступает от правого rail на right:76px). */
.rv__idrow{display:flex;align-items:center;gap:10px;min-width:0}
.rv__idrow .rv__who{flex:0 1 auto;min-width:0}
.rv__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.rv__follow{
  flex:0 0 auto;-webkit-appearance:none;appearance:none;
  padding:5px 12px;min-height:26px;border-radius:999px;
  border:0;background:#4a90e2;
  color:#fff;font:inherit;font-size:12px;font-weight:600;line-height:1;
  white-space:nowrap;cursor:pointer;-webkit-tap-highlight-color:transparent}
.rv__follow:active{background:#3f7fc9}
.rv__follow-label{pointer-events:none}
/* Подписан → кнопки нет вовсе (не «Вы подписаны», не disabled). syncFollowState тоглит .is-on по всему
   документу (лента↔Reels) — так один store прячет/показывает кнопку без второго состояния. */
.rv__follow.is-on{display:none}
.rv__text{
  margin:0;font-size:13.5px;line-height:1.38;color:rgba(255,255,255,.94);
  white-space:pre-wrap;overflow-wrap:anywhere}
.rv__text.is-clamped{
  display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
.rv__more-text{
  -webkit-appearance:none;appearance:none;border:0;padding:2px 0;
  align-self:flex-start;background:transparent;cursor:pointer;
  font-size:12.5px;color:rgba(255,255,255,.7)}

/* ── столбец действий ── */
.rv__actions{
  position:absolute;right:0;bottom:var(--rv-addc-h);z-index:5;
  display:flex;flex-direction:column;align-items:center;gap:14px;
  padding:0 calc(10px + env(safe-area-inset-right,0)) 14px 10px}

/* ── нижнее поле «Добавить комментарий…» (P0-D) ── */
.rv__addcomment{
  position:absolute;left:0;right:0;bottom:0;z-index:6;
  display:flex;align-items:center;height:var(--rv-addc-h);
  padding:0 calc(14px + env(safe-area-inset-right,0)) env(safe-area-inset-bottom,0) calc(14px + env(safe-area-inset-left,0));
  -webkit-appearance:none;appearance:none;cursor:pointer;text-align:left;
  /* Ни тёмной прямоугольной подложки, ни blur, ни верхней границы — поле лежит поверх видео и
     читается за счёт общего нижнего scrim (.rv__slide::after). Раньше bg+blur+border-top читались
     как отдельная нижняя панель, «режущая» ролик (P0-3). */
  border:0;background:transparent}
.rv__addcomment-text{flex:1 1 auto;color:rgba(255,255,255,.72);font-size:13.5px}
.rv__addcomment:focus{outline:0}
.rv__addcomment:focus-visible{outline:2px solid rgba(255,255,255,.6);outline-offset:-2px}

/* ГОСТЕВОЙ ПРИЗЫВ ВОЙТИ — НА МЕСТЕ НИЖНЕГО ПОЛЯ КОММЕНТАРИЯ.

   ЧТО БЫЛО СЛОМАНО. Кнопка брала себе высоту ПОЛОСЫ (--rv-addc-h), а полоса — это
   50px содержимого ПЛЮС безопасная зона снизу. Поле комментария рядом компенсирует
   эту зону нижним отступом, а кнопка — нет, и вдобавок сама поднималась на неё же
   через bottom. На айфоне с домашним индикатором зона равна 34px, поэтому кнопка
   вырастала до 84px и всплывала над краем: одна и та же величина учитывалась дважды.
   Ширина при этом тянулась от края до края, и внизу ролика оказывалась оранжевая
   плита вместо призыва.

   КАК СТАЛО. Полоса остаётся прежней (автор, подпись и столбец действий поднимаются
   над ней ровно так же — второй геометрии не заводится), но кнопка внутри неё живёт
   по контракту канонической .pub-login внизу ленты: та же ширина колонки
   min(420px, ширина − 28px) по центру и высота самого компонента .ds-auth-primary
   (52px). Безопасная зона учитывается ОДИН раз — отступом снизу. */
.rv__login{
  position:absolute;left:50%;transform:translateX(-50%);z-index:6;
  display:flex;align-items:center;justify-content:center;
  width:min(420px,calc(100% - 28px));
  bottom:calc(8px + env(safe-area-inset-bottom,0));
  text-decoration:none}
/* Нажатие: у центрированной кнопки своё преобразование, поэтому scale добавляется к
   translateX — общий :active компонента его бы обнулил (тот же приём, что у .pub-login). */
.rv__login:active{transform:translateX(-50%) scale(.985)}
/* В открытых комментариях низ ролика занят панелью — призыв прячется вместе с полем. */
.rv--comments .rv__login{display:none}
.rv__act{
  -webkit-appearance:none;appearance:none;border:0;
  display:flex;flex-direction:column;align-items:center;gap:3px;
  min-width:44px;min-height:44px;padding:4px;
  background:transparent;color:#fff;cursor:pointer;
  filter:drop-shadow(0 1px 2px rgba(0,0,0,.5))}
.rv__act:focus{outline:0}
.rv__act:focus-visible{outline:2px solid #fff;outline-offset:2px;border-radius:12px}
.rv__act.is-on{color:var(--pub-brand,#e8651c)}
.rv__act.is-on svg{fill:currentColor}
/* Активное избранное — заполненный БЕЛЫЙ bookmark (оранжевый закреплён за like; у save своего цвета
   акцента нет). Специфичнее .rv__act.is-on, поэтому цвет побеждает; заливку даёт правило выше. */
.rv__act[data-rv-save].is-on{color:#fff}
/* Счётчики like/comments/bookmark — единый контракт: ВСЕГДА белые (в т.ч. при active like/bookmark,
   где кнопка красится в акцент — счётчик задаёт свой цвет и не наследует его), один размер/вес/
   line-height, по центру своей иконки (родитель .rv__act — flex-column, align-items:center). */
.rv__count{
  color:#fff;font-size:12px;font-weight:600;line-height:1;
  font-variant-numeric:tabular-nums;letter-spacing:.01em;text-align:center;
  text-shadow:0 1px 2px rgba(0,0,0,.5)}
.rv__count:empty{display:none}

/* ── назад ──
   Белый chevron «‹» БЕЗ тёмной круглой подложки (как в референсе). Тач-таргет остаётся 44×44 —
   кликабельна вся область кнопки, а видна только стрелка (drop-shadow держит читаемость над светлым
   кадром). Поведение не меняется: тот же canonical requestClose() → history.back(). */
.rv__close{
  position:absolute;z-index:6;
  left:calc(6px + env(safe-area-inset-left,0));
  top:calc(10px + env(safe-area-inset-top,0));
  -webkit-appearance:none;appearance:none;border:0;background:transparent;
  width:44px;height:44px;display:flex;align-items:center;justify-content:center;
  color:#fff;cursor:pointer;filter:drop-shadow(0 1px 3px rgba(0,0,0,.55))}
.rv__close:focus{outline:0}
.rv__close:focus-visible{outline:2px solid #fff;outline-offset:2px;border-radius:12px}

/* ── пусто ── */
.rv__empty{
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;
  height:100dvh;padding:24px;text-align:center}
.rv__empty-text{margin:0;font-size:14px;color:rgba(255,255,255,.78);max-width:26ch}
.rv__btn{
  -webkit-appearance:none;appearance:none;border:0;
  min-height:44px;padding:0 18px;border-radius:999px;
  background:var(--pub-brand,#e8651c);color:#fff;
  font-size:13.5px;font-weight:600;cursor:pointer}

/* ── компьютер ──
   Растягивать вертикальный ролик на широкий монитор незачем: он от этого не станет
   шире. Показ ограничивается по ширине и центрируется, поля остаются СНАРУЖИ кадра. */
@media (min-width:768px){
  .rv__slide{display:flex;align-items:center;justify-content:center}
  .rv__stage{
    position:relative;inset:auto;
    width:min(calc(100dvh * 9 / 16), 480px);height:100dvh;
    margin:0 auto}
  .rv__meta{right:auto;left:50%;width:min(calc(100dvh * 9 / 16), 480px);
    translate:-50% 0;padding-right:76px}
  .rv__actions{right:50%;translate:calc(min(50dvh * 9 / 16, 240px) + 0px) 0}
}

/* Узкий телефон: подписи и счётчики не должны наезжать друг на друга. */
@media (max-width:360px){
  .rv__actions{gap:11px;padding-right:6px}
  .rv__act{min-width:42px;padding:2px}
  .rv__meta{right:66px}
  .rv__tap{right:66px}
}

/* Телефон в горизонтали: высоты почти нет, столбец действий поджимается. */
@media (orientation:landscape) and (max-height:560px){
  .rv__actions{gap:8px;padding-bottom:calc(8px + env(safe-area-inset-bottom,0))}
  .rv__meta{padding-bottom:calc(8px + env(safe-area-inset-bottom,0))}
  .rv__act{min-height:40px;min-width:40px}
}

/* ═══ ВИДЕО НЕДОСТУПНО ═══
   Плашка молчит, пока движок не поставит на слайд data-pub-video-play="failed".
   Полупрозрачная: под ней остаётся кадр, поэтому слайд не становится пустым. Геометрию
   не трогает и нажатий не перехватывает — свайпнуть дальше можно как обычно. */
.rv__fail{display:none}
.rv__slide[data-pub-video-play="failed"] .rv__fail{
  position:absolute;inset:0;z-index:4;
  display:flex;flex-direction:column;gap:6px;
  align-items:center;justify-content:center;text-align:center;
  padding:16px 24px;background:rgba(8,10,14,.58);
  pointer-events:none}
.rv__fail-title{
  color:rgba(255,255,255,.95);font-size:15px;font-weight:520;line-height:1.4}
.rv__fail-note{
  color:rgba(255,255,255,.66);font-size:12px;line-height:1.45}
/* Слушать нечего — кнопка звука на сломанном слайде не нужна. */
.rv__slide[data-pub-video-play="failed"] [data-pub-video-sound]{display:none}

/* ═══ ЖДЁМ ДАННЫХ ═══
   Как и в ленте: признак ставится сразу, видимым слой становится через полсекунды —
   короткая докачка проходит незаметно, мигания на каждой заминке нет. */
.rv__wait{
  position:absolute;left:50%;top:50%;width:30px;height:30px;margin:-15px 0 0 -15px;
  z-index:3;pointer-events:none;opacity:0;
  border:2px solid rgba(255,255,255,.22);border-top-color:rgba(255,255,255,.82);
  border-radius:50%;transition:opacity .18s ease-out}
.rv__slide[data-pub-video-play="waiting"] .rv__wait{
  opacity:1;transition-delay:.45s;animation:rvSpin .9s linear infinite}
@keyframes rvSpin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion: reduce){
  .rv__slide[data-pub-video-play="waiting"] .rv__wait{animation:none}
}

/* ═══════════════════════════════════════════════════════════════════════════
   ВСПЫШКА СЕРДЕЦ ПРИ ДВОЙНОМ НАЖАТИИ

   Слой временный: скрипт создаёт его в точке касания и снимает целиком по своему
   таймеру. Постоянных узлов анимации у ролика не заводится — иначе после сотни
   нажатий их накопилось бы столько же.

   pointer-events:none ОБЯЗАТЕЛЬНО. Слой лежит поверх зоны нажатия, и без этого он
   перехватывал бы свайп ровно в тот миг, когда человек листает дальше.

   z-index 5 — между подложкой действий (3-4) и центральной кнопкой показа (6):
   сердца видны поверх затемнения, но не перекрывают управление.
   ═══════════════════════════════════════════════════════════════════════════ */
.rv__hearts{
  position:absolute;width:0;height:0;z-index:5;
  pointer-events:none;
  /* Собственного фона и размера у слоя нет: он только точка отсчёта. */
  contain:layout style}

.rv__heart{
  position:absolute;left:0;top:0;
  /* Смещение — ровно половина размера значка (46px): центр сердца совпадает с точкой
     касания, а не его левый верхний угол. */
  display:block;margin:-23px 0 0 -23px;
  color:#ff3b5c;
  /* ВНЕШНИЙ УЗЕЛ ВЕЗЁТ ТОЛЬКО ПЕРЕМЕЩЕНИЕ — одним отрезком, без промежуточных кадров.
     Промежуточная точка означала бы стык, а на стыке сглаживание начинается заново: именно
     из этого и складывался рывок. */
  will-change:transform;
  transform:translateZ(0);
  animation:rv-heart-rise var(--rv-heart-dur,820ms) cubic-bezier(.16,.62,.28,1)
            var(--rv-delay,0ms) both}
.rv__heart svg{display:block;fill:currentColor;stroke:currentColor;stroke-width:1.2;
  /* РОСТ И ПРОЗРАЧНОСТЬ ЖИВУТ ЗДЕСЬ, на вложенном узле: два transform на разных
     элементах не спорят друг с другом, и перемещение остаётся непрерывным. */
  opacity:0;transform-origin:50% 50%;backface-visibility:hidden;
  will-change:transform,opacity;
  animation:rv-heart-pop var(--rv-heart-dur,820ms) linear var(--rv-delay,0ms) both;
  filter:drop-shadow(0 2px 6px rgba(0,0,0,.35))}

/* ═══ ДВИЖЕНИЕ И РОСТ РАЗВЕДЕНЫ ПО РАЗНЫМ ЭЛЕМЕНТАМ ═══
 *
 * ПОЧЕМУ БЫЛ РЫВОК. Всё — перемещение, масштаб, поворот, прозрачность — жило в ОДНОМ
 * наборе кадров с промежуточными точками 14 %, 34 %, 68 %. Браузер применяет функцию
 * сглаживания не ко всей анимации, а МЕЖДУ КАЖДОЙ ПАРОЙ соседних кадров. С сильным
 * ease-out каждый отрезок разгонялся и тормозил отдельно, а на стыке снова разгонялся —
 * получалась пульсация. На устройстве это и читалось как «появилось, прыгнуло, исчезло».
 * Числа тут ни при чём: сколько их ни правь, стыки останутся.
 *
 * ЛЕЧЕНИЕ. Перемещение уезжает на внешний узел и описывается ОДНИМ отрезком 0 → 100 %:
 * промежуточных точек нет, значит и стыков нет — одна непрерывная кривая. Масштаб,
 * поворот и прозрачность живут на вложенном значке своими кадрами. Два независимых
 * transform на разных узлах не конфликтуют и не перебивают друг друга.
 *
 * translate3d, а не translate: отдельный слой композитора вместо пересчёта на главном
 * потоке. Ни одно свойство раскладки (top/left/width/height) во время показа не трогается. */
@keyframes rv-heart-rise{
  from { transform:translate3d(0,0,0) }
  to   { transform:translate3d(var(--rv-dx,0px), var(--rv-dy,-110px), 0) }
}

/* Фазы роста — по договорённости: мягкое появление, рост до среднего, почти ровный
   участок, плавный уход. У КАЖДОГО отрезка своя функция сглаживания, подобранная так,
   чтобы скорость на стыке не менялась скачком: разгон — только в начале, дальше
   равномерно, торможение — только в конце. */
@keyframes rv-heart-pop{
  0%   { opacity:0; transform:scale(.30) rotate(0deg);
         animation-timing-function:cubic-bezier(.25,.85,.35,1) }        /* появление */
  10%  { opacity:1; transform:scale(.52) rotate(calc(var(--rv-rot,0deg) * .25));
         animation-timing-function:cubic-bezier(.22,.72,.30,1) }        /* рост */
  35%  { opacity:1; transform:scale(var(--rv-peak,1)) rotate(calc(var(--rv-rot,0deg) * .7));
         animation-timing-function:linear }                              /* ровный участок */
  70%  { opacity:.94; transform:scale(calc(var(--rv-peak,1) * .97)) rotate(var(--rv-rot,0deg));
         animation-timing-function:cubic-bezier(.4,0,.7,1) }             /* уход */
  100% { opacity:0; transform:scale(calc(var(--rv-peak,1) * .88)) rotate(var(--rv-rot,0deg)) }
}

/* ПРОСЬБА УМЕНЬШИТЬ ДВИЖЕНИЕ УВАЖАЕТСЯ.
   Подтверждение жеста остаётся — одно сердце коротко проявляется и гаснет на месте.
   Полёта, разлёта и поворотов нет. */
.rv__hearts--calm .rv__heart{
  /* Половина от 52px — размер спокойного показа. Полёта нет вовсе: внешний узел стоит. */
  margin:-26px 0 0 -26px;
  animation:none}
.rv__hearts--calm .rv__heart svg{
  animation:rv-heart-calm 320ms ease-out both}
@keyframes rv-heart-calm{
  0%   { opacity:0; transform:scale(.7) }
  35%  { opacity:1; transform:scale(1) }
  100% { opacity:0; transform:scale(1) }
}

@media (prefers-reduced-motion: reduce){
  /* Даже если разметка пришла обычной — движение всё равно снимается. Настройка системы
     важнее того, что решил скрипт: он мог не успеть её прочитать.
     Гасим ИМЕННО полёт (внешний узел) и оставляем короткое проявление на значке:
     подтверждение жеста должно остаться, движения быть не должно. */
  .rv__heart{ animation:none !important; transform:none !important }
  .rv__heart svg{ animation:rv-heart-calm 320ms ease-out both !important }
}
