/*
 * carousel.css — оформление просмотра кадров (carousel.js).
 *
 * Вынесено из site.css вместе с самим модулем: карусель нужна и публичной
 * галерее, и админке, а админка site.css не подключает — там своя палитра,
 * и притащить её целиком значило бы перекрасить всю страницу.
 *
 * Размеры кнопок на кадре берутся из токенов страницы, но с запасными
 * значениями: `var(--action-btn-size, 40px)`. На сайте выигрывают его
 * собственные токены, в том числе телефонные; в админке, где их нет,
 * работают запасные — и модуль остаётся самодостаточным.
 */

/* ---------- Lightbox ---------- */
.lightbox {
  /* Потолок высоты фотографии. Оставляет по 64px сверху и снизу, чтобы
     крестик закрытия и полоса позиции имели свою зону и ни при каком
     разрешении не ложились на снимок. На высоких экранах выигрывает 88vh,
     на низких — calc.

     Живёт ЗДЕСЬ, а не в site.css (где был до 23.09.2026): админка site.css
     не подключает, там var(--lb-maxh) не разрешался — а неразрешённая
     переменная делает недействительным всё объявление. max-height исчезал
     совсем, и в предпросмотре из админки фотография вылезала за нижний
     край экрана. То же число повторено в carousel.js (applySmartAspect) —
     держать в согласии. */
  --lb-maxh: min(88vh, calc(100vh - 128px));
}
.lightbox {
  position: fixed;
  inset: 0;
  /* Black, fully opaque: the photos read against an absolutely neutral
     field. It used to be 0.99 — on a bright screen the 500-tile grid
     ghosted through that last percent, and the page beneath was still being
     composited under every swipe frame. gallery.js also hides the page
     (body.lb-open) once the open zoom has finished. */
  background: #000;
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 100;
  user-select: none;
}
.lightbox.open { display: flex; }
/* Lightbox needs explicit overflow so the off-screen prev/next slides
   in the track aren't visible until you drag them into view. */
.lightbox { overflow: hidden; }
/* While the lightbox is fully open the page under it is not rendered at
   all. Set after the FLIP zoom completes (the zoom starts from the grid,
   which has to be visible then) and lifted while a drag-to-close is in
   progress, when the page shows through the fading backdrop. */
body.lb-open main,
body.lb-open .g-nav-bar,
body.lb-open .site-footer { visibility: hidden; }

/* The carousel track. Width = 3 viewports.
   The .lightbox parent is `display:flex; justify-content:center`, which
   already centers the track — so the MIDDLE slide naturally sits in the
   viewport. NO transform here: any translateX(-100vw) shift would push
   the centered slide off-screen to one side.
   z-index 0 / position relative is intentional: will-change creates a
   stacking context anyway, and we want lb-btn (z-index 2) to stack
   reliably above it regardless of document order. */
.lb-track {
  position: relative;
  z-index: 0;
  display: flex;
  width: 300vw;
  height: 100vh;
  align-items: center;
  will-change: transform;
  /* 3D perspective so neighbour rotateY() reads as a noticeable tilt.
     Smaller perspective = stronger foreshortening for the same angle. */
  perspective: 900px;
  /* `none` (not pan-y) — the lightbox body is overflow-hidden anyway,
     so we don't need browser-driven vertical scroll. Taking full
     control prevents Chrome / Safari from claiming a horizontal
     swipe for back-navigation or tab-switching. */
  touch-action: none;
  user-select: none;
}
.lb-slide {
  flex: 0 0 100vw;
  width: 100vw;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Smaller gutter — adjacent photos peek closer to the centered one */
  padding: 0 1vw;
  box-sizing: border-box;
  /* Resting state of an off-screen neighbour. JS writes the centered
     slide's transform/opacity directly to inline style on open. */
  transform: scale(0.85);
  opacity: 0.5;
  transform-origin: center;
  transform-style: preserve-3d;
}
/* The frame wraps the img and the per-slide action chips so the
   chips can be absolutely positioned relative to the photo's edges
   and travel with it during swipes / scales / FLIP zoom. */
