/* ---------------------------------------------------------
   tokens
--------------------------------------------------------- */
:root {
  --bg: #ffffff;
  --ink: #101010;
  --mute: #9a9a9a;
  --line: rgba(0, 0, 0, 0.08);
  --glass: rgba(255, 255, 255, 0.62);
  --glass-strong: rgba(255, 255, 255, 0.86);
  --font: -apple-system, "SF Pro Text", "Inter", "Helvetica Neue", Arial, sans-serif;

  --ease-spring: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  /* плавное затухающее торможение без отскока в конце — macOS-style
     переход (окна, свайпы Mission Control): быстрый старт, долгое мягкое
     доезжание до места, никакого пружинного перелёта */
  --ease-apple: cubic-bezier(0.19, 0.91, 0.38, 1);
  /* мягкий, без рывка, старт + плавное торможение — в отличие от
     --ease-apple (у неё крутой начальный участок, к 19% времени уже
     почти вся скорость набрана — на глаз это читается как дёрганый
     щелчок в начале), эта кривая набирает скорость постепенно.
     Для переходов, где сам старт движения должен быть виден и попадать
     в глаз (переключение колец, соседние карточки) — не для коротких
     разовых fade/scale, там дёрганость не успевает быть замеченной. */
  --ease-glide: cubic-bezier(0.4, 0, 0.2, 1);

  /* ---------------------------------------------------------
     breakpoint tokens — laptop/desktop (1025–1919px) значения "по
     умолчанию". Каждый брейкпоинт ниже (phone/tablet/2k/4k) переопределяет
     ЭТИ ЖЕ переменные в своём собственном @media-блоке — правки одного
     брейкпоинта («увеличь на телефоне») никогда не задевают остальные,
     т.к. они просто не попадают в соответствующий @media.
  --------------------------------------------------------- */
  --bp-font-size: 13px;
  --bp-logo-w: min(62vw, 860px);

  --bp-pill-top: 16px;
  --bp-pill-side: 16px;
  --bp-pill-pad-y: 9px;
  --bp-pill-pad-x: 18px;
  --bp-pill-gap: 16px;
  --bp-wordmark-size: 13px;
  --bp-nav-font-size: 13px;
  --bp-nav-gap: 18px;

  --bp-ringnav-gap: 9px;
  --bp-ringnav-glyph: 22px;
  --bp-ringnav-label-size: 10.5px;
  --bp-ringnav-side: 24px;

  --bp-panel-width: min(440px, 88vw);
  --bp-panel-pad: 28px 26px;
  --bp-panel-font-size: 13px;

  --bp-close-offset: 20px;
  --bp-close-pad-y: 9px;
  --bp-close-pad-x: 18px;
  --bp-close-font-size: 12px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--ink);
  height: 100%;
}

body {
  font-family: var(--font);
  font-size: var(--bp-font-size);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overflow: hidden; /* the carousel is the whole page, nothing to scroll */
}

a { color: inherit; text-decoration: none; }
::selection { background: var(--ink); color: var(--bg); }
:focus-visible { outline: 1px solid var(--ink); outline-offset: 2px; }

/* фоновый водяной знак — под барабаном, ничего не перекрывает и не
   реагирует на клики */
.bg-logo {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: var(--bp-logo-w);
  height: auto;
  /* z-index намеренно не ставим (auto) — иначе он оказался бы В ПЕРЕДНЕМ
     слое стекинга (выше .rings-viewport, у которого z-index не задан
     тоже, но с явным z-index этот элемент красился бы поверх любых
     auto-элементов независимо от порядка в DOM). auto + первый в body
     достаточно, чтобы лежать под всем остальным (у него у всего либо
     свой explicit z-index, либо он идёт позже в разметке) */
  opacity: .1;
  filter: grayscale(1);
  pointer-events: none;
  user-select: none;
}

/* лёгкое появление плавающих элементов при первой загрузке страницы —
   короткое, разово при заходе, не завязано на JS. Задержки чуть
   расходятся между блоками, чтобы страница не "вспыхивала" всей сразу. */
@keyframes chrome-in {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ---------------------------------------------------------
   intro: фото со всех колец медленно кружат 3D-вихрем на чисто белом
   фоне и стягиваются в центр — на лого ТОЧНО того же вида, размера и
   положения, что и постоянный .bg-logo за кольцами (см. .intro-logo
   ниже). Поэтому когда сама заставка в конце просто растворяется —
   переход на настоящий интерфейс не виден: лого визуально никуда не
   "перескакивает", под заставкой всё это время стоит тот же кадр.
   См. initIntro() в js/main.js.
--------------------------------------------------------- */
body.pre-intro .pill,
body.pre-intro .ring-nav {
  opacity: 0;
  pointer-events: none;
}

/* кольца открываются отдельным, более ранним шагом, чем масштхед/стрелки
   (.pre-intro) — см. revealRings()/revealChrome() в js/main.js: сперва
   карточки "прилетают снизу и замыкают кольцо" (см. entranceEase в
   render()), и только когда это в основном закончилось, следом
   проявляется остальной интерфейс */
body.rings-hidden .rings-viewport {
  opacity: 0;
  pointer-events: none;
}

#intro {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  overflow: hidden;
  transition: opacity 1.1s var(--ease-apple);
}

