/* ==========================================================================
   ViewChat — Connection Story: одна история соединения Seoul ↔ Paris
   Изолированный слой стилей: всё внутри .vc-story, глобальные стили не трогаем.

   Опорное решение файла: CSS отдаёт ФИНАЛЬНУЮ сцену, а JS её разбирает.
   Значение каждого токена по умолчанию — конечное состояние (--vcs-warm: 1,
   --vcs-line: 1, --vcs-o: 1), и каждый var()-фолбэк тоже конечное состояние.
   Отсюда бесплатно: <noscript> показывает собранную тёплую сцену, .is-static —
   то же самое, а опечатка в имени переменной даёт законченный кадр, а не
   чёрный прямоугольник.

   Состояния, которые ставит js/viewchat-connection-story.js:
     .is-static — sticky-анимация отключена (prefers-reduced-motion, нет
                  overflow: clip): секция раскладывается в нормальном потоке
                  и показывает финальный цветной кадр целиком;
     .is-photo  — на секции: снимок загрузился (по событию load обоих слоёв).

   Сюжет несёт ОДИН кадр 16:9 (CONNECTION_STORY.backgroundImage в JS): Сеул
   слева, Париж справа, пустая тёмная середина под центральной типографикой.
   Абстрактные области Сеула и Парижа остались фолбэком и работают ровно до
   тех пор, пока снимок не загрузился: битый путь не ломает секцию.
   ========================================================================== */

.vc-story {
  /* --- Палитра ---------------------------------------------------------- */
  --vcs-ink: #07080b;
  --vcs-ink-2: #0b0d12;
  --vcs-mono-hi: #d6dde6;
  --vcs-mono-mid: #6c7480;

  /* Единственный акцентный цвет ViewChat. */
  --vcs-coral: #f2565c;
  --vcs-coral-hi: #ff6d73;

  /* Настроение сторон: холодный неон Сеула и тёплое золото Парижа. */
  --vcs-seoul-cyan: #4fc8ff;
  --vcs-paris-gold: #ffc46b;

  /* Общий огонь городов — тот же, что --vcg-lights в секции глобуса: две
     тёмные секции страницы должны звучать одним светом. */
  --vcs-lights: #ffe6bc;

  /* --- Типографика финальной сцены -------------------------------------- */
  /* Кегль клейма — единственный источник и для типографики, и для геометрии:
     смещения соседних панелей считаются от него (высота клейма ровно
     3 * line-height * кегль), поэтому блок держит свой визуальный центр на
     любой ширине кадра, а не только на той, где его выверяли. */
  --vcs-claim-fs: clamp(48px, 4.4vw, 76px);
  --vcs-claim-lh: 0.96;
  /* Высота клейма в три строки. */
  --vcs-claim-h: calc(3 * var(--vcs-claim-lh) * var(--vcs-claim-fs));

  /* Ось связи. Вокруг неё собран финальный блок: eyebrow над осью, клейм под
     ней. Ось же держит и провод, поэтому линия читается как типографская
     линейка внутри блока, а не как отдельный виджет. */
  --vcs-axis: 32%;

  /* --- Геометрия -------------------------------------------------------- */
  /* Высота скролл-дорожки. Прогресс нормализованный, поэтому границы окон в
     JS от изменения этого значения не сдвигаются. */
  --vcs-track: 520vh;
  --vcs-header: 92px;

  /* --- Состояние сцены (пишется из JS каждый кадр) ----------------------- */
  /* Значения по умолчанию — финал истории. См. шапку файла. */
  --vcs-wake: 1;      /* 0 — области почти не видны, 1 — проявлены */
  --vcs-approach: 1;  /* 0 — области разведены, 1 — съехались навстречу */
  --vcs-warm: 1;      /* 0 — монохром, 1 — цвет */
  --vcs-line: 1;      /* 0 — линии нет, 1 — половины встретились в центре */
  --vcs-dot: 1;       /* 0 — точек нет, 1 — точки на месте */
  --vcs-flare: 0.55;  /* мягкая вспышка вокруг точек */
  --vcs-pulse: 0;     /* положение бегущего импульса вдоль оси, 0..1 */
  --vcs-pulse-o: 0;   /* его прозрачность */

  position: relative;
  z-index: 2;
  height: var(--vcs-track);
  background: var(--vcs-ink);
  color: var(--vcs-mono-hi);
  isolation: isolate;
}

/* Светлые разделители шаблона у краёв тёмного блока читаются как шов — тот же
   приём, что уже применён у секции глобуса. */
.vc-story + hr.divider,
hr.divider:has(+ .vc-story) {
  display: none;
}

/* --- Сцена ---------------------------------------------------------------- */