.lb-slide .lb-frame {
  position: relative;
  display: inline-block;
  line-height: 0;
  /* Keep the rounded corners on the frame so the chips can extend
     to the very edge without producing a square corner halo. */
  border-radius: 12px;
}
/* Scoped to the direct child of .lb-frame so the action-chip icons
   (which live deeper, inside button.g-icon-btn) don't inherit the
   max-width: 96vw and balloon to viewport size. */
.lb-slide .lb-frame > img {
  max-width: 96vw;
  max-height: var(--lb-maxh);
  object-fit: contain;
  /* Rounded corners on the photo itself */
  border-radius: 12px;
  user-select: none;
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
  pointer-events: none;
  display: block;
  /* drop-shadow on the centered slide is applied via JS so neighbours
     don't carry it. No blur — neighbours fade with opacity only. */
}

/* Video overlay on the centred slide. Fills the same box as the poster
   image (which sets the frame size) and sits on top so native controls are
   tappable. Poster img behind has pointer-events:none. */
.lb-slide .lb-frame > .lb-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  border-radius: 12px;
  background: #000;
  z-index: 1;             /* above the static poster img, below the action chips (z-index:2) */
}
.lb-slide .lb-frame > .lb-video[hidden] { display: none; }

/* Per-slide share + download chips. Anchored to the bottom-right
   corner of THIS slide's photo (relative to .lb-frame). Because
   the frame translates / scales / rotates with the slide, the
   chips stay attached to their photo through every animation. */
.lb-slide-actions {
  position: absolute;
  bottom: 16px;
  right: 16px;
  display: flex;
  /* Тот же зазор, что под заголовком галереи: это одна и та же пара. */
  gap: var(--action-btn-gap, 12px);
  z-index: 2;
  /* Don't intercept pointer events on the slide background — only
     the individual buttons should be clickable. */
  pointer-events: none;
}
.lb-slide-actions .g-icon-btn {
  pointer-events: auto;
}
/* Кнопки на слайде — ТЕ ЖЕ, что под заголовком галереи: те же два PNG,
   та же роль «поделиться / сохранить». Значит и токены те же: 40 с глифом
   34 и зазором 12 на ПК, 80 с глифом 68 и зазором 24 на телефоне.

   Здесь была прибавка +35% на узких экранах — от 19.09.2026, когда общий
   чип был 26px и владелец сказал «маловаты». После того как телефонные
   кнопки выросли до 65, эта прибавка давала 88 и снова выбивалась из
   ряда: «белые кнопки неправильного размера, тоже должны быть 65». */
.lb-slide-actions .g-icon-btn {
  width:  var(--action-btn-size, 40px);
  height: var(--action-btn-size, 40px);
}
.lb-slide-actions .g-icon-btn .social-img {
  width:  var(--action-btn-glyph, 34px);
  height: var(--action-btn-glyph, 34px);
}
/* ГЛИФ ПРИХОДИТ ДВУХ ВИДОВ, И ВТОРОЙ НАДО ЗАДАТЬ ЯВНО. Сайт приносит `<img>`
   с PNG, у которого внутри холста свои поля; админка — голый `<svg>` с
   viewBox 24 и без полей. У `<svg>` без width/height размер по умолчанию —
   100%, поэтому в админке глиф заполнял диск целиком: «стрёмные огромные
   иконки, сильно отличается от галереи по пропорциям».

   0.615 — не на глаз. Это ровно та доля, что у чипа на плитке админки
   (16 из 26), и при ней ВИДИМЫЕ чернила совпадают с PNG на сайте: у PNG
   чернила занимают 0.59 холста, то есть 34 x 0.59 = 20 из 40 диска; у SVG
   с учётом обводки — 0.78 рамки, то есть 24.6 x 0.78 = 19.2 из тех же 40.

   Правило живёт ЗДЕСЬ, в модуле, а не у хозяина: раньше оно было только в
   `admin-overlay.js` (кнопки админа на публичной галерее), и собственная
   карусель админки его не получала. Ровно то расхождение, которого быть
   не должно. */