#intro.is-closing { opacity: 0; }

.intro-vortex {
  position: absolute;
  inset: 0;
  perspective: 1400px;
  perspective-origin: 50% 50%;
}

.intro-photo {
  position: absolute;
  top: 50%;
  left: 50%;
  border-radius: 10px;
  overflow: hidden;
  opacity: 0;
  box-shadow: 0 20px 40px -16px rgba(0, 0, 0, .32), 0 2px 8px rgba(0, 0, 0, .16);
  animation: intro-spin var(--dur, 2.2s) var(--ease-apple) var(--delay, 0s) forwards;
  will-change: transform, opacity;
}

.intro-photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(.35) contrast(1.05);
}

/* настоящий 3D-вихрь: --tilt наклоняет орбитальную плоскость каждой
   карточки (у всех разная — облако, а не плоское кольцо), rotateY по
   ней крутит, translateZ уводит по радиусу в глубину, а два обратных
   поворота в конце цепочки("билборд"-приём) возвращают карточку лицом
   к камере — вращается и удаляется/приближается именно её положение,
   а не сама картинка. Кружит издалека (--r0, глубоко в -Z, мелкая и
   тусклая) на средний план (--r1, чётко видна) и высасывается в
   центр (0), одновременно ужимаясь scale() до точки — ровно туда, где
   всё это время неподвижно стоит лого. */
@keyframes intro-spin {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) rotateX(var(--tilt)) rotateY(var(--a0)) translateZ(var(--r0)) rotateY(calc(-1 * var(--a0))) rotateX(calc(-1 * var(--tilt))) scale(.6);
  }
  14% { opacity: .95; }
  55% {
    opacity: .92;
    transform: translate(-50%, -50%) rotateX(var(--tilt)) rotateY(var(--a1)) translateZ(var(--r1)) rotateY(calc(-1 * var(--a1))) rotateX(calc(-1 * var(--tilt))) scale(1);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) rotateX(var(--tilt)) rotateY(var(--a2)) translateZ(0px) rotateY(calc(-1 * var(--a2))) rotateX(calc(-1 * var(--tilt))) scale(.05);
  }
}

/* пиксель-в-пиксель повторяет постоянный .bg-logo (тот же файл, тот же
   размер, тот же ч/б фильтр) и с первого кадра застаявки стоит в той же
   точке — вихрь кружит вокруг/сквозь неё, а когда #intro в конце гаснет,
   под ним обнаруживается тот же самый кадр, просто уже настоящий
   .bg-logo. Opacity меняет только JS (см. initIntro) — по мере того,
   сколько карточек уже дососалось в центр, лого разгорается всё ярче,
   а перед самым раскрытием интерфейса плавно гаснет обратно ровно до
   .1 — того значения, на котором стоит постоянный .bg-logo — поэтому
   в момент, когда застаявка окончательно тает, подмены не видно. */
.intro-logo {
  position: relative;
  z-index: 2;
  width: var(--bp-logo-w);
  height: auto;
  opacity: .1;
  filter: grayscale(1);
  pointer-events: none;
  user-select: none;
  transition: opacity .5s var(--ease-apple);
}

/* ---------------------------------------------------------
   floating pills (mac menu-bar / dock vibe)
--------------------------------------------------------- */
.pill {
  position: fixed;
  top: var(--bp-pill-top);
  z-index: 40;
  display: flex;
  align-items: center;
  gap: var(--bp-pill-gap);
  padding: var(--bp-pill-pad-y) var(--bp-pill-pad-x);
  background: var(--glass);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border: 1px solid var(--line);
  border-radius: 999px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
  transition: transform .4s var(--ease-spring), box-shadow .4s var(--ease-spring), opacity 1s var(--ease-apple);
  animation: chrome-in .7s var(--ease-out) backwards;
}

.pill:hover {
  transform: translateY(-1px);
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.09), 0 1px 2px rgba(0, 0, 0, 0.05);
}

.pill:active { transform: translateY(0) scale(.97); }

/* название + навигация теперь живут в одной подложке слева сверху —
   вместо двух отдельных плавающих капсул по разным углам */
.pill--masthead { left: var(--bp-pill-side); animation-delay: .05s; }

.wordmark {
  font-weight: 600;
  font-size: var(--bp-wordmark-size);
  letter-spacing: -0.02em;
  text-transform: lowercase;
  transition: opacity .25s var(--ease-out);
}
.wordmark:hover { opacity: .7; }