.vc-story__stage {
  position: sticky;
  top: 0;
  /* Строго 100vh. svh/dvh здесь запрещены: readProgress() делит на
     window.innerHeight, и с динамическими единицами прогресс перестал бы
     упираться в 0 и 1 на краях залипания. */
  height: 100vh;
  min-height: 600px;
  overflow: hidden;
  /* Инвалидация стиля не выходит за пределы сцены. strict/paint-only нельзя:
     contain: strict сделал бы элемент containing block и сломал бы sticky у
     самой сцены — здесь это безопасно, потому что sticky живёт на ней самой,
     а не на потомке. */
  contain: layout paint;
  background:
    radial-gradient(72% 54% at 18% 40%, rgba(26, 32, 42, 0.55) 0%, rgba(7, 8, 11, 0) 70%),
    radial-gradient(72% 54% at 82% 40%, rgba(26, 32, 42, 0.5) 0%, rgba(7, 8, 11, 0) 70%),
    linear-gradient(180deg, #0b0d12 0%, #07080b 52%, #05060a 100%);
}

/* Ночное небо: редкие далёкие огни, чтобы фон не был плоской заливкой. */
.vc-story__sky {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(circle at 12% 18%, rgba(214, 221, 230, 0.1) 0 1px, rgba(214, 221, 230, 0) 3px),
    radial-gradient(circle at 31% 9%, rgba(214, 221, 230, 0.07) 0 1px, rgba(214, 221, 230, 0) 3px),
    radial-gradient(circle at 68% 13%, rgba(214, 221, 230, 0.09) 0 1px, rgba(214, 221, 230, 0) 3px),
    radial-gradient(circle at 88% 26%, rgba(214, 221, 230, 0.06) 0 1px, rgba(214, 221, 230, 0) 3px),
    radial-gradient(circle at 22% 82%, rgba(214, 221, 230, 0.06) 0 1px, rgba(214, 221, 230, 0) 3px),
    radial-gradient(circle at 76% 88%, rgba(214, 221, 230, 0.05) 0 1px, rgba(214, 221, 230, 0) 3px);
}

/* --- Атмосферные области -------------------------------------------------- */
/* Два слоя на сторону, оба абстрактные:
     __city  — поле боке: огни разного размера плюс подсвет неба сверху;
     __field — атмосферная толща: вертикальный градиент и мягкий rim-свет
               с внутренней стороны кадра.
   Монохром — базовый слой, он всегда непрозрачен. Цвет — тот же рисунок
   поверх (::after), и в кадре меняется только его opacity. Анимируемый filter
   не годится: Chromium каждый кадр заново выполняет color-matrix, а WebKit при
   смене значения перерастеризует слой (380x660 CSS px при DPR 3 — это ~3 Мп на
   кадр). opacity — единственное свойство, которое все движки меняют на
   композиторе без перерисовки. */

.vc-story__figures {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.vc-story__figure {
  position: absolute;
  /* Область появляется вместе с --vcs-wake и съезжает к центру по
     --vcs-approach. Направление задано на стороне безразмерным --vcs-dir,
     а ось выбирает медиазапрос: JS о брейкпоинте не знает. */
  opacity: calc(0.1 + 0.9 * var(--vcs-wake, 1));
  transform: translate3d(var(--vcs-gx, 0px), var(--vcs-gy, 0px), 0);
  transition: none;
  will-change: opacity, transform;
  /* Кромка растворения стоит на обёртке, а не на отдельном слое: тогда и поле
     боке, и атмосферная толща, и будущее фото гаснут к краям одинаково, и
     область не читается прямоугольной вырезкой. */
  -webkit-mask-image: var(--vcs-edge);
  mask-image: var(--vcs-edge);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

.vc-story__city,
.vc-story__field {
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

.vc-story__city::after,
.vc-story__field::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  opacity: var(--vcs-warm, 1);
  will-change: opacity;
}

.vc-story__city {
  background: var(--vcs-city-bw);
}

.vc-story__city::after {
  background: var(--vcs-city-warm);
}

/* Атмосферная толща. Форма всей области — мягкая виньетка, она задана маской
   на обёртке .vc-story__figure выше: кадр растворяется в ночи и не стоит
   прямоугольником. Никакой фигуры человека здесь нет. */
.vc-story__field {
  background: var(--vcs-field-bw);
}

.vc-story__field::after {
  background: var(--vcs-field-warm);
}

.vc-story {
  /* Кромка растворения. Одна и та же для плейсхолдера и для будущего фото.

     closest-side, а не проценты: радиус в процентах считается от РАЗМЕРА БОКСА,
     и при 78%/74% эллипс вылезал далеко за него — на самой кромке альфа
     оставалась около 0.87, то есть область читалась прямоугольником. При
     closest-side эллипс упирается в ближайшие стороны, и спад гарантированно
     заканчивается ровно на кромке при любых пропорциях области. */
  --vcs-edge:
    radial-gradient(ellipse closest-side at 50% 42%, #000 0 42%, rgba(0, 0, 0, 0.55) 74%, transparent 100%);
}

/* Движки без mask-image получают скруглённое световое пятно — для абстрактной
   области это нормальная форма, а не поломка. */
@supports not ((mask-image: radial-gradient(#000, #000)) or (-webkit-mask-image: radial-gradient(#000, #000))) {
  .vc-story__figure {
    border-radius: 42% 42% 38% 38% / 36% 36% 42% 42%;
    overflow: hidden;
  }
}

/* --- Сеул: неон и холодная улица ------------------------------------------ */
/* rgba-тройки ниже выведены из токенов палитры: 255,95,141 — розовый неон
   вывесок, 79,200,255 = --vcs-seoul-cyan, 255,230,188 = --vcs-lights,
   214,221,230 = --vcs-mono-hi. Произвольных цветов в градиентах нет. */

.vc-story__figure--seoul {
  --vcs-rim-x: 92%;

  --vcs-city-bw:
    radial-gradient(circle at 18% 22%, rgba(214, 221, 230, 0.3) 0 3px, rgba(214, 221, 230, 0.09) 10px, rgba(214, 221, 230, 0) 22px),
    radial-gradient(circle at 74% 16%, rgba(214, 221, 230, 0.22) 0 5px, rgba(214, 221, 230, 0.07) 16px, rgba(214, 221, 230, 0) 34px),
    radial-gradient(circle at 86% 40%, rgba(214, 221, 230, 0.18) 0 4px, rgba(214, 221, 230, 0.06) 12px, rgba(214, 221, 230, 0) 26px),
    radial-gradient(circle at 8% 52%, rgba(214, 221, 230, 0.16) 0 6px, rgba(214, 221, 230, 0.05) 18px, rgba(214, 221, 230, 0) 38px),
    radial-gradient(circle at 34% 8%, rgba(214, 221, 230, 0.24) 0 2px, rgba(214, 221, 230, 0.08) 8px, rgba(214, 221, 230, 0) 18px),
    radial-gradient(circle at 60% 62%, rgba(214, 221, 230, 0.12) 0 7px, rgba(214, 221, 230, 0.04) 20px, rgba(214, 221, 230, 0) 42px),
    radial-gradient(circle at 46% 34%, rgba(214, 221, 230, 0.1) 0 9px, rgba(214, 221, 230, 0) 30px),
    radial-gradient(112% 86% at 50% 6%, rgba(42, 48, 57, 0.85) 0%, rgba(18, 21, 27, 0.42) 46%, rgba(7, 8, 11, 0) 74%);

  --vcs-city-warm:
    radial-gradient(circle at 18% 22%, rgba(255, 95, 141, 0.44) 0 3px, rgba(255, 95, 141, 0.12) 10px, rgba(255, 95, 141, 0) 22px),
    radial-gradient(circle at 74% 16%, rgba(79, 200, 255, 0.34) 0 5px, rgba(79, 200, 255, 0.1) 16px, rgba(79, 200, 255, 0) 34px),
    radial-gradient(circle at 86% 40%, rgba(255, 178, 122, 0.32) 0 4px, rgba(255, 178, 122, 0.09) 12px, rgba(255, 178, 122, 0) 26px),
    radial-gradient(circle at 8% 52%, rgba(255, 95, 141, 0.24) 0 6px, rgba(255, 95, 141, 0.07) 18px, rgba(255, 95, 141, 0) 38px),
    radial-gradient(circle at 34% 8%, rgba(255, 230, 188, 0.34) 0 2px, rgba(255, 230, 188, 0.1) 8px, rgba(255, 230, 188, 0) 18px),
    radial-gradient(circle at 60% 62%, rgba(79, 200, 255, 0.18) 0 7px, rgba(79, 200, 255, 0.05) 20px, rgba(79, 200, 255, 0) 42px),
    radial-gradient(circle at 46% 34%, rgba(255, 95, 141, 0.14) 0 9px, rgba(255, 95, 141, 0) 30px),
    radial-gradient(112% 86% at 50% 6%, rgba(58, 32, 46, 0.9) 0%, rgba(26, 16, 24, 0.48) 46%, rgba(7, 8, 11, 0) 74%);

  --vcs-field-bw:
    radial-gradient(58% 44% at var(--vcs-rim-x) 30%, rgba(226, 232, 240, 0.44) 0%, rgba(226, 232, 240, 0.1) 38%, rgba(226, 232, 240, 0) 66%),
    linear-gradient(180deg, #3a4049 0%, #232830 42%, #12151b 78%, #0a0c11 100%);

  --vcs-field-warm:
    radial-gradient(58% 44% at var(--vcs-rim-x) 30%, rgba(255, 138, 120, 0.52) 0%, rgba(255, 95, 141, 0.14) 38%, rgba(255, 95, 141, 0) 66%),
    radial-gradient(40% 30% at 10% 62%, rgba(79, 200, 255, 0.16) 0%, rgba(79, 200, 255, 0) 70%),
    linear-gradient(180deg, #4a3038 0%, #2c1e26 44%, #150f14 80%, #0a080b 100%);
}

/* --- Париж: золото и медь -------------------------------------------------- */
/* 255,196,107 = --vcs-paris-gold, 255,230,188 = --vcs-lights, 224,122,74 — медь.
   Те же восемь слотов, но огни крупнее и реже: бульварные фонари вместо вывесок. */

.vc-story__figure--paris {
  --vcs-rim-x: 8%;

  --vcs-city-bw:
    radial-gradient(circle at 80% 20%, rgba(214, 221, 230, 0.28) 0 4px, rgba(214, 221, 230, 0.1) 12px, rgba(214, 221, 230, 0) 26px),
    radial-gradient(circle at 26% 15%, rgba(214, 221, 230, 0.2) 0 6px, rgba(214, 221, 230, 0.07) 18px, rgba(214, 221, 230, 0) 38px),
    radial-gradient(circle at 12% 42%, rgba(214, 221, 230, 0.17) 0 5px, rgba(214, 221, 230, 0.06) 14px, rgba(214, 221, 230, 0) 30px),
    radial-gradient(circle at 92% 56%, rgba(214, 221, 230, 0.14) 0 7px, rgba(214, 221, 230, 0.05) 20px, rgba(214, 221, 230, 0) 42px),
    radial-gradient(circle at 66% 8%, rgba(214, 221, 230, 0.22) 0 2px, rgba(214, 221, 230, 0.08) 8px, rgba(214, 221, 230, 0) 18px),
    radial-gradient(circle at 40% 64%, rgba(214, 221, 230, 0.11) 0 8px, rgba(214, 221, 230, 0.04) 22px, rgba(214, 221, 230, 0) 44px),
    radial-gradient(circle at 54% 32%, rgba(214, 221, 230, 0.09) 0 10px, rgba(214, 221, 230, 0) 32px),
    radial-gradient(112% 86% at 50% 6%, rgba(40, 45, 53, 0.85) 0%, rgba(17, 20, 26, 0.42) 46%, rgba(7, 8, 11, 0) 74%);

  --vcs-city-warm:
    radial-gradient(circle at 80% 20%, rgba(255, 196, 107, 0.46) 0 4px, rgba(255, 196, 107, 0.13) 12px, rgba(255, 196, 107, 0) 26px),
    radial-gradient(circle at 26% 15%, rgba(255, 230, 188, 0.34) 0 6px, rgba(255, 230, 188, 0.1) 18px, rgba(255, 230, 188, 0) 38px),
    radial-gradient(circle at 12% 42%, rgba(224, 122, 74, 0.3) 0 5px, rgba(224, 122, 74, 0.09) 14px, rgba(224, 122, 74, 0) 30px),
    radial-gradient(circle at 92% 56%, rgba(255, 196, 107, 0.24) 0 7px, rgba(255, 196, 107, 0.07) 20px, rgba(255, 196, 107, 0) 42px),
    radial-gradient(circle at 66% 8%, rgba(255, 230, 188, 0.32) 0 2px, rgba(255, 230, 188, 0.1) 8px, rgba(255, 230, 188, 0) 18px),
    radial-gradient(circle at 40% 64%, rgba(224, 122, 74, 0.16) 0 8px, rgba(224, 122, 74, 0.04) 22px, rgba(224, 122, 74, 0) 44px),
    radial-gradient(circle at 54% 32%, rgba(255, 196, 107, 0.12) 0 10px, rgba(255, 196, 107, 0) 32px),
    radial-gradient(112% 86% at 50% 6%, rgba(54, 38, 20, 0.9) 0%, rgba(26, 20, 12, 0.46) 46%, rgba(7, 8, 11, 0) 74%);

  --vcs-field-bw:
    radial-gradient(58% 44% at var(--vcs-rim-x) 30%, rgba(226, 232, 240, 0.4) 0%, rgba(226, 232, 240, 0.09) 38%, rgba(226, 232, 240, 0) 66%),
    linear-gradient(180deg, #383e46 0%, #21262e 42%, #11141a 78%, #0a0c11 100%);

  --vcs-field-warm:
    radial-gradient(58% 44% at var(--vcs-rim-x) 30%, rgba(255, 206, 138, 0.56) 0%, rgba(255, 196, 107, 0.16) 38%, rgba(255, 196, 107, 0) 66%),
    radial-gradient(40% 30% at 90% 64%, rgba(224, 122, 74, 0.14) 0%, rgba(224, 122, 74, 0) 70%),
    linear-gradient(180deg, #4a3a22 0%, #2b2216 44%, #15110b 80%, #0a0806 100%);
}

/* --- Снимок сцены ---------------------------------------------------------- */
/* ОДИН кадр 16:9 на всю сцену: Сеул слева, Париж справа, пустая тёмная
   середина — под центральной типографикой. Слоя два, но src у них ОДИН:
   запрос и декодирование одни, различается только постоянный filter нижнего.
   Постоянное значение фильтра растеризуется один раз при создании слоя;
   анимировать filter в кадре нельзя (Chromium каждый кадр заново выполняет
   color-matrix, WebKit при смене значения перерастеризует слой), поэтому
   переход ч/б → цвет — это кроссфейд ВЕРХНЕГО слоя по opacity, единственному
   свойству, которое все движки меняют на композиторе без перерисовки.

   Класс is-photo ставит JS по событию load и снимает по error: битый или
   отсутствующий путь оставляет абстрактные области Сеула и Парижа нетронутыми,
   и секция не ломается.

   z-index: -1 — снимок уходит под всё содержимое в ОБОИХ режимах: и в
   залипающем (небо, области, провод и панели там position: absolute), и в
   статичном (панели идут обычным потоком). За пределы секции отрицательный
   слой не выходит: у .vc-story стоит isolation: isolate, а у сцены —
   contain: layout paint. */

.vc-story__scene {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}

/* Размеры зарезервированы заранее: слой абсолютный и растянут по inset, так
   что появление картинки не двигает ни одного пикселя вокруг — CLS нет. */
.vc-story__photo {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--vcs-object-position, center center);
  pointer-events: none;
}

/* Нижний слой: постоянный монохром. Видимость ведёт --vcs-wake — кадр мягко
   проявляется из ночи в начале истории и дальше уже не мигает. */
.vc-story__photo--bw {
  filter: grayscale(1) contrast(1.04) brightness(0.88);
  opacity: calc(0.04 + 0.96 * var(--vcs-wake, 1));
  will-change: opacity;
}

/* Верхний слой: тот же кадр в цвете. Множитель --vcs-wake нужен, чтобы цвет
   не мог обогнать проявление сцены ни при какой раскладке окон прогресса. */
.vc-story__photo--color {
  opacity: calc(var(--vcs-wake, 1) * var(--vcs-warm, 1));
  will-change: opacity;
}

/* Снимок пришёл — абстрактные области и звёздное небо были его заглушкой и
   больше не нужны. В разметке они остаются: это единственный фолбэк. */
.vc-story.is-photo .vc-story__figures,
.vc-story.is-photo .vc-story__sky {
  display: none;
}

/* --- Линия связи ----------------------------------------------------------- */
/* Контейнер натянут ровно от точки Сеула до точки Парижа и НИКОГДА не меняет
   размер в кадре: ни одной записи left/right/width. Области съезжают к своим
   точкам, сами точки не движутся — это читается как «человек приходит к точке
   соединения». */

.vc-story__link {
  position: absolute;
  pointer-events: none;
}

/* Два сегмента по половине оси, каждый закреплён за своим ВНЕШНИМ концом.
   При --vcs-line: 0 схлопнуты на точках, при 1 их концы встречаются ровно в
   центре. width нельзя: это свойство layout, и каждый кадр давал бы
   layout+paint посреди залипшей сцены. scaleX на полоске высотой 1px —
   композиторный transform, и масштаб по X не трогает высоту. */
.vc-story__seg {
  position: absolute;
  top: 50%;
  width: 50%;
  height: 1px;
  margin-top: -0.5px;
  transform: scaleX(var(--vcs-line, 1));
  transition: none;
  will-change: transform;
}

.vc-story__seg--a {
  left: 0;
  transform-origin: left center;
  background: linear-gradient(90deg, rgba(242, 86, 92, 0.62) 0%, rgba(242, 86, 92, 0.3) 100%);
}

.vc-story__seg--b {
  right: 0;
  transform-origin: right center;
  background: linear-gradient(270deg, rgba(242, 86, 92, 0.62) 0%, rgba(242, 86, 92, 0.3) 100%);
}

/* Свечение растущих концов. Это сиблинги сегментов, а не их дети: ребёнок
   унаследовал бы scaleX и растянулся бы в эллипс. Смещение на 50% размера
   контейнера — ровно точка встречи. */
.vc-story__tip-track {
  position: absolute;
  inset: 0;
  transition: none;
  will-change: transform;
}

.vc-story__tip-track--a {
  transform: translate3d(calc(var(--vcs-line, 1) * 50%), 0, 0);
}

.vc-story__tip-track--b {
  transform: translate3d(calc(var(--vcs-line, 1) * -50%), 0, 0);
}

/* Ноль в начале, ноль в момент встречи, максимум посередине — это и есть
   деликатный импульс фазы построения, и он не стоит ни одной записи из JS. */
.vc-story__tip {
  position: absolute;
  top: 50%;
  width: 9px;
  height: 9px;
  margin: -4.5px 0 0 -4.5px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 109, 115, 0.62) 0%, rgba(242, 86, 92, 0) 70%);
  opacity: clamp(0, calc(var(--vcs-line, 1) * (1 - var(--vcs-line, 1)) * 3.4), 1);
}

.vc-story__tip-track--a .vc-story__tip {
  left: 0;
}

.vc-story__tip-track--b .vc-story__tip {
  left: 100%;
}

/* Бегущий импульс появляется ТОЛЬКО после встречи половин: импульс через ещё
   пустую середину был бы враньём. Позиция — чистая функция прогресса, не
   CSS-анимация, поэтому на скролле вверх он идёт назад. */
.vc-story__pulse {
  position: absolute;
  inset: 0;
  transform: translate3d(calc(var(--vcs-pulse, 0) * 100%), 0, 0);
  transition: none;
  will-change: transform;
}

.vc-story__pulse-dot {
  position: absolute;
  top: 50%;
  left: 0;
  width: 20px;
  height: 4px;
  margin: -2px 0 0 -10px;
  border-radius: 2px;
  background: linear-gradient(90deg, rgba(255, 109, 115, 0) 0%, rgba(255, 214, 196, 0.72) 50%, rgba(255, 109, 115, 0) 100%);
  opacity: var(--vcs-pulse-o, 0);
  will-change: opacity;
}

/* Точки на концах. Только opacity и transform: box-shadow-spread перерисовывает. */
.vc-story__dot {
  position: absolute;
  top: 50%;
  width: 6px;
  height: 6px;
  margin: -3px 0 0 -3px;
  border-radius: 50%;
  background: var(--vcs-coral);
  opacity: var(--vcs-dot, 1);
  transform: scale(calc(0.35 + 0.65 * var(--vcs-dot, 1)));
  transition: none;
  will-change: opacity, transform;
}

.vc-story__dot--seoul {
  left: 0;
}

.vc-story__dot--paris {
  left: 100%;
}

.vc-story__dot::after {
  content: "";
  position: absolute;
  inset: -8px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(242, 86, 92, 0.34) 0%, rgba(242, 86, 92, 0) 70%);
  opacity: calc(0.18 + 0.82 * var(--vcs-flare, 0.55));
  transform: scale(calc(1 + 0.75 * var(--vcs-flare, 0.55)));
  will-change: opacity, transform;
}

/* --- Текстовые панели ------------------------------------------------------ */

.vc-story__panels {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Свой контекст наложения: скрим ниже уходит под текст, но остаётся ВНУТРИ
     слоя панелей и не проваливается под снимок (у сцены contain: paint, то
     есть отрицательный z-index без изоляции ушёл бы за кадр). */
  isolation: isolate;
}

/* Читаемость держит ОДИН мягкий локальный скрим на всю центральную сцену, а не
   подложка под каждой строкой: прямоугольные пятна под словами и были тем
   «чёрным прямоугольником», от которого сцена выглядела пустой. Эллипс мягко
   гасит боке ровно там, где стоит текст, и полностью растворяется к краям —
   огни Сеула и Парижа по сторонам кадра остаются нетронутыми. */
.vc-story__panels::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(
    48% 46% at 50% var(--vcs-scrim-y, 42%),
    rgba(4, 5, 8, 0.55) 0%,
    rgba(4, 5, 8, 0.3) 52%,
    rgba(4, 5, 8, 0) 100%
  );
}

.vc-story__panel {
  position: absolute;
  left: 50%;
  margin: 0;
  opacity: var(--vcs-o, 1);
  /* Сглаживание живёт в smoothstep, а не в CSS: transition добавил бы
     запаздывание между скроллом и картинкой. */
  transition: none;
  will-change: opacity, transform;
  text-align: center;
  text-wrap: balance;
}

.vc-story__phrase {
  color: var(--vcs-mono-hi);
  font-size: clamp(1.05rem, 1.7vw, 1.5rem);
  font-weight: 500;
  letter-spacing: 0.01em;
}

.vc-story__phrase--accent {
  color: var(--vcs-coral-hi);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* Тихая надстрочная метка. Единственное место, где коралл звучит в тексте, и
   звучит приглушённо: сцена держится на типографике, а не на цвете. */
.vc-story__eyebrow {
  color: rgba(242, 86, 92, 0.72);
  font-size: clamp(10px, 0.76vw, 11px);
  font-weight: 600;
  letter-spacing: 0.18em;
  line-height: 1.2;
  text-transform: uppercase;
}

/* Клейм — кульминация кадра: три строки, ровно те, что в разметке. Перенос не
   отдан движку (text-wrap сломал бы разбивку на узких окнах): каждая строка —
   свой блочный span, поэтому «1 TAP. / 2 PEOPLE. / 0 BORDERS.» держится на
   любой ширине, а --vcs-claim-h остаётся верной высотой блока. */
.vc-story__claim {
  color: #fff;
  font-size: var(--vcs-claim-fs);
  font-weight: 600;
  letter-spacing: -0.045em;
  line-height: var(--vcs-claim-lh);
}

.vc-story__claim-line {
  display: block;
}

.vc-story__closing {
  color: #e8eef6;
  font-size: clamp(15px, 1.32vw, 20px);
  font-weight: 400;
  letter-spacing: -0.005em;
  opacity: calc(var(--vcs-o, 1) * 0.72);
}

/* --- Нижняя плашка --------------------------------------------------------- */
/* Только время и расстояние. Ни «online», ни числа пользователей, ни стран,
   ни скорости соединения — никаких метрик. */

.vc-story__plate {
  position: absolute;
  left: 50%;
  margin: 0;
  padding: 8px 16px;
  border: 1px solid rgba(226, 236, 246, 0.14);
  border-radius: 999px;
  /* Стекло, а не технический бейдж: подложка почти прозрачная, границу держит
     тонкая светлая линия, объём — размытие кадра под ней. */
  background: rgba(9, 11, 15, 0.44);
  -webkit-backdrop-filter: blur(10px) saturate(120%);
  backdrop-filter: blur(10px) saturate(120%);
  color: rgba(232, 238, 246, 0.82);
  font-size: clamp(10px, 0.72vw, 11px);
  font-weight: 600;
  letter-spacing: 0.14em;
  /* «8,967 km» в разметке остаётся строчным — регистр даёт стиль, а не правка
     текста: строка читается одним рядом капители с городами. */
  text-transform: uppercase;
  /* Табличные цифры на всей строке: минуты меняются, ширина плашки — нет. */
  font-variant-numeric: tabular-nums;
  /* Плашка не должна переноситься и не должна перецентровываться каждую
     минуту при смене цифр. */
  white-space: nowrap;
  opacity: var(--vcs-o, 1);
  transition: none;
  will-change: opacity, transform;
}

.vc-story__time {
  /* Без табличных цифр строка дёргается на каждой смене минуты. */
  font-variant-numeric: tabular-nums;
  color: #fff;
}

.vc-story__sep {
  margin: 0 0.5em;
  color: var(--vcs-coral);
  opacity: calc(0.45 + 0.55 * var(--vcs-warm, 1));
}

/* Движок без поддержки timeZone: показываем расстояние честно, без времени,
   вместо заведомо неверных цифр. */
.vc-story__plate[data-vc-clock="off"] .vc-story__time {
  display: none;
}

/* --- Десктоп --------------------------------------------------------------- */
/* Арифметика проверена на 1440x900: фигуры 345x648 стоят на 72–417 и
   1023–1368, свободный центр 606px; после съезда (43px с каждой стороны) —
   520px против текстовой колонки 420px, то есть по 50px зазора в самый тесный
   момент всей последовательности.

   Финальный блок собран вокруг оси (--vcs-axis: 32% высоты сцены):
     eyebrow  — нижним краем на 26px выше оси,
     провод   — на самой оси, шириной ровно в текстовую колонку,
     клейм    — верхним краем на 30px ниже оси,
     закрытие — под клеймом через 24px, смещение считается от --vcs-claim-h.
   Отсюда визуальный центр блока: 1440x900 — 44.3% высоты кадра, 1920x1080 —
   44.0%. Обе цифры внутри целевых 43–46%, и держатся они не подгонкой под
   конкретное окно, а тем, что все смещения выражены через кегль клейма.

   Ширина клейма 620px при центре 720 даёт 410..1030, самая длинная строка
   «0 BORDERS.» — около 360px, то есть 540..900. Лица стоят на 0–350 и
   1050–1440, Эйфелева башня — 1060–1180: текст не касается ни того, ни
   другого даже краем колонки. */

@media (min-width: 992px) {
  .vc-story {
    --vcs-side: clamp(20px, 5vw, 96px);
    --vcs-fig-w: clamp(240px, 24vw, 380px);
    --vcs-fig-h: min(72vh, 660px);
    --vcs-glide: clamp(20px, 3vw, 48px);
    --vcs-panel-w: min(420px, 30vw);
    /* Одна колонка на клейм и на провод: линия обязана быть частью блока. */
    --vcs-col-w: min(620px, 46vw);
  }

  .vc-story__figure {
    top: 50%;
    width: var(--vcs-fig-w);
    height: var(--vcs-fig-h);
    --vcs-gy: -50%;
    --vcs-gx: calc(var(--vcs-approach, 1) * var(--vcs-glide) * var(--vcs-dir));
  }

  .vc-story__figure--seoul {
    left: var(--vcs-side);
    --vcs-dir: 1;
  }

  .vc-story__figure--paris {
    right: var(--vcs-side);
    --vcs-dir: -1;
  }

  .vc-story__link {
    top: var(--vcs-axis);
    height: 0;
    left: calc(var(--vcs-side) + var(--vcs-fig-w) + 14px);
    right: calc(var(--vcs-side) + var(--vcs-fig-w) + 14px);
  }

  /* Со снимком провод больше не мерит кадр от края до края: он ровно в ширину
     текстовой колонки и стоит линейкой между eyebrow и клеймом. Колонка уже
     проверена на непопадание в лица, поэтому и концы линии гарантированно
     стоят в пустой тёмной середине на любом окне — прежние 30%/70% на широких
     мониторах расходились с текстом и читались отдельным виджетом. */
  .vc-story.is-photo .vc-story__link {
    left: 50%;
    right: auto;
    width: var(--vcs-col-w);
    /* Только сам контейнер: проценты в transform его детей считаются от этой
       же ширины, поэтому сегменты и точки остаются на своих местах. */
    margin-left: calc(var(--vcs-col-w) / -2);
  }

  .vc-story__panel {
    width: var(--vcs-panel-w);
  }

  /* Фраза стоит на самом проводе. */
  .vc-story__phrase {
    top: var(--vcs-axis);
    transform: translate3d(-50%, calc(-50% + var(--vcs-y, 0px)), 0);
  }

  /* Привязка снизу: собственная высота метки в расчёт не входит, поэтому
     зазор до провода ровно 26px при любом кегле. */
  .vc-story__eyebrow {
    top: auto;
    bottom: calc(100% - var(--vcs-axis) + 26px);
    width: var(--vcs-col-w);
    transform: translate3d(-50%, var(--vcs-y, 0px), 0);
  }

  .vc-story__claim {
    top: calc(var(--vcs-axis) + 30px);
    width: var(--vcs-col-w);
    transform: translate3d(-50%, var(--vcs-y, 0px), 0);
  }

  /* Смещение считается от кегля клейма, а не от подогнанной константы. */
  .vc-story__closing {
    top: calc(var(--vcs-axis) + 30px + var(--vcs-claim-h) + 24px);
    width: min(520px, 40vw);
    transform: translate3d(-50%, var(--vcs-y, 0px), 0);
  }

  .vc-story__plate {
    bottom: clamp(48px, 5.6vh, 64px);
    transform: translate3d(-50%, var(--vcs-y, 0px), 0);
  }
}

/* --- Мобильная композиция -------------------------------------------------- */
/* Вертикальная ось: Сеул сверху, Париж ниже. Проверено на 390x844: полоса
   снимка занимает 243–462, лица внутри неё; провод 313–393 по её тёмному
   центру; ниже стоит финальный блок — eyebrow ~515, клейм 548–656, финальная
   фраза 674–696, плашка 720–755. До полосы со лицами остаётся ~53px, то есть
   текст не наезжает на лица ни на одной строке. */

@media (max-width: 991.98px) {
  .vc-story {
    --vcs-track: 440vh;
    --vcs-header: 70px;
    --vcs-fig-top: 64px;
    --vcs-fig-w: min(62vw, 250px);
    --vcs-fig-h: min(25vh, 214px);
    --vcs-gap: 156px;
    --vcs-glide: clamp(14px, 2.4vh, 26px);
    /* Три строки клейма на 390px: самая длинная «2 PEOPLE.» — около 190px. */
    --vcs-claim-fs: clamp(36px, 9.6vw, 44px);
    /* Скрим переезжает вниз, к своему тексту: центр блока здесь ~73% высоты. */
    --vcs-scrim-y: 74%;
  }

  /* На узком экране эллипс шире: колонка текста занимает почти всю ширину. */
  .vc-story__panels::before {
    background: radial-gradient(
      82% 30% at 50% var(--vcs-scrim-y, 74%),
      rgba(4, 5, 8, 0.5) 0%,
      rgba(4, 5, 8, 0.28) 54%,
      rgba(4, 5, 8, 0) 100%
    );
  }

  .vc-story__stage {
    min-height: 560px;
  }

  /* Портретный вьюпорт и кадр 16:9 несовместимы при cover на всю сцену: в
     390x844 в кадре осталась бы одна пустая середина, а оба человека ушли бы
     за края — снимок читался бы пустым чёрным прямоугольником. Поэтому на
     мобильной раскладке снимок — полоса 16:9, посаженная ровно на ось связи:
     вертикальный провод идёт по пустому тёмному центру кадра, Сеул и Париж
     стоят слева и справа от него. Полоса абсолютная, layout не трогает. */
  .vc-story__scene {
    top: calc(var(--vcs-fig-top) + var(--vcs-fig-h) + var(--vcs-gap) / 2);
    bottom: auto;
    height: 56.25vw; /* фолбэк для движков без aspect-ratio */
    transform: translateY(-50%);
    /* Полоса не должна стоять в ночи прямоугольной вырезкой: верх и низ
       растворяются в фоне секции — тот же приём, что --vcs-edge у областей.
       Движки без mask-image получают ровный край, а не поломку. */
    -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 14%, #000 86%, transparent 100%);
    mask-image: linear-gradient(180deg, transparent 0%, #000 14%, #000 86%, transparent 100%);
  }

  @supports (aspect-ratio: 16 / 9) {
    .vc-story__scene {
      height: auto;
      aspect-ratio: 16 / 9;
    }
  }

  .vc-story__figure {
    left: 50%;
    width: var(--vcs-fig-w);
    height: var(--vcs-fig-h);
    --vcs-gx: -50%;
    --vcs-gy: calc(var(--vcs-approach, 1) * var(--vcs-glide) * var(--vcs-dir));
  }

  .vc-story__figure--seoul {
    top: var(--vcs-fig-top);
    --vcs-dir: 1;
  }

  .vc-story__figure--paris {
    top: calc(var(--vcs-fig-top) + var(--vcs-fig-h) + var(--vcs-gap));
    --vcs-dir: -1;
  }

  /* Короче прежнего: провод — тихая связка в тёмном центре полосы, а не
     самостоятельный элемент во весь просвет между областями. */
  .vc-story__link {
    left: 50%;
    width: 0;
    top: calc(var(--vcs-fig-top) + var(--vcs-fig-h) + 38px);
    height: calc(var(--vcs-gap) - 76px);
  }

  /* Та же переменная --vcs-line, только потребляется по вертикали. */
  .vc-story__seg {
    top: 0;
    left: 50%;
    width: 1px;
    height: 50%;
    margin: 0 0 0 -0.5px;
    transform: scaleY(var(--vcs-line, 1));
  }

  .vc-story__seg--a {
    top: 0;
    bottom: auto;
    transform-origin: center top;
    background: linear-gradient(180deg, rgba(242, 86, 92, 0.92) 0%, rgba(242, 86, 92, 0.5) 100%);
  }

  .vc-story__seg--b {
    top: auto;
    bottom: 0;
    transform-origin: center bottom;
    background: linear-gradient(0deg, rgba(242, 86, 92, 0.92) 0%, rgba(242, 86, 92, 0.5) 100%);
  }

  .vc-story__tip-track--a {
    transform: translate3d(0, calc(var(--vcs-line, 1) * 50%), 0);
  }

  .vc-story__tip-track--b {
    transform: translate3d(0, calc(var(--vcs-line, 1) * -50%), 0);
  }

  .vc-story__tip {
    top: 0;
    left: 50%;
  }

  .vc-story__tip-track--a .vc-story__tip {
    top: 0;
  }

  .vc-story__tip-track--b .vc-story__tip {
    top: 100%;
  }

  .vc-story__pulse {
    transform: translate3d(0, calc(var(--vcs-pulse, 0) * 100%), 0);
  }

  .vc-story__pulse-dot {
    top: 0;
    left: 50%;
    width: 6px;
    height: 26px;
    margin: -13px 0 0 -3px;
    background: linear-gradient(180deg, rgba(255, 109, 115, 0) 0%, rgba(255, 214, 196, 0.95) 50%, rgba(255, 109, 115, 0) 100%);
  }

  .vc-story__dot {
    top: auto;
    left: 50%;
  }

  .vc-story__dot--seoul {
    top: 0;
  }

  .vc-story__dot--paris {
    top: 100%;
  }

  .vc-story__panel {
    width: min(88vw, 340px);
  }

  .vc-story__phrase {
    top: calc(var(--vcs-fig-top) + var(--vcs-fig-h) + var(--vcs-gap) / 2);
    transform: translate3d(-50%, calc(-50% + var(--vcs-y, 0px)), 0);
  }

  /* Нижний стек поднят над плавающим виджетом AI CHAT: он фиксирован в правом
     нижнем углу страницы и на 390px закрывал правую половину плашки. Весь стек
     привязан снизу, поэтому три строки клейма растут ВВЕРХ, в свободную тьму
     под полосой, а не вниз, на плашку. */
  .vc-story__claim {
    top: auto;
    bottom: 188px;
    transform: translate3d(-50%, var(--vcs-y, 0px), 0);
  }

  /* Над клеймом: его высота известна из --vcs-claim-h. */
  .vc-story__eyebrow {
    top: auto;
    bottom: calc(188px + var(--vcs-claim-h) + 20px);
    transform: translate3d(-50%, var(--vcs-y, 0px), 0);
  }

  .vc-story__closing {
    top: auto;
    bottom: 148px;
    transform: translate3d(-50%, var(--vcs-y, 0px), 0);
  }

  .vc-story__plate {
    bottom: 92px;
    padding: 7px 13px;
    letter-spacing: 0.1em;
    transform: translate3d(-50%, var(--vcs-y, 0px), 0);
  }
}

@media (max-width: 575.98px) {
  .vc-story__plate {
    font-size: 10px;
    letter-spacing: 0.08em;
  }
}

/* --- Статичный режим ------------------------------------------------------- */
/* prefers-reduced-motion либо отсутствие overflow: clip. Секция раскладывается
   в нормальном потоке и показывает финальный цветной кадр целиком — потому что
   значения токенов по умолчанию и есть финал. Переопределять их не нужно. */

.vc-story.is-static {
  height: auto;
}

.vc-story.is-static .vc-story__stage {
  position: static;
  height: auto;
  min-height: 0;
  padding: calc(var(--vcs-header) + 40px) 0 84px;
  contain: none;
}

/* Провод — часть анимации, в статике его нет. */
.vc-story.is-static .vc-story__link,
.vc-story.is-static .vc-story__sky {
  display: none;
}

.vc-story.is-static .vc-story__figures {
  position: static;
  display: flex;
  justify-content: center;
  gap: clamp(16px, 4vw, 48px);
  padding: 0 6vw;
}

.vc-story.is-static .vc-story__figure {
  position: static;
  width: min(300px, 42vw);
  height: auto;
  /* Единственное место, где поздняя картинка могла бы сдвинуть layout: здесь
     фигуры в потоке, поэтому место резервируется соотношением сторон. */
  aspect-ratio: 3 / 4;
  opacity: 1;
  transform: none;
  will-change: auto;
}

.vc-story.is-static .vc-story__panels {
  /* relative, а не static: скрим ниже должен обводить ровно блок текста, а не
     всю сцену — иначе он затемнил бы и снимок над ним. */
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  margin-top: 44px;
  padding: 0 6vw;
}

.vc-story.is-static .vc-story__panel {
  position: static;
  opacity: 1;
  transform: none;
  will-change: auto;
}

.vc-story.is-static .vc-story__closing {
  opacity: 0.82;
}

/* Без снимка текст в статике стоит на собственном почти чёрном фоне секции —
   гасить под ним нечего. Со снимком скрим возвращается: см. ниже. */
.vc-story.is-static .vc-story__panels::before {
  display: none;
}

/* В статике история уже случилась: промежуточные реплики поиска не нужны. */
.vc-story.is-static .vc-story__phrase--search {
  display: none;
}

/* В потоке блочный <p> растянул бы пилюлю во всю ширину — обжимаем по тексту. */
.vc-story.is-static .vc-story__plate {
  position: static;
  width: max-content;
  max-width: calc(100% - 12vw);
  margin: 30px auto 0;
  transform: none;
  opacity: 1;
}

/* Статика со снимком.

   Сцене нужен собственный стек-контекст: в статике она не sticky и не
   contain, и слой z-index: -1 ушёл бы в отрицательный слой всей секции, где
   его накрыл бы фоновый градиент самой сцены (фоны блоков в потоке рисуются
   ПОЗЖЕ отрицательных слоёв). position: relative + z-index: 0 возвращают
   отрицательный слой внутрь сцены — над её фоном и под текстом. */
.vc-story.is-static.is-photo .vc-story__stage {
  position: relative;
  z-index: 0;
}

/* В статике области стоят в потоке и держат высоту секции. Прятать их
   display: none нельзя: класс is-photo приходит по событию load, и секция
   схлопнулась бы уже после первой раскладки — это CLS. visibility сохраняет
   коробку: снимок занимает ровно то место, которое областями и было
   зарезервировано, и ни один пиксель не двигается. */
.vc-story.is-static.is-photo .vc-story__figures {
  visibility: hidden;
}

/* В статике панели идут обычным потоком. Сброс left/top/bottom обязателен:
   значения остались от залипающей раскладки и сдвинули бы панель. */
.vc-story.is-static.is-photo .vc-story__panel {
  position: relative;
  top: auto;
  right: auto;
  bottom: auto;
  left: auto;
}

/* В статике снимок растянут на всю сцену, и текст лежит прямо на нём — скрим
   нужен так же, как в залипшей сцене, только обводит он блок в потоке. */
.vc-story.is-static.is-photo .vc-story__panels::before {
  display: block;
  inset: -30px -6vw;
  background: radial-gradient(
    72% 78% at 50% 50%,
    rgba(4, 5, 8, 0.86) 0%,
    rgba(4, 5, 8, 0.58) 54%,
    rgba(4, 5, 8, 0) 100%
  );
}

/* Мобильная статика: оси и провода нет, поэтому полоса 16:9 встаёт ровно на
   ряд, зарезервированный областями, — сразу под верхним отступом сцены.
   Правило намеренно висит на .is-static, а НЕ на .is-static.is-photo: иначе
   уже вставленные слои переехали бы в момент загрузки картинки, и этот
   переезд засчитался бы как сдвиг макета. Геометрия слоя не должна зависеть
   от того, загрузился снимок или нет. */
@media (max-width: 991.98px) {
  .vc-story.is-static .vc-story__scene {
    top: calc(var(--vcs-header) + 40px);
    transform: none;
  }
}

/* --- Доступность ----------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .vc-story__figure,
  .vc-story__panel,
  .vc-story__plate,
  .vc-story__seg,
  .vc-story__dot {
    transition: none;
  }
}

/* Почти чёрная композиция не должна схлопываться при запросе контраста. */
@media (prefers-contrast: more) {
  .vc-story {
    --vcs-mono-hi: #f2f5f9;
  }

  .vc-story__panels::before {
    background: radial-gradient(
      58% 54% at 50% var(--vcs-scrim-y, 42%),
      rgba(4, 5, 8, 0.88) 0%,
      rgba(4, 5, 8, 0.6) 56%,
      rgba(4, 5, 8, 0) 100%
    );
  }

  .vc-story__eyebrow {
    color: rgba(255, 138, 143, 0.92);
  }

  .vc-story__closing {
    opacity: calc(var(--vcs-o, 1) * 0.92);
  }

  .vc-story__plate {
    border-color: rgba(226, 232, 240, 0.4);
    color: #f2f5f9;
  }
}