.lb-slide-actions .g-icon-btn svg {
  width:  calc(var(--action-btn-size, 40px) * 0.615);
  height: calc(var(--action-btn-size, 40px) * 0.615);
}

/* Fixed-aspect mode. When the gallery admin picks a specific frame
   ratio (display_aspect = '9:16' etc.) the lightbox enforces it on
   every slide so the photo edge / counter / dot strip stay anchored.
   --lb-aspect-w / --lb-aspect-h are set by gallery.js from the API
   response. */
.lb-track.fixed-aspect .lb-slide .lb-frame > img {
  width: min(96vw, calc(var(--lb-maxh) * var(--lb-aspect-w) / var(--lb-aspect-h)));
  height: auto;
  max-width: none;
  max-height: var(--lb-maxh);
  aspect-ratio: var(--lb-aspect-w) / var(--lb-aspect-h);
  object-fit: cover;
}
/* Smart mode: per-photo override. When the photo's orientation does
   not match the frame (e.g. landscape photo in a 9:16 frame), JS adds
   .lb-natural AND sizes the box to the photo in px (applySmartAspect),
   fitted inside the SCREEN's limits (96vw / --lb-maxh) — so on a desktop
   a landscape photo fills the height instead of shrinking into the
   portrait frame's width. The box hugs the photo, so the 12px corners sit
   on its visible edges — with object-fit: contain in a frame-sized box the
   corners landed on the transparent letterbox bands and landscape photos
   showed square corners. */
.lb-track.fixed-aspect .lb-slide .lb-frame > img.lb-natural {
  width: auto;
  height: auto;
  max-width: none;
  max-height: none;
  aspect-ratio: auto;
  object-fit: cover;
  background: transparent;
}

.lightbox .lb-btn {
  position: absolute;
  background: none;
  border: 0;
  color: #fff;
  font-size: 28px;
  width: 56px;
  height: 56px;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Stay above the carousel track so clicks land on the button, not on
     the image underneath. */
  z-index: 2;
  /* Dimmed by default; full opacity on hover so the icons never compete
     with the photo for attention. */
  opacity: 0.6;
  transition: opacity 0.2s;
}
.lightbox .lb-btn:hover { opacity: 1; }

/* Once the user has successfully swiped the carousel, prev/next become
   redundant on touch — fade them out for the rest of the session.
   On hover-capable devices (desktop) the arrows always remain visible,
   because there's no other way to advance. */
.lightbox .lb-prev,
.lightbox .lb-next {
  transition: opacity 0.6s ease;
  /* Arrows 50% larger than the default 28px chevron / 56px box. */
  font-size: 42px;
  width: 84px;
  height: 84px;
}
.lightbox.swiped .lb-prev,
.lightbox.swiped .lb-next {
  opacity: 0;
  pointer-events: none;
}
@media (hover: hover) {
  .lightbox.swiped .lb-prev,
  .lightbox.swiped .lb-next {
    opacity: 0.6;
    pointer-events: auto;
  }
}
.lightbox .lb-close {
  /* Fixed in the top-right corner of the SCREEN, inside the reserved 64px
     top band (see --lb-maxh) so it never overlaps the photo at any
     resolution. Position is static CSS — no JS measurement. */
  top: 6px;
  right: 16px;
  font-size: 36px;
  /* Per user request: close cross stays subtle (20%) so it never
     competes with the photo. Hover lifts to 100%. */
  opacity: 0.2;
}
.lightbox .lb-close:hover { opacity: 1; }
.lightbox .lb-prev { left: 12px; top: 50%; transform: translateY(-50%); }
.lightbox .lb-next { right: 12px; top: 50%; transform: translateY(-50%); }
/* Clear hover state for the arrows (desktop): brighten to full + grow.
   translateY(-50%) is preserved so the button stays vertically centred. */