/* тонкий вертикальный разделитель между названием и пунктами меню —
   даёт им читаться как две разные по смыслу группы внутри одной подложки,
   не сливаясь в одну надпись */
.masthead-divider {
  width: 1px;
  height: 14px;
  background: var(--line);
  flex-shrink: 0;
}

.masthead-nav {
  display: flex;
  align-items: center;
  gap: var(--bp-nav-gap);
}

.masthead-nav button {
  position: relative;
  background: none;
  border: none;
  padding: 0 0 3px;
  font-family: var(--font);
  font-size: var(--bp-nav-font-size);
  color: var(--mute);
  text-transform: lowercase;
  cursor: pointer;
  transition: color .25s var(--ease-out);
}

/* тонкая точка-индикатор активной панели под названием — растёт из
   центра, а не просто появляется, чтобы переключение читалось как
   отдельный маленький жест, а не мгновенный щелчок */
.masthead-nav button::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 0;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: var(--ink);
  transform: translateX(-50%) scale(0);
  transition: transform .3s var(--ease-spring);
}

.masthead-nav button:hover,
.masthead-nav button[aria-pressed="true"] {
  color: var(--ink);
}

.masthead-nav button[aria-pressed="true"]::after {
  transform: translateX(-50%) scale(1);
}

.masthead-nav button:active { color: var(--ink); }

/* ---------------------------------------------------------
   несколько колец рядом друг с другом — текущее по центру,
   краешек соседнего виден по бокам. Переключение — стрелками,
   клавишами ←/→ или кликом по самому краешку соседнего кольца.
--------------------------------------------------------- */
.rings-viewport {
  position: fixed;
  inset: 0;
  overflow: hidden;
  transition: opacity 1.1s var(--ease-apple);
}

/* плавное "растворение" к краям экрана — обычный 2D-градиент поверх
   сцены, а не mask-image: маска на 3D-контенте (perspective +
   preserve-3d) в некоторых браузерах даёт жёсткий обрез вместо
   плавного фейда, из-за чего соседнее кольцо визуально "обрезалось" */
.rings-viewport::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  background: linear-gradient(
    90deg,
    var(--bg) 0%,
    transparent 10%,
    transparent 90%,
    var(--bg) 100%
  );
}

.rings-track {
  height: 100%;
  display: flex;
  align-items: stretch;
  /* --ease-glide, не --ease-apple: у последней очень крутой старт (почти
     вся скорость набирается в первые ~19% времени) — на самой заметной,
     самой протяжённой анимации сайта это читалось как дёрганый рывок в
     начале переключения. --ease-glide набирает скорость постепенно,
     тормозит так же мягко. */
  transition: transform .95s var(--ease-glide);
  will-change: transform;
}

.ring-slide {
  position: relative;
  height: 100%;
  flex: 0 0 auto; /* ширина проставляется инлайн из main.js под размер экрана */
}

.ring-slide:not(.is-active-slide) {
  cursor: pointer;
}

/* carousel — closed loop, gently bowed, centered on its slide */
.stage {
  position: absolute;
  inset: 0;
  /* overflow НЕ hidden: обрезание карточек по границе собственного
     слота кольца выглядело как жёсткий обрез соседнего кольца при
     перелистывании. Единственная граница, где контент должен реально
     исчезать — край всего экрана, а это уже .rings-viewport (overflow
     hidden) + градиент-фейд на .rings-viewport::before */
  cursor: grab;
  user-select: none;
  /* perspective ставится инлайн из main.js — пропорционально радиусу
     кольца (см. radiusScale), чтобы у колец с большим числом карточек
     (и потому бОльшим радиусом барабана) соотношение глубины и
     perspective оставалось тем же, а не "заваливало" сортировку по
     глубине на дальних карточках */
  perspective-origin: 50% 50%;
  transition: opacity .95s var(--ease-glide), transform .95s var(--ease-glide);
}

/* неактивное (соседнее, видное краешком) кольцо — приглушено и чуть
   отдалено (лёгкий scale-down даёт ощущение "уплывающей планеты" при
   переключении), не реагирует на драг/клик по карточкам — только клик
   по самому кольцу (см. .ring-slide) переключает на него.
   Раньше тут же был filter: blur() — на десятках 3D-карточек с тенями
   он ощутимо тормозил саму анимацию переключения (видно было рывки),
   убрал: opacity+scale дают ту же "уходящую вдаль" картинку намного
   дешевле для отрисовки. */
.stage:not(.is-active) {
  opacity: .5;
  transform: scale(.9);
  pointer-events: none;
}

/* лёгкий отклик при наведении на соседнее кольцо — подсказывает, что
   по нему можно кликнуть */
.ring-slide:not(.is-active-slide):hover .stage {
  opacity: .74;
  transform: scale(.935);
}

.stage.is-grabbing {
  cursor: grabbing;
}

.ring-nav {
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  z-index: 42;
  display: flex;
  align-items: center;
  gap: var(--bp-ringnav-gap);
  background: none;
  border: none;
  padding: 4px;
  margin: 0;
  cursor: pointer;
  color: var(--mute);
  font-family: var(--font);
  transition: color .3s var(--ease-out), gap .35s var(--ease-spring), transform .25s var(--ease-spring), opacity 1s var(--ease-apple);
  animation: chrome-in .7s var(--ease-out) .18s backwards;
}

/* без подложки — просто рамка-обёртка вокруг стрелки, ровно того же
   размера, что и сама иконка, чтобы стрелка и подпись центрировались
   по одной и той же логике (align-items:center у .ring-nav) и стояли
   строго на одной линии друг с другом */
/* глиф и подпись — оба ровно 22px высотой и центрируются каждый
   внутри себя тем же flex/align-items:center, вместо пиксельных
   translateY-подгонов на глаз (те давали несовпадающий центр между
   стрелкой и текстом) — одинаковая высота обеих коробок гарантирует,
   что их центры совпадают друг с другом ровно посередине */
.ring-nav__glyph {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--bp-ringnav-glyph);
  height: var(--bp-ringnav-glyph);
  flex-shrink: 0;
}

.ring-nav__glyph svg {
  width: var(--bp-ringnav-glyph);
  height: var(--bp-ringnav-glyph);
  transition: transform .4s var(--ease-spring), opacity .3s var(--ease-out);
  opacity: .8;
}

.ring-nav__label {
  display: flex;
  align-items: center;
  height: var(--bp-ringnav-glyph);
  line-height: 1;
  font-size: var(--bp-ringnav-label-size);
  letter-spacing: .16em;
  text-transform: uppercase;
  font-weight: 500;
  white-space: nowrap;
  /* коробки у glyph и label теперь буквально одной высоты (см. выше) —
     но сам символ шеврона в SVG геометрически центрирован в своей
     коробке, а буквы текста (даже все прописные, без нижних выносных)
     всё равно сидят чуть выше центра СВОЕЙ — так уже устроены метрики
     шрифта. Измерил разницу центров реального "чернила" (не коробок)
     напрямую в devtools — на глаз она маленькая, но заметная, около 1px;
     без волшебных чисел не обойтись, но теперь это конкретное измеренное
     число, а не подгонка на глаз, как было раньше. */
  transform: translateY(1px);
}
/* у --next текст и стрелка идут в обратном порядке (сперва текст, потом
   глиф) — на глаз у пользователя это давало на 1px больше рассинхрона,
   чем у --prev, даже с той же общей поправкой выше; дощипываем отдельно
   именно для --next вместо того чтобы разбалансировать --prev */
.ring-nav--next .ring-nav__label {
  transform: translateY(2px);
}

.ring-nav:hover { color: var(--ink); gap: 13px; }
.ring-nav:hover .ring-nav__glyph svg { opacity: 1; }
.ring-nav--prev:hover .ring-nav__glyph svg { transform: translateX(-3px); }
.ring-nav--next:hover .ring-nav__glyph svg { transform: translateX(3px); }
.ring-nav:active { color: var(--ink); transform: translateY(-50%) scale(.94); }
.ring-nav[disabled] { opacity: 0; pointer-events: none; }
.ring-nav--prev { left: var(--bp-ringnav-side); }
.ring-nav--next { right: var(--bp-ringnav-side); }


.carousel {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
}

/* один плоский элемент на карточку — это важно для 3D-барабана: сам
   .carousel-item — единственное, что участвует в общей 3D-сортировке
   preserve-3d у .carousel (позиция/глубина пишутся из JS каждый кадр,
   см. render()). overflow:hidden здесь одновременно и обрезает фото по
   скруглению, и (по спецификации) "расплющивает" сам элемент для целей
   3D — так все карточки остаются корректно отсортированы по глубине
   и клики/наведение всегда попадают ровно в ту карточку, что видна на
   экране. Если когда-нибудь понадобится доп. обёртка внутри — она НЕ
   должна получать самостоятельных 3D-совместимых свойств (opacity<1,
   filter, transform на :hover и т.п.), иначе она "просочится" в общий
   3D-контекст барабана и переиграет сортировку/попадания кликов у всех
   остальных карточек кольца (были мигание и промахи мимо нужного фото). */