@media (hover: hover) {
  .lightbox .lb-prev:hover,
  .lightbox .lb-next:hover {
    opacity: 1;
    /* Тот же множитель, что у всех кнопок (был свой, 1.18). translateY
       сохраняем, иначе стрелка при наведении съезжает на пол-высоты. */
    transform: translateY(-50%) scale(var(--hover-scale-chip, 1.12));
  }
}

/* Dot indicator strip. Positioned dynamically via JS to sit a few
   pixels below the actual photo bottom edge — works the same for
   portrait photos (bound by max-height 88vh) and landscape photos
   (bound by max-width 96vw, which leaves more vertical room). The
   `bottom` value here is a fallback used before JS measures the
   first image; positionDots() overwrites it. */
/* Position indicator, form 1 — DOTS, one per photo, used while they fit in
   this box (gallery.js indicatorMode() measures it: (max-width − 9px growth
   + gap) / (6px + 6px gap) ≈ 26 dots on a 390px phone, 40 at 480px).
   FIXED `bottom`, in the 64px band that --lb-maxh keeps clear of the photo
   at the bottom of the viewport (the close cross has the matching top
   band). It used to be centred under the current image and jumped whenever
   a landscape frame followed a portrait one. 26px centres the 11px dot in
   the band. */
.lightbox .lb-dots {
  position: absolute;
  bottom: 26px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  z-index: 2;
  max-width: min(80vw, 480px);
  overflow: hidden;
  pointer-events: none;
}
/* All dots are solid white (monolithic — no outline). Hierarchy is conveyed
   purely via opacity + size: current 11px / 1.0, neighbour 8px / 0.5, the
   rest 6px / ~0.12 (high transparency). These resting values are also the
   targets JS uses when it interpolates inline styles during a drag. */
.lightbox .lb-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #fff;
  border: none;
  flex: 0 0 auto;
  box-sizing: border-box;
  opacity: 0.12;
  transition: width 0.32s cubic-bezier(0.22, 1, 0.36, 1),
              height 0.32s cubic-bezier(0.22, 1, 0.36, 1),
              opacity 0.32s ease;
}
.lightbox .lb-dot.neighbor { width: 8px;  height: 8px;  opacity: 0.5; }
.lightbox .lb-dot.current  { width: 11px; height: 11px; opacity: 1; }

/* Position indicator, form 2 — STRIP with a marker, like an e-reader, plus
   a "12 / 503" count. Takes over when the dots would not fit. One element
   moves per frame — the dots are one <span> per photo restyled on every
   swipe frame, which with 500 photos was the lag itself. White in the
   lightbox by the same rule as .lb-btn / .lb-dot (§1, "what stays
   hard-coded"). The strip is a control: tap or drag it to jump. */
.lightbox .lb-progress {
  position: absolute;
  bottom: 20px;                   /* 24px strip centred in the 64px band */
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 12px;
  z-index: 2;
  width: min(60vw, 420px);
}
.lightbox .lb-progress-track {
  position: relative;
  flex: 1;
  height: 24px;                   /* hit area; the visible line is 2px */
  cursor: pointer;
  touch-action: none;
}
.lightbox .lb-progress-track::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 2px;
  margin-top: -1px;
  border-radius: 1px;
  background: rgba(255, 255, 255, 0.22);
}
.lightbox .lb-progress-thumb {
  position: absolute;
  top: 50%;
  height: 4px;
  margin-top: -2px;
  min-width: 10px;
  border-radius: 2px;
  background: #fff;
  opacity: 0.95;
  /* left/width are inline from gallery.js; the glide transition too. */
}
.lightbox .lb-progress-count {
  flex: 0 0 auto;
  font-family: 'DM Sans', sans-serif;
  font-size: 12px;
  letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.7);
  font-variant-numeric: tabular-nums;
  min-width: 5.5em;
  text-align: right;
  user-select: none;
}