.carousel-item {
  position: absolute;
  left: 50%;
  top: 0;
  border-radius: 15px;
  overflow: hidden;
  background: var(--bg);
  will-change: transform;
  transform-style: preserve-3d;
  backface-visibility: hidden;
  opacity: 0;
  box-shadow:
    0 1px 0 rgba(255, 255, 255, .55) inset,
    0 0 0 1px rgba(0, 0, 0, .045),
    0 10px 24px -10px rgba(0, 0, 0, .22),
    0 28px 54px -22px rgba(0, 0, 0, .28);
  /* transform И opacity сюда не через transition — оба каждый кадр
     перезаписывает JS (render() в main.js): opacity теперь ведёт
     "влёт" карточки при раскрытии интерфейса (entranceEase, уже сама
     по себе плавная eased-кривая), отдельный CSS-transition поверх неё
     только смазал бы движение. transition оставлен только на box-shadow
     (hover) — она не меняет геометрию/хитбокс элемента.
     Курсор (zoom-in/grab) сюда тоже не ставим и на :hover не завязываемся
     — при таком количестве перекрывающихся 3D-карточек под perspective
     нативный hit-test браузера ненадёжен для карточек вне центра (то
     видно, то нет — мигание курсора, промахи кликов). Вместо этого JS
     сам считает, какая карточка под курсором (см. hitTestCard/render()
     в main.js), навешивает .is-hovered и явно выставляет cursor на
     .stage — см. .stage ниже.
     width/height — тоже сюда, а не мимо: карточка стартует с ПРИБЛИЗИТЕЛЬНЫМИ
     пропорциями (кольцо не ждёт загрузки фото, см. clampDims/img.onload в
     main.js) и доводит размер до настоящего, когда фото реально
     загрузилось — без этого перехода было бы видно резкий скачок формы. */
  transition: box-shadow .45s var(--ease-spring), width .4s var(--ease-out), height .4s var(--ease-out);
}

.carousel-item.is-hovered {
  box-shadow:
    0 1px 0 rgba(255, 255, 255, .65) inset,
    0 0 0 1px rgba(0, 0, 0, .06),
    0 16px 32px -10px rgba(0, 0, 0, .26),
    0 36px 68px -22px rgba(0, 0, 0, .32);
}

.carousel-item img {
  display: block;
  width: 100%;
  height: 100%;
  /* сама карточка теперь всегда имеет родные пропорции фото (см.
     main.js) — object-fit тут просто подстраховка на случай долей
     пикселя округления, обрезки не происходит */
  object-fit: contain;
  filter: grayscale(var(--g, 0.7)) contrast(1.02);
  transition: filter .5s var(--ease-out);
}

/* наведение "проявляет" карточку в цвет — та же ч/б→цвет логика, что и
   при открытии Quick Look (см. openQuickLook в main.js), только тут это
   происходит прямо в дорожке, ещё до клика: карточка будто оживает под
   курсором, а не просто увеличивается */
.carousel-item.is-hovered img {
  filter: grayscale(0.15) contrast(1.04);
}

/* hover: карточка просто увеличивается — см. hoverBoost в main.js
   (сам transform управляется из JS каждый кадр, CSS тут его не трогает).
   Тень заодно углубляется — чисто визуально, размер карточки не меняет. */

/* карточки теперь никогда не становятся прозрачными (см. main.js) —
   они просто уменьшаются и плотно смыкаются по бокам, поэтому
   градиентные "маски" у верхнего/нижнего края больше не нужны:
   лента выглядит замкнутой и ничего не появляется из ниоткуда */

/* ---------------------------------------------------------
   quick look — mac-style zoom-from-thumbnail preview
--------------------------------------------------------- */
.ql-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(255, 255, 255, 0.5);
  backdrop-filter: blur(28px) saturate(140%);
  -webkit-backdrop-filter: blur(28px) saturate(140%);
  opacity: 0;
  z-index: 55;
  transition: opacity .35s var(--ease-out);
  pointer-events: none;
}
.ql-backdrop.is-open { opacity: 1; pointer-events: auto; }

.ql-image {
  position: fixed;
  z-index: 60;
  border-radius: 6px;
  box-shadow: 0 30px 70px rgba(0, 0, 0, 0.28);
  transition: top .5s var(--ease-spring), left .5s var(--ease-spring),
              width .5s var(--ease-spring), height .5s var(--ease-spring),
              border-radius .5s var(--ease-spring), filter .5s var(--ease-spring);
  pointer-events: none;
  object-fit: cover;
}

/* клик по ещё не догруженной карточке (см. openQuickLook) — вместо
   пустого прямоугольника, пока настоящий кадр только начал грузиться
   с повышенным приоритетом */
.ql-image.is-loading {
  background: rgba(0, 0, 0, 0.06) linear-gradient(
    100deg, transparent 30%, rgba(255, 255, 255, 0.35) 50%, transparent 70%
  );
  background-size: 200% 100%;
  animation: ql-image-shimmer 1.1s ease-in-out infinite;
}
@keyframes ql-image-shimmer {
  from { background-position: 150% 0; }
  to { background-position: -50% 0; }
}

.ql-close {
  /* раньше это был круг 32x32 с текстом "close" внутри — слово физически
     не влезало в такой маленький круг и вылезало за его границы (тот
     самый "кривится" на скрине). Теперь это капсула по размеру текста,
     как остальные текстовые кнопки на сайте (masthead-nav, ring-nav) —
     никакого конфликта формы и содержимого. */
  position: fixed;
  top: var(--bp-close-offset);
  right: var(--bp-close-offset);
  z-index: 61;
  padding: var(--bp-close-pad-y) var(--bp-close-pad-x);
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--glass-strong);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--bp-close-font-size);
  letter-spacing: .04em;
  text-transform: lowercase;
  cursor: pointer;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
  opacity: 0;
  /* без этого невидимая (opacity:0) кнопка всё равно перехватывала клики
     в своём углу экрана — там же, где сидит .index-close, когда открыт
     index: клик по нему на самом деле попадал в эту, а closeQuickLook()
     молча выходил (qlImage ещё не открыт) — снаружи выглядело так,
     будто у index-close сломан хитбокс */
  pointer-events: none;
  transition: opacity .3s var(--ease-out), transform .3s var(--ease-spring), box-shadow .3s var(--ease-out);
  transform: scale(.8);
}
.ql-close.is-open { opacity: 1; transform: scale(1); pointer-events: auto; }
.ql-close.is-open:hover { transform: scale(1.08); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); }
.ql-close.is-open:active { transform: scale(.94); }

/* ---------------------------------------------------------
   panels: information / index — floating glass card
--------------------------------------------------------- */
.panel-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(255, 255, 255, 0.4);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  z-index: 45;
  opacity: 0;
  pointer-events: none;
  transition: opacity .3s var(--ease-out);
}
.panel-backdrop.is-open { opacity: 1; pointer-events: auto; }

.panel {
  position: fixed;
  top: 50%;
  left: 50%;
  width: var(--bp-panel-width);
  max-height: 76vh;
  font-size: var(--bp-panel-font-size);
  overflow-y: auto;
  /* колонка с ровным шагом между блоками (текст / соцсети / copyright) —
     отступ сверху до текста и снизу после copyright теперь берётся из
     одного и того же padding, без "лишнего" margin абзацев */
  display: flex;
  flex-direction: column;
  gap: 22px;
  /* --glass, не --glass-strong — та же прозрачность, что у .pill
     (масштхед/меню), чтобы панели читались тем же самым стеклом, а не
     отдельным более плотным материалом */
  background: var(--glass);
  backdrop-filter: blur(30px) saturate(180%);
  -webkit-backdrop-filter: blur(30px) saturate(180%);
  border: 1px solid var(--line);
  border-radius: 20px;
  box-shadow: 0 30px 60px rgba(0, 0, 0, 0.16);
  z-index: 46;
  padding: var(--bp-panel-pad);
  transform: translate(-50%, -48%) scale(.96);
  opacity: 0;
  pointer-events: none;
  transition: opacity .3s var(--ease-spring), transform .35s var(--ease-spring);
}
.panel.is-open {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
  pointer-events: auto;
}

.panel p { margin: 0; max-width: 42ch; }
.panel .mute { color: var(--mute); }

/* ---------------------------------------------------------
   index: все фото сайта разбросаны по большому холсту (см.
   initIndexScatter в main.js), который вписан в экран целиком (без
   зума/пана от пользователя — .index-scatter-scene просто окно
   просмотра, .index-scatter — сам холст, на нём стоит только
   расчётный масштаб-вписывание). Драг ПО фото хватает и позволяет
   кинуть его: летит по инерции, бьётся об края холста и постепенно
   останавливается от трения. Обычный клик (без броска) открывает
   фото. Фон под всем этим блюрится тем же .panel-backdrop, что и у
   information.
--------------------------------------------------------- */
.index-scene {
  position: fixed;
  inset: 0;
  z-index: 50;
  opacity: 0;
  pointer-events: none;
  transition: opacity .4s var(--ease-out);
}
.index-scene.is-open {
  opacity: 1;
  pointer-events: auto;
}

/* стрелки переключения колец не имеют смысла поверх index — прячем на
   время, тем же приёмом, что и .pre-intro */
body.index-open .ring-nav {
  opacity: 0;
  pointer-events: none;
}