/* FLIP open animation: while .zooming is on, the chrome (close /
   prev / next buttons, position strip, per-slide chips) is hidden
   so only the photo is visible during the morph from thumb to full
   size. They fade back in once the zoom completes. */
.lightbox.zooming .lb-btn,
.lightbox.zooming .lb-dots,
.lightbox.zooming .lb-progress,
.lightbox.zooming .lb-slide-actions {
  opacity: 0 !important;
  pointer-events: none;
  transition: none;
}
.lightbox:not(.zooming) .lb-btn,
.lightbox:not(.zooming) .lb-dots,
.lightbox:not(.zooming) .lb-progress,
.lightbox:not(.zooming) .lb-slide-actions {
  transition: opacity 0.25s ease;
}

/* Pinch-to-zoom: while two fingers are on the photo, everything else steps
   aside so only the picture is left against the black — the same clearing
   Instagram does. The zoom itself is inline transform written by gallery.js;
   it springs back when the fingers lift. */
.lightbox.pinching .lb-btn,
.lightbox.pinching .lb-dots,
.lightbox.pinching .lb-progress,
.lightbox.pinching .lb-slide-actions {
  opacity: 0 !important;
  pointer-events: none;
  transition: opacity 0.15s ease;
}


/* ---------- Кнопки на кадре ----------
   Карусель черна в ОБЕИХ темах, поэтому её чипы идут не от темы, а от
   одного и того же `Icon containner` макета: светлый диск с тёмным глифом.
   Раньше в светлой теме они разрешались в тёмный диск на чёрном и вовсе
   теряли край.

   Описание чипа продублировано здесь НАМЕРЕННО, в области `.lightbox`:
   на сайте оно совпадает с `.g-icon-btn` из site.css слово в слово, а в
   админке, где site.css нет, без него кнопки остались бы голыми. Это цена
   самодостаточности модуля; значения держать в согласии с site.css. */
.lightbox {
  --icon-btn-bg: #dedede;
  --icon-btn-ring: #dedede;
  --icon-btn-fg: #141414;
  --icon-btn-img-filter: brightness(0);
}
/* РАЗМЕР ЗДЕСЬ НЕ ЗАДАЁТСЯ, И ЭТО ВАЖНО. Он задан выше, у
   `.lb-slide-actions .g-icon-btn`, через `--action-btn-size` — ту же пару,
   что под заголовком галереи (40 с глифом 34 на ПК). Когда размер стоял и
   здесь, побеждало ЭТО правило: селекторы равной силы, а оно ниже по файлу,
   — и на сайте чип на кадре выходил 35 (размер чипа ШАПКИ) вместо 40. В
   админке, где токенов сайта нет, работал запасной 40, и один и тот же
   модуль рисовал кнопки разного размера в двух местах. Владелец: «сильно
   отличается от галереи по пропорциям. Опять отличия, которых не должно
   быть». */
.lightbox .g-icon-btn {
  border-radius: 50%;
  border: 1px solid var(--icon-btn-ring);
  background: var(--icon-btn-bg);
  color: var(--icon-btn-fg);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Ноль обязателен: это <button>, и браузер сам добавляет боковые 6px —
     из-за них круглый глиф выходил овальным. */
  padding: 0;
  cursor: pointer;
  flex-shrink: 0;
}
/* Размер глифа — там же, где размер диска. Здесь только заливка. */
.lightbox .g-icon-btn img {
  object-fit: contain;
  /* Глифы — БЕЛЫЕ png, и `color` на них не действует. На светлом диске они
     оставались белым на белом; `brightness(0)` красит в чёрный, сохраняя
     прозрачность. */
  filter: var(--icon-btn-img-filter, none);
}