.index-scatter-scene {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.index-scatter {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
  will-change: transform;
}

.scatter-tile {
  position: absolute;
  top: 0;
  left: 0;
  border-radius: 4px;
  overflow: hidden;
  cursor: grab;
  user-select: none;
  touch-action: none;
  /* will-change НЕ ставим тут постоянно (было так раньше) — на холсте
     сотня с лишним тайлов, а двигается в любой момент максимум несколько
     (тот, что тащат/только что кинули): постоянный will-change на всех
     заставлял браузер держать КАЖДЫЙ как отдельный composited-слой с
     полноразмерным фото внутри — на мобильном GPU это съедало память
     и давало лаги независимо от того, что реально анимировано. Теперь
     проставляется/снимается точечно из JS (см. pointerdown/tick в
     initIndexScatter, main.js) — только пока тайл правда движется. */
  box-shadow: 0 10px 22px -10px rgba(0, 0, 0, .4), 0 2px 6px -2px rgba(0, 0, 0, .22);
  /* появление по порядку, а не разом — тайл стартует прозрачным, .is-in
     вешается по факту загрузки своего фото (или ошибки), с задержкой по
     порядку (см. transitionDelay в main.js, buildScatter) — так холст
     "рассыпается" по фотографиям, а не мигает целиком за один кадр.
     Только opacity, не transform: сам transform у тайла — инлайн-стиль,
     который каждый раз целиком перезаписывает JS (позиция/физика
     броска, см. applyTileTransform), так что CSS-transform тут всё
     равно был бы молча стёрт. */
  opacity: 0;
  transition: box-shadow .25s var(--ease-out), opacity .5s var(--ease-out);
}
.scatter-tile.is-in {
  opacity: 1;
}
.scatter-tile.is-dragging {
  cursor: grabbing;
  box-shadow: 0 22px 40px -14px rgba(0, 0, 0, .48), 0 4px 10px rgba(0, 0, 0, .24);
}

.scatter-tile img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

/* ---------------------------------------------------------
   index — плавающая кнопка закрытия полноэкранной сферы, тот же
   рецепт, что и .ql-close; видна только пока .index-scene открыта
   (чистый CSS через ~ combinator, без лишнего JS-состояния)
--------------------------------------------------------- */
.index-close {
  position: fixed;
  top: var(--bp-close-offset);
  right: var(--bp-close-offset);
  z-index: 51;
  padding: var(--bp-close-pad-y) var(--bp-close-pad-x);
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--glass-strong);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--bp-close-font-size);
  letter-spacing: .04em;
  text-transform: lowercase;
  cursor: pointer;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
  opacity: 0;
  pointer-events: none;
  transform: scale(.8);
  transition: opacity .3s var(--ease-out), transform .3s var(--ease-spring), box-shadow .3s var(--ease-out);
}
#panel-index.is-open ~ .index-close {
  opacity: 1;
  transform: scale(1);
  pointer-events: auto;
}
#panel-index.is-open ~ .index-close:hover { transform: scale(1.08); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); }
#panel-index.is-open ~ .index-close:active { transform: scale(.94); }

.contacts { display: flex; flex-direction: column; gap: 4px; }
.contacts a { color: var(--mute); transition: color .2s var(--ease-out); }
.contacts a:hover { color: var(--ink); }

/* ---------------------------------------------------------
   reduced motion
--------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* ---------------------------------------------------------
   responsive breakpoints — только переменные --bp-*, никаких других
   свойств тут. Каждый блок независимо переопределяет ровно те же
   имена, что объявлены в :root (см. начало файла) — значение,
   выставленное для phone, физически не может задеть tablet/2k/4k:
   они читают ту же переменную из СВОЕГО собственного @media-блока,
   а не наследуют друг у друга. Правь только нужный блок.
   Границы: phone ≤640px, tablet 641–1024px, laptop/desktop (база в
   :root) 1025–1919px, 2k 1920–2559px, 4k ≥2560px.
--------------------------------------------------------- */
@media (max-width: 640px) {
  :root {
    --bp-font-size: 12px;
    --bp-logo-w: min(78vw, 520px);

    --bp-pill-top: 10px;
    --bp-pill-side: 10px;
    --bp-pill-pad-y: 7px;
    --bp-pill-pad-x: 12px;
    --bp-pill-gap: 10px;
    --bp-wordmark-size: 12px;
    --bp-nav-font-size: 12px;
    --bp-nav-gap: 12px;

    --bp-ringnav-gap: 5px;
    --bp-ringnav-glyph: 18px;
    --bp-ringnav-label-size: 0px; /* подписи "prev"/"next" не помещаются рядом со стрелками на телефоне — прячем текст, оставляем только глиф */
    --bp-ringnav-side: 8px;

    --bp-panel-width: min(92vw, 420px);
    --bp-panel-pad: 20px 18px;
    --bp-panel-font-size: 13px;

    --bp-close-offset: 12px;
    --bp-close-pad-y: 7px;
    --bp-close-pad-x: 14px;
    --bp-close-font-size: 11px;
  }

  .ring-nav__label { overflow: hidden; }
}

/* ---------------------------------------------------------
   производительность на телефоне/планшете: несколько наложенных
   box-shadow на десятках карточек барабана + широкий backdrop-filter
   у неподвижного масштхеда (который каждый кадр пересчитывает блюр,
   т.к. кольца под ним постоянно едут) — на слабых мобильных GPU это
   и есть основной источник подлагивания при вращении/переключении
   колец. Тени тут упрощены до одного слоя, blur заметно уменьшен —
   визуально почти незаметно, а по нагрузке на композитор ощутимо легче.
--------------------------------------------------------- */
@media (max-width: 1024px) {
  .carousel-item {
    box-shadow: 0 8px 18px -8px rgba(0, 0, 0, .3);
  }
  .carousel-item.is-hovered {
    box-shadow: 0 10px 22px -8px rgba(0, 0, 0, .32);
  }
  .pill {
    backdrop-filter: blur(10px) saturate(140%);
    -webkit-backdrop-filter: blur(10px) saturate(140%);
  }
  .ql-backdrop {
    backdrop-filter: blur(14px) saturate(130%);
    -webkit-backdrop-filter: blur(14px) saturate(130%);
  }
  .panel-backdrop {
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
  }
  .index-close {
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
  }
  .scatter-tile {
    box-shadow: 0 6px 14px -8px rgba(0, 0, 0, .4);
  }
}

@media (min-width: 641px) and (max-width: 1024px) {
  :root {
    /* небольшая прибавка по всем значениям — по просьбе, "совсем немного" */
    --bp-font-size: 13.5px;
    --bp-logo-w: min(70vw, 680px);

    --bp-pill-top: 14px;
    --bp-pill-side: 14px;
    --bp-pill-pad-y: 9px;
    --bp-pill-pad-x: 17px;
    --bp-pill-gap: 14px;
    --bp-wordmark-size: 13.5px;
    --bp-nav-font-size: 13.5px;
    --bp-nav-gap: 16px;

    --bp-ringnav-gap: 8px;
    --bp-ringnav-glyph: 22px;
    --bp-ringnav-label-size: 10.5px;
    --bp-ringnav-side: 18px;

    --bp-panel-width: min(64vw, 470px);
    --bp-panel-pad: 26px 24px;
    --bp-panel-font-size: 14px;

    --bp-close-offset: 18px;
    --bp-close-pad-y: 9px;
    --bp-close-pad-x: 18px;
    --bp-close-font-size: 12.5px;
  }
}

/* верхняя граница поднята с 2559 до 3199: 2560×1440 ("1440p"/бытовое
   "2K") — это ещё этот класс экрана, а не следующий, более крупный —
   иначе 2560 сразу попадал в блок "4K" ниже и получал их укрупнённые
   значения, хотя физически это не такой большой скачок, как настоящий
   3840×2160. Порог 4K сдвинут туда же, симметрично, см. ниже.
   Нижняя граница — 1921, НЕ 1920: обычный full-HD экран 1920×1080
   должен остаться ровно на "laptop"-настройках (базовые значения в
   :root выше), без каких-либо изменений — а min-width:1920 захватывал
   ЕГО САМОГО тоже. */
@media (min-width: 1921px) and (max-width: 3199px) {
  :root {
    /* ещё немного прибавлено сверху по просьбе — "совсем немного" */
    --bp-font-size: 17px;
    --bp-logo-w: min(58vw, 1080px);

    --bp-pill-top: 25px;
    --bp-pill-side: 25px;
    --bp-pill-pad-y: 14px;
    --bp-pill-pad-x: 28px;
    --bp-pill-gap: 24px;
    --bp-wordmark-size: 17px;
    --bp-nav-font-size: 17px;
    --bp-nav-gap: 26px;

    --bp-ringnav-gap: 13px;
    --bp-ringnav-glyph: 31px;
    --bp-ringnav-label-size: 14.5px;
    --bp-ringnav-side: 37px;

    --bp-panel-width: min(650px, 34vw);
    --bp-panel-pad: 43px 40px;
    --bp-panel-font-size: 18px;

    --bp-close-offset: 30px;
    --bp-close-pad-y: 13px;
    --bp-close-pad-x: 26px;
    --bp-close-font-size: 16px;
  }
}

/* см. комментарий у 2K-блока выше — граница сдвинута симметрично с 2560
   до 3200, чтобы 2560×1440 однозначно доставался 2K-блок, а не этот */
@media (min-width: 3200px) {
  :root {
    /* этот раз — прибавка именно к меню/кнопкам/панели information
       (карточки и кольца в этом заходе не трогаем, по просьбе) */
    --bp-font-size: 28px;
    --bp-logo-w: min(46vw, 2000px);

    --bp-pill-top: 54px;
    --bp-pill-side: 54px;
    --bp-pill-pad-y: 30px;
    --bp-pill-pad-x: 56px;
    --bp-pill-gap: 48px;
    --bp-wordmark-size: 34px;
    --bp-nav-font-size: 34px;
    --bp-nav-gap: 50px;

    --bp-ringnav-gap: 30px;
    --bp-ringnav-glyph: 68px;
    --bp-ringnav-label-size: 28px;
    --bp-ringnav-side: 84px;

    --bp-panel-width: min(1100px, 34vw);
    --bp-panel-pad: 78px 70px;
    --bp-panel-font-size: 32px;

    --bp-close-offset: 66px;
    --bp-close-pad-y: 28px;
    --bp-close-pad-x: 50px;
    --bp-close-font-size: 30px;
  }
}