/* ============================================================
   3DGamBiT.ru — стили лендинга
   Шрифты подключены локально из папки ../fonts (woff2, вариативные).
   ============================================================ */

@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url('../fonts/manrope-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url('../fonts/manrope-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 100 800;
  font-display: swap;
  src: url('../fonts/jetbrains-mono-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 100 800;
  font-display: swap;
  src: url('../fonts/jetbrains-mono-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ============================================================
   3DGamBiT.ru — Landing
   ============================================================ */
:root{
  --bg:            #04060c;
  --bg-2:          #070b14;
  --surface:       rgba(16,22,36,.55);
  --surface-2:     rgba(22,30,48,.42);
  --line:          rgba(120,160,220,.14);
  --line-strong:   rgba(140,190,255,.28);

  --ink:           #eaf2ff;
  --ink-2:         #a9b8d4;
  --ink-3:         #6d7f9e;

  --cyan:          #22e6ff;
  --violet:        #8b5cff;
  --magenta:       #ff4fd8;
  --lime:          #6bffb8;

  --glow-cyan:     0 0 24px rgba(34,230,255,.45), 0 0 70px rgba(34,230,255,.18);
  --glow-violet:   0 0 24px rgba(139,92,255,.45), 0 0 80px rgba(139,92,255,.2);

  --r-sm: 12px;
  --r-md: 18px;
  --r-lg: 26px;

  --maxw: 1200px;
  /* Вертикальный шаг между разделами и блоками — ОДНО число на весь сайт.

     Владелец (04.10, ночь): «Надо везде уменьшить вот это растояние между
     блоками и разделами на основной странице и на странице публикации и на
     новых страница что созданы будут. Чтобы был единый стиль по рамеру между
     ними. умень в половину сейчас».

     Здесь было clamp(72px,9vw,140px) — на окне 1440 это 129.6 точки С
     КАЖДОЙ стороны, то есть между двумя соседними разделами главной лежала
     полоса 259.2. Замер 04.10: `servicesPadT/B = 129.6px`, полосы между
     всеми разделами главной — 259.2; на публикации вокруг встроенного
     раздела — 151.6 (22 точки потока .wp-body плюс 129.6 поля самого
     раздела). Отсюда и красные рамки на пяти снимках владельца.

     Половина от прежнего — 64.8 точки: между разделами 129.6 вместо 259.2,
     вокруг встроенного раздела на публикации 86.8 вместо 151.6. Число
     ОДНО, и оно же достаётся всем будущим страницам: и главная, и
     публикация, и страница сайта берут шаг отсюда, а не задают свой.

     Правило `section{…}` ниже — единственный потребитель. Это не
     случайность: встроенный раздел на публикации — ТОЖЕ <section>, поэтому
     одно правило уменьшает шаг сразу везде, и разойтись страницам нечем. */
  --gap-sec: clamp(36px,4.5vw,70px);
  --ease: cubic-bezier(.22,1,.36,1);
}

*,*::before,*::after{box-sizing:border-box}
html{scroll-behavior:smooth}
body{
  margin:0;
  background:var(--bg);
  color:var(--ink);
  font-family:'Manrope',-apple-system,'Segoe UI Variable','Segoe UI',system-ui,sans-serif;
  font-weight:400;
  line-height:1.65;
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden;
}
body.no-scroll{overflow:hidden}
img{max-width:100%;display:block}
a{color:inherit;text-decoration:none}
button{font-family:inherit}

::selection{background:rgba(34,230,255,.28);color:#fff}

/* scrollbar */
/* Полоса прокрутки сайта: 10px, дорожка под цвет фона, градиентная ручка.

   `height` здесь не для симметрии. Раньше стояла одна `width`, и ВЕРТИКАЛЬНАЯ
   полоса получала сайтовый вид, а ГОРИЗОНТАЛЬНАЯ — родную высоту браузера:
   одна и та же страница показывала две разные полосы. Владелец (06.10)
   обвёл полосу в окне товара и сказал: «прокрутка не соотвествует стилю
   сайта везде». В панели (`admin/admin.css`) `height` стояла с самого начала —
   здесь её просто забыли.

   Вторая половина той же правки — в правилах прокручиваемых блоков ниже:
   `scrollbar-width`/`scrollbar-color` заставляют Chrome ИГНОРИРОВАТЬ
   `::-webkit-scrollbar-*` целиком, и сайтовый вид там не применялся вовсе. */
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-track{background:#05080f}
::-webkit-scrollbar-thumb{
  background:linear-gradient(180deg,var(--cyan),var(--violet));
  border-radius:10px;border:2px solid #05080f;
}

/* ---------- ambient background ---------- */
.ambient{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden}
.ambient .orb{
  position:absolute;border-radius:50%;filter:blur(90px);opacity:.5;
  animation:drift 22s var(--ease) infinite alternate;
}
.orb-1{width:520px;height:520px;background:radial-gradient(circle,rgba(34,230,255,.55),transparent 68%);top:-140px;left:-120px}
.orb-2{width:600px;height:600px;background:radial-gradient(circle,rgba(139,92,255,.5),transparent 68%);top:34%;right:-220px;animation-delay:-7s}
.orb-3{width:460px;height:460px;background:radial-gradient(circle,rgba(255,79,216,.34),transparent 68%);bottom:-160px;left:28%;animation-delay:-13s}
@keyframes drift{
  0%{transform:translate3d(0,0,0) scale(1)}
  50%{transform:translate3d(60px,-50px,0) scale(1.14)}
  100%{transform:translate3d(-50px,60px,0) scale(.94)}
}
.grid-veil{
  position:absolute;inset:0;
  background-image:
    linear-gradient(rgba(120,170,255,.055) 1px,transparent 1px),
    linear-gradient(90deg,rgba(120,170,255,.055) 1px,transparent 1px);
  background-size:64px 64px;
  mask-image:radial-gradient(ellipse 90% 70% at 50% 30%,#000 20%,transparent 78%);
  -webkit-mask-image:radial-gradient(ellipse 90% 70% at 50% 30%,#000 20%,transparent 78%);
}
.noise{
  position:absolute;inset:0;opacity:.035;mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* cursor glow */
.cursor-glow{
  position:fixed;width:520px;height:520px;border-radius:50%;
  pointer-events:none;z-index:1;
  background:radial-gradient(circle,rgba(34,230,255,.10),rgba(139,92,255,.05) 42%,transparent 66%);
  transform:translate(-50%,-50%);
  transition:opacity .4s ease;
  will-change:transform;
}
@media (hover:none){.cursor-glow{display:none}}

/* ---------- layout ---------- */
.wrap{width:min(var(--maxw),92vw);margin-inline:auto;position:relative;z-index:2}
/* Шаг раздела — из переменной (см. --gap-sec в :root). Здесь стояло
   clamp(72px,9vw,140px) прямым числом, и то же число пришлось бы править в
   каждом новом месте; переменная одна на сайт, публикации и будущие
   страницы. */
section{position:relative;z-index:2;padding:var(--gap-sec) 0}
/* Блок, вложенный в раздел как САМ раздел (на главной так стоят
   калькулятор и экспресс-оценка: <section data-home-block> → <section
   class="wp-block pg-calc">), своих полей не получает.

   Поля здесь ДВОЙНЫЕ: внешний раздел даёт 64.8 сверху и снизу, и вложенный
   <section> — ещё столько же. Замер до правки: #home-calc 1297 точек при
   содержимом 779 — 518 из них воздух. На публикации того же нет:
   .wp-body section.wp-block{padding:0}, и блок стоит вплотную к потоку.
   Одно и то же содержимое выглядело на главной и на публикации по-разному —
   ровно то, что владелец назвал «единый стиль по размеру».

   Бьём ТОЛЬКО по вложенному <section>: прямое правило `.wp-block
   {padding:0}` сняло бы СВОИ поля у цитаты (.wp-quote) и списка
   (.wp-list) — их владелец задавал не для этого. */
main > section > section.wp-block{padding-block:0}
.sec-head{max-width:760px;margin-bottom:clamp(38px,5vw,64px)}
.eyebrow{
  display:inline-flex;align-items:center;gap:10px;
  font-size:.74rem;letter-spacing:.24em;text-transform:uppercase;
  color:var(--cyan);font-weight:600;
  padding:7px 15px;border-radius:100px;
  border:1px solid rgba(34,230,255,.28);
  background:rgba(34,230,255,.06);
  box-shadow:0 0 22px rgba(34,230,255,.14) inset;
}
.eyebrow::before{
  content:'';width:6px;height:6px;border-radius:50%;background:var(--cyan);
  box-shadow:0 0 12px var(--cyan);animation:pulse 2.4s infinite;
}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.7)}}

h1,h2,h3,h4{margin:0;line-height:1.08;letter-spacing:-.025em;font-weight:800}
h2{font-size:clamp(1.85rem,4vw,3.1rem);margin:22px 0 18px}
h3{font-size:1.16rem;letter-spacing:-.015em;font-weight:700}
.lead{color:var(--ink-2);font-size:clamp(1rem,1.35vw,1.12rem);font-weight:300;margin:0}
.grad{
  background:linear-gradient(100deg,var(--cyan),var(--violet) 42%,var(--magenta) 78%,var(--cyan));
  background-size:260% 100%;
  -webkit-background-clip:text;background-clip:text;color:transparent;
  animation:flow 9s linear infinite;
}
@keyframes flow{to{background-position:260% 0}}

/* Размер и цвет заголовка блока — настройки владельца.

   Владелец: «порой в таких блоках не видно что написано». Переменные
   ставит сервер (pubHeadStyle), и только когда он их задал: `--blk-title` и
   `--blk-head` по умолчанию НЕ выставлены, и тогда работает то, что
   написано в резервном значении, — то есть обычный вид сайта.

   Заголовок берёт цвет, но НЕ трогает цветную часть: у .grad стоит
   `color:transparent` (градиент режется по тексту), и он остаётся
   градиентным при любом выборе. Владелец выбирает цвет обычной части, а
   акцент — по-прежнему акцент. */
.sec-head h2{color:var(--blk-head,var(--ink));
  font-size:var(--blk-title,clamp(1.85rem,4vw,3.1rem))}
.sec-head .lead{color:var(--blk-head,var(--ink-2))}

/* ---------- nav ---------- */
.nav{
  position:fixed;top:0;left:0;right:0;z-index:60;
  transition:background .45s var(--ease),backdrop-filter .45s,border-color .45s;
  border-bottom:1px solid transparent;
}
.nav.solid{
  background:rgba(5,8,15,.72);
  backdrop-filter:blur(18px) saturate(160%);
  -webkit-backdrop-filter:blur(18px) saturate(160%);
  border-bottom-color:var(--line);
}
.nav-in{
  width:min(var(--maxw),92vw);margin-inline:auto;
  display:flex;align-items:center;justify-content:space-between;
  height:76px;
}

/* ---------- якорь не должен прятать цель под шапкой ----------

   Владелец (05.10) просил, чтобы сайт отзывался на якоря («#services»), — он
   будет ставить их в кнопки, блоки и тексты. Замер до этой правки: шапка
   `.nav` закреплена (`position:fixed`), её высота 77 точек (76 из `.nav-in`
   плюс нижняя граница), а цель якоря встаёт в top:0 — то есть 77 точек
   раздела прячутся ЗА шапкой. Нажал «Услуги» — и не понял, что произошло.
   Так же вели себя и пункты меню: они якорные с самого начала.

   `scroll-padding-top` — отступ у САМОЙ прокрутки, поэтому он работает для
   ЛЮБОЙ цели, а не только для разделов главной: якорь, который владелец
   поставит внутри блока или страницы, получит тот же отступ. Отдельно
   расставлять `scroll-margin-top` по разделам не пришлось бы, но каждый
   новый раздел пришлось бы не забыть — а это и есть та работа, которую
   забывают.

   89 = 76 (высота шапки) + 13 (зазор, чтобы заголовок раздела не липнул к
   границе). Правило под `:has(#nav)`: на странице БЕЗ шапки отступ не нужен
   вовсе — там цель встала бы ниже верха на пустом месте. */
html:has(#nav){scroll-padding-top:89px}
.logo{display:flex;align-items:center;gap:11px;font-weight:800;font-size:1.12rem;letter-spacing:-.02em}
.logo .mark{
  width:34px;height:34px;border-radius:10px;flex:none;
  display:grid;place-items:center;
  background:linear-gradient(140deg,rgba(34,230,255,.2),rgba(139,92,255,.24));
  border:1px solid rgba(34,230,255,.4);
  box-shadow:var(--glow-cyan);
  animation:markFloat 5s ease-in-out infinite;
}
@keyframes markFloat{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-3px) rotate(-4deg)}}
.logo .mark svg{width:19px;height:19px}
.logo b{background:linear-gradient(90deg,#fff,#9fd8ff);-webkit-background-clip:text;background-clip:text;color:transparent}
.logo span{color:var(--cyan);text-shadow:0 0 14px rgba(34,230,255,.8)}

/* ПУНКТ МЕНЮ НЕ СЖИМАЕТСЯ И НЕ ПЕРЕНОСИТСЯ.

   Владелец (07.10) показал шапку на ширине около 920 точек и написал:
   «кнопка не ровная Услуги картинка сьехала надо все исправить и сделать как
   в обычном режиме окна чтобы все было ровно и в один ряд». На снимке
   «Услуги» разъехалось на три строки — снимок пункта сверху, подпись
   посередине, стрелочка снизу, — и налезло на логотип.

   Причина не в «Услугах», а в общем правиле: пункты меню — флекс-элементы, и
   когда полосы не хватает, браузер СЖИМАЕТ их ниже содержимого
   (`flex-shrink:1` по умолчанию), а содержимое переносится. Замер
   `_probe-nav-head.js`: высота первого пункта 53 при 26 у соседей, левый край
   подписи на 35 точек левее своей коробки — отсюда и налезание на логотип.

   `flex:none` запрещает сжатие, `white-space:nowrap` — перенос подписи. Что
   будет ВМЕСТО сжатия, решают ширина окна и правила ниже: тесная полоса жмёт
   отступы и кегль («Мёртвая зона шапки»), а с 820 точек приходит бургер. */
.nav-links{display:flex;gap:4px;align-items:center}
.nav-links > a,.nav-links > .nav-item{flex:none}
.nav-links a{white-space:nowrap}
.nav-links a{
  position:relative;padding:9px 15px;border-radius:100px;
  font-size:.9rem;font-weight:500;color:var(--ink-2);
  transition:color .3s,background .3s;
}
.nav-links a::after{
  content:'';position:absolute;left:50%;bottom:4px;width:0;height:1.5px;
  background:linear-gradient(90deg,var(--cyan),var(--violet));
  transform:translateX(-50%);transition:width .35s var(--ease);
  box-shadow:0 0 10px var(--cyan);
}
.nav-links a:hover{color:#fff;background:rgba(120,170,255,.07)}
.nav-links a:hover::after{width:52%}

.btn{
  position:relative;display:inline-flex;align-items:center;justify-content:center;gap:10px;
  padding:14px 26px;border-radius:100px;border:1px solid transparent;
  font-size:.94rem;font-weight:700;letter-spacing:.01em;cursor:pointer;
  transition:transform .35s var(--ease),box-shadow .35s var(--ease),background .35s;
  overflow:hidden;isolation:isolate;
}
.btn-primary{
  color:#02121a;
  background:linear-gradient(115deg,var(--cyan),#6ee7ff 40%,var(--violet));
  box-shadow:0 8px 30px rgba(34,230,255,.28),0 0 0 1px rgba(255,255,255,.16) inset;
}
.btn-primary::before{
  content:'';position:absolute;inset:0;z-index:-1;
  background:linear-gradient(115deg,var(--magenta),var(--violet),var(--cyan));
  opacity:0;transition:opacity .4s;
}
.btn-primary:hover{transform:translateY(-3px);box-shadow:0 14px 44px rgba(34,230,255,.45),0 0 30px rgba(139,92,255,.5)}
.btn-primary:hover::before{opacity:1}
.btn-ghost{
  color:var(--ink);background:rgba(255,255,255,.03);
  border-color:var(--line-strong);backdrop-filter:blur(8px);
}
.btn-ghost:hover{
  transform:translateY(-3px);
  background:rgba(34,230,255,.08);
  border-color:rgba(34,230,255,.55);
  box-shadow:0 0 28px rgba(34,230,255,.3),0 0 60px rgba(34,230,255,.12) inset;
}
.btn-sm{padding:11px 20px;font-size:.86rem}

.burger{display:none;width:44px;height:44px;border-radius:12px;border:1px solid var(--line);
  background:rgba(255,255,255,.03);color:var(--ink);cursor:pointer;place-items:center}
.burger span{display:block;width:19px;height:2px;background:currentColor;border-radius:2px;position:relative;transition:.3s}
.burger span::before,.burger span::after{content:'';position:absolute;left:0;width:19px;height:2px;background:currentColor;border-radius:2px;transition:.3s}
.burger span::before{top:-6px}.burger span::after{top:6px}

/* progress bar */
.progress{position:fixed;top:0;left:0;height:2px;width:0;z-index:70;
  background:linear-gradient(90deg,var(--cyan),var(--violet),var(--magenta));
  box-shadow:0 0 14px rgba(34,230,255,.9)}

/* ---------- hero ---------- */
.hero{
  /* Высота первого экрана — не ровно окно, а окно МИНУС 72px.

     Содержимое здесь выключено по центру (align-items:center), поэтому
     «пустые полосы» сверху и снизу — это не поля, а остаток высоты: поля
     лишь сдвигают центр, а сами полосы считаются от min-height. Замер на
     живом сайте (05.10, окно 900px): полоса под шапкой 109.63px, полоса
     над бегущей строкой 134.59px. Владелец показал на обе красным:
     «надо немного уменьшить растояние». Урезав min-height на 72px, снимаем
     по 36px с каждой полосы (74.61 и 99.61) — первый экран перестаёт
     «висеть» в пустоте, а лента поднимается ближе к плашкам.

     На узком экране (см. @media 1024px ниже) высоту возвращаем: там
     содержимое в одну колонку и оно выше, полоса под шапкой всего 27px —
     ещё 36px увели бы подпись под саму шапку. */
  position:relative;min-height:calc(100vh - 72px);display:flex;align-items:center;
  padding-top:104px;padding-bottom:52px;overflow:hidden;
}
/* canvas — replaced-элемент: width/height обязательны, иначе он сожмётся до 150px */
#stage{
  position:absolute;inset:0;width:100%;height:100%;z-index:1;
  pointer-events:none;opacity:0;transition:opacity 1.6s ease .3s;
  -webkit-mask-image:linear-gradient(90deg,transparent 0%,transparent 46%,rgba(0,0,0,.35) 56%,rgba(0,0,0,.85) 66%,#000 76%);
  mask-image:linear-gradient(90deg,transparent 0%,transparent 46%,rgba(0,0,0,.35) 56%,rgba(0,0,0,.85) 66%,#000 76%);
}
#stage.on{opacity:1}
.scanline{
  position:absolute;left:0;right:0;height:170px;z-index:1;pointer-events:none;
  background:linear-gradient(180deg,transparent,rgba(34,230,255,.055),transparent);
  animation:scan 7s linear infinite;
}
@keyframes scan{0%{top:-170px}100%{top:100%}}

.hero-in{position:relative;z-index:3;display:grid;grid-template-columns:1.25fr .75fr;gap:48px;align-items:center}
.hero h1{
  font-size:clamp(2rem,4vw,3.5rem);
  margin:20px 0 22px;
  letter-spacing:-.035em;
}
.hero h1 .line{display:block;overflow:hidden}
.hero h1 .line > span{display:block;transform:translateY(105%);opacity:0;
  animation:riseUp 1.05s var(--ease) forwards}
.hero h1 .line:nth-child(1) > span{animation-delay:.15s}
.hero h1 .line:nth-child(2) > span{animation-delay:.3s}
.hero h1 .line:nth-child(3) > span{animation-delay:.45s}
@keyframes riseUp{to{transform:translateY(0);opacity:1}}
.hero .lead{max-width:530px;font-size:clamp(1rem,1.5vw,1.18rem);color:var(--ink-2)}
.hero-cta{display:flex;gap:14px;flex-wrap:wrap;margin:32px 0 36px}
.fade-in{opacity:0;animation:fadeUp 1s var(--ease) forwards}
.d1{animation-delay:.6s}.d2{animation-delay:.75s}.d3{animation-delay:.9s}.d4{animation-delay:1.05s}
@keyframes fadeUp{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:none}}

.hero-stats{display:flex;gap:34px;flex-wrap:wrap;padding-top:30px;border-top:1px solid var(--line)}
.hstat b{display:block;font-size:1.75rem;font-weight:800;letter-spacing:-.03em;
  background:linear-gradient(120deg,#fff,var(--cyan));-webkit-background-clip:text;background-clip:text;color:transparent}
.hstat small{color:var(--ink-3);font-size:.79rem;letter-spacing:.06em;text-transform:uppercase}

/* floating spec cards */
.hero-visual{position:relative;height:440px}
.float-card{
  position:absolute;padding:16px 20px;border-radius:var(--r-md);
  background:linear-gradient(150deg,rgba(20,28,46,.72),rgba(12,17,30,.5));
  border:1px solid var(--line-strong);
  backdrop-filter:blur(14px);
  box-shadow:0 24px 60px rgba(0,0,0,.5),0 0 40px rgba(34,230,255,.07) inset;
  animation:floaty 7s ease-in-out infinite;
}
.float-card .k{font-size:.7rem;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-3)}
.float-card .v{font-size:1.25rem;font-weight:800;letter-spacing:-.02em;margin-top:3px}
.float-card .v em{font-style:normal;color:var(--cyan);text-shadow:0 0 16px rgba(34,230,255,.85)}
/* Шесть позиций по краям фигуры: две колонки по три, ровными ярусами.
   Числа подобраны по замеру, а не на глаз: плашка занимает 88 px из 440
   (20 % высоты), а покачивание уводит её на ±16 px. Поэтому между
   ярусами оставлено 14 % (≈62 px) — этого хватает, чтобы соседи не
   задели друг друга даже в крайних фазах, а верхний и нижний ярусы не
   выходят за пределы первого экрана.
   Плашек в панели может быть и больше шести — лишние повторяют эти
   позиции по кругу (см. renderers.heroCards в js/main.js), поэтому
   ни одна не остаётся без места. */
.fc-1{top:5%;left:0;animation-delay:-1s}
.fc-2{top:5%;right:0;animation-delay:-3.4s}
.fc-3{top:39%;left:2%;animation-delay:-5.2s}
.fc-4{top:39%;right:2%;animation-delay:-2.2s}
.fc-5{top:73%;left:0;animation-delay:-4.3s}
.fc-6{top:73%;right:2%;animation-delay:-6.1s}
@keyframes floaty{0%,100%{transform:translateY(0)}50%{transform:translateY(-16px)}}
.fc-1 .v em{color:var(--violet);text-shadow:0 0 16px rgba(139,92,255,.9)}
.fc-3 .v em{color:var(--lime);text-shadow:0 0 16px rgba(107,255,184,.9)}
.fc-4 .v em{color:var(--violet);text-shadow:0 0 16px rgba(139,92,255,.9)}
.fc-5 .v em{color:var(--lime);text-shadow:0 0 16px rgba(107,255,184,.9)}

/* ---------- бегущая строка ----------

   Полоска технологий под первым экраном. Слова и вид движения владелец
   правит в панели («Бегущая строка»).

   ЗАЗОР ЖИВЁТ В САМОМ СЛОВЕ, А НЕ В ДОРОЖКЕ. Раньше у дорожки был `gap`,
   а сдвиг задавался как `translateX(-50%)` — и на стыке двух копий
   оставался шов ровно в половину зазора: 24 слова дают 23 промежутка, а
   половина ширины — это 11.5 промежутка. Замер на живом сайте: −50% =
   2380px, а до середины списка 2406px, разница 26px = 52/2. Полоска
   дёргалась каждый круг.

   Теперь у дорожки gap нет, а каждое слово несёт отступ справа. Ширина
   дорожки = слова × (слово + зазор), поэтому сдвиг на одну копию
   считается точно: calc(-100% / --marquee-copies).

   СКОЛЬКО КОПИЙ. Дорожка обязана быть шире экрана вдвое — иначе на
   последнем кадре анимации справа въезжает пустота, и это не теория:
   владелец прислал снимок с ней. Число копий считает js/main.js по ширине
   окна и кладёт в --marquee-copies. Здесь 2 — запасное значение для
   страницы без скрипта.

   Скорость в секундах тоже считает скрипт: она зависит от длины списка,
   а задана в пикселях в секунду, чтобы длинный список не уезжал быстрее
   короткого. Здесь 34s — запасное. */
/* Бегущая строка — самостоятельный раздел (`<section id="marquee">`), но
   отступы и слой берём не у общего правила тега, а свои: `section{padding:
   clamp(72px,9vw,140px) 0}` добавило бы полосе 115px пустоты сверху и снизу,
   а `z-index:2` опустил бы её под декор первого экрана, который она раньше
   перекрывала (у самой полосы был `z-index:3`). Гасим ТОЧЕЧНО — этот раздел,
   а не «всем секциям». */
main > section#marquee{padding:0;z-index:3}
.marquee{
  position:relative;z-index:3;border-block:1px solid var(--line);
  background:rgba(7,11,20,.6);backdrop-filter:blur(10px);
  overflow:hidden;padding:18px 0;
  mask-image:linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent);
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent);
}
.marquee-track{
  display:flex;width:max-content;
  animation:marquee-slide var(--marquee-dur,34s) linear infinite;
}
.marquee:hover .marquee-track{animation-play-state:paused}
@keyframes marquee-slide{
  to{transform:translateX(calc(-100% / var(--marquee-copies,2)))}
}
@keyframes marquee-slide-right{
  from{transform:translateX(calc(-100% / var(--marquee-copies,2)))}
  to{transform:translateX(0)}
}
.marquee-track>span{
  display:flex;align-items:center;gap:var(--marquee-gap,52px);
  padding-right:var(--marquee-gap,52px);
  font-size:.86rem;letter-spacing:.2em;text-transform:uppercase;color:var(--ink-3);white-space:nowrap;font-weight:600;
}
.marquee-track>span::after{content:'';width:5px;height:5px;border-radius:50%;
  background:var(--cyan);box-shadow:0 0 10px var(--cyan);flex:none}

/* Прокрутка вправо — та же дорожка, едет в другую сторону. */
.marquee[data-marquee-anim="scroll-right"] .marquee-track{
  animation-name:marquee-slide-right;
}

/* Прокрутка, и при этом слова вспыхивают по очереди. Задержка по номеру
   слова (--i) — поэтому подсветка идёт волной вдоль полосы, а не мигает
   вся сразу. */
@keyframes marquee-word-pulse{
  0%,100%{color:var(--ink-3);text-shadow:none}
  50%{color:var(--cyan);text-shadow:0 0 14px rgba(34,230,255,.8)}
}
.marquee[data-marquee-anim="pulse"] .marquee-track>span{
  animation:marquee-word-pulse 3.4s ease-in-out infinite;
  animation-delay:calc(var(--i,0) * .3s);
}

/* Виды без прокрутки: слова стоят по центру и вспыхивают по очереди.
   Дорожка перестаёт быть лентой — переносится по строкам и центрируется.
   Точки-разделители у них лишние: между словами и так есть отступ. */
.marquee[data-marquee-anim="appear"] .marquee-track,
.marquee[data-marquee-anim="rise"] .marquee-track,
.marquee[data-marquee-anim="none"] .marquee-track{
  width:auto;max-width:100%;margin:0 auto;flex-wrap:wrap;justify-content:center;
  row-gap:14px;animation:none;
}
.marquee[data-marquee-anim="appear"] .marquee-track>span::after,
.marquee[data-marquee-anim="rise"] .marquee-track>span::after,
.marquee[data-marquee-anim="none"] .marquee-track>span::after{display:none}

@keyframes marquee-word-appear{
  0%{opacity:0;transform:translateY(10px);color:var(--ink-3);text-shadow:none}
  35%{opacity:1;transform:translateY(0);color:var(--cyan);text-shadow:0 0 16px rgba(34,230,255,.85)}
  70%{color:var(--ink-3);text-shadow:none}
  100%{opacity:1;transform:translateY(0);color:var(--ink-3);text-shadow:none}
}
.marquee[data-marquee-anim="appear"] .marquee-track>span{
  animation:marquee-word-appear 4.6s ease-out infinite;
  animation-delay:calc(var(--i,0) * .26s);
}
@keyframes marquee-word-rise{
  0%{opacity:0;transform:translateY(22px) scale(.92);color:var(--ink-3);text-shadow:none}
  35%{opacity:1;transform:translateY(0) scale(1);color:var(--cyan);text-shadow:0 0 18px rgba(34,230,255,.85)}
  70%{color:var(--ink-3);text-shadow:none}
  100%{opacity:1;transform:translateY(0) scale(1);color:var(--ink-3);text-shadow:none}
}
.marquee[data-marquee-anim="rise"] .marquee-track>span{
  animation:marquee-word-rise 5s cubic-bezier(.22,1,.36,1) infinite;
  animation-delay:calc(var(--i,0) * .3s);
}
/* Движение строки — украшение. Кому анимация мешает, тот должен увидеть
   ровный список слов, а не мельтешение. */
@media (prefers-reduced-motion:reduce){
  .marquee-track,
  .marquee-track>span,
  .marquee-track>span::after{animation:none !important}
}

/* ---------- reveal ---------- */
.reveal{opacity:0;transform:translateY(38px);filter:blur(6px);
  transition:opacity 1s var(--ease),transform 1s var(--ease),filter 1s var(--ease)}
.reveal.in{opacity:1;transform:none;filter:blur(0)}

/* ---------- cards / services ---------- */
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:20px}
.card{
  position:relative;padding:30px 28px 32px;border-radius:var(--r-lg);
  background:linear-gradient(160deg,rgba(18,25,42,.6),rgba(9,13,23,.42));
  border:1px solid var(--line);
  isolation:isolate;
  transition:transform .55s var(--ease),border-color .5s,box-shadow .55s var(--ease);
  transform-style:preserve-3d;
}
.card::before{
  content:'';position:absolute;inset:0;z-index:0;opacity:0;transition:opacity .5s;border-radius:inherit;
  pointer-events:none;mix-blend-mode:screen;
  background:radial-gradient(420px circle at var(--mx,50%) var(--my,50%),rgba(34,230,255,.13),transparent 62%);
}
.card::after{
  content:'';position:absolute;inset:-1px;z-index:0;border-radius:inherit;
  padding:1px;pointer-events:none;
  background:linear-gradient(135deg,var(--cyan),var(--violet),var(--magenta),var(--cyan));
  background-size:300% 300%;opacity:0;transition:opacity .5s;animation:flow 6s linear infinite;
  -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
  mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  mask-composite:exclude;
}
.card:hover{transform:translateY(-8px);border-color:transparent;box-shadow:0 26px 60px rgba(0,0,0,.55),0 0 44px rgba(34,230,255,.16)}
.card:hover::before,.card:hover::after{opacity:1}
.card .ico{
  width:52px;height:52px;border-radius:15px;display:grid;place-items:center;margin-bottom:20px;
  background:linear-gradient(145deg,rgba(34,230,255,.14),rgba(139,92,255,.16));
  border:1px solid rgba(34,230,255,.3);
  box-shadow:0 0 26px rgba(34,230,255,.16),0 0 20px rgba(34,230,255,.1) inset;
  transition:transform .55s var(--ease),box-shadow .55s;
}
.card:hover .ico{transform:translateY(-4px) rotate(-6deg) scale(1.06);box-shadow:0 0 40px rgba(34,230,255,.4),0 0 24px rgba(139,92,255,.3) inset}
.card .ico svg{width:25px;height:25px;stroke:var(--cyan);fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;
  filter:drop-shadow(0 0 8px rgba(34,230,255,.9))}
.card .tag{font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;color:var(--violet);font-weight:700;margin-bottom:9px;display:block}
.card p{color:var(--ink-2);font-size:.95rem;font-weight:300;margin:12px 0 0}
.card .num{
  position:absolute;top:18px;right:24px;font-family:'JetBrains Mono',monospace;
  font-size:.78rem;color:var(--ink-3);letter-spacing:.1em;
}

/* ---------- materials ---------- */
.mats{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:16px}
.mat{
  position:relative;padding:24px 22px;border-radius:var(--r-md);
  border:1px solid var(--line);background:rgba(13,18,31,.5);
  transition:transform .45s var(--ease),border-color .45s,box-shadow .45s;
  overflow:hidden;
}
.mat::after{
  content:'';position:absolute;top:0;left:-100%;width:60%;height:100%;
  background:linear-gradient(100deg,transparent,rgba(34,230,255,.14),transparent);
  transition:left .8s var(--ease);
}
.mat:hover{transform:translateY(-6px);border-color:rgba(34,230,255,.45);box-shadow:0 0 34px rgba(34,230,255,.16)}
.mat:hover::after{left:130%}
.mat h4{font-size:1.02rem;letter-spacing:-.01em;margin-bottom:6px}
.mat h4 span{font-size:.74rem;color:var(--cyan);font-family:'JetBrains Mono',monospace;letter-spacing:.08em;display:block;margin-top:4px;font-weight:500}
.mat p{font-size:.87rem;color:var(--ink-3);margin:8px 0 0;font-weight:300}
.mat .bar{height:3px;border-radius:3px;margin-top:16px;background:rgba(255,255,255,.06);overflow:hidden}
.mat .bar i{display:block;height:100%;border-radius:3px;width:0;background:linear-gradient(90deg,var(--cyan),var(--violet));
  box-shadow:0 0 12px var(--cyan);transition:width 1.4s var(--ease)}
.mat.in .bar i{width:var(--w)}

/* ---------- process ---------- */
.steps{position:relative;display:grid;gap:0;margin-top:10px}
.steps::before{
  content:'';position:absolute;left:27px;top:14px;bottom:14px;width:1.5px;
  background:linear-gradient(180deg,transparent,var(--cyan) 8%,var(--violet) 55%,var(--magenta) 92%,transparent);
  opacity:.5;
}
.step{position:relative;display:grid;grid-template-columns:56px 1fr;gap:26px;padding:22px 0;align-items:start}
.step .dot{
  width:56px;height:56px;border-radius:50%;display:grid;place-items:center;
  background:radial-gradient(circle at 32% 28%,rgba(34,230,255,.22),rgba(9,13,23,.95));
  border:1px solid rgba(34,230,255,.4);
  font-family:'JetBrains Mono',monospace;font-weight:500;font-size:.9rem;color:var(--cyan);
  box-shadow:0 0 26px rgba(34,230,255,.28),0 0 22px rgba(34,230,255,.14) inset;
  transition:transform .5s var(--ease),box-shadow .5s;position:relative;z-index:2;
}
.step:hover .dot{transform:scale(1.1);box-shadow:0 0 44px rgba(34,230,255,.6),0 0 26px rgba(139,92,255,.4) inset}
.step .body{padding-top:6px}
.step h3{margin-bottom:7px}
.step p{color:var(--ink-2);font-size:.95rem;font-weight:300;margin:0;max-width:640px}
.step .body::after{
  content:'';position:absolute;left:0;right:0;bottom:0;height:1px;
  background:linear-gradient(90deg,var(--line),transparent 70%);
}

/* ---------- industries ---------- */
.ind{display:grid;grid-template-columns:repeat(auto-fill,minmax(158px,1fr));gap:12px}
.chip{
  display:flex;align-items:center;gap:11px;padding:15px 17px;border-radius:14px;
  border:1px solid var(--line);background:rgba(12,17,29,.5);
  font-size:.9rem;font-weight:500;color:var(--ink-2);
  transition:transform .4s var(--ease),color .4s,border-color .4s,box-shadow .4s;
}
.chip i{
  width:8px;height:8px;border-radius:50%;flex:none;
  background:linear-gradient(135deg,var(--cyan),var(--violet));
  box-shadow:0 0 12px rgba(34,230,255,.85);transition:transform .4s
}
.chip:hover{transform:translateY(-4px) scale(1.02);color:#fff;border-color:rgba(34,230,255,.45);
  box-shadow:0 0 26px rgba(34,230,255,.2),0 14px 34px rgba(0,0,0,.4)}
.chip:hover i{transform:scale(1.7)}

/* ---------- pricing ---------- */
.price-wrap{
  border-radius:var(--r-lg);border:1px solid var(--line);overflow:hidden;
  background:linear-gradient(165deg,rgba(16,23,39,.66),rgba(8,12,21,.5));
  box-shadow:0 30px 80px rgba(0,0,0,.5);
}
.price-note{
  display:flex;flex-wrap:wrap;gap:14px;align-items:center;justify-content:space-between;
  padding:22px 28px;border-bottom:1px solid var(--line);
  background:linear-gradient(90deg,rgba(34,230,255,.07),rgba(139,92,255,.07));
}
.price-note b{font-size:1.05rem}
.price-note span{color:var(--ink-2);font-size:.9rem}
table{width:100%;border-collapse:collapse}
th,td{padding:17px 28px;text-align:left;font-size:.94rem;border-bottom:1px solid rgba(120,160,220,.09)}
th{font-size:.73rem;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-3);font-weight:600;background:rgba(255,255,255,.02)}
tbody tr{transition:background .35s,box-shadow .35s}
tbody tr:hover{background:rgba(34,230,255,.055);box-shadow:0 0 34px rgba(34,230,255,.1) inset}
td:first-child{font-weight:600;color:var(--ink)}
td .tech{display:inline-block;margin-left:9px;padding:2px 9px;border-radius:100px;font-size:.68rem;font-family:'JetBrains Mono',monospace;
  letter-spacing:.08em;color:var(--cyan);border:1px solid rgba(34,230,255,.3);background:rgba(34,230,255,.07)}
td .p{font-weight:800;font-size:1.06rem;color:#fff;text-shadow:0 0 18px rgba(34,230,255,.6)}
td.muted{color:var(--ink-3);font-size:.88rem}
.price-foot{padding:20px 28px;color:var(--ink-3);font-size:.85rem;display:flex;gap:18px;flex-wrap:wrap;align-items:center;justify-content:space-between}

/* ---------- advantages ---------- */
.adv{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:22px}
.adv-item{position:relative;padding-left:22px}
.adv-item::before{content:'';position:absolute;left:0;top:8px;bottom:8px;width:2px;border-radius:2px;
  background:linear-gradient(180deg,var(--cyan),var(--violet));box-shadow:0 0 14px rgba(34,230,255,.8)}
.adv-item b{display:block;font-size:2.5rem;font-weight:800;letter-spacing:-.04em;line-height:1;
  background:linear-gradient(120deg,#fff,var(--cyan) 60%,var(--violet));-webkit-background-clip:text;background-clip:text;color:transparent}
.adv-item h4{font-size:1rem;margin:10px 0 6px}
.adv-item p{margin:0;color:var(--ink-3);font-size:.88rem;font-weight:300}

/* ---------- faq ---------- */
/* Вопросы — нативные <details>/<summary>. Высоту считает сам браузер,
   поэтому ответ не может оказаться обрезанным; открытие работает даже
   с выключенными скриптами. Класс .open больше не нужен — состояние
   берём из атрибута [open], который ставит браузер. */
/* Колонка вопросов занимает всю ширину раздела — и левым, и правым краем
   вровень с блоками выше и ниже (ряд плиток «Почему», блок контактов).

   Раньше здесь стоял max-width:900px. Слева раздел от этого не страдал —
   колонка прижата влево, — а справа оставалась полоса пустоты в 300px:
   плашки выглядели обрезанными на фоне соседних блоков во всю ширину.
   Ограничение убрано намеренно. Правило .faq больше не нужно, класс в
   разметке остаётся как точка привязки для скрипта и проверок. */
.q{border:1px solid var(--line);border-radius:var(--r-md);margin-bottom:12px;background:rgba(13,18,31,.45);
  overflow:hidden;transition:border-color .4s,box-shadow .4s,background .4s}
.q[open]{border-color:rgba(34,230,255,.4);box-shadow:0 0 34px rgba(34,230,255,.14);background:rgba(17,24,41,.6)}
.q summary{
  width:100%;display:flex;align-items:center;justify-content:space-between;gap:20px;
  padding:21px 24px;background:none;border:0;color:var(--ink);
  font-size:1.01rem;font-weight:600;text-align:left;cursor:pointer;letter-spacing:-.01em;
  list-style:none;
}
/* убираем системный треугольник: свой значок — .plus */
.q summary::-webkit-details-marker{display:none}
.q summary::marker{content:''}
.q summary:hover{color:#fff}
.q summary:focus-visible{outline:2px solid var(--cyan);outline-offset:-4px;border-radius:var(--r-md)}
.q .plus{width:26px;height:26px;flex:none;position:relative;border-radius:50%;
  border:1px solid rgba(34,230,255,.4);transition:transform .45s var(--ease),box-shadow .45s}
.q .plus::before,.q .plus::after{content:'';position:absolute;top:50%;left:50%;background:var(--cyan);
  border-radius:2px;transform:translate(-50%,-50%);box-shadow:0 0 8px var(--cyan)}
.q .plus::before{width:11px;height:1.6px}
.q .plus::after{width:1.6px;height:11px}
/* Значок: закрыт — «+», открыт — «×». Раньше круг поворачивался на 90°
   и вертикальная черта пряталась: в итоге оставалась одна вертикальная
   полоска, которая ничего не означала. Теперь поворот на 45°, а обе
   черты остаются — «+» чисто превращается в «×». */
.q[open] .plus{transform:rotate(45deg);box-shadow:0 0 20px rgba(34,230,255,.5)}
/* ---------- плавное раскрытие ответа ----------
   Высоту плашки анимирует САМ БРАУЗЕР, а не скрипт.

   Раньше здесь была только анимация появления содержимого (opacity +
   сдвиг), а сама плашка раскрывалась рывком: высоту никто не менял.
   Замерять её скриптом уже пробовали — max-height по замеру, — и
   отказались: стоило тексту переразбиться на строки (узкое окно,
   подгрузка шрифта, масштаб страницы), ответ обрезался.

   Теперь анимируем блок содержимого <details> — псевдоэлемент
   ::details-content. Ключевое слово auto в block-size разрешено через
   interpolate-size, поэтому высота едет от нуля до «сколько получилось».
   Замеров и запомненных чисел нет — обрезать нечего по построению.
   Свойство interpolate-size наследуется, поэтому задаём его на .q.

   Где ::details-content ещё не поддерживается, правила просто
   игнорируются: вопрос раскрывается мгновенно, как и раньше.

   Про закрытие. Содержимое закрытого <details> браузер убирает сразу,
   поэтому при закрытии текст пропадает в первый же момент, а плашка
   после этого плавно ужимается. Пробовал оставить текст видимым до
   конца ужимания (переход на content-visibility с задержкой) — вышло
   хуже: дискретное свойство переключается по своим правилам, и если
   вопрос закрывают вскоре после открытия, текст иногда оставался
   нарисованным. Мгновенное скрытие предсказуемо, а ужимание плашки всё
   равно плавное. */
.q{interpolate-size:allow-keywords;
  /* Кривая для раскрытия — своя. Общая --ease, cubic-bezier(.22,1,.36,1),
     отдаёт почти весь путь в первой четверти времени: на прозрачности и
     сдвиге это выглядит бодро, а на высоте читается как рывок с долгим
     доездом. Раскрытию нужна ровная кривая. */
  --ease-faq:cubic-bezier(.4,0,.2,1)}
.q::details-content{
  block-size:0;overflow:hidden;content-visibility:hidden;
  transition:block-size .45s var(--ease-faq);
}
.q[open]::details-content{block-size:auto;content-visibility:visible}
/* Отступ под вопросом растёт вместе с плашкой — иначе ответ «дёргается». */
.q summary{transition:padding-bottom .45s var(--ease-faq)}
.q .ans{overflow:hidden}
.q[open] summary{padding-bottom:12px}
/* Внутренняя часть ответа проявляется, пока плашка раздвигается. */
.q[open] .ans{animation:ansIn .45s var(--ease-faq)}
@keyframes ansIn{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:none}}
.q .ans p{margin:0;padding:0 24px 22px;color:var(--ink-2);font-size:.95rem;font-weight:300}
@media (prefers-reduced-motion:reduce){
  .q::details-content{transition:none}
  .q summary{transition:none}
  .q[open] .ans{animation:none}
}

/* ---------- contact ---------- */
.contact{display:grid;grid-template-columns:1fr 1fr;gap:34px;align-items:start}
.form{
  padding:32px;border-radius:var(--r-lg);border:1px solid var(--line);
  background:linear-gradient(160deg,rgba(18,25,42,.62),rgba(9,13,23,.45));
  box-shadow:0 30px 80px rgba(0,0,0,.5),0 0 60px rgba(34,230,255,.05) inset;
}
.field{margin-bottom:16px;position:relative}
.field label{display:block;font-size:.76rem;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);margin-bottom:8px;font-weight:600}
.field input,.field textarea,.field select{
  width:100%;padding:14px 16px;border-radius:13px;
  background:rgba(6,10,18,.75);border:1px solid var(--line);color:var(--ink);
  font-family:inherit;font-size:.95rem;transition:border-color .35s,box-shadow .35s,background .35s;
}
.field textarea{resize:vertical;min-height:104px}
.field input:focus,.field textarea:focus,.field select:focus{
  outline:none;border-color:rgba(34,230,255,.6);background:rgba(10,16,28,.9);
  box-shadow:0 0 0 3px rgba(34,230,255,.1),0 0 26px rgba(34,230,255,.2);
}
.field input::placeholder,.field textarea::placeholder{color:#4d5c76}
/* iOS зумит страницу, когда фокус попадает в поле со шрифтом меньше 16px.
   На тач-устройствах поднимаем размер до 16px: иначе посетитель с телефона
   тапает по полю, страница прыгает по масштабу и вёрстка уезжает вбок. */
@media (hover:none),(max-width:820px){
  .field input,.field textarea,.field select{font-size:16px}
}
.form-msg{
  margin-top:14px;padding:13px 16px;border-radius:12px;font-size:.9rem;display:none;
  border:1px solid rgba(107,255,184,.35);background:rgba(107,255,184,.08);color:#b6ffdb;
  box-shadow:0 0 26px rgba(107,255,184,.16);
}
.form-msg.on{display:block;animation:fadeUp .6s var(--ease)}
/* Отказ. Зелёная плашка на ошибке обманывает: «код отправлен» и «код не
   подошёл» должны различаться цветом, иначе человек читает цвет, а не
   текст, и уходит ждать письмо, которого не будет. */
.form-msg.bad{
  border-color:rgba(255,79,216,.4);background:rgba(255,79,216,.09);
  color:#ffc0ee;box-shadow:0 0 26px rgba(255,79,216,.14);
}
/* Ловушка для роботов. Спрятана за краем экрана, а не через display:none:
   часть ботов пропускает невидимые поля, но заполняет «просто спрятанные».
   Для человека и для программ чтения с экрана поле недоступно. */
.hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
.contacts-list{display:grid;gap:14px}
.c-item{
  display:flex;gap:16px;align-items:flex-start;padding:20px 22px;border-radius:var(--r-md);
  border:1px solid var(--line);background:rgba(12,17,29,.5);
  transition:transform .45s var(--ease),border-color .45s,box-shadow .45s;
}
.c-item:hover{transform:translateX(6px);border-color:rgba(34,230,255,.42);box-shadow:0 0 30px rgba(34,230,255,.16)}
.c-item .ci{width:42px;height:42px;flex:none;border-radius:12px;display:grid;place-items:center;
  background:linear-gradient(145deg,rgba(34,230,255,.13),rgba(139,92,255,.15));border:1px solid rgba(34,230,255,.28)}
.c-item .ci svg{width:19px;height:19px;stroke:var(--cyan);fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;
  filter:drop-shadow(0 0 7px rgba(34,230,255,.85))}
.c-item .k{display:block;font-size:.73rem;letter-spacing:.15em;text-transform:uppercase;color:var(--ink-3);font-weight:600}
.c-item .v{display:block;font-size:1rem;font-weight:600;margin-top:3px;word-break:break-word}
.c-item a.v:hover{color:var(--cyan);text-shadow:0 0 14px rgba(34,230,255,.8)}

/* ---------- footer ---------- */
footer{
  position:relative;z-index:2;border-top:1px solid var(--line);
  padding:52px 0 34px;background:linear-gradient(180deg,transparent,rgba(9,14,25,.7));
}
.foot-in{display:flex;justify-content:space-between;gap:28px;flex-wrap:wrap;align-items:center}
.foot-links{display:flex;gap:22px;flex-wrap:wrap;font-size:.9rem;color:var(--ink-3)}
.foot-links a:hover{color:var(--cyan);text-shadow:0 0 12px rgba(34,230,255,.7)}
.copy{color:var(--ink-3);font-size:.84rem;margin-top:22px;padding-top:22px;border-top:1px solid rgba(120,160,220,.08);
  display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap}
/* незаметный вход в панель управления — знак © в подвале */
.copy .admin-dot{color:inherit;text-decoration:none;cursor:default}
.copy .admin-dot:hover{color:var(--ink-3)}
/* Нижняя строка подвала: надписи и ссылки из панели («Подвал сайта» →
   «Нижняя строка подвала»). Одна надпись — как было раньше, без ссылки;
   их может быть и несколько. */
.copy-extra{display:flex;gap:16px;flex-wrap:wrap;align-items:baseline}
.copy-extra a:hover{color:var(--cyan);text-shadow:0 0 12px rgba(34,230,255,.7)}
/* Надпись, открывающая окно с политикой. Выглядит ссылкой, но это кнопка:
   ссылкой «открыть окно» не выразить. display здесь не задаём намеренно —
   иначе перебил бы [hidden], и спрятанная кнопка осталась бы на экране. */
.copy-link{
  padding:0;border:0;background:none;font:inherit;color:inherit;cursor:pointer;
  text-decoration:underline;text-decoration-color:rgba(120,160,220,.4);
  text-underline-offset:3px;transition:color .3s,text-decoration-color .3s;
}
.copy-link:hover{color:var(--cyan);text-decoration-color:currentColor}
/* Ссылка на окно, которое пока нечем открыть, спрятана. Два атрибута в
   селекторе — чтобы правило перебивало оформление .nav-btn и .copy-link:
   те задают свой цвет и отступы, а показать пустое окно всё равно нельзя. */
[data-modal][hidden]{display:none}

/* ---------- bento: что мы изготавливаем ---------- */
.bento{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}
.bento .b{
  position:relative;grid-column:span 1;min-height:200px;
  padding:26px 24px;border-radius:var(--r-lg);
  border:1px solid var(--line);
  background:linear-gradient(155deg,rgba(17,24,40,.62),rgba(8,12,21,.44));
  overflow:hidden;isolation:isolate;
  transition:transform .5s var(--ease),border-color .5s,box-shadow .5s;
}
.bento .b.b2{grid-column:span 2}
.bento .b::before{
  content:'';position:absolute;inset:0;z-index:0;pointer-events:none;opacity:0;transition:opacity .55s;
  mix-blend-mode:screen;
  background:radial-gradient(360px circle at var(--mx,50%) var(--my,50%),rgba(34,230,255,.16),transparent 64%);
}
.bento .b:hover{transform:translateY(-7px);border-color:rgba(34,230,255,.42);
  box-shadow:0 26px 60px rgba(0,0,0,.5),0 0 42px rgba(34,230,255,.15)}
.bento .b:hover::before{opacity:1}
.bento .b .bi{
  width:46px;height:46px;border-radius:14px;display:grid;place-items:center;margin-bottom:18px;
  background:linear-gradient(145deg,rgba(34,230,255,.14),rgba(139,92,255,.16));
  border:1px solid rgba(34,230,255,.28);
  box-shadow:0 0 24px rgba(34,230,255,.14),0 0 18px rgba(34,230,255,.09) inset;
  transition:transform .5s var(--ease),box-shadow .5s;
}
.bento .b:hover .bi{transform:translateY(-3px) rotate(-6deg) scale(1.07);box-shadow:0 0 38px rgba(34,230,255,.38)}
.bento .b .bi svg{width:22px;height:22px;stroke:var(--cyan);fill:none;stroke-width:1.6;
  stroke-linecap:round;stroke-linejoin:round;filter:drop-shadow(0 0 8px rgba(34,230,255,.85))}
.bento .b h4{font-size:1.04rem;letter-spacing:-.015em;margin-bottom:7px}
.bento .b p{margin:0;color:var(--ink-3);font-size:.88rem;font-weight:300;max-width:34ch}
.bento .b .wm{
  position:absolute;right:12px;bottom:-18px;line-height:.8;pointer-events:none;
  font-size:5.6rem;font-weight:800;letter-spacing:-.07em;
  color:transparent;-webkit-text-stroke:1px rgba(120,170,255,.13);
  transition:-webkit-text-stroke-color .55s,transform .55s var(--ease);
}
.bento .b:hover .wm{-webkit-text-stroke-color:rgba(34,230,255,.28);transform:translateY(-6px)}

/* ---------- floating actions ----------
   Значок связи. Свёрнут — одна круглая кнопка; развёрнут — столбик
   кнопок над ней. Раскрытие анимируется: каждая кнопка въезжает снизу
   со своей задержкой, поэтому веер «расходится», а не появляется
   рывком. Задержки задаёт --i (номер кнопки), его ставит main.js.

   Раскрытие работает и по наведению, и по нажатию. Наведение — для
   компьютера; на телефоне его нет, поэтому нажатие обязательно. */
.hub{
  position:fixed;right:20px;bottom:20px;z-index:65;
  display:flex;flex-direction:column;align-items:flex-end;gap:12px;
  /* Уход в 3D по расписанию владельца: поворот вокруг правого нижнего
     угла. Прозрачность не трогаем — она схлопнула бы объём у значков. */
  transform-origin:100% 100%;
  transition:transform .62s cubic-bezier(.32,.86,.28,1), visibility 0s;
}
.hub[data-show="off"]{
  transform:perspective(760px) rotateY(74deg) rotateX(-8deg) translate3d(22px,8px,-40px) scale(.72);
  visibility:hidden; pointer-events:none;
  transition:transform .62s cubic-bezier(.5,.02,.62,1), visibility 0s .62s;
}
/* Выключено владельцем — значка на сайте нет совсем. Без этой строки
   display:flex перебивал бы служебный атрибут hidden, и выключенный
   значок оставался бы на странице: панель говорила «выключено»,
   а посетитель видел кнопку. */
.hub[hidden]{display:none}
/* Столбик способов связи. Свёрнут — нулевая высота и ширина, поэтому
   он ничего не сдвигает и не перехватывает нажатия.

   Раньше здесь были только opacity:0 и pointer-events:none. Прозрачные
   кнопки при этом продолжали занимать своё место: столбик высотой в
   четыре кнопки поднимал значок связи на 236 px над нижним краем,
   и он висел в середине экрана, а не в углу. Нулевые размеры убирают
   это надёжнее всего, потому что не зависят от числа кнопок. */
/* Поле под свечение. Кнопка при наведении светится на 46 px вокруг
   себя, и обрезка по границе списка срезала этот ореол сверху и слева:
   у верхней кнопки (Telegram) он упирался в рамку и обрывался по
   прямой линии. Замер показывал выход ровно 0 — она стоит вплотную
   к краю.

   Поэтому список получает прозрачные поля ровно под ореол: 52 px
   сверху (46 размытия + запас на подъём кнопки на 3 px) и 52 px слева.
   Поля внутри элемента, а не margin снаружи: они входят в область
   обрезки overflow:hidden, значит ореол рисуется, а не режется.
   Снаружи размеры не меняются — поля и max-height вычитаются друг
   из друга, и веер по-прежнему сворачивается в ноль.

   Снизу поле тоже нужно, и по той же причине. Раньше здесь стояло
   4 px — «список и так прижат к нижнему краю, обрезать нечего». Это
   было неверно: нижняя кнопка в ряду последняя, её ореол уходит вниз
   ровно так же, как у верхней уходит вверх, и нижние 46 px срезались.
   Владелец показал это красной чертой под «Написать на почту».

   Но список стоит у нижней кромки экрана, и если просто добавить поле
   снизу, весь веер уедет вверх на те же 52 px. Поэтому список смещён
   вниз на величину поля (inset-bottom отрицательный) — видимое
   положение кнопок не меняется, а место под ореол появляется.

   Справа поля не нужны: подпись выровнена по правому краю, ореол идёт
   влево и вверх-вниз, и вправо ему выходить некуда.

   Поля анимируются вместе с размерами. Это не украшение: постоянные
   поля не давали списку схлопнуться в ноль — свёрнутый оставался
   52 на 56 px. Само по себе это безвредно (pointer-events:none, значка
   не сдвигает), но ноль надёжнее: невидимая рамка ровно ничего не
   занимает и не может помешать ни при каком числе кнопок. */
.hub-list{
  display:flex;flex-direction:column;align-items:flex-end;gap:0;
  pointer-events:none;overflow:hidden;
  padding:0;
  /* Смещение вниз на размер будущего поля. В покое ноль — свёрнутый
     список стоит ровно там же, где стоял. */
  bottom:0;
  max-height:0;max-width:0;
  transition:max-height .42s var(--ease),max-width .42s var(--ease),
             gap .42s var(--ease),padding .42s var(--ease),
             bottom .42s var(--ease);
}
/* Потолки включают поля: 52 px сверху и 52 px снизу. Без этой прибавки
   поля съели бы часть полезной высоты, и на телефоне список обрезался
   бы на одну кнопку раньше. */
.hub[data-open="1"] .hub-list{
  pointer-events:auto;gap:10px;
  padding:52px 0 52px 52px;
  /* Отрицательный inset-bottom: список опускается на нижнее поле и
     возвращает кнопки на прежнее место. Верх списка при этом уезжает
     вверх на 52 px — но там поле и нужно, оно под ореол верхней кнопки. */
  bottom:-52px;
  max-height:calc(70vh + 104px);max-width:392px;
}

/* Кнопка способа связи. */
.hub-item{
  display:flex;align-items:center;gap:10px;
  opacity:0;transform:translateY(14px) scale(.7);transform-origin:100% 50%;
  transition:opacity .32s var(--ease),transform .42s var(--ease);
  /* Задержка растёт с номером: нижняя кнопка стартует первой. */
  transition-delay:calc(var(--i,0) * 45ms);
  text-decoration:none;color:inherit;
  /* «Заказать звонок» — это <button>, а не <a>: у кнопок браузер
     рисует свой светлый фон, рамку и отступы. Без этой строки вокруг
     надписи оставалась белая полоса, которая выбивалась из ряда
     тёмных кнопок. Сбрасываем вид у самого элемента, а не глобально,
     чтобы не задеть другие кнопки сайта. */
  background:none;border:0;font:inherit;padding:0;cursor:pointer;
  -webkit-appearance:none;appearance:none;
}
.hub[data-open="1"] .hub-item{opacity:1;transform:none}

.hub-name{
  padding:7px 13px;border-radius:999px;white-space:nowrap;
  background:rgba(8,12,22,.9);border:1px solid var(--line-strong);
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);
  font-size:13.5px;font-weight:600;letter-spacing:.01em;
  box-shadow:0 8px 26px rgba(0,0,0,.5);
  /* Подпись тоже выезжает — но из-под кнопки, чуть отставая. */
  opacity:0;transform:translateX(10px);
  transition:opacity .3s var(--ease),transform .3s var(--ease),
             border-color .35s,box-shadow .35s,color .35s;
  transition-delay:calc(var(--i,0) * 45ms + 60ms);
}
.hub[data-open="1"] .hub-name{opacity:1;transform:none}

/* Подпись светится вместе с кружком.

   Раньше подсветка была только у кружка, и посетитель, наводя курсор
   на название — а целится он обычно именно в него, потому что там
   текст, — видел тёмную плашку и решал, что у этой кнопки эффекта
   нет. Теперь строка отвечает целиком: кружок и подпись загораются
   одним цветом.

   Цвет берём у соседнего кружка через :has(). Класс палитры
   (c-green, c-blue…) стоит на кружке, а подпись — его сосед справа,
   и обычным селектором «назад» к классу не дотянуться. Значение
   currentColor тут не помогло бы: у подписи он свой, унаследованный
   от текста страницы, а не от кнопки. */

/* Общая реакция: плашка приподнимается и чуть отъезжает от кружка —
   движение то же, что у кружка, чтобы строка шла одним целым. */
.hub-item:hover .hub-name,.hub-item:focus-visible .hub-name{
  transform:translateX(-3px);
  border-color:rgba(34,230,255,.7);
  box-shadow:0 8px 26px rgba(0,0,0,.5),0 0 20px rgba(34,230,255,.34);
}
/* Дальше — цвет палитры. Явные правила на каждый цвет: взять его из
   кружка нельзя (см. выше), а список цветов закрытый, он совпадает
   со списком классов кружка. */
.hub-item:has(.hub-btn.c-green):hover .hub-name,
.hub-item:has(.hub-btn.c-green):focus-visible .hub-name{border-color:rgba(107,255,184,.85);box-shadow:0 8px 26px rgba(0,0,0,.5),0 0 20px rgba(107,255,184,.4)}
.hub-item:has(.hub-btn.c-violet):hover .hub-name,
.hub-item:has(.hub-btn.c-violet):focus-visible .hub-name{border-color:rgba(139,92,255,.85);box-shadow:0 8px 26px rgba(0,0,0,.5),0 0 20px rgba(139,92,255,.4)}
.hub-item:has(.hub-btn.c-blue):hover .hub-name,
.hub-item:has(.hub-btn.c-blue):focus-visible .hub-name{border-color:rgba(70,170,255,.85);box-shadow:0 8px 26px rgba(0,0,0,.5),0 0 20px rgba(70,170,255,.4)}
.hub-item:has(.hub-btn.c-orange):hover .hub-name,
.hub-item:has(.hub-btn.c-orange):focus-visible .hub-name{border-color:rgba(255,160,60,.85);box-shadow:0 8px 26px rgba(0,0,0,.5),0 0 20px rgba(255,160,60,.4)}
.hub-item:has(.hub-btn.c-pink):hover .hub-name,
.hub-item:has(.hub-btn.c-pink):focus-visible .hub-name{border-color:rgba(255,79,216,.85);box-shadow:0 8px 26px rgba(0,0,0,.5),0 0 20px rgba(255,79,216,.42)}
.hub-item:has(.hub-btn.c-red):hover .hub-name,
.hub-item:has(.hub-btn.c-red):focus-visible .hub-name{border-color:rgba(255,90,90,.85);box-shadow:0 8px 26px rgba(0,0,0,.5),0 0 20px rgba(255,90,90,.4)}
.hub-item:has(.hub-btn.c-gray):hover .hub-name,
.hub-item:has(.hub-btn.c-gray):focus-visible .hub-name{border-color:rgba(195,206,230,.75);box-shadow:0 8px 26px rgba(0,0,0,.5),0 0 20px rgba(195,206,230,.3)}
.hub-item:has(.hub-btn.c-brand):hover .hub-name,
.hub-item:has(.hub-btn.c-brand):focus-visible .hub-name,
.hub-item:has(.hub-btn.c-):hover .hub-name,
.hub-item:has(.hub-btn.c-):focus-visible .hub-name{border-color:rgba(34,230,255,.85);box-shadow:0 8px 26px rgba(0,0,0,.5),0 0 20px rgba(34,230,255,.4)}

.hub-btn{
  width:50px;height:50px;flex:0 0 auto;border-radius:50%;
  display:grid;place-items:center;position:relative;
  border:1px solid var(--line-strong);background:rgba(10,15,26,.86);
  backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
  color:var(--ink);cursor:pointer;padding:0;overflow:hidden;
  box-shadow:0 10px 30px rgba(0,0,0,.55);
  /* Свечение собрано из двух слоёв: box-shadow (ореол вокруг) и
     ::before (мягкое пятно цвета внутри). Они меняются за разное
     время, поэтому кнопка отзывается «живо», а не включается рывком. */
  transition:transform .35s var(--ease),box-shadow .35s,border-color .35s,color .35s;
}
.hub-btn svg{width:22px;height:22px;stroke:currentColor;fill:none;
  stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;
  position:relative;z-index:2;
  transition:transform .35s var(--ease),filter .35s}
/* Своя картинка вместо значка. */
.hub-btn img{position:relative;z-index:2;width:26px;height:26px;
  object-fit:contain;display:block;
  transition:transform .35s var(--ease),filter .35s}
/* Мягкое цветное пятно под значком. Берёт цвет из currentColor, то
   есть из палитры кнопки, поэтому отдельного правила на каждый цвет
   не нужно. Пока курсора нет — пятна нет. */
.hub-btn::before{
  content:'';position:absolute;inset:0;border-radius:50%;
  background:radial-gradient(circle at 50% 58%,currentColor,transparent 70%);
  opacity:0;pointer-events:none;transition:opacity .4s var(--ease);
}
/* Свой код кнопки: он и есть содержимое, поэтому ячейка растягивается
   в прямоугольник и снимает обрезку по кругу. */
.hub-btn.is-html{
  width:auto;min-width:50px;max-width:230px;height:auto;min-height:50px;
  border-radius:16px;padding:8px 12px;overflow:visible;
}
/* У прямоугольной кнопки пятно идёт по её форме, а не по кругу —
   иначе углы оставались бы тёмными на светящемся фоне. */
.hub-btn.is-html::before{border-radius:16px}
.hub-btn.is-html .hub-custom{font-size:13px;line-height:1.35;text-align:center}
.hub-btn.is-html .hub-custom img{max-width:100%;height:auto}

/* Наведение. Кнопка приподнимается, внутри загорается цветное пятно,
   значок слегка увеличивается. Свечение и движение вместе и дают
   ощущение «живой» кнопки.

   Правило висит и на самом кружке, и на СТРОКЕ вокруг него. Строка
   шире кружка: в ней ещё подпись справа, и посетитель целится чаще
   всего в неё или в середину строки, а не в кружок. Со «.hub-btn:hover»
   подсветка загоралась только при точном попадании в 50 точек кружка,
   а при наведении на подпись кнопка оставалась тёмной — со стороны это
   выглядело как «на этой кнопке эффекта нет». Наведение на строку —
   это и есть наведение на кнопку. */
.hub-btn:hover,.hub-item:hover .hub-btn,.hub-item:focus-visible .hub-btn{
  transform:translateY(-3px) scale(1.08);
  border-color:rgba(34,230,255,.6);box-shadow:0 0 32px rgba(34,230,255,.42);
}
.hub-btn:hover::before,.hub-item:hover .hub-btn::before,.hub-item:focus-visible .hub-btn::before{opacity:.3}
.hub-btn:hover svg,.hub-btn:hover img,.hub-item:hover .hub-btn svg,.hub-item:hover .hub-btn img,
.hub-item:focus-visible .hub-btn svg,
.hub-item:focus-visible .hub-btn img{
  transform:scale(1.12);filter:drop-shadow(0 0 8px currentColor);
}

/* Цвета свечения. Ключ приходит из панели, поэтому список закрытый:
   значение не из списка просто не даст свечения, но кнопка останется. */
.hub-btn.c-green{color:#6bffb8;border-color:rgba(107,255,184,.42);
  box-shadow:0 0 24px rgba(107,255,184,.22)}
.hub-btn.c-green:hover,.hub-item:hover .hub-btn.c-green,.hub-item:focus-visible .hub-btn.c-green{
  border-color:rgba(107,255,184,.95);
  box-shadow:0 0 22px rgba(107,255,184,.55),0 0 46px rgba(107,255,184,.32),0 6px 24px rgba(0,0,0,.5)}
.hub-btn.c-violet{color:#b79cff;border-color:rgba(139,92,255,.45);
  box-shadow:0 0 24px rgba(139,92,255,.24)}
.hub-btn.c-violet:hover,.hub-item:hover .hub-btn.c-violet,.hub-item:focus-visible .hub-btn.c-violet{
  border-color:rgba(139,92,255,.95);
  box-shadow:0 0 22px rgba(139,92,255,.55),0 0 46px rgba(139,92,255,.34),0 6px 24px rgba(0,0,0,.5)}
.hub-btn.c-blue{color:#7ec8ff;border-color:rgba(70,170,255,.45);
  box-shadow:0 0 24px rgba(70,170,255,.24)}
.hub-btn.c-blue:hover,.hub-item:hover .hub-btn.c-blue,.hub-item:focus-visible .hub-btn.c-blue{
  border-color:rgba(70,170,255,.95);
  box-shadow:0 0 22px rgba(70,170,255,.55),0 0 46px rgba(70,170,255,.34),0 6px 24px rgba(0,0,0,.5)}
.hub-btn.c-orange{color:#ffbd6b;border-color:rgba(255,160,60,.45);
  box-shadow:0 0 24px rgba(255,160,60,.24)}
.hub-btn.c-orange:hover,.hub-item:hover .hub-btn.c-orange,.hub-item:focus-visible .hub-btn.c-orange{
  border-color:rgba(255,160,60,.95);
  box-shadow:0 0 22px rgba(255,160,60,.55),0 0 46px rgba(255,160,60,.34),0 6px 24px rgba(0,0,0,.5)}
.hub-btn.c-pink{color:#ff9ce4;border-color:rgba(255,79,216,.45);
  box-shadow:0 0 24px rgba(255,79,216,.24)}
.hub-btn.c-pink:hover,.hub-item:hover .hub-btn.c-pink,.hub-item:focus-visible .hub-btn.c-pink{
  border-color:rgba(255,79,216,.95);
  box-shadow:0 0 22px rgba(255,79,216,.55),0 0 46px rgba(255,79,216,.36),0 6px 24px rgba(0,0,0,.5)}
.hub-btn.c-red{color:#ff8b8b;border-color:rgba(255,90,90,.45);
  box-shadow:0 0 24px rgba(255,90,90,.24)}
.hub-btn.c-red:hover,.hub-item:hover .hub-btn.c-red,.hub-item:focus-visible .hub-btn.c-red{
  border-color:rgba(255,90,90,.95);
  box-shadow:0 0 22px rgba(255,90,90,.55),0 0 46px rgba(255,90,90,.34),0 6px 24px rgba(0,0,0,.5)}
/* Серая кнопка была единственной без реакции на курсор: правила
   :hover у неё не было вовсе, и она казалась «мёртвой» в ряду
   остальных. Теперь у неё та же реакция, только цвет сдержаннее. */
.hub-btn.c-gray{color:#c3cee6;border-color:var(--line-strong);
  box-shadow:0 10px 30px rgba(0,0,0,.55)}
.hub-btn.c-gray:hover,.hub-item:hover .hub-btn.c-gray,.hub-item:focus-visible .hub-btn.c-gray{
  border-color:rgba(195,206,230,.8);
  box-shadow:0 0 22px rgba(195,206,230,.4),0 0 46px rgba(195,206,230,.22),0 6px 24px rgba(0,0,0,.5)}
.hub-btn.c-brand,.hub-btn.c-{color:var(--cyan);
  border-color:rgba(34,230,255,.45);box-shadow:0 0 24px rgba(34,230,255,.24)}
.hub-btn.c-brand:hover,.hub-btn.c-:hover,.hub-item:hover .hub-btn.c-brand,.hub-item:hover .hub-btn.c-,.hub-item:focus-visible .hub-btn.c-brand{
  border-color:rgba(34,230,255,.95);
  box-shadow:0 0 22px rgba(34,230,255,.55),0 0 46px rgba(34,230,255,.34),0 6px 24px rgba(0,0,0,.5)}

/* Ведущая (свёрнутая) кнопка. У неё два значка: свой и крестик. */
.hub-toggle{
  width:58px;height:58px;border-radius:50%;position:relative;
  display:grid;place-items:center;cursor:pointer;padding:0;
  border:1px solid rgba(34,230,255,.5);background:rgba(10,15,26,.86);
  backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
  color:var(--cyan);box-shadow:0 0 28px rgba(34,230,255,.28),0 10px 30px rgba(0,0,0,.55);
  transition:transform .4s var(--ease),box-shadow .4s,border-color .4s;
}
.hub-toggle:hover{transform:translateY(-3px) scale(1.06);
  border-color:rgba(34,230,255,.95);box-shadow:0 0 42px rgba(34,230,255,.5)}
.hub-toggle svg{width:24px;height:24px;stroke:currentColor;fill:none;
  stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}
.hub-toggle img{width:28px;height:28px;object-fit:contain}
.hub-toggle-icon,.hub-toggle-x{
  position:absolute;inset:0;display:grid;place-items:center;
  transition:opacity .3s var(--ease),transform .4s var(--ease);
}
/* Значки, сменяющие друг друга внутри свёрнутого кружка.

   Все они лежат в одной ячейке друг поверх друга, поэтому позиция
   у них абсолютная: три значка в потоке растянули бы кружок и
   сдвинули крестик.

   ЦЕНТРОВКА. `place-items:center` на ячейке здесь НЕ работает:
   абсолютно позиционированный ребёнок выведен из потока сетки, и
   выравнивание сетки на него не действует. Владелец и увидел
   следствие: `inset:0` вместе с явной шириной/высотой прибивал
   значок к ЛЕВОМУ ВЕРХНЕМУ углу круга (left:0/top:0 при width:30px —
   это ровно угол), а не ставил по центру. Поэтому центрируем так,
   как это работает для absolute: inset:0 + margin:auto — тогда
   браузер сам делит свободное место поровну со всех сторон. */
.hub-toggle-icon .hub-icon{
  position:absolute;inset:0;margin:auto;
  display:block;
  will-change:transform,opacity;
}
/* Приход и уход — разные классы, потому что направление движения
   у них противоположное. Обе стороны идут по одному времени: если
   уход окажется длиннее прихода, в кружке на миг будет пусто.

   ТРИ состояния, и это важно. Значок проходит три фазы:
     нет классов   — видимый, устоявшийся;
     .is-in        — ВЫЕЗЖАЕТ (начальное смещение, отсюда переход);
     .is-out       — УЕЗЖАЕТ (конечное смещение, отсюда переход).
   Раньше конечное состояние выражалось тем же классом is-in
   (`.is-in.is-in`), и это была мина: значок, закончивший приезд,
   оставался с is-in навсегда, а когда позже становился уходящим,
   получал ещё и is-out — оба класса разом. Вес `.is-in.is-in`
   (0,3,0) перебивал `.is-out` (0,2,0), поэтому уходящий значок не
   гас и не двигался: два значка лежали друг на друге в углу, и
   смена выглядела как «анимации нет». Теперь «видимый» — это
   отсутствие обоих классов, и столкнуться им негде. */
.hub-toggle-icon .hub-icon.is-in,
.hub-toggle-icon .hub-icon.is-out{
  transition:opacity .42s var(--ease),transform .42s var(--ease);
}
/* Плавное затухание — спокойный вариант по умолчанию. */
.hub-toggle-icon[data-anim="fade"] .is-out{opacity:0}
.hub-toggle-icon[data-anim="fade"] .is-in{opacity:0}
/* Сдвиг снизу вверх. */
.hub-toggle-icon[data-anim="slide"] .is-out{opacity:0;transform:translateY(-14px)}
.hub-toggle-icon[data-anim="slide"] .is-in{opacity:0;transform:translateY(14px)}
/* Разворот вокруг горизонтальной оси — как табличка. */
.hub-toggle-icon[data-anim="flip"] .is-out{opacity:0;transform:rotateX(90deg)}
.hub-toggle-icon[data-anim="flip"] .is-in{opacity:0;transform:rotateX(-90deg)}
/* Наплыв из центра. */
.hub-toggle-icon[data-anim="zoom"] .is-out{opacity:0;transform:scale(.45)}
.hub-toggle-icon[data-anim="zoom"] .is-in{opacity:0;transform:scale(1.5)}
/* Поворот вокруг своей оси. Уходит в одну сторону, приходит с
   другой — так движение читается как «прокрутка», а не как рывок. */
.hub-toggle-icon[data-anim="rotate"] .is-out{opacity:0;transform:rotate(-110deg) scale(.5)}
.hub-toggle-icon[data-anim="rotate"] .is-in{opacity:0;transform:rotate(110deg) scale(.5)}
/* Конец приезда: класс is-in снимает скрипт, и значок возвращается
   к «устоявшемуся» виду — без смещения и прозрачности. Правило
   держим явным, чтобы у перехода была цель: без конечного значения
   браузер не знает, куда анимировать, и подменяет кадры рывком. */
.hub-toggle-icon .hub-icon:not(.is-in):not(.is-out){
  opacity:1;transform:none;
}
/* Значок в свёрнутом кружке крупнее, чем в кнопках веера: круг
   58 точек, и мелкий значок внутри него выглядел потерянным.

   ВАЖНО про селектор: класс hub-icon висит на САМОМ svg, а не на
   обёртке вокруг него. Поэтому «.hub-icon svg» не находил ничего —
   правило размера не срабатывало вовсе, и svg оставался со своей
   умолчательной величиной 24×24, прижатым к левому верхнему углу
   ячейки. Отсюда и жалоба владельца: значок мелкий и не по центру.
   Пишем селектор по самому элементу. */
.hub-toggle-icon .hub-icon{
  width:30px;height:30px;
}
/* Своя картинка вместо значка — тот же размер, что и у нарисованного. */
.hub-toggle-icon .hub-icon img,
.hub-toggle-icon img.hub-icon{
  width:30px;height:30px;object-fit:contain;display:block;
}
.hub-toggle-x{opacity:0;transform:rotate(-90deg) scale(.6)}
.hub[data-open="1"] .hub-toggle-icon{opacity:0;transform:rotate(90deg) scale(.6)}
.hub[data-open="1"] .hub-toggle-x{opacity:1;transform:none}
/* Подпись рядом со свёрнутой кнопкой — подсказка посетителю.

   Рамка сделана «облачком мысли»: сильно скруглённый прямоугольник
   плюс три кружка-хвостика, которые ведут от облачка к значку связи.
   Хвостик — псевдоэлементы, а не картинка: он должен тянуться вместе
   с длиной текста, а по ширине подпись меняется (владелец правит
   заголовок в панели). */
.hub-toggle-label{
  position:absolute;right:calc(100% + 22px);
  /* Облачко поднято над кружком, а не выровнено с ним по верху.
     Замер показывал, что верх подписи стоял на 11 px НИЖЕ верха
     кружка — читалось как «подпись свисает», хотя по смыслу это
     мысль, которая идёт от значка вверх-влево. negative top поднимает
     её на 22 px: облачко встаёт заметно выше значка и не спорит с
     кнопками веера, который раскрывается прямо под ним. */
  top:-22px;padding:9px 16px;
  border-radius:22px;white-space:nowrap;font-size:13.5px;font-weight:600;
  background:rgba(8,12,22,.92);border:1px solid var(--line-strong);
  color:var(--ink);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);
  box-shadow:0 8px 26px rgba(0,0,0,.5),
    inset 0 1px 0 rgba(255,255,255,.06),
    0 0 22px rgba(34,230,255,.12);
  transition:opacity .3s var(--ease),transform .3s var(--ease);
}
/* Два кружка-хвостика: крупный у самого облачка и мелкий ближе к
   значку — так читается «мысль идёт от значка к подписи». */
.hub-toggle-label::before,
.hub-toggle-label::after{
  content:'';position:absolute;border-radius:50%;
  background:rgba(8,12,22,.92);border:1px solid var(--line-strong);
}
.hub-toggle-label::before{width:9px;height:9px;right:-6px;bottom:5px}
.hub-toggle-label::after{width:5px;height:5px;right:-15px;bottom:-4px}
.hub[data-open="1"] .hub-toggle-label{opacity:0;transform:translateX(8px)}

/* Буквы, которые «печатаются». Каждая появляется со своим
   подпрыгиванием — это и создаёт ощущение, что текст набирают. */
.hub-type{display:inline-block;white-space:pre}
.hub-char{
  display:inline-block;
  animation:hubTypeChar .34s var(--ease) both;
}
@keyframes hubTypeChar{
  from{opacity:0;transform:translateY(5px) scale(.8)}
  to{opacity:1;transform:none}
}
/* Курсор набора. Мигает, пока текст пишется, и гаснет, когда всё
   набрано: постоянная мигалка отвлекала бы сильнее, чем помогает. */
.hub-caret{
  display:inline-block;width:2px;height:1em;margin-left:2px;
  vertical-align:-2px;background:var(--cyan);
  box-shadow:0 0 8px rgba(34,230,255,.8);
  animation:hubCaretBlink 1s steps(1,end) infinite;
}
.hub-caret.done{animation:none;opacity:0;transition:opacity .4s}
@keyframes hubCaretBlink{0%,49%{opacity:1}50%,100%{opacity:0}}
/* Стрелка наверх живёт здесь же и остаётся на месте.
   Базовые размеры обязательны: у .hub-toggle они свои, и без этой
   строки стрелка схлопывалась до размера значка внутри (16×24) —
   по ней нельзя было попасть пальцем на телефоне. */
.hub .fab{
  width:52px;height:52px;border-radius:50%;
  display:grid;place-items:center;cursor:pointer;padding:0;
  border:1px solid var(--line-strong);background:rgba(10,15,26,.86);
  backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
  color:var(--ink-dim);
  box-shadow:0 10px 30px rgba(0,0,0,.5);
  transition:transform .4s var(--ease),box-shadow .4s,border-color .4s,color .4s;
}
.hub .fab:hover{color:var(--cyan);transform:translateY(-3px);
  border-color:rgba(34,230,255,.7);box-shadow:0 0 30px rgba(34,230,255,.3)}
.hub .fab svg{width:22px;height:22px;stroke:currentColor;fill:none;
  stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}
.hub .fab.top{opacity:0;pointer-events:none;transform:translateY(16px)}
.hub .fab.top.show{opacity:1;pointer-events:auto;transform:none}

/* На узких экранах кнопки мельче, подписи не показываем: они закрыли бы
   половину страницы, а способ связи виден и по значку. */
@media (max-width:520px){
  .hub{right:14px;bottom:14px}
  .hub-toggle{width:52px;height:52px}
  .hub .fab{width:46px;height:46px}
  .hub .fab svg{width:20px;height:20px}
  .hub-btn{width:46px;height:46px}
  .hub-btn svg{width:20px;height:20px}
  .hub-name{display:none}
}

/* Кому анимация мешает — раскрываем сразу, без движения и задержек.
   Важно не отключить переходы у .hub-list: он отвечает за размеры, а
   не за движение. Если снять их и здесь, свёрнутый столбик останется
   нулевым навсегда — значок перестанет раскрываться вовсе. */
@media (prefers-reduced-motion:reduce){
  .hub-item,.hub-name,.hub-toggle-icon,.hub-toggle-x,.hub-btn,.hub-toggle,
  .hub-btn svg,.hub-btn img,.hub-btn::before,.hub-caret{
    transition-duration:.01ms !important;transition-delay:0ms !important}
  .hub-item{transform:none}
  .hub-name{transform:none}
  .hub-list{transition-duration:.01ms}
  /* Набор букв — украшение. Кому анимация мешает, тот должен увидеть
     готовый текст сразу, а не ждать его по буквам. */
  .hub-char{animation:none}
  .hub-caret{animation:none;opacity:0}
}

/* ---------- окошко «заказать звонок» ----------
   Z-СЛОЙ — 160, И ЭТО НЕ ЗАПАС «на всякий случай» (29.10, вечер).

   Владелец: «в корзине при оформлении заказа нажал на политику
   конфиденциальности и окно политики не видно, оно должно всплывать поверх
   оформления заказа». Окно политики — это та же рамка `.cb-wrap`, а она
   стояла на 80, тогда как корзина — 95, окно работы — 120, окно товара
   поверх работы и окно «в полный рост» — 130. Политика открывалась ПОД
   корзиной: видно было только затемнение.

   Отсюда общий порядок слоёв (снизу вверх), чтобы следующая правка не
   спорила с этой:
     80  — служебные полосы страницы (шапка 60, прогресс 70);
     95  — корзина;
    120  — окно работы;
    130  — окно товара ПОВЕРХ работы и окно «в полный рост»;
    160  — окна-документы: политика и «заказать звонок» (этот слой);
    200  — вопрос о возрасте: он обязан перекрыть всё, включая окно работы
           и окно товара (см. `.age`).
   Правило простое: окно, которое ЧИТАЮТ, лежит выше окна, из которого его
   открыли, а вопрос, без ответа на который дальше нельзя, — выше всех. */
.cb-wrap{position:fixed;inset:0;z-index:160;display:grid;place-items:center;padding:18px}
.cb-wrap[hidden]{display:none}
.cb-back{position:absolute;inset:0;background:rgba(2,4,10,.72);
  backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
  opacity:0;transition:opacity .3s var(--ease)}
.cb-wrap.on .cb-back{opacity:1}
.cb{
  position:relative;width:100%;max-width:400px;padding:26px 24px 22px;
  border-radius:var(--r-md);border:1px solid var(--line-strong);
  background:linear-gradient(160deg,rgba(16,22,36,.98),rgba(9,13,22,.99));
  box-shadow:0 30px 80px rgba(0,0,0,.7),0 0 60px rgba(34,230,255,.08);
  opacity:0;transform:translateY(18px) scale(.97);
  transition:opacity .3s var(--ease),transform .4s var(--ease);
}
.cb-wrap.on .cb{opacity:1;transform:none}
.cb-close{
  position:absolute;top:12px;right:12px;width:34px;height:34px;border-radius:50%;
  display:grid;place-items:center;cursor:pointer;padding:0;
  border:1px solid var(--line);background:rgba(10,15,26,.7);color:var(--ink-2);
  transition:color .3s,border-color .3s,transform .3s var(--ease);
}
.cb-close:hover{color:var(--ink);border-color:var(--line-strong);transform:rotate(90deg)}
.cb-close svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round}
.cb-title{margin:0 0 6px;font-size:21px;font-weight:700;letter-spacing:-.01em}
.cb-sub{margin:0 0 18px;font-size:14px;color:var(--ink-2);line-height:1.5}
.cb-field{display:block;margin-bottom:14px}
.cb-field > span{display:block;margin-bottom:6px;font-size:13px;font-weight:600;color:var(--ink-2)}
.cb-field input{
  width:100%;padding:13px 14px;border-radius:var(--r-sm);
  border:1px solid var(--line-strong);background:rgba(6,10,18,.8);
  color:var(--ink);font:inherit;font-size:15px;
  transition:border-color .3s,box-shadow .3s;
}
.cb-field input:focus{outline:none;border-color:rgba(34,230,255,.7);
  box-shadow:0 0 0 3px rgba(34,230,255,.14)}
.cb-field input.bad{border-color:rgba(255,79,216,.75);
  box-shadow:0 0 0 3px rgba(255,79,216,.16)}
/* Ловушка для ботов: поле есть, но человек его не видит и не заполняет. */
.cb-trap{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}

/* Капча. Вопрос и поле ответа стоят в одной строке с кнопкой «другое
   задание»: так видно, что вопрос можно сменить, если он непонятен.
   [hidden] обязателен с display:flex — иначе выключенная капча всё
   равно занимала бы место в окошке. */
.cb-captcha{margin-bottom:14px}
.cb-captcha[hidden]{display:none}
.cb-cap-row{display:flex;gap:8px;align-items:stretch}
.cb-cap-row input{flex:1 1 auto;min-width:0}
.cb-cap-reload{
  flex:0 0 auto;width:46px;border-radius:var(--r-sm);
  display:grid;place-items:center;cursor:pointer;padding:0;
  border:1px solid var(--line-strong);background:rgba(10,15,26,.7);
  color:var(--ink-2);
  transition:color .3s,border-color .3s,transform .4s var(--ease);
}
.cb-cap-reload:hover{color:var(--cyan);border-color:rgba(34,230,255,.7);
  transform:rotate(180deg)}
.cb-cap-reload svg{width:18px;height:18px;stroke:currentColor;fill:none;
  stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}
/* Подсказка о форме ответа. Приглушённая и мелкая: она объясняет, но не
   спорит с заданием за внимание. Появляется только когда капча включена —
   текст ставит скрипт, а при выключенной капче весь блок скрыт целиком. */
.cb-cap-hint{
  display:block;margin-top:6px;font-size:12px;line-height:1.35;
  color:var(--ink-dim);letter-spacing:.01em;
}
.cb-cap-hint:empty{display:none}

/* Капча главной формы заявки («Опишите задачу»). Отдельные классы, а не
   переиспользование .cb-*: у окошка звонка своя вёрстка и свои размеры,
   и общий класс связал бы две разные формы — правка одной ломала бы
   другую. Вид поля берём от полей формы, чтобы капча не выглядела чужой
   среди «Как вас зовут» и «Задача». */
.fc{margin-bottom:16px}
.fc[hidden]{display:none}
/* Вопрос набран обычным регистром, а не как подписи полей формы
   (.field label — заглавными). Заглавные превратили бы «Сколько будет
   семь × 4?» в крик, а прочитать его надо быстро. */
/* Размер и цвет вопроса задаёт владелец в настройках блока: у блока капчи
   стоят переменные --fc-q-size/--fc-q-color. Нет переменной — работает то,
   что было: у живой формы из раздела «Контакты» настроек нет вовсе. */
.fc-q{display:block;margin-bottom:8px;font-size:var(--fc-q-size,13px);font-weight:600;
  color:var(--fc-q-color,var(--ink-2))}
.fc-row{display:flex;gap:8px;align-items:stretch}
.fc-row input{
  flex:1 1 auto;min-width:0;padding:14px 16px;border-radius:13px;
  background:rgba(6,10,18,.75);border:1px solid var(--line);color:var(--ink);
  font-family:inherit;font-size:.95rem;
  transition:border-color .35s,box-shadow .35s,background .35s;
}
.fc-row input:focus{outline:none;border-color:rgba(34,230,255,.6);
  background:rgba(10,16,28,.9);
  box-shadow:0 0 0 3px rgba(34,230,255,.1),0 0 26px rgba(34,230,255,.2)}
/* Неверный ответ подсвечиваем тем же цветом, что и незаполненные поля
   формы: человеку не нужно учить второй язык ошибок. */
.fc-row input.bad{border-color:rgba(255,79,216,.75);
  box-shadow:0 0 0 3px rgba(255,79,216,.16)}
.fc-row input::placeholder{color:#4d5c76}
.fc-reload{
  flex:0 0 auto;width:52px;border-radius:13px;
  display:grid;place-items:center;cursor:pointer;padding:0;
  border:1px solid var(--line);background:rgba(6,10,18,.75);
  color:var(--ink-2);
  transition:color .3s,border-color .3s,transform .4s var(--ease);
}
.fc-reload:hover{color:var(--cyan);border-color:rgba(34,230,255,.7);
  transform:rotate(180deg)}
.fc-reload svg{width:18px;height:18px;stroke:currentColor;fill:none;
  stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}
.fc-hint{display:block;margin-top:6px;font-size:12px;line-height:1.35;
  color:var(--ink-dim);letter-spacing:.01em}
.fc-hint:empty{display:none}

/* ---------- капча-картинка: плитки и «найдите цифры» ----------

   Владелец (02.10): «сделать две отдельные капчи свои немного сложнее —
   ответы в виде картинок выбрать или найти цифры на картинках».

   Оформление ОДНО на все три места (форма заявки, окошко звонка, заказ):
   разметка у них разная, а задание приходит одинаково, и разойтись в
   трёх копиях оно не должно.

   `[hidden]` перебиваем своим `display` НЕ случайно: у плиток и картинки
   стоит `display:grid`/`block`, а он сильнее браузерного «спрятать» —
   без правил ниже скрытое задание всё равно рисовалось бы. */
.fc-pick[hidden],.fc-img[hidden]{display:none}
.fc-pick{
  display:grid;grid-template-columns:repeat(3,1fr);gap:8px;
  margin:0 0 10px;padding:10px;border-radius:13px;
  background:rgba(6,10,18,.55);border:1px solid var(--line);
  transition:border-color .35s,box-shadow .35s;
}
.fc-pick.bad{border-color:rgba(255,79,216,.75);
  box-shadow:0 0 0 3px rgba(255,79,216,.16)}
.fc-tile{
  position:relative;
  padding:0;border-radius:10px;cursor:pointer;overflow:hidden;
  border:2px solid transparent;background:#080d17;line-height:0;
  transition:border-color .25s,transform .25s var(--ease),box-shadow .25s;
}
.fc-tile img{display:block;width:100%;height:auto}
.fc-tile:hover{transform:translateY(-2px);
  box-shadow:0 6px 18px rgba(0,0,0,.45)}
.fc-tile[aria-pressed="true"]{
  border-color:var(--cyan);
  box-shadow:0 0 0 3px rgba(34,230,255,.18),0 0 22px rgba(34,230,255,.35);
}
.fc-tile:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
/* Отметка выбранного видна и без цвета: у части людей цветное зрение
   устроено иначе, а «какие картинки я уже нажал» — это не украшение.
   Галочка живёт в углу и появляется только у выбранной плитки. */
.fc-tile::after{
  content:'✓';position:absolute;top:4px;right:4px;
  width:20px;height:20px;border-radius:50%;
  display:grid;place-items:center;font-size:13px;font-weight:700;
  line-height:1;color:#04121a;background:var(--cyan);
  opacity:0;transform:scale(.6);
  transition:opacity .2s,transform .25s var(--ease);
}
.fc-tile[aria-pressed="true"]::after{opacity:1;transform:none}
.fc-img{
  display:block;margin:0 0 10px;border-radius:13px;
  border:1px solid var(--line);background:#080d17;
  /* Ширину не растягиваем: картинка нарисована точками, и растянутая она
     расплывётся — цифры читаются хуже всего именно в мыле. */
  max-width:100%;height:auto;
}
/* В окошке звонка поля шире, чем в форме: подгоняем поля плиток под ту же
   сетку, чтобы капча не выглядела чужой. */
.cb-captcha .fc-pick{margin-top:8px}
.cb-captcha .fc-img{margin-top:8px}

/* iOS зумит страницу, когда фокус попадает в поле со шрифтом меньше 16px.
   То же правило, что и у остальных полей формы. */
@media (hover:none),(max-width:820px){
  .fc-row input{font-size:16px}
}

.cb-send{
  width:100%;margin-top:6px;padding:14px;border-radius:var(--r-sm);
  border:1px solid rgba(34,230,255,.55);cursor:pointer;
  background:linear-gradient(135deg,rgba(34,230,255,.16),rgba(139,92,255,.16));
  color:var(--ink);font:inherit;font-size:15px;font-weight:700;
  transition:transform .3s var(--ease),box-shadow .3s,border-color .3s;
}
.cb-send:hover{transform:translateY(-2px);border-color:rgba(34,230,255,.9);
  box-shadow:0 0 30px rgba(34,230,255,.32)}
.cb-send:disabled{opacity:.6;cursor:default;transform:none;box-shadow:none}
.cb-note{margin:12px 0 0;font-size:13.5px;line-height:1.5;color:var(--ink-2);min-height:1px}
.cb-note.ok{color:#8effc4}
.cb-note.err{color:#ff9ce4}
@media (prefers-reduced-motion:reduce){
  .cb,.cb-back,.cb-close,.cb-send{transition-duration:.01ms !important}
  .cb{transform:none}
}

/* ---------- окно с политикой конфиденциальности ----------
   Та же рамка, что у окна звонка (.cb-wrap/.cb), но содержимое — длинный
   документ. Поэтому у окна ограничение высоты, а у тела своя прокрутка:
   без этого документ растянул бы окно выше экрана, и до кнопки закрытия
   было бы не добраться. */
.pv{max-width:760px;max-height:min(88vh,900px);display:flex;flex-direction:column;padding:26px 26px 16px}
.pv .cb-title{padding-right:38px;margin-bottom:2px}
.pv-body{
  overflow:auto;overscroll-behavior:contain;
  margin:10px -8px 0;padding:0 8px;
  font-size:14.5px;line-height:1.62;color:var(--ink-2);
}
.pv-body h4{margin:20px 0 8px;font-size:15.5px;font-weight:700;color:var(--ink);letter-spacing:-.01em}
.pv-body h4:first-child{margin-top:0}
.pv-body p{margin:0 0 12px}
.pv-body p:last-child{margin-bottom:0}

/* ---------- active nav link ---------- */
.nav-links a.active{color:#fff;background:rgba(34,230,255,.09)}
.nav-links a.active::after{width:52%}

@media (max-width:1024px){
  .bento{grid-template-columns:repeat(2,1fr)}
}
@media (max-width:560px){
  .bento{grid-template-columns:1fr}
  .bento .b.b2{grid-column:span 1}
  .bento .b{min-height:0}
}

/* ---------- responsive ---------- */
@media (max-width:1024px){
  .hero-in{grid-template-columns:1fr;gap:40px}
  .hero-visual{height:340px}
  /* Здесь первый экран снова во всё окно: в одну колонку содержимое выше,
     и полоса под шапкой уже 27px — урезанные 72px увели бы его под шапку. */
  .hero{min-height:100vh}
  #stage{
    -webkit-mask-image:linear-gradient(180deg,transparent 0%,rgba(0,0,0,.4) 40%,#000 60%);
    mask-image:linear-gradient(180deg,transparent 0%,rgba(0,0,0,.4) 40%,#000 60%);
  }
  #stage.on{opacity:.55}
  .contact{grid-template-columns:1fr}
}
/* ТЕСНАЯ ШАПКА: 891–1120 — жмём отступы и кегль, но ничего не прячем.

   Шапка — ОДНА строка: логотип, шесть пунктов меню, корзина и кнопка. Её
   собственная ширина не зависит от окна: замер `_probe-nav-head.js` (после
   того, как пунктам запретили сжиматься — `.nav-links > a{flex:none}`) даёт
   1019 точек при полосе `92vw`. Значит, без ужимания шапка перестаёт
   помещаться уже с ~1110 точек, а ниже «Рассчитать заказ» уезжала за правый
   край окна и срезалась.

   Владелец (07.10) обвёл шапку на ширине ~920 и попросил: «надо все
   исправить и сделать как в обычном режиме окна чтобы все было ровно и в
   один ряд». Поэтому здесь ничего не прячем и не переносим — уменьшаем
   поля и кегль, пока всё не встанет в одну строку. Полоса при этом шире
   обычной (96vw вместо 92vw): те же 4% ширины отданы меню.

   Замеры после правки (`_probe-nav-head.js`, полоса / что просит шапка /
   правый край группы / зазор между логотипом и первым пунктом):
     891 → 855 / 855 / 873 / 1    920 → 883 / 883 / 902 / 15
     1024 → 983 / 983 / 1004 / 65 1120 → 1075 / 1075 / 1098 / 111
     1121 → 1031 / 1031 / 1076 / 6 (ужимание выключено)
     1440 → 1200 / 1019 / 1320 / 91
   Нигде не налезает и нигде не выходит за окно; ниже 891 приходит бургер. */
@media (min-width:891px) and (max-width:1120px){
  .nav-in{width:min(var(--maxw),96vw)}
  .logo{font-size:1rem}
  .nav-links{gap:2px}
  .nav-links a{padding:8px 6px;font-size:.8rem}
  .nav-acts{gap:8px}
}

/* Бургер — с 890 точек и ниже.

   Было 820, и полоса 821–890 оставалась «мёртвой зоной»: пункты в ней уже не
   помещались, а бургер ещё не приходил — владелец видел ровно это. Теперь
   граница одна: пока шапка помещается, она в одну строку; как только
   перестаёт — приходит бургер, и в полосе остаются логотип, корзина и он. */
@media (max-width:890px){
  .nav-links{
    position:fixed;inset:76px 0 auto;flex-direction:column;align-items:stretch;gap:0;
    background:rgba(5,8,15,.97);backdrop-filter:blur(20px);
    padding:14px 4vw 26px;border-bottom:1px solid var(--line);
    transform:translateY(-130%);transition:transform .5s var(--ease);
  }
  .nav-links.open{transform:none}
  .nav-links a{padding:15px 12px;font-size:1rem;border-radius:12px}
  .burger{display:grid}
  .nav .btn-primary{display:none}
  th,td{padding:14px 18px;font-size:.87rem}
  .price-note{padding:18px}
  .step{grid-template-columns:46px 1fr;gap:18px}
  .step .dot{width:46px;height:46px;font-size:.82rem}
  .steps::before{left:22px}
}

/* Пункт меню, оформленный кнопкой (панель → «Шапка сайта» → «Вид»).
   Класс свой, а не .btn-primary: на телефоне .nav .btn-primary скрыт, и
   добавленная владельцем кнопка исчезала бы с телефонов. Стоит после
   медиазапроса намеренно: правило для телефона (.nav-links a) лежит выше
   по файлу, и без этого кнопка на телефоне теряла бы свои отступы. */
.nav-links a.nav-btn{
  padding:11px 20px;border-radius:100px;
  font-size:.86rem;font-weight:700;letter-spacing:.01em;color:#02121a;
  background:linear-gradient(115deg,var(--cyan),#6ee7ff 40%,var(--violet));
  box-shadow:0 8px 30px rgba(34,230,255,.28);
  transition:transform .35s var(--ease),box-shadow .35s var(--ease),color .3s;
}
/* Подчёркивание под обычным пунктом кнопке не нужно. */
.nav-links a.nav-btn::after{display:none}
.nav-links a.nav-btn:hover{
  color:#02121a;
  background:linear-gradient(115deg,var(--cyan),#6ee7ff 40%,var(--violet));
  transform:translateY(-2px);
  box-shadow:0 12px 36px rgba(34,230,255,.45);
}
/* Активный раздел у кнопки: фон не перекрашиваем — он и так цветной,
   иначе текст стал бы белым на светлом. */
.nav-links a.nav-btn.active{
  color:#02121a;
  background:linear-gradient(115deg,var(--cyan),#6ee7ff 40%,var(--violet));
}
/* Граница та же, что у бургера (890): пункт-кнопка живёт в том же меню, и
   разъехавшиеся границы дали бы на 821–890 кнопку с «телефонными» отступами
   в обычной строке шапки. */
@media (max-width:890px){
  .nav-links a.nav-btn{margin-top:8px;text-align:center;justify-content:center;font-size:1rem}
}

/* ============================================================
   Шапка: значок и фото у пункта, подменю, цвета и движение

   Владелец (02.10): «в админ панели я могу в шапке это редактировать и
   изменять и добавлять свои надписи и ссылки и так далее и цвета менять
   и эффекты разные много и кнопки туда добавлять с иконками и фотками
   маленькие рядом с названием». Он же показал образец: выпадающее
   подменю в шапке.

   ЗНАЧОК И ФОТО. Размер значка — 17 точек: при кегле пункта 0.9rem
   (около 14) он читается как знак перед словом, а не как картинка в
   строке. Фото ограничено и по высоте, и по ширине: у горизонтального
   снимка без max-width он растянул бы пункт на пол-экрана, а у
   вертикального — на всю высоту шапки. Оба получают скругление: иначе
   фото выглядит наклейкой поверх меню.

   Цвет приходит атрибутом data-color и раскладывается в ДВЕ переменные —
   цвет и свечение. Пункт без атрибута не трогается вовсе: он остаётся
   ровно таким, каким был до этой правки.

   Движение — атрибут data-fx. Часть видов срабатывает под курсором
   (подъём, наклон, блик), часть идёт сама (пульс, парение, неон): иначе
   «эффект» у пункта, на который не навели, не виден никому. */
.nav-links .nav-ico{
  width:17px;height:17px;flex:none;margin-right:7px;
  vertical-align:-3px;
}
.nav-links .nav-ico-photo{
  /* display и vertical-align здесь НЕ украшение, а починка. В оформлении
     есть общее правило `img{max-width:100%;display:block}` (см. начало
     файла) — оно и делало снимок в пункте меню БЛОЧНЫМ. Блок занимает
     строку целиком, поэтому подпись «Услуги» уезжала ПОД снимок, а не
     вставала с ним рядом: владелец показал это стрелкой (05.10). Строка
     «в один ряд» — это ровно inline-block плюс выравнивание по середине
     подписи; правило снимка ниже правила значка, поэтому middle
     перекрывает унаследованные -3px. */
  display:inline-block;vertical-align:middle;
  width:22px;height:22px;max-width:22px;max-height:22px;
  object-fit:cover;border-radius:6px;
}
.nav-links .nav-item{position:relative;display:flex;align-items:center}
.nav-links .nav-caret{width:13px;height:13px;margin-left:5px;opacity:.7;flex:none}
.nav-links .nav-item:hover .nav-caret{transform:rotate(180deg);transition:transform .25s var(--ease)}
/* Полоса выпадающего подменю. Лежит поверх содержимого, у самого низа
   пункта. Прозрачность и visibility переключаются вместе — так же, как у
   прочих всплывающих слоёв сайта: слой, оставшийся «видимым» на нулевой
   прозрачности, ловил бы нажатия по странице. */
.nav-links .nav-sub{
  /* Вид окна и появление списка — настройки владельца (панель → «Шапка
     сайта», поля «Окно подменю» и «Появление списка»). Здесь только
     УМОЛЧАНИЯ, и они совпадают с прежним видом: пункт, у которого эти
     настройки не трогали, выглядит ровно как раньше.

     Движение собрано из ДВУХ частей, и это важно. translateX(-50%) центрует
     полосу по пункту и нужен при любом появлении; --sub-t0 и --sub-t1 — это
     «до» и «после» выбранного эффекта. Пока всё лежало одним готовым
     transform, эффект со своей горизонталью («выезжает вбок») терял бы
     центровку, и список вставал бы не под пунктом. */
  --sub-t0:translateY(-6px);--sub-t1:translateY(0);
  --sub-bg:rgba(9,14,25,.97);--sub-bd:rgba(140,175,225,.22);--sub-bw:1px;
  --sub-r:14px;--sub-sh:0 24px 60px rgba(0,0,0,.6);--sub-blur:14px;
  --sub-pad:8px;
  position:absolute;top:calc(100% + 10px);left:50%;
  transform:translateX(-50%) var(--sub-t0);
  transform-origin:top center;
  min-width:190px;padding:var(--sub-pad);
  display:flex;flex-direction:column;gap:2px;
  border:var(--sub-bw) solid var(--sub-bd);border-radius:var(--sub-r);
  background:var(--sub-bg);
  -webkit-backdrop-filter:blur(var(--sub-blur));backdrop-filter:blur(var(--sub-blur));
  box-shadow:var(--sub-sh);
  opacity:0;visibility:hidden;pointer-events:none;
  transition:opacity .2s var(--ease),transform .2s var(--ease),visibility 0s linear .2s;
  z-index:5;
}
.nav-links .nav-item:hover .nav-sub,
.nav-links .nav-item:focus-within .nav-sub,
.nav-links .nav-item.open .nav-sub{
  opacity:1;visibility:visible;pointer-events:auto;
  transform:translateX(-50%) var(--sub-t1);
  transition:opacity .2s var(--ease),transform .2s var(--ease),visibility 0s;
}
/* ---- шесть окон подменю (панель → «Окно подменю») ----

   Окно меняет только краски и форму. Положение, размер и появление остаются
   общими — поэтому выбор окна не может сдвинуть список, потерять центровку
   или сломать показ: он и не трогает то, за что отвечают другие правила. */
.nav-links .nav-item[data-sub-win="panel"] .nav-sub{
  --sub-bg:#0d1424;--sub-bd:rgba(140,175,225,.38);--sub-r:10px;
  --sub-sh:0 18px 44px rgba(0,0,0,.72);--sub-blur:0px;
}
.nav-links .nav-item[data-sub-win="soft"] .nav-sub{
  --sub-bg:#141d33;--sub-bd:rgba(140,175,225,.14);--sub-r:22px;
  --sub-sh:0 30px 70px rgba(0,0,0,.55);--sub-pad:11px;--sub-blur:8px;
}
.nav-links .nav-item[data-sub-win="outline"] .nav-sub{
  --sub-bg:rgba(8,12,22,.55);--sub-bd:rgba(34,230,255,.55);--sub-bw:1.5px;
  --sub-r:12px;--sub-sh:0 0 0 1px rgba(34,230,255,.14),0 22px 54px rgba(0,0,0,.6);
}
.nav-links .nav-item[data-sub-win="neon"] .nav-sub{
  --sub-bg:rgba(6,10,20,.97);--sub-bd:rgba(34,230,255,.7);--sub-bw:1.5px;
  --sub-r:12px;
  --sub-sh:0 0 18px rgba(34,230,255,.35),0 0 46px rgba(167,139,250,.28),0 22px 54px rgba(0,0,0,.66);
}
.nav-links .nav-item[data-sub-win="sheet"] .nav-sub{
  --sub-bg:#0b1220;--sub-bd:rgba(140,175,225,.16);--sub-r:5px;
  --sub-sh:0 26px 60px rgba(0,0,0,.7);--sub-blur:0px;
  border-top:3px solid var(--violet);
}

/* ---- шесть появлений списка (панель → «Появление списка») ----

   Меняют только «до» и «после»: --sub-t0 — откуда список приходит,
   --sub-t1 — где встаёт. Центровку по пункту (translateX(-50%)) не трогает
   ни один: она в общем правиле выше. */
.nav-links .nav-item[data-sub-fx="settle"] .nav-sub{--sub-t0:translateY(-6px);--sub-t1:translateY(0)}
.nav-links .nav-item[data-sub-fx="rise"]   .nav-sub{--sub-t0:translateY(12px);--sub-t1:translateY(0)}
.nav-links .nav-item[data-sub-fx="drop"]   .nav-sub{--sub-t0:translateY(-18px);--sub-t1:translateY(0)}
.nav-links .nav-item[data-sub-fx="zoom"]   .nav-sub{--sub-t0:scale(.9);--sub-t1:scale(1)}
.nav-links .nav-item[data-sub-fx="flip"]   .nav-sub{--sub-t0:perspective(700px) rotateX(-22deg);--sub-t1:perspective(700px) rotateX(0)}
.nav-links .nav-item[data-sub-fx="slide"]  .nav-sub{--sub-t0:translateX(-16px);--sub-t1:translateX(0)}
/* Стрелка-мостик: между пунктом и полосой остаётся зазор в 10 точек, и
   без мостика курсор, сойдя с пункта, закрывал бы подменю. */
.nav-links .nav-item:hover::after{
  content:'';position:absolute;left:0;right:0;top:100%;height:12px;
}
.nav-links .nav-sub a,
.nav-links .nav-sub span{
  padding:9px 12px;border-radius:9px;font-size:.86rem;
  text-align:left;white-space:nowrap;
}
.nav-links .nav-sub a::after{display:none}
.nav-links .nav-sub span{color:var(--ink-3);cursor:default}

/* ---- цвета пунктов ----

   Атрибут стоит на САМОМ пункте (a/span/button), а не на обёртке: обёртка
   есть только у пункта с подменю, а цвет выбирается у любого. Поэтому и
   селекторы — по атрибуту на ссылке, а не по обёртке. */
.nav-links [data-color="cyan"]   {--nv-c:#22e6ff;--nv-g:rgba(34,230,255,.45)}
.nav-links [data-color="violet"] {--nv-c:#a78bfa;--nv-g:rgba(167,139,250,.45)}
.nav-links [data-color="magenta"]{--nv-c:#f472b6;--nv-g:rgba(244,114,182,.45)}
.nav-links [data-color="gold"]   {--nv-c:#fbbf24;--nv-g:rgba(251,191,36,.45)}
.nav-links [data-color="emerald"]{--nv-c:#34d399;--nv-g:rgba(52,211,153,.45)}
.nav-links [data-color="ice"]    {--nv-c:#bae6fd;--nv-g:rgba(186,230,253,.45)}
.nav-links [data-color="sunset"] {--nv-c:#fb923c;--nv-g:rgba(251,146,60,.45)}
.nav-links a[data-color],.nav-links span[data-color],.nav-links button[data-color]{
  color:var(--nv-c);
}
.nav-links a[data-color]:hover{color:var(--nv-c);text-shadow:0 0 12px var(--nv-g)}
.nav-links a[data-color]::after{background:var(--nv-c)}

/* ---- движение пунктов ----

   Часть видов работает под курсором, часть — постоянно. Постоянные
   объявлены с animation, и у всех есть общий стоп при
   prefers-reduced-motion (ниже): движение без спроса — это ровно то, от
   чего человек с чувствительностью к анимации уходит с сайта. */
.nav-links a[data-fx],.nav-links span[data-fx],.nav-links button[data-fx]{
  transition:transform .3s var(--ease),color .3s,background .3s,box-shadow .3s,text-shadow .3s;
}
.nav-links [data-fx="rise"]:hover{transform:translateY(-3px)}
.nav-links [data-fx="zoom"]:hover{transform:scale(1.07)}
.nav-links [data-fx="slide"]:hover{transform:translateX(4px)}
.nav-links [data-fx="tilt"]:hover{transform:rotate(-4deg)}
.nav-links [data-fx="bounce"]:hover{animation:nvBounce .55s var(--ease)}
.nav-links [data-fx="glow"]{text-shadow:0 0 14px var(--nv-g,rgba(34,230,255,.45))}
.nav-links [data-fx="glow"]:hover{box-shadow:0 0 22px var(--nv-g,rgba(34,230,255,.45))}
.nav-links [data-fx="shine"]{position:relative;overflow:hidden}
.nav-links [data-fx="shine"]::before{
  content:'';position:absolute;top:0;left:-70%;width:45%;height:100%;
  background:linear-gradient(100deg,transparent,rgba(255,255,255,.34),transparent);
  transform:skewX(-18deg);pointer-events:none;
}
.nav-links [data-fx="shine"]:hover::before{animation:nvShine .8s var(--ease)}
.nav-links [data-fx="sweep"]{position:relative;overflow:hidden}
.nav-links [data-fx="sweep"]::before{
  content:'';position:absolute;left:0;right:0;bottom:0;height:2px;
  background:var(--nv-c,#22e6ff);transform:scaleX(0);transform-origin:left;
  transition:transform .35s var(--ease);pointer-events:none;
}
.nav-links [data-fx="sweep"]:hover::before{transform:scaleX(1)}
.nav-links [data-fx="underline"]::after{width:0;transition:width .32s var(--ease)}
.nav-links [data-fx="underline"]:hover::after{width:100%}
.nav-links [data-fx="pulse"]{animation:nvPulse 2.4s ease-in-out infinite}
.nav-links [data-fx="float"]{animation:nvFloat 3.2s ease-in-out infinite}
.nav-links [data-fx="wobble"]{animation:nvWobble 2.8s ease-in-out infinite}
.nav-links [data-fx="neon"]{animation:nvNeon 2.2s ease-in-out infinite}
@keyframes nvBounce{0%{transform:translateY(0)}35%{transform:translateY(-7px)}70%{transform:translateY(2px)}100%{transform:translateY(0)}}
@keyframes nvShine{from{left:-70%}to{left:120%}}
@keyframes nvPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.05)}}
@keyframes nvFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}
@keyframes nvWobble{0%,100%{transform:rotate(0)}25%{transform:rotate(-2.5deg)}75%{transform:rotate(2.5deg)}}
@keyframes nvNeon{
  0%,100%{text-shadow:0 0 6px var(--nv-g,rgba(34,230,255,.3))}
  50%{text-shadow:0 0 18px var(--nv-c,#22e6ff),0 0 32px var(--nv-g,rgba(34,230,255,.45))}
}
@media (prefers-reduced-motion:reduce){
  .nav-links [data-fx]{animation:none}
  .nav-links [data-fx]:hover{transform:none}
  /* Появление списка — тоже движение. При «меньше движения» список просто
     проявляется: и подъём, и разворот, и увеличение выключены.

     Селектор нарочно с атрибутом, а не короткий `.nav-links .nav-sub`:
     правила появлений выше заданы через `.nav-item[data-sub-fx="…"]` и
     весят больше, поэтому короткий селектор их бы НЕ перебил и список
     продолжал бы разворачиваться — на вид «настройка не работает». */
  .nav-links .nav-item[data-sub-fx] .nav-sub{--sub-t0:translateY(0);--sub-t1:translateY(0)}
}
/* На телефоне подменю раскрывается нажатием и лежит В ПОТОКЕ: выпадающая
   полоса поверх списка меню перекрыла бы соседние пункты, а места под неё
   в вертикальном меню достаточно.

   Граница — 890, как у бургера. Раньше здесь стояло 980, и в полосе 891–980
   выходила смесь: меню ещё горизонтальное (бургер с 890), а окно подменю уже
   «телефонное» — лежит в потоке и растягивает пункт прямо посреди строки.
   Двух разных границ у одного и того же меню быть не должно. */
@media (max-width:890px){
  .nav-links .nav-sub{
    position:static;transform:none;opacity:1;visibility:visible;pointer-events:auto;
    display:none;min-width:0;margin:2px 0 6px 10px;box-shadow:none;
  }
  .nav-links .nav-item{display:block}
  .nav-links .nav-item.open .nav-sub{display:flex}
  /* `transform:none` ВЫШЕ не побеждает — и это не мелочь.

     Правило открытия подменю (`.nav-links .nav-item.open .nav-sub` — четыре
     класса) весит больше, чем `.nav-links .nav-sub` (два), а на разной силе
     порядок в файле не решает ничего. Поэтому у ОТКРЫТОГО окна оставался
     `translateX(-50%)` — центровка выпадающей полосы, — и окно уезжало влево
     на половину своей ширины.

     Владелец (07.10) показал это снимком: «при нажатии на Услуги они
     пропадают извиду и ни чего не открываеться», а на следующем снимке —
     окно, в котором пунктов не видно. Замер `_probe-nav-touch.js` (экран
     430px): окно 386px шириной стояло с x=-166 при полосе x=0, а пункты — с
     x=-157. Текст в пунктах выключен по левому краю, то есть целиком остался
     за краем экрана: человек видит пустую рамку.

     Пишем ТОТ ЖЕ селектор, что у правила открытия: тогда побеждает позднее
     — это, и окно встаёт в поток, как и задумано на телефоне. Заодно
     закрываем `:hover` и `:focus-within`: на телефоне состояние наведения
     после касания остаётся, и окно уезжало бы и без класса `open`. */
  .nav-links .nav-item:hover .nav-sub,
  .nav-links .nav-item:focus-within .nav-sub,
  .nav-links .nav-item.open .nav-sub{transform:none}
  .nav-links .nav-item:hover::after{display:none}
  /* На телефоне постоянное движение пунктов выключаем: меню там
     вертикальное и раскрывается поверх страницы — прыгающие строки в нём
     только мешают попасть пальцем. */
  .nav-links [data-fx]{animation:none}
}
@media (max-width:520px){
  .hero{padding-top:104px}
  .hero-stats{gap:22px}
  .hstat b{font-size:1.4rem}
  .form{padding:24px 20px}
  .hero-visual{height:290px}
  .float-card{padding:11px 14px}
  .float-card .v{font-size:.98rem}
  /* Первый экран на телефоне втрое ниже, и шесть плашек в нём наезжали
     бы друг на друга. Показываем первые три — столько их и было в
     исходном оформлении. Остальные остаются в панели и появляются на
     планшете и компьютере, поэтому ничего не теряется безвозвратно.
     Три видимые расставляем заново под низкую высоту и гасим
     покачивание: при 290 px карточки в крайних фазах задевали бы
     друг друга, а на телефоне меньше движения — только на пользу. */
  .float-card:nth-child(n+4){display:none}
  .fc-1{top:2%;left:0;animation:none}
  .fc-2{top:34%;right:0;animation:none}
  .fc-3{top:66%;left:2%;animation:none}
}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;
    transition-duration:.01ms !important;scroll-behavior:auto !important}
  .reveal{opacity:1;transform:none;filter:none}
}

/* ---------- скрытые разделы ----------

   Владелец может спрятать любой раздел сайта («Разделы сайта» → глаз у
   раздела в панели). Правило на ВСЕ секции сразу, а не на каждую: секций
   двенадцать, владелец может скрыть любую, и двенадцать отдельных правил
   означали бы, что однажды забудут одно.

   !important здесь не украшение: у секций есть своё оформление
   (display:flex, grid и прочее), и без него атрибут hidden проигрывал бы
   ему — раздел остался бы на месте. Раньше так было только у #blocks
   («все блоки выключены») — теперь правило общее и покрывает его. */
main > section[hidden]{display:none !important}
/* Ссылка на скрытый раздел прячется вместе с ним: вести в никуда она не
   может. Прячем и пункт списка, если ссылка лежит в нём, — пустая строка
   меню выглядела бы поломкой. */
a[hidden], li[hidden]{display:none !important}

/* ---------- blocks: рамка с логотипом и каруселью кнопок ---------- */
/* секция целиком скрывается, когда все блоки выключены в админке —
   правило общее, см. «скрытые разделы» выше */
#blocks[hidden]{display:none !important}

.lblocks{display:flex;flex-direction:column;gap:22px}

.lb{
  position:relative;padding:26px 26px 22px;border-radius:var(--r-lg);
  background:linear-gradient(160deg,rgba(18,25,42,.6),rgba(9,13,23,.42));
  border:1px solid var(--line);
  isolation:isolate;
  transition:transform .55s var(--ease),border-color .5s,box-shadow .55s var(--ease);
}
/* Рамка видна всегда, а не только при наведении: блок задуман как обрамление. */
.lb::after{
  content:'';position:absolute;inset:-1px;z-index:0;border-radius:inherit;
  padding:1px;pointer-events:none;opacity:.42;transition:opacity .5s;
  background:linear-gradient(135deg,var(--cyan),var(--violet),var(--magenta),var(--cyan));
  background-size:300% 300%;animation:flow 6s linear infinite;
  -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
  mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  mask-composite:exclude;
}
.lb:hover{transform:translateY(-5px);border-color:transparent;
  box-shadow:0 26px 60px rgba(0,0,0,.5),0 0 44px rgba(34,230,255,.15)}
.lb:hover::after{opacity:.95}

.lb-head{position:relative;z-index:1}
.lb-ph{
  display:flex;align-items:center;justify-content:center;
  width:100%;height:100%;
  font-size:.68rem;letter-spacing:.2em;text-transform:uppercase;
  color:var(--ink-3);text-align:center;
}
.lb-txt{min-width:0}
.lb-txt h3{font-size:1.16rem;font-weight:700;letter-spacing:-.01em}
.lb-txt p{margin-top:7px;color:var(--ink-2);font-size:.93rem;line-height:1.5;max-width:70ch}

/* Ряд карточек «логотип + кнопка».
   Своей рамки у ряда нет: рамку имеет только сама карточка. Иначе получалась
   рамка в рамке в рамке — блок, ряд и карточка одновременно. */
.lb-carousel{
  display:flex;align-items:center;gap:12px;
  margin-top:22px;
  position:relative;z-index:1;
}
.lb-track{
  flex:1 1 auto;min-width:0;
  display:flex;gap:14px;
  overflow-x:auto;overflow-y:hidden;
  scroll-behavior:smooth;scroll-snap-type:x proximity;
  padding:4px 2px;
  scrollbar-width:none;-ms-overflow-style:none;
}
.lb-track::-webkit-scrollbar{display:none}

/* Карточка: логотип сверху, кнопка снизу, всё в одной рамке.
   Целиком является ссылкой — поэтому кликается и логотип, и кнопка. */
.lb-item{
  flex:0 0 calc((100% - 56px) / 5);   /* ровно 5 карточек в ряд */
  scroll-snap-align:start;
  display:flex;flex-direction:column;gap:10px;
  /* В ряду карточки выравниваются по самой высокой, поэтому у карточки с
     маленьким логотипом снизу остаётся пустота. Центрируем содержимое —
     иначе такая карточка выглядит прижатой к верхнему краю. При одинаковых
     размерах (обычный случай) это ничего не меняет. */
  justify-content:center;
  padding:10px;
  border-radius:var(--r-md);
  border:1px solid var(--line);
  background:rgba(10,15,26,.6);
  text-decoration:none;
  transition:border-color .3s,background .3s,box-shadow .3s,transform .3s var(--ease);
}
/* Наведение на карточку: рамка заметно голубеет, карточка приподнимается.
   --line-strong (.28) здесь была почти неотличима от --line (.14), поэтому
   рамка идёт отдельным значением, а не переменной. */
.lb-item:hover,.lb-item:focus-visible{
  border-color:rgba(34,230,255,.42);
  background:rgba(34,230,255,.09);
  transform:translateY(-3px);
  box-shadow:0 10px 26px rgba(0,0,0,.42),0 0 26px rgba(34,230,255,.2);
}
.lb-item:focus-visible{outline:2px solid var(--cyan);outline-offset:3px}

.lb-item-logo{
  display:flex;align-items:center;justify-content:center;
  /* Высоту логотипа задаёт админка через --logo-h на карточке.
     88px — значение по умолчанию, если размер не задавали. */
  height:var(--logo-h,88px);padding:8px;
  border-radius:10px;
  background:rgba(8,12,22,.6);
  border:1px solid var(--line);
  transition:border-color .3s;
}
.lb-item-logo img{max-width:100%;max-height:100%;object-fit:contain;display:block}
.lb-item-logo.is-empty{border-style:dashed}
.lb-item:hover .lb-item-logo{border-color:var(--line-strong)}

.lb-item-btn{
  display:flex;align-items:center;justify-content:center;text-align:center;
  min-height:42px;padding:8px 10px;
  border-radius:10px;
  border:1px solid var(--line);
  background:rgba(34,230,255,.06);
  color:var(--ink-2);
  font-size:.85rem;font-weight:600;line-height:1.25;
  overflow-wrap:break-word;hyphens:auto;
  transition:color .25s,border-color .25s,background .25s,box-shadow .25s,transform .18s;
}
/* Наведение и фокус с клавиатуры подсвечивают кнопку одинаково:
   иначе мышью отклик виден, а при переходе табом — нет.

   Заливка становится заметно плотнее, рамка — цветной, вокруг появляется
   мягкое свечение, кнопка чуть приподнимается. Раньше здесь менялась только
   альфа заливки, с .06 на .14: на тёмном фоне это почти неразличимо, и
   наведение читалось только по побелевшему тексту — казалось, что его нет. */
.lb-item:hover .lb-item-btn,
.lb-item:focus-visible .lb-item-btn{
  color:#fff;
  border-color:rgba(34,230,255,.5);
  background:rgba(34,230,255,.22);
  box-shadow:0 0 18px rgba(34,230,255,.28),0 4px 12px rgba(0,0,0,.32);
  transform:translateY(-2px);
}

/* Нажатие. Карточка «вдавливается» на место, кнопка заливается цианом —
   видно, что нажатие дошло. Действует только на карточку-ссылку: у карточки
   без адреса нажимать нечего, и подсвечивать там нечего тоже. */
a.lb-item:active{
  transform:translateY(0) scale(.985);
  border-color:var(--cyan);
  background:rgba(34,230,255,.1);
  box-shadow:0 4px 14px rgba(0,0,0,.42),0 0 26px rgba(34,230,255,.2);
  /* короткий переход только на время нажатия: отклик должен быть мгновенным,
     а отпускание возвращается к плавным .3s из базового правила */
  transition-duration:.09s;
}
a.lb-item:active .lb-item-btn{
  color:#04121a;
  border-color:var(--cyan);
  background:var(--cyan);
  box-shadow:0 0 22px rgba(34,230,255,.45),inset 0 2px 6px rgba(0,0,0,.28);
  transform:translateY(1px);
  /* заливка должна появляться мгновенно, а не догонять нажатие */
  transition-duration:.09s;
}

/* Старые плоские кнопки: остались для запасного варианта разметки. */
.lb-btn{
  flex:0 0 calc((100% - 40px) / 5);
  scroll-snap-align:start;
  display:flex;align-items:center;justify-content:center;text-align:center;
  min-height:46px;padding:10px 12px;
  border-radius:12px;border:1px solid var(--line);
  background:rgba(10,15,26,.6);color:var(--ink-2);
  font-size:.86rem;font-weight:600;text-decoration:none;line-height:1.25;
  overflow-wrap:break-word;hyphens:auto;
  transition:color .3s,border-color .3s,background .3s,box-shadow .3s,transform .3s var(--ease);
}
.lb-btn:hover{
  color:#fff;border-color:var(--line-strong);
  background:rgba(34,230,255,.09);transform:translateY(-2px);
  box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 20px rgba(34,230,255,.16);
}

.lb-arrow{
  flex:0 0 auto;width:40px;height:40px;
  display:flex;align-items:center;justify-content:center;
  border-radius:50%;border:1px solid var(--line);
  background:rgba(10,15,26,.7);color:var(--ink-2);cursor:pointer;
  transition:color .3s,border-color .3s,background .3s,box-shadow .3s,opacity .3s;
}
.lb-arrow svg{width:18px;height:18px}
.lb-arrow:hover{color:#fff;border-color:var(--line-strong);
  background:rgba(34,230,255,.1);box-shadow:0 0 18px rgba(34,230,255,.2)}
.lb-arrow[disabled]{opacity:.25;cursor:default;pointer-events:none}
/* кнопки помещаются целиком — стрелки не нужны */
.lb-carousel.fits .lb-arrow{display:none}

@media (max-width:1100px){
  .lb-item,.lb-btn{flex-basis:calc((100% - 42px) / 4)}
}
@media (max-width:820px){
  .lb-item,.lb-btn{flex-basis:calc((100% - 28px) / 3)}
}
@media (max-width:560px){
  .lb{padding:20px 18px 18px}
  .lb-txt p{font-size:.88rem}
  .lb-carousel{gap:8px;margin-top:16px}
  .lb-track{gap:12px}
  .lb-item,.lb-btn{flex-basis:calc((100% - 12px) / 2);font-size:.8rem}
  .lb-item{padding:8px;gap:8px}
  /* размер из админки сохраняем, но на узком экране чуть уменьшаем:
     без множителя заданные 200px съели бы весь экран */
  .lb-item-logo{height:calc(var(--logo-h,88px) * .86)}
  .lb-item-btn{min-height:38px;font-size:.79rem}
  .lb-btn{min-height:44px}
  .lb-arrow{width:34px;height:34px}
  .lb-arrow svg{width:16px;height:16px}
}

/* ============================================================
   Магазин: карусель товаров
   ------------------------------------------------------------
   Раскладка «обложкой»: карточки стоят в одной точке и расходятся
   в стороны поворотом и уменьшением. Так делается без библиотек и без
   пересчёта ширин: положение каждой карточки задаётся переменными
   --tx/--tz/--ry/--sc, а переход между положениями рисует сам браузер
   (transition на transform). Скрипт только меняет числа.

   Почему не «лента со сдвигом»: при сдвиге пришлось бы считать ширину
   карточки и окна, и на каждой ширине экрана получалась бы своя
   арифметика с округлениями. Здесь арифметики нет вовсе — карточка
   либо в центре, либо отклонена на столько-то шагов.
   ============================================================ */
/* Класс, а не id: каруселей теперь до двадцати, и у каждой СВОЯ секция
   (см. buildShopSection в js/main.js) — иначе их нельзя было бы расставить по
   странице по одной. Настройки подписи вешаются атрибутами на саму секцию. */
.shop-sec .sec-head{margin-bottom:8px}

/* Каруселей может быть до трёх, и каждая — отдельный блок со своей
   подписью. Расстояние между блоками больше, чем внутри блока: иначе
   две карусели подряд слились бы в одну ленту, и подпись второй
   читалась бы как подпись товара. */
.shop-block{margin-top:26px}
.shop-block + .shop-block{margin-top:70px}
.shop-block-title{
  /* Нижнее поле — это «воздух» между подписью и лентой карточек, и оно не
     равно своему числу: у сцены карусели сверху лежит её собственный отступ
     под свечение (см. .car-stage ниже), который съедает часть зазора.
     Замер на живом сайте (05.10): при 6px видимый просвет до первой карточки
     был всего 10px — владелец показал на это: «надо поднять немного выше, а
     то как то близко стало». 20px даёт 24px просвета и подпись читается
     отдельно от ленты. */
  margin:0 0 20px;font-size:1.12rem;font-weight:800;letter-spacing:-.01em;
  color:var(--ink);display:flex;align-items:center;gap:10px;
}
/* Подпись карусели — цветным переходом. Оба цвета владелец задаёт в панели
   («Товары» → «Цвета товара»); пока не задал, работают цвета оформления.

   Градиент навешен на отдельный span, а не на сам заголовок: заголовок —
   флекс-контейнер во всю ширину, и фон, обрезанный по тексту, отдал бы
   подписи только начало перехода, а остальное ушло бы в пустоту справа.
   Span сжимается по тексту, поэтому переход ложится ровно на буквы.

   @supports — потому что при background-clip:text цвет букв делается
   прозрачным: там, где обрезка не поддерживается, подпись осталась бы
   невидимой. Без поддержки работает обычный цвет из правила выше. */
.shop-block-title-text{color:var(--ink)}
@supports ((-webkit-background-clip:text) or (background-clip:text)){
  .shop-block-title-text{
    background:linear-gradient(92deg,
      var(--shop-title-from,var(--ink)),var(--shop-title-to,var(--violet)));
    -webkit-background-clip:text;background-clip:text;
    -webkit-text-fill-color:transparent;color:transparent;
  }
}
/* Черта перед подписью отделяет карусель от предыдущей, не добавляя
   ещё одной рамки. Цвета те же, что у подписи: иначе черта жила бы
   своей жизнью рядом с настроенным владельцем переходом.

   Владелец выбирает в панели («Товары» → «Подпись карусели») три вещи:
   размер подписи, знак перед ней и его анимацию. Выбор приезжает
   атрибутами на саму секцию (#shop), а как это выглядит — решаем здесь.
   Так панель не может прислать «шрифт 400px»: список ступеней закрыт
   с обеих сторон, и на сайте всегда есть рабочее значение.

   Знак рисуется маской, а не картинкой: цвет берётся тот же переход,
   что у подписи, и знак обязан жить с ней в одной палитре. Маска здесь
   работает — в отличие от ленты карточек, у подписи нет 3D-контекста
   (см. комментарий у .car-body::after). */
.shop-sec{
  --shop-mark-arrow:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M3%2012h15M12%205.5%2018.5%2012%2012%2018.5'%20fill='none'%20stroke='%23000'%20stroke-width='2.4'%20stroke-linecap='round'%20stroke-linejoin='round'/%3E%3C/svg%3E");
  --shop-mark-chevron:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M8%204.5%2015.5%2012%208%2019.5'%20fill='none'%20stroke='%23000'%20stroke-width='2.8'%20stroke-linecap='round'%20stroke-linejoin='round'/%3E%3C/svg%3E");
  --shop-mark-dot:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Ccircle%20cx='12'%20cy='12'%20r='5.6'%20fill='%23000'/%3E%3C/svg%3E");
  --shop-mark-diamond:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M12%202.4%2021.6%2012%2012%2021.6%202.4%2012z'%20fill='%23000'/%3E%3C/svg%3E");
}

.shop-block-title::before{
  content:"";flex:none;width:1.25em;height:2px;border-radius:2px;
  background:linear-gradient(90deg,
    var(--shop-title-from,var(--cyan)),var(--shop-title-to,var(--violet)));
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-position:center;mask-position:center;
  -webkit-mask-size:contain;mask-size:contain;
}

/* Размер подписи. Ступень, а не число: подпись должна расти вместе с
   оформлением, а «14px» на другом экране выглядит случайно. Знак задан в
   em и растёт вместе с ней — иначе крупная подпись шла бы с мелкой чертой. */
.shop-sec[data-shop-title-size="s"]  .shop-block-title{font-size:.98rem}
.shop-sec[data-shop-title-size="m"]  .shop-block-title{font-size:1.12rem}
.shop-sec[data-shop-title-size="l"]  .shop-block-title{font-size:1.42rem}
.shop-sec[data-shop-title-size="xl"] .shop-block-title{font-size:1.78rem}

/* Знаки-рисунки: квадрат, залитый тем же переходом, но обрезанный по форме
   маски. Размер в em, чтобы знак рос вместе с подписью.

   По умолчанию (атрибута нет — сервер старее панели) остаётся черта: она
   была здесь раньше, и подпись без знака выглядела бы сломанной. Поэтому
   «квадратность» включаем только у рисованных знаков, а не снимаем у
   черты. */
.shop-sec[data-shop-title-mark="arrow"] .shop-block-title::before,
.shop-sec[data-shop-title-mark="chevron"] .shop-block-title::before,
.shop-sec[data-shop-title-mark="dot"] .shop-block-title::before,
.shop-sec[data-shop-title-mark="diamond"] .shop-block-title::before{
  width:1.1em;height:1.1em;border-radius:0;
}
.shop-sec[data-shop-title-mark="arrow"] .shop-block-title::before{
  -webkit-mask-image:var(--shop-mark-arrow);mask-image:var(--shop-mark-arrow);
}
.shop-sec[data-shop-title-mark="chevron"] .shop-block-title::before{
  -webkit-mask-image:var(--shop-mark-chevron);mask-image:var(--shop-mark-chevron);
}
.shop-sec[data-shop-title-mark="dot"] .shop-block-title::before{
  -webkit-mask-image:var(--shop-mark-dot);mask-image:var(--shop-mark-dot);
}
.shop-sec[data-shop-title-mark="diamond"] .shop-block-title::before{
  -webkit-mask-image:var(--shop-mark-diamond);mask-image:var(--shop-mark-diamond);
}
.shop-sec[data-shop-title-mark="none"] .shop-block-title::before{display:none}

/* Анимация знака. «Без анимации» — тоже выбор, а не отсутствие настройки:
   когда каруселей на странице несколько, движущиеся знаки мешают читать. */
@keyframes shop-mark-slide{
  0%,100%{transform:translateX(0)}
  50%{transform:translateX(.32em)}
}
@keyframes shop-mark-fade{
  0%,100%{opacity:1}
  50%{opacity:.12}
}
@keyframes shop-mark-pulse{
  0%,100%{transform:scale(1)}
  50%{transform:scale(1.3)}
}
.shop-sec[data-shop-title-anim="slide"] .shop-block-title::before{
  animation:shop-mark-slide 2.2s ease-in-out infinite;
}
.shop-sec[data-shop-title-anim="fade"] .shop-block-title::before{
  animation:shop-mark-fade 2.6s ease-in-out infinite;
}
.shop-sec[data-shop-title-anim="pulse"] .shop-block-title::before{
  animation:shop-mark-pulse 2s ease-in-out infinite;
}

.carousel{
  position:relative;margin-top:0;
  /* События мыши берут только дорожка и стрелки — и это не мелочь.

     У сцены внутри выросли отступы (место под свечение, см. ниже), а поля
     вернули ей прежнее место в потоке. Поэтому рамка карусели стала шире её
     видимого следа: внизу она накрывает точки и кнопку паузы, вверху —
     подпись раздела. Отступ прозрачный, но события он перехватывает как
     обычная площадка, и кнопка паузы под лентой перестала нажиматься.
     Поймал test-shop («car-stage intercepts pointer events», потом
     «.carousel intercepts pointer events»), а до набора это увидел бы
     владелец: точки и пауза под обеими лентами стали бы мёртвыми.

     События с дорожки всплывают к сцене, поэтому перелистывание свайпом
     работает как раньше: слушатель pointerdown висит на сцене и получает
     всплывшее событие. */
  pointer-events:none;
}

/* ---------- размер карточки товара ----------

   Владелец: «с карточками товара сделать в админ настройку их размера чтобы
   я мог менять размер их сам сделай 6 вариантов размера, но при этом чтобы
   они работали корректно на всех гаджетах и на всех браузерах компьютеров».

   Шесть ступеней заданы МНОЖИТЕЛЕМ к обычной ширине, а не шириной в
   пикселях. Причина — в последней части просьбы, «корректно на всех
   гаджетах». Обычная ширина и без настройки разная: 298px на широком окне,
   262px при 820px и 224px при 560px (см. медиазапросы ниже, у них меняется
   `--card-base`). Одно число в пикселях пришлось бы задавать шесть раз на
   каждое из трёх окон, и владелец, выбрав «самые крупные» на компьютере,
   получил бы на телефоне карточку шире экрана. Множитель работает на любом
   окне: ширину по-прежнему выбирает оформление по ширине окна, а ступень
   говорит только «крупнее обычной» или «мельче».

   Высота НЕ задаётся отдельно: она — та же доля ширины, что и была у каждой
   раскладки (432/298 на широком окне, 392/262 и 352/224 на узких). Поэтому
   пар две: `--card-base` с `--card-h-base`. Задай мы высоту одним числом, у
   каждой ступени была бы своя пропорция, и снимок товара то вытягивался бы,
   то сплющивался.

   Ширина меняется вместе с высотой не для красоты. Дорожка ленты равна
   высоте карточки (см. .car-track ниже): лента растёт вместе с карточкой, и
   точки листания остаются на прежнем расстоянии от неё. Пока дорожка и
   карточка носили каждый своё число (432px и 432px), они совпадали только
   потому, что совпадали два числа, — и стоило карточке вырасти, как она
   налезала на точки.

   Множители подобраны так, чтобы между соседними ступенями был заметный
   шаг (≈14%), но крайние не ломали раскладку: «самые крупные» на телефоне —
   это 323px при ширине окна 390px, то есть карточка ещё помещается целиком,
   а «самые маленькие» на широком экране — 221px, и в ряд влезает заметно
   больше товаров. */
.shop-block{
  --card-base:298px;
  --card-h-base:432px;
  /* Множитель ступени. «Обычные» — единица: это ровно то, что было до
     настройки, поэтому старый файл магазина (и файл, где поля нет вовсе)
     выглядит как раньше. */
  --card-k:1;
  --card-w:calc(var(--card-base) * var(--card-k));
  --car-h:calc(var(--card-h-base) * var(--card-k));
}
.shop-block[data-card="xs"]{--card-k:.74}
.shop-block[data-card="sm"]{--card-k:.87}
.shop-block[data-card="lg"]{--card-k:1.14}
.shop-block[data-card="xl"]{--card-k:1.28}
.shop-block[data-card="xxl"]{--card-k:1.44}

/* Перспектива задаётся здесь, а не на карточке: иначе у каждой карточки
   была бы своя точка схода, и «обложка» развалилась бы. */
.car-stage{
  perspective:1700px;
  perspective-origin:50% 44%;
  overflow:hidden;
  /* Отступы — это МЕСТО ПОД СВЕЧЕНИЕ, а не воздух вокруг ленты.

     Вспышка подсветки — это тень карточки (0 0 74px 6px), то есть свечение
     выходит за карточку почти на 80px во все стороны. Сцена обрезает
     содержимое (overflow:hidden), а край обрезки идёт по границе padding-box
     (проверено попаданием, probe-clip-rule.js), значит всё, что вылезло за
     отступ, не рисуется вовсе.

     С прежними отступами (22px сверху, 6px снизу) места было 4px и 34px, а в
     движении «поворот с подъёмом» карточка поднята ещё на 18px — и сверху
     вспышки не оставалось совсем. Замер по пикселям (probe-glow-profile.js):
     над карточкой был ровный фон (яркость 16-17), то есть обрыв приходился
     прямо на кромку. Владелец обвёл это на снимке и сказал «не полностью
     видно эффект вспышки» — и был прав.

     Теперь 120px сверху и 114px снизу: свечение помещается целиком, и вместо
     ступеньки — плавное затухание (89 → 16 на протяжении ~90px).

     Отрицательные поля возвращают ленте прежнее место в потоке: сумма
     «отступ + поле» осталась прежней (22px сверху, 6px снизу). Поэтому
     заголовок раздела, подпись карусели, точки и следующая лента стоят ровно
     там, где стояли, — раздувается только область обрезки, то есть ровно то
     место, где живёт свечение. Проверка test-card-flash сверяет и то, и
     другое: и что свечению хватает места, и что раскладка не поехала. */
  padding:120px 0 114px;
  margin-top:-98px;margin-bottom:-108px;
  /* Палец по горизонтали ведёт ленту, по вертикали — страницу. Без этого
     браузер на телефоне считает горизонтальное движение своим и лента
     «залипает». */
  touch-action:pan-y;
  /* События сцена не берёт: их берут дорожка и стрелки, а почему так —
     в правиле .carousel выше. Здесь только место под свечение. */
}
.car-track:active{cursor:grabbing}

.car-track{
  /* Высота дорожки и высота карточки — ОДНА переменная (--car-h), а не два
     совпадающих числа. Так было не всегда: в дорожке стояло 432px и в
     карточке 432px, и совпадали они ровно потому, что кто-то вписал одно
     число дважды. Как только у карточки появилась настройка размера, второе
     число разошлось бы с первым, и под карточкой встала бы пустая полоса
     (или карточка налезла бы на точки листания). */
  position:relative;height:var(--car-h);transform-style:preserve-3d;
  transition:transform .62s var(--ease);
  will-change:transform;
  /* Дорожка — то, что видно, и то, что берёт события: у сцены
     pointer-events:none (почему — там же). Курсор «рука» тоже здесь:
     у сцены он не показывался бы, потому что сцена не цель событий. */
  pointer-events:auto;
  cursor:grab;
}
/* Пока палец ведёт ленту, переход выключен: иначе лента отставала бы от
   пальца на четверть секунды. */
.car-track.is-dragging{transition:none}

/* Подсказка растрового слоя — на время хода и только на него.
   Ставится тем же атрибутом, что и пелена подписи (`markTurning` в
   js/main.js), поэтому включается до первой смены трансформации и
   выключается по концу перехода: карточка успевает проехать в слое, а
   после остановки слой отпускается и текст перерисовывается резко. */
.car-track[data-moving="1"] .car-card{will-change:transform}

/* padding:0 — не украшение, а выравнивание. Карточка — это <button>, а у
   кнопки в Chrome свои отступы (1px 6px). Из-за них фотография стояла на
   6px внутри рамки с боков и на 1px сверху: между рамкой и снимком
   оставалась тёмная полоса, а углы у снимка прямые — на скруглении 26px
   это читалось как «фотография выехала из рамки». Со снятыми отступами
   снимок упирается в рамку и обрезается по её скруглению. */
.car-card{
  position:absolute;top:0;left:50%;
  /* Высота карточки РАВНА высоте дорожки (.car-track, 432px).

     Так было не всегда: карточка была 404px, то есть на 28px ниже своей
     дорожки, и под ней в ленте оставалась пустая полоса. В покое она не
     бросалась в глаза, но у АКТИВНОЙ карточки к ней добавлялся ещё и
     подъём (-18px), и до точек листания набиралось 70px пустоты. Владелец
     обвёл это место на снимке: «карточки товара сделать немного длиннее и
     выше».

     Замер (probe-shop-card-gap.js): дорожка 432px, карточка 404px, низ
     активной карточки — за 46px до низа дорожки, а точки листания ещё на
     24px ниже. Заполняем дорожку целиком — ровно то место, которое лента
     уже отводит карточке.

     Дальше расти нельзя: у соседних карточек свой сдвиг вниз (+8px), и от
     высоты 448px они начинают задевать точки.

     Это «нельзя» снято иначе — не запретом, а тем, что дорожка считается
     ОТ ТОЙ ЖЕ переменной (--car-h), что и карточка. Карточка по-прежнему
     равна дорожке, и растут они вместе: у крупной ступени и дорожка выше,
     и точки листания отодвигаются вместе с ней. Пока в обоих местах стояло
     по своему числу, «равна» держалось на честном слове, и первая же
     настройка размера его нарушила бы. */
  width:var(--card-w);height:var(--car-h);margin-left:calc(var(--card-w) / -2);
  display:flex;flex-direction:column;overflow:hidden;cursor:pointer;
  padding:0;
  border:1px solid transparent;border-radius:var(--r-lg);
  /* Подложка — у ВСЕХ карточек, и обрезана по content-box.

     Так было не всегда: одно время подложку носили только центральная
     карточка, а задние стояли «голыми», и их снимки растворяли масками.
     Владелец увидел результат и сказал «каша какая-то» — и был прав:
     задняя карточка превращалась в мутное пятно, сквозь которое
     просвечивал сосед, а её подпись ложилась тёмным прямоугольником на
     чужое фото.

     Теперь каждая карточка — плотный скруглённый прямоугольник, как и
     было задумано. Ничего не просвечивает, наложение читается как
     укладка, а не как дефект.

     background-clip обязателен. Без него подложка рисуется по border-box,
     то есть и под прозрачной рамкой, а снимок — только по content-box.
     На кромке остаётся 1px тёмного фона поверх светлого снимка соседа —
     ровно та «видимая кромка задней карточки», на которую владелец
     жаловался трижды. */
  background:linear-gradient(168deg,rgba(17,24,41,.96),rgba(9,13,24,.96));
  background-clip:padding-box;
  -webkit-background-clip:padding-box;
  /* --ty добавлен ради настройки «Поворот с подъёмом»: она поднимает
     активную карточку над лентой, а подъём — это сдвиг по вертикали в той
     же трансформации. Ставить его отдельным transform нельзя: вторая
     трансформация затёрла бы первую, и карточка потеряла бы своё место в
     ленте. Владелец, выбравший «поворот», этой переменной не задаёт —
     работает запасной ноль. */
  transform:translate3d(var(--tx,0px),var(--ty,0px),var(--tz,0px))
            rotateY(var(--ry,0deg)) rotateX(var(--rx,0deg)) rotate(var(--rz,0deg))
            scale(var(--sc,1));
  opacity:var(--op,1);
  /* Появление карточки — БЕЗ перехода прозрачностью.
     Владелец (29.10, запись экрана 0:06–0:08): «карточка переходит — движется
     и в конце белая вспышка… надо убрать её оттуда». Вспышка — это была
     плавная прозрачность: въезжающая карточка полсекунды шла от `opacity:0`
     к 1, и сквозь неё просвечивал СВЕТЛЫЙ фон раздела. На тёмной теме такого
     не было видно, на бумаге полупрозрачная карточка выглядит выбеленной.
     Замер до правки (`_probe-b14-flash.js`, карточка справа от центра):
     op 0 → 0,15 → 0,28 → 0,4 → 0,51 → 0,6 → 0,67 … → 1 за 400 мс.
     `--op` остался: он по-прежнему ПРЯЧЕТ карточки, не поместившиеся в
     сцену, но теперь срабатывает сразу — карточка въезжает непрозрачной,
     как и должна. */
  transition:transform .62s var(--ease),
             border-color .4s,box-shadow .4s;
  /* `will-change` — ТОЛЬКО НА ВРЕМЯ ХОДА (см. правило ниже).
     Здесь стояло постоянное `will-change:transform`, и карточка навсегда
     получала свой растровый слой. Слой снимается по текущему масштабу: пока
     карточка едет и растёт, браузер рисует её в одном масштабе, а после
     остановки не всегда перерисовывает заново — текст остаётся мягким.
     Владелец (29.10) показал это на двух снимках подряд: «сразу не
     размытости, а после того как карточка встала — везде размытость».
     Подсказку слоя теперь даёт `.car-track[data-moving="1"]`: она живёт
     ровно столько, сколько длится переход, и гаснет вместе с ним — слой
     отпускается, и карточка перерисовывается в своём настоящем масштабе. */
  backface-visibility:hidden;
}
/* Объём. По кромке идёт светлая внутренняя грань, как на настоящей
   карточке из плотного картона: сверху блик, по периметру тонкая светлая
   линия. Ровно та же грань, что у карточки ленты работ, — владелец просил
   сделать товары «как в работах», и «как» здесь означает одинаковую
   кромку, а не похожую.

   Второе, что делает этот слой: затемняет ЗАДНИЕ карточки. Раньше глубина
   считалась прозрачностью самой карточки (--op падал до 0.55), и сквозь
   неё просвечивал кадр соседа. Теперь карточка непрозрачна всегда, а
   темнеет подложка: тот же шаг в глубину, но сквозь карточку ничего не
   видно. --dim считает скрипт. */
.car-card::after{
  content:'';position:absolute;inset:0;z-index:4;pointer-events:none;
  border-radius:inherit;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.13),
             inset 0 0 0 1px rgba(255,255,255,.06);
  background:rgba(12,18,32,var(--dim,0));
}
/* Дальние карточки не должны ловить нажатия: они за центральной, и клик
   по ним читался бы как клик по центру. */
.car-card[data-far="1"]{pointer-events:none}
/* Подмена состава ленты при смене категории — БЕЗ перехода.

   Владелец (10.10, снимок #3): «когда в каруселе товара выбираю категорию
   анимация товара медленная когда товар заменяеться ... видно что товар очень
   медленно подменяеться на нужный из категории нужно это исправить должен
   быстро и не заметно для человеческого зрения меняться и появляться».

   Причина была именно здесь: у карточки переход на transform 0.62s, и
   карточки, у которых сменилось место в ленте, ехали к нему на глазах. Смена
   категории — не листание: это ЗАМЕНА одного набора другим, и ехать тут
   нечему. Признак ставит скрипт на время подмены и снимает двойным кадром
   (см. applyFilter).

   Приём тот же, что у ведения пальцем (.car-track.is-dragging): гасим
   переход на время, а не переписываем числа. */
.shop-block[data-jump="1"] .car-card,
.shop-block[data-jump="1"] .car-track{transition:none}
/* Рамку и подложку носит ТОЛЬКО центральная карточка. У задних их нет
   намеренно.

   Раньше тёмный градиент лежал на всей карточке. Подложка рисуется по
   border-box, то есть и под рамкой, а фотография — только по content-box.
   Значит на самой кромке карточки оставался 1px тёмного фона, не закрытый
   снимком. Задние карточки перекрывают друг друга, и эта полоска ложилась
   ПОВЕРХ светлой фотографии соседа: на стыке двух задних карточек
   появлялась тёмная вертикальная черта, а в углу — тёмный скруглённый
   уголок. Владелец показывал это на снимке как «видно края задней
   карточки товара» — и был прав: это не край фотографии, а нарисованная
   нами линия.

   Теперь у задних карточек подложки нет вовсе, и их кромка ничем не
   обведена — на стыке видно только смену снимка, без черты. Подложку
   подписи держит .car-body, иначе название с ценой легли бы на чужое
   фото и стали нечитаемыми. */
.car-card[data-active="1"]{
  /* Рамка В ПОКОЕ — приглушённый цвет выбранной подсветки (--glow-rest), а
     не полный: под курсором рамка уходит в полный (--glow-a), и карточка
     отзывается. Подробности — у наборов --glow-* выше. */
  border-color:var(--glow-rest,rgba(34,230,255,.42));
  background:linear-gradient(168deg,rgba(17,24,41,.96),rgba(9,13,24,.96));
  box-shadow:0 30px 80px rgba(0,0,0,.55),0 0 46px var(--glow-b,rgba(34,230,255,.16));
}
/* Маски с задних карточек СНЯТЫ — и с самой карточки, и со снимка.

   Они появились, чтобы растворить кромку задней карточки в фоне. Идея
   была неверной: под задней карточкой лежит снимок соседа, маска делает
   её полупрозрачной, и на стыке двух карточек получалась мутная дымка,
   сквозь которую просвечивал чужой кадр. Владелец назвал это «каша» и
   попросил вернуть как было — «чтобы все карточки отображались чистыми
   без засветов от предыдущей карточки».

   Теперь карточка непрозрачна (подложка выше), её кромка — это её
   собственный скруглённый край, а не растворённое пятно. Наложение
   читается как укладка карточек, а не как дефект. */
/* Наведение — не только рамка, но и свечение: рамка в 1px на тёмном
   фоне читается слабо, а карточка должна отзываться сразу. */
.car-card:hover{
  border-color:var(--glow-a,rgba(34,230,255,.62));
  box-shadow:0 30px 80px rgba(0,0,0,.55),0 0 44px var(--glow-b,rgba(34,230,255,.28));
}
/* У задней карточки при наведении рамки нет — только свечение. Рамка у неё
   легла бы поверх фотографии соседа ровно той чертой, которую владелец
   просил убрать: подсветка читалась бы как дефект, а не как отклик.
   Свечение мягкое, кромку не обводит и при этом заметно. */
.car-card:not([data-active="1"]):hover{
  border-color:transparent;
  box-shadow:0 26px 64px rgba(0,0,0,.5),0 0 40px var(--glow-b,rgba(34,230,255,.24));
}

/* Лейбл — только у ЦЕНТРАЛЬНОЙ карточки. У соседних он выглядывал
   из-за неё обрывком: от «Хит продаж» оставалось «ТОПчиК», и это
   читалось как поломка вёрстки, а не как часть ленты.

   Название и цена у соседних ОСТАЮТСЯ. Скрывать их было ошибкой: место
   под подпись сохранялось (мы скрываем через visibility, а не display),
   поэтому под соседними карточками оставалась пустая тёмная полоса — и
   это выглядело хуже обрывка. Владелец попросил вернуть подпись, и он
   прав: подпись под соседней карточкой читается как часть ленты. */
.car-card:not([data-active="1"]) .car-badge{visibility:hidden}

/* Подпись соседа гаснет к тому боку, который закрыт соседней карточкой.
   Геометрия ленты постоянна — шаг 66% ширины карточки, поворот 12°,
   масштаб 0.96 на шаг, — поэтому доля закрытого бока одна и та же при
   любой ширине окна. Замер по кадру (красили карточки в чистые цвета,
   прятали текст и читали строку пикселей на уровне названия) даёт срез на
   54.5% ширины подписи и для 298px, и для 262px, и для 224px — совпадение
   до десятых. Значит подпись обрывается ровно на 54.5%, а гасить надо
   ПЕРЕД срезом: в закрытой части затухания всё равно не видно, и обрывок
   оставался жёстким. С гаснущим краем он читается как «продолжение ушло
   за карточку».

   Долю среза считает скрипт и кладёт в --cover: у второй карточки с краю
   подпись перекрывает уже не центральная, а первая, и срез у неё другой.
   Полоса затухания — половина закрытой доли: так она одинаково выглядит и
   у ближнего соседа, и у дальнего, где закрыто меньше.

   Погасить подпись маской нельзя: в ленте включён 3D-контекст (perspective
   у .car-stage и preserve-3d у .car-track), а в нём mask-image не
   отрисовывается вообще. Проверено на живом Chrome: маска молчит даже на
   центральной карточке, у которой нет ни поворота, ни наклона; снимаешь
   preserve-3d или perspective — маска тут же оживает. Отказываться от 3D
   ради подписи нельзя, поэтому гасим наложением.

   Наложение — ::after поверх подписи, залитый цветом фона карточки: низ её
   градиента даёт rgb(9,13,24), это замерено по кадру. Градиент непрерывный,
   поэтому шва не видно — цвет плавно перетекает от подписи к фону. Полная
   непрозрачность стоит ровно у среза, так что от обрывка не остаётся
   ничего. */
.car-card[data-cover] .car-body::after{
  content:"";position:absolute;inset:0;pointer-events:none;
}
.car-card[data-cover="right"] .car-body::after{
  background:linear-gradient(90deg,
    rgba(9,13,24,0)   calc(var(--cover,54.5%) * 0.5),
    rgba(9,13,24,.34) calc(var(--cover,54.5%) * 0.76),
    rgba(9,13,24,.78) calc(var(--cover,54.5%) * 0.93),
    rgba(9,13,24,.96) var(--cover,54.5%));
}
.car-card[data-cover="left"] .car-body::after{
  background:linear-gradient(270deg,
    rgba(9,13,24,0)   calc(var(--cover,54.5%) * 0.5),
    rgba(9,13,24,.34) calc(var(--cover,54.5%) * 0.76),
    rgba(9,13,24,.78) calc(var(--cover,54.5%) * 0.93),
    rgba(9,13,24,.96) var(--cover,54.5%));
}

.car-shot{
  position:relative;flex:1;min-height:0;overflow:hidden;
  /* Скругление снимка УБРАНО — партия #222, просьба владельца «в карточках
     товара нужно убрать скругления, сделай как было раньше»; на снимке он
     обвёл именно НИЖНИЕ углы фотографии.

     Раньше здесь стояло `border-radius:var(--r-md)` = 18px, и снимок читался
     отдельным скруглённым прямоугольником. Верхние углы и без этого срезает
     сама карточка (радиус 26px), поэтому теперь у снимка прямые нижние углы и
     скруглённые верхние — как и было до той правки. Скругление в ОКНЕ товара
     (.pm-shot) своё, этой правкой оно не затронуто. */
  background:radial-gradient(120% 90% at 50% 0%,rgba(34,230,255,.09),transparent 70%);
}
/* Снимок и видео-кадр одеты ОДНИМ правилом: кадр — он кадр и есть, и
   менять вид карточки от того, чем снят товар, не за что. `object-fit:cover`
   для видео так же обязателен, как для снимка: у вертикального ролика без
   него в кадре были бы чёрные полосы по бокам. */
.car-shot img,.car-shot video{width:100%;height:100%;object-fit:cover;
  display:block;transition:transform .7s var(--ease);
  /* Увеличение по наведению — МНОЖИТЕЛЬ, а не второй transform.

     Раньше здесь стояло `transform:scale(1.05)` прямо в правиле наведения.
     С настройкой размера снимка (раскладка карточки) у снимка появился СВОЙ
     transform — `scale(--ph-k)`, — и два правила об одном свойстве
     столкнулись бы: побеждает то, что ниже, и одно из двух молча пропало
     бы. Теперь transform ОДИН, а наведение меняет только множитель. */
  transform:scale(calc(var(--ph-k,1) * var(--ph-h,1)));
  object-position:var(--ph-pos,center center);
  transform-origin:var(--ph-origin,center center);
}
/* Видео НЕ берёт указатель: свайп по кадру идёт по `.car-frames`, а не по
   самому файлу. Без этого палец, потянувший по видео, не сдвинул бы кадр —
   браузер счёл бы жест своим (перемотка), и листание кадров сломалось бы
   ровно у товаров с видео. */
.car-shot video{pointer-events:none}
.car-card:hover .car-shot img,.car-card:hover .car-shot video{--ph-h:1.05}

/* ---------- ступени размера снимка (раскладка карточки) ----------

   Владелец (11.10): «самое главное могу фото товара менять по размеру в 6
   вариантах размера и вплоть до того чтобы полный размер фото вставал в
   карточке без увеличения как оригинал чтобы все было видно на фото в
   карточке и при этом сам размер карточке оставался по заданным размерам
   сайта».

   Замер до работы (probe-card-layout.js): карточка 298×432, полоса снимка
   296×244, снимки владельца вертикальные (800×1000). `object-fit: cover`
   режет у такого кадра 34% высоты — то есть «всё видно» сегодня неверно для
   КАЖДОГО снимка владельца.

   Отсюда: пять ступеней из шести вписывают кадр ЦЕЛИКОМ (`object-fit:
   contain`) и отличаются только множителем — от самого крупного, который
   занимает полосу целиком, до самого мелкого. Шестая, «как сейчас»,
   оставлена прежней (`cover`): она по умолчанию, и пока владелец не выбрал
   ступень, сайт выглядит ровно как до этой работы.

   `contain` НЕ увеличивает кадр сверх полосы и не режет его: покупатель
   видит снимок целиком, как оригинал. Пустое место по бокам у вертикального
   кадра — не недоделка: карточка держит свой размер (владелец сказал
   прямо), а «видно всё» и «карточка та же» вместе означают ровно это. */
.car-card:is([data-photo="full"],[data-photo="big"],[data-photo="mid"],
             [data-photo="small"],[data-photo="tiny"]) .car-shot img,
.car-card:is([data-photo="full"],[data-photo="big"],[data-photo="mid"],
             [data-photo="small"],[data-photo="tiny"]) .car-shot video{
  object-fit:contain;
}
/* Множитель ступени — на КАРТОЧКЕ, а не на снимке: снимок читает его через
   наследование, и ступень описывается одной строкой вместо четырёх. */
.car-card[data-photo="full"]{--ph-k:1}
.car-card[data-photo="big"]{--ph-k:.82}
.car-card[data-photo="mid"]{--ph-k:.66}
.car-card[data-photo="small"]{--ph-k:.5}
.car-card[data-photo="tiny"]{--ph-k:.36}

/* К какому краю прижат снимок. Нужно ровно потому, что ступени «весь снимок»
   вписывают кадр не во всю полосу: у вертикального кадра по бокам остаётся
   пустое место, и владелец решает, где снимку стоять.

   `object-position` двигает САМ кадр внутри коробки, а `transform-origin` —
   точку, от которой работает множитель. Врозь они бы разъехались: кадр
   прижался бы к левому краю, а уменьшался бы от середины и уполз вправо. */
.car-card[data-photo-align="left"]{--ph-pos:left center;--ph-origin:left center}
.car-card[data-photo-align="center"]{--ph-pos:center center;--ph-origin:center center}
.car-card[data-photo-align="right"]{--ph-pos:right center;--ph-origin:right center}

/* Снимок сверху или снизу от подписи. Порядок, а не перестановка узлов:
   `order` двигает на экране, и раскладка остаётся ОДНИМ правилом — иначе её
   пришлось бы собирать в скрипте, а правило жило бы в двух местах.

   Полоса снимка и здесь тянется на остаток высоты (`flex:1`), поэтому
   «снимок снизу» не ломает ни размер карточки, ни место подписи. */
.car-card[data-photo-place="bottom"] .car-shot{order:2}
.car-card[data-photo-place="bottom"] .car-body{order:1}

/* Плашка на снимке — слева, по центру или справа. */
.car-card[data-badge-align="center"] .car-badge{left:50%;transform:translateX(-50%)}
.car-card[data-badge-align="right"] .car-badge{left:auto;right:12px}

/* ---------- формат кадра 3:4 (карточка) ----------

   Владелец (12.10): «Надо добавить в админ панель дополнительный формат фото и
   видео и гиф чтобы показывашлись в карточке и в сплывающем окне размером 3:4
   вот тут в уменьшенном виде но полность чтобы видно было ... буду там
   загружать 1800 x 2400 пикс.»

   Признак `data-shot-frame="34"` ставит скрипт (см. `applyShopFrame` в
   js/main.js): он лежит и на корне документа, и на самом разделе магазина.
   Нет признака — нет ни одного правила ниже, и вид сайта прежний.

   ЧТО ЗДЕСЬ МЕНЯЕТСЯ И ЧТО НЕТ. Меняется только то, КАК нарисован кадр внутри
   полосы снимка. Сама полоса и карточка остаются прежних размеров — владелец
   говорил это прямо ещё в партии #11 («при этом сам размер карточке оставался
   по заданным размерам сайта»), и здесь это не отменяется.

   Замер до работы (`_probe-media-34.js`, кадр 1800×2400): полоса снимка
   296×314.8, а `cover` рисовал 296×394.7 — то есть резал верх и низ по 39.8px,
   и пятая часть кадра пропадала. Ровно это владелец и обвёл на снимке
   («в уменьшенном виде но полность чтобы видно было»).

   Отсюда кадр 3:4: снимок ВПИСЫВАЕТСЯ в коробку той же пропорции, что и файл,
   и виден целиком. Коробка 3:4 при полосе 296×314.8 выходит 236.3×315 — она
   ограничена ВЫСОТОЙ полосы, поэтому снимок «уменьшенный», а по бокам остаётся
   пустое поле по 29.9px. Это не недоделка: «видно всё» и «карточка та же»
   вместе означают ровно это.

   Почему `aspect-ratio` на САМОМ кадре, а не на полосе: полоса тянется на
   остаток высоты карточки (`flex:1`), и её пропорция зависит от карточки, а не
   от формата. Пропорцию задаёт кадр — тогда и видео, и гиф, и снимок получают
   одну и ту же рамку, какую бы пропорцию ни имел сам файл.

   `margin-inline:auto` центрирует кадр в полосе. `transform` для этого не
   годится: он у кадра уже занят движением при листании (`frameOffset` в
   js/main.js пишет `translateX`), и второй `transform` из правила спорил бы с
   первым, а не складывался.

   Центрировать приходится САМОМУ кадру: `.car-frame` — это и есть снимок
   (`frame0 = shopShotNode(...)`, `frame0.className = 'car-frame'`), а не слой
   вокруг него. Правила «слой кадра центрирует содержимое» здесь быть не может:
   у снимка нет содержимого. Такое правило тут стояло
   (`[data-shot-frame="34"] .car-frame{display:flex;...}`), и зуб
   `frame-center-gone` показал, что оно МЁРТВОЕ — снятие его ничего не меняет, а
   центрирование держится на `margin-inline:auto` выше. Правило снято. */
[data-shot-frame="34"] .car-shot img,
[data-shot-frame="34"] .car-shot video{
  width:auto;height:100%;max-width:100%;
  aspect-ratio:3 / 4;
  margin-inline:auto;
  /* Вписываем, а не заполняем: файл непропорции 3:4 (например, квадратный
     гиф) тоже обязан быть виден целиком, а не обрезан рамкой. */
  object-fit:contain;
}

/* ---------- фото без отступов (настройка карточки) ----------

   Владелец (30.10, снимок карточки): «уберем в карточках товара везде
   отступы, надо чтобы фото карточки товара было без отступов и это настройка
   должна быть в выбранном разделе Маркетплейс».

   Отступы были не у карточки и не у полосы снимка — их давал кадр 3:4 выше.
   Замер до правки: карточка 236, полоса снимка 234, а сам снимок 219 — по
   7,3 px пустоты слева и справа. Карточка «Маркетплейс» держит полосу 4:5, а
   кадр вписывался в 3:4 и центрировался: две пропорции не совпадали, остаток
   и становился полями.

   Теперь снимок ЗАПОЛНЯЕТ полосу по ШИРИНЕ (`width:100%`, поля 0). Правило
   весомее кадра 3:4 (три класса против двух), поэтому порядок в файле не
   важен, но стоит оно рядом — чтобы правку одного не потерять без другого.

   `object-fit` здесь `contain`, а НЕ `cover` (правка 30.10, второй снимок):
   владелец попросил «фото в полном объёме, без съедания краёв — буду такие
   фото закачивать всегда». `cover` заполнял полосу, но резал верх и низ
   постера. Вписывание не режет ничего; полей не будет, когда пропорции
   полосы и снимка совпадают, — а они совпадают у ступени «Маркетплейс»,
   которая теперь держит 3:4, как и формат кадра владельца.

   Признак `data-card-full="0"` возвращает прежнее поведение: владелец снимет
   галочку — и снимок снова вписывается по правилам формата кадра. */
.shop-block[data-card-full="1"] .car-shot img,
.shop-block[data-card-full="1"] .car-shot video{
  width:100%;height:100%;
  aspect-ratio:auto;
  margin-inline:0;
  object-fit:contain;
  /* Два ВЕРХНИХ угла снимка — ровно как у карточки. Постер 3:4 прижимается
     к верхней кромке, и его срезает сама карточка; а снимок другой пропорции
     после `contain` стоит с полями, и без этого радиуса его верхние углы
     были бы прямыми — владелец просил «чтобы края были скруглены два
     верхние». Нижние остаются прямыми: это его же просьба из партии #222.

     Радиус БЕРЁТСЯ У КАРТОЧКИ (`inherit`), а не считается из `--r-lg`:
     бумажная тема задаёт карточке свои 20px (theme-paper.css), и посчитанные
     26 оставляли в обоих верхних углах светлый клин — владелец обвёл его на
     снимке: «пробел в краях и не видно фото». Наследование знает про тему,
     а число — нет. Наследуется ЧЕРЕЗ `.car-shot` (см. правило ниже): у снимка
     родитель — полоса, а не карточка. */
  border-top-left-radius:inherit;
  border-top-right-radius:inherit;
}
/* Полоса снимка и лента кадров берут верхние углы у карточки — чтобы снимок
   мог взять их у ленты. Цепочка длиной в три звена, и это не оплошность:
   снимок лежит не прямо в полосе, а в `.car-frames` (разметку собирает
   `js/main.js`: кадров у товара может быть несколько). `inherit` берёт
   значение у РОДИТЕЛЯ, поэтому каждое звено обязано его передать. Нижние
   углы не трогаем: они внутри карточки и обязаны быть прямыми. */
.shop-block[data-card-full="1"] .car-shot,
.shop-block[data-card-full="1"] .car-frames,
.shop-block[data-card-full="1"] .car-shot img{
  border-top-left-radius:inherit;
  border-top-right-radius:inherit;
}

/* ---------- ступень карточки «Кадр 3:4 — фото 225 × 300» ----------

   Владелец (12.10, четыре снимка): «надо чтобы не было по бокам полос …
   надо чтобы там всегда был единый размер фото 225 x 300 чтобы когда
   загружаю вот такие фото по размерам всегда там были единым размером и без
   рамок и когда в эту карточку добавляю разные банеры скидку в наличии или
   распродажу или еще что то фото никогда не изменялось в размере и всегда
   было высокого качества … надо добавить новый Формат 3:4 чтобы я знал
   какой там размер карточки будет».

   ЭТО СТУПЕНЬ РАЗМЕРА КАРТОЧКИ, а не второй «формат кадра». Список тот же,
   что у «самых маленьких» и «крупных» (SHOP_CARD_SIZES на сервере), и выбор
   один — ступень. Признак кадра 3:4 (`data-shot-frame="34"`, партия #11)
   остаётся ОТДЕЛЬНОЙ настройкой и работает как работал: он про то, что
   показать в кадре, а ступень — про то, какого размера сам кадр.

   ЗАМЕР ДО РАБОТЫ (`_probe-b13-cardsize.js`, окно 1440, ступень «Крупные»):
   карточка 340×492, полоса снимка 338×275, нарисованный снимок 206×275,
   подпись 338×216 → боковые полосы по 66px. И у всех карточек блока они
   РАЗНЫЕ: 16, 29, 31, 33, 39, 47, 48, 66px. Причина одна: снимок вписан
   3:4, а ПОЛОСА — нет; остаток полосы и становится полосами по бокам.

   Вторая беда — размер снимка едет от подписи. Полоса берёт ОСТАТОК высоты
   карточки (`flex:1`), а высота карточки задана числом (`--car-h`). Значит,
   чем больше в подписи банеров, тем выше подпись и тем НИЖЕ полоса:
   подписи 105…216px, полосы 230…322px, снимки 225…321px. Ровно то, о чём
   владелец: «добавляю банеры — фото меняется в размере».

   Отсюда три правила, и все три — про то, что снимок ТВЁРДЫЙ:
   1) полоса САМА 3:4 — файл 1800×2400 ложится в неё без полос и без
      обрезки (`contain` в полосе 3:4 совпадает с `cover`);
   2) полоса не тянется (`flex:0 0 auto`) — её высоту задаёт ШИРИНА
      карточки, а не подпись;
   3) подпись растёт (`flex:1 1 auto`) — свободное место достаётся ей, а не
      снимку.

   ШИРИНА КАРТОЧКИ — 227px числом, а не множителем: 227 минус рамка 1px с
   каждой стороны = 225, ровно то число, что просил владелец. Полоса 225 при
   3:4 выходит 300. Оттого перебит `--card-base`, а не `--card-k`: множитель
   считался бы от 298, и «225» держалось бы на округлении.

   Множитель при этом 1 — и это не мелочь: кегль подписи, отступы и
   скругление читают `--card-k`, и на другой ступени они бы поехали за
   размером, которого нет.

   Ступень НЕ подчиняется окну: владелец просил ОДИН размер («всегда единый
   размер фото 225 x 300»), и на телефоне карточка тоже 227px. Поэтому
   правило объявлено с весом ВЫШЕ оконных: те написаны от `.shop-block`
   (один класс) и стоят ниже по файлу, а вес у них меньше — см. media
   (max-width:820px) и (max-width:560px).

   ВЫСОТУ КАРТОЧКИ СЧИТАЕТ СОДЕРЖИМОЕ. В ленте карточки лежат в дорожке
   `position:absolute`, и «высота по содержимому» у них сама не работает: у
   дорожки своя высота (`--car-h`), и карточка выше неё была бы обрезана
   сценой (`overflow:hidden` у `.car-stage`). Поэтому дорожка становится
   СЕТКОЙ В ОДНУ ЯЧЕЙКУ: все карточки встают в неё, ряд берёт высоту самой
   высокой, а растяжение (`align-self:stretch`, по умолчанию) уравнивает
   остальные — карточки одного ряда остаются одной высоты, как и раньше.

   Приём не новый: им уже живёт вид «сетка» (см. `[data-view="grid"]
   .car-card` ниже по файлу) — «снимок твёрдая доля, подпись берёт сколько
   нужно». Только там высоту задаёт ряд сетки, а здесь — ячейка дорожки.

   Скоуп — ПО ДОРОЖКЕ (`.car-track > .car-card`), а не по признаку вида: в
   виде «сетка» карточки лежат не в дорожке, а в `.shop-grid`, и правила про
   одну ячейку сломали бы сетку — все плитки встали бы в первую. */
.shop-block[data-card="f34"]{
  --card-base:227px;
  --card-k:1;
}
.shop-block[data-card="f34"] .car-track{
  display:grid;grid-template-columns:100%;height:auto;
}
.shop-block[data-card="f34"] .car-track > .car-card{
  position:relative;top:auto;left:auto;margin-left:0;
  width:var(--card-w);height:auto;
  grid-area:1 / 1;justify-self:center;
}
.shop-block[data-card="f34"] .car-shot{
  flex:0 0 auto;aspect-ratio:3 / 4;
}
.shop-block[data-card="f34"] .car-body{flex:1 1 auto}

/* ---------- ступень «Маркетплейс» ----------

   Владелец (20.10, снимок Ozon): «надо добавить в карусель товара новый размер
   карточек и назвать его Маркетплейс … именно такой же размер карточки и внизу
   кнопка которую я могу настраивать … фото должно при наведении не
   увеличиваться и можно было листать фотографии».

   Устройство то же, что у «Кадра 3:4»: полоса снимка — ТВЁРДАЯ доля, подпись
   берёт остаток, дорожка становится сеткой в одну ячейку (иначе «высота по
   содержимому» в ленте не работает — разбор у `f34` выше). Отличаются три
   вещи, и все три — про маркетплейс, а не про крупность:

   1) снимок 3:4, а не 4:5 (правка 30.10, второй снимок владельца: «надо
      чтобы там фото было в полном объёме, без съедания краёв… буду такие
      фото закачивать всегда»). Полоса 4:5 при постере 3:4 резала верх и низ
      на 13 % — владелец увидел именно это. 3:4 — его формат: постер
      заполняет полосу до пикселя, и ни полей, ни обрезки не остаётся;
   2) снимок НЕ увеличивается при наведении. Курсор по каталогу ходит часто, и
      «прыгающее» фото читается как дефект вёрстки. Гасим МНОЖИТЕЛЬ (`--ph-h`),
      а не сам `transform`: у снимка в `transform` уже стоит ступень
      «Весь снимок» (`--ph-k`), и второе правило об этом свойстве стёрло бы
      первое (разбор — у `.car-shot img`);
   3) кнопка внизу — своя строка карточки (`.car-cta`), а не часть подписи:
      подпись собирает владелец в панели и может поставить туда что угодно, а
      кнопка обязана стоять у нижней кромки всегда.

   Ширина — та же, что у КОЛОНКИ СЕТКИ, а не отдельное число.

   Владелец (30.10, два снимка рядом): «надо такие же по размерам карточки
   сделать вот тут в каруселе… а то они там узкие и не похожи на выбор
   Маркетплейс». Замер: в сетке (4 колонки, окно 1280) плитка 281 px, в ленте
   та же ступень давала 227 px — карточка выходила уже и оттого выше, чем
   шире: 227 × 532 против 281 × 560. Один и тот же «Маркетплейс» выглядел
   двумя разными раскладками.

   Числа 227 здесь больше нет: ширина считается из ТОЙ ЖЕ формулы, что у
   `.shop-grid` — «ширина блока минус промежутки, делённая на число колонок».
   `100%` в ширине карточки — это ширина дорожки, а она равна ширине блока
   (замер: 1178 и там, и там), поэтому результат совпадает с плиткой сетки
   до пикселя. Число колонок приходит переменной `--grid-cols` — её ставит
   js/main.js на блок (см. комментарий там), иначе владелец, выбрав «по 5 в
   ряд», получил бы в ленте ширину четырёх.

   Почему не оставить 227 и не подогнать сетку: сетка тянется по ширине окна,
   и на 1440 её плитка 319 px. Фиксированное число совпало бы с ней ровно на
   одной ширине экрана. */
.shop-block[data-card="mp"]{
  --card-base:227px;
  --card-k:1;
  --card-w:calc((100% - (var(--grid-cols, 5) - 1) * 18px) / var(--grid-cols, 5));
}
.shop-block[data-card="mp"] .car-track{
  display:grid;grid-template-columns:100%;height:auto;
}
.shop-block[data-card="mp"] .car-track > .car-card{
  position:relative;top:auto;left:auto;margin-left:0;
  width:var(--card-w);height:auto;
  grid-area:1 / 1;justify-self:center;
}
.shop-block[data-card="mp"] .car-shot{
  flex:0 0 auto;aspect-ratio:3 / 4;
}
/* Подпись у маркетплейса плотнее обычной: у Ozon между снимком и ценой нет
   широкой воздушной полосы, а нижний отступ отдан кнопке. */
.shop-block[data-card="mp"] .car-body{
  flex:1 1 auto;padding:12px 12px 6px;border-top:0;
}
.shop-block[data-card="mp"] .car-card:hover .car-shot img,
.shop-block[data-card="mp"] .car-card:hover .car-shot video{--ph-h:1}

/* Кнопка внизу карточки.

   Скрыта по умолчанию и включается ТОЛЬКО ступенью «Маркетплейс» — и в ленте,
   и в сетке. У остальных ступеней кнопки нет: их раскладка уже принята, и
   кнопка внизу сдвинула бы подпись у всех лент разом.

   `flex:0 0 auto` при подписи `flex:1 1 auto` — это и есть «кнопка у нижней
   кромки»: остаток высоты забирает подпись, и у карточек одного ряда кнопки
   стоят на одной линии, даже если названия разной длины. */
.car-cta{
  display:none;flex:0 0 auto;
  align-items:center;justify-content:center;
  margin:0 12px 12px;padding:0 10px;height:32px;
  border:0;border-radius:9px;cursor:pointer;
  font-family:inherit;font-size:12.5px;font-weight:700;line-height:1;
  letter-spacing:.01em;color:#fff;background:var(--cyan);
  transition:filter .25s var(--ease),transform .25s var(--ease);
}
.shop-block[data-card="mp"] .car-cta,
.shop-grid[data-grid-size="mp"] .car-cta{display:flex}

/* Товар 18+, возраст не подтверждён — кнопки в карточке НЕТ.

   Владелец (30.10, снимок): «в карточках где 18+ надо убрать кнопки, чтобы их
   в карточке не было». Под заглушкой кнопка всё равно ведёт в окно возраста, а
   не к товару: нажимать её незачем, и она читалась как «купить то, чего не
   видно». Признак `data-adult="1"` ставит скрипт только на ЗАКРЫТУЮ карточку;
   после ответа «есть 18» ленты собираются заново, признак не ставится — и
   кнопка возвращается сама, без отдельной памяти о ней.

   Селектор весомее правил выше (`.car-card` + `[data-adult]` + `.car-cta`
   против `.shop-block` + `[data-card]` + `.car-cta`), но всё равно стоит
   ПОСЛЕ них: так правка не зависит от того, кто из них ниже в файле. */
.car-card[data-adult="1"] .car-cta{display:none}
.car-cta:hover{filter:brightness(1.12)}
.car-cta:active{transform:translateY(1px)}
.car-cta:focus-visible{outline:2px solid var(--cyan);outline-offset:3px}
/* На бумаге `--cyan` уже переопределён на оранжевый филамент (см.
   theme-paper.css), поэтому кнопка сама берёт акцент темы — отдельного
   правила ей не нужно. */


/* ---------- строки подписи (раскладка карточки) ----------

   Блоки подписи собираются в строки: каждый блок — своя строка, а блоки с
   «рядом с предыдущим» встают в одну («ставить их рядом в ряд если место
   позволяет» владельца). Перенос на узкой карточке делает `flex-wrap` —
   обещание «если место позволяет» перестаёт быть обещанием.

   ОДИНОЧНАЯ строка — ОБЫЧНЫЙ БЛОК, а не флекс-строка, и это не мелочь:
   строка цен тянется во всю ширину карточки, и плашка у цены стоит у её
   правого края — ширина строки цен ИЗМЕРЕНА (probe-badge-fit.js, 278px под
   подпись 143px). Сделай одиночную строку флексом — строка цен сжалась бы
   по содержимому, плашка уехала бы на свою строку у всех товаров, где
   раньше помещалась. Поэтому флекс включается ТОЛЬКО при двух блоках и
   больше (`data-multi`). */
.car-line{display:block}
.car-line[data-multi="1"]{
  display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;
  justify-content:flex-start;
}
/* Выравнивание строки. Для текстовых блоков хватает `text-align`; строке цен
   его мало — она сама флекс-контейнер во всю ширину, и её содержимое двигает
   только `justify-content`. Пишем оба, каждый для своего случая. */
.car-line[data-align="center"]{text-align:center}
.car-line[data-align="right"]{text-align:right}
.car-line[data-align="center"] > .car-prices{justify-content:center}
.car-line[data-align="right"] > .car-prices{justify-content:flex-end}
.car-line[data-multi="1"][data-align="center"]{justify-content:center}
.car-line[data-multi="1"][data-align="right"]{justify-content:flex-end}
/* Строка остатка центрует себя САМА — `margin-left/right:auto` (владелец
   просил «в наличии сделать по центру внизу»). Автоматические поля сильнее
   `text-align`, поэтому «слева» и «справа» для неё надо снять явно, иначе
   выбор владельца не сработал бы ровно у этой строки. */
.car-line[data-align="left"] > .car-stock,
.car-line[data-align="right"] > .car-stock{margin-left:0;margin-right:0}


/* ---------- кадры товара в карточке ленты ----------
   Слои лежат друг на друге, показываем один: так кадр меняется без
   перезагрузки файла и без пустой вспышки. `is-dragging` снимает плавность,
   пока кадр идёт за пальцем, — с ней он отставал бы. */
.car-frames{
  position:absolute;inset:0;
  transition:transform .35s var(--ease);
}
.car-frames.is-dragging{transition:none}
.car-frame{
  position:absolute;inset:0;opacity:0;
  transition:opacity .45s var(--ease),transform .7s var(--ease);
}
.car-frame[data-on="1"]{opacity:1}

/* ---------- стрелки листания кадров: общий вид ----------

   Владелец просил: «стрелки листания на карточке товара и в окне товара —
   включение/выключение, больше 10 вариантов, цвет, иконки, анимации,
   размер; одинаково в обоих местах».

   Внешность ОДНА на оба места, поэтому она живёт здесь, а у `.car-nav` и
   `.pm-nav` остаётся только своё: где стоять и когда показываться. Владелец
   выбирает форму, значок, движение, размер и цвет в панели; страница ставит
   их атрибутами (`data-arr-*`) и переменной стиля — как у плашки у цены.

   Размер: основание своё у каждого места (у карточки кнопка мельче, в окне
   товара крупнее), а множитель общий. Поэтому «крупная» крупная и там, и
   там, и одна настройка не превращается в два разных размера.

   Цвет берётся из палитры магазина (`--stk-*`) — того же списка, что у
   плашки у цены, лупы, строки остатка и таймера: владелец задаёт цвет
   КАЖДОЙ настройке свой, а переменные ставятся на сам элемент, поэтому
   общие имена никого ни к чему не обязывают. В окне товара запасной цвет —
   цвет лупы (`--zoom-*`): пока владелец не выбрал цвет стрелкам, они
   остаются того же цвета, что и лупа, — как было. */
.car-nav,
.pm-nav{
  --arr-h:calc(var(--arr-base,34px) * var(--arr-k,1));
  --arr-w:var(--arr-h);
  --arr-i:calc(var(--arr-h) * .5);
  --arr-c:var(--stk-c,var(--zoom-c,#eaf6ff));
  --arr-bg:var(--stk-bg,rgba(6,10,18,.55));
  --arr-line:var(--stk-line,rgba(234,242,255,.42));
  --arr-on:var(--stk-ink,#0a1018);
  position:absolute;top:50%;
  width:var(--arr-w);height:var(--arr-h);margin-top:calc(var(--arr-h) / -2);
  display:flex;align-items:center;justify-content:center;
  padding:0;border:1px solid var(--arr-line);cursor:pointer;
  border-radius:50%;overflow:hidden;
  color:var(--arr-c);background:var(--arr-bg);
  -webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
  box-shadow:0 6px 18px rgba(0,0,0,.4);
  transition:opacity .25s var(--ease),background .25s var(--ease),
    border-color .25s var(--ease),transform .2s var(--ease);
}
.car-nav svg,
.pm-nav svg{
  width:var(--arr-i);height:var(--arr-i);
  fill:none;stroke:currentColor;stroke-width:2;
  stroke-linecap:round;stroke-linejoin:round;
  transition:transform .2s var(--ease);
}
/* Сторона, в которую стрелка ведёт. Нужна движениям «Покачивание»,
   «Качание» и «Подпрыгивание»: без неё обе стрелки качались бы в одну
   сторону, и «назад» показывала бы вперёд. */
.car-nav-prev,
.pm-nav-prev{--arr-dir:-1}
.car-nav-next,
.pm-nav-next{--arr-dir:1}

/* Размер. Основание задаёт место, множитель — настройка владельца. */
[data-arr-size="s"]{--arr-k:.82}
[data-arr-size="m"]{--arr-k:1}
[data-arr-size="l"]{--arr-k:1.18}
[data-arr-size="xl"]{--arr-k:1.42}

/* Форма кнопки. Четырнадцать видов — владелец просил «больше 10
   вариантов». Форма меняет только саму кнопку: значок внутри остаётся тем
   же, и листать кадры можно с любой из них. */
[data-arr-shape="squircle"]{border-radius:32%}
[data-arr-shape="square"]{border-radius:5px}
[data-arr-shape="pill"]{
  --arr-w:calc(var(--arr-h) * 1.6);--arr-i:calc(var(--arr-h) * .42);
  border-radius:999px;
}
[data-arr-shape="capsule"]{
  --arr-h:calc(var(--arr-base,34px) * var(--arr-k,1) * 1.45);
  --arr-w:calc(var(--arr-base,34px) * var(--arr-k,1));
  --arr-i:calc(var(--arr-w) * .55);
  border-radius:999px;
}
/* Ромб и «со срезом» режутся по контуру (`clip-path`), и ободок на них
   нарисовать нельзя: срез срезал бы и его. Поэтому край у них — сама
   заливка: берём цвет ободка палитры (`--arr-line`), а не тёмную подложку.
   С тёмной подложкой ромб на фотографии читался бы, а на тёмном участке
   снимка пропадал совсем — замерено снимком-доской `shot-arrows.js`. */
[data-arr-shape="diamond"]{
  --arr-i:calc(var(--arr-h) * .44);
  border:0;border-radius:0;
  background:var(--arr-line);color:var(--arr-on);
  clip-path:polygon(50% 0,100% 50%,50% 100%,0 50%);
}
[data-arr-shape="leaf"]{border-radius:50% 8% 50% 8%}
[data-arr-shape="cut"]{
  border:0;border-radius:0;
  background:var(--arr-line);color:var(--arr-on);
  clip-path:polygon(0 0,100% 0,100% 68%,68% 100%,0 100%);
}
[data-arr-shape="glass"]{
  border-color:rgba(255,255,255,.45);
  background:rgba(255,255,255,.14);color:#fff;
  -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
}
[data-arr-shape="solid"]{border:0;background:var(--arr-c);color:var(--arr-on)}
[data-arr-shape="ghost"]{background:transparent;border:2px solid var(--arr-c)}
[data-arr-shape="tag"]{border-radius:50% 14% 50% 50%}
[data-arr-shape="soft"]{border-radius:44% 56% 52% 48% / 48% 44% 56% 52%}
[data-arr-shape="ring"]{
  background:var(--arr-bg);border:3px solid var(--arr-c);
}

/* Движение. Значок двигается ВНУТРИ кнопки, а сама кнопка — только
   свечением и подложкой: у неё `transform` занят увеличением при
   наведении, и анимация перебила бы его — стрелка перестала бы отзываться
   на курсор. */
[data-arr-anim="pulse"]{animation:arrGlow 1.9s var(--ease) infinite}
[data-arr-anim="pulse"] svg{animation:arrPulse 1.9s var(--ease) infinite}
[data-arr-anim="glow"]{animation:arrGlow 2.1s ease-in-out infinite}
[data-arr-anim="blink"] svg{animation:arrBlink 1.6s ease-in-out infinite}
[data-arr-anim="slide"] svg{animation:arrSlide 1.6s ease-in-out infinite}
[data-arr-anim="bounce"] svg{animation:arrBounce 1.4s var(--ease) infinite}
[data-arr-anim="swing"] svg{animation:arrSwing 1.6s ease-in-out infinite}
[data-arr-anim="shine"]::after{
  content:'';position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(115deg,transparent 34%,
    rgba(255,255,255,.6) 50%,transparent 66%);
  transform:translateX(-130%);
  animation:arrShine 2.4s ease-in-out infinite;
}
@keyframes arrPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.18)}}
@keyframes arrGlow{
  0%,100%{box-shadow:0 6px 18px rgba(0,0,0,.4),0 0 0 0 var(--arr-line)}
  50%{box-shadow:0 6px 18px rgba(0,0,0,.4),0 0 0 5px transparent}
}
@keyframes arrBlink{0%,100%{opacity:1}50%{opacity:.22}}
@keyframes arrSlide{
  0%,100%{transform:translateX(0)}
  50%{transform:translateX(calc(var(--arr-dir,1) * 3px))}
}
@keyframes arrBounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}
@keyframes arrSwing{
  0%,100%{transform:rotate(0)}
  50%{transform:rotate(calc(var(--arr-dir,1) * 9deg))}
}
@keyframes arrShine{
  0%{transform:translateX(-130%)}
  55%,100%{transform:translateX(130%)}
}
@media (prefers-reduced-motion:reduce){
  .car-nav,.pm-nav{animation:none}
  .car-nav svg,.pm-nav svg{animation:none}
  .car-nav::after,.pm-nav::after{animation:none}
}

/* Карточка ленты: основание мельче, стрелки стоят у края снимка и видны
   только у ЦЕНТРАЛЬНОЙ карточки — у боковых она повёрнута и уменьшена,
   кнопка на ней была бы и мелкой, и лишней. Стрелка, которой некуда идти,
   гаснет — обещать листание, которого нет, нельзя. */
.car-nav{--arr-base:34px;z-index:3;opacity:0;pointer-events:none}
.car-nav-prev{left:8px}
.car-nav-next{right:8px}
/* У центральной карточки видны ОБЕ стрелки, даже когда идти некуда: та,
   что исчезает, читается как поломка. Недоступная просто гаснет — по ней
   видно, что вы в начале или в конце, и нажать её нельзя. */
.car-card[data-active="1"] .car-nav{opacity:.45;pointer-events:none}
.car-card[data-active="1"] .car-nav[data-can="1"]{opacity:.85;pointer-events:auto}
.car-card[data-active="1"] .car-nav[data-can="1"]:hover{
  opacity:1;background:var(--arr-c);color:var(--arr-on);
  border-color:transparent;transform:scale(1.08)}
.car-nav:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}

/* Точки-каталожки кадров: какой сейчас кадр и есть ли соседние.
   Как и лейбл в углу, видны только у центральной карточки — на боковых
   они превратились бы в рябь.

   Сам ВИД (кружки, полоски, ромбы…), РАЗМЕР и ЦВЕТ приходят из общей
   настройки точек — см. блок «точки: окно» ниже. Здесь остаётся только
   место на карточке: прежние правила вида стояли тут и спорили бы с ним
   числом классов, а «две правки одного вида» — это ровно та поломка,
   которую потом ищут полдня. */
.car-fdots{
  position:absolute;left:0;right:0;bottom:9px;z-index:3;
  display:flex;align-items:center;justify-content:center;gap:5px;
  opacity:0;transition:opacity .25s var(--ease);pointer-events:none;
}
.car-card[data-active="1"] .car-fdots{opacity:1}
/* Место без кадра (первый и последний кадр) остаётся на своём месте, но
   гаснет: так цветная точка всегда стоит посередине трёх, как просил
   владелец, и ряд не «переезжает» на крайних кадрах. */
.car-fdots[data-dots-window] .car-fdot[data-empty]{opacity:.28}

/* ---------- товар 18+: заглушка на фотографии ----------
   Владелец просил «область скрытия фото товара скрыть и название» и
   «выбирать скратие фотки сделать разные варианты и красивые с анимацией».
   Вид и движение приходят идентификаторами из закрытых списков сервера и
   вешают атрибуты — сами рисунки живут здесь, как у лупы и у значка у цены.

   Заглушка лежит ПОВЕРХ снимка, а не вместо него: фотография остаётся в
   разметке, и когда покупатель подтвердит возраст, показывать её заново не
   придётся — достаточно снять слой. Ради этого же она выходит за края на
   8px: «покачивание» и «качание» двигают слой, и без запаса по краям из-под
   него выглядывала бы полоска того самого снимка, который прячут. Обрезает
   запас .car-shot — у него overflow:hidden. */
.car-adult{
  position:absolute;inset:-8px;z-index:2;
  display:flex;align-items:center;justify-content:center;
  /* Радиуса у заглушки больше нет — вместе со скруглением снимка (см.
     .car-shot). Пока снимок был скруглён, заглушка обязана была повторять
     его радиус: иначе в её нижних углах проглядывала бы подложка .car-shot
     светлыми уголками. Теперь снимок прямоугольный, и радиус заглушки как
     раз оставлял бы такие же уголки — то есть работал бы против себя. */
  background:rgba(9,13,23,.94);
}
.car-adult[data-mask="blur"]{
  background:rgba(9,13,23,.34);
  -webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px);
}
.car-adult[data-mask="dark"]{background:rgba(4,7,14,.93)}
.car-adult[data-mask="stripes"]{
  background:repeating-linear-gradient(135deg,
    rgba(6,10,18,.96) 0 10px,rgba(24,32,50,.96) 10px 20px);
}
.car-adult[data-mask="pixel"]{
  background:repeating-conic-gradient(
    rgba(6,10,18,.97) 0 25%,rgba(22,30,48,.97) 0 50%) 0 0/18px 18px;
}
.car-adult[data-mask="curtain"]{
  background:linear-gradient(180deg,rgba(6,10,18,.97),
    rgba(20,28,48,.9) 50%,rgba(6,10,18,.97));
  box-shadow:inset 0 0 0 1px rgba(120,150,200,.16);
}
.car-adult[data-mask="solid"]{background:#080c16}
/* «Звёздное небо» — просьба владельца: «добавь типо блюра мутность самой
   фотографии» (партия #222).

   Подложка собрана из мягких световых пятен, а не из ровной заливки: пятна
   без резких границ читаются как расфокусированный снимок. Резких краёв тут
   быть не должно — именно они и выдают «плашку» вместо «мутного фото».

   `backdrop-filter` размывает то, что лежит ПОД заглушкой. Сейчас у закрытой
   карточки под ней пусто (фотография не запрашивается вовсе — это стережёт
   `test-adult`), поэтому видимого эффекта он не даёт и стоит здесь на случай,
   если владелец решит показывать снимок под мутностью. Вреда он не несёт:
   размывает подложку .car-shot, а не файл. */
.car-adult[data-mask="stars"]{
  background:
    radial-gradient(120% 80% at 22% 14%,rgba(152,198,248,.62),transparent 62%),
    radial-gradient(110% 70% at 78% 42%,rgba(80,134,202,.55),transparent 66%),
    radial-gradient(130% 90% at 40% 108%,rgba(12,22,42,.9),transparent 72%),
    linear-gradient(180deg,#2c4368,#0d1728 80%);
  -webkit-backdrop-filter:blur(16px) saturate(.7) brightness(.82);
  backdrop-filter:blur(16px) saturate(.7) brightness(.82);
}

/* Подпись на заглушке: сколько бы ни было видов, покупатель должен
   понимать, почему вместо товара пусто. */
.car-adult-mark{
  position:relative;z-index:1;
  display:inline-flex;align-items:center;gap:.42em;
  padding:8px 16px;border-radius:100px;
  font-size:1rem;font-weight:800;letter-spacing:.1em;
  color:var(--adult-ink,#fff);
  background:linear-gradient(135deg,var(--adult-a,#e0356b),var(--adult-b,#8b1a3c));
  box-shadow:0 10px 30px rgba(0,0,0,.5);
}

/* ---------- значки рядом с плашкой «18+» и тематика 18+ ----------

   Владелец просил: «нужно добавить в админ панель возможность менять рядом
   иконки добавлять или убирать их иконки больше 20 штук добавь тематека 18+
   и добавь вот такую» — и прислал рисунок «18+ в разомкнутом кольце с
   хвостиком» (первый в списке, ring18).

   Значки рисуются маской, как знак у подписи карусели: цвет берётся у
   самой плашки (currentColor), поэтому значок сам попадает в выбранную
   тематику и не может с ней разъехаться. Картинкой он был бы всегда одного
   цвета и на золотой плашке читался бы как чужой.

   Список значков — на сервере (SHOP_ADULT_ICONS); здесь только рисунки.
   Значок, которого тут нет, просто не покажется: панель не может прислать
   рисунок, которого сайт не нарисовал. */
:root{
  --adult-a:#e0356b;--adult-b:#8b1a3c;--adult-ink:#fff;

  --adult-ico-ring18:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M16.2%206.4A7.4%207.4%200%201%200%2018.6%2013.4'%20fill='none'%20stroke='%23000'%20stroke-width='2.1'%20stroke-linecap='round'/%3E%3Cpath%20d='M18.6%2013.4q1.6%201.2%201.4%203.2'%20fill='none'%20stroke='%23000'%20stroke-width='2.1'%20stroke-linecap='round'/%3E%3Cpath%20d='M19.2%202.2v4.4M17%204.4h4.4'%20fill='none'%20stroke='%23000'%20stroke-width='1.7'%20stroke-linecap='round'/%3E%3Ctext%20x='11.4'%20y='15.8'%20font-family='Arial,Helvetica,sans-serif'%20font-size='9.4'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18%3C/text%3E%3C/svg%3E");
  --adult-ico-ring18-closed:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9.4'%20fill='none'%20stroke='%23000'%20stroke-width='2.3'/%3E%3Ctext%20x='12'%20y='15.2'%20font-family='Arial,Helvetica,sans-serif'%20font-size='8.2'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18+%3C/text%3E%3C/svg%3E");
  --adult-ico-ring18-out:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Ccircle%20cx='10.8'%20cy='13.4'%20r='7.2'%20fill='none'%20stroke='%23000'%20stroke-width='2.1'/%3E%3Cpath%20d='M20%202.1000000000000005v4.6M17.7%204.4h4.6'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'/%3E%3Ctext%20x='10.8'%20y='16.6'%20font-family='Arial,Helvetica,sans-serif'%20font-size='8.8'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18%3C/text%3E%3C/svg%3E");
  --adult-ico-ring18-tangent:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M16.2%206.4A7.4%207.4%200%201%200%2018.6%2013.4'%20fill='none'%20stroke='%23000'%20stroke-width='2.1'%20stroke-linecap='round'/%3E%3Cpath%20d='M18.6%2013.4q1.6%201.2%201.4%203.2'%20fill='none'%20stroke='%23000'%20stroke-width='2.1'%20stroke-linecap='round'/%3E%3Cpath%20d='M18.2%203.0000000000000004v4.8M15.799999999999999%205.4h4.8'%20fill='none'%20stroke='%23000'%20stroke-width='2'%20stroke-linecap='round'/%3E%3Ctext%20x='11.4'%20y='15.8'%20font-family='Arial,Helvetica,sans-serif'%20font-size='9.4'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18%3C/text%3E%3C/svg%3E");
  --adult-ico-ring18-arrow:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M17.6%205.8A8.2%208.2%200%201%200%2020.4%2013.2'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'/%3E%3Cpath%20d='M14.6%202.6%2019.4%204.2%2017.8%208.8z'%20fill='%23000'/%3E%3Cpath%20d='M20.8%202.4000000000000004v4M18.8%204.4h4'%20fill='none'%20stroke='%23000'%20stroke-width='1.7'%20stroke-linecap='round'/%3E%3Ctext%20x='11'%20y='15.8'%20font-family='Arial,Helvetica,sans-serif'%20font-size='8.8'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18%3C/text%3E%3C/svg%3E");
  --adult-ico-ring18-thin:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9.6'%20fill='none'%20stroke='%23000'%20stroke-width='1.3'/%3E%3Ctext%20x='12'%20y='15.6'%20font-family='Arial,Helvetica,sans-serif'%20font-size='9'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18%3C/text%3E%3C/svg%3E");
  --adult-ico-ring18-double:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Ccircle%20cx='12'%20cy='12'%20r='10.4'%20fill='none'%20stroke='%23000'%20stroke-width='1.2'/%3E%3Ccircle%20cx='12'%20cy='12'%20r='7.6'%20fill='none'%20stroke='%23000'%20stroke-width='1.2'/%3E%3Ctext%20x='12'%20y='15'%20font-family='Arial,Helvetica,sans-serif'%20font-size='7.6'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18+%3C/text%3E%3C/svg%3E");
  --adult-ico-ring18-stamp:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9.4'%20fill='none'%20stroke='%23000'%20stroke-width='1.8'%20stroke-dasharray='2.6%202.2'/%3E%3Ctext%20x='12'%20y='15.6'%20font-family='Arial,Helvetica,sans-serif'%20font-size='8.4'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18%3C/text%3E%3C/svg%3E");
  --adult-ico-square18:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M14.2%204.4H9.2A4.8%204.8%200%200%200%204.4%209.2v5.6a4.8%204.8%200%200%200%204.8%204.8h5.6a4.8%204.8%200%200%200%204.8-4.8V9.4'%20fill='none'%20stroke='%23000'%20stroke-width='2'%20stroke-linecap='round'/%3E%3Cpath%20d='M18.2%201.2000000000000002v4.8M15.799999999999999%203.6h4.8'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'/%3E%3Ctext%20x='11.4'%20y='16'%20font-family='Arial,Helvetica,sans-serif'%20font-size='8.8'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18%3C/text%3E%3C/svg%3E");
  --adult-ico-square18-out:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Crect%20x='3.6'%20y='6'%20width='14.6'%20height='14.6'%20rx='4.2'%20fill='none'%20stroke='%23000'%20stroke-width='2'/%3E%3Cpath%20d='M20.2%201.5v4.6M17.9%203.8h4.6'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'/%3E%3Ctext%20x='10.9'%20y='16.8'%20font-family='Arial,Helvetica,sans-serif'%20font-size='8.6'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18%3C/text%3E%3C/svg%3E");
  --adult-ico-text18:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Ctext%20x='12'%20y='17.6'%20font-family='Arial,Helvetica,sans-serif'%20font-size='13.4'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18+%3C/text%3E%3C/svg%3E");
  --adult-ico-text18-frame:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Crect%20x='2.2'%20y='6.2'%20width='19.6'%20height='11.6'%20rx='3'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'/%3E%3Ctext%20x='12'%20y='15'%20font-family='Arial,Helvetica,sans-serif'%20font-size='8.4'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18+%3C/text%3E%3C/svg%3E");
  --adult-ico-badge18:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Crect%20x='3.2'%20y='7.4'%20width='14.4'%20height='12.4'%20rx='3.4'%20fill='none'%20stroke='%23000'%20stroke-width='2.3'/%3E%3Cpath%20d='M20%202.2v4.4M17.8%204.4h4.4'%20fill='none'%20stroke='%23000'%20stroke-width='1.8'%20stroke-linecap='round'/%3E%3Ctext%20x='10.4'%20y='16.4'%20font-family='Arial,Helvetica,sans-serif'%20font-size='7.4'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18%3C/text%3E%3C/svg%3E");
  --adult-ico-calendar18:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Crect%20x='3.4'%20y='5.6'%20width='17.2'%20height='15'%20rx='2.8'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'/%3E%3Cpath%20d='M3.4%2010.2h17.2'%20stroke='%23000'%20stroke-width='1.9'/%3E%3Cpath%20d='M7.8%203.2v4.2M16.2%203.2v4.2'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'/%3E%3Ctext%20x='12'%20y='18.6'%20font-family='Arial,Helvetica,sans-serif'%20font-size='6.6'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18%3C/text%3E%3C/svg%3E");
  --adult-ico-no-under18:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9.4'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'/%3E%3Ctext%20x='12'%20y='15.6'%20font-family='Arial,Helvetica,sans-serif'%20font-size='8.6'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18%3C/text%3E%3Cpath%20d='M5.8%205.8%2018.2%2018.2'%20fill='none'%20stroke='%23000'%20stroke-width='2'%20stroke-linecap='round'/%3E%3C/svg%3E");
  --adult-ico-age-check:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Ctext%20x='9.6'%20y='16.6'%20font-family='Arial,Helvetica,sans-serif'%20font-size='9'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18%3C/text%3E%3Cpath%20d='M13.8%2015.2%2016.4%2018.4%2021.4%2010.4'%20fill='none'%20stroke='%23000'%20stroke-width='2.2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3E%3C/svg%3E");
  --adult-ico-lock18:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Crect%20x='4.4'%20y='10.4'%20width='15.2'%20height='11'%20rx='2.6'%20fill='none'%20stroke='%23000'%20stroke-width='2'/%3E%3Cpath%20d='M7.8%2010.4V7.6a4.2%204.2%200%200%201%208.4%200v2.8'%20fill='none'%20stroke='%23000'%20stroke-width='2'/%3E%3Ctext%20x='12'%20y='19'%20font-family='Arial,Helvetica,sans-serif'%20font-size='6.6'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18%3C/text%3E%3C/svg%3E");
  --adult-ico-id18:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Crect%20x='2.4'%20y='5.6'%20width='19.2'%20height='13.4'%20rx='2.6'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'/%3E%3Ccircle%20cx='7.6'%20cy='11.4'%20r='2.2'%20fill='%23000'/%3E%3Cpath%20d='M4.6%2017c.5-1.7%201.7-2.6%203-2.6s2.5.9%203%202.6'%20fill='none'%20stroke='%23000'%20stroke-width='1.6'/%3E%3Ctext%20x='16.6'%20y='14.8'%20font-family='Arial,Helvetica,sans-serif'%20font-size='6.4'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18%3C/text%3E%3C/svg%3E");
  --adult-ico-shield18:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M12%202.6%2020%205.7v6.1c0%204.6-3.3%208.3-8%209.6-4.7-1.3-8-5-8-9.6V5.7z'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linejoin='round'/%3E%3Ctext%20x='12'%20y='14.6'%20font-family='Arial,Helvetica,sans-serif'%20font-size='7.4'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18%3C/text%3E%3C/svg%3E");
  --adult-ico-doc18:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M6%202.8h7.6L19.4%208.6V21.2H6z'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linejoin='round'/%3E%3Cpath%20d='M13.4%202.8v6h6'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linejoin='round'/%3E%3Ctext%20x='12.6'%20y='17'%20font-family='Arial,Helvetica,sans-serif'%20font-size='6.4'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18%3C/text%3E%3C/svg%3E");
  --adult-ico-eye18:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M2.6%2012S6%206.4%2012%206.4%2021.4%2012%2021.4%2012%2018%2017.6%2012%2017.6%202.6%2012%202.6%2012z'%20fill='none'%20stroke='%23000'%20stroke-width='1.8'/%3E%3Ctext%20x='12'%20y='14.8'%20font-family='Arial,Helvetica,sans-serif'%20font-size='5.8'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18%3C/text%3E%3C/svg%3E");
  --adult-ico-key18:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Ccircle%20cx='7'%20cy='8.4'%20r='3.4'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'/%3E%3Cpath%20d='M9.6%2010.8%2019%2020.2M15.6%2016.8l2.4-2.4M18%2019.2l2.4-2.4'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'/%3E%3Ctext%20x='17.4'%20y='7'%20font-family='Arial,Helvetica,sans-serif'%20font-size='6'%20font-weight='700'%20text-anchor='middle'%20fill='%23000'%3E18%3C/text%3E%3C/svg%3E");
}

/* Тематика 18+: окраска плашки на заглушке, плашки под ценой и кружка в окне
   вопроса. Ставится атрибутом на корень документа (js/main.js), поэтому
   доезжает и до окна вопроса, которое лежит ВНЕ секции магазина. */
[data-adult-theme="fire"]{--adult-a:#ff7a18;--adult-b:#d6202a;--adult-ink:#fff}
[data-adult-theme="crimson"]{--adult-a:#b3122f;--adult-b:#5c0716;--adult-ink:#fff}
[data-adult-theme="neon"]{--adult-a:#22e6ff;--adult-b:#0b8fb0;--adult-ink:#04121a}
[data-adult-theme="violet"]{--adult-a:#a259ff;--adult-b:#5a1bb0;--adult-ink:#fff}
[data-adult-theme="gold"]{--adult-a:#ffd24a;--adult-b:#b8860b;--adult-ink:#241300}
[data-adult-theme="mono"]{--adult-a:#4a5568;--adult-b:#1f2733;--adult-ink:#fff}

/* Сам значок. Ширина в em — растёт вместе с плашкой, как и знак у подписи
   карусели. Цвет — currentColor: значок всегда одного цвета с надписью
   «18+», в какой бы тематике она ни была. */
.car-adult-ico{
  --adult-ico-ink:currentColor;
  --adult-ico-bold:.5px;
  --adult-ico-bold-n:-.5px;
  display:inline-block;flex:none;width:1.08em;height:1.08em;
  background:var(--adult-ico-ink);
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-position:center;mask-position:center;
  -webkit-mask-size:contain;mask-size:contain;
}

/* ---------- размер, жирность и цвет ОТДЕЛЬНОГО значка ----------

   Владелец просил: «чтобы я мог редактировать каждый значок, а именно
   менять его размер из 4 вариантов и делать его жирнее или не жирнее и
   менять цвет. А то сейчас их не видно когда они мелкие и не заметные».

   Здесь только ИМЕНА из закрытого списка сервера
   (SHOP_ADULT_ICON_SIZES / SHOP_ADULT_ICON_COLORS), как у тематики 18+.
   Числа и краски живут тут, поэтому «размер 400» или «#ff00ff» из чужого
   запроса до значка не доедут.

   Четыре ступени, и разрыв между «обычным» и «крупным» в полтора раза:
   ступени в четверть em читались бы как одна.

   Сила «жирности» растёт вместе со значком: полпикселя на мелком заметны,
   на самом крупном — нет. */
.car-adult-ico[data-adult-size="s"]{width:.92em;height:.92em;--adult-ico-bold:.42px;--adult-ico-bold-n:-.42px}
.car-adult-ico[data-adult-size="m"]{width:1.08em;height:1.08em}
.car-adult-ico[data-adult-size="l"]{width:1.5em;height:1.5em;--adult-ico-bold:.7px;--adult-ico-bold-n:-.7px}
.car-adult-ico[data-adult-size="xl"]{width:2em;height:2em;--adult-ico-bold:.9px;--adult-ico-bold-n:-.9px}

.car-adult-ico[data-adult-color="ink"]{--adult-ico-ink:currentColor}
.car-adult-ico[data-adult-color="white"]{--adult-ico-ink:#fff}
.car-adult-ico[data-adult-color="gold"]{--adult-ico-ink:#ffd24a}
.car-adult-ico[data-adult-color="neon"]{--adult-ico-ink:#22e6ff}
.car-adult-ico[data-adult-color="lime"]{--adult-ico-ink:#8ef07a}
.car-adult-ico[data-adult-color="rose"]{--adult-ico-ink:#ff8ec4}
.car-adult-ico[data-adult-color="violet"]{--adult-ico-ink:#c08bff}
.car-adult-ico[data-adult-color="dark"]{--adult-ico-ink:#121826}

/* «Жирнее». Толщины у значка нет: он рисуется маской на сплошной заливке, а
   обводка вшита в сам рисунок. Наращиваем её тенями БЕЗ размытия в ту же
   краску — контур утолщается ровно, рисунок остаётся резким. Размытие тут
   превратило бы значок в мутное пятно, поэтому ноль размытия обязателен. */
.car-adult-ico[data-adult-bold="1"]{
  filter:drop-shadow(var(--adult-ico-bold) 0 0 var(--adult-ico-ink))
         drop-shadow(var(--adult-ico-bold-n) 0 0 var(--adult-ico-ink))
         drop-shadow(0 var(--adult-ico-bold) 0 var(--adult-ico-ink))
         drop-shadow(0 var(--adult-ico-bold-n) 0 var(--adult-ico-ink));
}
.car-adult-ico[data-adult-ico="ring18"]{
  -webkit-mask-image:var(--adult-ico-ring18);mask-image:var(--adult-ico-ring18)}
.car-adult-ico[data-adult-ico="ring18-closed"]{
  -webkit-mask-image:var(--adult-ico-ring18-closed);mask-image:var(--adult-ico-ring18-closed)}
.car-adult-ico[data-adult-ico="ring18-out"]{
  -webkit-mask-image:var(--adult-ico-ring18-out);mask-image:var(--adult-ico-ring18-out)}
.car-adult-ico[data-adult-ico="ring18-tangent"]{
  -webkit-mask-image:var(--adult-ico-ring18-tangent);mask-image:var(--adult-ico-ring18-tangent)}
.car-adult-ico[data-adult-ico="ring18-arrow"]{
  -webkit-mask-image:var(--adult-ico-ring18-arrow);mask-image:var(--adult-ico-ring18-arrow)}
.car-adult-ico[data-adult-ico="ring18-thin"]{
  -webkit-mask-image:var(--adult-ico-ring18-thin);mask-image:var(--adult-ico-ring18-thin)}
.car-adult-ico[data-adult-ico="ring18-double"]{
  -webkit-mask-image:var(--adult-ico-ring18-double);mask-image:var(--adult-ico-ring18-double)}
.car-adult-ico[data-adult-ico="ring18-stamp"]{
  -webkit-mask-image:var(--adult-ico-ring18-stamp);mask-image:var(--adult-ico-ring18-stamp)}
.car-adult-ico[data-adult-ico="square18"]{
  -webkit-mask-image:var(--adult-ico-square18);mask-image:var(--adult-ico-square18)}
.car-adult-ico[data-adult-ico="square18-out"]{
  -webkit-mask-image:var(--adult-ico-square18-out);mask-image:var(--adult-ico-square18-out)}
.car-adult-ico[data-adult-ico="text18"]{
  -webkit-mask-image:var(--adult-ico-text18);mask-image:var(--adult-ico-text18)}
.car-adult-ico[data-adult-ico="text18-frame"]{
  -webkit-mask-image:var(--adult-ico-text18-frame);mask-image:var(--adult-ico-text18-frame)}
.car-adult-ico[data-adult-ico="badge18"]{
  -webkit-mask-image:var(--adult-ico-badge18);mask-image:var(--adult-ico-badge18)}
.car-adult-ico[data-adult-ico="calendar18"]{
  -webkit-mask-image:var(--adult-ico-calendar18);mask-image:var(--adult-ico-calendar18)}
.car-adult-ico[data-adult-ico="no-under18"]{
  -webkit-mask-image:var(--adult-ico-no-under18);mask-image:var(--adult-ico-no-under18)}
.car-adult-ico[data-adult-ico="age-check"]{
  -webkit-mask-image:var(--adult-ico-age-check);mask-image:var(--adult-ico-age-check)}
.car-adult-ico[data-adult-ico="lock18"]{
  -webkit-mask-image:var(--adult-ico-lock18);mask-image:var(--adult-ico-lock18)}
.car-adult-ico[data-adult-ico="id18"]{
  -webkit-mask-image:var(--adult-ico-id18);mask-image:var(--adult-ico-id18)}
.car-adult-ico[data-adult-ico="shield18"]{
  -webkit-mask-image:var(--adult-ico-shield18);mask-image:var(--adult-ico-shield18)}
.car-adult-ico[data-adult-ico="doc18"]{
  -webkit-mask-image:var(--adult-ico-doc18);mask-image:var(--adult-ico-doc18)}
.car-adult-ico[data-adult-ico="eye18"]{
  -webkit-mask-image:var(--adult-ico-eye18);mask-image:var(--adult-ico-eye18)}
.car-adult-ico[data-adult-ico="key18"]{
  -webkit-mask-image:var(--adult-ico-key18);mask-image:var(--adult-ico-key18)}

/* Движение заглушки. Ни одно не трогает прозрачность до нуля: мелькающая
   заглушка открывала бы то, что ею закрыто. */
@keyframes adPulse{0%,100%{opacity:.86}50%{opacity:1}}
@keyframes adShimmer{0%{background-position:190% 0}100%{background-position:-90% 0}}
@keyframes adFloat{0%,100%{transform:translateY(-3px)}50%{transform:translateY(3px)}}
@keyframes adBreathe{0%,100%{transform:scale(1)}50%{transform:scale(1.05)}}
@keyframes adSwing{0%,100%{transform:rotate(-1.5deg)}50%{transform:rotate(1.5deg)}}
.car-adult[data-anim="pulse"]{animation:adPulse 2.6s var(--ease) infinite}
.car-adult[data-anim="float"]{animation:adFloat 3.4s var(--ease) infinite}
.car-adult[data-anim="breathe"]{animation:adBreathe 3.6s var(--ease) infinite}
.car-adult[data-anim="swing"]{animation:adSwing 3.6s var(--ease) infinite}
.car-adult[data-anim="shimmer"]::after{
  content:'';position:absolute;inset:0;pointer-events:none;
  border-radius:inherit;
  background:linear-gradient(105deg,transparent 38%,
    rgba(200,230,255,.24) 50%,transparent 62%);
  background-size:250% 100%;
  animation:adShimmer 3.2s linear infinite;
}

/* «Снег» — просьба владельца: «добавь анимированный типо такого как в
   телеграмме красивый» (партия #222).

   Частицы нарисованы ФОНОМ, а не сотней узлов в разметке: двести кружков в
   DOM на телефоне стоили бы кадров, а тут браузер рисует их сам.

   ПЛИТКА МАЛАЯ (64px и 120px), а точек на ней несколько. Первый заход был с
   плиткой 160px — на карточке шириной 296px это давало всего ~20 точек, и
   «звёздное небо» читалось ровным тёмным градиентом: на снимке частиц почти
   не видно. Плотность важнее размера — точек должно быть много и мелких.

   Два слоя с разной плиткой дают разную скорость и глубину: мелкие и частые
   идут медленнее, крупные и редкие — быстрее.

   Ход каждого слоя равен ВЫСОТЕ ЕГО ПЛИТКИ (64px и 120px). Это не украшение
   записи: при любом другом сдвиге на стыке цикла был бы рывок — ровно та
   ошибка, что была у бегущей строки (см. навык `seamless-css-marquee`). Слой
   ходит ТОЛЬКО по вертикали, по той же причине: сдвиг по горизонтали на
   нецелое число плиток рвёт стык.

   Частицы лежат на `::before`, а не на `::after`: `::after` занят «бликом»,
   и два движения на одном псевдоэлементе не уживаются. */
@keyframes adSnow{
  /* Ход выписан для КАЖДОГО слоя, а не двумя значениями на всех.

     Список `background-position` короче списка слоёв повторяется ПО КРУГУ.
     С двумя значениями восемь слоёв получали ход по очереди — 64, 120, 64,
     120, … — и слои 2, 4, 7 (у них плитка 64px) ехали на 120px. Ход, не
     равный высоте плитки, рвёт стык цикла: замер `probe-adult-travel.js`
     показал у этих трёх слоёв ровно 121px вместо 64 — то есть на каждом
     обороте звёзды прыгали бы на 57px. В оформлении этого не видно: там
     всего два числа, а разворачивает список по слоям уже браузер. */
  from{background-position:0 0, 0 0, 0 0, 0 0, 0 0, 0 0, 0 0, 0 0}
  to{background-position:0 -64px, 0 -64px, 0 -64px, 0 -64px, 0 -64px,
     0 -120px, 0 -120px, 0 -120px}
}
.car-adult[data-anim="snow"]::before{
  content:'';position:absolute;inset:0;pointer-events:none;
  border-radius:inherit;
  background-image:
    /* ближний слой: 64px, мелкие частые точки */
    radial-gradient(1.4px 1.4px at 8px 12px,#fff,transparent 60%),
    radial-gradient(1.1px 1.1px at 30px 40px,rgba(220,238,255,.9),transparent 60%),
    radial-gradient(1.6px 1.6px at 52px 22px,rgba(255,255,255,.85),transparent 60%),
    radial-gradient(1px 1px at 44px 56px,rgba(196,222,255,.85),transparent 60%),
    radial-gradient(1.3px 1.3px at 18px 54px,#fff,transparent 60%),
    /* дальний слой: 120px, крупнее и реже */
    radial-gradient(2.4px 2.4px at 24px 30px,rgba(255,255,255,.75),transparent 60%),
    radial-gradient(2px 2px at 88px 82px,rgba(214,236,255,.7),transparent 60%),
    radial-gradient(2.6px 2.6px at 62px 104px,rgba(255,255,255,.65),transparent 60%);
  background-size:64px 64px,64px 64px,64px 64px,64px 64px,64px 64px,
                  120px 120px,120px 120px,120px 120px;
  animation:adSnow 16s linear infinite;
}
/* Движение частиц выключено — гасим слой совсем, а не «оставляем стоять»:
   неподвижные точки читались бы как грязь на экране. */
.car-adult[data-anim="none"]::before{display:none}

/* Листать закрытые кадры незачем, и число их показывать не нужно: стрелки и
   точки у такого товара убираем совсем. */
.car-card[data-adult="1"] .car-nav,
.car-card[data-adult="1"] .car-fdots{display:none}
/* Название скрываем, а не стираем: место под две строки остаётся занятым, и
   карточки в ленте не начинают прыгать по высоте. */
.car-card[data-adult="1"] .car-name{visibility:hidden}

/* Плашка «18+» в строке под ценой — владелец просил именно плашку.
   Цвет берётся у тематики 18+ (см. блок значков у .car-adult-mark), чтобы
   плашка на заглушке и плашка под ценой не разъезжались по цвету. */
.car-adult-tag{
  display:inline-block;margin-top:6px;
  padding:3px 11px;border-radius:100px;
  font-size:.74rem;font-weight:800;letter-spacing:.08em;
  color:var(--adult-ink,#fff);
  background:linear-gradient(135deg,var(--adult-a,#e0356b),var(--adult-b,#8b1a3c));
  box-shadow:0 4px 14px rgba(224,53,107,.35);
}

/* Лейбл в углу: «Хит продаж», «Скидка», «Новинка». Вид задаёт владелец,
   цвет берётся по виду — так подпись читается сразу, без пояснений. */
.car-badge{
  position:absolute;top:12px;left:12px;z-index:2;
  padding:6px 12px;border-radius:100px;
  font-size:.72rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
  color:#04121a;background:var(--cyan);box-shadow:0 6px 18px rgba(0,0,0,.35);
}
.car-badge[data-kind="sale"]{background:var(--magenta);color:#1a0416}
.car-badge[data-kind="promo"]{background:var(--violet);color:#0b0418}
.car-badge[data-kind="new"]{background:var(--lime);color:#04180e}
/* «Хит продаж» — единственный вид, у которого раньше не было своего цвета:
   он попадал в общий голубой, как «Новинка» и своя надпись сразу. Панель
   предлагает его в списке, значит цвет обязан отличать его от соседей. */
.car-badge[data-kind="hit"]{background:#ffb454;color:#241300}
/* «Своя надпись» — единственный вид, цвет которого задаёт владелец: он сам
   решает, что там написано, значит и цвет должен быть его. Цвет букв
   приходит отдельной переменной: его считает js/main.js по яркости фона,
   иначе на светлом лейбле оказались бы светлые буквы и надпись пропала. */
.car-badge[data-kind="custom"]{
  background:var(--shop-badge,rgba(255,255,255,.86));
  color:var(--shop-badge-ink,#0a1018);
}

/* position:relative — опора для наложения, которым гаснет подпись соседа
   (см. выше, [data-cover] .car-body::after).

   Подложки здесь больше нет: она снова на карточке, у всех карточек
   сразу (см. .car-card выше). Держать её ещё и тут нельзя — градиент
   начинался бы заново от верха подписи, и посередине карточки появлялся
   бы шов: сверху тёмный низ карточного градиента, снизу — его светлый
   верх. Пока подложка была только здесь, шва не было видно лишь потому,
   что у задних карточек не было своей. */
.car-body{
  position:relative;padding:16px 12px 20px;border-top:1px solid var(--line);
  background:transparent;
}
/* Цвета задаёт владелец в панели («Товары» → «Цвета товара»); они приезжают
   переменными на секцию магазина. Пока владелец их не задал, переменной нет
   и работает второе значение в var() — цвет из оформления. */
/* min-height — не украшение, а выравнивание. Название занимает одну или
   две строки в зависимости от длины, и блок подписи выходил разной высоты.
   Картинка над ним тянется на остаток (flex:1), поэтому у товара с
   коротким названием фотография была выше, чем у соседа с длинным, — и в
   ленте кадры «плавали» вверх-вниз. Держим место под две строки всегда:
   2 строки × 1.32 (line-height) = 2.64em. */
.car-name{
  margin:0 0 10px;font-size:1rem;font-weight:700;line-height:1.32;
  min-height:2.64em;
  letter-spacing:-.01em;color:var(--shop-name,var(--ink));
  transition:color .35s var(--ease);
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
}
/* Наведение меняет цвет названия — это и есть «подсветка» карточки словами. */
.car-card:hover .car-name{color:var(--shop-name-hover,var(--cyan))}
/* position:relative нужен значку у цены: его пояснение (вид «Пояснение с
   расчётом») лежит здесь же, но вне потока — оно всплывает над строкой цен
   и не должно толкать ни цену, ни название.

   Ширина строки — не украшение, а место под плашку, и считалась она
   замером (probe-badge-fit.js) на данных владельца. Строка тянется на 6px
   ПРАВЕЕ текста карточки (`margin-right`), а сам текст ужат до 12px по
   бокам: у владельца подпись «КУПИ пока ВЫГОДНО!!!» шириной 143px, и рядом
   с «2 800 ₽ 3 500 ₽» (112px + два зазора) ей нужно 271px. Было 264px —
   не хватало 7px, и плашка уезжала вниз. Стало 278px, запас 7px.

   Влево строку НЕ расширяем: цена обязана стоять в одной колонке с
   названием товара, иначе 6px расхождения видно глазом. Вправо можно:
   там плашка — скруглённая таблетка, и 6px до кромки карточки читаются
   нормально. */
.car-prices{
  display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;position:relative;
  margin-right:-6px;
}
.car-price{font-size:1.16rem;font-weight:800;color:var(--shop-price,#fff);letter-spacing:-.02em}
.car-old{font-size:.9rem;color:var(--ink-3);text-decoration:line-through}
.car-out{font-size:.78rem;color:var(--ink-3);margin-left:auto}

/* Остаток в штуках — «в наличии: 3 штуки».

   Показывается только если владелец разрешил это у конкретного товара;
   обычно числа на карточке нет вовсе (владелец просил не показывать
   покупателю остаток). Отдельной строкой ПОД ценой, а не хвостом в строке
   цен: та строка рассчитана на определённую ширину (цена, старая цена и
   плашка у правого края), и лишний хвост ломал бы расчёт — плашка уезжала
   бы на свою строку там, где раньше помещалась. Приглушённо и мелко:
   это подсказка, а не второе название. */
.car-stock{
  /* flex, а не block: у плашки бывает значок, и он обязан стоять на одной
     строке с надписью, а не над ней. Для надписи без значка это ровно та же
     строка — текст занимает всю ширину.

     ПО ЦЕНТРУ, а не у левого края — так просил владелец: «В наличии сделать
     по центру внизу, а не по левому краю как это сейчас». Одним свойством
     этого не сделать, и вот почему: у вида «обычный» ширины нет вовсе, и
     строка тянется через всю карточку — её содержимое центрирует
     `justify-content`. У плашек (`pill`, `outline`, `glow`, `ribbon`) ширина
     обнимает надпись, центрировать внутри нечего — такую плашку ставит по
     центру пара `margin-left/right:auto`. Пишем оба: какое из двух сработает,
     решает вид, а результат один. */
  display:flex;align-items:center;justify-content:center;gap:6px;margin-top:4px;
  width:fit-content;margin-left:auto;margin-right:auto;
  font-size:.76rem;font-weight:600;color:var(--stk-c,var(--ink-3));
  letter-spacing:-.01em;
}
/* Атрибут `hidden` отменяется своим `display` — та же ловушка, что у
   `.pm-stock` ниже. Пока правило не написано, спрятанная строка оставалась
   бы на экране пустой полоской. */
.car-stock[hidden]{display:none}

/* Размер строки остатка и её анимация — настройки КАЖДОГО товара, поэтому
   виды живут атрибутами, а не классами: добавить вид можно, не трогая
   скрипт. «Обычный» размер правила не имеет — это то, что стоит в
   .car-stock и .pm-stock выше: у карточки и у окна товара свои исходные
   кегли, и общая лестница их бы сломала.

   Анимации — только то, что читается как «посмотрите сюда», и ни одна не
   трогает `color`: цвет владелец задаёт отдельной настройкой, и анимация,
   перебивающая его, была бы обманом. Свечение берёт цвет из --stk-glow —
   то есть из того же выбора. */
.car-stock[data-stock-size="s"],.pm-stock[data-stock-size="s"]{font-size:.66rem}
.car-stock[data-stock-size="l"],.pm-stock[data-stock-size="l"]{font-size:.9rem}
.car-stock[data-stock-size="xl"],.pm-stock[data-stock-size="xl"]{font-size:1.06rem}

.car-stock[data-stock-anim="pulse"],.pm-stock[data-stock-anim="pulse"]{
  animation:stkPulse 1.5s ease-in-out infinite}
.car-stock[data-stock-anim="blink"],.pm-stock[data-stock-anim="blink"]{
  animation:stkBlink 1.7s ease-in-out infinite}
.car-stock[data-stock-anim="float"],.pm-stock[data-stock-anim="float"]{
  animation:stkFloat 2.1s ease-in-out infinite}
.car-stock[data-stock-anim="glow"],.pm-stock[data-stock-anim="glow"]{
  animation:stkGlow 1.9s ease-in-out infinite}
.car-stock[data-stock-anim="breathe"],.pm-stock[data-stock-anim="breathe"]{
  animation:stkBreathe 2.6s ease-in-out infinite}

/* Масштаб, а не размер шрифта: строку никто не измеряет, а пересчёт кегля
   заставлял бы браузер перекладывать строку на каждом кадре. */
@keyframes stkPulse{
  0%,100%{transform:scale(1)}
  50%{transform:scale(1.07)}
}
@keyframes stkBlink{
  0%,100%{opacity:1}
  50%{opacity:.4}
}
@keyframes stkFloat{
  0%,100%{transform:translateY(0)}
  50%{transform:translateY(-3px)}
}
@keyframes stkGlow{
  0%,100%{text-shadow:0 0 0 transparent}
  50%{text-shadow:0 0 8px var(--stk-glow,rgba(255,255,255,.5))}
}
@keyframes stkBreathe{
  0%,100%{transform:scale(1);opacity:.92}
  50%{transform:scale(1.04);opacity:1}
}

/* Вид плашки «в наличии». Владелец просил не просто строку, а свою плашку:
   «включать и выключать видимость у каждого товара, править текст, много
   эффектов и анимаций, цвета, иконки».

   У вида `plain` правил НЕТ ВООБЩЕ, и это не упущение: `plain` — это ровно
   то, что было до настройки. Товары, заведённые раньше, выглядят как
   выглядели, и заводить вид заново им не нужно.

   `width:fit-content` у плашек — чтобы подложка обнимала надпись, а не
   тянулась через всю карточку. `align-self` тут не годится: родитель бывает
   и не flex-полосой, а тогда выравнивание просто не сработало бы. */
.car-stock[data-stock-badge="pill"],.pm-stock[data-stock-badge="pill"]{
  width:fit-content;max-width:100%;
  padding:4px 11px;border-radius:999px;
  background:var(--stk-bg,rgba(120,160,220,.14));
}
.car-stock[data-stock-badge="outline"],.pm-stock[data-stock-badge="outline"]{
  width:fit-content;max-width:100%;
  padding:4px 11px;border-radius:999px;
  border:1px solid var(--stk-line,rgba(120,160,220,.35));
}
.car-stock[data-stock-badge="glow"],.pm-stock[data-stock-badge="glow"]{
  width:fit-content;max-width:100%;
  padding:4px 11px;border-radius:999px;
  background:var(--stk-bg,rgba(120,160,220,.14));
  box-shadow:0 0 14px var(--stk-glow,rgba(120,160,220,.35));
}
.car-stock[data-stock-badge="ribbon"],.pm-stock[data-stock-badge="ribbon"]{
  width:fit-content;max-width:100%;
  padding:4px 12px 4px 9px;
  border-radius:4px 10px 10px 4px;
  border-left:3px solid var(--stk-c,rgba(120,160,220,.5));
  background:var(--stk-bg,rgba(120,160,220,.14));
}

/* Значок строки. Размер ступенями вместе с надписью: значок в 14px при
   кегле в 17px выглядел бы точкой, а при 10px — глыбой. */
.car-stock-ic{flex:none;width:14px;height:14px}
.car-stock[data-stock-size="s"] .car-stock-ic,.pm-stock[data-stock-size="s"] .car-stock-ic{
  width:12px;height:12px}
.car-stock[data-stock-size="l"] .car-stock-ic,.pm-stock[data-stock-size="l"] .car-stock-ic{
  width:16px;height:16px}
.car-stock[data-stock-size="xl"] .car-stock-ic,.pm-stock[data-stock-size="xl"] .car-stock-ic{
  width:18px;height:18px}

/* Блик идёт поверх надписи, поэтому строке нужен свой слой отсечения.
   `pointer-events:none` у полосы блика — иначе она перехватывала бы нажатие
   по карточке, и товар перестал бы открываться в тех местах, где прошёл
   блик. */
.car-stock[data-stock-anim="shine"],.pm-stock[data-stock-anim="shine"]{
  position:relative;overflow:hidden}
.car-stock[data-stock-anim="shine"]::after,.pm-stock[data-stock-anim="shine"]::after{
  content:'';position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(105deg,transparent 32%,rgba(255,255,255,.5) 50%,transparent 68%);
  animation:stkShine 2.6s ease-in-out infinite;
}
.car-stock[data-stock-anim="pop"],.pm-stock[data-stock-anim="pop"]{
  animation:stkPop 1.9s ease-in-out infinite}
.car-stock[data-stock-anim="swing"],.pm-stock[data-stock-anim="swing"]{
  transform-origin:left center;
  animation:stkSwing 2.2s ease-in-out infinite}

@keyframes stkShine{
  0%{transform:translateX(-110%)}
  55%,100%{transform:translateX(110%)}
}
/* Подпрыгивание с паузой: движение занимает меньше трети цикла, остальное
   строка стоит. Непрерывный подскок читался бы как дрожь и мешал бы
   читать цену рядом. */
@keyframes stkPop{
  0%,70%,100%{transform:translateY(0)}
  80%{transform:translateY(-5px)}
  90%{transform:translateY(0)}
}
@keyframes stkSwing{
  0%,100%{transform:rotate(0)}
  25%{transform:rotate(-2.2deg)}
  75%{transform:rotate(2.2deg)}
}

/* Кому анимации мешают — тому их не показываем. Тот же уговор, что и у
   остальных движений сайта.

   `::after` здесь не для полноты: полосу блика рисует псевдоэлемент, и
   правило только про сам элемент оставляло бы блик бегать по строке у того,
   кто просил движений не показывать. */
@media (prefers-reduced-motion:reduce){
  .car-stock[data-stock-anim],.pm-stock[data-stock-anim]{animation:none}
  .car-stock[data-stock-anim]::after,.pm-stock[data-stock-anim]::after{
    animation:none;opacity:0}
}

/* ТАЙМЕР АКЦИИ — на карточке товара и в окне товара.

   Владелец: «красивый таймер обратного отсчёта на карточке товара с
   эффектами... когда таймер кончился — цена меняется на другую». Стоит своей
   строкой ПОД ценой, как и остаток: строка цен рассчитана на определённую
   ширину (цена, старая цена и плашка у правого края), и таймер ломал бы этот
   расчёт у всех товаров сразу.

   Вид, размер и движение — атрибутами, как у плашки остатка: новый вид
   добавляется здесь, а не в скрипте. Цвет берётся из ОБЩЕЙ палитры
   (`--stk-c`, `--stk-bg`, `--stk-line`, `--stk-glow`), поэтому одной
   настройкой владелец красит и цифры, и подложку, и полосу — заведи мы
   таймеру свои имена, половина оформления осталась бы серой. */
.car-promo{
  display:flex;align-items:center;gap:7px;flex-wrap:wrap;
  /* По центру колонки, а не у левого края. Просил владелец про окно товара
     («распродажу сделать по центру, а не как сейчас по левому краю»), но
     правило одно на оба места: на карточке полоса и так занимает всю ширину
     (полосатая дорожка внутри тянется на остаток), и центрировать там
     нечего — а в окне она уже колонки, и без этих двух auto липла бы влево.
     Разное поведение в двух местах у одного и того же вида было бы хуже:
     владелец настраивает его один раз. */
  width:fit-content;max-width:100%;
  margin-top:6px;margin-left:auto;margin-right:auto;
  padding:5px 11px;border-radius:10px;
  background:var(--stk-bg,rgba(120,160,220,.14));
  box-shadow:inset 0 0 0 1px var(--stk-line,rgba(120,160,220,.35));
  color:var(--stk-c,var(--ink-2));
  font-size:.78rem;font-weight:600;letter-spacing:-.01em;
}
/* В окне товара строка шире и кегль у соседей крупнее — таймер не должен
   выглядеть вставкой из другого места. Правило стоит ДО лестницы размеров:
   иначе оно перебивало бы выбор владельца — при равной точности выигрывает
   то, что ниже. */
.pm-info .car-promo{font-size:.84rem}

.car-promo-cap{white-space:nowrap}
.car-promo-ic{flex:none;width:14px;height:14px}

/* Цифры. `tabular-nums` здесь не украшение: без него «11» уже, чем «08», и
   строка подрагивает раз в секунду, пока идёт отсчёт.

   `flex-wrap` у САМИХ ЯЧЕЕК — не украшение и не запас на будущее. На узкой
   карточке четыре ячейки («01 дн», «01 ч», «59 мин», «37 сек») вместе шире
   карточки, а до этой правки строка была `nowrap` и не могла ни сжаться,
   ни перенестись: замер на 390px — карточка 193px, плашка 169px, а одни
   только ячейки 174px, и последняя («сек») срезалась краем карточки.
   Перенос разрешён ПО ЯЧЕЙКАМ: пара «01 дн» остаётся парой — этого и просил
   владелец («чтобы был в один ряд»), а вот четыре пары в один ряд не влезают
   и не обязаны. */
.car-promo-cells{display:flex;align-items:center;gap:5px;
  flex-wrap:wrap;justify-content:center;max-width:100%}
/* Цифра и подпись — В ОДНУ СТРОКУ: «01 дн», а не «01» над «дн».
   Владелец (29.09, снимок): «надо сделать тут чтобы был в один ряд».
   Было `flex-direction:column`, и плитка отсчёта занимала две строки —
   на узкой карточке вся плашка уезжала на три строки. `white-space:nowrap`
   держит пару вместе, а `min-width` убран: он заставлял ячейку быть шире
   содержимого и разгонял строку. */
.car-promo-cell{
  display:flex;flex-direction:row;align-items:baseline;justify-content:center;
  gap:.28em;white-space:nowrap;
  padding:3px 6px;border-radius:8px;
  background:var(--stk-bg,rgba(120,160,220,.14));
}
/* Пустые дни уходят атрибутом — «00 дн 04:12:33» читается как сбой, а не
   как срок. Без этого правила `display:flex` отменил бы `hidden`, и пустая
   ячейка осталась бы на экране (та же ловушка, что у `.car-stock`). */
.car-promo-cell[hidden]{display:none}
.car-promo-cell b{
  font-size:1.32em;line-height:1.05;font-weight:700;
  font-variant-numeric:tabular-nums;color:var(--stk-c,var(--ink-2));
}
.car-promo-cell i{font-style:normal;font-size:.72em;opacity:.72;letter-spacing:.02em}

.car-promo-text{
  font-variant-numeric:tabular-nums;font-weight:700;
  color:var(--stk-c,var(--ink-2));
}

/* Вид «Кружки»: цифра в круге, подпись — под ним. Круг рисует сама цифра, а
   не ячейка: иначе подпись оказалась бы внутри круга и места цифре не
   осталось бы. */
.car-promo[data-promo-style="round"] .car-promo-cell{
  background:none;padding:0;gap:2px;min-width:0}
.car-promo[data-promo-style="round"] .car-promo-cell b{
  width:2.2em;height:2.2em;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  background:var(--stk-bg,rgba(120,160,220,.14));
  box-shadow:inset 0 0 0 1px var(--stk-line,rgba(120,160,220,.35));
}

/* Вид «Полоса»: остаток показывается полоской, которая утекает в последние
   сутки. Общий срок знает только владелец, и обещать его покупателю мы не
   вправе — поэтому полоса идёт не «от начала акции», а от последних суток. */
.car-promo-track{
  position:relative;flex:1 1 90px;min-width:56px;height:5px;
  border-radius:999px;overflow:hidden;
  background:var(--stk-bg,rgba(120,160,220,.14));
}
.car-promo-fill{
  position:absolute;top:0;bottom:0;left:0;width:100%;
  border-radius:999px;background:var(--stk-c,rgba(120,160,220,.5));
  transition:width .9s linear;
}

/* Вид «Тонкая строка»: ни подложки, ни рамки — только надпись и цифры.
   Для тех, кто хочет отсчёт, но не хочет второй плашки на карточке. */
.car-promo[data-promo-style="line"]{
  background:none;box-shadow:none;padding:0;border-radius:0}

/* Размер — настройка каждого товара, поэтому виды живут атрибутами.
   «Обычный» правила не имеет: это то, что стоит в `.car-promo` выше. */
.car-promo[data-promo-size="s"]{font-size:.68rem}
.car-promo[data-promo-size="l"]{font-size:.9rem}
.car-promo[data-promo-size="xl"]{font-size:1.02rem}

/* Движения — ТЕ ЖЕ кадры, что у плашки остатка: одна лестница движений на
   весь магазин, чтобы «пульсация» везде значила одно и то же. Ни одно из
   них не трогает `color`: цвет владелец задаёт отдельной настройкой, и
   анимация, перебивающая его, была бы обманом. */
.car-promo[data-promo-anim="pulse"]{animation:stkPulse 1.5s ease-in-out infinite}
.car-promo[data-promo-anim="blink"]{animation:stkBlink 1.7s ease-in-out infinite}
.car-promo[data-promo-anim="float"]{animation:stkFloat 2.1s ease-in-out infinite}
.car-promo[data-promo-anim="glow"]{animation:stkGlow 1.9s ease-in-out infinite}
.car-promo[data-promo-anim="breathe"]{animation:stkBreathe 2.6s ease-in-out infinite}
.car-promo[data-promo-anim="pop"]{animation:stkPop 1.9s ease-in-out infinite}
.car-promo[data-promo-anim="swing"]{
  transform-origin:left center;animation:stkSwing 2.2s ease-in-out infinite}

/* Блик идёт поверх цифр, поэтому таймеру нужен свой слой отсечения, а полосе
   блика — `pointer-events:none`: без него она перехватывала бы нажатие по
   карточке, и товар перестал бы открываться там, где прошёл блик. */
.car-promo[data-promo-anim="shine"]{position:relative;overflow:hidden}
.car-promo[data-promo-anim="shine"]::after{
  content:'';position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(105deg,transparent 32%,rgba(255,255,255,.5) 50%,transparent 68%);
  animation:stkShine 2.6s ease-in-out infinite;
}

/* Кому движения мешают — тому их не показываем. Полоса остатка гасится
   отдельно: её ширина меняется переходом, и без этого правило только про
   анимацию оставило бы ползущую полоску. */
@media (prefers-reduced-motion:reduce){
  .car-promo[data-promo-anim]{animation:none}
  .car-promo[data-promo-anim]::after{animation:none;opacity:0}
  .car-promo-fill{transition:none}
}

/* Значок у ЦЕНЫ: «Ваша выгода 23%», «СКИДКА», «Экономия 510 ₽».

   Стоит СПРАВА от цены, у правого края строки цен — владелец обвёл это
   место красной рамкой и попросил перенести туда плашку: под ценой она
   читалась отдельной строкой и выглядела припиской к карточке, а не
   пометкой к цене. Перенос работает и при переносе строки (flex-wrap:wrap):
   на своей строке значок тоже прижат вправо.

   Подпись, значок, анимацию, цвет, размер и поведение владелец выбирает в
   панели, и выбор свой у КАЖДОГО товара. Наружу уходят идентификаторы, а
   сами цвета и виды живут здесь — как у подсветки карточки и у кружка
   корзины. Числа подставляет скрипт из готовых, посчитанных сервером:
   подпись обязана следовать из самих цен.

   Цвет по умолчанию — тёплый, янтарный: цена и старая цена уже заняты
   цветом магазина и серым, и значку надо отличаться от обеих, иначе он
   читается как часть цены. Приходит он не прямо, а переменными: палитра
   владельца задаёт их ниже, а свой цвет скрипт ставит прямо на плашке. */
.car-deal{
  display:inline-flex;align-items:center;gap:4px;
  /* Правый край строки цен. Отметка «под заказ» тоже просит правый край —
     как два auto делят строку, написано ниже, у правила для неё. */
  margin-left:auto;
  /* Размер «обычный» — теснее прежнего (.76rem / 3px 9px / рисунок 13px).
     Плашка обязана стоять В СТРОКЕ с ценой, а не под ней, и каждый
     сэкономленный здесь пиксель ширины — это пиксель, которого не хватало
     строке. Крупнее владелец ставит сам размером s/m/l/xl. */
  padding:2px 7px;border-radius:999px;
  font-size:.72rem;font-weight:800;line-height:1.2;letter-spacing:-.02em;
  white-space:nowrap;
  color:var(--deal-ink,#20160a);
  background:var(--deal-bg,linear-gradient(135deg,#ffd166,#ff9f45));
  box-shadow:0 2px 10px -1px var(--deal-glow,rgba(255,159,69,.45));
}
/* Плашка, которой НЕ хватило места рядом с ценой (очень крупный размер или
   длинная подпись), уходит на свою строку и встаёт ПО ЦЕНТРУ — так просил
   владелец. Правый край для неё — плохой вариант: одиночная плашка у
   правой кромки читается как случайно брошенная.

   Двумя auto сразу не обойтись: `margin-left:auto` нужен и в строке с
   ценой (там он прижимает плашку вправо), а два auto растащили бы её
   посередине и в первой строке тоже. Поэтому вид строки отмечает скрипт
   атрибутом `data-deal-own="1"` — из оформления «стою ли я на своей
   строке» не видно.

   Отметка стоит на СТРОКЕ, а не на плашке: в окне товара плашку держит
   слот `.pm-deal`, и вправо-влево двигается он, а не она. */
.car-prices[data-deal-own="1"] .car-deal{margin-left:auto;margin-right:auto}
/* А в ОКНЕ ТОВАРА значок на своей строке прижат ВЛЕВО — под цену.

   Владелец (06.10) обвёл его на снимке и попросил «немного сдвинуть к
   ценам». По центру он читался как отдельная от цены вещь; под ценой видно,
   что это её пояснение. На карточке правило прежнее — по центру: о центре
   он просил отдельно и про карточку, и это была другая просьба. Разделил
   их, а не поменял общее: одно правило на двоих снова свело бы два разных
   пожелания в одно. */
.pm-prices[data-deal-own="1"] .pm-deal{margin-left:0;margin-right:auto}
/* Перенос строки перед «очень крупной» плашкой. Ставит его скрипт
   (`markDealLine`), когда владелец выбрал самый большой размер: подпись
   может быть короткой и формально влезла бы рядом с ценой, но размер выбран
   осознанно, и плашка обязана встать внизу по центру.

   Узел занимает ВСЮ ширину строки и не имеет высоты: сам он не виден, а
   строку переносит — дальше плашка оказывается на своей строке одна, и её
   центрирует правило выше. Одной отметки `data-deal-own` для этого мало:
   пока плашка делит строку с ценой, `margin-left:auto` просто прижимает её
   к правому краю. */
.deal-br{flex:0 0 100%;height:0;margin:0;padding:0;border:0;overflow:hidden}
/* Отметка «под заказ» уводит себя к правому краю (margin-left:auto). Два
   auto в одной строке делят свободное место ПОРОВНУ — и значок оказался бы
   посередине строки, между ценой и отметкой. Поэтому у отметки, которая
   идёт после значка, отступ снят: её место определяет сам значок. Когда
   значка нет, отметка по-прежнему у правого края. */
.car-deal ~ .car-out{margin-left:0}
.car-deal svg{width:12px;height:12px;flex:none}

/* Палитра плашки, лупы, строки остатка, таймера акции, стрелок листания,
   отзывов и «Доставки и оплаты» — ОДИН список на семь настроек: владелец
   просил и там и там «цвет на выбор или свой». Наружу из панели уходит
   идентификатор, а сами цвета живут здесь, поэтому один набор значений
   описан одним правилом на все селекторы — как у подсветки карточки. Свой
   цвет скрипт ставит теми же переменными прямо на элементе: `--deal-bg`/
   `--deal-ink`/`--deal-glow` для плашки, `--zoom-c`/`--zoom-ink` для лупы,
   `--stk-c`/`--stk-ink`/`--stk-glow`/`--stk-bg`/`--stk-line` для строки
   остатка, таймера, стрелок, отзывов и «Доставки и оплаты».

   Просмотр кадра в полный рост (07.10) — ещё один потребитель этой же
   палитры, и своих восьми цветов у него НЕТ намеренно: список разошёлся бы с
   остальными на первой же правке. Атрибут у него свой (`data-full-color`), но
   перечислен он в тех же правилах ниже — рядом с `data-deal-color` и
   `data-arrow-color`. Значки-лупа, стрелки просмотра, счётчик и крестик
   закрытия берут из палитры те же имена переменных, что и всё остальное.

   Артикул товара (10.10) — тот же случай и по той же причине: атрибут свой
   (`data-sku-color`), а цвета — из этого же списка, рядом со строкой остатка.
   Свои восемь цветов завели бы девятый список, который разошёлся бы с
   первыми восемью на первой же новой краске.

   Таймер, стрелки, отзывы и доставка берут те же имена, что строка
   остатка, и это не путаница: цвет владелец задаёт КАЖДОЙ настройке свою,
   а переменные ставятся на сам элемент, поэтому общие имена никого ни к
   чему не обязывают. Заведи мы таймеру и стрелкам отдельные
   `--promo-*`/`--arr-*` цвета — пришлось бы дублировать весь список восьми
   цветов, и первый же новый цвет разошёлся бы в четырёх местах.

   Отзывы и «Доставка и оплата» — единственные настройки этой палитры,
   которые живут в content.json, а не в файле магазина: их владелец правит
   вместе с остальным содержимым страницы. Значит, и список цветов обязан
   быть известен СХЕМЕ (js/schema.js, REVIEW_COLORS), а не серверу: иначе
   панель нарисовала бы пустой выпадающий список. Значения там ровно эти
   же.

   У «Доставки и оплаты» атрибут цвета стоит И на разделе, и на отдельном
   пункте — так владелец красит пункты по-разному. Работает это не
   каскадом, а НАСЛЕДОВАНИЕМ: пункт со своим цветом получает переменные
   своим правилом и перебивает унаследованное от раздела; пункт со
   значением «как у раздела» атрибута не имеет вовсе и наследует цвет
   раздела. Порядок правил в файле тут ни при чём — важно, у кого
   объявлено. */
[data-deal-color="amber"],
[data-full-color="amber"],
.pm-shot[data-zoom-color="amber"],
[data-stock-color="amber"],
[data-sku-color="amber"],
[data-promo-color="amber"],
[data-arrow-color="amber"],
[data-dots-color="amber"],
[data-grid-color="amber"],
[data-row-color="amber"],
[data-review-color="amber"],
[data-ship-color="amber"],
[data-bonus-color="amber"]{
  --deal-bg:linear-gradient(135deg,#ffd166,#ff9f45);--deal-ink:#20160a;
  --deal-glow:rgba(255,159,69,.45);--zoom-c:#ffb454;--zoom-ink:#241300;
  --stk-c:#ffb454;--stk-glow:rgba(255,159,69,.5);
  --stk-bg:rgba(255,159,69,.16);--stk-line:rgba(255,159,69,.5);
  --zoom-fill:rgba(255,159,69,.3);
}
[data-deal-color="gold"],
[data-full-color="gold"],
.pm-shot[data-zoom-color="gold"],
[data-stock-color="gold"],
[data-sku-color="gold"],
[data-promo-color="gold"],
[data-arrow-color="gold"],
[data-dots-color="gold"],
[data-grid-color="gold"],
[data-row-color="gold"],
[data-review-color="gold"],
[data-ship-color="gold"],
[data-bonus-color="gold"]{
  --deal-bg:linear-gradient(135deg,#ffe082,#ffb300);--deal-ink:#241a02;
  --deal-glow:rgba(255,179,0,.45);--zoom-c:#ffca28;--zoom-ink:#241a02;
  --stk-c:#ffca28;--stk-glow:rgba(255,179,0,.5);
  --stk-bg:rgba(255,179,0,.16);--stk-line:rgba(255,179,0,.5);
  --zoom-fill:rgba(255,179,0,.3);
}
[data-deal-color="fire"],
[data-full-color="fire"],
.pm-shot[data-zoom-color="fire"],
[data-stock-color="fire"],
[data-sku-color="fire"],
[data-promo-color="fire"],
[data-arrow-color="fire"],
[data-dots-color="fire"],
[data-grid-color="fire"],
[data-row-color="fire"],
[data-review-color="fire"],
[data-ship-color="fire"],
[data-bonus-color="fire"]{
  --deal-bg:linear-gradient(135deg,#ff8a5c,#ff3d3d);--deal-ink:#2b0705;
  --deal-glow:rgba(255,61,61,.5);--zoom-c:#ff6b4a;--zoom-ink:#2b0705;
  --stk-c:#ff6b4a;--stk-glow:rgba(255,61,61,.55);
  --stk-bg:rgba(255,61,61,.18);--stk-line:rgba(255,61,61,.5);
  --zoom-fill:rgba(255,61,61,.3);
}
[data-deal-color="berry"],
[data-full-color="berry"],
.pm-shot[data-zoom-color="berry"],
[data-stock-color="berry"],
[data-sku-color="berry"],
[data-promo-color="berry"],
[data-arrow-color="berry"],
[data-dots-color="berry"],
[data-grid-color="berry"],
[data-row-color="berry"],
[data-review-color="berry"],
[data-ship-color="berry"],
[data-bonus-color="berry"]{
  --deal-bg:linear-gradient(135deg,#ff8ab8,#e0338f);--deal-ink:#2a0518;
  --deal-glow:rgba(224,51,143,.5);--zoom-c:#ff6fae;--zoom-ink:#2a0518;
  --stk-c:#ff6fae;--stk-glow:rgba(224,51,143,.55);
  --stk-bg:rgba(224,51,143,.18);--stk-line:rgba(224,51,143,.5);
  --zoom-fill:rgba(224,51,143,.3);
}
[data-deal-color="violet"],
[data-full-color="violet"],
.pm-shot[data-zoom-color="violet"],
[data-stock-color="violet"],
[data-sku-color="violet"],
[data-promo-color="violet"],
[data-arrow-color="violet"],
[data-dots-color="violet"],
[data-grid-color="violet"],
[data-row-color="violet"],
[data-review-color="violet"],
[data-ship-color="violet"],
[data-bonus-color="violet"]{
  --deal-bg:linear-gradient(135deg,#b98cff,#7a3ff2);--deal-ink:#180634;
  --deal-glow:rgba(122,63,242,.5);--zoom-c:#a678ff;--zoom-ink:#180634;
  --stk-c:#a678ff;--stk-glow:rgba(122,63,242,.55);
  --stk-bg:rgba(122,63,242,.2);--stk-line:rgba(122,63,242,.55);
  --zoom-fill:rgba(122,63,242,.3);
}
[data-deal-color="sky"],
[data-full-color="sky"],
.pm-shot[data-zoom-color="sky"],
[data-stock-color="sky"],
[data-sku-color="sky"],
[data-promo-color="sky"],
[data-arrow-color="sky"],
[data-dots-color="sky"],
[data-grid-color="sky"],
[data-row-color="sky"],
[data-review-color="sky"],
[data-ship-color="sky"],
[data-bonus-color="sky"]{
  --deal-bg:linear-gradient(135deg,#7fd4ff,#2196f3);--deal-ink:#03192b;
  --deal-glow:rgba(33,150,243,.5);--zoom-c:#63c7ff;--zoom-ink:#03192b;
  --stk-c:#63c7ff;--stk-glow:rgba(33,150,243,.55);
  --stk-bg:rgba(33,150,243,.18);--stk-line:rgba(33,150,243,.5);
  --zoom-fill:rgba(33,150,243,.3);
}
[data-deal-color="mint"],
[data-full-color="mint"],
.pm-shot[data-zoom-color="mint"],
[data-stock-color="mint"],
[data-sku-color="mint"],
[data-promo-color="mint"],
[data-arrow-color="mint"],
[data-dots-color="mint"],
[data-grid-color="mint"],
[data-row-color="mint"],
[data-review-color="mint"],
[data-ship-color="mint"],
[data-bonus-color="mint"]{
  --deal-bg:linear-gradient(135deg,#8ef0c4,#12b76a);--deal-ink:#032015;
  --deal-glow:rgba(18,183,106,.5);--zoom-c:#4fe0a4;--zoom-ink:#032015;
  --stk-c:#4fe0a4;--stk-glow:rgba(18,183,106,.55);
  --stk-bg:rgba(18,183,106,.18);--stk-line:rgba(18,183,106,.5);
  --zoom-fill:rgba(18,183,106,.3);
}
[data-deal-color="graph"],
[data-full-color="graph"],
.pm-shot[data-zoom-color="graph"],
[data-stock-color="graph"],
[data-sku-color="graph"],
[data-promo-color="graph"],
[data-arrow-color="graph"],
[data-dots-color="graph"],
[data-grid-color="graph"],
[data-row-color="graph"],
[data-review-color="graph"],
[data-ship-color="graph"],
[data-bonus-color="graph"]{
  --deal-bg:linear-gradient(135deg,#5b6a83,#1f2733);--deal-ink:#eaf2ff;
  --deal-glow:rgba(31,39,51,.6);--zoom-c:#8ea3c0;--zoom-ink:#0a1018;
  --stk-c:#8ea3c0;--stk-glow:rgba(142,163,192,.5);
  --stk-bg:rgba(142,163,192,.14);--stk-line:rgba(142,163,192,.4);
  --zoom-fill:rgba(142,163,192,.3);
}

/* Размер шрифта плашки — отдельная настройка владельца. Значок растёт
   вместе с буквами и отступы тоже: крупная надпись с крошечным рисунком в
   тесной плашке читалась бы как недоделка. «Обычный» правила не имеет —
   это то, что стоит в .car-deal выше.

   Лестница: .64 → .72 (обычный) → .86 → .98rem. «Мелкий» пришлось на шаг
   увести вниз, когда «обычный» сам стал теснее: иначе s и m отличались бы
   на глаз одним кеглем и настройка читалась бы как нерабочая. */
.car-deal[data-deal-size="s"]{font-size:.64rem;padding:2px 6px;gap:4px}
.car-deal[data-deal-size="s"] svg{width:11px;height:11px}
.car-deal[data-deal-size="l"]{font-size:.86rem;padding:4px 11px}
.car-deal[data-deal-size="l"] svg{width:15px;height:15px}
.car-deal[data-deal-size="xl"]{font-size:.98rem;padding:5px 13px}
.car-deal[data-deal-size="xl"] svg{width:17px;height:17px}

/* «Функционал» плашки: что она делает, кроме того что стоит у цены.
   Список закрыт на сервере (PRICE_BADGE_BEHAVIORS).

   «Просто плашка» правила не имеет — это покой, то же поведение, что было
   у значка до появления настройки.

   «Отклик на наведение» — плашка подрастает и светится. На карточке
   достаточно наведения на саму карточку: она и без того отвечает рамкой, и
   требовать попасть точно в плашку было бы придиркой. В окне товара
   карточки нет, там считаем наведение на саму плашку. */
.car-deal[data-deal-behavior="hover"]{
  transition:transform .3s var(--ease),box-shadow .3s;
}
.car-card:hover .car-deal[data-deal-behavior="hover"],
.car-deal[data-deal-behavior="hover"]:hover{
  transform:scale(1.07);
  box-shadow:0 3px 16px -1px var(--deal-glow,rgba(255,159,69,.62));
}

/* «Пояснение с расчётом»: над плашкой всплывает строка с обычной ценой,
   процентом и экономией. Числа считает сервер и присылает готовыми — та же
   причина, что и у подписи: после правки цены вписанное руками число стало
   бы неправдой. Показываем по наведению на саму плашку; текст лежит в
   элементе, а не в подсказке браузера — владелец просил нативный тултип
   убрать ещё на кнопках связи. */
.car-deal-tip{
  position:absolute;right:0;bottom:calc(100% + 8px);z-index:6;
  max-width:min(280px,100%);padding:6px 10px;border-radius:10px;
  font-size:.72rem;font-weight:700;line-height:1.35;
  color:#0b1018;background:#eaf2ff;
  box-shadow:0 8px 24px rgba(0,0,0,.45);
  opacity:0;transform:translateY(4px);pointer-events:none;
  transition:opacity .22s var(--ease),transform .22s var(--ease);
}
.car-prices:has(.car-deal:hover) .car-deal-tip{
  opacity:1;transform:translateY(0);
}

/* Анимации значка. Список закрыт на сервере (PRICE_BADGE_ANIMS), поэтому
   здесь по одному правилу на вид; «без анимации» правила не имеет — это и
   есть покой.

   Двигается значок ТОЛЬКО у центральной карточки. В ленте одновременно
   видно несколько карточек, и значки, мигающие у всех сразу, читались бы
   как рябь, а не как приглашение. Соседние карточки и без того приглушены
   затемнением подложки — им движение ни к чему.

   Из этого правила есть три исключения, и каждое названо в списках панели:
   вид «Движется у всех карточек» (владелец вправе решить иначе) и окно
   товара — там карточка одна, и «только у центральной» означало бы
   «не двигается никогда». Поэтому у каждого правила по три селектора.

   Значок есть только у товаров: у работ цены нет, поэтому селектора ленты
   работ здесь нет, в отличие от подсветки и движения. */
@keyframes dealPulse{
  0%,100%{transform:scale(1)}
  50%    {transform:scale(1.06)}
}
.car-card[data-active="1"] .car-deal[data-deal-anim="pulse"],
.car-deal[data-deal-behavior="every"][data-deal-anim="pulse"],
.pm-deal .car-deal[data-deal-anim="pulse"]{
  animation:dealPulse 2.2s ease-in-out infinite;
}

@keyframes dealFloat{
  0%,100%{transform:translateY(0)}
  50%    {transform:translateY(-3px)}
}
.car-card[data-active="1"] .car-deal[data-deal-anim="float"],
.car-deal[data-deal-behavior="every"][data-deal-anim="float"],
.pm-deal .car-deal[data-deal-anim="float"]{
  animation:dealFloat 2.6s ease-in-out infinite;
}

@keyframes dealPop{
  0%,100%{transform:translateY(0) scale(1)}
  35%    {transform:translateY(-4px) scale(1.05)}
  70%    {transform:translateY(0) scale(1)}
}
.car-card[data-active="1"] .car-deal[data-deal-anim="pop"],
.car-deal[data-deal-behavior="every"][data-deal-anim="pop"],
.pm-deal .car-deal[data-deal-anim="pop"]{
  animation:dealPop 2.4s ease-in-out infinite;
}

@keyframes dealGlow{
  0%,100%{box-shadow:0 2px 10px -1px var(--deal-glow,rgba(255,159,69,.45))}
  50%    {box-shadow:0 2px 18px 3px var(--deal-glow,rgba(255,159,69,.7))}
}
.car-card[data-active="1"] .car-deal[data-deal-anim="glow"],
.car-deal[data-deal-behavior="every"][data-deal-anim="glow"],
.pm-deal .car-deal[data-deal-anim="glow"]{
  animation:dealGlow 2.4s ease-in-out infinite;
}

/* «Блик»: по значку пробегает светлая полоса. Полоса — наложение ПОВЕРХ
   значка, поэтому у значка появляется обрезка по скруглению. Сам значок
   при этом не двигается: двигающийся значок читался бы как дрожащий. */
.car-deal[data-deal-anim="shine"]{position:relative;overflow:hidden}
.car-deal[data-deal-anim="shine"]::after{
  content:"";position:absolute;top:-40%;bottom:-40%;width:38%;left:-60%;
  background:linear-gradient(90deg,transparent,rgba(255,255,255,.85),transparent);
  transform:skewX(-18deg);
  pointer-events:none;
}
.car-card[data-active="1"] .car-deal[data-deal-anim="shine"]::after,
.car-deal[data-deal-behavior="every"][data-deal-anim="shine"]::after,
.pm-deal .car-deal[data-deal-anim="shine"]::after{
  animation:dealShine 2.8s ease-in-out infinite;
}
@keyframes dealShine{
  0%,55%{left:-60%}
  100%  {left:120%}
}

/* Кому движение мешает, у того его не будет. Селектор с атрибутом — не
   украшение: у правил выше он тоже есть, и по весу они равны, а значит
   побеждает то, что ниже. Без атрибута это правило проиграло бы. */
@media (prefers-reduced-motion:reduce){
  .car-deal[data-deal-anim]{animation:none}
  .car-deal[data-deal-anim="shine"]::after{animation:none;opacity:0}
  /* Значок лупы тоже замирает. Саму лупу не трогаем: это не движение, а
     ответ на курсор — без курсора её просто нет. */
  .pm-zoom-hint,
  .pm-zoom-hint svg,
  .pm-zoom-hint::after{animation:none}
  .pm-zoom-hint::after{opacity:0}
}

/* ---------- что стоит ВНУТРИ карточки, ходит за её размером ----------

   Ступень размера — это размер всей карточки, а не одной её рамки. Если
   растянуть рамку, а подпись с ценой оставить прежними, крупная карточка
   выглядит пустой, а мелкая — забитой текстом: подпись займёт полкарточки.
   Поэтому за множителем идут и буквы, и отступы, и значки.

   Множитель берём из --card-k с ЗАПАСНОЙ ЕДИНИЦЕЙ. Запасная нужна не для
   красоты: часть этих же классов живёт в ОКНЕ товара (`.pm-…`), а окно
   лежит вне карусели, и --card-k до него не доходит вовсе. Единица
   означает там ровно то, что было до этой правки, поэтому вид окна не
   изменился.

   Почему отдельным блоком, а не правкой на месте: у каждого из этих правил
   уже есть своя история (плашка у цены подбиралась замером, строка остатка
   — по центру), и трогать её текст ради множителя значило бы переписывать
   разборы. Здесь только множитель, и при «обычных» (--card-k:1) каждое
   число равно прежнему до знака.

   Правки на месте всё же понадобились в трёх местах — там, где кегль задан
   в медиазапросе (телефон) или где правило со своим атрибутом перебило бы
   это по весу: строка остатка, таймер акции и плашка у цены задают размер
   списком владельца, и их ступени обязаны ходить за карточкой тоже. */
.car-body{
  padding:calc(16px * var(--card-k,1)) calc(12px * var(--card-k,1))
          calc(20px * var(--card-k,1));
}
.car-name{margin:0 0 calc(10px * var(--card-k,1));
  font-size:calc(1rem * var(--card-k,1))}
.car-prices{gap:calc(8px * var(--card-k,1));
  margin-right:calc(-6px * var(--card-k,1))}
.car-price{font-size:calc(1.16rem * var(--card-k,1))}
.car-old{font-size:calc(.9rem * var(--card-k,1))}
.car-out{font-size:calc(.78rem * var(--card-k,1))}

/* ---------- семь ступеней размера у КАЖДОЙ надписи карточки ----------

   Владелец (07.10): «отдельно в админ панели мог настраивать размер в
   карточке всех банеров и надписей, чтобы было 7 вариантов размера у каждой
   позиции». Цель он назвал сам: уменьшая надписи, отдать их высоту
   фотографии — снимок в карточке берёт ОСТАТОК высоты (`flex:1`).

   Ступени — те же семь, что у строки остатка на сервере (SHOP_TEXT_SIZES), и
   устроены так же: «обычная» (m) правила НЕ имеет. Это не пропуск: m — ровно
   то, что стоит в правилах выше, поэтому у товаров без настройки вид не
   меняется ни на знак. Заодно и «вернуть как было» работает само собой —
   достаточно очистить поле в панели.

   У строки остатка, таймера акции и плашки выгоды ступени s/l/xl были и
   раньше; здесь к ним добавлены xs/xxl/xxxl, чтобы лестница стала общей на
   семь у всех позиций сразу.

   Каждое правило умножает СВОЙ основной кегль на --card-k: крупная карточка
   остаётся соразмерной, мелкая — читаемой. Значки и отступы идут за кеглем:
   огромная надпись с крошечным значком читается как недоделка. */
.car-name[data-name-size="xs"]{font-size:calc(.76rem * var(--card-k,1))}
.car-name[data-name-size="s"]{font-size:calc(.87rem * var(--card-k,1))}
.car-name[data-name-size="l"]{font-size:calc(1.18rem * var(--card-k,1))}
.car-name[data-name-size="xl"]{font-size:calc(1.38rem * var(--card-k,1))}
.car-name[data-name-size="xxl"]{font-size:calc(1.62rem * var(--card-k,1))}
.car-name[data-name-size="xxxl"]{font-size:calc(1.9rem * var(--card-k,1))}

.car-price[data-price-size="xs"]{font-size:calc(.88rem * var(--card-k,1))}
.car-price[data-price-size="s"]{font-size:calc(1.01rem * var(--card-k,1))}
.car-price[data-price-size="l"]{font-size:calc(1.37rem * var(--card-k,1))}
.car-price[data-price-size="xl"]{font-size:calc(1.6rem * var(--card-k,1))}
.car-price[data-price-size="xxl"]{font-size:calc(1.88rem * var(--card-k,1))}
.car-price[data-price-size="xxxl"]{font-size:calc(2.2rem * var(--card-k,1))}

.car-old[data-old-size="xs"]{font-size:calc(.68rem * var(--card-k,1))}
.car-old[data-old-size="s"]{font-size:calc(.78rem * var(--card-k,1))}
.car-old[data-old-size="l"]{font-size:calc(1.06rem * var(--card-k,1))}
.car-old[data-old-size="xl"]{font-size:calc(1.24rem * var(--card-k,1))}
.car-old[data-old-size="xxl"]{font-size:calc(1.46rem * var(--card-k,1))}
.car-old[data-old-size="xxxl"]{font-size:calc(1.71rem * var(--card-k,1))}

/* Лейбл в углу снимка («Хит продаж», «Скидка», «Новинка») растёт вместе с
   подложкой: у одного кегля с прежними отступами надпись вылезала бы за
   скруглённый край. */
.car-badge[data-badge-size="xs"]{font-size:calc(.55rem * var(--card-k,1));
  padding:calc(4px * var(--card-k,1)) calc(9px * var(--card-k,1))}
.car-badge[data-badge-size="s"]{font-size:calc(.63rem * var(--card-k,1));
  padding:calc(5px * var(--card-k,1)) calc(10px * var(--card-k,1))}
.car-badge[data-badge-size="l"]{font-size:calc(.85rem * var(--card-k,1));
  padding:calc(7px * var(--card-k,1)) calc(14px * var(--card-k,1))}
.car-badge[data-badge-size="xl"]{font-size:calc(1rem * var(--card-k,1));
  padding:calc(8px * var(--card-k,1)) calc(16px * var(--card-k,1))}
.car-badge[data-badge-size="xxl"]{font-size:calc(1.17rem * var(--card-k,1));
  padding:calc(9px * var(--card-k,1)) calc(19px * var(--card-k,1))}
.car-badge[data-badge-size="xxxl"]{font-size:calc(1.37rem * var(--card-k,1));
  padding:calc(11px * var(--card-k,1)) calc(22px * var(--card-k,1))}

/* Добавка к лестнице строки остатка: значок ходит за кеглем. */
.car-stock[data-stock-size="xs"]{font-size:calc(.58rem * var(--card-k,1))}
.car-stock[data-stock-size="xs"] .car-stock-ic{
  width:calc(11px * var(--card-k,1));height:calc(11px * var(--card-k,1))}
.car-stock[data-stock-size="xxl"]{font-size:calc(1.23rem * var(--card-k,1))}
.car-stock[data-stock-size="xxl"] .car-stock-ic{
  width:calc(20px * var(--card-k,1));height:calc(20px * var(--card-k,1))}
.car-stock[data-stock-size="xxxl"]{font-size:calc(1.44rem * var(--card-k,1))}
.car-stock[data-stock-size="xxxl"] .car-stock-ic{
  width:calc(23px * var(--card-k,1));height:calc(23px * var(--card-k,1))}

/* Добавка к лестнице плашки выгоды. Отступы и значок — за кеглем: плашка
   живёт В СТРОКЕ с ценой, и разросшиеся буквы без отступов наехали бы на неё. */
.car-deal[data-deal-size="xs"]{font-size:calc(.55rem * var(--card-k,1));
  padding:calc(2px * var(--card-k,1)) calc(6px * var(--card-k,1));
  gap:calc(4px * var(--card-k,1))}
.car-deal[data-deal-size="xs"] svg{width:calc(10px * var(--card-k,1));
  height:calc(10px * var(--card-k,1))}
.car-deal[data-deal-size="xxl"]{font-size:calc(1.17rem * var(--card-k,1));
  padding:calc(6px * var(--card-k,1)) calc(15px * var(--card-k,1))}
.car-deal[data-deal-size="xxl"] svg{width:calc(20px * var(--card-k,1));
  height:calc(20px * var(--card-k,1))}
.car-deal[data-deal-size="xxxl"]{font-size:calc(1.37rem * var(--card-k,1));
  padding:calc(7px * var(--card-k,1)) calc(17px * var(--card-k,1))}
.car-deal[data-deal-size="xxxl"] svg{width:calc(23px * var(--card-k,1));
  height:calc(23px * var(--card-k,1))}

/* Добавка к лестнице таймера акции. Цифры внутри него заданы в `em` и растут
   сами — за ступенью идёт только кегль полосы. */
.car-promo[data-promo-size="xs"]{font-size:calc(.59rem * var(--card-k,1))}
.car-promo[data-promo-size="xxl"]{font-size:calc(1.26rem * var(--card-k,1))}
.car-promo[data-promo-size="xxxl"]{font-size:calc(1.48rem * var(--card-k,1))}

/* Строка остатка: и своя строка, и её ступени размера, и плашки. */
.car-stock{gap:calc(6px * var(--card-k,1));
  margin-top:calc(4px * var(--card-k,1));
  font-size:calc(.76rem * var(--card-k,1))}
.car-stock[data-stock-size="s"]{font-size:calc(.66rem * var(--card-k,1))}
.car-stock[data-stock-size="l"]{font-size:calc(.9rem * var(--card-k,1))}
.car-stock[data-stock-size="xl"]{font-size:calc(1.06rem * var(--card-k,1))}
.car-stock-ic{width:calc(14px * var(--card-k,1));
  height:calc(14px * var(--card-k,1))}
.car-stock[data-stock-size="s"] .car-stock-ic{
  width:calc(12px * var(--card-k,1));height:calc(12px * var(--card-k,1))}
.car-stock[data-stock-size="l"] .car-stock-ic{
  width:calc(16px * var(--card-k,1));height:calc(16px * var(--card-k,1))}
.car-stock[data-stock-size="xl"] .car-stock-ic{
  width:calc(18px * var(--card-k,1));height:calc(18px * var(--card-k,1))}
.car-stock[data-stock-badge="pill"],
.car-stock[data-stock-badge="outline"],
.car-stock[data-stock-badge="glow"]{
  padding:calc(4px * var(--card-k,1)) calc(11px * var(--card-k,1));
}
.car-stock[data-stock-badge="ribbon"]{
  padding:calc(4px * var(--card-k,1)) calc(12px * var(--card-k,1))
          calc(4px * var(--card-k,1)) calc(9px * var(--card-k,1));
  border-left-width:calc(3px * var(--card-k,1));
}

/* Плашка у цены — самая тесная строка карточки: у неё подобрана ширина под
   то, чтобы стоять В СТРОКЕ с ценой. Без множителя у мелкой ступени она
   уезжала бы на свою строку у всех товаров сразу. */
.car-deal{gap:calc(4px * var(--card-k,1));
  padding:calc(2px * var(--card-k,1)) calc(7px * var(--card-k,1));
  font-size:calc(.72rem * var(--card-k,1))}
.car-deal svg{width:calc(12px * var(--card-k,1));
  height:calc(12px * var(--card-k,1))}
.car-deal[data-deal-size="s"]{font-size:calc(.64rem * var(--card-k,1));
  padding:calc(2px * var(--card-k,1)) calc(6px * var(--card-k,1));
  gap:calc(4px * var(--card-k,1))}
.car-deal[data-deal-size="s"] svg{width:calc(11px * var(--card-k,1));
  height:calc(11px * var(--card-k,1))}
.car-deal[data-deal-size="l"]{font-size:calc(.86rem * var(--card-k,1));
  padding:calc(4px * var(--card-k,1)) calc(11px * var(--card-k,1))}
.car-deal[data-deal-size="l"] svg{width:calc(15px * var(--card-k,1));
  height:calc(15px * var(--card-k,1))}
.car-deal[data-deal-size="xl"]{font-size:calc(.98rem * var(--card-k,1));
  padding:calc(5px * var(--card-k,1)) calc(13px * var(--card-k,1))}
.car-deal[data-deal-size="xl"] svg{width:calc(17px * var(--card-k,1));
  height:calc(17px * var(--card-k,1))}

/* Таймер акции. Цифры внутри него заданы в `em`, поэтому растут сами — за
   множителем идут только кегль полосы, её отступы и дорожка. */
.car-promo{gap:calc(7px * var(--card-k,1));
  margin-top:calc(6px * var(--card-k,1));
  padding:calc(5px * var(--card-k,1)) calc(11px * var(--card-k,1));
  border-radius:calc(10px * var(--card-k,1));
  font-size:calc(.78rem * var(--card-k,1))}
.car-promo[data-promo-size="s"]{font-size:calc(.68rem * var(--card-k,1))}
.car-promo[data-promo-size="l"]{font-size:calc(.9rem * var(--card-k,1))}
.car-promo[data-promo-size="xl"]{font-size:calc(1.02rem * var(--card-k,1))}
.car-promo-ic{width:calc(14px * var(--card-k,1));
  height:calc(14px * var(--card-k,1))}
.car-promo-cells{gap:calc(5px * var(--card-k,1))}
.car-promo-cell{padding:calc(3px * var(--card-k,1)) calc(5px * var(--card-k,1));
  border-radius:calc(8px * var(--card-k,1))}
.car-promo-track{height:calc(5px * var(--card-k,1));
  min-width:calc(56px * var(--card-k,1))}

/* Лейбл в углу снимка и плашка «18+» под ценой. */
.car-badge{top:calc(12px * var(--card-k,1));left:calc(12px * var(--card-k,1));
  padding:calc(6px * var(--card-k,1)) calc(12px * var(--card-k,1));
  font-size:calc(.72rem * var(--card-k,1))}
.car-adult-tag{margin-top:calc(6px * var(--card-k,1));
  padding:calc(3px * var(--card-k,1)) calc(11px * var(--card-k,1));
  font-size:calc(.74rem * var(--card-k,1))}
.car-adult-mark{padding:calc(8px * var(--card-k,1)) calc(16px * var(--card-k,1));
  font-size:calc(1rem * var(--card-k,1))}

/* Стрелки листания кадров и точки-каталожки стоят НА снимке, поэтому их
   место тоже в масштабе карточки: у мелкой ступени кнопка в 34px занимала
   бы половину кадра. Размер задаётся основанием (--arr-base), у окна товара
   оно своё — см. разбор у общего вида стрелок. */
.car-nav{--arr-base:calc(34px * var(--card-k,1))}
.car-nav-prev{left:calc(8px * var(--card-k,1))}
.car-nav-next{right:calc(8px * var(--card-k,1))}
/* Место точек карточки в её же масштабе, а их РАЗМЕР задаёт общая настройка
   точек (`--dot-base` в блоке «точки: окно»): владелец просил у точек
   карточки те же четыре настройки, что у точек под лентой, и второй набор
   размеров разошёлся бы с первым. */
.car-fdots{bottom:calc(9px * var(--card-k,1));gap:calc(5px * var(--card-k,1))}

/* Скругление карточки — тоже её размер: у мелкой ступени прежние 26px
   съедали бы угол целиком. */
.car-card{border-radius:calc(var(--r-lg) * var(--card-k,1))}

/* ============================================================
   УЗКАЯ КАРТОЧКА: фотография и название важнее отсчёта
   ============================================================

   Владелец (07.10, снимок): «если окно браузера делать меньше то карточка
   товара вот как выглядит … сделай так чтобы в таком виде наличие товара и
   таймер и скидку были меньше уменьшались потому что мне важно в том виде
   чтобы посетитель видел полноценное фото и название товара».

   ЗАМЕР ДО (`_probe-e-card.js`, размер карточки у владельца «lg»):

     окно 390 и 512px: карточка 255x401, фото 62px (15% высоты),
                       таймер 142px (35%), скидка 28px, наличие 32px
     окно 768px:       карточка 299x447, фото 92px (21%), таймер 142px (32%)
     окно 1440px:      карточка 340x492, фото 191px (39%), таймер 89px (18%)

   Фотография — флекс-элемент `.car-shot` с `flex:1`, то есть она берёт
   ОСТАТОК высоты карточки после тела. Высота карточки задана (`--car-h`), а
   тело в узком окне вырастает вдвое: ячейки отсчёта переносятся на вторую
   строку, и плашка отсчёта занимает 142px против 89px на широком экране.
   Отсюда и «фото 62px» — его съел отсчёт, а не оно само.

   Поэтому уменьшаем ВТОРОСТЕПЕННОЕ: отсчёт, скидку у цены и наличие. Выбор
   владельца при этом сохраняется — ступени s/m/l/xl сдвигаются на шаг вниз,
   а не подменяются одним числом: он ставил размер сам, и «крупный» обязан
   остаться крупнее «мелкого» и здесь.

   ПОЧЕМУ С ПРИСТАВКОЙ `.car-card`. Ступени размеров записаны селекторами
   с атрибутом (`.car-deal[data-deal-size="l"]`) — это на один класс больше,
   чем просто `.car-deal`, и правило без приставки им проиграло бы. При
   равной точности решает ПОРЯДОК в файле, поэтому весь блок стоит ПОСЛЕ
   лестницы размеров `--card-k` (см. выше) — иначе он был бы мёртвым.

   Чего здесь НЕТ нарочно: название, цена и сама карточка. Просьба была «фото
   и название», и трогать то, на что владелец не жаловался, нельзя. */
@media (max-width:820px){
  /* Поля тела: каждый пиксель отсюда уходит фотографии. */
  .car-card .car-body{
    padding:calc(12px * var(--card-k,1)) calc(10px * var(--card-k,1))
            calc(13px * var(--card-k,1));
  }

  /* Отсчёт акции. `flex-wrap` у ячеек НЕ снимаем: он держит пару «01 дн»
     вместе. Уменьшаем шаг, а не запрещаем перенос. */
  .car-card .car-promo{
    gap:4px;margin-top:4px;padding:3px 8px;border-radius:8px;
    font-size:calc(.68rem * var(--card-k,1));
  }
  .car-card .car-promo[data-promo-size="s"]{font-size:calc(.6rem * var(--card-k,1))}
  .car-card .car-promo[data-promo-size="l"]{font-size:calc(.78rem * var(--card-k,1))}
  .car-card .car-promo[data-promo-size="xl"]{font-size:calc(.9rem * var(--card-k,1))}
  .car-card .car-promo-cells{gap:3px}
  .car-card .car-promo-cell{padding:1px 4px;border-radius:6px}
  .car-card .car-promo-ic{width:11px;height:11px}

  /* Скидка у цены («Ваша выгода»). */
  .car-card .car-deal{gap:3px;padding:2px 6px;font-size:calc(.64rem * var(--card-k,1))}
  .car-card .car-deal[data-deal-size="s"]{font-size:calc(.58rem * var(--card-k,1))}
  .car-card .car-deal[data-deal-size="l"]{font-size:calc(.72rem * var(--card-k,1));
    padding:3px 8px}
  .car-card .car-deal[data-deal-size="xl"]{font-size:calc(.86rem * var(--card-k,1));
    padding:3px 9px}
  .car-card .car-deal svg{width:10px;height:10px}
  .car-card .car-deal[data-deal-size="l"] svg{width:12px;height:12px}
  .car-card .car-deal[data-deal-size="xl"] svg{width:13px;height:13px}

  /* Наличие. */
  .car-card .car-stock{gap:4px;margin-top:2px;
    font-size:calc(.66rem * var(--card-k,1))}
  .car-card .car-stock[data-stock-size="s"]{font-size:calc(.6rem * var(--card-k,1))}
  .car-card .car-stock[data-stock-size="l"]{font-size:calc(.76rem * var(--card-k,1))}
  .car-card .car-stock[data-stock-size="xl"]{font-size:calc(.9rem * var(--card-k,1))}
  .car-card .car-stock-ic{width:11px;height:11px}
}

/* Телефон: тот же шаг ещё раз.

   Замер после первого шага (окно 390/512, карточка 255x401): фото 109px
   (27%), отсчёт 115px (29%) — отсчёт ВСЁ ЕЩЁ выше фотографии, а владелец
   просил ровно обратного. Ячейки отсчёта в такую ширину не влезают в одну
   строку и переносятся, поэтому мало уменьшить кегль плашки: уменьшаем и
   сами ячейки, и высоту цифры (`b`), и поля тела. */
@media (max-width:560px){
  .car-card .car-body{
    padding:calc(10px * var(--card-k,1)) calc(8px * var(--card-k,1))
            calc(11px * var(--card-k,1));
  }
  .car-card .car-name{margin-bottom:calc(7px * var(--card-k,1))}
  .car-card .car-promo{
    gap:3px;margin-top:3px;padding:2px 6px;
    font-size:calc(.6rem * var(--card-k,1));
  }
  .car-card .car-promo[data-promo-size="s"]{font-size:calc(.54rem * var(--card-k,1))}
  .car-card .car-promo[data-promo-size="l"]{font-size:calc(.68rem * var(--card-k,1))}
  .car-card .car-promo[data-promo-size="xl"]{font-size:calc(.78rem * var(--card-k,1))}
  .car-card .car-promo-cells{gap:2px}
  .car-card .car-promo-cell{padding:1px 3px;gap:.2em}
  /* Цифра отсчёта — доля кегля плашки, поэтому её надо уменьшать ОТДЕЛЬНО:
     иначе плашка сжимается, а цифры остаются прежними. */
  .car-card .car-promo-cell b{font-size:1.24em;line-height:1}
  .car-card .car-promo-ic{width:10px;height:10px}
  .car-card .car-deal{gap:2px;padding:1px 5px;font-size:calc(.58rem * var(--card-k,1))}
  .car-card .car-deal svg{width:9px;height:9px}
  .car-card .car-stock{gap:3px;margin-top:1px;
    font-size:calc(.6rem * var(--card-k,1))}
  .car-card .car-stock-ic{width:10px;height:10px}
}

/* Стрелки */
.car-arrow{
  position:absolute;top:44%;z-index:12;width:48px;height:48px;
  display:flex;align-items:center;justify-content:center;
  border-radius:50%;border:1px solid var(--line);cursor:pointer;
  background:rgba(10,15,26,.82);color:var(--ink);
  transition:border-color .35s,color .35s,transform .35s var(--ease),box-shadow .35s;
  /* Стрелки лежат ВНУТРИ карусели, а карусель событий не берёт
     (см. .carousel) — здесь их надо вернуть, иначе стрелки онемеют. */
  pointer-events:auto;
}
.car-arrow svg{width:22px;height:22px}
.car-arrow:hover{border-color:rgba(34,230,255,.55);color:#fff;
  box-shadow:0 0 26px rgba(34,230,255,.24);transform:scale(1.06)}
.car-arrow:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
.car-arrow[disabled]{opacity:.32;cursor:default;box-shadow:none;transform:none}
.car-prev{left:0}
.car-next{right:0}

/* Точки и кнопка паузы — одной строкой по центру. Кнопка стоит рядом с
   точками, а не в углу: она относится ко всей ленте, и искать её в
   стороне никто не станет. */
.car-foot{
  /* `position:relative` — не украшение и не для раскладки: без него
     `offsetParent` у строки становится выше по дереву, и любой замер
     «в разметке» (offset-цепочка) начинает складывать чужие смещения.
     На живом замере это дало точки «на +120px от центра» при верной
     раскладке. Строка `.car-foot-row` позиционирована сама, поэтому
     положение кнопки паузы от этого не зависит. */
  position:relative;
  display:flex;align-items:center;justify-content:center;
  margin-top:18px;
}
/* Строка узкая ПО СОДЕРЖИМОМУ (`fit-content`) и потому центрируется по
   ТОЧКАМ, а не по «точки + кнопка». Кнопка внутри лежит вне потока, поэтому
   ширину строки не меняет — иначе точки снова уехали бы влево на её ширину. */
.car-foot-row{
  position:relative;
  display:flex;align-items:center;
}
.car-dots{display:flex;justify-content:center;gap:8px;flex-wrap:wrap}
.car-dot{
  width:8px;height:8px;padding:0;border:0;border-radius:50%;cursor:pointer;
  background:rgba(255,255,255,.2);transition:background .35s,transform .35s var(--ease);
}
.car-dot[aria-current="true"]{background:var(--cyan);transform:scale(1.5)}

/* Кнопка паузы ВНЕ ПОТОКА — и это не мелочь: пока она стояла в потоке рядом
   с точками, точки делили не строку, а её остаток, и уезжали влево на ширину
   кнопки с зазором — на 1440px это 22px (точки 678…718 при строке 120…1320).
   «Сделать по центру» на глаз не получалось именно из-за этого, и касалось
   это обеих лент: строка у товаров и у работ одна и та же.

   Сначала я увёл кнопку к правому краю ленты (`right:0`) — точки встали
   ровно, но владелец обвёл на снимке всю нижнюю полосу и сказал «кнопка
   стопа слетела в сторону»; замер подтвердил: +584px от центра. Теперь она
   снова РЯДОМ с точками — сразу за их правым краем, — а точки при этом
   остаются ровно по центру.

   Отсчёт идёт от края СТРОКИ, а не от края ленты, поэтому положение кнопки
   не зависит от числа точек: их бывает и 2, и 11. */
.car-pause{
  position:absolute;left:calc(100% + 14px);top:50%;transform:translateY(-50%);
  width:30px;height:30px;padding:0;flex:none;
  display:flex;align-items:center;justify-content:center;
  border-radius:50%;border:1px solid var(--line);cursor:pointer;
  background:rgba(10,15,26,.72);color:var(--ink-3);
  transition:border-color .3s,color .3s,box-shadow .3s;
}
.car-pause svg{width:15px;height:15px}
.car-pause:hover{border-color:rgba(34,230,255,.5);color:#fff;
  box-shadow:0 0 18px rgba(34,230,255,.2)}
.car-pause:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
/* Пока карусель идёт сама — знак паузы; остановлена — знак пуска. */
.car-pause .ic-play{display:none}
.car-pause.paused .ic-pause{display:none}
.car-pause.paused .ic-play{display:block}
.car-pause.paused{color:var(--cyan);border-color:rgba(34,230,255,.4)}

/* ============================================================
   Окно подтверждения возраста (товар 18+)
   ============================================================
   Отдельное окно, а не часть окна товара: спрашивать возраст надо ДО того,
   как покупатель увидит фотографию, а окно товара показало бы её в тот же
   миг, когда открылось. Оформлено как окно товара — то же затемнение, та же
   рамка, — чтобы не выглядело чужеродной вставкой.

   Z-СЛОЙ — 200, ВЫШЕ ВСЕХ ОКОН (29.10, вечер). Владелец: «во всплывающем
   окне работы нажал на карточку товара 18+ и она открылась не поверх окна,
   а открылась взаде». Так и было: вопрос стоял на 95 — ниже окна работы
   (120), окна товара поверх работы и окна «в полный рост» (130). Нажатие
   внутри окна работы не показывало НИЧЕГО: вопрос открывался за окном.

   Почему выше всех, а не «как остальные окна»: пока возраст не подтверждён,
   дальше нельзя ни смотреть товар, ни читать работу — вопрос должен лечь
   поверх того окна, из которого его позвали, каким бы оно ни было. Полный
   порядок слоёв — в комментарии у `.cb-wrap`. */
.age{position:fixed;inset:0;z-index:200;display:flex;align-items:center;
  justify-content:center;padding:22px}
.age[hidden]{display:none}
body.age-open{overflow:hidden}
.age-back{position:absolute;inset:0;background:rgba(3,5,11,.86);
  -webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);
  animation:pmFade .3s var(--ease)}
.age-box{
  position:relative;z-index:1;text-align:center;
  /* 470, а не 430: надписи на кнопках владелец правит в панели, и его
     «Да, мне есть 18 лет» в 430px не помещалось — см. .age-btn ниже. */
  width:min(470px,100%);
  padding:30px 28px 26px;border:1px solid var(--line);
  border-radius:var(--r-lg);
  background:linear-gradient(162deg,rgba(17,24,41,.99),rgba(8,12,22,.99));
  box-shadow:0 40px 130px rgba(0,0,0,.65);
  animation:pmIn .42s var(--ease);
}
.age-mark{
  display:inline-flex;align-items:center;justify-content:center;
  min-width:64px;height:64px;margin-bottom:16px;padding:0 12px;
  border-radius:50%;
  font-size:1.2rem;font-weight:800;letter-spacing:.06em;
  color:var(--adult-ink,#fff);
  background:linear-gradient(135deg,var(--adult-a,#e0356b),var(--adult-b,#8b1a3c));
  box-shadow:0 12px 34px rgba(224,53,107,.4);
  animation:adPulse 2.6s var(--ease) infinite;
}
/* Значки рядом с «18+» в окне вопроса: кружок становится пилюлей, иначе
   значок вылез бы за круг. Признак ставит скрипт (js/main.js) — так
   оформление не гадает, есть ли значки, и окно без значков остаётся
   прежним кругом. */
.age-mark[data-adult-icons]{
  border-radius:100px;min-width:0;padding:0 22px;gap:.38em;
}
.age-title{margin:0 0 10px;font-size:1.24rem;font-weight:700;
  letter-spacing:-.01em;color:var(--ink)}
.age-text{margin:0 0 22px;font-size:.92rem;line-height:1.5;color:var(--ink-3)}
.age-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
.age-btn{
  /* Кнопка обязана быть шире своей надписи, а не делиться поровну с соседкой.

     Было `flex:1 1 0;min-width:130px`: обе кнопки получали ровно половину
     ряда (177.3px), а «Да, мне есть 18 лет» требует 141.78px текста плюс
     40px полей — 181.78px. Не хватало 4.66px, и надпись ломалась на две
     строки («Да, мне есть 18» / «лет») — владелец показал на это красным:
     «надо кнопку сделать шире чтобы текст помещался весь».

     Лечим двумя правками: `white-space:nowrap` запрещает перенос, а
     `min-width:auto` (вместо 130px) заставляет кнопку ужиматься не ниже
     СВОЕЙ надписи. Теперь кнопка растёт под любой текст, который владелец
     впишет в панели, а ряд переносит соседку на новую строку, если места
     не осталось (flex-wrap выше). */
  flex:1 1 0;min-width:auto;white-space:nowrap;
  padding:12px 20px;border-radius:100px;border:1px solid transparent;
  font:inherit;font-size:.94rem;font-weight:700;cursor:pointer;
  transition:transform .2s var(--ease),box-shadow .25s var(--ease),
    background .25s var(--ease),color .25s var(--ease);
}
.age-yes{color:#04121a;background:var(--cyan);
  box-shadow:0 10px 26px rgba(34,230,255,.3)}
.age-yes:hover{transform:translateY(-2px);
  box-shadow:0 16px 34px rgba(34,230,255,.42)}
.age-no{color:var(--ink-2);background:transparent;border-color:var(--line)}
.age-no:hover{color:var(--ink);border-color:var(--line-strong);
  background:rgba(255,255,255,.04)}
.age-btn:focus-visible{outline:2px solid var(--cyan);outline-offset:3px}
/* На узком экране кнопки встают друг под друга во всю ширину: рядом они
   сжимаются до 130px, и «Есть 18» перестаёт помещаться.

   min-width оставлен auto, а не 0: надпись не переносится (nowrap выше), и
   нулевой минимум позволил бы кнопке сжаться уже собственного текста —
   буквы уехали бы за край. */
@media (max-width:520px){
  .age-actions{flex-direction:column}
  .age-btn{flex:1 1 auto;min-width:auto}
}

/* ============================================================
   Окно товара
   ============================================================ */
.pm{position:fixed;inset:0;z-index:90;display:flex;align-items:center;
  justify-content:center;padding:22px}
.pm[hidden]{display:none}
/* ОКНО ВЫРОСЛО — ПРОКРУЧИВАЕТСЯ СТРАНИЦА, А НЕ КОЛОНКА.

   Владелец (07.10) обвёл полосу прокрутки в окне товара и сказал прямо:
   «ее там никогда не должно быть». Прокрутка внутри окна была не украшением,
   а лекарством: содержимое колонки не влезало в потолок окна (`96vh`), и без
   неё низ был бы недостижим. Но лекарство он не принял — и он прав: полоса
   внутри окна спорит с полосой страницы и режет колонку на глазах.

   Отсюда второй выход: когда содержимому тесно, окну СНИМАЕМ ПОТОЛОК и
   растем вниз, а листается страница. Признак `data-pm-grow` ставит
   js/main.js — ПО ЗАМЕРУ (не влезло) и по раскрытому сгибу, а не по числу
   блоков: потолок задан в `vh`, и «влезет ли» зависит от экрана покупателя.

   `align-items:flex-start` вместо `center` — не для вида. У выросшего окна
   высота больше экрана, а флекс-элемент с `center` вылезает ВВЕРХ за край, и
   до этой части не долистать даже с прокруткой: полоса прокрутки начинается
   от начала содержимого. Центрирование при этом не теряется — его делает
   `margin:auto` у самого окна, а он сам уступает место, когда места нет. */
.pm[data-pm-grow="1"]{overflow-y:auto;align-items:flex-start}
.pm[data-pm-grow="1"] .pm-box{max-height:none;margin:auto}
/* Товар, открытый ИЗ ОКНА РАБОТЫ, ложится поверх него. Окно работы — 120,
   окно товара — 90: на главной так и надо, а в окне публикации товар
   оказывался ПОД ним и его не было видно. Число то же, что у окна «в полный
   рост» (.rc-lb, 130): ему тоже приходится ложиться поверх окна работы.
   Признак ставит скрипт (js/main.js, openProduct) — по нему видно, что
   товар открыт именно из окна, и на самой странице порядок остаётся
   прежним. */
.pm[data-over-work="1"]{z-index:130}
/* Пока окно открыто, страница под ним не прокручивается: иначе палец на
   телефоне листает товары за окном вместо описания. */
body.pm-open{overflow:hidden}
/* `fixed`, а не `absolute`: у выросшего окна страница листается, и подложка
   на `absolute` уехала бы вверх вместе с содержимым — под окном открылась бы
   голая страница сайта. В неподвижном окне разницы нет: `.pm` занимает
   ровно экран, и обе привязки дают один и тот же прямоугольник. */
.pm-back{position:fixed;inset:0;background:rgba(3,5,11,.8);
  backdrop-filter:blur(7px);animation:pmFade .3s var(--ease)}
.pm-box{
  position:relative;z-index:1;
  display:grid;grid-template-columns:minmax(0,1.02fr) minmax(0,1fr);gap:28px;
  width:min(1040px,100%);max-height:min(96vh,918px);
  padding:26px;border:1px solid var(--line);border-radius:var(--r-lg);
  background:linear-gradient(162deg,rgba(17,24,41,.99),rgba(8,12,22,.99));
  box-shadow:0 40px 130px rgba(0,0,0,.65);
  animation:pmIn .42s var(--ease);
  /* Содержимое не вылезает за рамку окна. Раньше строку сетки задавало
     содержимое: у товара с длинным описанием она выходила 802px при
     max-height 780px, окно обрезалось до 780, а overflow оставался
     visible — и кнопка «В корзину», последняя в правой колонке, повисала
     на 23px НИЖЕ рамки. Владелец обвёл это красным («кнопка упала»).
     Сам прижим строки к высоте окна — ниже, в правиле для двух колонок:
     в одноколоночном виде (телефон) строк две, и 1fr сжал бы колонку
     фотографий, налезая на описание. */
  overflow:hidden;
}
/* Две колонки: строка не выше самого окна, лишнее уходит в прокрутку
   правой колонки (.pm-info), а не за рамку. Предел высоты поднят с 780 до
   880px: у товара с описанием на пол-экрана содержимое — 854px, то есть
   раньше оно не влезало всего на 22px, и кнопка уезжала под кромку.
   Проверка на это есть в test-shop, но её товар давал ровно 775px при
   пределе 780 — проходил впритык; теперь описание в проверке длиннее.

   Второй подъём, 880 → 918px (06.10). Владелец обвёл нижнюю кромку окна и
   попросил «сделать немного больше, чтобы вот эти блоки всегда были видно,
   а не пролистывались вниз» — плашка промокода, строка кода, «Промокод
   активирован», таймер и «в наличии: N штук» под фотографиями.

   Числа замера на его раскладке (1440x957, товар «Шалаш»): окну 880px,
   левой колонке видно 826px, а содержимого 851px — не влезало ровно 25px,
   и строка остатка уходила на 884…917 при нижней кромке 892. Полностью
   содержимое влезает с 905px; 918px берём с запасом в 13px — иначе
   следующий добавленный блок снова упёрся бы. `96vh` вместо `92vh`, потому
   что при 957px экрана 92vh = 880px и потолок в px не давал бы вырасти:
   связали оба, и на низком экране окно по-прежнему сжимается. */
@media (min-width:901px){
  .pm-box{grid-template-rows:minmax(0,1fr)}
}
@keyframes pmFade{from{opacity:0}to{opacity:1}}
@keyframes pmIn{from{opacity:0;transform:translateY(18px) scale(.985)}
                to{opacity:1;transform:none}}

.pm-x{
  position:absolute;top:14px;right:14px;z-index:3;width:40px;height:40px;
  display:flex;align-items:center;justify-content:center;cursor:pointer;
  border-radius:50%;border:1px solid var(--line);
  background:rgba(10,15,26,.9);color:var(--ink);
  transition:border-color .3s,color .3s,transform .3s var(--ease);
}
.pm-x svg{width:18px;height:18px;stroke:currentColor;stroke-width:2;
  stroke-linecap:round;fill:none}
.pm-x:hover{border-color:rgba(34,230,255,.5);color:#fff;transform:rotate(90deg)}

/* БЛОКИ КОЛОНОК НЕ СЖИМАЮТСЯ.

   Владелец (07.10) нажал «Показать полностью» и увидел, что текст так и
   остался обрезанным, а кнопка «В корзину» стала ниже. Обе беды — одна:
   блоки колонки — флекс-элементы, а `flex-shrink` у них по умолчанию равен
   единице. Колонка выше потолка окна, и флекс делит нехватку МЕЖДУ ВСЕМИ:
   вместо того чтобы отдать лишнее прокрутке, он сжимает каждый блок.

   Замерено на раскладке владельца (товар «Шалаш», экран 1440x900): описание
   просит 869px, ему оставили 180 свёрнутых и всего 271 после раскрытия;
   кнопка была 190x51 и стала 190x37. Ошибка тихая — ни полосы прокрутки, ни
   обрезанной рамки: `overflow-y:auto` на колонке не срабатывает вовсе, потому
   что сжатые блоки в неё влезают. Владелец видел ровно это.

   Тот же капкан уже ловили у `.pm-thumbs` и `.pm-under` — там он вылечен
   точечно. Здесь правило общее, по ДЕТЯМ колонки: владелец сам переносит
   блоки и заводит свои, и перечень классов пришлось бы дописывать после
   каждой его правки в панели. */
.pm-media > *, .pm-info > *{flex:none}

.pm-media{display:flex;flex-direction:column;gap:8px;min-width:0}
/* Кадр фотографии — одной пропорции на все товары. Раньше он тянулся по
   высоте колонки (flex:1), а высоту задавало описание СПРАВА: у товара с
   длинным описанием кадр выходил выше, у короткого ниже. Один и тот же
   товар в двух окнах выглядел по-разному, и фотография «плавала» вверх и
   вниз. Теперь кадр один, а само фото вписывается в него (object-fit), как
   в рамку, — и у большого снимка, и у маленького. */
.pm-shot{
  position:relative;flex:none;aspect-ratio:1 / 1;
  /* Фотографию центрируем сами: она больше не растянута на всю рамку —
     иначе её углы не скруглить (см. правило ниже). */
  display:flex;align-items:center;justify-content:center;
  border-radius:var(--r-md);overflow:hidden;
  background:radial-gradient(110% 80% at 50% 0%,rgba(34,230,255,.1),transparent 72%),
             rgba(6,10,19,.7);
}
/* Скругление ФОТОГРАФИИ, а не рамки.

   Раньше снимок был растянут на всю рамку (`width/height:100%` плюс
   `object-fit:contain`): КОРОБКА картинки совпадала с рамкой, а видимый
   снимок лежал внутри неё. У кадра 1000×813 в рамке 484×484 это полосы по
   45px сверху и снизу — замерено `probe-pm-photo.js`. Скругление рамки
   (18px) обрезало ПУСТОЕ ПОЛЕ, а углы самого снимка оставались прямыми;
   владелец обвёл их на снимке и просил «красивое скругление».

   Теперь коробка картинки САМА равна видимому снимку: `max-width` и
   `max-height` ужимают её по пропорциям кадра, и `border-radius` ложится на
   настоящие углы фотографии. Видимый прямоугольник при этом не меняется
   (те же 484×393), поэтому лупа, считающая его от рамки в `frame()`,
   работает как прежде — и увеличение, и положение кружка. */
.pm-shot img,.pm-shot video{
  max-width:100%;max-height:100%;display:block;
  border-radius:var(--r-md);
}
/* Ушедший кадр обязан ИСЧЕЗНУТЬ, а не «спрятаться».

   Владелец (07.10) обвёл кадр окна и написал: «надо исправить чтобы не было
   задвоения двух картинок в одном окне». Замер на его товаре «Шалаш» (8
   кадров, `_probe-pm-double.js`): после кадра-видео узел `[data-pm-video]`
   оставался с `hidden`, но с `display:block` — и стоял РЯДОМ со снимком,
   поделив кадр пополам. Видно было и на кадрах-гиф: слева обрывок видео,
   справа гиф.

   Причина в том, что `[hidden]` — это браузерное `display:none`, и оно
   СЛАБЕЕ правила выше: `.pm-shot img,.pm-shot video{display:block}` весит
   больше, чем `[hidden]` из таблицы браузера, поэтому «скрытый» кадр
   продолжал занимать место. Ровно тот же капкан уже ловили у `.pm-deal`
   (см. `.pm-deal[hidden]` ниже) — там правило тоже объявлено явно.

   Почему это не косметика: `.pm-shot` — flex-строка, и второй видимый кадр
   не просто лишний, он ОТОДВИГАЕТ снимок вбок (замер: снимок 387px шириной
   стоял с x=184 вместо x=48). Лупа и стрелки считают своё от рамки, и
   снимок «уезжал» вместе с ними. */
.pm-shot img[hidden],.pm-shot video[hidden]{display:none}
/* Кадр-видео — наравне со снимком: и вписан в рамку по тем же правилам, и
   указатель не берёт (жест листания идёт по `.pm-shot`, и кнопок плеера,
   которые его перехватили бы, у кадра нет нарочно — см. `shotVideo` в
   js/main.js). */
.pm-shot video{pointer-events:none}
/* Лупа — только по снимку. Она берёт размеры файла (`naturalWidth`), а у
   видео их нет: кружок повис бы в пустоте, а курсор исчез бы (`cursor:none`
   у включённой лупы) — окно выглядело бы заевшим. Признак ставит
   `showPhoto`: 'image' / 'gif' / 'video'.
   Курсор здесь не гасим — это правило стоит ДО правил лупы, и на равной
   силе селекторов победило бы их (`cursor:none`). Он снят ниже, в разделе
   лупы, — там, где его видно рядом с тем, что он отменяет. */
.pm-shot[data-media="video"] .pm-zoom-hint,
.pm-shot[data-media="video"] .pm-lens{display:none}
/* Вбок жест наш, вниз-вверх — браузера: без этого телефон считает движение
   своим и кадр «залипает» вместо листания. Так же сделано у ленты работ. */
.pm-shot{touch-action:pan-y}

/* Стрелки листания кадров в окне товара. Владелец просил «рядом поставить
   стрелки листать стрелками». Стоят по бокам снимка на середине его
   высоты: у края рамки они читаются как часть фотографии, а не как
   украшение окна. Скрипт показывает их, только когда кадров больше одного
   и стрелки не выключены настройкой магазина.

   Сам вид — в общем блоке выше («стрелки листания кадров: общий вид»):
   владелец просил, чтобы на карточке и в окне товара они выглядели
   ОДИНАКОВО. Здесь только своё: основание крупнее (снимок большой, и
   мелкая стрелка потерялась бы) и отступы. */
.pm-nav{--arr-base:42px;z-index:4}
.pm-nav-prev{left:12px}
.pm-nav-next{right:12px}
.pm-nav:hover{
  background:var(--arr-c);color:var(--arr-on);
  border-color:transparent;transform:scale(1.08);
}
.pm-nav:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
/* Выключенные стрелки прячем явно: `.pm-nav` задаёт `display:flex`, а он
   отменяет атрибут `hidden` — та же ловушка, что у `.pm-buy[hidden]`. */
.pm-nav[hidden]{display:none}

/* ---------- лупа на фотографии ----------

   Владелец просил «красивый значок лупы с разной анимацией, показывающий,
   что нужно нажать на фото», и чтобы внутри круга фотография увеличивалась.
   Здесь ровно две вещи: значок-приглашение в углу снимка и сам круг.

   Значок событий НЕ берёт: нажимать надо на фотографию целиком, и промах по
   кружку не должен оставлять человека ни с чем — кружок всего 54px.

   Круг ходит за курсором, а увеличенный кусок рисуется фоном самой
   картинки: `background-size` и `background-position` считает скрипт — по
   НАСТОЯЩЕМУ размеру кадра, а не по рамке. Фотографию показывает
   `object-fit:contain`, то есть не квадратная лежит с полями, и увеличение
   по рамке съезжало бы ровно на величину поля.

   Включение и выключение — из панели («Товары» → «Лупа на фотографии»).
   Выключенная лупа не оставляет на снимке ничего: ни значка, ни обработчика
   нажатия. */
.pm-zoom-hint{
  position:absolute;right:14px;bottom:14px;z-index:3;
  width:var(--hint,54px);height:var(--hint,54px);border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  color:var(--zoom-ink,#eaf2ff);
  /* Нутро кружка. Было почти чёрным (rgba(8,12,22,.74)) и читалось серой
     нашлёпкой на фотографии — владелец просил «не такая серая». Теперь это
     ЦВЕТ ВЛАДЕЛЬЦА в четверть силы (--zoom-fill) плюс блик сверху: кружок
     остаётся заметным на любом снимке, но перестаёт быть серым пятном.
     Тёмная подложка под цветом нужна для читаемости значка на светлых
     фотографиях — без неё белый значок пропадал бы на белом фоне. */
  background:
    linear-gradient(165deg,rgba(255,255,255,.22),rgba(255,255,255,0) 58%),
    linear-gradient(0deg,var(--zoom-fill,rgba(142,163,192,.3)),
      var(--zoom-fill,rgba(142,163,192,.3))),
    rgba(10,16,28,.52);
  border:1px solid var(--zoom-c,rgba(234,242,255,.55));
  box-shadow:0 6px 20px rgba(0,0,0,.5),
    0 0 18px -4px var(--zoom-c,rgba(234,242,255,.4));
  pointer-events:none;
  transition:opacity .3s var(--ease),transform .3s var(--ease);
}
.pm-zoom-hint svg{width:24px;height:24px}

/* Размер кружка — настройка владельца («менять размер её»). Кегль значка
   растёт вместе с кружком: крупный круг с крошечной лупой внутри читался бы
   как недоделка. «Обычный» правила не имеет — это то, что стоит выше.
   Размер задаётся переменной, а не четырьмя наборами свойств: круг и значок
   должны оставаться соразмерными. */
.pm-shot[data-zoom-size="s"] .pm-zoom-hint{--hint:42px}
.pm-shot[data-zoom-size="s"] .pm-zoom-hint svg{width:19px;height:19px}
.pm-shot[data-zoom-size="l"] .pm-zoom-hint{--hint:68px}
.pm-shot[data-zoom-size="l"] .pm-zoom-hint svg{width:30px;height:30px}
.pm-shot[data-zoom-size="xl"] .pm-zoom-hint{--hint:84px}
.pm-shot[data-zoom-size="xl"] .pm-zoom-hint svg{width:38px;height:38px}

/* Угол кружка — настройка владельца, и она ДОЛЖНА работать всегда.

   Скрипт ставит отступы сам, когда знает размеры кадра (`naturalWidth`), и
   это главный путь: у не квадратной фотографии по краям остаются поля, и угол
   рамки — не угол снимка. Но ставит он их только ПОСЛЕ замера — то есть после
   `load` картинки. Пока кадр не загрузился, полевых отступов нет вовсе, и
   кружок висел в правом нижнем углу, какой бы угол владелец ни выбрал
   (замерено: при `data-zoom-place="tl"` и снятых полях круг уходил в правый
   низ). У кадра из кэша это мгновение, а на медленной связи — заметный
   прыжок через весь снимок.

   Эти правила задают тот же угол ПО РАМКЕ, чтобы настройка работала и в этот
   промежуток. Полевые отступы скрипта, когда они есть, остаются главнее —
   они инлайновые. */
.pm-shot[data-zoom-place="br"] .pm-zoom-hint{right:14px;left:auto;bottom:14px;top:auto}
.pm-shot[data-zoom-place="bl"] .pm-zoom-hint{right:auto;left:14px;bottom:14px;top:auto}
.pm-shot[data-zoom-place="tr"] .pm-zoom-hint{right:14px;left:auto;bottom:auto;top:14px}
.pm-shot[data-zoom-place="tl"] .pm-zoom-hint{right:auto;left:14px;bottom:auto;top:14px}
/* Пока лупа работает, значок уходит: он уже сделал своё дело, а на снимке
   теперь есть чем занять глаз. */
.pm-shot[data-zoom="on"] .pm-zoom-hint{opacity:0;transform:scale(.85)}
.pm-shot[data-zoom="off"]:hover .pm-zoom-hint{transform:scale(1.08)}
/* Курсор прячем: курсор и есть лупа. */
.pm-shot[data-zoom="on"]{cursor:none}

/* Виды движения значка. Список закрыт на сервере (SHOP_LENS_ANIMS), поэтому
   по одному правилу на вид; «без движения» правила не имеет — это покой.
   Кольцо и блик занимают ::after, но никогда не вместе: их включает разное
   значение одного атрибута. */
@keyframes zoomPulse{
  0%,100%{transform:scale(1)}
  50%    {transform:scale(1.1)}
}
.pm-shot[data-zoom-anim="pulse"] .pm-zoom-hint{animation:zoomPulse 2.2s ease-in-out infinite}

@keyframes zoomFloat{
  0%,100%{transform:translateY(0)}
  50%    {transform:translateY(-7px)}
}
.pm-shot[data-zoom-anim="float"] .pm-zoom-hint{animation:zoomFloat 2.6s ease-in-out infinite}

@keyframes zoomSpin{ to{transform:rotate(360deg)} }
.pm-shot[data-zoom-anim="spin"] .pm-zoom-hint svg{animation:zoomSpin 6s linear infinite}

@keyframes zoomRing{
  0%  {transform:scale(1);opacity:.85}
  100%{transform:scale(1.75);opacity:0}
}
.pm-shot[data-zoom-anim="ring"] .pm-zoom-hint::after{
  content:"";position:absolute;inset:-2px;border-radius:50%;
  border:2px solid var(--zoom-c,rgba(234,242,255,.55));
  animation:zoomRing 2.2s ease-out infinite;
}

@keyframes zoomShine{
  0%,50%{left:-60%}
  100%  {left:120%}
}
.pm-shot[data-zoom-anim="shine"] .pm-zoom-hint{overflow:hidden}
.pm-shot[data-zoom-anim="shine"] .pm-zoom-hint::after{
  content:"";position:absolute;top:-30%;bottom:-30%;width:40%;left:-60%;
  background:linear-gradient(90deg,transparent,rgba(255,255,255,.9),transparent);
  transform:skewX(-18deg);
  animation:zoomShine 2.6s ease-in-out infinite;
}

/* ВНУТРЕННЕЕ движение значка — своя настройка, отдельная от внешней:
   владелец просил, чтобы «анимация была снаружи и внутри». Правила висят
   на самом значке (svg), поэтому кольцо снаружи (::after) и покачивание
   кружка работают ОДНОВРЕМЕННО с любым из этих видов.

   Стоят ПОСЛЕ внешних правил намеренно: внешний вид «Вращение» тоже
   вращает значок, и при двух включённых сразу побеждает тот, что ниже, —
   то есть явный выбор «внутри», а не оставшийся снаружи. */
@keyframes zoomInTurn{ to{transform:rotate(360deg)} }
.pm-shot[data-zoom-anim-in="turn"] .pm-zoom-hint svg{
  animation:zoomInTurn 6s linear infinite}

@keyframes zoomInBeat{
  0%,100%{transform:scale(1)}
  50%    {transform:scale(1.22)}
}
.pm-shot[data-zoom-anim-in="beat"] .pm-zoom-hint svg{
  animation:zoomInBeat 1.4s ease-in-out infinite}

@keyframes zoomInSwing{
  0%,100%{transform:rotate(-13deg)}
  50%    {transform:rotate(13deg)}
}
.pm-shot[data-zoom-anim-in="swing"] .pm-zoom-hint svg{
  animation:zoomInSwing 1.8s ease-in-out infinite}

@keyframes zoomInBlink{
  0%,100%{opacity:1}
  50%    {opacity:.35}
}
.pm-shot[data-zoom-anim-in="blink"] .pm-zoom-hint svg{
  animation:zoomInBlink 1.6s ease-in-out infinite}

@keyframes zoomInGlow{
  0%,100%{filter:drop-shadow(0 0 0 transparent)}
  50%    {filter:drop-shadow(0 0 7px var(--zoom-c,rgba(234,242,255,.7)))}
}
.pm-shot[data-zoom-anim-in="glow"] .pm-zoom-hint svg{
  animation:zoomInGlow 1.9s ease-in-out infinite}

/* Включённая лупа гасит курсор над кадром (`cursor:none` выше) — на видео
   это выглядело бы заевшим окном: кружка нет, курсора нет, а перетаскивать
   нечего. Поэтому на кадре-видео курсор обычный.
   Стоит ЗДЕСЬ, а не рядом с правилами `.pm-shot video`, нарочно: у того
   селектора сила та же (класс + признак), и он проиграл бы правилу лупы
   просто по порядку — наверх файла. */
.pm-shot[data-media="video"],
.pm-shot[data-media="video"][data-zoom="on"]{cursor:default}

/* Сам круг. Размер задан здесь, а не в скрипте: скрипт его ЧИТАЕТ
   (offsetWidth) и по нему считает, какой кусок кадра показать, — поэтому на
   узком экране круг можно уменьшить, и расчёт останется верным. */
.pm-lens{
  position:absolute;left:0;top:0;z-index:4;
  width:150px;height:150px;border-radius:50%;
  transform:translate(-50%,-50%);
  background-repeat:no-repeat;
  border:2px solid var(--zoom-c,rgba(234,242,255,.7));
  box-shadow:0 12px 36px rgba(0,0,0,.6),inset 0 0 0 1px rgba(255,255,255,.16);
  opacity:0;pointer-events:none;
  transition:opacity .18s var(--ease);
}
.pm-lens.on{opacity:1}
/* Блик стекла: без него круг читается плоской наклейкой. */
.pm-lens::after{
  content:"";position:absolute;inset:0;border-radius:50%;
  background:linear-gradient(145deg,rgba(255,255,255,.26),transparent 46%);
  pointer-events:none;
}
/* На узком экране круг и значок меньше. Отступы значка от края снимка
   задаёт скрипт (значок ставится на саму фотографию, а не в угол рамки) —
   поэтому здесь только размеры. */
@media (max-width:620px){
  .pm-lens{width:118px;height:118px}
  .pm-zoom-hint{width:46px;height:46px}
  .pm-zoom-hint svg{width:20px;height:20px}
}

/* Миниатюры: ряд кадров под снимком.

   Поля сверху и снизу — не украшение. Блок обрезает содержимое по краю поля
   (сегодня `overflow:hidden`, раньше — прокрутка), а приподнятая при
   наведении миниатюра выходила за верхнюю границу: верхняя рамка срезалась, и
   владелец видел, что «при наведении пропадает верхний край». Поле даёт
   подъёму место. Сколько именно — считано по самому крупному движению кадра,
   см. `.pm-thumbs`.

   `flex:none` — не украшение, а починка поломки, которую видно только
   замером. Ряд миниатюр — сгибаемый элемент колонки (`flex:0 1 auto`), а у
   сгибаемого элемента с обрезкой автоматический минимум равен НУЛЮ:
   он единственный в колонке мог сжаться — и сжимался на всю нехватку высоты.
   Пока колонка не прокручивалась, содержимое в неё влезало и сжатия не
   было; с перенесёнными под фотографии блоками колонка стала выше окна, и
   ряд миниатюр сплющился с 74px до 8px (замерено `_probe-pm-under.js`:
   `thumbs.h` = 8 при `flex:none` = 74). Запрещаем сжатие: лишнее уходит в
   прокрутку колонки, как и задумано. */
/* ---------- мини-карусель кадров под снимком ----------

   Владелец (07.10): «надо сделать мини карусеть фото видео и гиф чтобы не
   было под ними прокрутки ... с эффектами и анимациями и наклонами и
   размерами которое я сам задам и обязательно стрелки по бокам».

   Прокрутки здесь больше НЕТ и быть не должно: полоса была сделана
   `overflow-x:auto`, и полоса прокрутки под кадрами — ровно то, на что он
   жаловался. Вместо неё дорожка сдвигается преобразованием, а листают её
   стрелки по бокам. */
.pm-thumbs{
  display:flex;flex:none;align-items:center;gap:6px;
  /* Отступ сверху и снизу — не воздух, а ЗАПАС для движения кадра. Полоса
     обрезает всё, что вылезло (`overflow:hidden`, иначе плитки уезжали бы
     поверх окна), а обрезает она по краю ВНУТРЕННЕГО отступа — значит, в
     этих 10px эффект и живёт. Без запаса «увеличение» и «подъём» выглядели
     бы обрезанными сверху, и владелец решил бы, что выбрал поломанный
     эффект. Самый крупный рост — 7,7px (плитка 96px при увеличении 1,16). */
  padding:10px 0;position:relative;overflow:hidden;
}
.pm-thumbs-track{
  display:flex;gap:8px;flex:1 1 auto;min-width:0;
  /* `position:relative` — не украшение, а ОПОРА ОТСЧЁТА для плиток.

     Скрипт ведёт полосу, считая, на сколько сдвинуть дорожку до нужной
     плитки, и читает для этого `offsetTop`/`offsetLeft` самой плитки. А этот
     отсчёт идёт от ближайшего РАЗМЕЧЕННОГО предка, и без этой строки опорой
     была то полоса, то сама дорожка: дорожка ездит преобразованием, а
     преобразованный элемент браузер сам делает опорой. Смена опоры на ходу
     незаметна, но считает по-разному — у полосы есть внутренний отступ под
     стрелки, и он то входил в разность, то нет. Замер `_dbg-pm34.js` (столбец
     миниатюр, отступ 34px, шаг плитки 96): полоса вставала на -62 вместо -96.
     Теперь опора всегда дорожка, и разность читается без вычитаний. */
  position:relative;
  /* Полоса едет ПОСТЕПЕННО, и обе половины этого — ответ на одну жалобу.

     Владелец (08.10, снимок #7): «свайп работает плохо, он очень резко все
     листает… не так резко а постепенно». Было `calc(var(--thumb-t) * .45)`
     с общей плавностью сайта `--ease` = `cubic-bezier(.22,1,.36,1)`. Та
     кривая — с БЫСТРЫМ началом и медленным концом: движение набирало почти
     всю скорость в первые кадры и читалось как рывок, хотя по времени было
     коротким.

     Поэтому здесь своя кривая — `cubic-bezier(.4,0,.2,1)` (плавное начало,
     плавный конец), — и множитель поднят с .45 до .8. Своя кривая именно у
     ПОЛОСЫ, а не у всего сайта: `--ease` работает на десятках переходов, и
     менять её всю из-за одной жалобы нельзя. Множитель остался от настройки
     владельца («Скорость», 0.5–3с): он по-прежнему задаёт длительность, но
     теперь его «0.5» значит 0.4с, а не 0.22с. */
  transition:transform calc(var(--thumb-t,1s) * .8) cubic-bezier(.4,0,.2,1);
}
.pm-thumb{
  flex:none;width:var(--thumb,66px);height:var(--thumb,66px);
  padding:0;border-radius:12px;overflow:hidden;
  border:1px solid var(--line);background:none;cursor:pointer;
  /* `position:relative` — под значок видео: он лежит поверх миниатюры, а
     без опоры встал бы по ближайшему размеченному предку, то есть не туда
     (или вообще за краем окна). */
  position:relative;
  transition:border-color .3s,transform .3s var(--ease),opacity .3s var(--ease);
}
.pm-thumb img,.pm-thumb video{width:100%;height:100%;object-fit:cover;display:block}
/* Миниатюра видео — стоп-кадр, и по ней не видно, что там движение. Значок
   говорит об этом прямо. Рисуем его рамками, а не буквой «▶»: буква
   приходит из шрифта, у разных систем выглядит по-разному и на 66px
   читается как мусор. */
.pm-thumb[data-kind="video"]::after{
  content:"";position:absolute;right:5px;bottom:5px;
  width:0;height:0;border-style:solid;
  border-width:5px 0 5px 8px;
  border-color:transparent transparent transparent #fff;
  filter:drop-shadow(0 1px 3px rgba(0,0,0,.85));
  pointer-events:none;
}
.pm-thumb:hover{transform:translateY(-2px)}
.pm-thumb[aria-current="true"]{border-color:var(--cyan);
  box-shadow:0 0 0 1px rgba(34,230,255,.4)}

/* Размер плитки — настройка владельца («размерами которое я сам задам»).
   «Обычный» правила не имеет: это то, что стоит выше, поэтому у магазина без
   этой настройки вид не меняется ни на пиксель. */
.pm-thumbs[data-thumb-size="xs"] .pm-thumb{--thumb:44px}
.pm-thumbs[data-thumb-size="s"]  .pm-thumb{--thumb:54px}
.pm-thumbs[data-thumb-size="l"]  .pm-thumb{--thumb:80px}
.pm-thumbs[data-thumb-size="xl"] .pm-thumb{--thumb:96px}

/* Наклон плиток — это ПОКОЙ, а не движение при листании: плитки стоят под
   углом. Отдельно от движения намеренно — одно можно включить, другое
   выключить.

   Наклон задан свойством `rotate`, а не `transform`, и это не придирка:
   движение при листании (ниже) тоже двигает плитку — через `transform`, — а
   два `transform` из разных правил не складываются, а спорят: побеждает тот,
   что ниже в файле. Поставь владелец наклон и эффект вместе, текущий кадр
   терял бы наклон (или наоборот), и он решил бы, что настройка сломана.
   `rotate` и `transform` — РАЗНЫЕ свойства, браузер применяет их вместе. */
.pm-thumbs[data-thumb-tilt="left"]  .pm-thumb{rotate:-4deg}
.pm-thumbs[data-thumb-tilt="right"] .pm-thumb{rotate:4deg}
.pm-thumbs[data-thumb-tilt="fan"]   .pm-thumb{rotate:-7deg}
.pm-thumbs[data-thumb-tilt="fan"]   .pm-thumb:nth-child(even){rotate:7deg}
.pm-thumbs[data-thumb-tilt="wave"]  .pm-thumb{rotate:-3deg}
.pm-thumbs[data-thumb-tilt="wave"]  .pm-thumb:nth-child(3n){rotate:4deg}

/* Движение при листании. Виды — те же, что у каруселей сайта
   (PUB_CAROUSEL_FX): владелец просил, чтобы мини-карусель «соответствовала
   каруселям на сайте». Правило есть у КАЖДОГО вида из списка: настройка без
   правила — это настройка, которая ничего не делает, и владелец решил бы,
   что она сломана. */
.pm-thumbs[data-thumb-fx="none"] .pm-thumbs-track{transition:none}
.pm-thumbs[data-thumb-fx="zoom"] .pm-thumb[aria-current="true"]{transform:scale(1.16)}
.pm-thumbs[data-thumb-fx="rise"] .pm-thumb[aria-current="true"]{transform:translateY(-7px)}
.pm-thumbs[data-thumb-fx="tilt"] .pm-thumb[aria-current="true"]{transform:rotate(-7deg) scale(1.06)}
.pm-thumbs[data-thumb-fx="swing"] .pm-thumb[aria-current="true"]{transform:rotate(6deg)}
.pm-thumbs[data-thumb-fx="flip"] .pm-thumb[aria-current="true"]{transform:rotateY(22deg)}
.pm-thumbs[data-thumb-fx="cube"] .pm-thumbs-track{perspective:420px}
.pm-thumbs[data-thumb-fx="cube"] .pm-thumb[aria-current="true"]{transform:rotateX(24deg)}
.pm-thumbs[data-thumb-fx="fade"] .pm-thumb{opacity:.5}
.pm-thumbs[data-thumb-fx="fade"] .pm-thumb[aria-current="true"]{opacity:1}
/* «Вращение» крутит КАДР ВНУТРИ плитки, а не саму плитку: квадрат в полный
   оборот вырастает до 1,41 своей стороны, и на полпути углы срезало бы краем
   полосы (у крупной плитки это 20px — никакого запаса не хватит). Кадр на
   время оборота увеличен, поэтому плитку он закрывает при любом угле, а
   после оборота возвращается к обычному виду: увеличение задано ВНУТРИ
   кадров движения, а не на самом кадре, — иначе текущая плитка осталась бы
   навсегда крупнее соседних. */
.pm-thumbs[data-thumb-fx="spin"] .pm-thumb[aria-current="true"] img,
.pm-thumbs[data-thumb-fx="spin"] .pm-thumb[aria-current="true"] video{
  animation:thumbSpin calc(var(--thumb-t,1s) * .8) var(--ease) 1}
@keyframes thumbSpin{
  0%   {transform:rotate(0deg);   scale:1.45}
  85%  {transform:rotate(360deg); scale:1.45}
  100% {transform:rotate(360deg); scale:1}
}
/* `slide` правила не имеет намеренно: сдвиг дорожки — это и есть листание,
   и оно работает всегда, при любом виде. */

/* Стрелки мини-карусели — те же кнопки, что у большого снимка, и вид у них
   ОБЩИЙ (SHOP_ARROW_*): владелец просил менять их «по всем параметрам как
   сейчас на сайте». Своё у них только место и размер: у снимка кнопка 42px,
   здесь она должна быть мельче, иначе заслонит плитки. */
.pm-thumbs .pm-nav{--arr-base:28px;z-index:3}
.pm-thumbs .pm-thumb-nav.pm-nav-prev{left:0}
.pm-thumbs .pm-thumb-nav.pm-nav-next{right:0}

/* ---------- формат кадра 3:4 (окно товара) ----------

   Владелец (12.10): «а вот в таком виде открытого окна ... чтобы был вот такой
   показ в точности по размерам ... как надо сделать формат просмотра 3:4 и
   пролистывания слева фото и видео и гиф и рядом само фото и точно такой же
   размер и формат. Вот как пример фото ... вот такие по размерам буду там
   загружать 1800 x 2400 пикс.»

   Три перемены, и все — от одной настройки магазина:

     • кадр `.pm-shot` из квадрата становится 3:4 — той же пропорции, что и
       снимок владельца (1800×2400). Замер до работы (`_probe-media-34.js`):
       рамка 483.7×483.7, а снимок внутри 362.8×483.7 — то есть по бокам
       пустовало по 60.5px. При кадре 3:4 снимок встаёт в рамку во всю ширину,
       и пустых полос не остаётся вовсе;
     • миниатюры уезжают ВЛЕВО вертикальным столбцом — «пролистывания слева»;
     • плитка становится 3:4, а не квадратом — «точно такой же ... формат».

   Признак `data-shot-frame="34"` ставит скрипт на корень документа и на раздел
   магазина (см. `applyShopFrame` в js/main.js). Нет признака — ни одно правило
   ниже не действует, и окно выглядит ровно как прежде. */
[data-shot-frame="34"] .pm-media{
  flex-direction:row;
  /* Перенос строк нужен для нижнего блока: в колонке фотографий живёт ещё и
     `.pm-under` («цвет и размер» с кнопками покупки), и без переноса он встал
     бы ТРЕТЬИМ СТОЛБЦОМ рядом со снимком. Правило ниже уводит его на свою
     строку во всю ширину. */
  flex-wrap:wrap;
  align-items:stretch;
  gap:14px;
}
/* Кадр берёт остаток ширины после столбца миниатюр. Пропорцию держит
   `aspect-ratio` — высота выходит сама.

   Почему высота кадра не задана явно: окно само подстраивается под ширину
   колонки, и «высота 3:4» от этой ширины зависит. Заданная в пикселях, она
   разошлась бы с шириной на первом же экране покупателя, и владелец увидел бы
   не 3:4, а случайный прямоугольник. `aspect-ratio` считает её сам.

   Раньше здесь был ещё и `max-height` («чтобы кадр не вырос выше окна»), но
   такого правила тут нет и не было: потолок окна держит `.pm-box`
   (`max-height`), а кадр при ширине колонки окна выходит 511,6px — замер
   `_dbg-pm34.js` при 1440×1200: `.pm-box` 1040×769 при потолке 918. Потолок на
   самом кадре сломал бы пропорцию: у обрезанного по высоте кадра ширина
   осталась бы прежней, и 3:4 превратилось бы в 3:2,6. */
[data-shot-frame="34"] .pm-shot{
  aspect-ratio:3 / 4;
  /* Основание 0, а не `auto`: при `auto` кадр просил бы ширину по своему
     содержимому (у снимка 1800×2400 это вся ширина колонки), не оставлял бы
     места столбцу миниатюр и вместе с ним уезжал бы на вторую строку — замер:
     полоса вставала НАД кадром, а не слева. С нулевым основанием кадр берёт
     ровно остаток после столбца. */
  flex:1 1 0;
  min-width:0;
}
[data-shot-frame="34"] .pm-thumbs{
  flex-direction:column;
  /* Столбец СЛЕВА от кадра. `order` двигает на экране, а не в разметке: полоса
     остаётся тем же узлом, и её слушатели, сдвиг дорожки и «текущая плитка»
     никуда не переезжают. */
  order:-1;
  align-items:center;
  /* Запас под эффекты — по бокам, а не сверху и снизу: у столбца кадры растут
     вбок. Без него «увеличение» и «подъём» обрезались бы краем полосы, и
     владелец решил бы, что выбрал поломанный эффект. */
  padding:0 10px;
  flex:0 0 auto;
}
[data-shot-frame="34"] .pm-thumbs-track{
  flex-direction:column;
  align-items:center;
  /* Основание 0 — и это НЕ украшение, а починка, найденная замером.

     Высоту строки в окне задаёт САМЫЙ ВЫСОКИЙ её участник, а кадр растянут
     (`align-items:stretch`), поэтому столбец миниатюр задавал высоту и кадру.
     При основании `auto` дорожка просила высоту по содержимому — 568px у шести
     плиток по 88px, — а сверху и снизу лежит запас под стрелки (по 34px,
     `thumbLook` ставит его ПРЯМО в разметку: `thumbsEl.style.paddingTop`).
     Вместе 636px. Кадр растягивался до них и переставал быть 3:4, а полосе
     нечего было листать: дорожка 568 при видимых 636 — замер `_dbg-pm34.js`
     (кадр 383,7×636 при нужных 511,6; протяжка давала `translateY(0px)`).

     С нулевым основанием дорожка ничего не просит, высоту строки задаёт кадр
     (511,6 по пропорции), столбец растягивается до неё же, а дорожка получает
     остаток — 443,6px. Ей этого мало (содержимое 568), поэтому она
     переполнена, обрезана краем полосы и листается. `min-height:0` —
     обязательная пара к основанию 0: без него автоматический минимум сгибаемого
     элемента вернул бы те же 568px. */
  flex:1 1 0;
  min-height:0;
}
[data-shot-frame="34"] .pm-thumb{
  width:var(--thumb,66px);
  height:auto;
  aspect-ratio:3 / 4;
}
/* Стрелки полосы-столбца: сверху и снизу по центру. Общий вид ставит кнопку на
   середину высоты (`top:50%` + `margin-top`), а у столбца она легла бы поверх
   плиток — снимаем и то, и другое. */
[data-shot-frame="34"] .pm-thumbs .pm-thumb-nav.pm-nav-prev{
  left:50%;right:auto;top:0;bottom:auto;margin-top:0;
  margin-left:calc(var(--arr-w) / -2);
}
[data-shot-frame="34"] .pm-thumbs .pm-thumb-nav.pm-nav-next{
  left:50%;right:auto;top:auto;bottom:0;margin-top:0;
  margin-left:calc(var(--arr-w) / -2);
}
/* Нижний блок колонки фотографий — на СВОЮ строку во всю ширину: в строке
   миниатюр он иначе встал бы столбцом рядом со снимком. */
[data-shot-frame="34"] .pm-under{flex:1 1 100%;width:100%}

/* ---------- кадр в полный рост ----------

   Владелец (07.10): «надо добавить возможность открыть фото или видео или гиф
   в полный рост и потом пролистывать все фото видео и гиф в полный размер …
   разные иконки сделай и эфекты и размеры и цвета этой новой функции …
   задавать размер, менять там стрелки и цвета и эффекты у стрелок при
   перелистывании».

   Здесь две вещи: кнопка-приглашение в углу снимка и сама накладка с открытым
   кадром. Место кнопки считает скрипт — тем же расчётом, что у лупы (угол
   СНИМКА, а не рамки).

   Накладка растянута по ЭКРАНУ (`position:fixed`), а не по окну товара: у
   выросшего окна страница листается, и накладка на `absolute` уехала бы вверх
   вместе с содержимым. Сверху она встаёт порядком в разметке — после
   `.pm-box`, у которого `z-index:1`.

   Кнопка — настоящая `<button>`, а не значок-украшение: у лупы значок событий
   не берёт (нажимать надо по всему снимку), а здесь нажатие и есть весь
   смысл. Обработчик гасит всплытие, иначе то же нажатие включило бы лупу. */
.pm-full-hint{
  position:absolute;top:14px;right:14px;z-index:3;
  width:var(--fhint,54px);height:var(--fhint,54px);border-radius:50%;
  display:flex;align-items:center;justify-content:center;padding:0;
  cursor:pointer;
  color:var(--zoom-ink,#eaf2ff);
  /* Нутро кружка — ЦВЕТ ВЛАДЕЛЬЦА в четверть силы плюс блик сверху, как у
     лупы: одинаковые кнопки должны и выглядеть одинаково. Тёмная подложка под
     цветом нужна для читаемости значка на светлых фотографиях. */
  background:
    linear-gradient(165deg,rgba(255,255,255,.22),rgba(255,255,255,0) 58%),
    linear-gradient(0deg,var(--zoom-fill,rgba(142,163,192,.3)),
      var(--zoom-fill,rgba(142,163,192,.3))),
    rgba(10,16,28,.52);
  border:1px solid var(--zoom-c,rgba(234,242,255,.55));
  box-shadow:0 6px 20px rgba(0,0,0,.5),
    0 0 18px -4px var(--zoom-c,rgba(234,242,255,.4));
  transition:opacity .3s var(--ease),transform .3s var(--ease);
}
.pm-full-hint svg{width:24px;height:24px}
/* Скрытую кнопку гасим ЯВНО: `display:flex` выше отменяет атрибут `hidden`, и
   без этого правила выключенный просмотр оставил бы кнопку на снимке. */
.pm-full-hint[hidden]{display:none}
.pm-full-hint:hover{transform:scale(1.08)}

/* Размер кнопки — настройка владельца, ступени те же, что у лупы («по принципу
   как у лупы»). «Обычный» правила не имеет: это то, что стоит выше, поэтому у
   магазина без этой настройки вид не меняется ни на пиксель. */
.pm-shot[data-full-size="s"] .pm-full-hint{--fhint:42px}
.pm-shot[data-full-size="s"] .pm-full-hint svg{width:19px;height:19px}
.pm-shot[data-full-size="l"] .pm-full-hint{--fhint:68px}
.pm-shot[data-full-size="l"] .pm-full-hint svg{width:30px;height:30px}
.pm-shot[data-full-size="xl"] .pm-full-hint{--fhint:84px}
.pm-shot[data-full-size="xl"] .pm-full-hint svg{width:38px;height:38px}

/* Движение кнопки — ТОТ ЖЕ словарь, что у лупы (SHOP_FULL_ANIMS — ссылка на
   SHOP_LENS_ANIMS): второго набора кадров тех же движений быть не должно, он
   разошёлся бы с первым на первой же правке. Поэтому здесь только селекторы, а
   кадры берутся готовые. «Без движения» правила не имеет — это покой. */
.pm-shot[data-full-anim="pulse"] .pm-full-hint{animation:zoomPulse 2.2s ease-in-out infinite}
.pm-shot[data-full-anim="float"] .pm-full-hint{animation:zoomFloat 2.6s ease-in-out infinite}
.pm-shot[data-full-anim="spin"] .pm-full-hint svg{animation:zoomSpin 6s linear infinite}
.pm-shot[data-full-anim="ring"] .pm-full-hint::after{
  content:"";position:absolute;inset:-2px;border-radius:50%;
  border:2px solid var(--zoom-c,rgba(234,242,255,.55));
  animation:zoomRing 2.2s ease-out infinite;
}
.pm-shot[data-full-anim="shine"] .pm-full-hint{overflow:hidden}
.pm-shot[data-full-anim="shine"] .pm-full-hint::after{
  content:"";position:absolute;top:-30%;bottom:-30%;width:40%;left:-60%;
  background:linear-gradient(90deg,transparent,rgba(255,255,255,.9),transparent);
  transform:skewX(-18deg);
  animation:zoomShine 2.6s ease-in-out infinite;
}

/* Сама накладка. `display:flex` — чтобы `[hidden]` пришлось гасить явно (та же
   ловушка, что у `.pm-nav`). */
.pm-full{
  position:fixed;inset:0;z-index:6;
  display:flex;align-items:center;justify-content:center;
  animation:pmFade .3s var(--ease);
  /* Отступ кадра от края экрана — ОДИН набор переменных на все пять ступеней
     размера. Он же задаёт, где стоят стрелки, счётчик и крестик: владелец
     (08.10, снимок) обвёл их красным и сказал «надо чтобы все было внутри
     рамки а не снаружи». Второй записью тех же отступов у управления они
     разошлись бы с кадром на первой же правке ступени. */
  --full-pad-y:24px;--full-pad-x:24px;--full-in:12px;
}
.pm-full[hidden]{display:none}
.pm-full-back{
  position:absolute;inset:0;background:rgba(3,5,11,.94);
  -webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);
}

/* Место кадра на экране — настройка владельца («задавать размер»). Меняется
   ОТСТУП сцены, а не размер кадра: сам кадр всегда вписан в то, что ему
   оставили (`object-fit:contain`), поэтому «во весь экран» показывает его
   крупнее, а «с полями» — целиком и с воздухом по краям.

   Отступ задан на сцене, а не на кадре, нарочно: `padding` у сцены сжимает
   место и для картинки, и для видео одинаково, а у самого кадра пришлось бы
   держать две разные записи размеров. */
.pm-full-stage{position:absolute;inset:0;display:flex;
  align-items:center;justify-content:center;
  padding:var(--full-pad-y) var(--full-pad-x)}
.pm-full[data-full-frame="wide"] {--full-pad-y:72px; --full-pad-x:96px}
.pm-full[data-full-frame="inset"]{--full-pad-y:120px;--full-pad-x:16vw}
/* Кадр один и лежит поверх подложки: сцена — слой, а не колонка. */
.pm-full-stage>*{width:100%;height:100%;object-fit:contain;z-index:1}

/* Движение при перелистывании. Виды — те же, что у каруселей сайта
   (PUB_CAROUSEL_FX) и у мини-карусели: владелец просил «эфекты у стрелок при
   перелистывании», а третий словарь тех же эффектов разошёлся бы с двумя
   первыми. Правило есть у КАЖДОГО вида из списка: настройка без правила — это
   настройка, которая ничего не делает, и владелец решил бы, что она сломана.
   Кадр приходит ЗАНОВО, поэтому кадры движения играют сами, без скрипта. */
@keyframes fullFade{from{opacity:0}to{opacity:1}}
@keyframes fullSlide{from{opacity:.3;transform:translateX(9%)}to{opacity:1;transform:none}}
@keyframes fullZoom{from{opacity:0;transform:scale(.86)}to{opacity:1;transform:none}}
@keyframes fullFlip{
  from{opacity:0;transform:perspective(1400px) rotateY(-70deg)}
  to{opacity:1;transform:none}
}
@keyframes fullCube{
  from{opacity:.2;transform:perspective(1600px) rotateY(-58deg) translateZ(-140px)}
  to{opacity:1;transform:none}
}
@keyframes fullSwing{
  from{opacity:.25;transform:perspective(1200px) translateX(11%)
       rotateY(-24deg) rotate(3deg)}
  to{opacity:1;transform:none}
}
@keyframes fullSpin{
  from{opacity:.2;transform:scale(.74) rotate(-16deg)}
  to{opacity:1;transform:none}
}
@keyframes fullRise{from{opacity:.35;transform:translateY(12%)}to{opacity:1;transform:none}}
@keyframes fullTilt{
  from{opacity:.3;transform:perspective(1200px) translateX(13%) rotateX(26deg)}
  to{opacity:1;transform:none}
}
.pm-full[data-full-fx="fade"]  .pm-full-stage>*{animation:fullFade .5s var(--ease) both}
.pm-full[data-full-fx="slide"] .pm-full-stage>*{animation:fullSlide .5s var(--ease) both}
.pm-full[data-full-fx="zoom"]  .pm-full-stage>*{animation:fullZoom .5s var(--ease) both}
.pm-full[data-full-fx="flip"]  .pm-full-stage>*{animation:fullFlip .55s var(--ease) both}
.pm-full[data-full-fx="cube"]  .pm-full-stage>*{animation:fullCube .55s var(--ease) both}
.pm-full[data-full-fx="swing"] .pm-full-stage>*{animation:fullSwing .55s var(--ease) both}
.pm-full[data-full-fx="spin"]  .pm-full-stage>*{animation:fullSpin .55s var(--ease) both}
.pm-full[data-full-fx="rise"]  .pm-full-stage>*{animation:fullRise .5s var(--ease) both}
.pm-full[data-full-fx="tilt"]  .pm-full-stage>*{animation:fullTilt .55s var(--ease) both}
/* «Без эффекта» — кадр встаёт сразу. Правило нужно, а не «и так сойдёт»:
   иначе вид унаследовал бы движение соседнего правила по порядку в файле. */
.pm-full[data-full-fx="none"]  .pm-full-stage>*{animation:none}

/* Стрелки просмотра — ТЕ ЖЕ кнопки, что листают кадры у снимка, и вид у них
   ОБЩИЙ (SHOP_ARROW_*). Своё только место и размер: кадр во весь экран, и
   мелкая стрелка на нём потерялась бы. Выключенные прячет скрипт, а гасит их
   общее правило `.pm-nav[hidden]`. */
.pm-full .pm-nav{--arr-base:54px;z-index:3}
/* Место управления — ВНУТРИ САМОГО КАДРА.

   Владелец дважды обводил это красным. Сначала (08.10, утро) — стрелки у
   краёв ЭКРАНА, счётчик под кадром и крестик в углу экрана: «надо чтобы все
   было внутри рамки а не снаружи». Управление перенесли к отступу сцены. Но
   отступ сцены — это место, ОСТАВЛЕННОЕ кадру, а сам кадр вписан в него
   целиком (`object-fit:contain`): у высокого снимка по бокам остаются широкие
   поля, у широкого — сверху и снизу. Отсчитанное от отступа управление
   вставало в эти поля, то есть СНАРУЖИ снимка, — и владелец обвёл его снова
   (вечер, снимок #1: крестик в правом верхнем углу ЭКРАНА, левая стрелка у
   левого края экрана, счётчик под кадром).

   Поэтому теперь меряем САМУ КОРОБКУ КАДРА. Скрипт кладёт её края
   переменными (`--full-img-*`, см. `fullPlaceControls`), а правила считают
   место от них. Управление ложится на кадр при любой из пяти ступеней
   размера и при любой форме снимка — фото, видео или гиф.

   Запасное значение в каждой переменной — прежнее поведение: страница,
   сохранённая до этой правки, остаётся рабочей.

   Селектор — ДВА класса, а не один. В разметке у стрелок просмотра стоит
   `class="pm-nav pm-full-nav pm-nav-prev"`: класса `pm-full-nav-prev` не
   существует вовсе, и правило с ним не совпадало НИ С ЧЕМ. Стрелки при этом
   оставались на месте от `.pm-nav-prev{left:12px}` — то есть у края ЭКРАНА,
   ровно как на снимке владельца. Два класса дают силу выше, чем у одного,
   поэтому победа больше не зависит от порядка правил в файле. */
.pm-full-nav.pm-nav-prev{left:calc(var(--full-img-l,var(--full-pad-x)) + var(--full-in))}
.pm-full-nav.pm-nav-next{right:calc(var(--full-img-r,var(--full-pad-x)) + var(--full-in))}

/* Счёт кадров: «3 / 12». Без него непонятно, кончились кадры или сломалось
   листание. */
.pm-full-count{
  position:absolute;left:50%;bottom:calc(var(--full-img-b,var(--full-pad-y)) + var(--full-in));
  transform:translateX(-50%);z-index:3;
  padding:6px 14px;border-radius:999px;
  border:1px solid var(--stk-line,rgba(234,242,255,.42));
  background:rgba(6,10,18,.72);color:var(--stk-c,#eaf6ff);
  font-size:.86rem;font-variant-numeric:tabular-nums;
  -webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
  pointer-events:none;
}
.pm-full-count[hidden]{display:none}

/* Закрыть просмотр. Крестик, а не только Escape и подложка: на телефоне
   клавиатуры нет, а промах по подложке кадр не закрывает. */
.pm-full-x{
  position:absolute;
  right:calc(var(--full-img-r,var(--full-pad-x)) + var(--full-in));
  top:calc(var(--full-img-t,var(--full-pad-y)) + var(--full-in));z-index:3;
  width:44px;height:44px;padding:0;border-radius:50%;cursor:pointer;
  display:flex;align-items:center;justify-content:center;
  color:var(--stk-c,#eaf6ff);
  border:1px solid var(--stk-line,rgba(234,242,255,.42));
  background:rgba(6,10,18,.55);
  -webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
  transition:background .25s var(--ease),color .25s var(--ease),
    transform .2s var(--ease);
}
.pm-full-x svg{width:22px;height:22px;fill:none;stroke:currentColor;
  stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.pm-full-x:hover{background:var(--stk-c,#eaf6ff);color:#0a1018;transform:scale(1.06)}
.pm-full-x:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}

/* На узком экране кнопка, стрелки и поля мельче: кадр и без того во всю
   ширину, и крупные украшения съели бы его. */
@media (max-width:620px){
  .pm-full-hint{width:46px;height:46px}
  .pm-full-hint svg{width:20px;height:20px}
  .pm-full .pm-nav{--arr-base:40px}
  .pm-full[data-full-frame="wide"] {--full-pad-y:52px; --full-pad-x:24px}
  .pm-full[data-full-frame="inset"]{--full-pad-y:72px; --full-pad-x:9vw}
  .pm-full{--full-in:8px}
  .pm-full-count{font-size:.8rem}
  .pm-full-x{width:38px;height:38px}
}

/* Место ПОД ФОТОГРАФИЯМИ. Владелец (06.10) обвёл его красным дважды —
   отдельно под блок «Цвет и размер», отдельно под группу покупки, — и
   просил, чтобы положение выбиралось у каждой карточки товара в панели.

   Здесь только ГНЁЗДА; сами блоки переставляет скрипт (js/main.js,
   placeWindow). Отсюда три правила.

   Пустое место не оставляет следа. Прячет его скрипт — по ВИДИМОМУ
   содержимому, а не по факту переезда: у товара без цветов и размеров блока
   выбора нет вовсе, и место под него тоже не нужно. Здесь только страховка
   на случай, если скрипт не успел.

   Отступы блокам даём их собственные (`margin-top` у .pm-opts и .pm-code), и
   это нарочно: с `gap` у контейнера пустое гнездо между двумя блоками всё
   равно дало бы лишнюю щель. */
/* `flex:none` — по той же причине, что у ряда миниатюр выше: сгибаемый
   элемент в переполненной колонке сжался бы, и «Цвет и размер» с кнопкой
   влезли бы в неё сплющенными вместо того, чтобы уйти в прокрутку.

   СГИБАЕМАЯ КОЛОНКА, как `.pm-info`. Владелец переносит блоки между
   колонками сам, и колонки обязаны вести себя ОДИНАКОВО: вид блока не может
   зависеть от того, справа он или под фотографиями. Здесь стояло
   `display:block`, и одиночная кнопка уезжала к ЛЕВОМУ краю — её центрирует
   `align-self`, а он в блочном родителе не действует (замерено 06.10:
   −147px от середины колонки, проверка `test-shop-buttons` краснела на
   верном окне). `gap` по-прежнему НЕ задаём — отступы у блоков свои. */
.pm-under{display:flex;flex-direction:column;flex:none}
.pm-under[hidden]{display:none}
.pm-under > div:empty{display:none}

/* Таймер акции и плашка промокода В ОКНЕ — теснее, чем на карточке.

   Именно здесь, а не в правилах самих `.car-promo` и `.code-in`: они стоят
   ещё и на карточках товаров, а о карточках владелец не жаловался — менять
   их вид нельзя. Селектор из двух классов бьёт одиночный, поэтому порядок
   правил в файле тут ни на что не влияет.

   Числа — из замера (07.10, товар «Шалаш», экран 1440x900). Левая колонка
   задаёт высоту строки окна, и ей не хватало 41px до потолка `96vh`; эти
   четыре просвета отдают 24px, остальное — отступы блоков правой колонки и
   просвет самой колонки. Ни кегль, ни размеры плашек не тронуты. */
.pm .car-promo{gap:5px;margin-top:2px;padding:3px 11px}
.pm-code .code-in{padding:10px 24px}

/* ЗАПАС ПОД РАЗМАХ «КАЧЕЛЕЙ» — плашка поворачивается и выходит за свою рамку.

   Владелец (08.10, снимки #3–#6): «банер со скидкой вниз движеться и задевает
   количество товара, такого не должно быть… он не должен так задевать все при
   движении вверх и вниз». Он прав, и вот почему это было не видно глазами.

   `transform` НЕ двигает соседей по потоку: плашка поворачивается ВНУТРИ
   своего места, и место остаётся прежним. Задевает она только тем, что её
   собственные углы выходят за её собственную рамку. Замерено пробой
   `_probe-overlap.js` на товаре «Шалаш» (его настройки: `promoAnim:"swing"`,
   `stockAnim:"breathe"`) — наименьший просвет за 5 секунд наблюдения:

     карточка:  плашка отсчёта → строка остатка  −7.5px, площадь 1032px²
                строка цен     → плашка отсчёта  −4.7px, площадь 1421px²
     окно:      плашка промокода → плашка отсчёта −8.1px, площадь 2119px²
                плашка отсчёта → строка остатка  −6.6px, площадь 1047px²

   Размах плашки за тот же замер — 11.5px на карточке и 10px в окне, и это
   ровно `W·sin(2.2°)` от угла поворота в `stkSwing`: 302px × 0.0384 = 11.6px
   и 263px × 0.0384 = 10.1px. То есть размах считается, а не подбирается.

   ЗАПАС «КАЧЕЛЕЙ» БЕРЁМ ОТ ШИРИНЫ КОЛОНКИ, а не плашки, и это не
   приблизительность: процентный отступ ВСЕГДА считается от ширины
   содержащего блока, а плашка никогда не шире его (`max-width:100%`). Значит
   4% колонки заведомо больше, чем 3.84% любой плашки, — и запаса хватает при
   любом размере и любой длине надписи, без единого числа, подобранного на
   глаз. Два пикселя сверх — на субпиксельное округление и на «дыхание»
   строки остатка (её `stkBreathe` растит коробку на 0.6px с каждой стороны,
   замерено). У остальных движений запас другой и с числом — почему, сказано
   ниже.

   Стоит ПРАВИЛО ЗДЕСЬ, а не в разделе плашек, по одной причине: у окна есть
   своё, более тесное `.pm .car-promo` выше (`margin-top:2px`). Точность у
   него и у запаса ОДИНАКОВАЯ (класс с признаком — это те же 0,2,0, что и два
   класса), поэтому решает порядок в файле, и запас обязан стоять НИЖЕ. Это и
   есть единственная причина, по которой правило живёт в разделе окна.

   ЗАДЕВАЮТ НЕ ОДНИ «КАЧЕЛИ». Набор движений у плашки и у строки остатка
   ОДИН и тот же (девять штук), и геометрию двигают пять из них. Промер
   `test-promo-clearance.js` по всей лестнице (наименьший зазор за 6 с,
   минус = налезли) — до этого запаса:

     движение   карточка   окно
     swing        2.1      7.8     поворот, запас уже стоял
     float        3.0     −1.0     перенос на 3px вверх
     pop          1.0     −3.0     перенос на 5px вверх
     pulse        1.5      0.8     рост коробки в 1.07
     breathe      2.4      1.3     рост коробки в 1.04
     blink        3.7      2.0     только прозрачность
     glow         3.7      2.0     только тень
     shine        3.7      2.0     только блик по псевдоэлементу

   То есть «float» и «pop» задевали ровно так же, как «качели», и владелец
   сказал про это же: «не должен так задевать все при движении вверх и вниз».
   Запас стоял только у «качелей», и это была половина правки.

   ПОЧЕМУ ДВА РАЗНЫХ ЗАПАСА, А НЕ ОДИН. Размах «качелей» — доля ШИРИНЫ
   (`W·sin 2.2°`), и на широкой колонке окна он доходит до 10.5px; перенос
   же (float, pop) и рост коробки (pulse, breathe) — ЧИСЛО пикселей, от
   ширины не зависящее: 3px, 5px, 1.2px и 0.7px. Общий запас пришлось бы
   брать по худшему (4% колонки = 21.4px в окне) и платить сорока тремя
   пикселями высоты окна даже за «дыхание» — а высота окна у владельца на
   счету. Поэтому у качелей запас от ширины, у остальных — числом, по
   замеренному размаху: 5px (pop) + 2px на субпиксельное округление.

   ЗАПАС ХОДИТ ЗА ДВИЖЕНИЕМ, а не за элементом: движение у плашки одно, и
   правила эти друг друга не перебивают — признак один и тот же, значение
   разное. Высота окна от смены движения поэтому меняется, и это верно:
   «качели» и вправду размашистее «дыхания». */
.car-promo[data-promo-anim="swing"]{margin-block:calc(4% + 2px)}
.car-promo[data-promo-anim="float"],
.car-promo[data-promo-anim="pop"],
.car-promo[data-promo-anim="pulse"],
.car-promo[data-promo-anim="breathe"]{margin-block:7px}
/* Строка остатка. Движения у неё те же девять, но задевать успевают не
   пять, а три: поворот и два переноса. «Дыхание» и «пульсация» растили её
   коробку на 0.6px и 1.2px с каждой стороны, и зазора хватало (замерено:
   3.4px и 3.7px на карточке, 2px в окне) — запас им не нужен, и ставить его
   значило бы растить окно впустую. */
.car-stock[data-stock-anim="swing"],
.pm-stock[data-stock-anim="swing"]{margin-block:calc(4% + 2px)}
.car-stock[data-stock-anim="float"],
.car-stock[data-stock-anim="pop"],
.pm-stock[data-stock-anim="float"],
.pm-stock[data-stock-anim="pop"]{margin-block:7px}

/* Свёрнутый длинный текст — из конструктора окна товара.

   Замер, ради которого это появилось: правое поле окна 774px, а содержимого
   в нём 1444px; 869px из них — ОДНО описание. Свёрнутое описание снимает
   689px, и окно перестаёт «не помещаться». Одной расстановкой блоков задача
   не решалась: описание выше окна целиком, и ни в какой колонке не влезло бы.

   Обрезаем ВЫСОТОЙ, а не числом строк: строки зависят от ширины колонки, и
   на телефоне «пять строк» оказались бы вдвое ниже, чем на 1440px.

   Градиентной «шторки» внизу нет намеренно: фон окна — это градиент, и любая
   одноцветная заливка на нём читалась бы полосой (владелец называет такое
   «каша»). Обрыв текста честнее показывает кнопка. */
.pm-fold{position:relative;overflow:hidden}
.pm-fold-btn{
  display:block;margin:6px 0 0;padding:7px 14px;
  font:inherit;font-size:.86rem;font-weight:500;color:var(--ink);
  background:rgba(255,255,255,.06);border:1px solid var(--line);
  border-radius:999px;cursor:pointer;
  transition:background .18s ease,border-color .18s ease;
}
.pm-fold-btn:hover{background:rgba(255,255,255,.12);border-color:rgba(255,255,255,.28)}
.pm-fold-btn[hidden]{display:none}

/* Свои текстовые блоки владельца — тоже из конструктора окна. */
.pm-own{margin:12px 0 0}
.pm-own:first-child{margin-top:0}
.pm-own-title{font-size:1rem;font-weight:600;color:var(--ink);margin:0 0 8px}
.pm-own-body{color:var(--ink-2);font-size:.95rem;font-weight:300;line-height:1.62}

/* Левая колонка стала длиннее окна — и НЕ прокручивается.

   Раньше она прокручивалась: высоту задавал квадратный кадр и ряд
   миниатюр, а с перенесёнными блоками содержимое выходило за `max-height`
   окна, и без прокрутки низ был бы недостижим — и выбор цвета, и кнопка
   «В корзину».

   Прокрутка здесь БЫЛА — `overflow-y:auto` по признаку «что-то переехало».
   Владелец (07.10) обвёл её на снимке и сказал: «ее там никогда не должно
   быть». И он прав дважды.

   Во-первых, полоса внутри окна спорит с полосой страницы и режет колонку
   на глазах. Во-вторых, она появлялась там, где прокручивать НЕЧЕГО: кадр
   фотографии квадратный по ШИРИНЕ колонки, ширина дробная, и высота колонки
   тоже дробная — 850.4px. Строку окна задаёт самая высокая колонка, значит
   её `clientHeight` округляется ВНИЗ до 850, а `scrollHeight` ВВЕРХ до 851,
   и браузер честно рисует полосу на полпикселя перелива. Именно её владелец и
   видел: ползунок во всю высоту. Убрать этот перелив отступами нельзя — он
   вернётся у любой колонки, которая задаёт строку, и при любом дробном
   размере.

   Поэтому прокрутки внутри окна нет ВООБЩЕ, ни у одной колонки. Тесно —
   окно растёт, а листается страница (`.pm[data-pm-grow="1"]`, признак ставит
   js/main.js по замеру). Проверка `test-window` стережёт это прямо: у окна не
   должно быть ни одной прокручиваемой колонки. */

/* Поля здесь не для красоты. Кнопка стоит вплотную к левому и нижнему краю
   колонки, а при наведении вокруг неё разливается свечение. Контейнер с
   overflow срезал его по краям, и вместо мягкого ореола получался
   обрубленный прямоугольник — владелец именно это и заметил: «эффект
   выглядит не полный». Поля дают свечению место растечься. */
.pm-info{display:flex;flex-direction:column;min-width:0;
  padding:2px 10px 26px 10px;
  /* `scrollbar-width:thin` здесь БЫЛ — и это оказалось ошибкой.

     Владелец (06.10) обвёл полосу прокрутки в окне товара крупным планом и
     сказал: «прокрутка не соотвествует стилю сайта везде». Причина именно в
     этой строке: `scrollbar-width` заставляет Chrome ИГНОРИРОВАТЬ
     `::-webkit-scrollbar-*`, и вместо сайтовой полосы (10px, градиентная
     ручка, дорожка #05080f) окно рисовало родную тонкую — серую и чужую.
     Убрали — полоса стала сайтовой, как и просили.

     С 07.10 здесь нет и `overflow-y:auto`: прокрутки внутри окна нет вообще,
     и полосы тут быть не может ни при каком размере. Разбор — у правила
     `.pm-media` выше и у `.pm[data-pm-grow="1"]`. */
  /* Длинное слово без пробелов рвало окно. Колонка — флекс-элемент сетки, и
     её `min-content` задавало слово целиком: описание «УрнаУрнаУрна…»
     распирало окно шире экрана, а с ним уезжала и горизонтальная полоса.
     overflow-wrap:anywhere разрешает переносить слово ГДЕ УГОДНО и, что
     важно, уменьшает min-content колонки — без этого колонка не сжималась бы
     и окно всё равно распирало. Наследуется на всё внутри — название,
     описание, характеристики. */
  overflow-wrap:anywhere}
/* ОТСТУПЫ МЕЖДУ БЛОКАМИ ОКНА — ТЕСНЕЕ.

   Владелец (07.10) выбрал, откуда взять недостающие пиксели: «сжать отступы
   между блоками». Числа замера на его раскладке (товар «Шалаш», экран
   1440x900): правой колонке нужно 850px, потолок окна `96vh` даёт 810 —
   не хватало 40px. Потолок он трогать не просил, а отступы просил.

   Сжимаем ОТСТУПЫ, а не содержимое: ни кегль, ни фотография, ни кнопка не
   меняются ни на пиксель. Пропорции внутри блока тоже не тронуты — только
   просветы МЕЖДУ блоками и внутренние поля плашек.

   Запас считается не «на глаз»: после сжатия колонка просит 794px вместо
   850, и на экране владельца окно снова укладывается в потолок. Проверка
   `test-window` мерит это на живом окне. */
.pm-badge{align-self:flex-start;margin-bottom:6px;padding:6px 12px;
  border-radius:100px;font-size:.72rem;font-weight:700;letter-spacing:.04em;
  text-transform:uppercase;color:#04121a;background:var(--cyan)}
.pm-badge[data-kind="sale"]{background:var(--magenta);color:#1a0416}
.pm-badge[data-kind="promo"]{background:var(--violet);color:#0b0418}
.pm-badge[data-kind="new"]{background:var(--lime);color:#04180e}
.pm-badge[data-kind="hit"]{background:#ffb454;color:#241300}
.pm-badge[data-kind="custom"]{
  background:var(--shop-badge,rgba(255,255,255,.86));
  color:var(--shop-badge-ink,#0a1018);
}

/* ЧЕТЫРЕ МЕСТА ПЛАШКИ. Владелец (06.10) обвёл её на снимке и попросил
   «менять положение в трёх местах» — над названием, в конце строки цен и в
   правом верхнем углу фотографии, — а в ответе добавил четвёртое: левый
   верхний угол фотографии.

   Гнёзда уже есть в разметке: `.pm-info` (над названием), `.pm-prices`
   (строка цен) и `.pm-shot` (кадр фотографии). Кладёт плашку в нужное
   гнездо js/main.js; здесь — как она в этом гнезде выглядит. Атрибут
   `data-pm-badge-place` ставит скрипт ВСЕГДА, вместе с умолчанием «above»:
   без него правило ниже не сработало бы, и владелец решил бы, что выбор
   места ничего не делает.

   `order` в строке цен, а не перестановка узлов: перенос строки перед
   значком «Ваша выгода» (`deal-br`) скрипт ставит и снимает на каждом
   товаре заново, и вставка «перед ним» зависела бы от того, что успело
   случиться раньше. С порядком плашка всегда стоит в ПЕРВОЙ строке у
   правого края, а значок — после разрыва, на своей. */
.pm-badge[data-pm-badge-place="prices"]{
  order:1;align-self:center;margin:0 0 0 auto;
}
.pm-prices .deal-br{order:2}
.pm-prices .pm-deal{order:3}
/* ВЫГОДА РЯДОМ С ЦЕНОЙ, «ХИТ ПРОДАЖ» — У ПРАВОГО КРАЯ (владелец, 07.10).

   Он обвёл обе плашки на снимке окна: «надо их поменять местами чтобы выгода
   была рядом с ценой, а хит продаж справа заместо выгоды». До этого у правого
   края стояла выгода, а «Хит продаж» — сразу за ценой.

   Отступы переставляем ВМЕСТЕ с порядком, и это не украшение: `margin-left:
   auto` и означает «прижмись к правому краю». Оставь его у выгоды — она снова
   уехала бы вправо, а плашка встала бы в середине строки (ровно этот капкан
   описан ниже, у `.pm-deal[hidden]`).

   Правило под `:has` и с `:not(:has(.deal-br))` нарочно: когда выгоде не
   хватило места и скрипт поставил перед ней перенос (размер «очень крупный»),
   меняться местами нечем — выгода и так на своей строке. Тогда порядок
   остаётся прежним: плашка в строке цен у правого края, выгода под ценой. */
.pm-prices:has(.pm-badge[data-pm-badge-place="prices"]):not(:has(.deal-br)) .pm-deal{
  order:1;margin-left:0;
}
.pm-prices:has(.pm-badge[data-pm-badge-place="prices"]):not(:has(.deal-br)) .pm-badge[data-pm-badge-place="prices"]{
  order:3;margin:0 0 0 auto;
}
/* На фотографии — тот же вид, что на карточке: тот же угол в 12px и тот же
   тёмный ореол. `pointer-events:none` — нажимать надо на СНИМОК: плашка его
   закрывает, и без этого щелчок по ней не открывал бы лупу. */
.pm-badge[data-pm-badge-place="photo-r"],
.pm-badge[data-pm-badge-place="photo-l"]{
  position:absolute;top:12px;z-index:2;margin:0;align-self:auto;
  box-shadow:0 6px 18px rgba(0,0,0,.35);pointer-events:none;
}
.pm-badge[data-pm-badge-place="photo-r"]{right:12px}
.pm-badge[data-pm-badge-place="photo-l"]{left:12px}
/* Название и цена в окне — теми же цветами, что на карточке. Владелец
   задаёт их один раз в панели, и одно и то же название не должно на сайте
   выглядеть двумя разными. Переменные вешает js/main.js и на секцию
   магазина, и на это окно: окно лежит в конце страницы, вне секции. */
.pm-name{margin:0 0 6px;font-size:1.42rem;font-weight:800;
  letter-spacing:-.02em;line-height:1.22;color:var(--shop-name,var(--ink))}
/* position:relative — ради пояснения к значку у цены: оно всплывает над
   строкой цен и не должно толкать ни цену, ни название. */
.pm-prices{display:flex;align-items:baseline;gap:12px;margin-bottom:10px;
  flex-wrap:wrap;position:relative}
.pm-price{font-size:1.66rem;font-weight:800;color:var(--shop-price,#fff);letter-spacing:-.02em}
.pm-old{font-size:1.02rem;color:var(--ink-3);text-decoration:line-through}
/* Значок у цены в окне товара: та же плашка, что на карточке, — владелец
   отметил это место красной рамкой. Слот нужен, чтобы прятать плашку у
   товара без значка: у самой плашки «скрыт» не бывает, она появляется
   вместе с содержимым. Размер тот же, что на карточке: настройка размера
   должна значить одно и то же везде, а не расти от окна к карточке.

   `margin-left:auto` — как на карточке: плашка встаёт у ПРАВОГО края
   строки цен, а не сразу за ценой. Без него `margin-left:auto` самой
   плашки не работает: она лежит внутри этого слота, а слот сжимается по
   содержимому — свободного места внутри него нет, и auto делить нечего. */
.pm-deal{display:inline-flex;align-items:center;margin-left:auto}
/* Пустой слот значка не должен участвовать в строке.

   `.pm-deal` объявлен `inline-flex`, а это отменяет `[hidden]` — правило
   браузера про `display:none` слабее. Ширина у пустого слота нулевая, и
   потому поломки не было видно, НО его `margin-left:auto` всё равно делит
   свободное место в строке. Пока в строке был один такой отступ, это ничего
   не значило; с плашкой «Хит продаж», которую владелец может поставить в
   строку цен, свободное место стало делиться ПОРОВНУ — и плашка вставала
   посередине строки вместо правого края (замер: правый край плашки 1108 при
   правом крае строки 1203, `margin-left` вышел 83.14px вместо auto).

   У всех прочих слотов окна правило `[hidden]{display:none}` есть; у этого
   его не было, и это единственный слот, который ставит `display` сам. */
.pm-deal[hidden]{display:none}
/* Отступ сверху — как у соседей по колонке (`.pm-opts`, `.pm-specs`).
   Раньше его не было вовсе, и описание начиналось вплотную под кнопками
   выбора: владелец обвёл этот стык красной полосой и попросил «сделать
   интервал, чтобы не так близко текст». Пропуск принадлежит БЛОКУ, а не
   соседу сверху: описание владелец может перетащить, и отступ обязан
   уехать вместе с ним. */
.pm-desc{margin:12px 0 0;color:var(--ink-2);font-size:.95rem;font-weight:300;line-height:1.62}
.pm-desc p{margin:0 0 12px}
.pm-desc p:last-child{margin-bottom:0}

.pm-specs{margin:12px 0 0;padding:12px 0 0;border-top:1px solid var(--line);
  display:grid;grid-template-columns:auto 1fr;gap:9px 18px;font-size:.9rem}
/* Та же беда, что у выбора цвета и размера: `hidden` слабее `display`, и
   список характеристик у товара БЕЗ характеристик оставался на экране —
   пустой полосой 13px с разделительной чертой над ней. Владелец (08.10,
   вечер, снимок #3): «если у товара не заданы эти значения то их не надо
   показывать». Скрипт прячет список верно (`specsEl.hidden = …children.length
   === 0`), правило страницы это отменяло. Нашлось не глазами, а пробой
   `_probe-hidden.js`: она спрашивает ЖИВУЮ страницу про каждый узел с
   `hidden` — какой у него вычисленный `display`. */
.pm-specs[hidden]{display:none}
.pm-specs dt{color:var(--ink-3)}
.pm-specs dd{margin:0;color:var(--ink);font-weight:500}

.pm-delivery{margin:12px 0 0;padding:11px 14px;border-radius:var(--r-sm);
  border:1px solid rgba(107,255,184,.28);background:rgba(107,255,184,.07);
  color:var(--lime);font-size:.86rem}

/* Остаток в окне товара. Тот же смысл, что у карточки, но в окне строка
   шире — держимся строкой под ценой, а не плашкой: это справка, а не
   предложение. Скрытый слот не оставляет отступа: `hidden` на span ничего
   не рисует, но `display:block` ниже отменил бы это — поэтому явное
   правило для скрытого состояния. */
.pm-stock{display:flex;align-items:center;justify-content:center;gap:6px;margin-top:4px;
  /* Та же пара приёмов, что у `.car-stock`: `justify-content` центрирует
     содержимое строки без плашки, пара auto — саму плашку. Ширину задаём
     здесь потому, что колонка окна — flex со `stretch`: без `width` строка
     растянулась бы на всю колонку, и auto делить было бы нечего. */
  width:fit-content;margin-left:auto;margin-right:auto;
  font-size:.82rem;font-weight:600;
  color:var(--stk-c,var(--ink-3));letter-spacing:-.01em}
.pm-stock[hidden]{display:none}

/* Артикул в окне товара. Владелец (10.10) обвёл это место на снимке и
   попросил показывать номер там: «в карточке товара он должен быть вот в
   этом месте». Место — под строкой «в наличии».

   Строка остатка стоит ПО ЦЕНТРУ (`justify-content:center` с `auto` по
   бокам), а артикул — СЛЕВА под ней: это справка о товаре, и читают её
   слева, как строку документа. Поэтому `display:block` и `text-align:left`,
   а не пара `auto`, которой держится остаток.

   Цвет и размер — признаками, как у соседей (`data-sku-color`/
   `data-sku-size`), и из ТОЙ ЖЕ палитры: переменные ставятся на сам
   элемент, поэтому общее имя `--stk-c` никого ни к чему не обязывает — у
   артикула свой цвет, у строки остатка свой (тот же уговор, что у таймера и
   стрелок). «Обычный» размер правила не имеет — это кегль ниже. */
.pm-sku{display:block;margin-top:6px;text-align:left;
  font-size:.82rem;font-weight:600;letter-spacing:-.01em;
  color:var(--stk-c,var(--ink-3))}
/* Лестница та же, что у строки остатка на карточке, — все семь ступеней
   списка владельца. У `.pm-stock` их только три (s/l/xl), и это НЕ образец
   для подражания: в окне остальные ступени просто падали в «обычный», и
   владелец, выбрав «Огромную», не увидел бы перемены. */
.pm-sku[data-sku-size="xs"]{font-size:.58rem}
.pm-sku[data-sku-size="s"]{font-size:.66rem}
.pm-sku[data-sku-size="l"]{font-size:.9rem}
.pm-sku[data-sku-size="xl"]{font-size:1.06rem}
.pm-sku[data-sku-size="xxl"]{font-size:1.23rem}
.pm-sku[data-sku-size="xxxl"]{font-size:1.44rem}
.pm-sku[hidden]{display:none}

/* ============================================================
   БОНУСНАЯ ПРОГРАММА — показ выгоды и баланса
   ------------------------------------------------------------
   Четыре места: строка выгоды на карточке (`.car-bonus`) и в окне товара
   (`.pm-bonus`), значок баланса в шапке (`.bonus-badge`), галочка списания
   в корзине (`.cart-bonus`) и раздел «Мои бонусы` в кабинете (`.acc-bonus`).

   Цвет берётся из ОБЩЕЙ палитры магазина (`--stk-*`) — того же списка,
   которым владелец красит артикул, остаток и промокод. Своих цветов здесь
   нет нарочно: владелец выбирает один оттенок и ждёт, что он будет один и
   на карточке, и в окне. Атрибут `data-bonus-color` добавлен к каждому
   правилу палитры выше — без него выбор цвета ничего бы не делал.
   ============================================================ */
.car-bonus{
  display:flex;align-items:center;gap:5px;margin-top:4px;
  width:fit-content;margin-left:auto;margin-right:auto;
  font-size:.76rem;font-weight:600;color:var(--stk-c,var(--ink-3));
  letter-spacing:-.01em;
}
/* Атрибут hidden отменяется своим display — та же ловушка, что у
   `.car-stock`. Без этой строки спрятанная строка осталась бы пустой
   полоской на карточке. */
.car-bonus[hidden]{display:none}
.pm-bonus{display:block;margin-top:6px;text-align:left;
  font-size:.82rem;font-weight:600;letter-spacing:-.01em;
  color:var(--stk-c,var(--ink-3))}
.pm-bonus[hidden]{display:none}
/* Лестница размеров — все семь ступеней списка владельца, как у артикула.
   «Обычный» правила не имеет: это кегли выше, у карточки и у окна свои. */
.car-bonus[data-bonus-size="xs"],.pm-bonus[data-bonus-size="xs"]{font-size:.58rem}
.car-bonus[data-bonus-size="s"],.pm-bonus[data-bonus-size="s"]{font-size:.66rem}
.car-bonus[data-bonus-size="l"],.pm-bonus[data-bonus-size="l"]{font-size:.9rem}
.car-bonus[data-bonus-size="xl"],.pm-bonus[data-bonus-size="xl"]{font-size:1.06rem}
.car-bonus[data-bonus-size="xxl"],.pm-bonus[data-bonus-size="xxl"]{font-size:1.23rem}
.car-bonus[data-bonus-size="xxxl"],.pm-bonus[data-bonus-size="xxxl"]{font-size:1.44rem}

/* Значок баланса в шапке — рядом с кабинетом. Основа та же, что у кнопки
   корзины (`.cart-btn`), чтобы ряд не разъезжался: свои отступы и своя
   высота сделали бы «прыжок» соседних кнопок. Свечение — от выбранного
   оттенка; пока владелец его не выбрал, работает цвет сайта. */
.bonus-badge{
  position:relative;display:inline-flex;align-items:center;gap:8px;
  padding:10px 15px;border-radius:100px;cursor:pointer;
  border:1px solid var(--line);background:rgba(255,255,255,.03);
  color:var(--ink);font-size:.86rem;font-weight:600;
  transition:border-color .3s,color .3s,box-shadow .3s;
  --bns-c:var(--stk-c,var(--cyan));
  --bns-glow:var(--stk-glow,rgba(34,230,255,.3));
}
.bonus-badge[hidden]{display:none}
.bonus-badge svg{width:17px;height:17px;flex:none;fill:none;
  stroke:var(--bns-c);stroke-width:1.7;stroke-linejoin:round}
.bonus-badge:hover{border-color:var(--bns-c);color:#fff;
  box-shadow:0 0 24px var(--bns-glow)}
.bonus-badge:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
.bonus-badge-label{white-space:nowrap}
@media (max-width:920px){
  /* На узком экране подпись значка уходит: ряд кнопок в шапке и без неё
     не помещается, а сама кнопка со звездой остаётся и ведёт в кабинет. */
  .bonus-badge-label{display:none}
  .bonus-badge{padding:10px 12px}
}

/* Галочка «Списать баллы» в корзине — между списком товаров и итогом.
   Строкой-подписью, а не полем формы: это выбор покупателя, а не данные
   заказа, и попадать в «Проверьте поля» он не должен. */
.cart-bonus{
  display:flex;align-items:flex-start;gap:10px;margin-top:14px;
  padding:11px 14px;border-radius:var(--r-sm);cursor:pointer;
  border:1px solid var(--line);background:rgba(255,255,255,.02);
}
.cart-bonus[hidden]{display:none}
.cart-bonus-box{width:18px;height:18px;flex:none;margin-top:1px;
  accent-color:var(--stk-c,var(--cyan));cursor:pointer}
.cart-bonus-box:disabled{cursor:default;opacity:.5}
.cart-bonus-text{display:block;min-width:0;line-height:1.4}
.cart-bonus-cap{display:block;font-size:.88rem;font-weight:600;color:var(--ink)}
.cart-bonus-note{display:block;margin-top:2px;font-size:.75rem;color:var(--ink-3)}
/* Строка списания в итоге. Цвет — как у скидки по промокоду: обе строки
   про «стало дешевле», и разный цвет читался бы как разные вещи. */
.cart-sum > [data-cart-bonus-sumrow]{flex:1 1 100%}
.cart-sum-row [data-cart-bonus-sumcut],
.cart-sum-row [data-cart-bonus-sumcut2]{color:#8fd8a0}

/* ---------- «Мои бонусы» в кабинете ---------- */
.acc-bonus{
  margin:14px 0 4px;padding:14px 16px;border-radius:var(--r-sm);
  border:1px solid var(--line);background:rgba(255,255,255,.02);
}
.acc-bonus[hidden]{display:none}
.acc-bonus-cap{margin:0 0 10px;font-size:.95rem;font-weight:700;color:var(--ink)}
.acc-bonus-top{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap}
.acc-bonus-bal{font-size:.86rem;color:var(--ink-3)}
.acc-bonus-bal b{font-size:1.5rem;font-weight:800;color:#fff;letter-spacing:-.02em}
.acc-bonus-next{font-size:.78rem;color:#ffc48a}
.acc-bonus-next[hidden]{display:none}
.acc-bonus-hold{margin:8px 0 0;font-size:.76rem;line-height:1.45;color:var(--ink-3)}
.acc-bonus-hold[hidden]{display:none}
.acc-bonus-hist{list-style:none;margin:12px 0 0;padding:10px 0 0;
  border-top:1px solid var(--line)}
.acc-bonus-hist:empty{display:none}
.acc-bonus-row{display:grid;grid-template-columns:minmax(0,1fr) auto;
  gap:2px 12px;padding:7px 0;border-bottom:1px solid var(--line)}
.acc-bonus-row:last-child{border-bottom:0}
.acc-bonus-row-label{min-width:0;font-size:.82rem;color:var(--ink-2);
  overflow:hidden;text-overflow:ellipsis}
.acc-bonus-row-val{font-size:.85rem;font-weight:700;white-space:nowrap}
.acc-bonus-row[data-sign="plus"] .acc-bonus-row-val{color:#8fd8a0}
.acc-bonus-row[data-sign="minus"] .acc-bonus-row-val{color:#ff9aa8}
.acc-bonus-row-at{grid-column:1 / -1;font-size:.72rem;color:var(--ink-3)}
.acc-bonus-empty{margin:0;padding:7px 0;font-size:.78rem;color:var(--ink-3)}

/* ПРОМОКОД ТОВАРА — плашка по центру над кнопкой «В корзину».

   Владелец (04.10): «плашка ... должна выглядить красиво и мог в админ
   панели ее настраивать по рамке выбирать по цвету по анимации и движению».
   Четыре настройки — четыре признака на элементе: `data-code-frame`,
   `data-code-color`, `data-code-anim`, `data-code-motion`. Признаками, а не
   классами вида `is-ticket`: оформление само решает, что значит каждая
   ступень, и новая ступень не требует правки скрипта.

   Цвет берётся из ОБЩЕЙ палитры магазина (`--stk-c`, `--stk-bg`,
   `--stk-line`, `--stk-glow`) — той же, что у строки остатка, таймера акции
   и стрелок листания. Свой цвет владельца приходит с признаком `custom` и
   тремя переменными; пусто — значит «цвет сайта», и работают запасные.

   ДВА СЛОЯ. Движение живёт на внешнем (`.code-plate`), рамка и анимация —
   на внутреннем (`.code-in`). Не для красоты: и движение, и анимация пишут
   в `transform`, и на одном элементе «подпрыгивает» плюс «пульсация»
   съели бы друг друга — победило бы то правило, что ниже в файле. Разные
   слои складывают их, как владелец и ожидает. */
/* Слот промокода — СТОЛБИКОМ, а не строкой: в нём теперь до двух вещей —
   плашка с кодом и поле «у меня есть промокод». Строкой они встали бы
   рядом и на узком окне разъехались бы в кашу. */
.pm-code{margin-top:12px;display:flex;flex-direction:column;align-items:center;gap:8px}
/* Пустой слот прячем целиком, а не оставляем полосой: `hidden` отменяется
   своим `display`, поэтому правило явное. */
.pm-code[hidden]{display:none}
/* Плашка стоит между выбором цвета и кнопкой. Свой отступ сверху она уже
   задала; здесь — отступ ДО кнопки. Без этого правила кнопка получила бы
   свои 22px плюс отступ плашки, и «по центру над кнопкой» превратилось бы
   в дыру. */
.pm-code + .pm-buy{margin-top:14px}

/* Цвет «выреза» купона: тот же тёмный, что у подложки окна товара.
   Объявлен на ВНЕШНЕМ слое, а не на внутреннем: переменные наследуются
   только вниз, а вырезы рисует внешний слой — с внутреннего он бы этой
   переменной не увидел и залил бы вырезы прозрачным. */
.code-plate{position:relative;display:inline-flex;max-width:100%;--code-cut:#0d1424}
.code-in{
  position:relative;
  display:inline-flex;flex-direction:column;align-items:center;gap:9px;
  max-width:100%;padding:15px 24px;border-radius:15px;
  /* Рамка есть всегда, но прозрачная: `border` участвует в раскладке, и
     «без рамки» через `border:0` сдвигало бы содержимое на пиксель при
     каждой смене вида. */
  border:1px solid transparent;
  background:var(--stk-bg,rgba(120,160,220,.14));
  color:var(--stk-c,var(--ink-2));
  text-align:center;
}
.code-cap{font-size:.82rem;font-weight:600;letter-spacing:-.01em;opacity:.92}
.code-row{display:inline-flex;align-items:center;gap:12px;flex-wrap:wrap;
  justify-content:center;max-width:100%}
/* Код — моноширинным, с разрядкой: его переписывают в корзину руками, и в
   обычном шрифте «0» не отличить от «O», а «1» от «l». Это не придирка, а
   прямая причина неработающего промокода у покупателя. */
.code-val{
  font-family:'JetBrains Mono',ui-monospace,SFMono-Regular,monospace;
  font-size:1.04rem;font-weight:700;letter-spacing:.14em;
  padding:6px 13px 6px 15px;border-radius:10px;
  background:rgba(255,255,255,.07);
  box-shadow:inset 0 0 0 1px var(--stk-line,rgba(120,160,220,.35));
  overflow-wrap:anywhere;
}
.code-copy{
  appearance:none;cursor:pointer;
  padding:7px 14px;border-radius:999px;
  border:1px solid var(--stk-line,rgba(120,160,220,.35));
  background:transparent;color:inherit;
  font:inherit;font-size:.8rem;font-weight:600;white-space:nowrap;
  transition:background .2s var(--ease),border-color .2s var(--ease);
}
.code-copy:hover{background:rgba(255,255,255,.08)}
.code-copy.is-ok{border-color:var(--stk-c,rgba(120,160,220,.6));
  background:var(--stk-bg,rgba(120,160,220,.18))}

/* ---------- поле ввода промокода ----------

   Владелец: «Покупатель вводит промокод и нажимает кнопку применить и
   появляется подсказка что Промокод активирован и краткая информация по
   промокоду и сразу видно изменение цены товара».

   Поле показывается по своему признаку (`codeLive`), а не вместе с плашкой:
   плашку владелец может спрятать, а код при этом продолжает работать — он
   называет его покупателю словами, и вписать его тому должно быть где.
   Ширина не «по содержимому»: поле и кнопка стоят одной строкой, и на
   телефоне им нужно ужиматься вместе с окном, а не вылезать за него. */
.code-enter{
  display:flex;align-items:stretch;gap:8px;flex-wrap:wrap;
  justify-content:center;max-width:100%;
}
.code-field{
  flex:1 1 160px;min-width:0;max-width:230px;
  padding:9px 13px;border-radius:11px;
  border:1px solid var(--stk-line,rgba(120,160,220,.35));
  background:rgba(255,255,255,.05);color:inherit;
  font-family:'JetBrains Mono',ui-monospace,SFMono-Regular,monospace;
  font-size:.88rem;letter-spacing:.08em;text-transform:uppercase;
}
.code-field::placeholder{
  font-family:inherit;font-size:.84rem;letter-spacing:0;
  text-transform:none;opacity:.6;
}
.code-field:focus{outline:none;
  border-color:var(--stk-c,rgba(120,160,220,.7));
  box-shadow:0 0 0 3px var(--stk-glow,rgba(120,160,220,.22))}
/* Погашенное поле — активированный код: менять его нечем, а стереть можно
   кнопкой «Убрать» рядом. Так покупатель видит, ЧТО именно применилось. */
.code-field:disabled{opacity:.85;cursor:default}
.code-apply{
  flex:0 0 auto;padding:9px 16px;border-radius:11px;
  font-size:.86rem;font-weight:600;white-space:nowrap;
  /* Цвет букв задаём ЯВНО и цветом общей палитры магазина. Без этого
     кнопка брала цвет у `.btn:disabled`, а он рассчитан на СВЕТЛУЮ
     подложку: на тёмной сцене окна надпись «Активирован» получалась
     почти чёрной на тёмном — кнопка выглядела пустой коробкой. */
  color:var(--stk-c,var(--ink-2));
}
/* Погашенная кнопка — это «готово», а не «недоступно»: покупатель должен
   прочитать, что код принят. Поэтому цвет букв остаётся читаемым. */
.code-apply:disabled{cursor:default;opacity:1;color:var(--stk-c,var(--ink-2))}
.code-apply.is-ok{
  border-color:var(--stk-c,rgba(120,160,220,.6));
  background:var(--stk-bg,rgba(120,160,220,.18));
}
.code-drop{
  appearance:none;cursor:pointer;flex:0 0 auto;
  padding:9px 12px;border-radius:11px;
  border:1px solid transparent;background:transparent;color:inherit;
  font:inherit;font-size:.82rem;opacity:.7;
  text-decoration:underline;text-underline-offset:3px;
}
.code-drop:hover{opacity:1}
/* Подсказка под полем: и «активирован», и отказ. Цветом их различаем, но не
   одним цветом — отказ ещё и словом сервера, иначе цветной текст не прочтёт
   тот, кто не различает цвета. */
.code-note{
  flex:1 1 100%;margin-top:2px;
  font-size:.82rem;line-height:1.4;opacity:.9;text-align:center;
}
.code-note[data-kind="ok"]{color:var(--stk-c,var(--ink-2))}
.code-note[data-kind="bad"]{color:#e8736a}

/* Строка промокода в корзине: код и сколько он снял. Отдельной плашкой под
   названием товара — покупатель активировал код в окне и обязан видеть в
   корзине, что скидка не потерялась по дороге. */
.cart-code{
  display:inline-block;margin-top:5px;padding:3px 9px;border-radius:999px;
  font-size:.76rem;font-weight:600;letter-spacing:.01em;
  background:var(--stk-bg,rgba(120,160,220,.14));
  color:var(--stk-c,var(--ink-2));
  box-shadow:inset 0 0 0 1px var(--stk-line,rgba(120,160,220,.3));
}

/* ---------- рамка ---------- */
.code-in[data-code-frame="thin"]{border-color:var(--stk-line,rgba(120,160,220,.35))}
.code-in[data-code-frame="double"]{
  border-style:double;border-width:3px;
  border-color:var(--stk-line,rgba(120,160,220,.45))}
.code-in[data-code-frame="dashed"]{
  border-style:dashed;border-color:var(--stk-line,rgba(120,160,220,.5))}
/* Купон: рамка как у тонкой линии, а вырезы рисует внешний слой (ниже).
   Рамка и вырезы живут на разных слоях, поэтому признак стоит на обоих. */
.code-in[data-code-frame="ticket"]{
  border-color:var(--stk-line,rgba(120,160,220,.35))}
.code-in[data-code-frame="glow"]{
  border-color:var(--stk-line,rgba(120,160,220,.45));
  box-shadow:0 0 20px var(--stk-glow,rgba(120,160,220,.35))}
.code-in[data-code-frame="ribbon"]{
  border-color:var(--stk-line,rgba(120,160,220,.35));
  border-left:4px solid var(--stk-c,rgba(120,160,220,.55))}
/* Купон с вырезами: два полукруга по бокам, залитые цветом окна.

   Вырезы живут на ВНЕШНЕМ слое, а рамка — на внутреннем. Оба псевдоэлемента
   у элемента всего два, и на одном слое вырезы подрались бы с бликом и
   искрами, которые тоже рисуются `::before`/`::after`. Поэтому признак
   рамки стоит на обоих слоях: внутренний берёт из него рамку, внешний —
   только вырезы. */
.code-plate[data-code-frame="ticket"]::before,
.code-plate[data-code-frame="ticket"]::after{
  content:'';position:absolute;top:50%;width:13px;height:13px;border-radius:50%;
  background:var(--code-cut);transform:translateY(-50%);pointer-events:none;
  box-shadow:inset 0 0 0 1px var(--stk-line,rgba(120,160,220,.35));
}
.code-plate[data-code-frame="ticket"]::before{left:-7px}
.code-plate[data-code-frame="ticket"]::after{right:-7px}

/* ---------- анимация: что плашка ДЕЛАЕТ ----------
   Ни одна не трогает `color`: цвет владелец задаёт отдельной настройкой, и
   анимация, перебивающая его, была бы обманом. */
.code-in[data-code-anim="pulse"]{animation:codePulse 1.6s ease-in-out infinite}
.code-in[data-code-anim="blink"]{animation:codeBlink 1.8s ease-in-out infinite}
.code-in[data-code-anim="glow"]{animation:codeGlow 2s ease-in-out infinite}
.code-in[data-code-anim="rainbow"]{animation:codeRainbow 4.2s linear infinite}
/* Блик и искры идут поверх надписи, поэтому у внутреннего слоя своё
   отсечение. `pointer-events:none` у полосы — иначе она перехватывала бы
   нажатие на кнопку «Скопировать» в тех местах, где прошёл блик. */
.code-in[data-code-anim="shine"]{overflow:hidden}
.code-in[data-code-anim="shine"]::before{
  content:'';position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(105deg,transparent 34%,rgba(255,255,255,.45) 50%,transparent 66%);
  animation:codeShine 2.7s ease-in-out infinite;
}
.code-in[data-code-anim="sparkle"]{overflow:hidden}
.code-in[data-code-anim="sparkle"]::before,
.code-in[data-code-anim="sparkle"]::after{
  content:'';position:absolute;width:4px;height:4px;border-radius:50%;
  background:var(--stk-c,rgba(255,255,255,.85));
  box-shadow:0 0 7px var(--stk-glow,rgba(255,255,255,.6));
  pointer-events:none;opacity:0;
}
.code-in[data-code-anim="sparkle"]::before{top:16%;left:12%;animation:codeSpark 2.3s ease-in-out infinite}
.code-in[data-code-anim="sparkle"]::after{bottom:18%;right:14%;animation:codeSpark 2.3s ease-in-out .9s infinite}

/* ---------- движение: куда плашка СМЕЩАЕТСЯ ----------
   Внешний слой, поэтому с анимацией выше не спорит. */
.code-plate[data-code-motion="sway"]{animation:codeSway 2.6s ease-in-out infinite}
.code-plate[data-code-motion="float"]{animation:codeFloat 2.4s ease-in-out infinite}
.code-plate[data-code-motion="bounce"]{animation:codeBounce 2.2s ease-in-out infinite}
.code-plate[data-code-motion="slide"]{animation:codeSlide 2.8s ease-in-out infinite}
.code-plate[data-code-motion="tilt"]{
  transform-origin:center bottom;
  animation:codeTilt 3s ease-in-out infinite}

@keyframes codePulse{0%,100%{transform:scale(1)}50%{transform:scale(1.045)}}
@keyframes codeBlink{0%,100%{opacity:1}50%{opacity:.55}}
/* Свечение — через `filter`, а не `box-shadow`: box-shadow уже занят рамкой
   («светящаяся рамка»), и два правила спорили бы за одно свойство. */
@keyframes codeGlow{
  0%,100%{filter:drop-shadow(0 0 0 transparent)}
  50%{filter:drop-shadow(0 0 10px var(--stk-glow,rgba(255,255,255,.5)))}
}
/* Переливание ведёт по кругу оттенок САМОГО цвета владельца, а не подменяет
   его радугой: иначе «свой цвет» пропадал бы на каждой второй секунде. */
@keyframes codeRainbow{0%{filter:hue-rotate(0)}100%{filter:hue-rotate(360deg)}}
@keyframes codeShine{
  0%{transform:translateX(-115%)}
  55%,100%{transform:translateX(115%)}
}
@keyframes codeSpark{
  0%,100%{opacity:0;transform:scale(.4)}
  45%{opacity:1;transform:scale(1)}
}
@keyframes codeSway{0%,100%{transform:rotate(-1.4deg)}50%{transform:rotate(1.4deg)}}
@keyframes codeFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-4px)}}
/* Подпрыгивание с паузой: движение занимает меньше трети цикла, остальное
   плашка стоит. Непрерывный подскок читался бы как дрожь. */
@keyframes codeBounce{
  0%,68%,100%{transform:translateY(0)}
  78%{transform:translateY(-6px)}
  88%{transform:translateY(0)}
}
@keyframes codeSlide{0%,100%{transform:translateX(-5px)}50%{transform:translateX(5px)}}
@keyframes codeTilt{0%,100%{transform:rotate(-2.4deg)}50%{transform:rotate(2.4deg)}}

/* Кому движения мешают — тому их не показываем. Тот же уговор, что у
   остальных движений сайта. Псевдоэлементы перечислены отдельно: полосу
   блика и искры рисуют они, и правило только про сами элементы оставляло бы
   блик бегать по плашке у того, кто просил движений не показывать. */
@media (prefers-reduced-motion:reduce){
  .code-in[data-code-anim],
  .code-plate[data-code-motion]{animation:none}
  .code-in[data-code-anim]::before,
  .code-in[data-code-anim]::after{animation:none;opacity:0}
}

.pm-buy{margin-top:14px;align-self:center;min-width:190px}
/* Одиночную кнопку скрипт гасит атрибутом `hidden`, когда владелец завёл
   свои кнопки. Одного атрибута НЕ ХВАТАЕТ: `.btn` задаёт `display:flex`, а
   он отменяет `hidden` — кнопка оставалась на экране пустой полоской
   (текст ей в этой ветке не подставляется), и в окне висели ЛИШНЯЯ
   кнопка и свои. Правило явное, как у `.pm[hidden]` выше и у тела сгиба
   в панели. Проверка — `test-shop-buttons`, замер — `probe-pm-buttons`. */
.pm-buy[hidden]{display:none}
/* Свечение кнопки в окне чуть собраннее общего: у общего размах 44px, и в
   колонке с прокруткой он всё равно упирался бы в край. */
.pm-buy:hover{
  box-shadow:0 10px 26px rgba(34,230,255,.42),0 0 22px rgba(139,92,255,.45);
}
/* Подсказка стоит ПОД кнопкой «В корзину» — там её место и по смыслу
   («чего не хватает, чтобы товар ушёл в корзину»), и по просьбе владельца
   (06.10): он обвёл сообщение внизу колонки и полосу прямо под кнопкой.
   Само место задаёт раскладка (блок `note` идёт сразу за `buy`), здесь —
   только просвет: 10px читались как «прилипло к кнопке», он просил
   «сделать, чтобы не так близко». */
/* ПОДСКАЗКА ПОД КНОПКОЙ — по центру и по настройке владельца.

   Место задаёт раскладка (блок `note` идёт сразу за `buy`), а выравнивание —
   это правило. Владелец (07.10) обвёл строку красным и сказал: «должен быть
   по центру под кнопкой». Строка — флекс-элемент колонки, а колонка
   растягивает детей на всю ширину, поэтому `text-align:center` центрирует её
   ровно под кнопкой, а не под краем колонки.

   Цвет и размер приходят ПРИЗНАКАМИ с сервера (`data-note-color`,
   `data-note-size`), как у мест плашки и вида кнопок: закрытый список живёт
   на сервере, оформление решает, как выглядит каждая ступень. Умолчание
   (`muted`/`m`) правил НЕ имеет — это ровно то, что было до настройки, и
   товары, заведённые раньше, выглядят как выглядели.

   Просвет до кнопки — 10px: 14px владелец называл «прилипло к кнопке», но и
   отрывать строку от неё нельзя, она объясняет именно её. */
.pm-note{margin:10px 0 0;font-size:.8rem;color:var(--ink-3);text-align:center}
.pm-note[data-note-color="ink"]{color:var(--ink)}
.pm-note[data-note-color="warn"]{color:#ffc48a}
.pm-note[data-note-color="bad"]{color:#e8736a}
.pm-note[data-note-color="ok"]{color:var(--lime)}
.pm-note[data-note-color="cyan"]{color:var(--cyan)}
.pm-note[data-note-color="gold"]{color:#ffb454}
.pm-note[data-note-size="s"]{font-size:.72rem}
.pm-note[data-note-size="l"]{font-size:.9rem}
.pm-note[data-note-size="xl"]{font-size:1rem}

/* Свои кнопки товара. Владелец заводит их списком в панели: включает и
   выключает, вписывает надписи, выбирает, что кнопка делает, как выглядит и
   как движется. Список пуст — работает .pm-buy выше, и разметка та же, что
   была: эта настройка ничего не ломает у товаров, заведённых раньше.

   ФУНКЦИЯ и ВИД — разные настройки, поэтому вид задаётся атрибутом
   data-pm-btn-style, а не классом: панель шлёт идентификатор, оформление
   решает, как это выглядит, и добавить вид можно, не трогая скрипт. */
/* Кнопки окна товара — вниз по центру.

   Владелец просил: свои кнопки, которые он заводит и включает сам, должны
   стоять внизу по центру, а не у правого края. Центрируем и контейнер
   своих кнопок, и одиночную кнопку: окно с одной кнопкой и окно с тремя
   обязаны выглядеть одинаково, иначе выбор «одна кнопка или свои» менял бы
   ещё и раскладку. */
.pm-buttons{
  margin-top:22px;display:flex;flex-wrap:wrap;gap:10px;
  align-items:center;justify-content:center;
}
.pm-buttons[hidden]{display:none}
.pm-btn{min-width:170px}

/* Шесть готовых видов. «Главная» правила не имеет — это общий вид .btn,
   тот же, что у кнопки покупки: владелец просил «красивые кнопки на выбор»,
   и главная обязана выглядеть как всё остальное на сайте. */
.pm-btn[data-pm-btn-style="outline"]{
  background:transparent;color:var(--cyan);
  border:1px solid rgba(34,230,255,.45);
}
.pm-btn[data-pm-btn-style="outline"]:hover{
  border-color:var(--cyan);background:rgba(34,230,255,.08);
}
.pm-btn[data-pm-btn-style="ghost"]{
  background:transparent;color:var(--ink-2);
  border:1px solid var(--line);box-shadow:none;
}
.pm-btn[data-pm-btn-style="ghost"]:hover{color:var(--ink-1);border-color:var(--ink-3)}
.pm-btn[data-pm-btn-style="glow"]{
  box-shadow:0 0 20px rgba(34,230,255,.35),0 0 44px rgba(139,92,255,.28);
}
.pm-btn[data-pm-btn-style="gradient"]{
  background:linear-gradient(135deg,var(--cyan),#8b5cf6);
  border-color:transparent;color:#06121c;
}
.pm-btn[data-pm-btn-style="pill"]{border-radius:999px;padding-inline:26px}

/* Движение кнопки. Как у строки остатка и у лупы: виды описаны оформлением,
   а не скриптом. «Отклик при наведении» — единственный вид, который НЕ
   движется сам: он отвечает на наведение, и постоянное движение тут было бы
   шумом. */
@keyframes pmBtnPulse{
  0%,100%{transform:scale(1)}
  50%    {transform:scale(1.04)}
}
.pm-btn[data-pm-btn-anim="pulse"]{animation:pmBtnPulse 2s ease-in-out infinite}

@keyframes pmBtnFloat{
  0%,100%{transform:translateY(0)}
  50%    {transform:translateY(-4px)}
}
.pm-btn[data-pm-btn-anim="float"]{animation:pmBtnFloat 2.4s ease-in-out infinite}

@keyframes pmBtnGlow{
  0%,100%{box-shadow:0 0 0 transparent}
  50%    {box-shadow:0 0 22px rgba(34,230,255,.45),0 0 46px rgba(139,92,255,.3)}
}
.pm-btn[data-pm-btn-anim="glow"]{animation:pmBtnGlow 2.2s ease-in-out infinite}

@keyframes pmBtnShine{
  0%,55%{left:-70%}
  100%  {left:130%}
}
.pm-btn[data-pm-btn-anim="shine"]{position:relative;overflow:hidden}
.pm-btn[data-pm-btn-anim="shine"]::after{
  content:"";position:absolute;top:-40%;bottom:-40%;width:38%;left:-70%;
  background:linear-gradient(90deg,transparent,rgba(255,255,255,.75),transparent);
  transform:skewX(-18deg);pointer-events:none;
  animation:pmBtnShine 2.8s ease-in-out infinite;
}

.pm-btn[data-pm-btn-anim="press"]{transition:transform .22s var(--ease)}
.pm-btn[data-pm-btn-anim="press"]:hover{transform:translateY(-2px) scale(1.03)}
.pm-btn[data-pm-btn-anim="press"]:active{transform:translateY(0) scale(.99)}

/* Кому движение мешает — тому его не показываем. */
@media (prefers-reduced-motion:reduce){
  .pm-btn[data-pm-btn-anim]{animation:none}
  .pm-btn[data-pm-btn-anim="shine"]::after{animation:none;opacity:0}
}

/* Выбор цвета и размера. Кнопки, а не раскрывающийся список: значений
   немного, и глазами выбрать быстрее, чем открывать список. Непустой
   список — обязательный выбор, поэтому у блока есть состояние «здесь не
   выбрано»: кнопка покупки не отправляет товар, а подсвечивает его. */
.pm-opts{margin:12px 0 0;padding:12px 0 0;border-top:1px solid var(--line);
  display:flex;flex-direction:column;gap:14px}
.pm-opt{display:flex;flex-direction:column;gap:8px}

/* `hidden` ПРОИГРЫВАЕТ правилу с `display` — и здесь это стоило владельцу
   целой строки в окне товара.

   Скрипт прячет выбор правильно: `box.hidden = !list.length` — у товара без
   цветов блок цвета скрыт. Но `hidden` в разметке рисует БРАУЗЕР, а его
   правило слабее ЛЮБОГО правила страницы: `display:flex` выше по силе и
   оставляет узел на экране. Владелец (08.10, вечер, снимок #3) обвёл
   красным «Цвет: не выбран» и «Размер: не выбран» у товара, которому ни
   цвета, ни размера не задано: «если у товара не заданы эти значения то их
   не надо показывать, а то сейчас их видно а так не должно быть».

   Правило нужно ИМЕННО явное и именно после `display:flex`: тем же числом
   классов, как и у соседей по файлу. Одно правило на оба узла — у них общая
   беда и общая причина. */
.pm-opts[hidden], .pm-opt[hidden]{display:none}
.pm-opt-cap{font-size:.84rem;color:var(--ink-3)}
.pm-opt-cap b{color:var(--ink);font-weight:600}
/* Подпись видна ВСЕГДА — и когда ничего не выбрано.

   Раньше строку прятали, пока выбора нет («не показывать то, что ничего не
   сообщает»). Владелец увидел это иначе (06.10): «почему то не видно у них
   обозначений в состоянии пока не выбрана, а когда выбираешь появляеться
   обозначение цвет и размер исправь это надо чтобы было видно».

   И он прав по сути: подпись — это НАЗВАНИЕ выбора. Без неё кнопки «Серый»
   и «Белый» читаются как угодно, и покупатель узнаёт, что это цвет, только
   нажав. На месте значения теперь «не выбран»: своё значение остаётся белым
   и плотным, «не выбран» — приглушённым и обычного веса, чтобы состояние
   было видно, но не спорило с настоящим выбором. */
.pm-opt-cap b.pm-opt-none{color:var(--ink-2);font-weight:400}
.pm-opt-row{display:flex;flex-wrap:wrap;gap:8px}
.pm-opt-btn{
  padding:8px 14px;border-radius:10px;cursor:pointer;
  border:1px solid var(--line);background:rgba(255,255,255,.03);
  color:var(--ink);font-size:.86rem;line-height:1.2;
  transition:border-color .25s,box-shadow .25s,color .25s;
}
.pm-opt-btn:hover{border-color:rgba(34,230,255,.5)}
.pm-opt-btn.on{
  border-color:var(--cyan);color:var(--cyan);
  box-shadow:0 0 0 1px rgba(34,230,255,.45),0 0 14px rgba(34,230,255,.22);
}
/* Цвет невыбранного блока — тот же тёплый, что у пометки «не в продаже»:
   он читается как «обратите внимание», а не как украшение. */
.pm-opt-need .pm-opt-cap{color:#ffc48a}
.pm-opt-need .pm-opt-btn{border-color:rgba(255,170,90,.55)}

/* Узкие экраны: окно становится одноколоночным, фотографии уходят
   наверх. Прокручивается всё окно целиком — иначе на телефоне
   получились бы две вложенные прокрутки, и палец «залипал» бы. */
@media (max-width:900px){
  .pm{padding:0}
  /* align-content:start — ГЛАВНОЕ здесь, а не украшение. Окно растянуто
     на весь экран (height:100%), строк у сетки две и обе `auto`; по
     умолчанию align-content растягивает такие строки, чтобы занять
     высоту окна. Лишняя высота уходила ВНУТРЬ первой строки — под
     миниатюрами появлялась пустая полоса. Замер на 485×1257: строка
     колонки фотографий 610px при содержимом 535px, между миниатюрами и
     значком 95px пустоты (при окне 485×1100 — 20px, то есть полоса
     зависит от высоты экрана: чем выше телефон, тем больше дыра).
     С прижимом к верху строки ровно по содержимому, а остаток уходит
     вниз — его видно только у самого короткого товара и он не режет
     взгляд между блоками. */
  .pm-box{grid-template-columns:1fr;gap:18px;width:100%;height:100%;
    max-height:none;border:0;border-radius:0;padding:18px;overflow-y:auto;
    align-content:start}
  /* min-height:52vh здесь БЫЛ — и оставлял пустую полосу под миниатюрами.
     Кадр фотографии на телефоне квадратный, то есть шириной с экран: уже
     он сам занимает больше половины высоты, а миниатюры добавляли к нему
     ещё 74px. Прижим к 52vh ничего не давал, а на высоком окне раздувал
     колонку: при окне 390×1010 это 525px против содержимого 440px, и
     между миниатюрами и названием товара зияла дыра в 100px. Владелец
     обвёл её красным. Колонка должна быть ровно по содержимому. */
  .pm-info{overflow:visible}
}
@media (max-width:820px){
  /* Обычный размер карточки на этом окне — 262×392. Это не «уменьшенная
     копия» широкой раскладки: у неё и пропорция своя (392/262, а не
     432/298), и она сохранена. Ступень размера владельца работает поверх
     этих чисел: множитель один и тот же на любом окне, меняются только
     основа и высота. */
  .shop-block{--card-base:262px;--card-h-base:392px}
  .car-arrow{width:42px;height:42px}
  .car-prev{left:-6px}
  .car-next{right:-6px}
}
@media (max-width:560px){
  .shop-block{--card-base:224px;--card-h-base:352px}
  /* Кегль подписи на телефоне мельче — и он тоже ходит за ступенью
     размера: без множителя у «самых маленьких» подпись занимала бы
     полкарточки, а у «самых крупных» терялась бы в пустоте. */
  .car-body{padding:calc(13px * var(--card-k,1)) calc(14px * var(--card-k,1))
                    calc(16px * var(--card-k,1))}
  .car-name{font-size:calc(.92rem * var(--card-k,1))}
  .car-price{font-size:calc(1.04rem * var(--card-k,1))}
}
/* Кто просил меньше движения — получает карточки без «доезда». */
@media (prefers-reduced-motion:reduce){
  .car-card{transition:opacity .2s}
  /* Знак перед подписью тоже замирает: он мигает и дёргается без остановки,
     а просьба «меньше движения» относится ко всей странице, не только к
     ленте. Настройка владельца при этом сохраняется — вернётся, когда
     человек снимет её в системе. */
  .shop-block-title::before{animation:none}
  .pm-box,.pm-back{animation:none}
  .cart-box,.cart-back{animation:none}
  .cart-line,.cart-btn{transition:none}
  /* Кружок корзины замирает вместе со всем остальным. Вид при этом
     сохраняется: шар остаётся шаром, катушка катушкой — просто перестают
     крутиться. Число читается всегда, поэтому терять тут нечего. */
  .cart-count,.cart-count-n,
  .cart-count::after,.cart-count::before{animation:none}
  /* Свечение корзины и её эффекты замирают вместе со всем остальным. Свет
     при этом остаётся: кнопка по-прежнему светится и остаётся заметной —
     гасим только движение, а не настройку владельца. */
  .cart-btn,.cart-btn svg,.cart-btn::after{animation:none}
  .car-card:hover .car-shot img,.car-card:hover .car-shot video{transform:none}
}

/* ============================================================
   Корзина и оформление заказа
   ------------------------------------------------------------
   Выдвижная панель справа. Шаги (список, оформление, «принят»,
   «мои заказы») лежат в одной панели и переключаются атрибутом
   hidden: так корзина не теряется из виду между «оформить» и
   «готово», а браузер не держит четыре отдельных окна.

   Сумма здесь ТОЛЬКО для показа. В запрос уходят идентификаторы и
   количество, а цену считает сервер по своей копии каталога —
   подмена цены в запросе не должна менять сумму к оплате.
   ============================================================ */

/* ---------- кнопка в шапке ---------- */
/* Группа справа не сжимается и не переносится — по той же причине, что и
   пункты меню (см. `.nav-links > a` выше). Владелец (07.10) показал шапку на
   ширине 920: «Рассчитать заказ» разъехалось на две строки, и кнопка читалась
   как сломанная. `flex:none` не даёт сжать саму группу, `white-space:nowrap` —
   разорвать подпись кнопки пополам. */
.nav-acts{display:flex;align-items:center;gap:10px;flex:none;white-space:nowrap}

/* Сама кнопка — её основа и настройки (значок, цвет, свечение, эффект) —
   лежит НИЖЕ, в разделе «корзина в шапке». Там же её дословная копия в
   панели: сверка копий (badge-css-drift/cart-css-drift) сравнивает участки
   файлов по опорным строкам, и основа кнопки обязана лежать внутри участка
   корзины, иначе сверка не увидит, что копия разошлась. */
.cart-btn[hidden]{display:none}
.cart-btn:hover{border-color:rgba(var(--cart-rgb),.55);color:#fff;
  box-shadow:0 0 24px rgba(var(--cart-rgb),.22)}
.cart-btn:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
/* Товар добавили — кнопка коротко «подпрыгивает». Это единственный
   сигнал, что нажатие сработало, если панель не открылась. */
.cart-btn.bump{animation:cartBump .5s var(--ease)}
@keyframes cartBump{
  0%{transform:none}
  35%{transform:translateY(-4px) scale(1.05)}
  100%{transform:none}
}

.cart-count{
  /* Свой контекст наложения: внутри кружка лежат блик (::after) и число
     (.cart-count-n), и порядок «блик над фоном, число над бликом» должен
     считаться здесь, а не от кнопки. */
  position:relative;z-index:0;
  min-width:20px;padding:2px 6px;border-radius:100px;
  font-size:.72rem;font-weight:800;line-height:1.5;text-align:center;
  /* Цвет кружка — тот же, что у свечения кнопки: выбранный владельцем цвет
     должен быть виден не только в ореоле, но и на самом числе. */
  color:var(--cart-ink,#02121a);
  background:linear-gradient(115deg,var(--cart-from,var(--cyan)),var(--cart-to,var(--violet)));
}
.cart-count[hidden]{display:none}
/* Число. Отдельным элементом, а не текстом кружка: у видов с вращением
   крутится сам кружок, а число поворачивается навстречу — иначе на шаре
   оно стояло бы вверх ногами. */
.cart-count-n{display:block;position:relative;z-index:2;font-weight:inherit}

/* ---------- вид кружка: выбирается в панели ----------

   Владелец выбирает вид в панели («Товары» → «Кружок корзины»): «Без
   анимации» оставляет прежний ровный кружок с переходом, остальные виды
   нарисованы здесь. Наружу из панели уходят только идентификаторы —
   поэтому список закрыт и здесь, и на сервере: чужого значения в разметку
   не попадёт, и сломать вид нечем.

   Скорость владелец задаёт числом, и она приезжает множителем в
   --badge-speed. Обычная скорость у каждого вида своя (--badge-base):
   шар оборачивается быстро, катушка — не спеша. Одно общее число секунд
   означало бы, что «×2» для шара быстро, а для катушки еле-еле.

   Кружок поворачивается целиком, а число (.cart-count-n) — навстречу, тем
   же ключом и с обратным направлением. Так «крутится» виден, а число
   остаётся читаемым.

   Кто просил в системе меньше движения, увидит кружок неподвижным — как и
   остальные анимации сайта (см. prefers-reduced-motion выше). */

@keyframes badgeSpin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
@keyframes badgeGlow{0%,100%{opacity:.3}50%{opacity:1}}
@keyframes badgeLayerRise{
  from{background-position:0 0,0 0}
  to{background-position:0 0,0 -4px}
}

/* Бильярдный шар: цветной шар с бликом, а число — в белом пятне, как на
   настоящем шаре. Крутится шар, блик обходит его по кругу. */
.cart-btn[data-badge-anim="ball"]{--badge-base:1.6s}
.cart-btn[data-badge-anim="ball"] .cart-count:not([hidden]){
  display:inline-grid;place-items:center;overflow:hidden;
  width:26px;height:26px;min-width:26px;padding:0;border-radius:50%;
  color:#0b1220;
  background:
    radial-gradient(circle at 50% 50%,#fff 0 7.6px,rgba(255,255,255,0) 7.7px),
    radial-gradient(circle at 33% 26%,rgba(255,255,255,.85),rgba(255,255,255,0) 44%),
    radial-gradient(circle at 50% 46%,#ffd23f 0%,#f0a90a 52%,#8a5a00 100%);
  box-shadow:inset 0 -4px 7px rgba(0,0,0,.42),0 2px 8px rgba(0,0,0,.35);
  animation:badgeSpin calc(var(--badge-base) / var(--badge-speed,1)) linear infinite;
}
.cart-btn[data-badge-anim="ball"] .cart-count-n{
  animation:badgeSpin calc(var(--badge-base) / var(--badge-speed,1)) linear infinite reverse;
}

/* Катушка филамента: намотка видна как зубцы по кругу, в середине — втулка
   с числом. Крутится намотка, втулка с числом остаётся на месте. */
.cart-btn[data-badge-anim="spool"]{--badge-base:2.6s}
.cart-btn[data-badge-anim="spool"] .cart-count:not([hidden]){
  display:inline-grid;place-items:center;
  width:28px;height:28px;min-width:28px;padding:0;border-radius:50%;
  color:#dff7ff;
  background:
    radial-gradient(circle at 50% 50%,rgba(0,0,0,0) 0 8.4px,rgba(4,8,16,.92) 8.5px 10.4px,rgba(0,0,0,0) 10.5px),
    repeating-conic-gradient(from 0deg,#2ad4f0 0 8deg,#0a5f78 8deg 16deg,#1aa3c4 16deg 24deg);
  box-shadow:inset 0 0 0 1px rgba(120,240,255,.35),0 2px 8px rgba(0,0,0,.4);
  animation:badgeSpin calc(var(--badge-base) / var(--badge-speed,1)) linear infinite;
}
.cart-btn[data-badge-anim="spool"] .cart-count-n{
  display:grid;place-items:center;width:15px;height:15px;border-radius:50%;
  background:radial-gradient(circle at 50% 40%,#1b2436,#080d16);
  box-shadow:inset 0 0 0 1px rgba(34,230,255,.55);
  animation:badgeSpin calc(var(--badge-base) / var(--badge-speed,1)) linear infinite reverse;
}

/* Сопло принтера: латунная гайка сопла — шестигранник с тёмным отверстием
   посередине и раскалённым кончиком, который дышит: то разгорается, то
   гаснет. По кругу здесь ничего не движется: сопло не крутится, и вращать
   его значило бы показывать неправду. Шестигранник, а не круг, ещё и
   потому, что круг с бликом уже занят бильярдным шаром, — два похожих
   вида в списке выбирать бессмысленно. */
.cart-btn[data-badge-anim="nozzle"]{--badge-base:1.5s}
.cart-btn[data-badge-anim="nozzle"] .cart-count:not([hidden]){
  display:inline-grid;place-items:center;
  width:26px;height:26px;min-width:26px;padding:0;border-radius:0;
  clip-path:polygon(50% 0%,93% 25%,93% 75%,50% 100%,7% 75%,7% 25%);
  color:#ffcf8a;
  background:
    radial-gradient(circle at 50% 50%,#241704 0 7.4px,rgba(0,0,0,0) 7.5px),
    linear-gradient(180deg,#f6d79a 0%,#c08f38 52%,#6b4711 100%);
  /* Тень через filter, а не box-shadow: box-shadow срезался бы вместе с
     углами шестигранника, а drop-shadow обводит уже обрезанную форму. */
  filter:drop-shadow(0 2px 4px rgba(0,0,0,.5));
}
.cart-btn[data-badge-anim="nozzle"] .cart-count:not([hidden])::after{
  content:'';position:absolute;inset:0;z-index:1;pointer-events:none;
  background:radial-gradient(circle at 50% 78%,rgba(255,196,90,.95),rgba(255,120,0,0) 52%);
  animation:badgeGlow calc(var(--badge-base) / var(--badge-speed,1)) ease-in-out infinite;
}

/* Слои печати: полосы набегают снизу вверх, как растёт напечатанная
   деталь. Число лежит на тёмной подложке, чтобы не теряться на полосах. */
.cart-btn[data-badge-anim="layers"]{--badge-base:2s}
.cart-btn[data-badge-anim="layers"] .cart-count:not([hidden]){
  display:inline-grid;place-items:center;overflow:hidden;
  width:26px;height:26px;min-width:26px;padding:0;border-radius:50%;
  color:#eaf6ff;
  background:
    linear-gradient(180deg,rgba(34,230,255,.20),rgba(139,92,255,.20)),
    repeating-linear-gradient(180deg,rgba(34,230,255,.95) 0 2px,rgba(6,20,34,.95) 2px 4px);
  background-size:100% 100%,100% 4px;
  box-shadow:inset 0 0 0 1px rgba(34,230,255,.45),0 2px 8px rgba(0,0,0,.4);
  animation:badgeLayerRise calc(var(--badge-base) / var(--badge-speed,1)) linear infinite;
}
.cart-btn[data-badge-anim="layers"] .cart-count-n{
  display:grid;place-items:center;width:17px;height:17px;border-radius:50%;
  background:rgba(5,10,20,.82);
  box-shadow:inset 0 0 0 1px rgba(34,230,255,.4);
}

/* Голова принтера по кругу: тёмный круг с ободом, по ободу бежит яркая
   точка — как печатающая голова, идущая по контуру. Число в середине. */
.cart-btn[data-badge-anim="orbit"]{--badge-base:2.2s}
.cart-btn[data-badge-anim="orbit"] .cart-count:not([hidden]){
  display:inline-grid;place-items:center;
  width:26px;height:26px;min-width:26px;padding:0;border-radius:50%;
  color:#eaf6ff;
  background:radial-gradient(circle at 50% 50%,#131c2e 0 62%,#0a1120 100%);
  box-shadow:inset 0 0 0 1px rgba(34,230,255,.5),0 2px 8px rgba(0,0,0,.4);
}
.cart-btn[data-badge-anim="orbit"] .cart-count:not([hidden])::after{
  content:'';position:absolute;inset:0;border-radius:50%;z-index:1;pointer-events:none;
  background:
    radial-gradient(circle at 50% 50%,rgba(34,230,255,0) 0 9.2px,rgba(34,230,255,.55) 9.3px 10.6px,rgba(34,230,255,0) 10.7px),
    radial-gradient(circle at 50% 3.9px,#eafcff 0 2.5px,rgba(234,252,255,0) 2.9px);
  animation:badgeSpin calc(var(--badge-base) / var(--badge-speed,1)) linear infinite;
}

/* Шестерня принтера: зубцы по ободу и светлая втулка с числом. Зубцы
   поворачиваются, втулка стоит — как у настоящей шестерни на валу. */
.cart-btn[data-badge-anim="gear"]{--badge-base:3.4s}
.cart-btn[data-badge-anim="gear"] .cart-count:not([hidden]){
  display:inline-grid;place-items:center;
  width:28px;height:28px;min-width:28px;padding:0;border-radius:50%;
  color:#08111f;
  background:
    radial-gradient(circle at 50% 50%,#0d1524 0 10.6px,rgba(0,0,0,0) 10.7px),
    repeating-conic-gradient(from 0deg,#cfe6ff 0 11deg,#5f7896 11deg 22deg);
  box-shadow:inset 0 0 0 1px rgba(180,220,255,.35),0 2px 8px rgba(0,0,0,.4);
  animation:badgeSpin calc(var(--badge-base) / var(--badge-speed,1)) linear infinite;
}
.cart-btn[data-badge-anim="gear"] .cart-count-n{
  display:grid;place-items:center;width:17px;height:17px;border-radius:50%;
  background:linear-gradient(180deg,#eaf4ff,#b9cfe8);
  box-shadow:inset 0 0 0 1px rgba(10,20,35,.45);
  animation:badgeSpin calc(var(--badge-base) / var(--badge-speed,1)) linear infinite reverse;
}

/* ---------- корзина в шапке: значок, цвет, свечение, эффект ----------

   Владелец (29.09): «изменения самой корзины в админ панель добавить
   настройку свечения и цвета и эффекты и много разных логотипов корзины...
   а то её сейчас незаметно, надо чтобы она была привлекательная и не
   забываемая для посетителя и покупателя сайта». Настраивается в панели
   («Товары» → «Корзина в шапке»).

   Наружу из панели уходят только идентификаторы, поэтому списки закрыты и
   здесь, и на сервере: чужого значения в разметку не попадёт, и сломать вид
   нечем. Исключение — значок: его пути приезжают с сервера вместе с
   настройками (см. CART_LOGOS в server.js) и подставляются в <svg> кнопки.

   Сила свечения приезжает множителем в --cart-glow-power. Обычная сила у
   каждого вида своя (у «ореола» свечение шире, у «обводки» — вплотную), и
   одно общее число пикселей означало бы, что «×2» для одного вида много, а
   для другого незаметно. Ровно тот же приём, что у скорости кружка. */

/* Основа кнопки. Лежит здесь, а не в разделе «корзина и оформление заказа»,
   потому что её дословная копия есть в панели — в образцах раздела «Корзина
   в шапке». Сверка копий сравнивает участки файлов целиком, и основа обязана
   быть внутри участка: иначе она тихо разошлась бы, и владелец выбрал бы вид
   по образцу, а на сайте получил другое. */
.cart-btn{
  position:relative;display:inline-flex;align-items:center;gap:8px;
  padding:10px 15px;border-radius:100px;cursor:pointer;
  border:1px solid var(--line);background:rgba(255,255,255,.03);
  color:var(--ink);font-size:.86rem;font-weight:600;
  transition:border-color .3s,color .3s,transform .3s var(--ease),box-shadow .3s;
  /* Оттенки кнопки. Пока владелец не выбрал свой цвет, это цвет сайта —
     тот же голубой с фиолетовым, что был у кружка с числом. Приезжают
     переменными стиля из панели («Товары» → «Корзина в шапке»), а
     `--cart-rgb` — тот же оттенок триплетом каналов: свечения построены на
     `rgba(var(--cart-rgb), X)`, а собрать такую запись из «#7a3ff2»
     средствами CSS нельзя. Триплет считает сервер и присылает готовым. */
  --cart-from:var(--cyan);
  --cart-to:var(--violet);
  --cart-ink:#02121a;
  --cart-rgb:34,230,255;
  --cart-glow-power:1;
  /* Числа свечения: кольцо вплотную, внутреннее свечение и ореол с его
     разлётом и яркостью. Вид свечения задаёт только их, а тень рисует одно
     общее правило — поэтому наведение работает у всех видов одинаково. */
  --cart-ring:0px; --cart-ring-a:0;
  --cart-inner:0px; --cart-inner-a:0;
  --cart-halo:0px; --cart-spread:0px; --cart-alpha:0;
}
.cart-btn svg{width:19px;height:19px;fill:none;stroke:currentColor;
  stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}

/* Виды свечения. Каждый задаёт только числа — саму тень рисует общее
   правило ниже. Поэтому вид свечения и эффект движения сочетаются как
   угодно: «двойное свечение» с «покачиванием значка» — это два независимых
   атрибута, а не сороковая пара в списке готовых сочетаний. */
.cart-btn[data-cart-glow="none"]{--cart-ring-a:0;--cart-alpha:0;--cart-inner-a:0}
.cart-btn[data-cart-glow="soft"]{--cart-halo:20px;--cart-spread:-4px;--cart-alpha:.5}
.cart-btn[data-cart-glow="ring"]{
  --cart-ring:1px;--cart-ring-a:.45;
  --cart-halo:22px;--cart-spread:-6px;--cart-alpha:.6}
.cart-btn[data-cart-glow="halo"]{--cart-halo:34px;--cart-spread:3px;--cart-alpha:.42}
.cart-btn[data-cart-glow="inner"]{
  --cart-inner:16px;--cart-inner-a:.7;
  --cart-halo:14px;--cart-spread:-6px;--cart-alpha:.35}
.cart-btn[data-cart-glow="double"]{
  --cart-ring:1px;--cart-ring-a:.32;
  --cart-halo:18px;--cart-spread:-4px;--cart-alpha:.65}
.cart-btn[data-cart-glow="breathe"]{--cart-halo:14px;--cart-spread:-6px;--cart-alpha:.3}
.cart-btn[data-cart-glow="rainbow"]{--cart-halo:24px;--cart-spread:-4px;--cart-alpha:.6}

/* Одна тень на все виды: кольцо, свечение внутри и ореол снаружи. Кольцо и
   внутреннее свечение рисуются нулевыми, когда вид их не задал, — лишних
   правил на каждый вид не нужно.

   ВНИМАНИЕ: наружу свечение выходит только потому, что кнопка НЕ обрезает
   потомков. У карточки работы свечение пришлось переносить на кадр именно
   из-за `overflow:hidden`; здесь обрезка включается лишь эффектом «блик»,
   и она режет только слой блика внутри кнопки — тень это не задевает. */
.cart-btn[data-cart-glow]{
  border-color:rgba(var(--cart-rgb),.5);color:#fff;
  box-shadow:
    0 0 0 var(--cart-ring) rgba(var(--cart-rgb),var(--cart-ring-a)),
    inset 0 0 var(--cart-inner) -2px rgba(var(--cart-rgb),var(--cart-inner-a)),
    0 0 calc(var(--cart-halo) * var(--cart-glow-power)) var(--cart-spread)
      rgba(var(--cart-rgb),var(--cart-alpha));
}
/* Наведение — ярче, а не «другое свечение»: кольцо и ореол поднимаются на
   четверть, и это одинаково читается у всех восьми видов. */
.cart-btn[data-cart-glow]:hover{
  --cart-ring-a:calc(var(--cart-ring-a) + .25);
  --cart-alpha:calc(var(--cart-alpha) + .25);
}
/* Значок красим выбранным цветом: у «графитового» кнопка становится
   сдержанной, у «огненного» — тёплой. Только у кнопки магазина: атрибут
   стоит лишь на ней, а кнопка кабинета рядом остаётся прежней. */
.cart-btn[data-cart-glow]:not([data-cart-glow="none"]) svg{color:var(--cart-to)}

/* Дышащее свечение: сила ходит туда-сюда. Ключи перебивают общую тень —
   иначе дыхания не было бы видно. */
.cart-btn[data-cart-glow="breathe"]{
  animation:cartGlowBreathe 3.6s ease-in-out infinite;
}
@keyframes cartGlowBreathe{
  0%,100%{box-shadow:0 0 calc(12px * var(--cart-glow-power)) -6px rgba(var(--cart-rgb),.28)}
  50%{box-shadow:0 0 calc(32px * var(--cart-glow-power)) -2px rgba(var(--cart-rgb),.8)}
}

/* Радужное переливание: оттенок всей кнопки уходит по кругу. Крутится не
   свечение, а оттенок целиком — так переливается и кружок с числом, и
   значок, а надпись остаётся белой: белый от поворота оттенка не меняется. */
.cart-btn[data-cart-glow="rainbow"]{animation:cartGlowHue 7s linear infinite}
@keyframes cartGlowHue{from{filter:hue-rotate(0deg)}to{filter:hue-rotate(360deg)}}

/* Кнопка коротко «подпрыгивает», когда товар добавили. У двух видов
   свечения своя анимация на той же кнопке, и без этого правила подпрыгивание
   пропало бы: у «animation» на элементе один хозяин. Здесь победитель —
   подпрыгивание (оно короче), а свечение продолжается сразу после него. */
.cart-btn[data-cart-glow="breathe"].bump,
.cart-btn[data-cart-glow="rainbow"].bump{animation:cartBump .5s var(--ease)}

/* ---------- эффект кнопки ----------
   Эффект — про движение и отклик, свечение — про свет. Разведены нарочно:
   «золотая кнопка с бликом» и «золотая кнопка с покачиванием» — разные
   вещи, и один список готовых сочетаний снова раздулся бы до сорока пар.

   Движения значка работают ВСЕГДА, а при наведении ускоряются: владелец
   просил кнопку заметную и не забываемую, а движение только под курсором
   замечает лишь тот, кто уже навёл. Движения самой кнопки — только при
   наведении: кнопка, которая сама уезжает из-под курсора, мешает нажимать. */

/* Пробегающий блик. Полоса — отдельный слой, поэтому кнопка обрезает её по
   своим скруглениям. Само свечение при этом не режется: оно наружное, а
   обрезка потомков наружную тень не задевает. */
.cart-btn[data-cart-fx="shine"]{overflow:hidden}
.cart-btn[data-cart-fx="shine"]::after{
  content:'';position:absolute;top:-30%;bottom:-30%;left:-70%;width:45%;
  background:linear-gradient(105deg,rgba(255,255,255,0),
    rgba(255,255,255,.5),rgba(255,255,255,0));
  transform:skewX(-18deg);pointer-events:none;
  animation:cartShine 3.4s ease-in-out infinite;
}
@keyframes cartShine{
  0%{left:-70%}
  60%,100%{left:130%}
}

/* Движения кнопки — по наведению. */
.cart-btn[data-cart-fx="lift"]:hover{transform:translateY(-3px)}
.cart-btn[data-cart-fx="grow"]:hover{transform:scale(1.06)}
.cart-btn[data-cart-fx="tilt"]:hover{transform:rotate(-3deg) translateY(-2px)}
.cart-btn[data-cart-fx="pop"]:active{transform:scale(.93)}

/* Движения значка. Анимируется сам <svg>, а не кнопка: на кнопке уже живут
   свечение и наведение, и второй хозяин «animation» у того же элемента
   просто вытеснил бы первого. */
.cart-btn[data-cart-fx="swing"] svg{animation:cartSvgSwing 3.6s ease-in-out infinite}
.cart-btn[data-cart-fx="swing"]:hover svg{animation-duration:1s}
@keyframes cartSvgSwing{0%,100%{transform:rotate(-11deg)}50%{transform:rotate(11deg)}}

.cart-btn[data-cart-fx="bounce"] svg{animation:cartSvgBounce 2.8s ease-in-out infinite}
.cart-btn[data-cart-fx="bounce"]:hover svg{animation-duration:.8s}
@keyframes cartSvgBounce{
  0%,60%,100%{transform:translateY(0)}
  75%{transform:translateY(-4px)}
  88%{transform:translateY(1px)}
}

.cart-btn[data-cart-fx="spin"] svg{animation:cartSvgSpin 6s linear infinite}
.cart-btn[data-cart-fx="spin"]:hover svg{animation-duration:1.2s}
@keyframes cartSvgSpin{from{transform:rotate(0)}to{transform:rotate(360deg)}}

.cart-btn[data-cart-fx="wiggle"] svg{animation:cartSvgWiggle .5s ease-in-out infinite}
.cart-btn[data-cart-fx="wiggle"]:hover svg{animation-duration:.22s}
@keyframes cartSvgWiggle{0%,100%{transform:rotate(-6deg)}50%{transform:rotate(6deg)}}

/* Подчёркивание названия: черта растёт из середины. */
.cart-btn[data-cart-fx="underline"] .cart-btn-label{position:relative}
.cart-btn[data-cart-fx="underline"] .cart-btn-label::after{
  content:'';position:absolute;left:0;right:0;bottom:-3px;height:2px;border-radius:2px;
  background:linear-gradient(90deg,var(--cart-from),var(--cart-to));
  transform:scaleX(0);transform-origin:50% 50%;
  transition:transform .34s var(--ease);
}
.cart-btn[data-cart-fx="underline"]:hover .cart-btn-label::after{transform:scaleX(1)}

/* ---------- панель ---------- */
.cart{position:fixed;inset:0;z-index:95;display:flex;justify-content:flex-end}
.cart[hidden]{display:none}
body.cart-open{overflow:hidden}

.cart-back{position:absolute;inset:0;background:rgba(3,5,11,.78);
  backdrop-filter:blur(6px);animation:pmFade .3s var(--ease)}

.cart-box{
  position:relative;z-index:1;display:flex;flex-direction:column;
  width:min(468px,100%);height:100%;max-height:100%;
  border-left:1px solid var(--line);
  background:linear-gradient(200deg,rgba(17,24,41,.99),rgba(8,12,22,.99));
  box-shadow:-30px 0 90px rgba(0,0,0,.6);
  animation:cartIn .42s var(--ease);
}
@keyframes cartIn{from{transform:translateX(100%)}to{transform:none}}

.cart-head{
  display:flex;align-items:center;justify-content:space-between;gap:12px;
  padding:16px 20px;border-bottom:1px solid var(--line);flex:none;
}
.cart-title{margin:0;font-size:1.1rem;font-weight:800;letter-spacing:-.01em}
.cart-x{
  width:38px;height:38px;flex:none;display:flex;align-items:center;
  justify-content:center;cursor:pointer;border-radius:50%;
  border:1px solid var(--line);background:rgba(10,15,26,.9);color:var(--ink);
  transition:border-color .3s,color .3s,transform .3s var(--ease);
}
.cart-x svg{width:17px;height:17px;stroke:currentColor;stroke-width:2;
  stroke-linecap:round;fill:none}
.cart-x:hover{border-color:rgba(34,230,255,.5);color:#fff;transform:rotate(90deg)}

/* `scrollbar-width:thin` убран: он отменял сайтовую полосу прокрутки (см.
   разбор у `.pm-info`). Здесь полоса вертикальная — та самая, что владелец
   обвёл красным. */
.cart-body{flex:1;min-height:0;overflow-y:auto;padding:16px 20px 30px}
.cart-step[hidden]{display:none}

/* Прячем части корзины атрибутом hidden — список, итог, кнопки, «мои
   заказы» включаются и выключаются скриптом. Здесь это приходится
   говорить явно: атрибут hidden прячет элемент только стилем браузера, а
   любое наше правило с display его перебивает — .btn это inline-flex,
   .cart-sum и .cart-acc-link тоже. Без этой строки пустая корзина
   показывала бы «Итого 0 ₽» и кнопку «Оформить заказ». */
.cart [hidden]{display:none}

/* ---------- строки товаров ---------- */
.cart-lines{list-style:none;margin:0;padding:0}
.cart-line{
  display:grid;grid-template-columns:64px minmax(0,1fr) auto;gap:12px;
  align-items:start;padding:14px 0;border-bottom:1px solid var(--line);
  animation:fadeUp .4s var(--ease);
}
.cart-line:first-child{padding-top:0}

.cart-shot{
  width:64px;height:64px;border-radius:12px;overflow:hidden;flex:none;
  border:1px solid var(--line);background:rgba(6,10,19,.7);
}
.cart-shot img,.cart-shot video{width:100%;height:100%;object-fit:cover;display:block}
.cart-shot .cart-noimg{width:100%;height:100%;display:flex;align-items:center;
  justify-content:center;font-size:.62rem;color:var(--ink-3);text-align:center}

.cart-lb{min-width:0}
.cart-ln{display:block;font-size:.92rem;font-weight:600;line-height:1.35;
  margin-bottom:4px;overflow-wrap:anywhere}
/* Строки «столько же за штуку» здесь больше нет — владелец просил убрать:
   справа уже стоит сумма по строке, и цена за штуку рядом с ней была
   повтором. Правило .cart-lp удалено вместе со строкой, чтобы не осталось
   оформления без разметки. */
/* Выбранные цвет и размер — своей строкой под названием: «ваза × 2» без
   них не говорит покупателю, что именно он заказал. */
.cart-lo{display:block;margin-bottom:4px;font-size:.8rem;color:var(--ink-2)}
.cart-ls{font-size:.96rem;font-weight:800;white-space:nowrap;letter-spacing:-.01em}

.cart-qty{display:flex;align-items:center;gap:6px;margin-top:9px}
.cart-qty button{
  width:30px;height:30px;flex:none;display:flex;align-items:center;
  justify-content:center;cursor:pointer;border-radius:9px;
  border:1px solid var(--line);background:rgba(255,255,255,.03);
  color:var(--ink);font-size:1rem;font-weight:700;line-height:1;
  transition:border-color .25s,color .25s,background .25s;
}
.cart-qty button:hover{border-color:rgba(34,230,255,.5);color:#fff;
  background:rgba(34,230,255,.09)}
.cart-qty button:disabled{opacity:.35;cursor:default;border-color:var(--line);
  color:var(--ink-3);background:rgba(255,255,255,.03)}
.cart-qty [data-cart-qty]{
  min-width:26px;text-align:center;font-size:.9rem;font-weight:700;
  font-variant-numeric:tabular-nums;
}
.cart-del{
  margin-left:6px;width:30px;height:30px;flex:none;display:flex;
  align-items:center;justify-content:center;cursor:pointer;border-radius:9px;
  border:1px solid transparent;background:none;color:var(--ink-3);
  transition:color .25s,border-color .25s,background .25s;
}
.cart-del svg{width:15px;height:15px;fill:none;stroke:currentColor;
  stroke-width:1.9;stroke-linecap:round}
.cart-del:hover{color:#ff8fa8;border-color:rgba(255,120,150,.4);
  background:rgba(255,90,120,.09)}

/* Товар убрали из каталога, пока корзина была открыта. Строку не
   выкидываем молча: человек должен узнать, почему сумма изменилась. */
.cart-line-gone{opacity:.72}
.cart-line-gone .cart-ln{color:var(--ink-2)}
.cart-gone{
  display:inline-block;margin-top:6px;padding:4px 10px;border-radius:100px;
  font-size:.72rem;color:#ffc48a;border:1px solid rgba(255,170,90,.32);
  background:rgba(255,170,90,.08);
}

/* ---------- общий промокод корзины ----------
   Стоит между списком товаров и «Итого»: покупатель вводит код и сразу
   видит, как изменился итог под ним. */
.cart-promo{margin-top:14px}
.cart-promo[hidden]{display:none}
.cart-promo-row{display:flex;align-items:stretch;gap:8px}
.cart-promo-field{
  flex:1 1 auto;min-width:0;
  padding:9px 13px;border-radius:11px;
  border:1px solid var(--line-strong);
  background:rgba(255,255,255,.04);color:#fff;
  font-family:'JetBrains Mono',ui-monospace,SFMono-Regular,monospace;
  font-size:.88rem;letter-spacing:.08em;text-transform:uppercase;
}
.cart-promo-field::placeholder{
  font-family:inherit;font-size:.84rem;letter-spacing:0;
  text-transform:none;color:var(--ink-3);
}
.cart-promo-field:focus{outline:none;border-color:var(--accent,#6ea8ff);
  box-shadow:0 0 0 3px rgba(110,168,255,.18)}
/* Погашенное поле — применённый код: менять его нечем, стереть можно
   кнопкой «Убрать» рядом. Так покупатель видит, ЧТО применилось. */
.cart-promo-field:disabled{opacity:.85;cursor:default}
.cart-promo-apply{
  flex:0 0 auto;padding:9px 16px;border-radius:11px;
  font-size:.86rem;font-weight:600;white-space:nowrap;color:#fff;
}
.cart-promo-apply:disabled{cursor:default;opacity:1;color:#fff}
.cart-promo-drop{
  appearance:none;cursor:pointer;flex:0 0 auto;
  padding:9px 12px;border-radius:11px;
  border:1px solid transparent;background:transparent;color:var(--ink-3);
  font:inherit;font-size:.82rem;
  text-decoration:underline;text-underline-offset:3px;
}
.cart-promo-drop:hover{color:var(--ink-2)}

/* ============ Кнопка «Применить» у поля промокода ============

   Владелец: «цвет кнопок их не видно надо сделать в админ панели чтобы мог
   выбирать цвета кнопок и эффекты и анимацию кнопок». Настройка СВОЯ у окна
   товара и СВОЯ у корзины.

   Вид и анимацию ставит скрипт на КОРЕНЬ документа
   (`html[data-apply-win-fx=…]`), а не на сами кнопки: кнопка окна товара
   рождается заново на каждый товар, и настройка на ней пропала бы вместе с
   прежней кнопкой.

   Оттенки приезжают ГОТОВЫМИ (`--apply-win-from/-to/-ink/-rgb`): палитры на
   странице нет, а собрать `rgba()` из «#7a3ff2» средствами CSS нельзя.
   Каждая кнопка берёт свои через посредника `--a-*`, поэтому все виды ниже
   написаны один раз, а не по разу на каждую область. Переменных нет —
   «цвет сайта»: тогда работают запасные --cyan/--violet. */
.code-apply{
  --a-from:var(--apply-win-from,var(--cyan));
  --a-to:var(--apply-win-to,var(--violet));
  --a-ink:var(--apply-win-ink,#08131f);
  --a-rgb:var(--apply-win-rgb,34,230,255);
}
.cart-promo-apply{
  --a-from:var(--apply-cart-from,var(--cyan));
  --a-to:var(--apply-cart-to,var(--violet));
  --a-ink:var(--apply-cart-ink,#08131f);
  --a-rgb:var(--apply-cart-rgb,34,230,255);
}

/* Вид. «Главная» — заливка, а не контур: владелец жаловался, что кнопку НЕ
   ВИДНО, и «по умолчанию» обязано быть заметным.

   Заливку задаём ДВУМЯ свойствами — `background-color` и `background-image`,
   а не сокращением `background`. Причина не стилистическая: если положиться
   на один градиент, у кнопки не остаётся СВОЕГО цвета подложки, и цвет букв
   считается от того, что за кнопкой. На тёмной странице это давало тёмные
   буквы на тёмном — ровно то, на что владелец и жаловался. Цвет-подложка
   обязан быть настоящим. */
html[data-apply-win-fx="primary"] .code-apply,
html[data-apply-cart-fx="primary"] .cart-promo-apply{
  border-color:transparent;
  background-color:var(--a-from);
  background-image:linear-gradient(135deg,var(--a-from),var(--a-to));
  color:var(--a-ink);
}
html[data-apply-win-fx="outline"] .code-apply,
html[data-apply-cart-fx="outline"] .cart-promo-apply{
  border-color:var(--a-from);
  background-color:transparent;
  background-image:none;
  color:var(--a-from);
}
html[data-apply-win-fx="ghost"] .code-apply,
html[data-apply-cart-fx="ghost"] .cart-promo-apply{
  border-color:transparent;
  background-color:rgba(var(--a-rgb),.14);
  background-image:none;
  color:var(--a-from);
}
html[data-apply-win-fx="glow"] .code-apply,
html[data-apply-cart-fx="glow"] .cart-promo-apply{
  border-color:var(--a-from);
  background-color:rgba(var(--a-rgb),.16);
  background-image:none;
  color:#fff;
  box-shadow:0 0 22px rgba(var(--a-rgb),.45);
}
html[data-apply-win-fx="gradient"] .code-apply,
html[data-apply-cart-fx="gradient"] .cart-promo-apply{
  border-color:transparent;
  background-color:var(--a-from);
  background-image:linear-gradient(135deg,var(--a-from),var(--a-to),var(--a-from));
  background-size:200% 100%;
  color:var(--a-ink);
}
html[data-apply-win-fx="pill"] .code-apply,
html[data-apply-cart-fx="pill"] .cart-promo-apply{
  border-color:transparent;
  background-color:var(--a-from);
  background-image:linear-gradient(135deg,var(--a-from),var(--a-to));
  color:var(--a-ink);
  border-radius:999px;
}

/* Погашенная кнопка — это «готово», а не «недоступно»: покупатель должен
   прочитать, что код принят, поэтому буквы остаются читаемыми. Цвет берём
   из правила вида выше — здесь его НЕ задаём, иначе вид проиграл бы. */
html[data-apply-win-fx] .code-apply:disabled,
html[data-apply-cart-fx] .cart-promo-apply:disabled{opacity:1;cursor:default}
html[data-apply-win-fx] .code-apply.is-ok,
html[data-apply-cart-fx] .cart-promo-apply.is-ok{filter:saturate(.75);opacity:.92}

/* Анимация. Общее «нет» — первым: без него на кнопке осталась бы анимация
   от прежней настройки, и «без анимации» ничего бы не выключало. */
html[data-apply-win-anim] .code-apply,
html[data-apply-cart-anim] .cart-promo-apply{animation:none}
html[data-apply-win-anim="pulse"] .code-apply,
html[data-apply-cart-anim="pulse"] .cart-promo-apply{
  animation:applyPulse 1.8s ease-in-out infinite;
}
html[data-apply-win-anim="float"] .code-apply,
html[data-apply-cart-anim="float"] .cart-promo-apply{
  animation:applyFloat 2.4s ease-in-out infinite;
}
html[data-apply-win-anim="glow"] .code-apply,
html[data-apply-cart-anim="glow"] .cart-promo-apply{
  animation:applyGlow 2s ease-in-out infinite;
}
/* Блик — ОТДЕЛЬНЫМ СЛОЕМ поверх кнопки, а не второй картинкой фона. Фон
   здесь занят видом («рамка», «тихая»), и блик, положенный в фон, перебил бы
   выбранный вид: рамка превратилась бы в градиент. Слой же ходит поверх
   любого вида и ничего ему не диктует. */
html[data-apply-win-anim="shine"] .code-apply,
html[data-apply-cart-anim="shine"] .cart-promo-apply{
  position:relative;
  overflow:hidden;
}
html[data-apply-win-anim="shine"] .code-apply::after,
html[data-apply-cart-anim="shine"] .cart-promo-apply::after{
  content:'';
  position:absolute;
  inset:0;
  pointer-events:none;
  background-image:linear-gradient(115deg,transparent 38%,rgba(255,255,255,.5) 50%,transparent 62%);
  background-size:220% 100%;
  animation:applyShine 2.4s linear infinite;
}
html[data-apply-win-anim="press"] .code-apply:hover,
html[data-apply-cart-anim="press"] .cart-promo-apply:hover{filter:brightness(1.14)}
html[data-apply-win-anim="press"] .code-apply:active,
html[data-apply-cart-anim="press"] .cart-promo-apply:active{transform:scale(.96)}

@keyframes applyPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.045)}}
@keyframes applyFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}
@keyframes applyGlow{
  0%,100%{box-shadow:0 0 14px rgba(var(--a-rgb),.3)}
  50%{box-shadow:0 0 28px rgba(var(--a-rgb),.75)}
}
@keyframes applyShine{
  0%{background-position:180% 0}
  100%{background-position:-180% 0}
}

/* ============ Кнопка «Вставить» у поля промокода ============

   Владелец: «надо добавить кнопку вставить иконку ... и при наведении на нее
   курсора всплывает текст Вставить, эту иконку и текст я к каждому товару
   добавляю по отдельности и могу редактировать иконки и цвет и эффекты она
   всегда по умолчанию включена в стандартном режиме».

   Настройка у КАЖДОГО товара, поэтому вид и анимация стоят атрибутами на
   самой кнопке, а не на корне документа: кнопка рождается заново на каждый
   товар, и настройка с корня достала бы её только одну. У кнопки «Применить»
   наоборот — там настройка общая на весь магазин, и живёт она на корне.

   Оттенки приезжают ГОТОВЫМИ (`--paste-from/-to/-ink/-rgb`): палитры на
   странице нет, а собрать `rgba()` из «#7a3ff2» средствами CSS нельзя.
   Переменных нет — «цвет сайта»: тогда работают запасные --cyan/--violet. */
.code-paste{
  --p-from:var(--paste-from,var(--cyan));
  --p-to:var(--paste-to,var(--violet));
  --p-ink:var(--paste-ink,#08131f);
  --p-rgb:var(--paste-rgb,34,230,255);
  /* Общие кадры анимации (`applyGlow`) считают свечение от `--a-rgb`. Без
     этого псевдонима `rgba(var(--a-rgb),.3)` недействителен, и «свечение»
     молча ничего бы не делало. Свои кадры заводить не стали: они разошлись
     бы с кадрами кнопки «Применить». */
  --a-rgb:var(--paste-rgb,34,230,255);
  position:relative;flex:0 0 auto;
  display:inline-flex;align-items:center;justify-content:center;
  width:40px;padding:0;
  border:1px solid var(--p-from);border-radius:11px;
  /* Заливку задаём ДВУМЯ свойствами, а не сокращением `background`: если
     положиться на один градиент, своего цвета подложки у кнопки не остаётся,
     и цвет значка считается от того, что за кнопкой. На тёмной странице это
     давало тёмное на тёмном — ровно та жалоба владельца, из-за которой
     появились настройки вида. */
  background-color:transparent;background-image:none;
  color:var(--p-from);
  font:inherit;cursor:pointer;
  transition:filter .16s ease, transform .16s ease;
}
/* Авторский стиль сильнее браузерного `[hidden]`, поэтому `display` у кнопки
   обязан уступать скрытию отдельным правилом: без него погашенная кнопка
   «Вставить» осталась бы на экране и жалась бы впустую. */
.code-paste[hidden]{display:none}
.paste-ic{display:block;width:19px;height:19px}
.paste-ic svg{
  display:block;width:100%;height:100%;
  fill:none;stroke:currentColor;stroke-width:1.7;
  stroke-linecap:round;stroke-linejoin:round;
}

/* Всплывающее слово. Владелец: «при наведении на нее курсора всплывает текст
   Вставить». Слово живёт ВНУТРИ строки промокода, а не выносится за окно,
   поэтому `overflow` у колонки окна (`overflow-y:auto`) его не срезает —
   и полосы прокрутки в колонке от него не появляется. */
.paste-tip{
  position:absolute;left:50%;bottom:calc(100% + 7px);
  transform:translateX(-50%);
  white-space:nowrap;pointer-events:none;
  padding:5px 9px;border-radius:8px;
  border:1px solid var(--line-strong,rgba(120,160,220,.35));
  background:rgba(8,14,24,.96);
  color:var(--ink-2,#dfe9f7);
  font-size:.74rem;font-weight:600;letter-spacing:.01em;
  opacity:0;
  transition:opacity .16s ease, transform .16s ease;
}
.code-paste:hover .paste-tip,
.code-paste:focus-visible .paste-tip{
  opacity:1;
  transform:translateX(-50%) translateY(-2px);
}

/* Вид. Те же шесть, что у кнопки «Применить», и по той же причине:
   владелец выбирает из закрытого списка, и вид, которого нет в оформлении,
   панель предлагать не должна. «Рамка» — вид по умолчанию: владелец
   жаловался, что кнопок НЕ ВИДНО, и прятать эту значило бы повторить
   жалобу; но заливкой она не спорит с главной кнопкой строки. */
.code-paste[data-paste-fx="primary"]{
  border-color:transparent;
  background-color:var(--p-from);
  background-image:linear-gradient(135deg,var(--p-from),var(--p-to));
  color:var(--p-ink);
}
.code-paste[data-paste-fx="outline"]{
  border-color:var(--p-from);
  background-color:transparent;
  background-image:none;
  color:var(--p-from);
}
.code-paste[data-paste-fx="ghost"]{
  border-color:transparent;
  background-color:rgba(var(--p-rgb),.14);
  background-image:none;
  color:#fff;
}
.code-paste[data-paste-fx="glow"]{
  border-color:var(--p-from);
  background-color:rgba(var(--p-rgb),.16);
  background-image:none;
  color:#fff;
  box-shadow:0 0 18px rgba(var(--p-rgb),.45);
}
.code-paste[data-paste-fx="gradient"]{
  border-color:transparent;
  background-color:var(--p-from);
  background-image:linear-gradient(135deg,var(--p-from),var(--p-to),var(--p-from));
  background-size:200% 100%;
  color:var(--p-ink);
}
.code-paste[data-paste-fx="pill"]{
  border-color:transparent;
  background-color:var(--p-from);
  background-image:linear-gradient(135deg,var(--p-from),var(--p-to));
  color:var(--p-ink);
  border-radius:999px;
}

/* Анимация. Общее «нет» — первым: без него на кнопке осталась бы анимация
   от прежней настройки, и «без анимации» ничего бы не выключало. */
.code-paste[data-paste-anim]{animation:none}
.code-paste[data-paste-anim="pulse"]{animation:applyPulse 1.8s ease-in-out infinite}
.code-paste[data-paste-anim="float"]{animation:applyFloat 2.4s ease-in-out infinite}
.code-paste[data-paste-anim="glow"]{animation:applyGlow 2s ease-in-out infinite}
/* Блик — отдельным слоем поверх кнопки, а не второй картинкой фона: фон
   занят видом, и блик в фоне перебил бы выбранный вид. */
.code-paste[data-paste-anim="shine"]{overflow:hidden}
.code-paste[data-paste-anim="shine"]::after{
  content:'';position:absolute;inset:0;pointer-events:none;
  background-image:linear-gradient(115deg,transparent 38%,rgba(255,255,255,.5) 50%,transparent 62%);
  background-size:220% 100%;
  animation:applyShine 2.4s linear infinite;
}
.code-paste[data-paste-anim="press"]:hover{filter:brightness(1.14)}
.code-paste[data-paste-anim="press"]:active{transform:scale(.96)}

.cart-promo-note{margin:8px 0 0;font-size:.8rem;line-height:1.45}
.cart-promo-note[data-kind="ok"]{color:#8fd8a0}
.cart-promo-note[data-kind="bad"]{color:#ff9aa8}
/* Строка скидки внутри итога. `flex:1 1 100%` выносит её на отдельную
   строку: сам `.cart-sum` — флекс «подпись слева, сумма справа», и без
   этого три куска встали бы в один ряд. */
.cart-sum > [data-cart-promo-sumrow]{flex:1 1 100%}
.cart-sum-row [data-cart-promo-sumcut],
.cart-sum-row [data-cart-promo-sumcut2]{color:#8fd8a0}

/* ---------- итог и кнопки ---------- */
.cart-sum{
  display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  flex-wrap:wrap;
  margin-top:18px;padding-top:16px;border-top:1px solid var(--line-strong);
  font-size:1rem;color:var(--ink-2);
}
.cart-sum b{font-size:1.4rem;font-weight:800;color:#fff;letter-spacing:-.02em}
/* Итог на шаге оформления: не пара «Итого — сумма», а три строки.
   Товары и доставка стоят порознь, потому что доставка бывает платной:
   без разбивки итог выглядел бы больше суммы товаров просто так, и
   покупатель решил бы, что его обсчитали.
   `display:block` перебивает `display:flex` у .cart-sum, поэтому скрытие
   приходится задавать отдельным правилом: авторский стиль сильнее
   браузерного [hidden], и без второй строки блок остался бы виден. */
.cart-sum-final{display:block}
.cart-sum-final[hidden]{display:none}
.cart-sum-row{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
.cart-sum-row + .cart-sum-row{margin-top:6px}
.cart-sum-row b{font-size:.95rem;font-weight:600;color:var(--ink-2);letter-spacing:0}
.cart-sum-all{margin-top:10px;padding-top:10px;border-top:1px solid var(--line)}
.cart-sum-all b{font-size:1.4rem;font-weight:800;color:#fff;letter-spacing:-.02em}
.cart-hint{margin:10px 0 0;font-size:.78rem;color:var(--ink-3);line-height:1.5}
/* Подсказка под полем формы заказа: зачем здесь эта почта. Отдельным
   классом, а не .fc-hint — тот живёт в формах связи и назван по ним. */
.cart-note{display:block;margin-top:7px;font-size:.74rem;line-height:1.4;
  color:var(--ink-3)}
.cart-warn{
  margin:14px 0 0;padding:11px 14px;border-radius:var(--r-sm);
  border:1px solid rgba(255,170,90,.32);background:rgba(255,170,90,.08);
  color:#ffc48a;font-size:.82rem;line-height:1.5;
}
.cart-empty{margin:22px 0 0;color:var(--ink-2);font-size:.92rem;line-height:1.6}

.cart-go{width:100%;margin-top:16px}
.cart-clear{
  display:block;width:100%;margin-top:12px;padding:9px;border:0;
  background:none;color:var(--ink-3);font-size:.82rem;cursor:pointer;
  border-radius:10px;transition:color .25s,background .25s;
}
.cart-clear:hover{color:#ff8fa8;background:rgba(255,90,120,.07)}
.cart-acc-link{
  display:flex;align-items:center;justify-content:space-between;gap:10px;
  width:100%;margin-top:14px;padding:12px 14px;cursor:pointer;
  border-radius:var(--r-sm);border:1px solid var(--line);
  background:rgba(255,255,255,.02);color:var(--ink-2);font-size:.86rem;
  transition:border-color .25s,color .25s;
}
.cart-acc-link:hover{border-color:rgba(34,230,255,.45);color:#fff}
.cart-acc-link[hidden]{display:none}
.cart-acc-link span{font-weight:700;color:var(--ink)}

.cart-back-link{
  display:inline-flex;align-items:center;gap:7px;margin-bottom:16px;
  padding:7px 12px 7px 8px;border-radius:100px;cursor:pointer;
  border:1px solid var(--line);background:rgba(255,255,255,.03);
  color:var(--ink-2);font-size:.82rem;
  transition:border-color .25s,color .25s;
}
.cart-back-link svg{width:15px;height:15px;fill:none;stroke:currentColor;
  stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.cart-back-link:hover{border-color:rgba(34,230,255,.45);color:#fff}

/* ---------- оформление ---------- */
.cart-label{
  display:block;font-size:.76rem;letter-spacing:.14em;text-transform:uppercase;
  color:var(--ink-3);margin-bottom:8px;font-weight:600;
}
.cart-ship{display:flex;flex-direction:column;gap:8px}
.cart-radio{
  display:flex;align-items:center;gap:10px;padding:12px 14px;cursor:pointer;
  border-radius:var(--r-sm);border:1px solid var(--line);
  background:rgba(255,255,255,.02);font-size:.88rem;
  transition:border-color .25s,background .25s;
}
.cart-radio:hover{border-color:rgba(34,230,255,.4)}
.cart-radio input{accent-color:var(--cyan);width:17px;height:17px;flex:none}
.cart-radio:has(input:checked){border-color:rgba(34,230,255,.55);
  background:rgba(34,230,255,.07)}

.cart-consent{
  display:flex;align-items:flex-start;gap:10px;margin:4px 0 0;
  font-size:.8rem;color:var(--ink-2);line-height:1.5;cursor:pointer;
}
.cart-consent input{margin-top:2px;accent-color:var(--cyan);width:16px;
  height:16px;flex:none}
.cart-pol{
  padding:0;border:0;background:none;cursor:pointer;
  color:var(--cyan);font:inherit;text-decoration:underline;
  text-underline-offset:2px;
}
.cart-pol:hover{color:#fff}

/* ---------- «заказ принят» ---------- */
.cart-done{padding:14px 0 0;text-align:center;animation:fadeUp .5s var(--ease)}
.cart-done-ic{
  width:62px;height:62px;margin:0 auto 16px;display:grid;place-items:center;
  border-radius:50%;border:1px solid rgba(107,255,184,.45);
  background:rgba(107,255,184,.1);box-shadow:0 0 34px rgba(107,255,184,.28);
}
.cart-done-ic svg{width:30px;height:30px;fill:none;stroke:var(--lime);
  stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}
.cart-done h4{margin:0 0 10px;font-size:1.24rem;font-weight:800;
  letter-spacing:-.02em}
.cart-done-sum{margin:0 0 12px;font-size:1.06rem;font-weight:700;color:#fff}
.cart-done-note{margin:0;color:var(--ink-2);font-size:.88rem;line-height:1.6}

/* ---------- оплата: итог заказа и кабинет ---------- */

/* Строка об оплате на экране «заказ принят» и после возврата с формы
   ЮKassa. У плохого исхода свой цвет: «оплата не прошла» должно бросаться
   в глаза, а не читаться как обычная приписка под итогом. */
.cart-done-pay{margin:0 0 12px;color:var(--ink-2);font-size:.88rem;line-height:1.6}
.cart-done-pay.bad{color:var(--magenta)}

/* Значок оплаты в карточке заказа в кабинете. Слово обязательно: у части
   людей цвет не различается, а «Оплачен» и «Ждёт оплаты» путать нельзя. */
.acc-paychip{flex:none;padding:4px 10px;border-radius:999px;
  font-size:.72rem;font-weight:700;border:1px solid currentColor}
.acc-paychip.ok{color:var(--lime)}
.acc-paychip.wait{color:var(--ink-3)}
.acc-paychip.back{color:var(--violet)}

/* Кнопка «Оплатить» рядом с «Повторить заказ». Заливная, как главная
   кнопка сайта: для неоплаченного заказа это главное действие. */
.acc-pay{margin-left:auto;flex:none;padding:9px 15px;border-radius:999px;
  border:0;background:linear-gradient(115deg,var(--cyan),var(--violet));
  color:#02121a;font-family:inherit;font-size:.79rem;font-weight:800;
  cursor:pointer;transition:box-shadow .3s,transform .3s}
.acc-pay:hover{box-shadow:0 0 20px rgba(34,230,255,.35)}
.acc-pay:disabled{opacity:.6;cursor:default;box-shadow:none}
/* Когда «Оплатить» уже отодвинута вправо, «Повторить заказ» идёт следом,
   а не занимает свободное место второй раз. */
.acc-ofoot .acc-pay + .acc-again{margin-left:0}

/* ---------- личный кабинет ---------- */
.cart-acc-note{margin:0 0 14px;color:var(--ink-3);font-size:.8rem;line-height:1.55}

/* Кто вошёл и выход. Почта стоит слева не для красоты: кабинет обязан
   показывать, ЧЕЙ он, иначе непонятно, почему заказов не видно. */
.acc-who{display:flex;align-items:center;gap:12px;margin:0 0 14px;
  padding:10px 14px;border:1px solid var(--line);border-radius:var(--r-sm);
  background:var(--surface-2)}
.acc-mail{flex:1;min-width:0;font-size:.86rem;font-weight:600;color:var(--ink);
  overflow-wrap:anywhere}
.acc-out{flex:none;padding:7px 12px;border:1px solid var(--line);
  border-radius:999px;background:none;color:var(--ink-2);font-family:inherit;
  font-size:.78rem;font-weight:600;cursor:pointer;
  transition:color .3s,border-color .3s}
.acc-out:hover{color:#fff;border-color:rgba(34,230,255,.45)}

.acc-orders{list-style:none;margin:0;padding:0;display:grid;gap:14px}
.acc-order{border:1px solid var(--line);border-radius:var(--r-sm);
  padding:14px 16px;background:var(--surface-2)}
.acc-ohead{display:flex;align-items:center;gap:10px;margin-bottom:6px}
.acc-ohead b{font-size:.95rem;font-weight:800;color:var(--ink)}
/* Стадия — цветом и словом. Одного цвета мало: у части людей он не
   различается, а «Отказ» и «Выполнена» путать нельзя. */
.acc-status{margin-left:auto;flex:none;padding:4px 10px;border-radius:999px;
  font-size:.72rem;font-weight:700;border:1px solid currentColor}
.acc-s-new{color:var(--cyan)}
.acc-s-work{color:var(--violet)}
.acc-s-paid{color:var(--lime)}
.acc-s-sent{color:var(--cyan)}
.acc-s-done{color:var(--ink-3)}
.acc-s-rej{color:var(--magenta)}
.acc-oat{font-size:.78rem;color:var(--ink-3);margin-bottom:10px}

/* Куда выбрана доставка и как оплачивается заказ. Строки «подпись —
   значение» в две колонки: подписи одной ширины, значения по левому краю —
   так адрес читается глазами, а не выискивается в строке. */
.acc-ofacts{display:grid;grid-template-columns:max-content 1fr;gap:4px 12px;
  margin:0 0 12px;font-size:.8rem}
.acc-ofact{display:contents}
.acc-ofact-k{color:var(--ink-3)}
.acc-ofact-v{color:var(--ink-2);overflow-wrap:anywhere}
/* Сообщение мастерской по заказу — под стадией, отдельной плашкой: текст
   владельца должен читаться как сообщение, а не как ещё одна служебная
   строка вроде даты. */
.acc-omsg{margin:0 0 10px;padding:10px 13px;border-radius:var(--r-sm);
  border:1px solid rgba(34,230,255,.28);background:rgba(34,230,255,.06)}
.acc-omsg-txt{font-size:.85rem;color:var(--ink);white-space:pre-wrap;
  overflow-wrap:anywhere}
.acc-omsg-at{font-size:.72rem;color:var(--ink-3);margin-top:5px}
/* Лента общих сообщений — НАД списком заказов: она про мастерскую целиком,
   а не про конкретный заказ, и место у неё своё. */
.acc-news{margin:0 0 14px;display:grid;gap:8px}
.acc-news-head{font-size:.74rem;letter-spacing:.1em;text-transform:uppercase;
  color:var(--ink-3)}
.acc-news-row{padding:10px 13px;border-radius:var(--r-sm);
  border:1px solid var(--line);background:rgba(255,255,255,.03)}
.acc-news-txt{font-size:.85rem;color:var(--ink-2);white-space:pre-wrap;
  overflow-wrap:anywhere}
.acc-news-at{font-size:.72rem;color:var(--ink-3);margin-top:5px}
/* Пометка «новое» — одна деталь на весь кабинет: и у сообщения, и у
   заказа, и на кнопке в шапке. Магента, а не голубой: голубым в кабинете
   уже помечены стадии заказа, и «новое» слилось бы со стадией. */
.acc-new{display:inline-block;margin-left:8px;padding:2px 8px;border-radius:999px;
  border:1px solid rgba(255,92,180,.45);background:rgba(255,92,180,.12);
  color:var(--magenta);font-size:.68rem;font-weight:700;font-style:normal;
  letter-spacing:.04em;vertical-align:middle}
/* Плашка с новым сообщением выделяется целиком: покупатель должен найти её
   в списке, не вычитывая подряд все заказы. */
.acc-omsg.fresh{border-color:rgba(255,92,180,.4);background:rgba(255,92,180,.06)}
.acc-news-row.fresh{border-color:rgba(255,92,180,.4);background:rgba(255,92,180,.06)}

/* Ссылка внутри сообщения. Владелец пишет адрес обычным текстом; по нажатию
   он копируется и через N секунд открывается. N задаёт владелец в панели, и
   число приходит с сервера — своего «5» здесь нет. */
.acc-link{display:inline-flex;flex-wrap:wrap;align-items:center;gap:6px;
  vertical-align:baseline}
.acc-link-btn{padding:2px 9px;border-radius:999px;
  border:1px solid rgba(34,230,255,.4);background:rgba(34,230,255,.08);
  color:var(--cyan);font-family:inherit;font-size:.8rem;font-weight:600;
  cursor:pointer;transition:border-color .3s,box-shadow .3s;max-width:100%;
  overflow-wrap:anywhere}
.acc-link-btn:hover{border-color:rgba(34,230,255,.75);
  box-shadow:0 0 16px rgba(34,230,255,.22)}
.acc-link-btn:disabled{cursor:default}
.acc-link.on .acc-link-btn{border-color:rgba(160,255,120,.5);
  background:rgba(160,255,120,.08);color:var(--lime)}
.acc-link-note{font-size:.72rem;color:var(--ink-3)}
.acc-link-stay{padding:2px 8px;border:0;background:none;color:var(--ink-3);
  font-family:inherit;font-size:.72rem;text-decoration:underline;cursor:pointer}
.acc-link-stay:hover{color:var(--ink)}
.acc-link-stay[hidden]{display:none}

/* Возврат по заказу. Форма раскрывается по кнопке: в свёрнутом виде это
   одна строка со сроком, и она не должна отвлекать от стадии заказа. */
.acc-ret{margin-top:12px;border-top:1px solid var(--line);padding-top:11px}
.acc-ret-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.acc-ret-btn{flex:none;padding:9px 15px;border-radius:999px;
  border:1px solid rgba(255,92,180,.4);background:rgba(255,92,180,.08);
  color:#fff;font-family:inherit;font-size:.79rem;font-weight:700;
  cursor:pointer;transition:border-color .3s,box-shadow .3s}
.acc-ret-btn:hover{border-color:rgba(255,92,180,.75);
  box-shadow:0 0 18px rgba(255,92,180,.2)}
.acc-ret-btn:disabled{opacity:.55;cursor:default;box-shadow:none}
.acc-ret-hint{font-size:.74rem;color:var(--ink-3)}
.acc-ret-form{display:grid;gap:10px;margin-top:12px}
.acc-ret-form[hidden]{display:none}
.acc-ret-area{width:100%;box-sizing:border-box;padding:11px 13px;
  border-radius:var(--r-sm);border:1px solid var(--line);
  background:rgba(6,10,18,.5);color:var(--ink);font-family:inherit;
  font-size:.85rem;line-height:1.55;resize:vertical}
.acc-ret-area:focus{outline:none;border-color:rgba(34,230,255,.45)}
.acc-ret-file{display:flex;align-items:center;gap:10px;flex-wrap:wrap;
  font-size:.78rem;color:var(--ink-3)}
.acc-ret-cap{flex:none;min-width:150px}
.acc-ret-file input[type=file]{flex:1;min-width:0;font-family:inherit;
  font-size:.76rem;color:var(--ink-2)}
.acc-ret-list{flex-basis:100%;color:var(--cyan);overflow-wrap:anywhere}
.acc-ret-acts{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.acc-ret-send{flex:none;padding:9px 15px;border-radius:999px;border:0;
  background:var(--magenta);color:#0b0b10;font-family:inherit;font-size:.79rem;
  font-weight:800;cursor:pointer}
.acc-ret-send:disabled{opacity:.55;cursor:default}

/* Копия правил ниже лежит в admin/admin.css — панель показывает живые
   образцы, а у неё свой файл стилей. Правишь здесь — правь и там:
   расхождение ловит ret-css-drift.js. */
/* ---------- кнопка «Отправить возврат»: вид владельца ----------

   Владелец обвёл эту кнопку на снимке кабинета и просил менять из панели
   «название кнопки цвет размер анимацию и эффекты». Коды вида приезжают с
   сервера (`accState.retBtn`), а что каждый код значит — вот здесь. Списки
   закрытые: панель не может прислать вид, которого тут нет. */
.ret-btn{
  /* «Как на сайте» — это цвет самой кнопки, он же и по умолчанию.
     Выбранный владельцем оттенок приезжает ГОТОВЫМИ значениями и
     подставляется сюда через style: из кода «amber» цвет не вывести. */
  --ret-from:var(--magenta); --ret-to:var(--magenta); --ret-ink:#0b0b10;
  position:relative; overflow:hidden;
  display:inline-flex; align-items:center; justify-content:center;
  font-family:inherit; font-weight:800; cursor:pointer;
  border:1px solid transparent; background:var(--magenta); color:var(--ret-ink);
  transition:translate .18s var(--ease), scale .18s var(--ease),
    box-shadow .18s var(--ease), background .18s var(--ease),
    color .18s var(--ease), border-color .18s var(--ease)}
.ret-btn:disabled{opacity:.55;cursor:default}
.ret-btn.ret-size-s{font-size:.74rem;padding:8px 14px;border-radius:10px}
.ret-btn.ret-size-m{font-size:.79rem;padding:9px 15px;border-radius:999px}
.ret-btn.ret-size-l{font-size:.9rem;padding:13px 24px;border-radius:14px}
.ret-btn.ret-size-xl{font-size:1rem;padding:16px 30px;border-radius:16px}
.ret-btn.ret-style-primary{background:linear-gradient(135deg,var(--ret-from),var(--ret-to));color:var(--ret-ink)}
.ret-btn.ret-style-outline{background:transparent;color:var(--ret-to);border-color:var(--ret-to)}
.ret-btn.ret-style-ghost{background:rgba(255,255,255,.07);color:var(--ink)}
.ret-btn.ret-style-glow{background:linear-gradient(135deg,var(--ret-from),var(--ret-to));color:var(--ret-ink);
  box-shadow:0 0 24px -6px var(--ret-to)}
.ret-btn.ret-style-gradient{background:linear-gradient(90deg,var(--ret-from),var(--ret-to),var(--ret-from));
  background-size:200% 100%; color:var(--ret-ink)}
.ret-btn.ret-style-pill{background:linear-gradient(135deg,var(--ret-from),var(--ret-to));
  color:var(--ret-ink);border-radius:999px}
/* Движение — то, что кнопка делает САМА. Живёт на `transform`, а эффекты
   под курсором — на отдельных `translate`/`scale`/`rotate`: так одно
   свойство не перебивает другое, и «подъём» работает вместе с
   «пульсацией». Одна общая `transform` на оба списка — и владелец решает,
   что половина выбора не работает. */
.ret-btn.ret-anim-pulse{animation:retPulse 2.4s var(--ease) infinite}
.ret-btn.ret-anim-float{animation:retFloat 3.2s var(--ease) infinite}
.ret-btn.ret-anim-glow{animation:retGlow 2.8s var(--ease) infinite}
.ret-btn.ret-anim-shine::after{content:'';position:absolute;top:0;bottom:0;left:-45%;width:38%;
  background:linear-gradient(90deg,transparent,rgba(255,255,255,.38),transparent);
  animation:retShine 3.2s linear infinite;pointer-events:none}
.ret-btn.ret-anim-press{transition-duration:.12s}
.ret-btn.ret-anim-press:hover{transform:scale(1.045)}
/* Эффекты — то, что кнопка делает ПОД КУРСОРОМ. */
.ret-btn.ret-fx-lift:hover{translate:0 -2px}
.ret-btn.ret-fx-grow:hover{scale:1.05}
.ret-btn.ret-fx-tilt:hover{rotate:-2deg}
.ret-btn.ret-fx-ring:hover{box-shadow:0 0 0 3px rgba(255,255,255,.2)}
.ret-btn.ret-fx-shadow{box-shadow:0 8px 20px -10px rgba(0,0,0,.8)}
.ret-btn.ret-fx-shadow:hover{box-shadow:0 14px 28px -10px rgba(0,0,0,.9)}
.ret-btn.ret-fx-underline{text-decoration:underline;text-underline-offset:3px}
.ret-btn.ret-fx-underline:hover{text-decoration-thickness:2px}
@keyframes retPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.05)}}
@keyframes retFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}
@keyframes retGlow{0%,100%{filter:brightness(1)}50%{filter:brightness(1.25)}}
@keyframes retShine{0%{left:-45%}60%,100%{left:120%}}
@media (prefers-reduced-motion:reduce){
  .ret-btn,.ret-btn::after{animation:none}
  .ret-btn{transition:none}}
/* ---------- конец правил кнопки «Отправить возврат» ---------- */
.acc-ret-cancel{flex:none;padding:9px 14px;border-radius:999px;
  border:1px solid var(--line);background:none;color:var(--ink-3);
  font-family:inherit;font-size:.79rem;cursor:pointer}
.acc-ret-cancel:hover{color:var(--ink)}
.acc-ret-cancel:disabled{opacity:.55;cursor:default}
.acc-ret-done{margin-top:12px;padding:11px 13px;border-radius:var(--r-sm);
  border:1px solid rgba(255,92,180,.35);background:rgba(255,92,180,.07)}
.acc-ret-done b{font-size:.85rem;color:#fff}
.acc-ret-reason{margin-top:6px;font-size:.83rem;color:var(--ink-2);
  white-space:pre-wrap;overflow-wrap:anywhere}
.acc-ret-files{margin-top:6px;font-size:.74rem;color:var(--ink-3)}

/* ---------- спор по возврату: переписка и ответ покупателя ----------
   Владелец: «клиент мог ответить раз в 24 часа» и «вставить файлы фото или
   видео». Здесь — лента разговора и форма ответа. */

.acc-ret-talk{margin-top:12px;border-top:1px dashed var(--line);padding-top:11px}
/* Лента прокручивается: спор тянется месяцами, и растущая лента вытолкнула
   бы из виду саму форму ответа. */
.acc-ret-thread{display:grid;gap:8px;max-height:320px;overflow-y:auto}
/* Сторона разговора видна КРАЕМ: в споре важно с одного взгляда понять, где
   мастерская, а где сам покупатель. */
.acc-ret-msg{display:grid;gap:4px;padding:8px 11px;border-radius:var(--r-sm);
  border:1px solid var(--line);background:rgba(255,255,255,.02)}
.acc-ret-msg.from-owner{border-left:3px solid var(--cyan)}
.acc-ret-msg.from-customer{border-left:3px solid var(--magenta)}
.acc-ret-msg.from-system{border-left:3px solid var(--line);background:none}
.acc-ret-msg-top{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
.acc-ret-msg-top b{font-size:.8rem;color:var(--ink)}
.acc-ret-msg-at{font-size:.72rem;color:var(--ink-3)}
.acc-ret-msg-text{font-size:.83rem;color:var(--ink-2);white-space:pre-wrap;
  overflow-wrap:anywhere}
.acc-ret-msg-files{display:flex;flex-wrap:wrap;gap:8px}
.acc-ret-msg-file{font-size:.76rem;color:var(--cyan)}
.acc-ret-untils{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
.acc-ret-until{font-size:.77rem;color:var(--ink-2);border:1px solid var(--line);
  border-radius:999px;padding:3px 10px}
.acc-ret-shut,.acc-ret-wait{margin-top:10px;font-size:.8rem;padding:8px 11px;
  border-radius:var(--r-sm)}
.acc-ret-shut{color:#ffd166;background:rgba(255,209,102,.08)}
/* Окно «ожидай ответ». Оно ВСТАЁТ НА МЕСТО формы ответа: пока срок ожидания
   не прошёл, кабинет не рисует ни поля, ни прикрепления файлов (см.
   `accRetTalk`). Владелец (08.10, вечер, снимок #4) просил править у него
   «цвет размер шрифта».

   Цвет — ПЕРЕМЕННОЙ, а не правилом на каждый оттенок: значения оттенков
   живут на сервере (`ACCENT_COLORS`), и вторая их копия в оформлении
   разошлась бы с ним на первой правке. Не выбрано — работает прежний
   `--ink-2`, поэтому вид по умолчанию не изменился. */
.acc-ret-wait{color:var(--wait-ink,var(--ink-2));background:rgba(255,255,255,.03)}

/* Ступени размера слова. Правило есть у КАЖДОЙ ступени списка: настройка без
   правила — это настройка, которая ничего не делает. Два класса в селекторе
   — не для красоты: у `.acc-ret-wait` выше уже стоит `font-size`, и одно
   имя класса проиграло бы ему по порядку. `m` совпадает с прежним числом. */
.acc-ret-wait.ret-wait-size-xs  {font-size:.72rem}
.acc-ret-wait.ret-wait-size-s   {font-size:.76rem}
.acc-ret-wait.ret-wait-size-m   {font-size:.8rem}
.acc-ret-wait.ret-wait-size-l   {font-size:.9rem}
.acc-ret-wait.ret-wait-size-xl  {font-size:1rem}
.acc-ret-wait.ret-wait-size-xxl {font-size:1.12rem}
.acc-ret-wait.ret-wait-size-xxxl{font-size:1.26rem}
.acc-ret-replyform{display:grid;gap:10px;margin-top:12px}
.acc-ret-replysend{flex:none}

/* Значок на кнопке «Личный кабинет» в шапке. Два класса, а не один: общее
   правило `.cart-acc-link span` красит вложенные span в цвет текста и
   перебило бы одиночный класс. */
.cart-acc-link .acc-new-dot{display:inline-block;margin-left:6px;min-width:18px;
  padding:1px 6px;border-radius:999px;background:var(--magenta);color:#0b0b10;
  font-size:.68rem;font-weight:800;text-align:center;line-height:1.5;
  vertical-align:middle}
.cart-acc-link .acc-new-dot[hidden]{display:none}
/* Состав итога заказа. Итог больше суммы позиций, когда есть платная
   доставка, — без этой строки покупатель решил бы, что его обсчитали. */
.acc-obreak{font-size:.78rem;color:var(--ink-3);margin:0 0 9px}
.acc-oitems{list-style:none;margin:0 0 12px;padding:0;display:grid;gap:6px}
.acc-oitems li{display:flex;gap:12px;font-size:.84rem;color:var(--ink-2)}
.acc-oitems li b{margin-left:auto;flex:none;color:var(--ink);font-weight:600}
.acc-ofoot{display:flex;align-items:center;gap:12px;
  border-top:1px solid var(--line);padding-top:11px}
.acc-ofoot > b{font-size:1rem;font-weight:800;color:#fff}
.acc-again{margin-left:auto;flex:none;padding:9px 15px;border-radius:999px;
  border:1px solid rgba(34,230,255,.35);background:rgba(34,230,255,.08);
  color:#fff;font-family:inherit;font-size:.79rem;font-weight:700;
  cursor:pointer;transition:border-color .3s,box-shadow .3s}
.acc-again:hover{border-color:rgba(34,230,255,.7);
  box-shadow:0 0 18px rgba(34,230,255,.22)}

@media (max-width:560px){
  .acc-order{padding:12px 13px}
  .acc-ohead{flex-wrap:wrap}
  .acc-status{margin-left:0}
  .acc-ofoot{flex-wrap:wrap}
  .acc-again{margin-left:0;width:100%;text-align:center}
}

@media (max-width:560px){
  .cart-box{width:100%;border-left:0}
  .cart-btn-label{display:none}
  .cart-btn{padding:10px 12px}
  .cart-body{padding:14px 16px 26px}
  .cart-line{grid-template-columns:52px minmax(0,1fr) auto;gap:10px}
  .cart-shot{width:52px;height:52px}
  .cart-sum b{font-size:1.24rem}
}


/* ============================================================
   РАБОТЫ: лента карточек
   ------------------------------------------------------------
   Раздел #portfolio. Раскладку «обложкой» считает js/main.js по тем же
   формулам, что и карусель товаров (carPlan / carProject / carShiftX /
   carShiftZ / carScale / carRot), поэтому здесь только внешний вид:
   карточка, кадры внутри неё и отклик на лайк.

   ВНУТРИ КАДРА — своя мини-обложка. Кадр сдвинут от показанного на --k
   шагов, сжат по --ak и повёрнут по Y: перелистывание читается как
   перекидывание карточек, а не как рывок картинки. Считает это браузер
   (одна переменная на кадр), а не скрипт — иначе на каждом движении
   пальца пересобиралась бы разметка.

   ГЕОМЕТРИЯ КАРТОЧКИ здесь своя: 380×470. Владелец просил карточку шире
   и «много места вверху для описания и названия» — поэтому выросла не
   только ширина, но и шапка: название и подпись переносятся на вторую
   строку, а не обрезаются. Числа связаны с раскладкой — меняешь ширину,
   проверь, сколько карточек влезает в сцену (шаг считает carPlan).
   ============================================================ */
/* РАЗМЕР КАРТОЧКИ выбирает владелец в панели — пять ступеней.

   Ширина и высота живут в переменных не ради красоты: от ширины считает
   геометрия ленты. `carPlan` берёт `offsetWidth` карточки, а шаг и поворот
   заданы долями ширины, поэтому смена числа ЗДЕСЬ сама пересчитывает,
   сколько карточек влезает в сцену, — раскладку трогать не нужно.

   Пропорция у всех ступеней одна (0.809): при смене размера не должны
   ехать пропорции шапки и кадра. Высота дорожки — высота карточки плюс
   28px запаса на свечение, поэтому она тоже от переменной.
   Ступень «normal» отдельным правилом не описана: это и есть база. */
#portfolio, .wr-roll{--wr-w:380px;--wr-h:470px}
#portfolio[data-size="compact"] , .wr-roll[data-size="compact"] {--wr-w:320px;--wr-h:396px}
#portfolio[data-size="large"]   , .wr-roll[data-size="large"]   {--wr-w:440px;--wr-h:544px}
#portfolio[data-size="wide"]    , .wr-roll[data-size="wide"]    {--wr-w:500px;--wr-h:618px}
#portfolio[data-size="showcase"], .wr-roll[data-size="showcase"]{--wr-w:560px;--wr-h:692px}

#portfolio .wr-track, .wr-roll .wr-track{height:calc(var(--wr-h,470px) + 28px)}

.wr-card{
  position:absolute;top:0;left:50%;
  width:var(--wr-w,380px);height:var(--wr-h,470px);
  margin-left:calc(var(--wr-w,380px) / -2);
  display:flex;flex-direction:column;overflow:hidden;
  padding:0;
  border:1px solid transparent;border-radius:var(--r-lg);
  /* Подложка непрозрачная и обрезана по padding-box. Без обрезки фон
     рисуется и под прозрачной рамкой, а содержимое — только по
     content-box, и на кромке остаётся 1px фона поверх снимка соседа. Это
     ровно та «видимая кромка задней карточки», которую владелец показывал
     трижды, поэтому обрезка здесь не украшение. */
  background:#0c1220;
  background-clip:padding-box;
  -webkit-background-clip:padding-box;
  cursor:pointer;
  transform:translate3d(var(--tx,0px),var(--ty,0px),var(--tz,0px))
            rotateY(var(--ry,0deg)) rotateX(var(--rx,0deg)) rotate(var(--rz,0deg))
            scale(var(--sc,1));
  transition:transform .62s var(--ease),
             border-color .35s var(--ease),box-shadow .35s var(--ease);
  opacity:var(--op,1);
  /* `will-change` — ТОЛЬКО НА ВРЕМЯ ХОДА, как у карточки товара.
     Здесь стояло постоянное `will-change:transform`, и карточка навсегда
     получала свой растровый слой. Слой снимается по текущему масштабу: пока
     карточка едет и поворачивается, браузер рисует её в одном масштабе, а
     после остановки не всегда перерисовывает заново — текст остаётся мягким.
     Владелец (29.10) показал это на ленте работ: сразу после хода карточка
     резкая, «через доли миллисекунды» — размытая. Подсказку слоя теперь даёт
     `.wr-track[data-moving="1"]` (ниже): она живёт ровно те 700 мс, что и
     переход, и снимается вместе с `is-turning`. */
  /* Скрывать изнанку — как у карточки товара (.car-card).

     Лента поворачивает карточку до −108°: у такой карточки к зрителю
     повёрнута ИЗНАНКА, и без этого правила она рисуется — зеркально,
     поверх соседа. Владелец видел это как «отражение предыдущей карточки
     на другой карточке» и просил сделать как в товарах, где карточка
     дальше 90° просто исчезает. Правило стоит и с -webkit-: Chrome
     понимает оба написания, но без префикса его нет в старых сборках. */
  backface-visibility:hidden;
  -webkit-backface-visibility:hidden;
}
.wr-card[data-far="1"]{pointer-events:none}

/* Подсказка растрового слоя — на время хода и только на него.

   `data-moving` ставит тот же `markTurning` (js/main.js), что и `is-turning`,
   ПЕРЕД `layout()`: слой появляется до первого кадра перехода. Снимается он
   через 700 мс вместе с `is-turning` — после остановки карточка снова живёт
   в обычном слое и текст перерисовывается резко. Одно правило на обе ленты:
   `#portfolio` на сайте и `.wr-roll` в лендинге, у обеих дорожка `.wr-track`. */
.wr-track[data-moving="1"] .wr-card{will-change:transform}

/* Объём. По кромке идёт светлая внутренняя грань, как на настоящей
   карточке из плотного картона: сверху блик, по периметру тонкая
   светлая линия. Без неё карточка читается плоским прямоугольником, и
   перспектива на перелистывании пропадает — а владелец просил именно
   «как настоящая карточка». */
.wr-card::after{
  content:'';position:absolute;inset:0;z-index:4;pointer-events:none;
  border-radius:inherit;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.13),
             inset 0 0 0 1px rgba(255,255,255,.06);
  /* Затемнение задних карточек — ПОДЛОЖКОЙ, а не прозрачностью карточки.

     Раньше глубина делалась через opacity у .wr-card (--op падал до 0.85),
     и карточка становилась полупрозрачной: сквозь неё просвечивал кадр
     соседа. Владелец видел это как «отражение предыдущей карточки на
     другой» и просил сделать как в товарах — там ровно эту причину убрали
     («чтобы карточки отображались чистыми без засветов от предыдущей»).

     Теперь карточка НЕПРОЗРАЧНА всегда, а темнеет подложка: тот же
     визуальный шаг в глубину, но сквозь карточку ничего не видно.
     --dim считает скрипт (1 − прежняя прозрачность). */
  background:rgba(12,18,32,var(--dim,0));
}

/* Вид подсветки выбирает владелец. Здесь только цвет и размах: сами
   правила ниже одни и те же, поэтому новый вид — это одна строка.

   Один набор на ДВЕ ленты — на ленту работ (#portfolio) и на карусели
   товаров (.shop-block). Владелец просил сделать товары «как в работах»,
   и два отдельных набора означали бы, что «как в работах» однажды
   перестанет быть правдой: добавили вид в одну ленту — в другой его нет.
   Список видов приходит с сервера (CARD_GLOWS) и тоже один на две ленты.

   --glow-a и --glow-b читает лента работ (вспышка и свечение).
   --glow-rest — только для карточек товара: рамка карточки В ПОКОЕ.
   Нужна отдельно, потому что у товара карточка должна отзываться на
   наведение, а если в покое рамка уже стоит в полном цвете свечения,
   отзываться ей нечем: под курсором ничего не меняется, и карточка
   кажется неживой. В ленте работ такого требования нет — там карточка
   отвечает вспышкой на перелистывании, поэтому --glow-rest ей не задан. */
#portfolio[data-glow="edge"], .shop-block[data-glow="edge"], .wr-roll[data-glow="edge"]{--glow-a:rgba(34,230,255,.95); --glow-b:rgba(34,230,255,.20); --glow-rest:rgba(34,230,255,.42)}
#portfolio[data-glow="halo"], .shop-block[data-glow="halo"], .wr-roll[data-glow="halo"]{--glow-a:rgba(139,92,246,.95); --glow-b:rgba(139,92,246,.24); --glow-rest:rgba(139,92,246,.42)}
#portfolio[data-glow="neon"], .shop-block[data-glow="neon"], .wr-roll[data-glow="neon"]{--glow-a:rgba(255,79,216,.95); --glow-b:rgba(34,230,255,.22); --glow-rest:rgba(255,79,216,.42)}
#portfolio[data-glow="gold"], .shop-block[data-glow="gold"], .wr-roll[data-glow="gold"]{--glow-a:rgba(255,196,92,.95); --glow-b:rgba(255,150,40,.22); --glow-rest:rgba(255,196,92,.42)}
#portfolio[data-glow="rainbow"], .shop-block[data-glow="rainbow"], .wr-roll[data-glow="rainbow"]{--glow-a:rgba(34,230,255,.95); --glow-b:rgba(255,79,216,.22); --glow-rest:rgba(34,230,255,.42)}
#portfolio[data-glow="none"], .shop-block[data-glow="none"], .wr-roll[data-glow="none"]{--glow-a:transparent;--glow-b:transparent;--glow-rest:transparent}

.wr-card[data-active="1"]{
  border-color:var(--glow-a,rgba(34,230,255,.42));
  box-shadow:0 30px 80px rgba(0,0,0,.55),0 0 46px var(--glow-b,rgba(34,230,255,.16));
}
.wr-card:not([data-active="1"]):hover{
  border-color:var(--glow-a,rgba(34,230,255,.28));
  box-shadow:0 24px 60px rgba(0,0,0,.5);
}

/* Свечение НА ПЕРЕЛИСТЫВАНИИ. В покое карточка только подсвечена рамкой;
   вспышка включается, пока идёт движение, — иначе лента выглядела бы
   витриной, которая светится всегда. Класс is-turning ставит скрипт на
   время перехода. */
#portfolio.is-turning .wr-card[data-active="1"], .wr-roll.is-turning .wr-card[data-active="1"]{
  animation:wrGlowTurn .62s var(--ease) both;
}
@keyframes wrGlowTurn{
  0%  {box-shadow:0 30px 80px rgba(0,0,0,.55),0 0 0 0 transparent}
  38% {box-shadow:0 34px 92px rgba(0,0,0,.6),0 0 74px 6px var(--glow-a,transparent)}
  100%{box-shadow:0 30px 80px rgba(0,0,0,.55),0 0 46px var(--glow-b,transparent)}
}
/* Радужный ободок переливается ЦВЕТОМ свечения, а не фильтром оттенка:
   фильтр перекрасил бы заодно и фотографии внутри карточки. */
#portfolio[data-glow="rainbow"].is-turning .wr-card[data-active="1"], .wr-roll[data-glow="rainbow"].is-turning .wr-card[data-active="1"]{
  animation:wrGlowRainbow .9s linear both;
}
@keyframes wrGlowRainbow{
  0%  {box-shadow:0 30px 80px rgba(0,0,0,.55),0 0 70px 5px rgba(34,230,255,.85)}
  33% {box-shadow:0 30px 80px rgba(0,0,0,.55),0 0 70px 5px rgba(255,79,216,.85)}
  66% {box-shadow:0 30px 80px rgba(0,0,0,.55),0 0 70px 5px rgba(255,196,92,.85)}
  100%{box-shadow:0 30px 80px rgba(0,0,0,.55),0 0 46px var(--glow-b,transparent)}
}
@media (prefers-reduced-motion:reduce){
  #portfolio.is-turning .wr-card[data-active="1"],   .wr-roll.is-turning .wr-card[data-active="1"]{animation:none}
}

/* ---------- Товары: то же перелистывание, что в ленте работ ----------
   Владелец просил сделать анимацию карточек товара «как анимация сейчас в
   карусели работах». Поэтому здесь НЕ второй набор правил, а те же самые
   ключевые кадры (wrGlowTurn, wrGlowRainbow) на карточку товара: две копии
   разошлись бы при первой же правке, и «как в работах» перестало бы быть
   правдой. Свечения тоже общие — см. наборы --glow-a / --glow-b /
   --glow-rest выше.

   Класс is-turning ставит скрипт на время перехода (js/main.js,
   markTurning): в покое карточка только подсвечена рамкой, вспышка
   включается, пока идёт движение. Иначе витрина светилась бы всегда.

   Настройка — на каждой карусели своя, поэтому правила идут от
   .shop-block (корень одной карусели), а не от #shop: у магазина может
   быть несколько лент, и владелец настраивает каждую отдельно. */
.shop-block.is-turning .car-card[data-active="1"]{
  animation:wrGlowTurn .62s var(--ease) both;
}
.shop-block[data-glow="rainbow"].is-turning .car-card[data-active="1"]{
  animation:wrGlowRainbow .9s linear both;
}
/* НЕТ ПОДСВЕТКИ — НЕТ И ВСПЫШКИ.
   Владелец (29.10): «я в этом разделе отключил Подсветку рамки при
   перелистывании, а там всё равно есть в конце перелистывания на карточке
   белая вспышка». И был прав: вид «без подсветки» гасил только ЦВЕТ
   (`--glow-a` / `--glow-b` в transparent), а сама анимация оставалась. В
   ключевых кадрах цвет свечения — не единственное, что меняется: там ещё и
   тень, и она росла с `0 30px 80px rgba(0,0,0,.55)` до
   `0 34px 92px rgba(0,0,0,.6)` и обратно. На тёмной теме этого не было
   видно, на бумаге вокруг карточки вспухал тёмный ореол — то самое «мигание»
   в конце хода, которое владелец принял за белую вспышку.
   Правило стоит ПОСЛЕ общих: селектор с `[data-glow="none"]` тяжелее, но
   порядок всё равно важен — так виднее, что именно оно отменяет. */
.shop-block[data-glow="none"].is-turning .car-card[data-active="1"],
#portfolio[data-glow="none"].is-turning .wr-card[data-active="1"],
.wr-roll[data-glow="none"].is-turning .wr-card[data-active="1"]{
  animation:none;
}
/* «Без вспышки» описан ниже, вместе с остальными видами движения: там же
   и лента работ. Здесь его быть не должно — правило «без вспышки» обязано
   стоять ПОСЛЕ радужного обода, иначе обод его перебьёт. */
/* «Поворот с подъёмом»: активная карточка приподнимается над лентой, а
   соседи чуть опускаются — движение читается как укладка карточек, а не
   как перекраска. Подъём задан переменной, которую читает трансформация
   карточки (см. .car-card выше): отдельной трансформацией его не задать,
   вторая затёрла бы первую и карточка потеряла бы своё место в ленте. */
.shop-block[data-move="lift"] .car-card[data-active="1"],
#portfolio[data-move="lift"] .wr-card[data-active="1"], .wr-roll[data-move="lift"] .wr-card[data-active="1"]{--ty:-18px}
.shop-block[data-move="lift"] .car-card:not([data-active="1"]),
#portfolio[data-move="lift"] .wr-card:not([data-active="1"]), .wr-roll[data-move="lift"] .wr-card:not([data-active="1"]){--ty:8px}

/* «Без вспышки»: движение прежнее, вспышка погашена. Рамка в покое при
   этом светится — это не то же самое, что вид «без подсветки», где гаснет
   и рамка тоже. Правило идёт ПОСЛЕ радужного: у того же правила селектор
   с #portfolio такой же по весу, и побеждает то, что ниже. */
.shop-block[data-move="quiet"].is-turning .car-card[data-active="1"],
#portfolio[data-move="quiet"].is-turning .wr-card[data-active="1"], .wr-roll[data-move="quiet"].is-turning .wr-card[data-active="1"]{
  animation:none;
}

/* ---------- Остальные виды движения ----------
   Список видов ОДИН на две ленты (SHOP_MOVES на сервере): владелец просил
   сделать товары «как в работах», и второй список означал бы, что «как в
   работах» однажды перестанет быть правдой.

   Задавать вид можно ТОЛЬКО через --ty, --rx и --rz. Остальные переменные
   трансформации (--tx, --tz, --ry, --sc) считает скрипт и ставит их прямо
   на карточку; правка из оформления их не перебьёт, и получился бы вид,
   который «есть в списке, но на сайте ничего не меняет». */

/* «Отскок»: карточки доезжают с перехлёстом. Меняется только кривая
   перехода — ни одной новой переменной, поэтому вид работает одинаково и в
   товарах, и в ленте работ. */
.shop-block[data-move="bounce"] .car-card,
#portfolio[data-move="bounce"] .wr-card, .wr-roll[data-move="bounce"] .wr-card{
  transition-timing-function:cubic-bezier(.34,1.56,.64,1);
}

/* «Наклон вперёд»: активная карточка наклоняется по X, соседи — чуть в
   обратную сторону. Наклон читается объёмно только вместе с перспективой
   сцены, поэтому здесь угол, а не сдвиг. */
.shop-block[data-move="tilt"] .car-card[data-active="1"],
#portfolio[data-move="tilt"] .wr-card[data-active="1"], .wr-roll[data-move="tilt"] .wr-card[data-active="1"]{--rx:-7deg}
.shop-block[data-move="tilt"] .car-card:not([data-active="1"]),
#portfolio[data-move="tilt"] .wr-card:not([data-active="1"]), .wr-roll[data-move="tilt"] .wr-card:not([data-active="1"]){--rx:3deg}

/* «Высокий подъём»: активная поднимается выше, соседи опускаются. Отличие
   от «поворота с подъёмом» в размахе: 30px против 18px, и соседи идут
   навстречу, а не попутно. */
.shop-block[data-move="rise"] .car-card[data-active="1"],
#portfolio[data-move="rise"] .wr-card[data-active="1"], .wr-roll[data-move="rise"] .wr-card[data-active="1"]{--ty:-30px}
.shop-block[data-move="rise"] .car-card:not([data-active="1"]),
#portfolio[data-move="rise"] .wr-card:not([data-active="1"]), .wr-roll[data-move="rise"] .wr-card:not([data-active="1"]){--ty:12px}

/* «Покачивание»: лёгкий наклон по Z, соседи — в противофазе. Угол мелкий
   намеренно: сильнее читалось бы как кривая вёрстка, а не как движение. */
.shop-block[data-move="sway"] .car-card[data-active="1"],
#portfolio[data-move="sway"] .wr-card[data-active="1"], .wr-roll[data-move="sway"] .wr-card[data-active="1"]{--rz:-1.6deg}
.shop-block[data-move="sway"] .car-card:not([data-active="1"]),
#portfolio[data-move="sway"] .wr-card:not([data-active="1"]), .wr-roll[data-move="sway"] .wr-card:not([data-active="1"]){--rz:1.2deg}

/* «Дыхание»: кадр внутри АКТИВНОЙ карточки медленно растёт и опадает. Это
   единственный вид, который живёт постоянно, а не только в момент
   перелистывания, — и единственный, где двигается ВНУТРЕННИЙ слой.

   Двигать саму карточку нельзя: её трансформацию оформление собирает по
   переменным, а вторая трансформация затёрла бы первую — карточка потеряла
   бы своё место в ленте. Кадр внутри — отдельный слой, с раскладкой он не
   спорит. Кадр именно РАСТЁТ: уменьшаясь, он показал бы из-под себя фон
   карточки. */
@keyframes cardBreathe{
  0%,100%{transform:scale(1)}
  50%    {transform:scale(1.045)}
}
.shop-block[data-move="breathe"] .car-card[data-active="1"] .car-shot,
#portfolio[data-move="breathe"] .wr-card[data-active="1"] .wr-media, .wr-roll[data-move="breathe"] .wr-card[data-active="1"] .wr-media{
  animation:cardBreathe 6s ease-in-out infinite;
  transform-origin:50% 60%;
}

@media (prefers-reduced-motion:reduce){
  .shop-block.is-turning .car-card[data-active="1"]{animation:none}
  .shop-block[data-move="lift"] .car-card,
  .shop-block[data-move="rise"] .car-card,
  .shop-block[data-move="tilt"] .car-card,
  .shop-block[data-move="sway"] .car-card,
  #portfolio[data-move="lift"] .wr-card,
  .wr-roll[data-move="lift"] .wr-card,
  #portfolio[data-move="rise"] .wr-card,
  .wr-roll[data-move="rise"] .wr-card,
  #portfolio[data-move="tilt"] .wr-card,
  .wr-roll[data-move="tilt"] .wr-card,
  #portfolio[data-move="sway"] .wr-card,
  .wr-roll[data-move="sway"] .wr-card{--ty:0px;--rx:0deg;--rz:0deg}
  .shop-block[data-move="breathe"] .car-card[data-active="1"] .car-shot,
  #portfolio[data-move="breathe"] .wr-card[data-active="1"] .wr-media,
  .wr-roll[data-move="breathe"] .wr-card[data-active="1"] .wr-media{animation:none}
}

/* Шапка: кружок, название с подписью, срок. Места сверху стало больше —
   владелец просил, чтобы название и описание не обрезались: и то и
   другое переносится на вторую строку. Кружок прижат к ВЕРХУ, а не
   центрирован: при двух строках текста центр уводил бы его вниз, и
   шапка переставала быть ровной строкой. */
.wr-head{
  position:relative;z-index:3;flex:none;
  display:flex;align-items:flex-start;gap:12px;padding:15px 16px 14px;
}
/* Шапка соседа гаснет к тому краю, который закрыт ближней карточкой:
   --cover — ДОЛЯ ВИДИМОГО, её считает скрипт по проекции, а не на глаз.

   Затухание МЯГКОЕ, четырьмя ступенями — как в карусели товаров
   (.car-body::after). Раньше здесь стояло жёсткое из двух ступеней
   (`transparent → #0c1220`), и на кромке читалась тёмная полоса: глаз
   видит не «подпись ушла за карточку», а резкую тень от соседа.
   Владелец обвёл это на снимке и попросил как в товарах. Ступени
   считаются от --cover, поэтому у ближнего и дальнего соседа полоса
   затухания разной ширины и читается одинаково. Цвет — тот же #0c1220,
   что и подложка карточки: rgba() здесь его дословная запись. */
.wr-card[data-cover] .wr-head::after{
  content:'';position:absolute;inset:0;pointer-events:none;z-index:2;
}
.wr-card[data-cover="right"] .wr-head::after{
  background:linear-gradient(90deg,
    rgba(12,18,32,0)   calc(var(--cover,0%) * 0.5),
    rgba(12,18,32,.34) calc(var(--cover,0%) * 0.76),
    rgba(12,18,32,.78) calc(var(--cover,0%) * 0.93),
    rgba(12,18,32,.96) var(--cover,0%));
}
.wr-card[data-cover="left"] .wr-head::after{
  background:linear-gradient(270deg,
    rgba(12,18,32,0)   calc(var(--cover,0%) * 0.5),
    rgba(12,18,32,.34) calc(var(--cover,0%) * 0.76),
    rgba(12,18,32,.78) calc(var(--cover,0%) * 0.93),
    rgba(12,18,32,.96) var(--cover,0%));
}

/* ПЕЛЕНА ЗАКРЫТИЯ — ТОЛЬКО НА СТОЯЩЕЙ ЛЕНТЕ.

   Владелец (29.10, запись 0:05): «вспышка всё ещё осталась — белая в конце,
   когда карточка передвинулась». Замер по кадрам (`_probe-cover2.js`):
   `--cover` и `data-cover` ставятся в САМЫЙ МОМЕНТ нажатия и считаются по
   ЦЕЛЕВОЙ раскладке, а карточка в это время ещё стоит на прежнем месте.
   Значит всю дорогу она едет с чужой пеленой: у въезжающей карточки подпись
   затянута кремовым (в бумажной теме — почти белым), хотя закрывать её пока
   нечем, а у выезжающей пелена стоит поверх ещё открытой подписи. В конце
   хода карточка встаёт на место, пелена становится «правильной» — и подпись
   разом возвращается. Глаз читает этот возврат как вспышку.

   Пелена нужна только СТОЯЩЕЙ ленте: она прячет жёсткий срез подписи под
   соседом, а в движении срез и так едет. Поэтому на время хода (`data-moving`
   на дорожке, его ставит markTurning) пелена гаснет, а после хода
   возвращается мягко, за .28 с, — без рывка.

   Гаснет `opacity`, а не заливка: у псевдоэлемента нет ни детей, ни 3D, и
   схлопывать preserve-3d ему нечего (в отличие от самой карточки, где
   прозрачность когда-то гасила и карточку, и её глубину). */
.car-card[data-cover] .car-body::after,
.wr-card[data-cover] .wr-head::after{
  transition:opacity .28s var(--ease);
}
.car-track[data-moving="1"] .car-body::after,
.car-track[data-moving="1"] .wr-head::after{
  opacity:0;
}

.wr-ava{
  flex:none;width:40px;height:40px;border-radius:50%;overflow:hidden;
  background:linear-gradient(140deg,var(--cyan),var(--violet));
  box-shadow:inset 0 0 0 2px rgba(255,255,255,.1);
}
.wr-ava img{width:100%;height:100%;object-fit:cover;display:block}
.wr-titles{min-width:0;flex:1 1 auto;display:flex;flex-direction:column;gap:3px}
.wr-name{
  font-size:1rem;font-weight:700;color:var(--ink);line-height:1.25;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
  overflow:hidden;
}
.wr-cap{
  font-size:.78rem;font-style:normal;color:var(--ink-3);line-height:1.3;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
  overflow:hidden;
}
/* Срок. Подпись владелец выбирает сам, и она бывает длинной («Срок
   изготовления: 30 дней»), поэтому значок идёт с переносом и с пределом по
   ширине.

   МЕСТО — ПОДВАЛ КАРТОЧКИ, У ПРАВОГО КРАЯ. Так владелец попросил 28.09:
   «срок работы переместить вниз справа» — на свободное место правее сердца и
   «поделиться». До этого значок жил в правом нижнем углу КАДРА, на самом
   фото; оттуда он ушёл вместе с точками кадров, которые поднялись на кадр
   (см. .wr-media .wr-pages ниже) — они поменялись местами.

   Отметка `data-term-place="br"` на карточке осталась и по-прежнему ВСЕГДА
   одна: по ней проверки находят значок. Выбора места у работы нет — он был
   убран ещё раньше (из панели, данных и сервера): двух источников правды тут
   быть не должно.

   Почему НЕ в шапке. Там значок жил раньше, и «bottom» означал бы низ ШАПКИ
   (замером вышло 393px от низа вместо 14), а ещё шапка гасит своё содержимое
   у закрытого края (::after внутри .wr-head), и значок гас бы вместе с
   подписью. На кадре он заслонял фотографию и спорил с точками. Подвал —
   единственное место, где он ничего не перекрывает.

   `margin-left:auto` прижимает значок к правому краю подвала — то есть
   напротив «поделиться», как и просил владелец.

   Значок УЖИМАЕТСЯ, а не переносится (29.09). Раньше на узком окне подвал
   переносил его на свою строку, и владелец прислал снимок, где значок
   «съезжает» вниз. Теперь он остаётся в строке кнопок и при нехватке места
   обрезается многоточием. `min-width:0` тут обязателен: без него
   flex-элемент не отдаёт место ниже своей ширины, и обрезка не наступает
   вовсе — значок выдавил бы кнопки за край.

   Подложка ПЛОТНАЯ: подпись бывает и на светлом фоне, а полупрозрачная
   циановая заливка на белом не читается. */
.wr-term{
  margin-left:auto;
  flex:0 1 auto;min-width:0;
  max-width:100%;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  padding:3px 9px;border-radius:999px;
  font-size:.68rem;font-weight:700;line-height:1.25;text-align:right;
  color:var(--cyan);
  border:1px solid rgba(34,230,255,.3);
  background:rgba(12,18,32,.82);
}

/* Кадры. */
.wr-media{
  position:relative;z-index:1;flex:1 1 auto;min-height:0;overflow:hidden;
  background:#080d17;
  perspective:900px;
  /* Палец по горизонтали листает кадры, по вертикали — страницу. Без
     этого браузер на телефоне считает движение своим и кадр «залипает». */
  touch-action:pan-y;
  cursor:grab;
}
.wr-media.is-dragging{cursor:grabbing}
.wr-media.is-dragging .wr-slide{transition:none}

/* Кадр. margin и padding обнулены НАРОЧНО.

   В блоке «Галерея» кадр — это <figure> (его пишет сервер, см. galCardHtml),
   а у figure браузер держит СВОИ поля: 1em сверху и снизу, 40px по бокам.
   `inset:0` их не снимает — он ставит только top/right/bottom/left, поэтому
   кадр оказывался меньше подложки, и по краям снимка проступала её тёмная
   заливка. Владелец показал это снимком: «в самой карусели исправить вот
   тут не должно быть чёрной рамки». Замер (_probe-22-galcard): подложка
   498×616, кадр 418×584 — ровно 40+40 по бокам и 16+16 сверху и снизу.

   У ленты работ кадр — <div>, полей у него нет; оттого полосы там не было,
   и замечание касалось только галереи. Обнуляем в ОДНОМ месте на двоих:
   второй набор правил рядом разошёлся бы с первым на первой же правке. */
.wr-slide{
  position:absolute;inset:0;
  margin:0;padding:0;
  transform:translate3d(calc(var(--k,0) * 100%),0,0)
            scale(calc(1 - min(var(--ak,0),1) * .08))
            rotateY(calc(var(--k,0) * -9deg));
  opacity:calc(1 - min(var(--ak,0),1) * .55);
  transition:transform .52s var(--ease),opacity .42s var(--ease);
  z-index:calc(10 - var(--ak,0));
}
.wr-slide img,.wr-slide video{
  width:100%;height:100%;object-fit:cover;display:block;
  /* опыт: без запрета перетаскивания */user-select:none;
}
/* Значок «играть» лежит поверх видео и гаснет, когда оно пошло. */
.wr-slide > svg{
  position:absolute;left:50%;top:50%;width:46px;height:46px;
  margin:-23px 0 0 -23px;z-index:4;pointer-events:none;
  fill:rgba(255,255,255,.92);stroke:none;
  filter:drop-shadow(0 4px 16px rgba(0,0,0,.6));
  transition:opacity .3s var(--ease),transform .3s var(--ease);
}
.wr-slide.is-playing > svg{opacity:0;transform:scale(1.5)}

/* Счётчик кадров «1 / 6».

   z-index здесь обязан быть ВЫШЕ слоя кадров, а не «просто побольше нуля».
   Кадр задан как `z-index: calc(10 - var(--ak,0))` (см. .wr-slide), то есть
   видимый кадр — это ровно 10. Пока тут стояло 5, снимок рисовался ПОВЕРХ
   счётчика, и владелец видел пустой угол: «вверху справа не видно цифр».
   Числа на экране были — их закрывала фотография.

   Ловушка на будущее: прямоугольник у элемента при этом правильный, и
   сторож, меряющий только `getBoundingClientRect`, остаётся зелёным. Чтобы
   такое не проходило молча, test-works спрашивает браузер, ЧТО ЛЕЖИТ под
   курсором в точке марки (`elementsFromPoint`). */
/* Слой-контейнер плашек: счётчик и точки кадров.

   Владелец (29.09, вечер, ВТОРОЙ раз): «при перелистывании картинок работ
   движутся точки и цифры, исправляй они должны быть наместе как пример у
   карточек товара точки на месте».

   Почему прошлое лечение не помогло — измерено пробой на живой странице.
   Тогда поворот снимали СО ВСЕГО СЛОЯ: `.wr-level` растянут на кадр и
   повёрнут на обратный угол. Плашки при этом действительно стояли прямо
   (net = 0°), НО поворот шёл вокруг ЦЕНТРА КАДРА, а плашки лежат далеко от
   него — счётчик в правом верхнем углу (≈316px от центра), точки внизу.
   Поворот вокруг чужого центра — это ещё и ПЕРЕНОС: пока лента едет и
   наклон карточки меняется с +1,2° на −1,6° (размах 2,8°), плашка едет по
   снимку на `2,8° × 316px ≈ 15px`. Замер: место счётчика НА ФОТОГРАФИИ
   гуляло на 23px по X и 12px по Y.

   У карточек товара этого нет по простой причине: у них `data-move="rise"`,
   то есть карточка НЕ поворачивается вокруг Z вовсе (замер: угол 0°), и
   плашке не с чего съезжать. В работах стоит «покачивание» — и оно съезжает.

   Лечение: поворот снимает КАЖДАЯ ПЛАШКА САМА, вокруг СВОЕГО центра.
   Поворот вокруг собственного центра не двигает центр — значит место плашки
   на снимке остаётся ровно тем, что задал layout, а на экране она по-прежнему
   стоит прямо. Два требования сразу: «не перекашиваются» и «на месте».

   `--rz` объявлена на карточке и наследуется — плашкам её видно. У карточек
   без «движения» переменная пуста, и `calc(-1 * var(--rz,0deg))` даёт ноль:
   правило безопасно и для прежних настроек. */
.wr-media .wr-level{
  position:absolute;inset:0;z-index:12;
  /* Нажатия сквозь слой: точки возвращают их себе сами (см. .wr-pages
     button), а счётчик — украшение и не должен перехватывать свайп. */
  pointer-events:none;
}
/* Общий поворот снят с плашек. Темп — тот же, что у наклона карточки
   (`transform .62s`): своя скорость заставила бы плашку «догонять» кадр. */
.wr-media .wr-level .wr-count,
.wr-media .wr-level .wr-pages{
  transform-origin:50% 50%;
  transform:rotate(calc(-1 * var(--rz,0deg)));
  transition:transform .62s var(--ease);
}

.wr-count{
  position:absolute;top:10px;right:12px;z-index:12;
  padding:3px 9px;border-radius:999px;
  font-size:.72rem;font-weight:700;color:#fff;
  /* Палитра плашки: `--bd-bg` задаёт вид, `--bd-wash` — цвет. Оба пусты по
     умолчанию, и тогда работает прежнее значение — «плашка» остаётся ровно
     прежней, а не «похожей». */
  background:var(--bd-bg,var(--bd-wash,rgba(6,10,18,.62)));
  -webkit-backdrop-filter:blur(var(--bd-blur,6px));backdrop-filter:blur(var(--bd-blur,6px));
  border:var(--bd-line-w,0px) solid var(--bd-line,transparent);
  box-shadow:var(--bd-glow,none);
  /* Без плашки числа остались бы белым по белому снимку. */
  text-shadow:var(--bd-txt,none);
  /* Слой-родитель пропускает нажатия (pointer-events:none), а счётчику их
     возвращаем: так было и до слоя, и на этом стоит проверка «счётчик лежит
     ПОВЕРХ фотографии» — она спрашивает браузер, что лежит в точке марки, а
     элемент, пропускающий нажатия, браузер не назовёт. */
  pointer-events:auto;
}

/* Подвал: сердце со счётчиком, «поделиться», значок срока.
   Точки кадров здесь больше НЕ живут — они ушли на сам кадр, а на их место
   справа встал значок срока (владелец, 28.09). Подвал снова простой: строка
   кнопок слева и значок, прижатый вправо (`margin-left:auto`). */
.wr-foot{
  position:relative;z-index:3;flex:none;
  /* nowrap, а не wrap: владелец (29.09) просил, чтобы значок срока ВСЕГДА
     стоял напротив «поделиться». С переносом он на узком окне уезжал на
     свою строку — на снимке владельца он «съезжал». Теперь ужимается
     значок (см. .wr-term), а не строка. */
  display:flex;align-items:center;flex-wrap:nowrap;
  gap:6px;padding:8px 12px 10px;
}
/* Кнопки не ужимаются никогда: сердце и «поделиться» — цель нажатия, и
   сжатая кнопка промахивается мимо пальца. Место уступает значок. */
.wr-foot > .wr-like,.wr-foot > .wr-share{flex:none}
.wr-like{
  position:relative;display:inline-flex;align-items:center;gap:6px;
  border:0;background:transparent;cursor:pointer;font:inherit;
  font-size:.86rem;font-weight:700;color:var(--ink-2);
  padding:6px 8px;border-radius:12px;
  transition:color .25s var(--ease),background .25s var(--ease);
}
.wr-like svg{
  width:22px;height:22px;fill:none;stroke:currentColor;stroke-width:2;
  transition:fill .25s var(--ease),transform .25s var(--ease);
}
.wr-like:hover{color:var(--magenta);background:rgba(255,79,216,.08)}
.wr-like[aria-pressed="true"]{color:var(--magenta)}
.wr-like[aria-pressed="true"] svg{fill:currentColor}
.wr-like:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}

.wr-share{
  display:inline-flex;align-items:center;justify-content:center;
  width:36px;height:36px;border:0;border-radius:12px;background:transparent;
  color:var(--ink-2);cursor:pointer;
  transition:color .25s var(--ease),background .25s var(--ease);
}
.wr-share svg{width:20px;height:20px}
.wr-share:hover{color:var(--cyan);background:rgba(34,230,255,.1)}
.wr-share:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}

/* ---------- Кнопка «поделиться»: цвет и движение ----------

   Владелец (02.10): «сделать так чтобы я мог отдельно в настройках
   редактировать кнопку под себя добавить ей эффекты и анимацию больше 10
   штук на выбор и цвета чтобы она летала или двигалась при нажатии с
   эффектом».

   Значит наружу из панели уходят только ИДЕНТИФИКАТОРЫ (data-color,
   data-fx), а сами оттенки и движения живут здесь — как у анимации лайка и
   у перехода карусели. Цвет ставится ДВУМЯ переменными, а не одной: значку
   нужен оттенок, подложке — он же с прозрачностью, а `color-mix` в старых
   браузерах не работает, и на нём кнопка осталась бы серой.

   «Как на сайте» — ровно прежний вид: серый значок, голубой на наведении.
   Поэтому у него своих переменных нет, он просто не попадает под правило
   ниже. */
.wr-share[data-color="cyan"]   {--sh-c:#22e6ff;--sh-bg:rgba(34,230,255,.14);--sh-glow:rgba(34,230,255,.6)}
.wr-share[data-color="violet"] {--sh-c:#966eff;--sh-bg:rgba(150,110,255,.16);--sh-glow:rgba(150,110,255,.6)}
.wr-share[data-color="magenta"]{--sh-c:#ff4fd8;--sh-bg:rgba(255,79,216,.14);--sh-glow:rgba(255,79,216,.6)}
.wr-share[data-color="gold"]   {--sh-c:#ffc45c;--sh-bg:rgba(255,196,92,.16);--sh-glow:rgba(255,196,92,.6)}
.wr-share[data-color="emerald"]{--sh-c:#34e29b;--sh-bg:rgba(52,226,155,.14);--sh-glow:rgba(52,226,155,.6)}
.wr-share[data-color="ice"]    {--sh-c:#b4f0ff;--sh-bg:rgba(180,240,255,.16);--sh-glow:rgba(180,240,255,.6)}
.wr-share[data-color="sunset"] {--sh-c:#ff8a5c;--sh-bg:rgba(255,138,92,.16);--sh-glow:rgba(255,138,92,.6)}
.wr-share[data-color]:not([data-color="auto"]){color:var(--sh-c)}
.wr-share[data-color]:not([data-color="auto"]):hover{
  color:#fff;background:var(--sh-bg);
  box-shadow:inset 0 0 0 1px var(--sh-glow),0 0 18px var(--sh-glow);
}

/* Движение при нажатии. Класс is-send вешает скрипт и снимает его сам
   (js/main.js): у «без анимации» события окончания не будет вовсе, и по
   animationend класс остался бы висеть. */
.wr-share.is-send[data-fx="fly"]    {animation:shFly .7s var(--ease)}
.wr-share.is-send[data-fx="plane"]  {animation:shPlane .8s var(--ease)}
.wr-share.is-send[data-fx="pulse"]  {animation:shPulse .7s var(--ease)}
.wr-share.is-send[data-fx="shake"]  {animation:shShake .5s var(--ease)}
.wr-share.is-send[data-fx="jump"]   {animation:shJump .6s var(--ease)}
.wr-share.is-send[data-fx="spin"]   {animation:shSpin .7s var(--ease)}
.wr-share.is-send[data-fx="pop"]    {animation:shPop .6s var(--ease)}
.wr-share.is-send[data-fx="ripple"] {animation:shRipple .8s var(--ease)}
.wr-share.is-send[data-fx="orbit"]  {animation:shOrbit .9s var(--ease)}
.wr-share.is-send[data-fx="flip"]   {animation:shFlip .8s var(--ease)}
.wr-share.is-send[data-fx="slide"]  {animation:shSlide .7s var(--ease)}
.wr-share.is-send[data-fx="zoom"]   {animation:shZoom .7s var(--ease)}
.wr-share.is-send[data-fx="flash"]  {animation:shFlash .6s var(--ease)}
.wr-share.is-send[data-fx="rocket"] {animation:shRocket .8s var(--ease)}
.wr-share.is-send[data-fx="wave"]   {animation:shWave .8s var(--ease)}

@keyframes shFly{
  0%{transform:none}
  40%{transform:translate(14px,-18px) rotate(14deg) scale(1.1)}
  100%{transform:translate(30px,-40px) rotate(30deg) scale(.5);opacity:0}
}
@keyframes shPlane{
  0%{transform:none;opacity:1}
  30%{transform:translate(-6px,4px) rotate(-12deg)}
  100%{transform:translate(52px,-30px) rotate(24deg) scale(.55);opacity:0}
}
@keyframes shPulse{
  0%,100%{transform:none}
  25%{transform:scale(1.28)}
  50%{transform:scale(1)}
  75%{transform:scale(1.16)}
}
@keyframes shShake{
  0%,100%{transform:none}
  15%{transform:translateX(-4px) rotate(-4deg)}
  35%{transform:translateX(4px) rotate(4deg)}
  55%{transform:translateX(-3px) rotate(-3deg)}
  75%{transform:translateX(2px) rotate(2deg)}
}
@keyframes shJump{
  0%,100%{transform:none}
  20%{transform:translateY(3px) scaleY(.86)}
  55%{transform:translateY(-14px) scaleY(1.08)}
  80%{transform:translateY(0) scaleY(.95)}
}
@keyframes shSpin{
  0%{transform:rotate(0) scale(1)}
  60%{transform:rotate(300deg) scale(1.15)}
  100%{transform:rotate(360deg) scale(1)}
}
@keyframes shPop{
  0%{transform:scale(1)}
  35%{transform:scale(.72)}
  70%{transform:scale(1.38)}
  100%{transform:scale(1)}
}
@keyframes shRipple{
  0%{box-shadow:0 0 0 0 var(--sh-glow,var(--cyan))}
  100%{box-shadow:0 0 0 18px rgba(0,0,0,0)}
}
@keyframes shOrbit{
  0%{transform:none}
  25%{transform:translate(16px,-16px) rotate(12deg)}
  50%{transform:translate(0,-24px) rotate(0)}
  75%{transform:translate(-16px,-16px) rotate(-12deg)}
  100%{transform:none}
}
@keyframes shFlip{
  0%{transform:perspective(400px) rotateY(0)}
  100%{transform:perspective(400px) rotateY(360deg)}
}
@keyframes shSlide{
  0%{transform:none}
  35%{transform:translateX(24px) rotate(10deg);opacity:.35}
  60%{transform:translateX(-10px)}
  100%{transform:none}
}
@keyframes shZoom{
  0%{transform:scale(1);opacity:1}
  45%{transform:scale(1.6);opacity:.7}
  100%{transform:scale(1);opacity:1}
}
@keyframes shFlash{
  0%,100%{filter:none}
  30%{filter:brightness(2.6) drop-shadow(0 0 10px var(--sh-glow,var(--cyan)))}
  60%{filter:brightness(1.4)}
}
@keyframes shRocket{
  0%{transform:none;opacity:1}
  25%{transform:translateY(6px) scale(.9)}
  100%{transform:translateY(-44px) scale(.35) rotate(-16deg);opacity:0}
}
@keyframes shWave{
  0%,100%{transform:none}
  20%{transform:rotate(-14deg)}
  45%{transform:rotate(12deg)}
  70%{transform:rotate(-7deg)}
  88%{transform:rotate(4deg)}
}
/* «Уменьшить движение» — не украшение: тем, кому движение мешает, кнопка
   обязана отвечать мгновенно и без прыжков. */
@media (prefers-reduced-motion:reduce){
  .wr-share.is-send{animation:none}
}

.wr-hint{
  font-size:.72rem;color:var(--ink-3);min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  transition:color .25s var(--ease);
}
.wr-hint.is-ok{color:var(--lime)}
.wr-hint.is-fail{color:var(--magenta)}

/* Вид подсказки «поделиться» — настройка владельца: цвет и размер текста.

   Владелец (04.10): «интервал появления текста я сам в админ панели должен
   менять и цвет и размер текста тоже чтобы мог менять».

   Правила стоят НИЖЕ правил исхода (is-ok / is-fail) нарочно: сила
   селекторов у них одинаковая, и выигрывает нижнее. Поставь их выше — и
   выбранный владельцем цвет не работал бы никогда.

   `:not(.is-fail)` — не украшение. Неудачное копирование обязано остаться
   своего цвета: раскрашенное в цвет владельца, оно выглядело бы как удача.
   Добавочная сила `:not()` заодно перебивает `.is-ok`.

   Оттенки — ТЕ ЖЕ, что у самой кнопки (.wr-share[data-color]): владелец
   выбирает цвет один раз и ждёт, что текст и кнопка будут одного цвета.
   «Как на сайте» своего правила не имеет — остаются прежние цвета исхода. */
.wr-hint[data-color="cyan"]:not(.is-fail)   {color:#22e6ff}
.wr-hint[data-color="violet"]:not(.is-fail) {color:#966eff}
.wr-hint[data-color="magenta"]:not(.is-fail){color:#ff4fd8}
.wr-hint[data-color="gold"]:not(.is-fail)   {color:#ffc45c}
.wr-hint[data-color="emerald"]:not(.is-fail){color:#34e29b}
.wr-hint[data-color="ice"]:not(.is-fail)    {color:#b4f0ff}
.wr-hint[data-color="sunset"]:not(.is-fail) {color:#ff8a5c}

/* Размер — ступенями, а не пикселями из панели. «Обычный» и есть прежний
   вид подсказки (.72rem), поэтому правило есть только у крайних ступеней:
   иначе пришлось бы повторять размер, который уже стоит выше. */
.wr-hint[data-size="sm"]{font-size:.64rem}
.wr-hint[data-size="lg"]{font-size:.88rem}

/* Дорожка кадров — НА САМОМ КАДРЕ, внизу по центру.

   Так владелец попросил 28.09: «точки слайдера перенести выше, в карточку,
   ниже по центру». Раньше они стояли в подвале, на одной высоте с сердцем и
   «поделиться»; оттуда ушли вместе со значком срока, который встал на их
   место справа — они поменялись местами.

   Дорожка ВНЕ ПОТОКА и лежит поверх кадра: подвал своей высоты не меняет, а
   у карточки работы высота ФИКСИРОВАННАЯ — значит всё, что дорожка забрала
   бы себе, отъедалось бы у фотографии.

   `pointer-events:none` на плашке — чтобы она не перехватывала нажатия по
   снимку под собой (а по кадру работает свайп). Сами точки возвращают себе
   нажатия явно — см. `.wr-pages button` ниже.

   Подложка-плашка нужна из-за светлых снимков: на белом фото полупрозрачные
   точки не читаются вовсе, а плашка отделяет их от любой фотографии. */
.wr-media .wr-pages{
  position:absolute;left:0;right:0;bottom:12px;
  margin:0 auto;width:fit-content;
  /* Выше слоя кадров по той же причине, что и у .wr-count: видимый кадр —
     это z-index 10, и при прежнем 6 точки кадров рисовались ПОД снимком.
     Владелец видел ровный кадр без точек внизу. */
  z-index:13;
  display:flex;align-items:center;
  padding:5px 9px;border-radius:999px;
  background:var(--bd-bg,var(--bd-wash,rgba(6,10,18,.52)));
  -webkit-backdrop-filter:blur(var(--bd-blur,6px));backdrop-filter:blur(var(--bd-blur,6px));
  border:var(--bd-line-w,0px) solid var(--bd-line,transparent);
  box-shadow:var(--bd-glow,none);
  pointer-events:none;
}
/* Точки кадров: кнопки, а не украшения. Кружок нарисован через ::before,
   поэтому сама кнопка крупнее точки — в шесть пикселей пальцем не попасть.
   Зазор между кружками получается из полей кнопки, поэтому у дорожки
   своего gap нет.
   `pointer-events:auto` на кнопке возвращает нажатия, которые снял
   растянутый по кадру слой дорожки. */
.wr-pages{display:flex}
.wr-pages button{
  width:12px;height:12px;padding:3px;
  border:0;background:none;cursor:pointer;
  display:flex;align-items:center;justify-content:center;
  -webkit-tap-highlight-color:transparent;
  /* Возвращаем нажатия, которые сняла плашка дорожки. */
  pointer-events:auto;
}
.wr-pages button::before{
  content:'';width:6px;height:6px;border-radius:50%;
  background:var(--bd-dot,rgba(160,190,230,.32));
  box-shadow:var(--bd-dot-shadow,none);
  transition:background .3s var(--ease),transform .3s var(--ease);
}
.wr-pages button[aria-current="true"]::before{
  background:var(--bd-dot-on,var(--cyan));transform:scale(1.35);
}
.wr-pages button:focus-visible{outline:2px solid var(--cyan);outline-offset:2px;border-radius:50%}

/* ---------- плашка под точками и числами: вид и цвет ----------

   Владелец (29.09): «в админ панель добавь возможность делать их в рамке
   или без рамки, т.е. задней обводки на выбор, и добавь ещё несколько
   вариантов и цветов с эффектами».

   Вид и цвет — ДВЕ настройки, а не список готовых пар: «неон» и «золото»
   сочетаются, а сорок пар владелец не переберёт. Вид задаёт форму (заливка,
   обводка, размытие, свечение), цвет — оттенок обводки, свечения и
   активной точки плюс лёгкая подкраска подложки (`--bd-wash`).

   Наружу с сервера уходят только идентификаторы: список закрыт там, а
   оттенки живут здесь — сменить цвет значит поправить оформление, а не
   данные владельца.

   «plate» намеренно НЕ задаёт ни одной переменной: это прежний вид, и он
   должен остаться прежним до пикселя, а не «примерно таким же». */
#portfolio[data-badge="none"], .wr-roll[data-badge="none"]{
  --bd-bg:transparent;--bd-blur:0px;
  --bd-dot:rgba(232,240,255,.78);
  --bd-txt:0 1px 4px rgba(0,0,0,.85);
  --bd-dot-shadow:0 1px 3px rgba(0,0,0,.8);
}
#portfolio[data-badge="outline"], .wr-roll[data-badge="outline"]{
  --bd-bg:transparent;--bd-blur:0px;
  --bd-line-w:1px;--bd-line:var(--bd-tint,rgba(160,190,230,.55));
  --bd-dot:rgba(232,240,255,.78);
  --bd-txt:0 1px 4px rgba(0,0,0,.85);
  --bd-dot-shadow:0 1px 3px rgba(0,0,0,.8);
}
#portfolio[data-badge="soft"], .wr-roll[data-badge="soft"]{
  --bd-bg:rgba(6,10,18,.34);--bd-blur:2px;
}
#portfolio[data-badge="glass"], .wr-roll[data-badge="glass"]{
  --bd-bg:rgba(10,16,28,.40);--bd-blur:12px;
  --bd-line-w:1px;--bd-line:rgba(255,255,255,.20);
}
/* Свечение — то, что владелец назвал «с эффектами»: подложка светится
   своим оттенком, а обводка остаётся тонкой. */
#portfolio[data-badge="glow"], .wr-roll[data-badge="glow"]{
  --bd-bg:rgba(6,10,18,.58);
  --bd-glow:0 0 20px -3px var(--bd-tint,rgba(34,230,255,.55));
}
#portfolio[data-badge="neon"], .wr-roll[data-badge="neon"]{
  --bd-bg:rgba(6,10,18,.62);
  --bd-line-w:1px;--bd-line:var(--bd-tint,rgba(34,230,255,.65));
  --bd-glow:0 0 16px -2px var(--bd-tint,rgba(34,230,255,.55)),
            inset 0 0 10px -3px var(--bd-tint,rgba(34,230,255,.55));
  --bd-dot-on:var(--bd-tint,var(--cyan));
}
/* Двойная рамка: обводка плюс кольцо отступа вокруг неё. Второе кольцо
   рисует тень, а не второй border — у элемента он один. */
#portfolio[data-badge="frame"], .wr-roll[data-badge="frame"]{
  --bd-bg:rgba(6,10,18,.60);
  --bd-line-w:1px;--bd-line:var(--bd-tint,rgba(180,205,240,.60));
  --bd-glow:0 0 0 3px rgba(6,10,18,.55);
}

/* Цвет. `--bd-tint` — обводка, свечение и активная точка; `--bd-wash` —
   лёгкая подкраска подложки, чтобы цвет было видно и на простой «плашке».
   `auto` не задаёт ничего: тёмная плашка без оттенка читается на любом
   снимке, поэтому цветной по умолчанию её делать нельзя. */
#portfolio[data-badge-color="cyan"]   , .wr-roll[data-badge-color="cyan"]   {--bd-tint:rgba(34,230,255,.62); --bd-wash:rgba(8,42,56,.62)}
#portfolio[data-badge-color="magenta"], .wr-roll[data-badge-color="magenta"]{--bd-tint:rgba(255,79,216,.62); --bd-wash:rgba(52,14,44,.62)}
#portfolio[data-badge-color="violet"] , .wr-roll[data-badge-color="violet"] {--bd-tint:rgba(150,110,255,.62);--bd-wash:rgba(30,20,60,.62)}
#portfolio[data-badge-color="gold"]   , .wr-roll[data-badge-color="gold"]   {--bd-tint:rgba(255,196,92,.66); --bd-wash:rgba(52,38,10,.62)}
#portfolio[data-badge-color="emerald"], .wr-roll[data-badge-color="emerald"]{--bd-tint:rgba(52,226,155,.62); --bd-wash:rgba(8,44,32,.62)}
#portfolio[data-badge-color="ice"]    , .wr-roll[data-badge-color="ice"]    {--bd-tint:rgba(180,240,255,.66);--bd-wash:rgba(14,38,54,.62)}

/* ------------------------------------------------------------
   ОТКЛИК НА ЛАЙК
   ------------------------------------------------------------
   Вид анимации приходит с сервера закрытым списком и ставится атрибутом
   на секцию — как у кружка корзины и бегущей строки. Здесь нарисованы
   все виды; какой включён, решает data-like-anim.

   Смысл происходящего: сердце сжимается и подпрыгивает, под ним
   вспыхивает круг, от сердца разлетаются искры, счётчик дёргается вверх,
   а сама карточка на миг светлеет. Ощущение «щелчка», а не смены значка.
   ------------------------------------------------------------ */

/* 1. Сердце: сжалось — выстрелило — вернулось. */
.is-liked .wr-like svg{animation:wrHeart .64s var(--ease)}
@keyframes wrHeart{
  0%{transform:scale(1)}
  16%{transform:scale(.7)}
  40%{transform:scale(1.44)}
  62%{transform:scale(.92)}
  80%{transform:scale(1.14)}
  100%{transform:scale(1)}
}

/* 2. Вспышка-кольцо под сердцем. */
.wr-like::after{
  content:'';position:absolute;left:19px;top:50%;width:26px;height:26px;
  margin:-13px 0 0 -13px;border-radius:50%;pointer-events:none;opacity:0;
  background:radial-gradient(circle,rgba(255,79,216,.55),rgba(255,79,216,0) 70%);
}
.is-liked .wr-like::after{animation:wrFlash .64s var(--ease)}
@keyframes wrFlash{
  0%{opacity:.95;transform:scale(.35)}
  100%{opacity:0;transform:scale(2.7)}
}

/* 3. Искры. Десять штук, угол --a и длина --d задаёт скрипт: сколько и
   как далеко — вопрос вида, а не кода. */
.wr-fx{position:absolute;left:19px;top:50%;width:0;height:0;pointer-events:none;z-index:5}
.wr-fx i{
  position:absolute;left:0;top:0;width:5px;height:5px;
  margin:-2.5px 0 0 -2.5px;border-radius:50%;opacity:0;
  background:var(--magenta);
  box-shadow:0 0 8px rgba(255,79,216,.9);
  animation:wrSpark var(--t,.6s) var(--ease) forwards;
}
@keyframes wrSpark{
  0%{opacity:1;transform:rotate(var(--a)) translateX(4px) scale(1)}
  70%{opacity:1}
  100%{opacity:0;transform:rotate(var(--a)) translateX(var(--d,36px)) scale(.3)}
}

/* 4. Счётчик подпрыгивает. */
.is-liked .wr-likes{animation:wrCount .64s var(--ease);display:inline-block}
@keyframes wrCount{
  0%{transform:translateY(0) scale(1)}
  34%{transform:translateY(-5px) scale(1.2)}
  100%{transform:translateY(0) scale(1)}
}

/* 5. Карточка на миг светлеет — «щелчок» чувствуется всей карточкой. */
.is-liked{animation:wrKick .56s var(--ease)}
@keyframes wrKick{
  0%{filter:none}
  38%{filter:brightness(1.14)}
  100%{filter:none}
}

/* Виды анимации. «Вспышка и искры» — заводской: он и стоит в разметке,
   если сервер ничего не прислал. */
[data-like-anim="none"] .is-liked .wr-like svg,
[data-like-anim="none"] .is-liked .wr-like::after,
[data-like-anim="none"] .is-liked .wr-likes,
[data-like-anim="none"] .is-liked{animation:none}
[data-like-anim="none"] .wr-fx{display:none}

[data-like-anim="sparks"] .is-liked .wr-like::after{animation:none}
[data-like-anim="sparks"] .is-liked .wr-like svg{animation:none}
[data-like-anim="sparks"] .is-liked{animation:none}

[data-like-anim="pulse"] .wr-fx{display:none}
[data-like-anim="pulse"] .is-liked .wr-like::after{animation:none}
[data-like-anim="pulse"] .is-liked{animation:none}

/* Конфетти: те же искры, но разноцветные, прямоугольные и падают вниз. */
[data-like-anim="confetti"] .wr-fx i{
  width:7px;height:4px;border-radius:1px;
  background:hsl(var(--a),88%,62%);
  box-shadow:none;
  animation-name:wrConfetti;
}
@keyframes wrConfetti{
  0%{opacity:1;transform:rotate(var(--a)) translateX(6px) rotate(0deg)}
  100%{opacity:0;
      transform:rotate(var(--a)) translateX(var(--d,40px)) translateY(30px) rotate(340deg)}
}

/* Кому движение мешает — тому его не показываем. Сердце всё равно
   закрасится: смысл лайка не в искрах. */
@media (prefers-reduced-motion: reduce){
  .is-liked,
  .is-liked .wr-like svg,
  .is-liked .wr-like::after,
  .is-liked .wr-likes{animation:none}
  .wr-fx{display:none}
  .wr-slide{transition:opacity .2s linear}
  #portfolio .wr-track,   .wr-roll .wr-track{transition:none}
}

/* На узком экране карточка меньше: так в кадр влезает и сосед, и видно,
   что лента продолжается. */
@media (max-width:560px){
  /* На телефоне размер задаёт ЭКРАН, а не владелец: ступень «showcase»
     шириной 560px в кадр не влезет. Но ширину и высоту берём ПЕРЕМЕННЫМИ,
     а не числами, как было раньше: числа здесь пришлось бы править заново
     при каждой правке ступеней, а забытое число означало бы карточку,
     вылезшую из дорожки. Дорожка и карточка уже считаются от переменных,
     поэтому отдельных правил им не нужно.

     `[data-size]` нужен для веса: без него это правило проиграло бы
     ступеням из начала файла — у них тот же идентификатор плюс атрибут.
     Атрибут ставит main.js всегда, в том числе для «обычного» размера. */
  #portfolio[data-size],   .wr-roll[data-size]{--wr-w:270px;--wr-h:392px}
  .wr-head{padding:10px 12px 9px;gap:9px}
  .wr-ava{width:34px;height:34px}
  .wr-name{font-size:.86rem}
  .wr-term{font-size:.68rem;padding:3px 7px}
  .wr-foot{padding:7px 10px 9px}
}

/* ------------------------------------------------------------
   МИНИ-СТРАНИЦА РАБОТЫ — /work/<id>
   ------------------------------------------------------------
   Страница собирается СЕРВЕРОМ из блоков, которые владелец задал в
   панели. Здесь только оформление: страница не зависит от js/main.js и
   живёт без него — так её видят и роботы мессенджеров, которые
   разворачивают ссылку в превью при пересылке.

   Отклик на лайк — общий с лентой. Он привязан к классу is-liked на
   «хозяине» (в ленте это .wr-card, здесь .wp-acts) и к атрибуту
   data-like-anim, поэтому вид анимации, выбранный в панели, работает в
   обоих местах одинаково.
   ------------------------------------------------------------ */
/* ============================================================
   РАБОТЫ: всплывающее окно работы
   ------------------------------------------------------------
   Открывается нажатием на шапку карточки. Содержимое — то же, что на
   странице /work/<id>: разметку окно берёт оттуда, поэтому классы wp-*
   оформлены ниже и повторять их здесь не нужно. Здесь только рамка, фон
   и прокрутка — то, чего у страницы публикации нет.
   ============================================================ */
.wr-modal{
  position:fixed;inset:0;z-index:120;
  display:flex;align-items:center;justify-content:center;padding:24px;
  opacity:0;transition:opacity .24s var(--ease);
}
.wr-modal.is-open{opacity:1}
.wr-modal-back{
  position:absolute;inset:0;background:rgba(4,6,12,.78);
  backdrop-filter:blur(6px);
}
.wr-modal-box{
  position:relative;z-index:1;
  width:min(920px,100%);max-height:min(86vh,1000px);
  display:flex;flex-direction:column;overflow:hidden;
  background:#0c1220;border:1px solid rgba(34,230,255,.28);
  border-radius:var(--r-lg);
  box-shadow:0 40px 120px rgba(0,0,0,.7),0 0 60px rgba(34,230,255,.12);
  transform:translateY(14px) scale(.98);
  transition:transform .26s var(--ease);
}
.wr-modal.is-open .wr-modal-box{transform:none}
.wr-modal-x{
  position:absolute;top:12px;right:12px;z-index:3;
  width:38px;height:38px;display:grid;place-items:center;
  border:1px solid rgba(255,255,255,.14);border-radius:50%;
  background:rgba(8,13,23,.8);color:var(--ink-2);cursor:pointer;
}
.wr-modal-x svg{width:18px;height:18px}
.wr-modal-x:hover{color:var(--ink);border-color:rgba(34,230,255,.5)}
.wr-modal-x:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
/* Прокрутка внутри окна, а не у страницы: иначе на телефоне под окном
   уезжает лента, и палец листает не то, что человек видит. */
.wr-modal-content{
  overflow-y:auto;overscroll-behavior:contain;
  padding:26px 26px 30px;
}
/* Обёртка страницы (.wp-wrap) в окно НЕ попадает: js/main.js берёт её
   innerHTML и вставляет только содержимое (openPopup: «src =
   doc.querySelector('.wp-wrap') … markup = src.innerHTML»). Селектор
   оставлен ЗАПАСНЫМ: он сработает, если вставку когда-нибудь сменят на
   «вставить обёртку целиком». Владельцу (02.10) полосу дал не он, а .wrap
   блоков — правило ниже. */
.wr-modal-content .wp-wrap{width:auto;max-width:none;margin:0;padding:0}
/* А это — полоса БЛОКОВ внутри работы (шапка, FAQ, «почему мы» и прочее):
   каждый блок приносит с собой .wrap ГЛАВНОЙ, шириной min(--maxw,92vw).

   vw считает ОКНО БРАУЗЕРА, а не родителя, — и внутри окна шириной 918 при
   окне браузера 1440 полоса оставалась 1200 и уезжала за правый край на
   308. Отсюда ГОРИЗОНТАЛЬНАЯ полоса прокрутки, которую владелец обвёл
   02.10: «почему то окно больше и это неправильно».

   Замер (probe-32): содержимое 1226 при поле 918 на 1440, 1204/918 на 1280,
   1024/918 на 1085, 968/918 на 1024 — и ровно 0 после этой правки.
   Из двух селекторов на пути полосы стоит ТОЛЬКО этот: .wp-wrap в окно не
   приезжает (см. выше). Оба правила идут парой — снимать их надо парой. */
.wr-modal-content .wrap{width:auto;max-width:none}
/* Кнопка закрытия стоит в правом верхнем углу окна, а срок работы — в
   правом верхнем углу самой работы: без отступа они накладываются, и
   срок уезжает под крестик.

   Отступ даём ТОЛЬКО верхней строке шапки (.wp-hero-top), а не всей
   шапке. Раньше он стоял на .wp-hero — и вместе с шапкой сдвигал влево
   карусель: сцена шириной 814 стояла в поле 866 не по центру (26 слева,
   78 справа), и левая стрелка листания уезжала за край окна. Замерено:
   сцена 287…1101 при середине окна 720. */
.wr-modal-content .wp-hero-top{padding-right:52px}

/* Стрелки листания в окне работы — в полях, но ВНУТРИ окна.

   Владелец: «в сплывающем окне работ неполностью помещаються кнопки
   перелистывания, сделай стобы все помещалось». Стрелки выходят за
   карусель на --nav-out (размер кнопки плюс 14) в каждую сторону, а окно
   шириной 920 и с обрезкой по краю: левая кнопка уезжала за край на 31
   точку, и видно было половину круга. Замерено (probe-02five): сцена
   287…1101, слой стрелок 229…1159, окно 260…1180.

   Поэтому в окне под стрелки ЗАРАНЕЕ оставляем место: обёртка карусели
   ужимается на --nav-out с каждой стороны, и кнопки встают ровно по краям
   поля, не выходя за окно. Выбор владельца («сбоку от карусели») при этом
   сохраняется — стрелки по-прежнему ВНЕ кадра, как он и просил, просто
   кадр в окне чуть уже.

   Только для широкого окна: на узком (до 900 точек) стрелки и без того
   уходят внутрь кадра — там правило ниже по листу, и запас места ему
   только мешал бы. */
@media (min-width:901px){
  .wr-modal-content .wp-car-hold[data-arrow-pos="out"]{
    margin-left:var(--nav-out);margin-right:var(--nav-out)}
}
.wr-modal-note{margin:14px 0 0;font-size:.9rem;color:var(--ink-3)}
/* Прокрутка страницы под окном замирает. */
body.wr-modal-open{overflow:hidden}
@media (max-width:640px){
  .wr-modal{padding:0}
  .wr-modal-box{width:100%;height:100%;max-height:100%;border-radius:0;border:0}
  .wr-modal-content{padding:20px 16px 26px}
}
@media (prefers-reduced-motion:reduce){
  .wr-modal,.wr-modal-box{transition:none}
}

.wp-page{min-height:100vh;display:flex;flex-direction:column}
.wp-nav{
  position:sticky;top:0;z-index:20;
  background:rgba(4,6,12,.74);
  -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
  border-bottom:1px solid rgba(120,160,220,.12);
}
.wp-nav-back{
  font-size:.86rem;font-weight:700;color:var(--ink-2);text-decoration:none;
  padding:9px 15px;border-radius:999px;white-space:nowrap;
  border:1px solid rgba(120,160,220,.22);
  transition:color .25s var(--ease),border-color .25s var(--ease),background .25s var(--ease);
}
.wp-nav-back:hover{color:var(--cyan);border-color:rgba(34,230,255,.45);background:rgba(34,230,255,.07)}

/* Полоса страницы работы и страницы сайта — ТОЙ ЖЕ ширины, что полоса
   главной (.wrap: min(var(--maxw), 92vw)).

   Владелец: «Страницы публикации надо сделать шире как основной сайт,
   сделать единый размер у всех блоков… надо сделать его как все блоки
   шире, чтобы они были похожи на основной сайт» и «в таком же стиле и
   шире надо сделать страницы сайта, чтобы были все по одной ширине, не
   узкие».

   Было 780 точек — уже шапки (её .nav-in объявлен ровно как .wrap) и уже
   широких блоков, которые вылезали на весь экран. Замерено на его сайте
   при окне 1440: шапка 1200, полоса 780, блоки в ней 780, а «широкие» —
   1440. Три разные ширины на одной странице и читались как разнобой.

   Число берём ТО ЖЕ, а не своё: разойдись полоса с шапкой хоть на
   пиксель — заголовок страницы перестал бы вставать под логотипом, и
   владелец увидел бы ступеньку там, где просил ровный край.

   Доли внутри (свободное размещение текста) считаются от ЭТОЙ полосы,
   поэтому её ширина — не мелочь: она меняет смысл сохранённых чисел. У
   владельца свободно поставленных блоков нет ни на одной публикации —
   проверено по data/publications.json перед правкой. */
.wp-wrap{
  position:relative;z-index:2;flex:1 1 auto;
  width:min(var(--maxw),92vw);margin:0 auto;padding:36px 0 64px;
}

/* Шапка: то же, что на карточке ленты, — кружок, название, срок. Так
   переход по ссылке не выглядит прыжком на другой сайт. */
.wp-hero{display:flex;flex-direction:column;gap:16px}
.wp-hero-top{display:flex;align-items:center;gap:12px}
.wp-ava{
  flex:none;width:52px;height:52px;border-radius:50%;overflow:hidden;
  background:linear-gradient(140deg,var(--cyan),var(--violet));
  box-shadow:inset 0 0 0 2px rgba(255,255,255,.12);
}
.wp-ava img{width:100%;height:100%;object-fit:cover;display:block}
.wp-hero-who{min-width:0;display:flex;flex-direction:column;gap:3px}
.wp-hero-who b{font-size:1.3rem;color:var(--ink);line-height:1.2}
.wp-hero-who span{font-size:.86rem;color:var(--ink-3)}
/* Срок — НАПРОТИВ НАЗВАНИЯ, а не по середине строки шапки.

   Владелец (02.10): «рамка с сроками съехала вниз, а надо чтобы она была
   всегда напротив названия справа вверху». Замер: строка шапки 52 точки
   (название 25 + подпись 23), срок 35 — при `align-items:center` он встаёт
   по середине СТРОКИ, то есть на 8 точек НИЖЕ названия, и уезжает тем
   ниже, чем длиннее подпись. `flex-start` прижимает его к верхнему краю
   строки — ровно к верхнему краю названия, и так при любой длине подписи.

   `margin-left:auto` оставлен: он и держит срок у правого края. */
.wp-term{
  flex:none;align-self:flex-start;margin-left:auto;white-space:nowrap;
  padding:6px 12px;border-radius:999px;font-size:.78rem;font-weight:700;
  color:var(--cyan);border:1px solid rgba(34,230,255,.3);background:rgba(34,230,255,.08);
}
/* Карусель фотографий на публикации. Раньше здесь стояло ОДНО фото; владелец
   попросил карусель из всех кадров работы — «красивую и с эффектами».

   Слайды лежат СТОПКОЙ (position:absolute), а не столбиком: без скрипта
   виден первый кадр, а не десять друг под другом. Смещение слайда от
   показанного задаёт --k, а --ak — то же по модулю (считает js/work.js): так
   формулы читаются без abs(), который поддержан не везде.

   Скорость приходит МНОЖИТЕЛЕМ (--car-speed ставит скрипт), а не секундами:
   у каждого перехода свой природный темп. Делим, а не умножаем — «быстрее»
   обязано ускорять.

   Эффект выбирает владелец в панели (список живёт на сервере, PUB_CAROUSEL_FX),
   а движения — здесь: наружу уходит только идентификатор. */
/* Карусель фотографий на публикации: размер, вид карточки и расстановка.

   ТРИ СЛОЯ — НЕ УКРАШЕНИЕ, А НЕОБХОДИМОСТЬ. У элемента ровно один
   transform, поэтому эффект, расстановка и рамка на одном элементе затирали
   бы друг друга. Отсюда разделение:

     .wp-car       — СЦЕНА: размер, обрезка, место для полосы с точками;
     .wp-car-slide — СЛАЙД: эффект (data-fx) либо расстановка (data-pos);
     .wp-car-card  — КАРТОЧКА: рамка и толщина (data-look).

   Слайды лежат СТОПКОЙ (position:absolute), а не столбиком: без скрипта
   виден первый кадр, а не десять друг под другом. Смещение слайда от
   показанного задаёт --k, а --ak — то же по модулю (считает js/work.js): так
   формулы читаются без abs(), который поддержан не везде.

   Скорость приходит МНОЖИТЕЛЕМ (--car-speed ставит скрипт), а не секундами:
   у каждого перехода свой природный темп. Делим, а не умножаем — «быстрее»
   обязано ускорять.

   Наружу приходят только идентификаторы (эффект, размер, вид, расстановка) из
   закрытых списков на сервере; что каждый значит — знает это оформление. */
.wp-car{
  position:relative;
  /* Сцена — ЭКРАН, и рамка у неё своя, прежняя. Так «без рамки + во всю
     ширину + по одной» — это ровно тот вид публикации, который владелец уже
     знает, а размер и вид карточки меняют то, что лежит ВНУТРИ экрана.
     Обрезка обязательна: без неё уехавший кадр пролетел бы по всей странице.
     Заодно она срезает соседние кадры колоды и ленты по краю экрана — они и
     должны выглядывать из-за края, а не вылезать на саму страницу. */
  overflow:hidden;
  /* Поля сцены — не украшение, а МЕСТО ПОД НАКЛОН.

     Сцена обрезает содержимое (`overflow:hidden`, иначе уехавший кадр
     пролетел бы по всей странице), а наклонённая карточка выходит за
     дорожку углами. Значит поле сцены обязано быть не меньше этого выноса,
     иначе карточку срезает по краю — ровно то, на что владелец и показал.

     ВЛАДЕЛЕЦ (02.10): «сами карточки не помещаются в окно, исправь чтобы
     полностью было видно карточку» — и обвёл красным левый край и верхний
     левый угол сцены. Замер на его сайте при окне 1440 (ширина полосы
     1200): коробка карточки 1167×950 при дорожке 1138×854, нижний левый
     угол вылезает ВЛЕВО на 48 и верхний левый ВВЕРХ на 12 — сцена режет.

     ПОЧЕМУ БЫЛО, А СТАЛО. Запас стоял числом 30px и был подобран замером
     при дорожке 758px. 02.10 полосу страницы расширили с 780 до 1200
     (см. .wp-wrap), карточка выросла в полтора раза — а запас остался тем
     же числом. Вынос растёт ВМЕСТЕ с карточкой, поэтому и запас обязан
     расти вместе с ней: он задан ДОЛЕЙ ширины (`--tilt-pad-w` на обёртке), а
     не точками. Доля честна и на 1440, и на телефоне — точками она была бы
     верна только на одном окне.

     Величина — из того же замера, с запасом: вынос влево 78/1138 = 6.9 %
     ширины дорожки. При поле 6.5 % дорожка 1042, вынос 71 — влезает.

     Поля считаются в ОДНОМ месте (`--scene-pad` и `--scene-pad-b` в
     .wp-car-hold): их читают и сцена, и слой стрелок. Своих
     `--car-pad`/`--tilt-pad` здесь больше нет — иначе слой стрелок считал
     бы по одному полю, а сцена стояла бы по другому, и стрелки уезжали бы
     от середины дорожки.

     Нижнее поле — СВОЁ и записано отдельным значением: под полосой с
     точками общее поле не нужно (разбор у самой переменной). Записывать
     его короткой записью нельзя — она снесла бы верхнее поле, а по нему
     сцена держит вынос карточки. */
  padding:var(--scene-pad) var(--scene-pad) var(--scene-pad-b);
  border-radius:var(--car-radius);
  background:#080d17;
  border:1px solid rgba(120,160,220,.16);
  box-shadow:0 26px 70px rgba(0,0,0,.5);
  --car-base:.62s;
  --car-speed:1;
  --car-w:100%;
  --car-radius:20px;
}
/* Размер задан ДОЛЕЙ ширины сцены, а не пикселями: публикацию открывают и с
   телефона, и с широкого монитора. Доля честна на любой ширине, пиксели —
   только на одной. Нижняя граница в clamp() — чтобы на телефоне «самый
   маленький» не стал маркой. Пол именно 160px, а не 210: с 210 на экране
   390px «самый маленький» и «средний» сходились почти вплотную (210 и 242),
   и семь ступеней превращались в четыре — замерено пробой. */
.wp-car[data-size="xxs"]{--car-w:52%;--car-radius:12px}
.wp-car[data-size="xs"]{--car-w:58%;--car-radius:13px}
.wp-car[data-size="sm"]{--car-w:64%;--car-radius:14px}
.wp-car[data-size="md"]{--car-w:72%;--car-radius:16px}
.wp-car[data-size="lg"]{--car-w:80%;--car-radius:17px}
.wp-car[data-size="xl"]{--car-w:88%;--car-radius:18px}
.wp-car[data-size="full"]{--car-w:100%;--car-radius:20px}
.wp-car-track{
  position:relative;
  width:clamp(160px, var(--car-w), 100%);
  margin:0 auto;
  aspect-ratio:4 / 3;
  perspective:1100px;
  /* Курсор говорит, что кадр можно тянуть: без него мышь «не реагировала»
     не только на жест, но и на вид — непонятно, что тут вообще что-то
     есть. Владелец (29.09): «не реагирует на палец и на курсор». */
  cursor:grab;
}
.wp-car-track.is-grabbing,.wp-car.is-grabbing .wp-car-track{cursor:grabbing}
.wp-car-track img,.wp-car-track video{user-select:none}
/* Свайп по кадру.

   `touch-action:pan-y` — не мелочь: без него браузер на телефоне считает
   горизонтальное движение СВОИМ (жест «назад» в Chrome Android), и свайп
   по кадру уводит со страницы вместо листания. Правило нужно на КАЖДОМ
   элементе под пальцем, а не только на дорожке: touch-action не
   наследуется, и палец, попавший по снимку, видео или гифке, слушал бы
   своё умолчание. Владелец просил именно этого: «чтобы фото и видео и
   гифки листались свайпом». */
.wp-car-track,.wp-car-slide,
.wp-car-slide img,.wp-car-slide video{touch-action:pan-y}
.wp-car-slide{
  /* margin:0 обязателен: кадр — это <figure>, а у него от браузера отступ
     1em 40px. inset:0 его НЕ снимает — inset задаёт края, но отступы
     остаются, и кадр выходит 698px в дорожке 778px. Тогда текущий кадр
     меньше рамки, а соседний выглядывает полоской у края — та самая
     «щель», которую видно на снимке. */
  margin:0;
  position:absolute;inset:0;
  transition:transform calc(var(--car-base) / var(--car-speed)) var(--ease),
             opacity calc(var(--car-base) / var(--car-speed)) var(--ease);
  z-index:calc(10 - var(--ak,0));
}
/* Соседний кадр виден (в колоде и ленте), но он не должен ловить нажатия:
   у видео внутри есть своя панель, и клик по чужому кадру запускал бы его. */
.wp-car-slide[aria-hidden="true"]{pointer-events:none}
.wp-car-card{
  position:absolute;inset:0;z-index:1;
  display:block;overflow:hidden;
  border-radius:var(--car-radius);
  background:#080d17;
}
.wp-car-slide img,.wp-car-slide video{
  width:100%;height:100%;object-fit:contain;display:block;
}

/* ---------- подложка кадра: размытая копия вместо чёрных полей ----------

   Владелец (29.09): «черный фон по краям или вверху или внизу надо сделать
   так чтобы он размывался фоткой типо блюр эффекта чтобы небыло темных
   полос ... и в гиф и в видео и фото если есть такие черные края».

   Полосы рисует не сцена и не рамка, а ФОН САМОГО СНИМКА: класс `wp-img`
   носит `background:#080d17`, и он закрашивает поля вписывания `contain`.
   Слой ПОЗАДИ снимка их не закрыл бы — фон непрозрачный, и владелец видел
   ровно его. Поэтому фон у снимка внутри карусели снят, а на его место
   положен этот слой: тот же кадр, растянутый на весь кадр (`cover`) и
   размытый.

   Габарит слоя — ровно габарит снимка. У «толстой» рамки снимок вставлен на
   --fr-w со всех сторон, у остальных видов занимает всю карточку; отсюда
   --fr-in. Место он берёт от края карточки, как и сам снимок. */
.wp-car-card{--fr-in:0px}
.wp-car[data-look="thick"] .wp-car-card{--fr-in:var(--fr-w)}
.wp-car-slide .wp-car-card > .wp-img,
.wp-car-slide .wp-car-card > .wp-video{
  background:transparent;
  /* Снимок — над подложкой. `relative` его не двигает: место задают
     width/height, а у «толстой» рамки — absolute со смещением. */
  position:relative;z-index:1;
}
.wp-car-slide .wp-car-card > .wp-car-bg{
  --bg-blur:28px;
  /* Слой ШИРЕ снимка на два с половиной размытия с каждой стороны. У
     размытого края кромка полупрозрачная, и без запаса из-под неё выглядывал
     бы тёмный фон — то есть ровно та полоса, которую владелец и просил
     убрать. Запас ФИКСИРОВАННЫЙ, а не в процентах: он привязан к размытию, а
     оно не зависит от размера кадра. Лишнее срезает обрезка карточки. */
  --bg-out:calc(var(--bg-blur) * 2.5);
  position:absolute;
  left:calc(var(--fr-in) - var(--bg-out));
  top:calc(var(--fr-in) - var(--bg-out));
  width:calc(100% - var(--fr-in) * 2 + var(--bg-out) * 2);
  height:calc(100% - var(--fr-in) * 2 + var(--bg-out) * 2);
  z-index:0;pointer-events:none;
  border-radius:calc(var(--car-radius) - 2px);
  background-image:var(--car-bg,none);
  background-size:cover;background-position:center;
  /* Цвет — тот же, что был у снимка: подложка есть не всегда (у видео без
     постера её снимает скрипт, и это может не выйти), и без картинки кадр
     обязан выглядеть как раньше, а не светлой полосой рамки. */
  background-color:#080d17;
  /* Размытие сильное намеренно: подложке не нужно быть узнаваемой, её дело —
     закрасить поля. */
  filter:blur(var(--bg-blur)) saturate(1.15) brightness(.9);
}
/* Кадр видео — по его собственным пропорциям (размер ставит js/work.js: в
   оформлении этих чисел нет и быть не может).

   Поля вписывания видео браузер закрашивает ЧЁРНЫМ сам, из-под любой
   подложки: слой позади этих полей не закрывает — проверено снимком, где
   подложка с кадром была на месте, а полосы оставались чёрными. У картинки
   поля прозрачные, у видео нет. Поэтому видео получает свой настоящий
   габарит, полей не остаётся вовсе, а место вокруг занимает подложка.
   `position:absolute` здесь обязателен: сдвиг и размер приходят из скрипта
   в координатах карточки. */
.wp-car-slide .wp-car-card > .wp-video.is-fit{
  position:absolute;
  width:auto;height:auto;
  max-width:none;max-height:none;
  object-fit:fill;
}
/* ---------- вид карточки ----------

   «Толстая объёмная» переписана 29.09 после снимка владельца: «этой рамки
   не видно и она не объёмная». Причина — прежний вид рисовал толщину
   СВЕТЛОЙ полосой подложки справа и снизу, а подложка рамки
   (#5a76a4 → #1c2537) на тёмной сцене (#080d17) почти не отличалась от
   неё: владелец видел просто фотографию, сдвинутую в угол.

   Теперь рамка настоящая: фотография вставлена в неё со всех четырёх
   сторон, ребро ловит свет сверху-слева и уходит в тень снизу-справа —
   так читается скошенная грань. Цвет ребра — из палитры (data-frame),
   перелив — отдельный слой (data-shine), наклон — на самой карточке
   (data-tilt).

   Всё по-прежнему ВНУТРИ габарита карточки: сцена обрезает содержимое, и
   любая задняя плита наружу была бы срезана — на это уже потратили вечер. */
.wp-car[data-look="flat"] .wp-car-card{
  border:0;
}
.wp-car[data-look="card"] .wp-car-card{
  border:1px solid rgba(150,190,255,.24);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.07),
             inset 0 0 0 1px rgba(0,0,0,.35);
}

/* ---------- палитра рамки ----------

   Наружу уезжает ИМЯ («cyan»), а не значение: значения живут здесь, и
   смена оттенка — правка оформления, а не файла владельца. Четыре
   величины на палитру: --fr-a светлое ребро, --fr-b середина, --fr-c
   тёмный край, --fr-glow свечение вокруг карточки.

   Палитру получает КАЖДОЕ имя из списка сервера, включая умолчание
   «steel»: у него своя строка рядом с голым .wp-car, в одном блоке. Так
   видно, что ни одно имя не осталось без величин, и палитра умолчания не
   разъезжается с самим умолчанием. Голый .wp-car в списке оставлен
   нарочно: страница без data-frame (старый снимок, чужой сервер)
   по-прежнему выглядит как раньше.
   Толщина рамки (--fr-w) от цвета не зависит и живёт на голом .wp-car. */
.wp-car,
.wp-car[data-frame="steel"]{
  --fr-a:#a8bcd8;--fr-b:#4a5f86;--fr-c:#161d2c;
  --fr-glow:rgba(150,190,255,.42);
}
.wp-car{
  --fr-w:16px;
}
.wp-car[data-frame="cyan"]{
  --fr-a:#a6f4ff;--fr-b:#2f7d97;--fr-c:#0b2130;
  --fr-glow:rgba(34,230,255,.5);
}
.wp-car[data-frame="magenta"]{
  --fr-a:#ffb3ee;--fr-b:#9b2f7d;--fr-c:#2a0b21;
  --fr-glow:rgba(255,79,216,.5);
}
.wp-car[data-frame="violet"]{
  --fr-a:#c3b0ff;--fr-b:#5b3f9e;--fr-c:#191030;
  --fr-glow:rgba(150,110,255,.5);
}
.wp-car[data-frame="gold"]{
  --fr-a:#ffe9a8;--fr-b:#a8842f;--fr-c:#2d210b;
  --fr-glow:rgba(255,206,84,.5);
}
.wp-car[data-frame="emerald"]{
  --fr-a:#a8f5cf;--fr-b:#2f8a63;--fr-c:#0b2318;
  --fr-glow:rgba(52,226,155,.5);
}
.wp-car[data-frame="copper"]{
  --fr-a:#ffc9a8;--fr-b:#9c5a35;--fr-c:#2a150b;
  --fr-glow:rgba(255,146,84,.5);
}
.wp-car[data-frame="mono"]{
  --fr-a:#c8cfda;--fr-b:#4a5260;--fr-c:#13171e;
  --fr-glow:rgba(180,190,205,.34);
}

/* Рамка. Поля карточке НЕ задаём: у неё inset:0, и padding сдвинул бы
   фотографию на двойную толщину — абсолютный потомок отсчитывается от
   поля вложенности, а не от края. Толщину задаёт само смещение снимка. */
.wp-car[data-look="thick"] .wp-car-card{
  border:0;
  border-radius:calc(var(--car-radius) + 3px);
  background:linear-gradient(148deg,
    var(--fr-a) 0%,var(--fr-b) 34%,var(--fr-c) 78%,var(--fr-b) 100%);
  /* Верхнее-левое ребро ловит свет, нижнее-правое уходит в тень. Без
     этой пары рамка читается плоской наклейкой, а не скошенной гранью —
     ровно на это и жаловался владелец. */
  box-shadow:
    inset 2px 2px 0 rgba(255,255,255,.34),
    inset -2px -2px 0 rgba(0,0,0,.62),
    inset 0 0 0 1px rgba(0,0,0,.45),
    0 0 0 1px var(--fr-glow),
    0 20px 44px rgba(0,0,0,.55),
    0 0 34px -6px var(--fr-glow);
  /* Наклон — «как будто это реальная карта». Ровная карточка на тёмной
     сцене снова слилась бы с ней; под наклоном видно боковое ребро, и
     объём читается даже на чёрном. */
  transform-origin:50% 50%;
  will-change:transform;
}
.wp-car[data-tilt="left"] .wp-car-card{
  transform:perspective(1200px) rotateY(-13deg) rotateX(6deg) rotate(-1.2deg);
}
.wp-car[data-tilt="right"] .wp-car-card{
  transform:perspective(1200px) rotateY(13deg) rotateX(6deg) rotate(1.2deg);
}
.wp-car[data-tilt="up"] .wp-car-card{
  transform:perspective(1200px) rotateX(-15deg) scale(.965);
}
.wp-car[data-tilt="none"] .wp-car-card{transform:none}

/* Запас под наклон — ПОЛЕ СЦЕНЫ, а не карточки, и задан ДОЛЕЙ ШИРИНЫ.

   Наклон выносит углы карточки за дорожку; сцена обрезает, и карточку
   теряет край. Вынос НЕ выведен из формулы — он держится сразу тремя
   поворотами и перспективой, и формула на глаз ошибалась. Величина взята
   замером на живой странице владельца, но пересчитана в ДОЛЮ: точки,
   подобранные при одной ширине, при другой перестают подходить — на этом
   и вышло 02.10, когда полосу расширили с 780 до 1200.

     наклон «вбок» — 6.5 % ширины обёртки
     наклон «назад» — 5 % ширины обёртки

   Наклон «ровно» запаса не получает: карточка не повёрнута и дорожку не
   перерастает. Зато получает поле вида карточки (см. --car-pad ниже).

   ИМЯ С ОКОНЧАНИЕМ `-w` — не украшение: `--tilt-x` в этом же листе уже
   занято углом наклона у другой карусели, и одно имя на две разные
   величины (долю ширины и градусы) рано или поздно свело бы их вместе.

   ЗАЗОР ДО ПОЛОСЫ (`--bar-gap-w`) ЗАВИСИТ ОТ НАКЛОНА, и это не вкус.
   Наклонённая карточка свешивается под дорожку, и полоса, стоящая вплотную,
   оказывается ПОД ней: владелец (02.10) обвёл красным полосу и написал
   «нижняя часть — надо опустить ниже». Замер до правки: дорожка кончалась
   в 1066, полоса начиналась в 1086 (зазор 20), а низ карточки доходил до
   1115 — то есть на 29 точек ЗАХОДИЛ на полосу. Теперь зазор 6 % ширины
   дорожки: при дорожке 1042 это 62, низ карточки остаётся в 19 точках НАД
   полосой. Наклон «назад» свешивает мало (низ коробки вообще не доходит до
   дорожки) — там хватает 3 %.

   ПРАВИЛА ЖИВУТ НА ОБЁРТКЕ, а не на сцене: то же поле читает слой стрелок
   (он лежит рядом со сценой и унаследовать переменные сцены не может).
   Отсюда и `data-tilt` на обёртке — его кладёт сервер. */
.wp-car-hold[data-tilt="left"],
.wp-car-hold[data-tilt="right"]{--tilt-pad-w:.065;--bar-gap-w:.06}
.wp-car-hold[data-tilt="up"]{--tilt-pad-w:.05;--bar-gap-w:.03}

/* Слой перелива. Лежит ПОД фотографией (z-index 0 против 1), поэтому
   виден ровно в полосе рамки: закрасить снимок он не может, а наклон и
   толщина остаются видны. */
.wp-car[data-look="thick"] .wp-car-card::before{
  content:'';position:absolute;inset:0;z-index:0;
  border-radius:inherit;pointer-events:none;
}
.wp-car[data-shine="sweep"] .wp-car-card::before{
  background:linear-gradient(115deg,
    transparent 34%,rgba(255,255,255,.5) 47%,
    rgba(255,255,255,.08) 54%,transparent 66%);
  background-size:260% 100%;
  animation:wpCarSweep 3.4s linear infinite;
}
.wp-car[data-shine="hue"] .wp-car-card::before{
  background:linear-gradient(100deg,
    #ff5f6d,#ffc371,#7bff9b,#5fd0ff,#b07bff,#ff5f6d);
  background-size:400% 100%;
  opacity:.62;
  animation:wpCarHue 6.5s linear infinite;
}
.wp-car[data-shine="pulse"] .wp-car-card::before{
  background:radial-gradient(120% 90% at 30% 20%,
    rgba(255,255,255,.55),transparent 62%);
  animation:wpCarPulse 2.6s ease-in-out infinite;
}
@keyframes wpCarSweep{
  from{background-position:180% 0}
  to{background-position:-80% 0}
}
@keyframes wpCarHue{
  from{background-position:0 0}
  to{background-position:400% 0}
}
@keyframes wpCarPulse{
  0%,100%{opacity:.18}
  50%{opacity:.62}
}

/* ---------- вспышка рамки при перелистывании ----------

   Владелец (29.09): «Добавить эффекты в рамки при движении, они светятся
   как у карточек товара и как у карточек работ, в админ панели чтобы мог их
   менять, большой выбор чтобы был».

   У товаров и работ вспышка живёт на `box-shadow` самой карточки и
   включается классом `is-turning`. Здесь так НЕЛЬЗЯ: у «толстой» карточки
   `box-shadow` уже занят объёмом — свет сверху-слева, тень снизу-справа,
   свечение рамки. Анимация затирала бы его целиком, и на время вспышки
   рамка становилась бы плоской наклейкой — ровно то, на что владелец
   жаловался в прошлый раз.

   Поэтому свечение рисует ОТДЕЛЬНЫЙ слой `::after`: в покое он пуст, на
   движении вспыхивает. Слой лежит поверх карточки, но фона у него нет —
   только тени, поэтому снимок он не закрашивает.

   Палитра — три величины на вид: `--gl-a` яркая вспышка, `--gl-b` тихое
   свечение, `--gl-ring` ободок по ВНУТРЕННЕЙ кромке. Ободок нужен потому,
   что сцена обрезает наружное свечение по своему краю; внутренний виден
   всегда. */
.wp-car{--gl-a:transparent;--gl-b:transparent;--gl-ring:transparent}
.wp-car[data-glow="edge"]   {--gl-a:rgba(34,230,255,.95);--gl-b:rgba(34,230,255,.30);--gl-ring:rgba(34,230,255,.55)}
.wp-car[data-glow="halo"]   {--gl-a:rgba(120,190,255,.85);--gl-b:rgba(120,190,255,.26);--gl-ring:rgba(150,210,255,.42)}
.wp-car[data-glow="neon"]   {--gl-a:rgba(255,79,216,.95);--gl-b:rgba(255,79,216,.30);--gl-ring:rgba(255,120,230,.60)}
.wp-car[data-glow="ring"]   {--gl-a:rgba(255,255,255,.95);--gl-b:rgba(200,220,255,.28);--gl-ring:rgba(255,255,255,.75)}
.wp-car[data-glow="gold"]   {--gl-a:rgba(255,196,92,.95);--gl-b:rgba(255,196,92,.30);--gl-ring:rgba(255,214,130,.62)}
.wp-car[data-glow="emerald"]{--gl-a:rgba(52,226,155,.95);--gl-b:rgba(52,226,155,.28);--gl-ring:rgba(90,240,180,.58)}
.wp-car[data-glow="magenta"]{--gl-a:rgba(255,79,216,.95);--gl-b:rgba(180,60,255,.30);--gl-ring:rgba(255,110,230,.58)}
.wp-car[data-glow="ice"]    {--gl-a:rgba(180,240,255,.95);--gl-b:rgba(120,200,255,.28);--gl-ring:rgba(210,245,255,.70)}
.wp-car[data-glow="rainbow"]{--gl-a:rgba(34,230,255,.95);--gl-b:rgba(255,79,216,.30);--gl-ring:rgba(255,196,92,.60)}
.wp-car[data-glow="double"] {--gl-a:rgba(255,120,60,.95);--gl-b:rgba(255,60,120,.30);--gl-ring:rgba(255,150,90,.60)}

/* ---------- свечение вокруг рамки ----------

   Владелец (29.09, вечер): «не работает свечение рамки при перелистывании,
   надо чтобы были наружу свечения, пример как у карусели товара — смотрится
   эффектно».

   Причина ИЗМЕРЕНА: слой свечения стоял на самой КАРТОЧКЕ
   (`.wp-car-card::after`), а карточка обрезает своё содержимое
   (`overflow:hidden` — им она держит скруглённые углы). Наружу от такой
   карточки не выходит ничего, поэтому свечения не было видно даже в покое:
   в замере у слоя было `box-shadow: rgba(180,60,255,.3) 0 0 34px -6px`, а
   карточка — `overflow:hidden`.

   Теперь слой живёт на КАДРЕ (`.wp-car-slide`): кадр не обрезает, а сцена
   вокруг оставляет запас под наклон (6.5 % ширины) — этого хватает
   наружному ореолу. Наклон повторяем на слое: у карточки своя 3D-проекция
   (`data-tilt`), и без повтора свечение стояло бы ровным прямоугольником
   позади наклонённой рамки.

   Величины подобраны ПО ЗАПАСУ СЦЕНЫ: было 76px при запасе 30px, и всё,
   что дальше края, срезалось бы жёсткой линией. Стало 30px наружу — ореол
   заполняет запас целиком и гаснет сам, не упираясь в край. */
.wp-car .wp-car-slide::after{
  content:'';position:absolute;inset:0;z-index:3;
  border-radius:var(--car-radius);
  transform-origin:50% 50%;
  pointer-events:none;
}
/* Свечение — только у показанного кадра. У соседей в «колоде» его нет:
   иначе у края сцены светились бы все сразу. */
.wp-car .wp-car-slide[aria-hidden="true"]::after{content:none}
.wp-car[data-look="thick"] .wp-car-slide::after{border-radius:calc(var(--car-radius) + 3px)}
.wp-car[data-tilt="left"] .wp-car-slide::after{
  transform:perspective(1200px) rotateY(-13deg) rotateX(6deg) rotate(-1.2deg);
}
.wp-car[data-tilt="right"] .wp-car-slide::after{
  transform:perspective(1200px) rotateY(13deg) rotateX(6deg) rotate(1.2deg);
}
.wp-car[data-tilt="up"] .wp-car-slide::after{
  transform:perspective(1200px) rotateX(-15deg) scale(.965);
}
.wp-car[data-tilt="none"] .wp-car-slide::after{transform:none}

/* Свечение рамки в покое — с палитры рамки (`--fr-glow`). */
.wp-car[data-look="card"] .wp-car-slide::after,
.wp-car[data-look="thick"] .wp-car-slide::after{
  box-shadow:0 0 0 1px var(--fr-glow),0 0 26px -6px var(--fr-glow);
}
/* Свечение при перелистывании — своя палитра (`--gl-*`). */
.wp-car[data-glow] .wp-car-slide::after{box-shadow:0 0 30px -4px var(--gl-b)}
.wp-car[data-glow="none"] .wp-car-slide::after{box-shadow:none}
.wp-car.is-turning .wp-car-slide::after{animation:wpCarGlow .62s var(--ease) both}
@keyframes wpCarGlow{
  0%  {box-shadow:0 0 0 0 transparent,inset 0 0 0 0 transparent}
  38% {box-shadow:0 0 30px 2px var(--gl-a),inset 0 0 22px 1px var(--gl-ring)}
  100%{box-shadow:0 0 30px -4px var(--gl-b),inset 0 0 0 0 transparent}
}
/* Радужный ободок переливается ЦВЕТОМ свечения, а не фильтром оттенка:
   фильтр перекрасил бы заодно и снимок внутри карточки. Тот же приём, что
   у ленты работ. */
.wp-car[data-glow="rainbow"].is-turning .wp-car-slide::after{
  animation:wpCarGlowRainbow .9s linear both;
}
@keyframes wpCarGlowRainbow{
  0%  {box-shadow:0 0 28px 2px rgba(34,230,255,.9),inset 0 0 20px 1px rgba(34,230,255,.6)}
  33% {box-shadow:0 0 28px 2px rgba(255,79,216,.9),inset 0 0 20px 1px rgba(255,79,216,.6)}
  66% {box-shadow:0 0 28px 2px rgba(255,196,92,.9),inset 0 0 20px 1px rgba(255,196,92,.6)}
  100%{box-shadow:0 0 30px -4px var(--gl-b),inset 0 0 0 0 transparent}
}
/* «Двойной удар»: вспышка, короткая пауза, вспышка сильнее. */
.wp-car[data-glow="double"].is-turning .wp-car-slide::after{
  animation:wpCarGlowDouble .8s var(--ease) both;
}
@keyframes wpCarGlowDouble{
  0%  {box-shadow:0 0 0 0 transparent,inset 0 0 0 0 transparent}
  22% {box-shadow:0 0 30px 2px var(--gl-a),inset 0 0 20px 1px var(--gl-ring)}
  44% {box-shadow:0 0 12px 0 transparent,inset 0 0 0 0 transparent}
  70% {box-shadow:0 0 32px 4px var(--gl-a),inset 0 0 26px 2px var(--gl-ring)}
  100%{box-shadow:0 0 30px -4px var(--gl-b),inset 0 0 0 0 transparent}
}
/* Фотография — ПЕРЕДНЯЯ грань карточки: вставлена в рамку на --fr-w со
   всех четырёх сторон, а не сдвинута в угол. Смещение отсчитывается от
   края карточки, потому что полей у неё нет. */
.wp-car[data-look="thick"] .wp-car-card > img,
.wp-car[data-look="thick"] .wp-car-card > video{
  position:absolute;z-index:1;
  left:var(--fr-w);top:var(--fr-w);
  width:calc(100% - var(--fr-w) * 2);
  height:calc(100% - var(--fr-w) * 2);
  border-radius:calc(var(--car-radius) - 2px);
  /* Тень передней грани падает на ребро — без неё рамка не читается. */
  box-shadow:0 0 0 1px rgba(0,0,0,.55),0 3px 12px rgba(0,0,0,.5);
}
/* На узком экране рамка тоньше: 16px с каждой стороны съедали у кадра
   ширину, которой и так мало. */
@media (max-width:640px){
  .wp-car{--fr-w:11px}
}
/* Карточке нужно поле у сцены: без него она упирается в рамку экрана и
   «лежит» уже не на экране, а в нём впритык. «Без рамки» поля не получает —
   это ровно прежний вид публикации.

   Объявлено на ОБЁРТКЕ: поле сцены — одно на сцену и на слой стрелок, и
   складывается оно там же, где читается (см. --scene-pad в .wp-car-hold).
   Величина остаётся точками: она про рамку карточки, а рамка не тянется. */
.wp-car-hold[data-look="card"],.wp-car-hold[data-look="thick"]{--car-pad:10px}
/* ---------- расстановка ----------

   «По одной» — прежний вид: виден ровно один кадр, соседние уехали за край.
   «Колодой» и «лентой» соседние кадры ОСТАЮТСЯ ВИДНЫ (позади и по бокам),
   поэтому их смещение задано не сырым --k (он равен ±1, ±2, … и уводил бы
   кадр на целую ширину), а сжатым --kx: направление ±1, а дальность — уже
   забота оформления.

   Сам --k при этом не трогаем: он держит порядок наложения
   (z-index: 10 - ak), и по нему же считает эффект. */
/* «По одной» — дорожка ОБРЕЗАЕТ кадры по себе.

   Не украшение, а починка. Кадр уезжает на 100 % ШИРИНЫ ДОРОЖКИ, а дорожка
   уже сцены (размер кадра — доля сцены), поэтому уехавший сосед оставлял за
   краем экрана полоску шириной с поле сцены: при размере «средний» это
   110 px следующей фотографии, торчащих у правого края. Проверено пробой:
   «по одной» показывала соседа полосой 110 px. Обрезка по дорожке убирает
   полоску ровно, а кадру вредить не может — он дорожку и занимает целиком. */
.wp-car[data-pos="one"] .wp-car-track{overflow:hidden}
.wp-car[data-pos="deck"] .wp-car-slide,
.wp-car[data-pos="rail"] .wp-car-slide{
  --kx:min(max(var(--k,0),-1),1);
  transform:translate3d(calc(var(--kx) * var(--pos-x)),0,0)
            translateZ(calc(min(var(--ak,0),2) * var(--pos-z)))
            scale(calc(1 - min(var(--ak,0),2) * var(--pos-shrink,.04)
                       - min(var(--ak,0),1) * var(--fx-scale,0)))
            rotateY(calc(var(--kx) * var(--fx-rot, var(--pos-rot, 0deg))))
            rotate(calc(var(--kx) * var(--fx-roll, 0deg)));
  opacity:calc(1 - min(var(--ak,0),2) * .26
               - min(var(--ak,0),1) * var(--fx-fade,0));
}
/* Колода: соседние кадры прямые, меньше и глубже — стопка карточек.
   Лента: те же соседи, но развёрнутые в 3D и на другом удалении — витрина.
   Наклон ленты задан как ЗАПАСНОЕ значение --fx-rot: если владелец выбрал
   эффект со своим поворотом («переворот», «грань», «маятник»), он и
   победит, а не сложится с наклоном в нечитаемый угол.

   Усадка — доля на ШАГ (--pos-shrink), а не готовый масштаб: готовый
   масштаб уменьшил бы и ТЕКУЩИЙ кадр (у него --ak = 0), и «во всю ширину»
   дало бы 94 % сцены вместо всей ширины. Пробой поймано. */
.wp-car[data-pos="deck"]{--pos-x:26%;--pos-z:-46px;--pos-shrink:.06;--pos-rot:0deg}
.wp-car[data-pos="rail"]{--pos-x:70%;--pos-z:-90px;--pos-shrink:.02;--pos-rot:-34deg}
/* Оттенок выбранного эффекта для колоды и ленты: сдвиг задаёт сама
   расстановка, а эффект добавляет поворот, наплыв или затухание. Одна
   таблица переменных вместо двадцати правил «эффект × расстановка». */
.wp-car[data-fx="fade"]{--fx-fade:.55}
.wp-car[data-fx="slide"]{--fx-fade:.18}
.wp-car[data-fx="zoom"]{--fx-scale:.3}
.wp-car[data-fx="flip"]{--fx-rot:-70deg}
.wp-car[data-fx="cube"]{--fx-rot:-46deg}
.wp-car[data-fx="swing"]{--fx-rot:-30deg;--fx-roll:4deg}
.wp-car[data-fx="spin"]{--fx-roll:-11deg;--fx-scale:.22}
.wp-car[data-fx="rise"]{--fx-fade:.4}
.wp-car[data-fx="tilt"]{--fx-scale:.16;--fx-fade:.3}
.wp-car[data-fx="none"]{--fx-fade:.5}
/* ---------- эффекты для расстановки «по одной» ---------- */
.wp-car[data-pos="one"][data-fx="slide"] .wp-car-slide{
  transform:translate3d(calc(var(--k,0) * 100%),0,0);
}
.wp-car[data-pos="one"][data-fx="fade"] .wp-car-slide{
  opacity:calc(1 - min(var(--ak,0),1));
  transform:translate3d(calc(var(--k,0) * 8%),0,0);
}
.wp-car[data-pos="one"][data-fx="zoom"] .wp-car-slide{
  opacity:calc(1 - min(var(--ak,0),1));
  transform:scale(calc(1 + min(var(--ak,0),1) * .18));
}
.wp-car[data-pos="one"][data-fx="flip"] .wp-car-slide{
  opacity:calc(1 - min(var(--ak,0),1) * .9);
  transform:perspective(1200px) rotateY(calc(var(--k,0) * -90deg));
}
.wp-car[data-pos="one"][data-fx="cube"] .wp-car-slide{
  opacity:calc(1 - min(var(--ak,0),1) * .6);
  transform:perspective(1400px) rotateY(calc(var(--k,0) * -60deg))
            translateZ(calc(min(var(--ak,0),1) * -120px));
  transform-origin:left center;
}
.wp-car[data-pos="one"][data-fx="swing"] .wp-car-slide{
  opacity:calc(1 - min(var(--ak,0),1) * .55);
  transform:perspective(1200px) translate3d(calc(var(--k,0) * 30%),0,0)
            rotateY(calc(var(--k,0) * -26deg))
            rotateZ(calc(var(--k,0) * 3.5deg));
}
.wp-car[data-pos="one"][data-fx="spin"] .wp-car-slide{
  opacity:calc(1 - min(var(--ak,0),1) * .8);
  transform:scale(calc(1 - min(var(--ak,0),1) * .28))
            rotate(calc(var(--k,0) * -14deg));
}
.wp-car[data-pos="one"][data-fx="rise"] .wp-car-slide{
  transform:translate3d(0,calc(var(--k,0) * 100%),0);
}
.wp-car[data-pos="one"][data-fx="tilt"] .wp-car-slide{
  opacity:calc(1 - min(var(--ak,0),1) * .7);
  transform:perspective(900px) translate3d(calc(var(--k,0) * 42%),0,0)
            rotateX(calc(var(--k,0) * 32deg));
}
.wp-car[data-pos="one"][data-fx="none"] .wp-car-slide{
  transition:none;
  opacity:calc(1 - min(var(--ak,0),1));
}
/* ---------- стрелки листания ----------

   Стрелки лежат в ОБЁРТКЕ (.wp-car-hold), рядом со сценой, а не внутри
   неё: сцена обрезает содержимое, и стрелку, стоящую сбоку от карусели,
   внутри неё срезало бы по краю. Размер — ступень владельца
   (data-arrow-size), место — тоже его выбор (data-arrow-pos): сбоку от
   карусели в полях страницы или внутри кадра, как было.

   Середину ДОРОЖКИ слой берёт ПОЛЕМ, а не вычитанием высоты полосы.

   Было `top:calc((100% - var(--bar-h)) / 2)`, и `--bar-h` — число, которое
   полагалось держать равным «высота полосы + её отступ сверху». Владелец
   правил полосу — число надо было править руками, и сторож это стерёг.
   Теперь слой растянут по обёртке (`inset:0`), а сверху и снизу получает
   ровно то, что занимают поле сцены и полоса: `padding-top` = поле сцены,
   `padding-bottom` = поле сцены + зазор + высота полосы. В контенте
   остаётся ровно высота дорожки, и `align-items:center` кладёт кнопки по её
   середине сам.

   ПОЧЕМУ ДОЛИ, А НЕ ТОЧКИ. Проценты в `padding` считаются от ширины
   содержащего блока — то есть от ширины ОБЁРТКИ, ровно как поле сцены.
   Обе величины растут вместе, и разъехаться им нечем. В `margin-top`
   полосы процент считается от ширины ДОРОЖКИ (её содержащий блок — сцена),
   а дорожка уже обёртки на два поля сцены; поэтому в нижнем поле стоит
   множитель `(1 - 2 * --tilt-pad-w)` — он переводит одну основу в другую.
   Без него нижнее поле вышло бы на несколько точек больше нужного, и
   стрелка уехала бы вниз ровно на половину разницы.

   `--bar-h` остался точками: это высота самой полосы, от ширины она не
   зависит. */
.wp-car-hold{
  position:relative;
  --car-pad:0px;
  --tilt-pad-w:0;
  --bar-gap-w:.02;
  --bar-h:44px;
  /* Поле сцены — ОДНО на всех. Его читают и сцена (padding), и слой стрелок
     (padding-top/bottom), поэтому объявлено оно здесь, на обёртке: сцена
     лежит ВНУТРИ обёртки и переменную оттуда не отдаст, а слой стрелок ей
     не потомок.

     Нижняя граница в 1px — не красота. У сцены своя рамка в 1px, и обрезка
     идёт по её внутреннему краю: карточка, вставшая вплотную (вид «без
     рамки» + наклон «ровно» — там запаса нет вовсе), упирается ровно в этот
     край, и проба показывает срез в точку. Точка незаметна, но правило
     «карточка всегда внутри рамки» перестаёт быть правилом. */
  --scene-pad:max(1px, var(--car-pad), calc(var(--tilt-pad-w) * 100%));
  /* Нижнее поле сцены — ОТДЕЛЬНОЕ, и вот почему.

     Общее поле держит ВЫНОС наклонённой карточки: карточка выходит углами
     за дорожку, и сцена обязана оставить под них место. Пока под дорожкой
     ничего нет, это место и есть нижнее поле.

     Но когда под дорожкой стоит ПОЛОСА С ТОЧКАМИ, вынос достаётся зазору
     до неё (--bar-gap-w — он по выносу и подобран), а ниже полосы общее
     поле уже не нужно вовсе. Владелец (02.10): «поднять выше нижнюю часть
     рамки, она сейчас очень сильно опущена» — и обвёл пустую полосу ПОД
     полосой. Замер на его сайте при окне 1440: полоса кончается на 1148,
     нижняя кромка рамки на 1227 — 79 точек пустоты.

     Полосы нет — зазор 0, и место под вынос обязано остаться полным. */
  --scene-pad-b:var(--scene-pad);
  --nav-size:68px;
  --nav-out:calc(var(--nav-size) + 14px);
}
/* Полосы нет — нет ни её высоты, ни зазора до неё. */
.wp-car-hold[data-no-bar="1"]{--bar-h:0px;--bar-gap-w:0}
/* Полоса ЕСТЬ — нижнее поле ужимается до вида рамки: вынос карточки уже
   отодвинут зазором до полосы, и держать под полосой ещё одно поле нечего.
   Доля общего поля, а не число: на телефоне поле 23 точки, и зашитые
   двадцать там — уже вся полоса. Двенадцать точек — пол: ниже полоса
   сливалась бы с кромкой рамки. */
.wp-car-hold:not([data-no-bar="1"]){
  --scene-pad-b:max(12px, calc(var(--scene-pad) * .3))
}
.wp-car-hold[data-arrow-size="sm"]{--nav-size:44px}
.wp-car-hold[data-arrow-size="md"]{--nav-size:56px}
.wp-car-hold[data-arrow-size="lg"]{--nav-size:68px}
.wp-car-hold[data-arrow-size="xl"]{--nav-size:80px}
/* Слой стрелок лежит во ВСЮ обёртку, а середину дорожки держит РАЗНОСТЬЮ
   своих полей: сверху — верхнее поле сцены, снизу — нижнее плюс зазор плюс
   высота полосы. Поэтому нижнее поле здесь берётся то же, что у сцены
   (`--scene-pad-b`), и это не повтор ради повтора: возьми он общее поле, а
   сцена — своё, и стрелки уехали бы от середины дорожки ровно на разницу
   между ними (замер: 55 точек вниз). */
.wp-car-navs{
  position:absolute;inset:0;box-sizing:border-box;
  padding-top:var(--scene-pad);
  padding-bottom:calc(var(--scene-pad-b) +
    var(--bar-gap-w) * (1 - var(--tilt-pad-w) * 2) * 100% + var(--bar-h));
  display:flex;align-items:center;justify-content:space-between;
  z-index:12;pointer-events:none;
}
.wp-car-navs > button{pointer-events:auto}
/* Внутри кадра — прежнее место: у самых краёв сцены.
   Поля заданы ПООТДЕЛЬНОСТИ: короткая запись `padding:0 12px` снесла бы
   верхнее и нижнее поле, а по ним слой и держит середину дорожки. */
.wp-car-hold[data-arrow-pos="in"] .wp-car-navs{padding-left:12px;padding-right:12px}
/* Сбоку от карусели — в полях страницы, как на снимке владельца. Слой
   расширяется на --nav-out в обе стороны, и кнопки встают ровно на его
   края, то есть на --nav-out наружу от сцены. */
.wp-car-hold[data-arrow-pos="out"] .wp-car-navs{
  left:calc(-1 * var(--nav-out));
  right:calc(-1 * var(--nav-out));
}
/* Наружу — но не дальше края окна.

   Замерено: полоса страницы (и .wrap главной, и .wp-wrap страниц работы и
   сайта — теперь одна и та же ширина) отступает от края окна на 4vw, а
   слой стрелок вылезает на --nav-out = 58 точек. При окне 1024 это 41
   против 58: стрелка уезжала за край на 17 точек и обрезалась, потому что
   лишнее по бокам прячет body (overflow-x:hidden).

   Правило трогает ТОЛЬКО страницы работы и сайта (.wp-page): в окне работы
   на главной стрелки ужимает своя обёртка (см. .wr-modal-content выше), и
   мешать ей не нужно.

   Восемь точек про запас — не красота: 100vw включает ширину полосы
   прокрутки, а окно её не включает, и без запаса стрелка уезжала бы под
   полосу прокрутки ровно на её половину.

   При окне 1440 поле 112 против 58 — то есть на размере владельца правило
   не делает НИЧЕГО: стрелки стоят там же, где стояли. */
.wp-page .wp-car-hold[data-arrow-pos="out"]{
  --nav-out:min(calc(var(--nav-size) + 14px),
    calc((100vw - min(var(--maxw),92vw)) / 2 - 8px));
}
.wp-car-nav{
  width:var(--nav-size);height:var(--nav-size);
  flex:none;border:1px solid rgba(120,160,220,.22);
  border-radius:50%;cursor:pointer;
  display:flex;align-items:center;justify-content:center;
  color:#fff;background:rgba(6,10,18,.62);
  -webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
  box-shadow:0 10px 26px rgba(0,0,0,.45);
  transition:background .25s var(--ease),color .25s var(--ease),
             transform .25s var(--ease);
}
.wp-car-nav svg{width:46%;height:46%}
.wp-car-nav:hover{
  background:rgba(34,230,255,.22);color:var(--cyan);
  transform:scale(1.06);
}
.wp-car-nav:focus-visible{outline:2px solid var(--cyan);outline-offset:3px}
/* На узком окне сбоку места нет: стрелки возвращаются внутрь кадра и
   ужимаются, какую бы ступень владелец ни выбрал. Крупная стрелка на
   телефоне закрыла бы половину фотографии. Вес у правила тот же, что у
   ступеней (два), поэтому оно их и перебивает — оно ниже по листу. */
@media (max-width:900px){
  .wp-car-hold[data-arrow-pos="out"] .wp-car-navs{
    left:0;right:0;padding-left:10px;padding-right:10px}
  .wp-car-hold[data-arrow-size]{--nav-size:48px}
}
@media (max-width:480px){
  .wp-car-hold[data-arrow-size]{--nav-size:42px}
}

/* ---------- полоса под дорожкой ----------

   Три места: слева сердце и «поделиться», по центру точки, справа счётчик
   «2 / 6». Центр держит сетка 1fr auto 1fr — она честно центрует точки и
   тогда, когда слева пусто (у работы нет лайков), и тогда, когда справа
   нет счётчика. На flex с margin:auto точки уезжали бы вбок ровно на
   разницу ширин соседей — владелец и прислал снимок с точками у левого
   края.

   ОТСТУП СВЕРХУ — ДОЛЯ ШИРИНЫ ДОРОЖКИ (`--bar-gap-w`), а не число, и это
   ответ на замер, а не вкус.

   Владелец (29.09): «отметил красным их немного ниже надо спустить, а то
   они сейчас очень близко к рамке» — тогда зазор был 20px и его не хватало:
   замер по пикселям снимка дал 5–8 px от кромки рамки до полосы.

   Владелец (02.10): «карусель карточек съехала … нижняя часть надо опустить
   ниже» — обвёл красным саму полосу. Замер: зазор 20, а низ наклонённой
   карточки доходил до 1115 при полосе от 1086, то есть ЗАХОДИЛ на неё на
   29 точек. Причина та же, что у среза карточки: запас под наклон стоял
   числом и не вырос вместе с карточкой, когда полосу страницы расширили.

   Поэтому и зазор задан долей — он обязан расти вместе с выносом карточки.
   Величина живёт рядом с наклоном (`--bar-gap-w` в .wp-car-hold).

   Высота полосы — ЧИСЛО: она от ширины не зависит. По ней слой стрелок
   считает своё нижнее поле (`--bar-h` в .wp-car-hold), поэтому, меняя
   высоту, поправь и там. */
.wp-car-bar{
  display:grid;grid-template-columns:1fr auto 1fr;
  align-items:center;gap:12px;
  height:44px;margin-top:calc(var(--bar-gap-w) * 100%);
}
.wp-car-acts{justify-self:start;min-width:0;display:flex;align-items:center}
/* Сердце и «поделиться» внутри полосы — те же элементы, что на странице,
   но без своих полей: там они стояли отдельной строкой под шапкой, и
   margin:16px сверху раздувал бы полосу вдвое. */
.wp-car-acts .wp-acts{margin:0}
.wp-car-acts .wr-like{padding:4px 6px;font-size:.84rem}
.wp-car-acts .wr-like svg{width:22px;height:22px}
.wp-car-acts .wr-share{padding:4px 6px}
.wp-car-acts .wr-share svg{width:22px;height:22px}
.wp-car-count{
  justify-self:end;
  font-size:.78rem;font-weight:700;letter-spacing:.02em;white-space:nowrap;
  color:#cfe3ff;padding:4px 10px;border-radius:999px;
  background:rgba(6,10,18,.6);border:1px solid rgba(120,160,220,.22);
}
.wp-car-dots{
  justify-self:center;
  display:flex;align-items:center;
  padding:5px 9px;border-radius:999px;
  background:rgba(6,10,18,.52);
  -webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
}
.wp-car-dots button{
  width:12px;height:12px;padding:3px;
  border:0;background:none;cursor:pointer;
  display:flex;align-items:center;justify-content:center;
  -webkit-tap-highlight-color:transparent;
}
.wp-car-dots button::before{
  content:'';width:6px;height:6px;border-radius:50%;
  background:rgba(160,190,230,.32);
  transition:background .3s var(--ease),transform .3s var(--ease);
}
.wp-car-dots button[aria-current="true"]::before{background:var(--cyan);transform:scale(1.35)}
.wp-car-dots button:focus-visible{outline:2px solid var(--cyan);outline-offset:2px;border-radius:50%}
@media (prefers-reduced-motion:reduce){
  .wp-car-slide{transition:none}
  /* Перелив рамки — тоже движение, и «меньше движения» его выключает. */
  .wp-car[data-look="thick"] .wp-car-card::before{animation:none}
  /* Свечение при перелистывании — движение по определению. Гасим ВСЕ три
     набора кадров (обычный, радужный, двойной): правило выше перечисляет
     только `wpCarGlow`, а радужный и двойной объявлены своими селекторами
     с большей точностью — без этой строки они остались бы гореть. */
  .wp-car.is-turning .wp-car-slide::after,
  .wp-car[data-glow="rainbow"].is-turning .wp-car-slide::after,
  .wp-car[data-glow="double"].is-turning .wp-car-slide::after{animation:none}
}
.wp-img,.wp-video{width:100%;display:block;max-height:74vh;object-fit:contain;background:#080d17}

/* Сердце и «поделиться» — те же самые элементы, что на карточке ленты:
   классы wr-like / wr-share / wr-hint / wr-likes общие, поэтому и
   отклик на нажатие здесь ровно такой же. */
.wp-acts{position:relative;display:flex;align-items:center;gap:8px;margin:16px 0 6px}
.wp-acts .wr-like{font-size:1rem}
.wp-acts .wr-like svg{width:26px;height:26px}

.wp-body{display:flex;flex-direction:column;gap:22px;margin-top:26px}
.wp-block{margin:0}

/* Шапка блока, которая стоит ОТДЕЛЬНЫМ блоком в потоке (см. pubHeadBlock в
   server.js): у «Текста», «Цитаты», «Списка», «Характеристик», картинки,
   видео и галереи — у них своя рамка и свои поля, и заголовок внутри неё
   оказался бы внутри рамки.

   Свой нижний отступ шапки (38–64px, как у разделов сайта) здесь УМЕНЬШЕН
   ровно на 22px: столько уже добавляет поток между блоками
   (.wp-body{gap:22px}). Без вычета между названием и содержимым выходило бы
   60–86px, и один и тот же заголовок стоял бы у широких блоков ближе, чем у
   узких. */
.wp-head{margin-bottom:calc(clamp(38px,5vw,64px) - 22px)}

/* Блок публикации — это <section>, и общее правило `section{…}` выше даёт
   ему 115 пикселей пустоты сверху и снизу. Здесь блок не раздел сайта, а
   абзац статьи: расстояние между блоками задаёт поток (.wp-body{gap:22px}).

   Замерено: абзац в одну строку занимал 258px, из них 230 — пустота. С
   отступами разъезжались и свободно поставленные блоки: владелец тянул
   строку текста, а тащил за собой двести пикселей воздуха.

   Бьём ровно по виновнику, и только по <section>. Широкое правило
   `.wp-body .wp-block{padding:0}` было бы ошибкой: оно перебило бы СВОИ
   отступы блока — у цитаты (`.wp-quote{padding:20px 22px}`) и у списка
   (`.wp-list{padding-left:22px}`), — и владелец увидел бы цитату без
   полей, а список без отступа под точки. */
.wp-body section.wp-block{padding:0}

/* ---------- свободное размещение текста на публикации ----------
   Владелец: «в свободном режиме могу её там на странице перемещать текст
   куда мне нужно». Блок выходит из общего потока и встаёт по числам,
   которые владелец набрал, перетаскивая его на самой странице: слева и
   ширина — в процентах ширины полосы, сверху — в пикселях.

   position:relative у полосы обязателен. Без него абсолютный блок
   отсчитывался бы от окна и уехал бы вверх при первой же прокрутке.

   position:absolute ставим ТОЛЬКО блоку с готовым положением
   (data-wp-pos). Блок, которому владелец лишь включил режим и ещё не
   выбрал место, обязан остаться в потоке: иначе он всплыл бы в начало
   полосы и накрыл первый абзац ровно в тот миг, когда галочку поставили. */
.wp-body[data-wp-free-canvas]{position:relative}
.wp-free[data-wp-pos="1"]{position:absolute;margin:0;box-sizing:border-box}
.wp-free[data-wp-pos="1"] > .wp-block{margin:0}
.wp-body.is-dragging{user-select:none}

/* Ручки свободного размещения. Рисует их СЕРВЕР и только владельцу: у
   посетителя этих элементов в разметке нет вовсе. Прятать их оформлением
   было бы не защитой, а видимостью защиты — спрятанное правило снимается
   одной строкой в консоли. */
.wp-free-bar{
  display:flex;align-items:center;gap:10px;
  padding:5px 10px;margin-bottom:7px;
  border:1px dashed rgba(120,160,220,.45);border-radius:10px;
  background:rgba(20,32,54,.55);
  font-size:.74rem;color:var(--ink-3);
  cursor:grab;user-select:none;touch-action:none;
}
.wp-free-bar:focus{outline:2px solid rgba(34,230,255,.6);outline-offset:1px}
.wp-free.is-dragging .wp-free-bar{cursor:grabbing}
.wp-free-move{color:#22e6ff;font-weight:600}
.wp-free-info{
  flex:1 1 auto;min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.wp-free-save{flex:0 0 auto;color:#22e6ff}
.wp-free-save.is-bad{color:#ff8f8f}
/* z-index у уголка обязателен, а не для порядка. Блок публикации — это
   <section>, и общее правило `section{position:relative;z-index:2}` кладёт
   его ВЫШЕ уголка (у уголка z-index был «auto»). Замерено: в середине
   уголка лежал `wp-block wp-text`, нажатие доставалось тексту, и ширина
   не менялась ни на пиксель. Полоса — тот же случай, только она ещё и
   статична, поэтому ей нужен position:relative, иначе z-index не при
   чём. */
.wp-free-bar{position:relative;z-index:3}
.wp-free-grip{
  position:absolute;top:50%;right:-5px;width:10px;height:34px;margin-top:-17px;
  z-index:3;
  border-radius:5px;background:rgba(120,160,220,.5);
  cursor:ew-resize;touch-action:none;
}
.wp-free-grip:hover{background:rgba(34,230,255,.85)}

/* Телефон: свободное размещение выключается, и текст возвращается в общий
   поток. Числа при этом НЕ теряются — они ждут возврата к широкому окну.
   Иначе подпись, поставленная «слева 70 %», уехала бы за правый край
   экрана, и владелец увидел бы на телефоне пустое место.

   !important здесь обязателен, а не для порядка: положение приходит
   ВСТРОЕННЫМ стилем (left/top/width прямо в разметке), а встроенный стиль
   побеждает любое правило оформления. */
@media (max-width:900px){
  .wp-free[data-wp-pos="1"]{
    position:static;left:auto !important;top:auto !important;width:auto !important;
  }
  .wp-free-bar,.wp-free-grip{display:none}
}
.wp-text{display:flex;flex-direction:column;gap:12px}
.wp-text p,.wp-quote p{margin:0;color:var(--ink-2);line-height:1.72;font-size:1.02rem}
.wp-fig{display:flex;flex-direction:column;gap:9px}
.wp-fig .wp-img,.wp-fig .wp-video{border-radius:18px;border:1px solid rgba(120,160,220,.16)}
.wp-gallery{display:flex;flex-direction:column;gap:9px}

/* ---------- блок «Галерея» — лента-карусель, а не сетка ----------

   Владелец показал снимком ленту работ: «надо чтобы в админ панели при
   добавлении этого блока была полноценно настраиваемая карусель фотографий
   видео и гиф как например карусель из работ… без названия просто фото и
   видео и гиф в обычной карточке, при нажатии она увеличивалась в рамке и
   пролистывались были стрелки».

   Оболочка у галереи ОБЩАЯ с лентой работ (.carousel, .car-stage,
   .car-track, .car-arrow, .car-foot), а карточка — тот же .wr-card. Поэтому
   здесь нет ни одной новой геометрической цифры: правила вида (наклоны,
   ступени размера, свечения, движения) записаны ОДНИМ селектором на
   #portfolio и на .wr-roll (см. выше, «РАЗМЕР КАРТОЧКИ» и соседей).

   Отличий ровно два, и оба — про содержимое карточки, а не про её вид:
     • у карточки галереи НЕТ шапки (.wr-head): «без названия». Поэтому
       кадр занимает её целиком — .wr-media растянут на всю высоту
       (.wr-card — столбик, .wr-media — flex:1);
     • кадров в карточке ОДИН, а не стопка: листаются сами карточки.
       Поэтому .wr-slide стоит на месте (--k:0), и в разметке он один.

   Отрицательные поля сцены (-98/-108 при отступах 120/114) оставлены как
   есть, хотя .wp-body — флекс и поля его детей не схлопываются. Проверено
   расчётом и замером: без схлопывания коробка .carousel всё равно выходит
   равной дорожке плюс 28px, то есть лента встаёт ровно так же, как в
   разделе «Работы». Менять их «для чистоты» не нужно — это сдвинуло бы
   ленту на 98px. */
.wr-roll .wr-card{cursor:pointer}

/* Шапка блока и лента встают друг от друга на 22px дальше, чем у остальных
   блоков, — и это не «на глаз», а следствие того же раздутого отступа сцены
   (разбор выше, у .car-stage): её отрицательные поля оставляют 22px сверху.
   Вычитаем ровно эти 22px, чтобы лента не отставала от своей шапки сильнее
   прочих блоков. Столько же вычитает и .wp-head из своего нижнего отступа —
   по той же причине и с тем же числом. */
.wr-roll{margin-top:-22px}

/* Сетка файлов работы БЕЗ публикации (server.js: «карточка работы с её
   файлами»). Это не блок «Галерея» и не карусель: там нет ни владельца, ни
   настроек, и лента со снапом тут честнее — она работает без скрипта.
   Прокрутка идёт по САМОЙ ленте (.wp-grid), а не по обёртке: иначе браузер
   не знает, что прокручивать, и снап не срабатывает. */
.wp-grid{
  display:flex;gap:10px;margin:0;
  overflow-x:auto;overflow-y:hidden;
  scroll-snap-type:x mandatory;
  scroll-behavior:smooth;
  /* Полосу прокрутки не прячем: она честно говорит, что кадров больше,
     чем видно. `scrollbar-width:thin` отсюда убран — он отменял сайтовую
     полосу (см. разбор у `.pm-info`), и лента показывала родную тонкую
     вместо общей. */
  padding-bottom:6px;
}
.wp-cell{
  margin:0;flex:0 0 auto;width:min(340px,72vw);
  scroll-snap-align:center;
  border-radius:16px;overflow:hidden;background:#080d17;
  border:1px solid rgba(120,160,220,.16);
}
.wp-cell .wp-img,.wp-cell .wp-video{border-radius:0;max-height:none;aspect-ratio:4/3;object-fit:cover}

/* Кнопка «во весь экран» — сам кадр. padding:0 не украшение: у кнопки в
   Chrome свои отступы, и снимок встал бы на 6px внутрь рамки. */
.wp-zoom{
  display:block;width:100%;padding:0;margin:0;border:0;background:none;
  cursor:zoom-in;position:relative;
}
.wp-zoom img{display:block;width:100%}
/* Лупа в углу — единственная подсказка, что кадр нажимается: без неё
   курсор-лупа виден только при наведении мышью, а на телефоне его нет. */
.wp-zoom::after{
  content:'';position:absolute;top:8px;right:8px;width:26px;height:26px;
  border-radius:9px;background:rgba(6,10,20,.62);
  border:1px solid rgba(255,255,255,.22);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23eaf2ff' stroke-width='1.9' stroke-linecap='round'%3E%3Cpath d='M14 4h6v6M20 4l-7 7M10 20H4v-6M4 20l7-7'/%3E%3C/svg%3E");
  background-size:15px 15px;background-position:center;background-repeat:no-repeat;
  opacity:0;transition:opacity .2s var(--ease);
}
.wp-zoom:hover::after,.wp-zoom:focus-visible::after{opacity:1}
.wp-zoom:focus-visible{outline:2px solid var(--cyan);outline-offset:3px;border-radius:16px}

/* ---------- всплывашка во весь размер ----------
   Показывает тот же файл, что и кадр: отдельной копии картинки нет,
   поэтому и «не та картинка открылась» случиться не может. */
.wp-lb{
  position:fixed;inset:0;z-index:120;
  display:flex;align-items:center;justify-content:center;
  background:rgba(3,6,13,.93);backdrop-filter:blur(3px);
  padding:clamp(14px,4vw,48px);
}
.wp-lb[hidden]{display:none}
.wp-lb-media{max-width:100%;max-height:100%;display:block;border-radius:12px}
.wp-lb-media.wp-video{width:min(1100px,100%)}
.wp-lb-x,.wp-lb-prev,.wp-lb-next{
  position:absolute;z-index:2;border-radius:50%;padding:0;cursor:pointer;
  width:46px;height:46px;display:grid;place-items:center;
  border:1px solid rgba(120,160,220,.3);background:rgba(10,16,28,.86);
}
.wp-lb-x{top:14px;right:14px}
.wp-lb-prev{left:14px;top:50%;margin-top:-23px}
.wp-lb-next{right:14px;top:50%;margin-top:-23px}
.wp-lb-x::before,.wp-lb-prev::before,.wp-lb-next::before{
  content:'';display:block;border-right:2px solid var(--ink);border-bottom:2px solid var(--ink);
  width:11px;height:11px;
}
.wp-lb-x::before{transform:rotate(45deg);width:14px;height:14px}
.wp-lb-prev::before{transform:rotate(135deg) translate(-1px,-1px)}
.wp-lb-next::before{transform:rotate(-45deg) translate(-1px,-1px)}
.wp-lb-x:hover,.wp-lb-prev:hover,.wp-lb-next:hover{border-color:var(--cyan)}
.wp-lb-count{
  position:absolute;bottom:16px;left:50%;transform:translateX(-50%);
  font-size:.82rem;font-weight:700;color:var(--ink-2);letter-spacing:.06em;
}
.wp-lb[data-single="1"] .wp-lb-prev,
.wp-lb[data-single="1"] .wp-lb-next,
.wp-lb[data-single="1"] .wp-lb-count{display:none}

/* ---------- живая шапка сайта на публикации ----------
   На главной .nav стоит position:fixed и места в потоке НЕ занимает: там
   под него отведён padding-top у .hero. На странице публикации такого
   отступа нет, и содержимое уезжало ПОД шапку — в полосе шапки оказывались
   и название работы, и значок срока («1 день» налезал на кнопку корзины).

   Делаем шапку липкой: она занимает своё место в потоке и остаётся наверху
   при прокрутке. Это ровно то поведение, что у простой шапки публикации
   (.wp-nav), поэтому страница выглядит одинаково с галочкой и без неё.
   left/right обнуляем: у fixed-шапки они задавали растяжку по окну, а
   липкой они не нужны и только сбивали бы ширину. */
.wp-page > header.nav{
  position:sticky;top:0;left:auto;right:auto;
}

/* ---------- корзина без шапки ----------
   Кнопка корзины живёт ВНУТРИ шапки сайта. Владелец может включить корзину,
   не включая шапку, — и тогда окошко заказа на странице есть, а открыть его
   нечем. Сервер достаёт кнопку из шапки и ставит её этой полоской.

   Ширина и отступы — как у шапки: иначе кнопка уехала бы к самому краю окна
   и читалась бы как чужая. Полоска намеренно не липкая: липкая шапка уже
   есть, а две липкие полосы спорили бы за верх экрана. */
.wp-cartbar{
  display:flex;justify-content:flex-end;align-items:center;
  width:min(1200px,92vw);margin:14px auto 0;
}

/* ---------- живой раздел сайта ----------
   Живой раздел (врезка куска главной) раньше выпускался на всю ширину
   окна: его внутренний .wrap объявлен как min(1200px,92vw) и в колонке
   публикации в 780 точек не помещался — вылезал за край и обрезался.

   Полоса публикации стала той же ширины, что .wrap (см. пояснение у
   .wp-wrap), поэтому выпускать больше нечего: раздел занимает ровно
   полосу, а его .wrap считается от неё и тоже даёт 1200. Замерено на
   сайте владельца: до правки врезка была 1440 при полосе 780 — на одной
   странице две разные ширины, и он показал это снимком.

   Правило не удалено, а обнулено: класс .wp-embed стоит в разметке, и
   сторож на него смотрит. */
.wp-embed{margin-inline:0}
/* Правила .wp-embed-tag и .wp-site-tag убраны вместе с самими подписями:
   владелец (29.09) просил не показывать «Раздел сайта: …» на странице
   публикации — ни у врезки живого раздела, ни у карточки-ссылки на раздел.
   Оформление без разметки — мёртвый груз, который следующий читатель
   примет за живое. */
.wp-page figcaption{font-size:.8rem;color:var(--ink-3)}
.wp-quote{
  display:flex;flex-direction:column;gap:10px;
  padding:20px 22px;border-radius:18px;
  border-left:3px solid var(--magenta);background:rgba(255,79,216,.05);
}
.wp-quote cite{font-style:normal;font-size:.82rem;font-weight:700;color:var(--magenta)}
.wp-list{margin:0;padding-left:22px;display:flex;flex-direction:column;gap:9px;color:var(--ink-2);line-height:1.6}
.wp-list li::marker{color:var(--cyan)}
.wp-spec{display:flex;flex-direction:column;margin:0;border-radius:18px;overflow:hidden;border:1px solid rgba(120,160,220,.16)}
.wp-spec-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.4fr);gap:14px;padding:12px 16px;border-top:1px solid rgba(120,160,220,.12)}
.wp-spec-row:first-child{border-top:0}
.wp-spec-row:nth-child(odd){background:rgba(255,255,255,.02)}
.wp-spec dt{margin:0;font-size:.86rem;color:var(--ink-3)}
.wp-spec dd{margin:0;font-size:.92rem;font-weight:600;color:var(--ink)}
.wp-cta{display:flex;flex-direction:column;align-items:flex-start;gap:10px}
.wp-btn{
  display:inline-flex;align-items:center;gap:9px;
  padding:13px 24px;border-radius:999px;text-decoration:none;
  font-weight:800;font-size:.94rem;color:#04060c;
  background:linear-gradient(120deg,var(--cyan),var(--violet));
  transition:transform .25s var(--ease),box-shadow .25s var(--ease);
}
.wp-btn:hover{transform:translateY(-2px);box-shadow:0 14px 34px rgba(34,230,255,.28)}
.wp-cta-note{margin:0;font-size:.82rem;color:var(--ink-3)}
.wp-site{
  display:flex;flex-direction:column;gap:6px;text-decoration:none;
  padding:20px 22px;border-radius:18px;
  border:1px solid rgba(34,230,255,.24);background:rgba(34,230,255,.05);
  transition:border-color .25s var(--ease),transform .25s var(--ease);
}
.wp-site:hover{border-color:rgba(34,230,255,.5);transform:translateY(-2px)}
.wp-site b{font-size:1.06rem;color:var(--ink)}
.wp-site-text{font-size:.9rem;color:var(--ink-2);line-height:1.6}
.wp-site-go{margin-top:4px;font-size:.84rem;font-weight:700;color:var(--cyan)}
.wp-back{margin:34px 0 0;font-size:.88rem}
.wp-back a{color:var(--ink-3);text-decoration:none;transition:color .25s var(--ease)}
.wp-back a:hover{color:var(--cyan)}
.wp-foot{padding:26px 0 34px;text-align:center;font-size:.8rem;color:var(--ink-3);border-top:1px solid rgba(120,160,220,.1)}
.wp-foot p{margin:0}

@media (max-width:560px){
  .wp-wrap{padding:24px 0 48px}
  .wp-hero-top{flex-wrap:wrap}
  .wp-hero-who b{font-size:1.1rem}
  .wp-ava{width:44px;height:44px}
  .wp-term{margin-left:0;order:-1}
  .wp-spec-row{grid-template-columns:1fr;gap:4px}
}

/* ============================================================
   Оформленный текст (редактор в админ-панели)
   ------------------------------------------------------------
   Класс rich-body ставит js/main.js на тот элемент, в который
   положили текст из редактора: политика конфиденциальности, ответ в
   «Вопросах», описание товара, описание блока. Один набор правил на
   четыре разных места — иначе списки и таблицы пришлось бы описывать
   четырежды, и три описания из четырёх разошлись бы.

   Отступы АБЗАЦЕВ здесь намеренно не заданы. У каждого места они
   свои (.pv-body p, .pm-desc p, .q .ans p), и общее правило подралось
   бы с ними: ответ в «Вопросах» получил бы 22px отступа от своего
   правила плюс ещё столько же от общего. Поэтому общие правила
   описывают только то, чего в оформлении ещё не было: списки, цитаты,
   код, таблицы, картинки, линии и ссылки.
   ============================================================ */
.rich-body h1,
.rich-body h2,
.rich-body h3,
.rich-body h5,
.rich-body h6{
  margin:20px 0 8px;font-weight:700;letter-spacing:-.01em;
  color:var(--ink);line-height:1.25;
}
/* Заголовок третьего уровня и ниже — h4 — уже описан там, где
   показывается политика (.pv-body h4), и второй раз его задавать
   нельзя: правила разошлись бы по размеру. */
.rich-body h1{font-size:1.42rem}
.rich-body h2{font-size:1.28rem}
.rich-body h3{font-size:1.14rem}
.rich-body h5{font-size:.98rem}
.rich-body h6{font-size:.9rem;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-2)}
/* Первый и последний элемент не должны «отклеиваться» от края. */
.rich-body > :first-child{margin-top:0}
.rich-body > :last-child{margin-bottom:0}

.rich-body ul,
.rich-body ol{margin:0 0 12px;padding-left:1.5em}
.rich-body ul{list-style:disc}
.rich-body ol{list-style:decimal}
.rich-body li{margin:0 0 6px}
.rich-body li::marker{color:var(--cyan)}
.rich-body ul ul,
.rich-body ol ol,
.rich-body ul ol,
.rich-body ol ul{margin-bottom:4px}

.rich-body blockquote{
  margin:14px 0;padding:4px 0 4px 16px;
  border-left:2px solid var(--line-strong);
  color:var(--ink-2);font-style:italic;
}

.rich-body code{
  font-family:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:.88em;padding:2px 6px;border-radius:6px;
  background:rgba(120,160,220,.12);border:1px solid var(--line);
  color:var(--ink);
}
.rich-body pre{
  margin:0 0 12px;padding:14px 16px;overflow:auto;
  border-radius:var(--r-sm);border:1px solid var(--line);
  background:rgba(4,6,12,.55);color:var(--ink-2);
  font-family:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:.84rem;line-height:1.55;
}
.rich-body pre code{padding:0;border:0;background:none;font-size:inherit;color:inherit}

.rich-body a{
  color:var(--cyan);text-decoration:underline;
  text-decoration-color:rgba(34,230,255,.42);text-underline-offset:3px;
  transition:text-decoration-color .25s var(--ease);
}
.rich-body a:hover{text-decoration-color:var(--cyan)}

.rich-body hr{margin:20px 0;border:0;height:1px;background:var(--line-strong)}
.rich-body img{
  max-width:100%;height:auto;display:block;margin:14px 0;
  border-radius:var(--r-sm);
}
.rich-body mark{
  background:rgba(255,209,102,.26);color:inherit;
  padding:1px 4px;border-radius:4px;
}
.rich-body sub,.rich-body sup{font-size:.72em;line-height:0}

/* Таблица. Ячейкам разрешён перенос по любому месту: описание с
   длинным словом или адресом иначе выдавило бы таблицу за край
   колонки, и текст уехал бы под соседний блок. */
.rich-body table{width:100%;margin:14px 0;border-collapse:collapse;font-size:.92rem}
.rich-body th,
.rich-body td{
  padding:9px 12px;border:1px solid var(--line);
  text-align:left;vertical-align:top;
  word-break:break-word;overflow-wrap:anywhere;
}
.rich-body th{background:rgba(120,160,220,.09);font-weight:600;color:var(--ink)}
.rich-body caption{
  margin:0 0 8px;text-align:left;font-size:.86rem;color:var(--ink-3);
}

.rich-body dl{margin:0 0 12px}
.rich-body dt{font-weight:600;color:var(--ink)}
.rich-body dd{margin:0 0 8px 1.2em;color:var(--ink-2)}
.rich-body figure{margin:14px 0}
.rich-body figcaption{margin-top:6px;font-size:.84rem;color:var(--ink-3)}

/* ---------- Анимированные смайлики редактора ----------

   Владелец: «Также в редактор добавь анимированные смайлики».

   Значок остаётся ЗНАКОМ, а не картинкой: движение висит на обычном
   `<span class="rc-a rc-a-fire">🔥</span>`, и такой значок переживает и
   санитайзер, и сохранение, и копирование в чужое окно. Картинку-гифку
   пришлось бы грузить, хранить и чистить отдельно, а в тексте она
   смотрелась бы вставкой из другого документа.

   Класс, а не `style`: у `span` разрешён `class` (и `data-rc-*`), а
   `animation` в списке разрешённых свойств санитайзера нет намеренно —
   иначе через поле ввода можно было бы протащить любое оформление. Значит,
   и вид, и движение обязаны жить в оформлении.

   ПОЧЕМУ БЛОК ЛЕЖИТ В ДВУХ ФАЙЛАХ. Значок показывается в трёх местах:
   кнопки палитры, поле редактора и текст на сайте. Панель грузит только
   admin.css, сайт — только style.css, общего файла оформления у них нет.
   Поэтому блок продублирован дословно, а `test-editor.js` СВЕРЯЕТ обе
   копии: список классов и их движения обязан совпасть. Без этой сверки
   новая анимация работала бы в палитре и молча не работала на сайте.

   Ни одно движение не трогает `color`: значок — это буква, и цвет ей
   владелец задаёт сам, выделяя текст. `filter`, `opacity`, масштаб и
   поворот цвета не перебивают. */
.rc-a{display:inline-block}
@keyframes rcAFire{
  0%,100%{transform:scale(1) rotate(-3deg)}
  50%{transform:scale(1.14) rotate(3deg)}
}
.rc-a-fire{animation:rcAFire 1.4s ease-in-out infinite}
@keyframes rcABeat{
  0%,55%,100%{transform:scale(1)}
  15%{transform:scale(1.2)}
  30%{transform:scale(.94)}
  45%{transform:scale(1.08)}
}
.rc-a-beat{animation:rcABeat 1.6s ease-in-out infinite}
@keyframes rcASpin{to{transform:rotate(360deg)}}
.rc-a-spin{animation:rcASpin 2.6s linear infinite}
@keyframes rcATwinkle{
  0%,100%{transform:scale(1);opacity:1}
  50%{transform:scale(.78);opacity:.45}
}
.rc-a-twinkle{animation:rcATwinkle 1.8s ease-in-out infinite}
@keyframes rcABoom{
  0%,60%,100%{transform:scale(1)}
  20%{transform:scale(1.35)}
  35%{transform:scale(.9)}
}
.rc-a-boom{animation:rcABoom 2s ease-out infinite}
@keyframes rcAParty{
  0%,100%{transform:translateY(0) rotate(0)}
  30%{transform:translateY(-6px) rotate(-9deg)}
  60%{transform:translateY(0) rotate(0)}
  80%{transform:translateY(-3px) rotate(7deg)}
}
.rc-a-party{animation:rcAParty 1.9s ease-in-out infinite}
@keyframes rcABalloon{
  0%,100%{transform:translateY(0)}
  50%{transform:translateY(-7px)}
}
.rc-a-balloon{animation:rcABalloon 2.6s ease-in-out infinite}
@keyframes rcAFlash{
  0%,44%,56%,72%,100%{opacity:1}
  50%{opacity:.25}
  78%{opacity:.4}
}
.rc-a-flash{animation:rcAFlash 2.2s linear infinite}
@keyframes rcAPulse{
  0%,100%{transform:scale(1)}
  50%{transform:scale(1.22)}
}
.rc-a-pulse{animation:rcAPulse 1.3s ease-in-out infinite}
@keyframes rcAWave{
  0%,100%{transform:rotate(0)}
  25%{transform:rotate(16deg)}
  75%{transform:rotate(-16deg)}
}
.rc-a-wave{animation:rcAWave 1.5s ease-in-out infinite}
@keyframes rcAShake{
  0%,100%{transform:translateX(0) rotate(0)}
  20%{transform:translateX(-3px) rotate(-6deg)}
  40%{transform:translateX(3px) rotate(6deg)}
  60%{transform:translateX(-2px) rotate(-4deg)}
  80%{transform:translateX(2px) rotate(4deg)}
}
.rc-a-shake{animation:rcAShake 1.7s ease-in-out infinite}
@keyframes rcASwing{
  0%,100%{transform:rotate(-12deg)}
  50%{transform:rotate(12deg)}
}
.rc-a-swing{animation:rcASwing 2s ease-in-out infinite;transform-origin:top center}
@keyframes rcADance{
  0%,100%{transform:translateY(0) skewX(0)}
  25%{transform:translateY(-4px) skewX(-12deg)}
  75%{transform:translateY(-4px) skewX(12deg)}
}
.rc-a-dance{animation:rcADance 1.6s ease-in-out infinite}
@keyframes rcADrip{
  0%,70%,100%{transform:translateY(0) scaleY(1)}
  35%{transform:translateY(3px) scaleY(1.18)}
}
.rc-a-drip{animation:rcADrip 2.4s ease-in-out infinite}
@keyframes rcARocket{
  0%,100%{transform:translate(0,0) rotate(0)}
  45%{transform:translate(4px,-7px) rotate(6deg)}
}
.rc-a-rocket{animation:rcARocket 2.8s ease-in-out infinite}
@keyframes rcARainbow{to{filter:hue-rotate(360deg)}}
.rc-a-rainbow{animation:rcARainbow 4s linear infinite}
/* Второй десяток (просьба владельца: «В текстовый редактор везде добавить
   больше анимированных смайлов»).

   Движения выбраны так, чтобы НЕ повторяться: у каждого значка своя
   причина шевелиться — посылка подпрыгивает, шестерня вращается, корона
   покачивается, колокольчик звенит. Два одинаковых движения в палитре
   владелец не различит, и выбор превратится в угадывание: он будет
   нажимать значки, пока не найдёт «то самое», а «то самое» уже стоит
   рядом под другим именем.

   Поэтому и имена ключевых кадров у каждого свои (rcAOrbit, rcARing …), а
   не переиспользованные: одно имя на два значка однажды разъедется — у
   одного из них движение поправят, а второй поедет следом. */
@keyframes rcAOrbit{
  0%{transform:rotate(0) scale(1)}
  50%{transform:rotate(180deg) scale(1.16)}
  100%{transform:rotate(360deg) scale(1)}
}
.rc-a-orbit{animation:rcAOrbit 3.4s ease-in-out infinite}
@keyframes rcARing{
  0%,100%{transform:rotate(0)}
  10%{transform:rotate(20deg)}
  25%{transform:rotate(-18deg)}
  40%{transform:rotate(13deg)}
  55%{transform:rotate(-9deg)}
  70%{transform:rotate(5deg)}
}
.rc-a-ring{animation:rcARing 2.4s ease-in-out infinite;transform-origin:top center}
@keyframes rcAFuse{
  0%,100%{transform:translateX(0) scale(1)}
  20%{transform:translateX(-2px) scale(.94)}
  40%{transform:translateX(2px) scale(1.06)}
  60%{transform:translateX(-1px) scale(.97)}
  80%{transform:translateX(1px) scale(1.03)}
}
.rc-a-fuse{animation:rcAFuse 1.2s linear infinite}
@keyframes rcALift{
  0%,100%{transform:translateY(0)}
  50%{transform:translateY(-6px)}
}
.rc-a-lift{animation:rcALift 1.8s ease-in-out infinite}
@keyframes rcASwipe{
  0%,100%{transform:translateX(0) skewX(0)}
  30%{transform:translateX(-5px) skewX(8deg)}
  70%{transform:translateX(5px) skewX(-8deg)}
}
.rc-a-swipe{animation:rcASwipe 2s ease-in-out infinite}
@keyframes rcACheck{
  0%,100%{transform:scale(1) rotate(0)}
  35%{transform:scale(1.3) rotate(-8deg)}
  55%{transform:scale(1.02) rotate(4deg)}
}
.rc-a-check{animation:rcACheck 1.9s ease-out infinite}
@keyframes rcAGear{to{transform:rotate(-360deg)}}
.rc-a-gear{animation:rcAGear 3.2s linear infinite}
@keyframes rcADrive{
  0%,100%{transform:translateX(-4px)}
  50%{transform:translateX(4px)}
}
.rc-a-drive{animation:rcADrive 2.2s ease-in-out infinite}
@keyframes rcAMoney{
  0%,100%{transform:translateY(0) scaleY(1)}
  30%{transform:translateY(-7px) scaleY(1.06)}
  50%{transform:translateY(0) scaleY(.94)}
  70%{transform:translateY(-3px) scaleY(1.02)}
}
.rc-a-money{animation:rcAMoney 1.7s ease-in-out infinite}
@keyframes rcACrown{
  0%,100%{transform:rotate(-7deg)}
  50%{transform:rotate(7deg)}
}
.rc-a-crown{animation:rcACrown 2.8s ease-in-out infinite;transform-origin:bottom center}
@keyframes rcAShine{
  0%,100%{transform:scale(1);filter:brightness(1)}
  45%{transform:scale(1.12);filter:brightness(1.5)}
}
.rc-a-shine{animation:rcAShine 2.1s ease-in-out infinite}
@keyframes rcALuck{
  0%,100%{transform:rotate(-10deg) scale(1)}
  50%{transform:rotate(10deg) scale(1.08)}
}
.rc-a-luck{animation:rcALuck 2.3s ease-in-out infinite}
@keyframes rcAHit{
  0%,100%{transform:scale(1)}
  25%{transform:scale(.82)}
  45%{transform:scale(1.18)}
}
.rc-a-hit{animation:rcAHit 1.6s ease-out infinite}
@keyframes rcASand{
  0%,100%{transform:scaleX(1)}
  50%{transform:scaleX(-1)}
}
.rc-a-sand{animation:rcASand 3s ease-in-out infinite}
@keyframes rcAGuard{
  0%,100%{transform:scale(1) rotate(0)}
  50%{transform:scale(1.1) rotate(-3deg)}
}
.rc-a-guard{animation:rcAGuard 2.5s ease-in-out infinite}
@keyframes rcASnap{
  0%,72%,100%{opacity:1;transform:scale(1)}
  78%{opacity:.35;transform:scale(.94)}
  86%{opacity:1;transform:scale(1.05)}
  92%{opacity:.6}
}
.rc-a-snap{animation:rcASnap 2.7s linear infinite}
/* Третий и четвёртый десяток (просьба владельца: «добавь в текстовый
   редактор ещё больше анимированных эмодзи — разные стрелки, плюсики и
   минусы, галочки и точки и ещё много разных»).

   Значки лежат на ДВУХ полках: «Стрелки и знаки» — ровно то, что владелец
   назвал, «Ещё движения» — остальные. В одной сетке из семи десятков
   значков нужный ищут глазами по всему полю, на двух полках выбор короче.

   Двух ОДИНАКОВЫХ тел в блоке нет: у каждого класса своё имя ключевых
   кадров И свои кадры. Иначе владелец нажимал бы разные значки, а получал
   одно и то же движение, и «добавили ещё» на деле не добавило бы ничего.
   За этим следит test-editor.js — он сверяет и имена, и сами кадры. */
@keyframes rcAArrowRight{
  0%,100%{transform:translateX(0)}
  50%{transform:translateX(7px)}
}
.rc-a-arrowright{animation:rcAArrowRight 1.5s ease-in-out infinite}
@keyframes rcAArrowLeft{
  0%,100%{transform:translateX(0)}
  50%{transform:translateX(-7px)}
}
.rc-a-arrowleft{animation:rcAArrowLeft 1.5s ease-in-out infinite}
@keyframes rcAArrowUp{
  0%,100%{transform:translateY(0)}
  40%{transform:translateY(-8px)}
  70%{transform:translateY(-2px)}
}
.rc-a-arrowup{animation:rcAArrowUp 1.5s ease-in-out infinite}
@keyframes rcAArrowDown{
  0%,100%{transform:translateY(0)}
  40%{transform:translateY(8px)}
  70%{transform:translateY(2px)}
}
.rc-a-arrowdown{animation:rcAArrowDown 1.5s ease-in-out infinite}
@keyframes rcADiagUp{
  0%,100%{transform:translate(0,0)}
  50%{transform:translate(6px,-6px)}
}
.rc-a-diagup{animation:rcADiagUp 1.7s ease-in-out infinite}
@keyframes rcADiagDown{
  0%,100%{transform:translate(0,0)}
  50%{transform:translate(6px,6px)}
}
.rc-a-diagdown{animation:rcADiagDown 1.7s ease-in-out infinite}
@keyframes rcARefresh{
  0%{transform:rotate(0)}
  55%,100%{transform:rotate(360deg)}
}
.rc-a-refresh{animation:rcARefresh 2.4s ease-in-out infinite}
@keyframes rcARecycle{
  0%{transform:rotate(0) scale(1)}
  50%{transform:rotate(180deg) scale(.84)}
  100%{transform:rotate(360deg) scale(1)}
}
.rc-a-recycle{animation:rcARecycle 3.6s ease-in-out infinite}
@keyframes rcAPlus{
  0%,100%{transform:scale(1)}
  40%{transform:scale(1.36)}
  70%{transform:scale(.94)}
}
.rc-a-plus{animation:rcAPlus 1.4s ease-in-out infinite}
@keyframes rcAMinus{
  0%,100%{transform:scaleX(1)}
  50%{transform:scaleX(.42)}
}
.rc-a-minus{animation:rcAMinus 1.6s ease-in-out infinite}
@keyframes rcATick{
  0%{transform:translateX(-9px) scale(.6);opacity:0}
  45%{transform:translateX(0) scale(1.08);opacity:1}
  100%{transform:translateX(0) scale(1);opacity:1}
}
.rc-a-tick{animation:rcATick 2s ease-out infinite}
@keyframes rcABoxCheck{
  0%{transform:scale(1.5);opacity:0}
  25%{transform:scale(.92);opacity:1}
  40%{transform:scale(1.04)}
  100%{transform:scale(1)}
}
.rc-a-boxcheck{animation:rcABoxCheck 2.2s ease-out infinite}
@keyframes rcACross{
  0%,100%{transform:rotate(0) scale(1)}
  50%{transform:rotate(-19deg) scale(.93)}
}
.rc-a-cross{animation:rcACross 1.8s ease-in-out infinite}
@keyframes rcARingDot{
  0%,100%{transform:scale(1);opacity:.9}
  50%{transform:scale(1.4);opacity:.35}
}
.rc-a-ringdot{animation:rcARingDot 2.1s ease-in-out infinite}
@keyframes rcADotRed{
  0%,100%{transform:scale(.7);opacity:.5}
  50%{transform:scale(1.2);opacity:1}
}
.rc-a-dotred{animation:rcADotRed 1.3s ease-in-out infinite}
@keyframes rcADotGreen{
  0%,100%{transform:translateY(0) scale(1)}
  45%{transform:translateY(-5px) scale(1.08)}
}
.rc-a-dotgreen{animation:rcADotGreen 1.5s ease-in-out infinite}
@keyframes rcADotBlue{
  0%,100%{transform:skewX(0)}
  50%{transform:skewX(24deg)}
}
.rc-a-dotblue{animation:rcADotBlue 1.9s ease-in-out infinite}
@keyframes rcADotYellow{
  0%,100%{transform:scale(1)}
  25%{transform:scale(.62)}
  45%{transform:scale(1.12)}
  65%{transform:scale(.8)}
}
.rc-a-dotyellow{animation:rcADotYellow 1.7s ease-in-out infinite}
@keyframes rcAWarn{
  0%,100%{transform:rotate(0) translateY(0)}
  20%{transform:rotate(-12deg) translateY(-2px)}
  50%{transform:rotate(11deg) translateY(0)}
  75%{transform:rotate(-6deg) translateY(-1px)}
}
.rc-a-warn{animation:rcAWarn 2.1s ease-in-out infinite}
@keyframes rcAExcl{
  0%,100%{transform:translateY(0) scaleY(1)}
  35%{transform:translateY(-7px) scaleY(1.08)}
  60%{transform:translateY(0) scaleY(.92)}
}
.rc-a-excl{animation:rcAExcl 1.6s ease-out infinite}
@keyframes rcAQuest{
  0%{transform:rotate(0)}
  35%,65%{transform:rotate(-14deg)}
  100%{transform:rotate(0)}
}
.rc-a-quest{animation:rcAQuest 2.6s ease-in-out infinite}
@keyframes rcAZoom{
  0%,100%{transform:translate(0,0) scale(1)}
  30%{transform:translate(-4px,3px) scale(1.2)}
  65%{transform:translate(3px,-2px) scale(1.08)}
}
.rc-a-zoom{animation:rcAZoom 2.3s ease-in-out infinite}
@keyframes rcAPin{
  0%,100%{transform:translateY(0) rotate(0)}
  40%{transform:translateY(-6px) rotate(-11deg)}
  70%{transform:translateY(0) rotate(4deg)}
}
.rc-a-pin{animation:rcAPin 1.9s ease-in-out infinite}
@keyframes rcALink{
  0%,100%{transform:rotate(0) scale(1)}
  35%{transform:rotate(-16deg) scale(.95)}
  65%{transform:rotate(6deg) scale(1.04)}
}
.rc-a-link{animation:rcALink 2.2s ease-in-out infinite}
@keyframes rcAConfetti{
  0%,100%{transform:translate(0,0) rotate(0)}
  25%{transform:translate(-3px,-4px) rotate(-11deg)}
  50%{transform:translate(3px,1px) rotate(9deg)}
  75%{transform:translate(-2px,-2px) rotate(-6deg)}
}
.rc-a-confetti{animation:rcAConfetti 2.4s ease-in-out infinite}
@keyframes rcAMedal{
  0%,100%{transform:translateY(0) rotate(0) scale(1)}
  35%{transform:translateY(-5px) rotate(-8deg) scale(1.12)}
  65%{transform:translateY(0) rotate(5deg) scale(1)}
}
.rc-a-medal{animation:rcAMedal 2s ease-in-out infinite}
@keyframes rcATrophy{
  0%,100%{transform:scale(1)}
  30%{transform:scale(1.16)}
  55%{transform:scale(1.02)}
  75%{transform:scale(1.09)}
}
.rc-a-trophy{animation:rcATrophy 2.5s ease-in-out infinite}
@keyframes rcAMagnet{
  0%,100%{transform:translateX(0) rotate(0)}
  30%{transform:translateX(5px) rotate(4deg)}
  65%{transform:translateX(-5px) rotate(-4deg)}
}
.rc-a-magnet{animation:rcAMagnet 2.7s ease-in-out infinite}
@keyframes rcAClock{
  0%,100%{transform:rotate(0)}
  50%{transform:rotate(15deg)}
}
.rc-a-clock{animation:rcAClock 3s ease-in-out infinite}
@keyframes rcAChartUp{
  0%,100%{transform:translateY(0) scale(1)}
  50%{transform:translateY(-6px) scale(1.1)}
}
.rc-a-chartup{animation:rcAChartUp 2s ease-in-out infinite}
@keyframes rcAChartDown{
  0%,100%{transform:translateY(0) scale(1)}
  50%{transform:translateY(6px) scale(.92)}
}
.rc-a-chartdown{animation:rcAChartDown 2s ease-in-out infinite}
@keyframes rcAReceipt{
  0%,100%{transform:rotate(0) skewY(0)}
  50%{transform:rotate(3deg) skewY(-6deg)}
}
.rc-a-receipt{animation:rcAReceipt 2.6s ease-in-out infinite}
@keyframes rcATag{
  0%,100%{transform:rotate(0)}
  25%{transform:rotate(-18deg)}
  50%{transform:rotate(0)}
  75%{transform:rotate(18deg)}
}
.rc-a-tag{animation:rcATag 2.2s ease-in-out infinite}
@keyframes rcABow{
  0%,100%{transform:scale(1) rotate(0)}
  50%{transform:scale(1.14) rotate(0)}
}
.rc-a-bow{animation:rcABow 1.9s ease-in-out infinite}
@keyframes rcAGlow{
  0%,100%{transform:scale(1);opacity:.75}
  50%{transform:scale(1.18);opacity:1}
}
.rc-a-glow{animation:rcAGlow 2.3s ease-in-out infinite}
@keyframes rcAIdea{
  0%,100%{transform:translateY(0) rotate(0)}
  30%{transform:translateY(-7px) rotate(-6deg)}
  55%{transform:translateY(-2px) rotate(3deg)}
}
.rc-a-idea{animation:rcAIdea 2.1s ease-in-out infinite}
@keyframes rcALock{
  0%,100%{transform:scaleY(1)}
  40%{transform:scaleY(.86)}
  70%{transform:scaleY(1.05)}
}
.rc-a-lock{animation:rcALock 2s ease-out infinite}
@keyframes rcAKey{
  0%,100%{transform:rotate(0) scale(1)}
  30%{transform:rotate(20deg) scale(1.06)}
  60%{transform:rotate(-12deg) scale(1)}
}
.rc-a-key{animation:rcAKey 2.4s ease-in-out infinite}
@keyframes rcACart{
  0%,100%{transform:translateX(0)}
  30%{transform:translateX(-5px)}
  60%{transform:translateX(5px)}
  85%{transform:translateX(-2px)}
}
.rc-a-cart{animation:rcACart 2.2s ease-in-out infinite}
@keyframes rcACall{
  0%,100%{transform:rotate(0)}
  15%{transform:rotate(-16deg)}
  30%{transform:rotate(14deg)}
  45%{transform:rotate(-10deg)}
  60%{transform:rotate(6deg)}
  75%{transform:rotate(-3deg)}
}
.rc-a-call{animation:rcACall 2.6s ease-in-out infinite}
/* Кому движения мешают — тому их не показываем. Правило стоит ПОСЛЕ
   остальных: у `.rc-a` и `.rc-a-fire` одинаковая точность (один класс),
   значит выигрывает то, что ниже. */
@media (prefers-reduced-motion:reduce){
  .rc-a{animation:none}
}

/* ---------- карусель внутри текста (js/main.js, buildTextCarousel) ----------

   Кадры едут вбок. Дорожка — flex-строка, каждый кадр занимает ровно
   свою долю (100%), поэтому сдвиг считается в процентах от ширины, без
   замеров в точках: при повороте телефона и смене ширины окна карусель
   остаётся на том же кадре, а не уезжает вбок.

   touch-action:pan-y — не мелочь. Без него наше перетаскивание съело бы
   вертикальную прокрутку страницы, и владелец, водя пальцем по
   фотографии, не смог бы прокрутить страницу вниз.

   Высота в точках, а не долей ширины: у видео своё соотношение сторон,
   и доля его растянула бы. Потолок в 70vh нужен для телефона: кадр
   высотой в триста сорок точек там выше самого экрана. */
.rich-carousel{
  position:relative;
  margin:18px 0;
  --rc-h:340px;
  --rc-fit:cover;
}
.rich-carousel:focus-visible{outline:none;box-shadow:0 0 0 3px rgba(34,230,255,.22);border-radius:18px}
.rc-track{
  display:flex;
  height:var(--rc-h);
  max-height:70vh;
  border-radius:18px;
  overflow:hidden;
  background:rgba(9,13,23,.55);
  transition:transform .55s cubic-bezier(.22,.61,.36,1);
  touch-action:pan-y;
  will-change:transform;
}
.rc-track.rc-drag{transition:none;cursor:grabbing}
.rc-track.rc-notrans{transition:none}
.rc-slide{
  flex:0 0 100%;
  width:100%;
  min-width:100%;
  margin:0;
  position:relative;
  display:flex;
  align-items:center;
  justify-content:center;
  overflow:hidden;
}
.rc-slide img,
.rc-slide video{
  width:100%;height:100%;object-fit:var(--rc-fit);display:block;
  margin:0;border-radius:0;
  /* Перетаскивать кадр и выделять его в карусели незачем. Перетаска
     отбирает жест у листания — браузер начинает своё «перетащи
     картинку» и присылает pointercancel вместо pointerup (см. js/main.js,
     обработчик dragstart). Выделение мажет подсветкой по фотографии,
     пока по ней ведут пальцем. */
  -webkit-user-drag:none;
  user-select:none;
}
.rc-slide figcaption{
  position:absolute;left:0;right:0;bottom:0;margin:0;
  padding:26px 16px 12px;
  background:linear-gradient(to top,rgba(4,6,12,.86),rgba(4,6,12,0));
  color:var(--ink);font-size:.86rem;
}
/* Подпись не должна налезать на точки: место под них оставляем заранее,
   а не подгоняем по высоте подписи — та зависит от длины текста. */
.rich-carousel.rc-has-dots .rc-slide figcaption{padding-bottom:38px}

.rc-arrow{
  position:absolute;top:50%;transform:translateY(-50%);
  width:42px;height:42px;padding:0;
  display:flex;align-items:center;justify-content:center;
  border:1px solid var(--line-strong);border-radius:50%;
  background:rgba(4,6,12,.62);color:var(--ink);
  cursor:pointer;z-index:3;
  -webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);
  transition:background .2s var(--ease),border-color .2s var(--ease),opacity .2s var(--ease);
}
.rc-arrow svg{width:22px;height:22px}
.rc-arrow:hover{background:rgba(34,230,255,.2);border-color:rgba(34,230,255,.5)}
.rc-prev{left:12px}
.rc-next{right:12px}
.rc-next svg{transform:rotate(180deg)}
/* На краю без петли стрелка гаснет. Выключенная стрелка честнее
   исчезнувшей: владелец видит, что листание есть, и понимает, почему
   оно сейчас не идёт. */
.rc-arrow:disabled{opacity:.3;cursor:default}
.rc-arrow:disabled:hover{background:rgba(4,6,12,.62);border-color:var(--line-strong)}

.rc-dots{
  position:absolute;left:0;right:0;bottom:13px;z-index:3;
  display:flex;align-items:center;justify-content:center;gap:7px;
  pointer-events:none;
}
.rc-dot{
  width:8px;height:8px;padding:0;
  border:1px solid rgba(234,242,255,.45);border-radius:50%;
  background:transparent;cursor:pointer;pointer-events:auto;
  transition:background .25s var(--ease),width .25s var(--ease);
}
.rc-dot.on{background:var(--cyan);border-color:var(--cyan);width:20px;border-radius:4px}

@media (prefers-reduced-motion:reduce){
  .rc-track{transition:none}
}

/* ---------- карусель карточками (data-rc-mode="cards") ----------

   Владелец: «сделать так чтобы фото и видео и гиф анимации добавлялись
   как карточки работ что то подобное чтобы я мог редактировать там размер
   карточек анимацию и эффекты».

   Отличие от широкой карусели одно: кадры не на всю ширину, а карточками
   заданной ширины, и дорожку двигает её СОБСТВЕННАЯ прокрутка (scrollLeft),
   а не подстановка transform. Так у листания есть прилипание к карточке и
   не нужен свой код протяжки за пальцем.

   touch-action:pan-y — то же правило, что у широкой дорожки, и оно тут
   НУЖНО, а не мешает. Браузеру оставляем вертикаль (страница листается
   пальцем как обычно), а горизонталь забираем себе: решение принимает
   наш свайп в js/main.js — одно движение, один кадр.

   Так вышло не сразу. Сначала здесь стояло touch-action:auto, и весь
   горизонтальный жест отдавался браузеру: он прокручивал дорожку сам.
   Пальцем это работало, а МЫШЬЮ — нет вовсе: родная прокрутка на протяжку
   курсором не отвечает (замерено: протяжка 150, 300 и 500 точек — сдвиг
   0). Владелец (02.10): «они сейчас не листаються пальцем или не
   свайпаються… проводить пальцем или курсором». Плюс у карточки шириной
   600 точек короткий мах откатывался назад по прилипанию, и это тоже
   выглядело как «не листается».

   Ширина карточки и её высота приходят из настроек редактора: ширина —
   переменной --rc-card, высота — той же --rc-h, что у широкого кадра. */
.rich-carousel.rc-cards{padding-bottom:30px}
.rich-carousel.rc-cards .rc-track{
  height:auto;max-height:none;
  background:none;border-radius:0;
  gap:14px;
  padding:6px 2px 8px;
  overflow-x:auto;overflow-y:hidden;
  overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory;
  scroll-behavior:smooth;
  touch-action:pan-y;
  transform:none;
  transition:none;
  /* will-change снимаем вслед за преобразованием: дорожка больше не
     двигается слоем, и просить под неё отдельный слой незачем. */
  will-change:auto;
  /* Полоса прокрутки — САЙТОВАЯ, и обе строки отсюда убраны вместе.

     `scrollbar-width` и `scrollbar-color` заставляют Chrome игнорировать
     `::-webkit-scrollbar-*` (разбор — у `.pm-info`), а здесь это было
     особенно обидно: парой правил ниже дорожка САМА описывала свою полосу —
     8px и свою ручку. Тот вид не показывался ни разу. Владелец (06.10):
     «прокрутка не соотвествует стилю сайта везде». Теперь дорожка берёт
     общую полосу сайта — тот же вид, что у страницы, корзины и окна товара. */
  /* Ручка: дорожку можно тянуть — и курсор об этом говорит. Без неё
     владелец не догадывается, что карточки листаются протяжкой. */
  cursor:grab;
}
/* Пока тянут — рука сжата. Класс ставит и снимает свайп (js/main.js). */
.rich-carousel.rc-cards .rc-track.rc-grab{cursor:grabbing;user-select:none}
/* Своя полоса у дорожки карточек убрана вместе с `scrollbar-width` выше.

   Здесь стояли `::-webkit-scrollbar{height:8px}` и своя ручка. Пока у
   дорожки было `scrollbar-width:thin`, Chrome эти правила ИГНОРИРОВАЛ
   целиком — то есть описанный вид не показывался ни разу, а работал родной.
   Оставить их теперь значило бы вернуть дорожке чужую полосу: у сайта полоса
   одна, 10px с градиентной ручкой, и владелец просил именно её. */
.rich-carousel.rc-cards .rc-slide{
  flex:0 0 var(--rc-card,260px);
  width:var(--rc-card,260px);
  min-width:var(--rc-card,260px);
  height:var(--rc-h,340px);
  border-radius:16px;
  scroll-snap-align:start;
  background:rgba(9,13,23,.55);
  border:1px solid var(--line);
  box-shadow:0 10px 26px rgba(0,0,0,.4);
  transform-origin:center center;
  transition:transform .32s var(--ease),box-shadow .32s var(--ease),
    border-color .32s var(--ease),opacity .32s var(--ease);
}
.rich-carousel.rc-cards .rc-slide figcaption{font-size:.8rem;padding:22px 12px 10px}
.rich-carousel.rc-cards[data-rc-light="1"] .rc-slide{cursor:zoom-in}
/* Точки в карточках стоят ПОД дорожкой, а не поверх нижнего края
   карточек: поверх они читались бы как часть подписи на самой
   фотографии. */
.rich-carousel.rc-cards .rc-dots{bottom:0}
.rich-carousel.rc-cards.rc-has-dots .rc-slide figcaption{padding-bottom:10px}

/* Движение карточки (data-rc-anim). «Наклон» считает скрипт — он кладёт
   два угла переменными, а поворот, перспективу и плавный возврат делает
   оформление: так вид наклона правится без правки кода. */
.rich-carousel[data-rc-anim="lift"] .rc-slide:hover{
  transform:translateY(-6px);
  box-shadow:0 20px 44px rgba(0,0,0,.55);
  border-color:rgba(34,230,255,.4);
}
.rich-carousel[data-rc-anim="tilt"] .rc-slide{
  transform:perspective(760px)
    rotateX(var(--tilt-x,0deg)) rotateY(var(--tilt-y,0deg));
}
.rich-carousel[data-rc-anim="glow"] .rc-slide:hover{
  border-color:rgba(34,230,255,.62);
  box-shadow:0 0 0 1px rgba(34,230,255,.35),0 0 34px rgba(34,230,255,.3),
    0 18px 40px rgba(0,0,0,.5);
}

/* Эффект карточки (data-rc-fx) — постоянный, не на наведение: рамка, тень
   или стекло. Держим его на inset-тени, а не на padding: padding изменил
   бы габарит карточки, и она разошлась бы с шириной из настроек. */
.rich-carousel[data-rc-fx="frame"] .rc-slide{
  box-shadow:inset 0 0 0 1px rgba(34,230,255,.4),0 10px 26px rgba(0,0,0,.4);
}
.rich-carousel[data-rc-fx="shadow"] .rc-slide{
  box-shadow:0 22px 48px rgba(0,0,0,.66),0 4px 12px rgba(0,0,0,.5);
}
.rich-carousel[data-rc-fx="glass"] .rc-slide{
  background:linear-gradient(160deg,rgba(255,255,255,.09),rgba(255,255,255,.02));
  -webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
  border-color:rgba(255,255,255,.2);
}

/* ---------- окно «в полный рост» (data-rc-light) ----------

   Владелец: «чтобы каждая карточка при нажатии раскрывалась красиво и
   эффектно в полный рост в окне и можно было листать дальше их по порядку
   в этом окне».

   Раскрытие «из карточки» делает скрипт: он считает, насколько большое
   окно смещено и сжато относительно нажатой карточки, и отдаёт это
   оформлению одним преобразованием — оно и разъезжается в полный рост.
   Здесь только сама рамка, подложка и полоса с подписью.

   z-index выше окна работы (.wr-modal, 120): карусель встречается и внутри
   работы, и её окно обязано лечь поверх, а не под него. */
.rc-lb{
  /* Размер окна — ДВЕ переменные, а кадр один и тот же. Пять ступеней
     (data-rc-size) задают только их, и добавить шестую значит дописать
     одну строку, а не ветку в скрипте. Значение по умолчанию — прежнее
     поведение: во весь экран. */
  --lb-w:100%;--lb-h:100%;
  position:fixed;inset:0;z-index:130;
  display:flex;align-items:center;justify-content:center;
  /* Поля одинаковые со всех сторон. Прежний нижний отступ в 76 точек
     оставлял место полосе с подписью — она жила в самом окне, во всю его
     ширину. Теперь подпись стоит В РАМКЕ (см. .rc-lb-frame), и запас под
     неё здесь был бы двойным: рамка стала бы ниже, а под ней зияла бы
     пустота. */
  padding:28px;
  /* Закрытое окно обязано пропускать нажатия. Оно лежит во всю страницу и
     с нулевой прозрачностью — без этих трёх строк невидимый слой ловил бы
     КАЖДОЕ нажатие на странице, и после первого закрытия окна сайт
     становился бы «мёртвым». Сторож (test-richcards) ловит это нажатием
     по карточке сразу после Escape.
     visibility переключается не сразу, а вместе с концом проявления: иначе
     окно исчезало бы мгновенно, без плавного ухода. */
  opacity:0;visibility:hidden;pointer-events:none;
  transition:opacity .22s var(--ease),visibility 0s linear .22s;
}
.rc-lb.is-open{
  opacity:1;visibility:visible;pointer-events:auto;
  transition:opacity .22s var(--ease),visibility 0s;
}

/* ---------- пять ступеней размера окна (data-rc-size) ----------

   Владелец: «сделай чтобы при нажатии фотографии и видео не такими
   большими открывались… добавь 5 вариантов размеров чтобы я мог сам
   выбирать каким размером фото или видео в этой карусели будет
   открываться».

   Доли считаются от ОКНА, а не от экрана: поля окна и нижняя полоса с
   подписью уже вычтены, и кадр ни на одной ступени не уезжает под них.
   Ширина в долях, высота в vh — так ступени одинаково выглядят и на
   широком мониторе, и на ноутбуке.

   Порядок ступеней — от меньшей к большей, и он же порядок в списке
   санитайзера (js/richsanitize.js, RC_SIZE) и в панели: сторож
   test-richcards проверяет, что ширина растёт от ступени к ступени.

   «Во весь экран» — ПРЕЖНЕЕ поведение. Оно осталось, но стало пятой
   ступенью: владелец жаловался, что окно слишком большое, поэтому по
   умолчанию (карусель без метки) открывается «большое» — lg.
   Середина (md) значением по умолчанию не стала нарочно: у владельца
   карточки шириной до 600 точек, и окно в 46% от 1440 (637) вышло бы всего
   чуть шире самой карточки — нажал и почти ничего не изменилось. */
.rc-lb.rc-lb-size-sm  {--lb-w:30%;--lb-h:34vh}
.rc-lb.rc-lb-size-md  {--lb-w:46%;--lb-h:48vh}
.rc-lb.rc-lb-size-lg  {--lb-w:62%;--lb-h:62vh}
.rc-lb.rc-lb-size-xl  {--lb-w:80%;--lb-h:78vh}
.rc-lb.rc-lb-size-full{--lb-w:100%;--lb-h:100%}

.rc-lb-back{
  position:absolute;inset:0;background:rgba(3,5,11,.9);
  -webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);
}
.rc-lb-holder{
  position:relative;z-index:1;
  display:flex;align-items:center;justify-content:center;
  width:100%;height:100%;
  /* Нажатия пропускаем сквозь себя, а ловит их сам кадр. Без этого пустое
     место вокруг снимка съедало бы нажатие, и «нажать мимо кадра, чтобы
     закрыть» — привычный способ — не работало бы вовсе. */
  pointer-events:none;
  /* pan-y: вертикальный жест отдаём браузеру, горизонтальный оставляем
     себе — иначе он отбирал бы его себе и присылал отмену вместо
     отпускания (тем же приёмом живёт широкая карусель). */
  touch-action:pan-y;
}
/* Рамка окна — то, чего владельцу не хватало: «нет рамки и кнопки листать
   далеко и кнопка закрытия далеко… надо сделать чтобы они были в рамке
   красиво расположены».

   Раньше рамки не было вовсе: кадр лежал прямо в окне, а стрелки и крестик
   стояли по краям ЭКРАНА (left/right:22px у .rc-lb-nav и top/right:18px у
   .rc-lb-x). Пока окно занимало весь экран, это выглядело как рамка; с
   пятью ступенями размера (data-rc-size) окно стало маленьким, а кнопки
   остались у краёв экрана — за полтысячи точек от снимка. Он показал это
   снимком с красными пометками.

   Теперь рамка — отдельный слой ровно выбранного размера, и кнопки с
   подписью живут в НЕЙ: «слева» и «справа» отсчитываются от рамки, а не
   от окна. Столбик из сцены и полосы: сцена тянется, полоса берёт своё.

   Обрезки (overflow:hidden) здесь нет нарочно. С ней углы снимка
   скруглились бы по рамке, но разъезжающаяся анимация открытия (кадр
   едет из карточки, см. lbShow) начиналась бы ЗА рамкой и была бы срезана
   — то есть окно открывалось бы пустым и кадр появлялся бы из ниоткуда. */
.rc-lb-frame{
  position:relative;
  display:flex;flex-direction:column;
  box-sizing:border-box;
  width:var(--lb-w,100%);height:var(--lb-h,100%);
  min-width:0;min-height:0;
  border:1px solid rgba(140,175,225,.22);
  border-radius:16px;
  background:rgba(7,11,20,.66);
  box-shadow:0 30px 80px rgba(0,0,0,.72);
  /* Нажатия ловит рамка, а не окно: протяжка по пустому месту рамки —
     листание, а нажатие мимо рамки (по тёмной подложке) — закрытие. */
  pointer-events:auto;
}
/* Сцена — место самого кадра и кнопок. Стрелки считаются от неё, а не от
   рамки: иначе на ступени с подписью они съезжали бы вниз вместе с ней. */
.rc-lb-stage{
  position:relative;flex:1 1 auto;min-height:0;
  display:flex;align-items:center;justify-content:center;
}

/* Снимковый слой — коробка РОВНО по снимку, а не по сцене.

   Сцена тянется на всю рамку (её размер выбирает владелец, ступенями), и
   кадр в ней вписывается object-fit:contain — по большей стороне остаются
   поля. Поля и портили обе вещи сразу:
     • скругление углов стояло на КОРОБКЕ кадра, а её углы лежат в поле:
       снимок оставался прямоугольным, хотя border-radius был задан;
     • стрелки, отсчитанные от сцены, стояли на краях РАМКИ. Владелец:
       «стрелки не так далеко в рамке самой фотографии или видео».

   Замер на его кадре 4:3 (2392×1792), ступень lg: рамка 858, сцена 856,
   снимок 691 — поле 82 точки с каждой стороны, стрелка в поле. На кадре
   7:6 поле 204 точки.

   Размер слоя ставит скрипт (lbFit): сцена тянется, а снимок вписывается —
   одного оформления для этого мало, потому что «вписать» зависит от
   соотношения сторон файла, а его оформление не знает.

   max-width/max-height — страховка на первый кадр, до подгонки: слой без
   размера не должен растянуться на всю сцену и толкнуть рамку. */
.rc-lb-shot{
  position:relative;flex:none;
  display:flex;align-items:center;justify-content:center;
  max-width:100%;max-height:100%;
}

/* Кадр занимает СНИМКОВЫЙ СЛОЙ целиком, а не сцену: слой уже подогнан под
   снимок (см. .rc-lb-shot и lbFit), поэтому object-fit:contain здесь —
   страховка на первый кадр, до подгонки, а не рабочий режим. Габарит
   по-прежнему задаёт РАМКА: размер, выбранный владельцем, остаётся
   пределом, и ступени .rc-lb-size-* объявлены в одном месте.

   Скругление — здесь и только здесь, потому что коробка кадра теперь
   совпадает со снимком. Пока кадр занимал сцену, углы скруглялись у
   пустого поля, а сам снимок оставался прямоугольным — ровно то, на что
   владелец показал снимком («само фото или видео должно быть немного
   края скруглены»). Четырнадцать точек: на снимке в 700 точек одиннадцать
   читались как случайная неровность, двадцать уже спорят с рамкой в 16.

   Свечение по краю даёт drop-shadow: он идёт по силуэту снимка, а не по
   прямоугольнику коробки. */
.rc-lb-media{
  display:block;width:100%;height:100%;object-fit:contain;
  border-radius:14px;
  pointer-events:auto;
  filter:drop-shadow(0 24px 60px rgba(0,0,0,.75));
  transform:scale(.96);opacity:0;
  /* Кадр в окне не перетаскивают и не выделяют: перетаска отбирает жест
     у листания, выделение мажет подсветкой по фотографии. */
  -webkit-user-drag:none;user-select:none;
  transition:transform .34s cubic-bezier(.22,.61,.36,1),opacity .3s var(--ease);
}
.rc-lb.is-open .rc-lb-media{transform:none;opacity:1}

/* Крестик и стрелки — по краям РАМКИ и СНИМКА, а не окна.

   Крестик отсчитывается от СЦЕНЫ (его ближайший предок с position —
   .rc-lb-stage): его место — угол рамки. Двенадцать точек от её края:
   меньше уже сливалось бы с границей, больше — уводило бы в глубь снимка.

   Стрелки отсчитываются от СНИМКОВОГО СЛОЯ (.rc-lb-shot, position:relative)
   и потому стоят у кромки САМОГО СНИМКА на любой ступени размера. Раньше
   они считались от сцены — то есть от рамки, — и на узком кадре уезжали
   в поле: замер на кадре 4:3 дал 82 точки поля с каждой стороны.

   Восемь точек — отступ от кромки снимка: стрелка в 52 точки ложится на
   снимок на треть своего поперечника, оставаясь читаемой и не закрывая
   середину кадра.

   Прежние значения (18 и 22 точки от края ОКНА) на маленькой ступени
   давали ту самую картину со снимка владельца: снимок по центру, а
   стрелки и крестик — у краёв экрана. */
.rc-lb-x{
  position:absolute;top:12px;right:12px;z-index:3;
  width:42px;height:42px;padding:0;display:grid;place-items:center;
  border:1px solid rgba(255,255,255,.16);border-radius:50%;
  background:rgba(8,13,23,.82);color:var(--ink-2);cursor:pointer;
}
.rc-lb-x svg{width:18px;height:18px}
.rc-lb-x:hover{color:var(--ink);border-color:rgba(34,230,255,.5)}
.rc-lb-nav{
  position:absolute;top:50%;transform:translateY(-50%);z-index:3;
  width:52px;height:52px;padding:0;display:grid;place-items:center;
  border:1px solid var(--line-strong);border-radius:50%;
  background:rgba(4,6,12,.68);color:var(--ink);cursor:pointer;
  -webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);
  transition:background .2s var(--ease),border-color .2s var(--ease);
}
.rc-lb-nav svg{width:24px;height:24px}
.rc-lb-nav:hover{background:rgba(34,230,255,.2);border-color:rgba(34,230,255,.5)}
.rc-lb-prev{left:8px}
.rc-lb-next{right:8px}
.rc-lb-next svg{transform:rotate(180deg)}
/* Один кадр — листать нечего: стрелки убираем, а не гасим. В окне они
   не подсказка (листание видно и по полосе с подписью), а обман. */
.rc-lb.rc-lb-one .rc-lb-nav{display:none}

/* Полоса с подписью и счётчиком — нижняя часть РАМКИ, а не строка в окне.

   Было: position:absolute, во всю ширину ОКНА, у его нижнего края. На
   маленькой ступени подпись оказывалась далеко под снимком и читалась как
   чужая. Теперь полоса — обычный (не вынутый из потока) ребёнок рамки:
   сцена тянется, полоса берёт свою высоту, и подпись стоит ровно под
   снимком, по его ширине. Граница сверху отделяет её от снимка — без неё
   подпись на светлой фотографии читалась бы как часть картинки. */
.rc-lb-bar{
  flex:0 0 auto;
  display:flex;flex-direction:column;align-items:center;gap:4px;
  padding:9px 16px 11px;text-align:center;pointer-events:none;
  border-top:1px solid rgba(140,175,225,.16);
}
/* Подписи нет — полоса ужимается до одной строки счётчика: держать под
   ней пустое место значило бы отнять у снимка высоту ни за что. */
.rc-lb.rc-lb-nocap .rc-lb-bar{padding:7px 16px 9px}
.rc-lb-count{
  font-size:.78rem;letter-spacing:.06em;color:var(--cyan);
  font-variant-numeric:tabular-nums;
}
.rc-lb-cap{font-size:.9rem;color:var(--ink-2);max-width:70ch}
.rc-lb.rc-lb-nocap .rc-lb-cap{display:none}
.rc-lb-x:focus-visible,.rc-lb-nav:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
/* Прокрутка страницы под окном замирает. */
body.rc-lb-open{overflow:hidden}
@media (max-width:640px){
  /* Поля одинаковые: полоса с подписью живёт в рамке (см. .rc-lb-bar), и
     нижний запас в 62 точки оставлял бы под рамкой пустую полосу. */
  .rc-lb{padding:12px}
  /* Те же пять ступеней, но доли другие. На телефоне окно и так узкое:
     30% от 390 точек — это 117 точек, не «маленькое фото», а огрызок, и
     все пять ступеней слиплись бы в одну. Поэтому ступени подтянуты к
     ширине экрана, а разница между ними читается.
     Селектор с классом окна (.rc-lb.rc-lb-size-…) — не для красоты:
     у запроса ширины НЕТ своей силы, и правило `.rc-lb-size-sm` проиграло
     бы правилу `.rc-lb.rc-lb-size-sm` выше просто по числу классов. */
  .rc-lb.rc-lb-size-sm  {--lb-w:62%;--lb-h:38vh}
  .rc-lb.rc-lb-size-md  {--lb-w:80%;--lb-h:52vh}
  .rc-lb.rc-lb-size-lg  {--lb-w:92%;--lb-h:66vh}
  .rc-lb.rc-lb-size-xl  {--lb-w:100%;--lb-h:82vh}
  .rc-lb.rc-lb-size-full{--lb-w:100%;--lb-h:100%}
  /* Кнопки ужимаются и жмутся к краям рамки: на телефоне рамка узкая, и
     крупная стрелка закрыла бы половину снимка. */
  .rc-lb-nav{width:42px;height:42px}
  .rc-lb-nav svg{width:20px;height:20px}
  .rc-lb-prev{left:6px}
  .rc-lb-next{right:6px}
  .rc-lb-x{top:6px;right:6px;width:38px;height:38px}
  .rc-lb-frame{border-radius:12px}
  .rc-lb-bar{padding:7px 10px 9px}
}
@media (prefers-reduced-motion:reduce){
  .rc-lb,.rc-lb-media{transition:none}
  .rich-carousel.rc-cards .rc-slide{transition:none}
  .rich-carousel.rc-cards .rc-track{scroll-behavior:auto}
}

/* Описание блока «Блоки» — своё место, поэтому отступы задаём здесь.
   Раньше это был просто <p> прямо в .lb-txt, и отступ ему давало
   правило .lb-txt p. Теперь абзацев может быть несколько, и они
   живут в своей обёртке — иначе список или таблица разорвали бы
   абзац, и шапка блока поехала бы. */
.lb-note{margin-top:7px}
.lb-note p{margin:0 0 8px}
.lb-note p:last-child{margin-bottom:0}

/* ============================================================
   ОТЗЫВЫ — карусель отзывов (раздел #reviews)

   Владелец: «Новая вкладка "Отзывы" с подзаголовком "Живые отзывы
   покупателей и клиентов": карусель отзывов (слева кружок с фото, справа
   имя и текст), с эффектами, правится в панели, скрывается, переносится».

   Устройство — дорожка со сдвигом, а не лента со снапом: показываем ровно
   ОДИН отзыв, поэтому «сколько влезло» считать не нужно и мерить ширину
   тоже. Кадр занимает ровно 100 % ширины окна, значит сдвиг на один кадр —
   это ровно `-100 %`, а на кадр с номером i — `-100 % × i`. Считает это
   оформление, а не скрипт: `translateX` в процентах берётся от ШИРИНЫ
   САМОГО ЭЛЕМЕНТА, а дорожка — обычный блочный флекс-контейнер, и её
   ширина равна ширине окна, а не сумме кадров (кадры из неё вылезают).
   Отсюда `-100 %` = один кадр. Замер в пикселях здесь был бы лишним и
   разъехался бы при смене ширины окна.

   Высоту окна НЕ выставляем: кадры стоят в потоке рядом друг с другом,
   поэтому самую высокую карточку задаёт содержимое. Замер высоты был бы
   той самой ловушкой, из-за которой содержимое вылезает из своей рамки
   (навык grid-ignores-container-height).

   Цвет — из общей палитры магазина (`--stk-*`, см. выше): владелец задаёт
   цвет отзывам отдельно, а переменные ставятся на сам раздел, поэтому
   общие имена никого ни к чему не обязывают. Запасной цвет — цвет сайта. */
/* Наклон объёмной карточки — три ЧИСЛА, которые плавно меняются сами.

   Почему зарегистрированные свойства, а не переход `transform`: `transform`
   у карточки занят видом карусели (кольцо, стопка, барабан…), и переход на
   нём растянул бы заодно и переезд кадра на кольце — вид стал бы вялым.
   Зарегистрированное свойство анимируется как число, а формула поворота
   просто читает его на каждом кадре. Скрипт ставит числа, браузер их
   сглаживает, оформление поворачивает. */
@property --rev-mx{syntax:'<number>';inherits:true;initial-value:0}
@property --rev-my{syntax:'<number>';inherits:true;initial-value:0}
@property --rev-s{syntax:'<number>';inherits:true;initial-value:1}
.rev{
  --rev-pad:12px;
  --rev-c:var(--stk-c,var(--cyan));
  --rev-bg:var(--stk-bg,rgba(120,160,220,.09));
  --rev-line:var(--stk-line,var(--line-strong));
  position:relative;
  transition:--rev-mx .18s var(--ease),--rev-my .18s var(--ease),--rev-s .18s var(--ease);
}

/* Окно карусели. Отступ оставлен только СВЕРХУ и СНИЗУ: `overflow:hidden`
   режет по padding-box, и вертикальные 12px дают место мягкому свечению
   карточки. По бокам отступа нет намеренно — иначе в него заглядывал бы
   соседний кадр, стоящий сразу за кромкой. Отрицательное поле возвращает
   окну прежнюю высоту, чтобы отступы не раздвигали раздел. */
.rev-view{overflow:hidden;padding:var(--rev-pad) 0;margin:calc(var(--rev-pad) * -1) 0}
.rev-track{
  display:flex;
  transform:translateX(calc(-100% * var(--rev-i,0)));
  transition:transform .55s var(--ease);
}
/* Лента отзывов листается пальцем и курсором (жест ловит скрипт).

   `touch-action:pan-y` — не украшение, а разделение труда: вертикальный
   жест остаётся СТРАНИЦЕ (иначе раздел отзывов запирал бы на себе прокрутку
   телефона), горизонтальный отдаётся ленте. Курсор показывает, что кадр
   можно потянуть; на время протяжки текст не выделяется. */
.rev-track{touch-action:pan-y;cursor:grab}
.rev-track:active{cursor:grabbing}
[data-reviews][data-rev-drag] .rev-track{cursor:grabbing}
[data-reviews][data-rev-drag] .rev-card{user-select:none;-webkit-user-select:none}
/* Кадр = вся ширина окна. Внешних отступов и зазоров нет намеренно: любой
   из них сдвинул бы расчёт `-100 %` и кадры разъехались бы. */
.rev-card{
  flex:0 0 100%;
  margin:0;
  display:grid;
  grid-template-columns:auto minmax(0,1fr);
  gap:24px;
  align-items:start;
  padding:28px 30px;
  border:1px solid var(--rev-line);
  border-radius:22px;
  background:var(--rev-bg);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.05);
}

/* Кружок с фотографией. Без фотографии — первая буква имени: пустая серая
   заглушка на месте лица читалась бы как сломанная картинка. */
.rev-face{
  width:104px;height:104px;border-radius:50%;overflow:hidden;
  display:flex;align-items:center;justify-content:center;
  border:2px solid var(--rev-c);
  background:rgba(6,10,18,.45);
  box-shadow:0 0 22px var(--rev-bg);
  font-size:36px;font-weight:600;line-height:1;color:var(--rev-c);
}
.rev-face img{width:100%;height:100%;object-fit:cover;display:block}

.rev-body{min-width:0;display:flex;flex-direction:column;gap:12px;height:100%}
.rev-stars{display:flex;gap:3px;color:var(--rev-c);font-size:15px;line-height:1}
/* Погасшие звёзды показываем, а не выкидываем: «4 из 5» читается только
   тогда, когда видно пятую. Приглушённые — цветом, а не прозрачностью
   одной звезды: так они не выпадают из строки по высоте. */
.rev-stars .off{color:var(--ink-3);opacity:.55}
.rev-quote{position:relative;padding-left:24px}
.rev-quote::before{
  content:'“';position:absolute;left:0;top:-8px;
  font-size:42px;line-height:1;color:var(--rev-c);opacity:.45;
}
/* Текст отзыва правится редактором, поэтому показываем его разметкой.
   Отступы абзацам задаём здесь: у <p> внутри редактора свои поля, и без
   этого длинный отзыв разъезжался бы по высоте. */
.rev-text{color:var(--ink)}
.rev-text p{margin:0 0 10px}
.rev-text p:last-child{margin-bottom:0}
/* «Показать полностью» — КНОПКА, а не ссылка: она ничего не открывает и не
   уводит со страницы, а разворачивает текст на месте.

   `[hidden]` объявлен отдельной строкой и ПОСЛЕ `display`: правило
   `display:inline-flex` сильнее атрибута `hidden`, и без этой строки кнопка
   торчала бы под каждым коротким отзывом, где сокращать нечего.

   Кегль и насыщенность заданы СВОЙСТВАМИ, а не сокращением `font` с
   `var(--f-main)`: такой переменной в оформлении нет, и всё сокращение
   браузер выбрасывал целиком — кнопка оставалась системного кегля 13.3px
   обычной насыщенности. Ошибка была невидимой: проверка читала надпись, а не
   кегль. */
.rev-more{
  align-self:flex-start;
  display:inline-flex;align-items:center;gap:6px;
  margin-top:10px;padding:0;border:0;background:none;cursor:pointer;
  font-family:inherit;font-size:12px;font-weight:600;line-height:1.5;
  letter-spacing:.01em;
  color:var(--rev-c);
  text-decoration:underline;text-underline-offset:3px;
  text-decoration-thickness:1px;text-decoration-color:currentColor;
  transition:color .2s var(--ease);
}
.rev-more[hidden]{display:none}
.rev-more:hover{color:var(--cyan)}
.rev-more:focus-visible{outline:2px solid var(--cyan);outline-offset:3px;border-radius:4px}
.rev-who{margin-top:auto;display:flex;flex-direction:column;gap:2px}
.rev-name{font-size:16px;font-weight:600;color:var(--ink)}
.rev-role{font-size:13px;color:var(--ink-3)}

/* ============================================================
   ПРОКРУТКА ОТЗЫВОВ У ПРАВОЙ КРОМКИ

   Владелец (29.10, два снимка): обвёл полосу со стрелками и точками под
   отзывами и отдельно — узкую полоску у правого края карточки, и сказал
   «перенести сюда, чтобы была там прокрутка без стрелок, красивая и
   эффектная».

   Стрелок и точек в разделе больше нет. Вместо них — вертикальная полоса у
   правой кромки карточки с бегунком, как у настоящей прокрутки.

   Ничего не меряем. Всё, что нужно, приходит двумя числами с корня раздела:
   `--rev-n` (сколько отзывов) и `--rev-p` (где мы, 0…1). Из них считается и
   длина бегунка (1/n полосы), и его ход. При смене ширины окна
   пересчитывать нечего: проценты берутся от самой полосы.

   Полоса НЕ накрывает карточку собой: она лежит в 2 px от кромки и получает
   нажатия только в своих пределах — плюс невидимое расширение `::before`,
   иначе в 4 px мышью не попасть. */
.rev-rail{
  position:absolute;top:50%;right:2px;
  width:4px;height:150px;
  transform:translateY(-50%);
  border-radius:99px;
  background:var(--rev-line);
  cursor:pointer;z-index:6;
  /* Вертикальная протяжка по полосе принадлежит ПОЛОСЕ, а не странице:
     иначе тянуть бегунок было бы невозможно — страница уезжала бы вниз. */
  touch-action:none;
  transition:width .22s var(--ease),background .22s var(--ease);
}
/* Невидимая площадка для нажатия: 4 px — это рис, а не мишень. */
.rev-rail::before{
  content:'';position:absolute;inset:-12px -14px;
  border-radius:99px;
}
.rev-rail:hover,.rev-rail:focus-visible,.rev[data-rev-scrub] .rev-rail{width:6px}
.rev-rail:focus-visible{outline:2px solid var(--rev-c);outline-offset:4px}
.rev-rail-thumb{
  /* Длина бегунка — 1/n полосы, но не меньше 26 px: у десяти отзывов
     честная 1/10 была бы волоском, который не ухватить. */
  --rev-th:max(26px,calc(100% / var(--rev-n,3)));
  position:absolute;left:0;right:0;
  height:var(--rev-th);
  top:calc((100% - var(--rev-th)) * var(--rev-p,0));
  border-radius:99px;
  background:var(--rev-c);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.55),0 0 12px var(--rev-bg);
  transition:top .5s var(--ease),box-shadow .22s var(--ease);
}
/* Пока бегунок тянут, он не «догоняет» палец переходом: иначе полоса
   отставала бы от руки. */
.rev[data-rev-scrub] .rev-rail-thumb{transition:box-shadow .22s var(--ease)}
.rev[data-rev-scrub] .rev-rail-thumb,
.rev-rail:hover .rev-rail-thumb{box-shadow:inset 0 1px 0 rgba(255,255,255,.6),0 0 18px var(--rev-c)}
/* Счётчик «2/3» — подсказка, а не украшение: без точек не видно, сколько
   всего отзывов. Показываем только когда на полосу смотрят или тянут. */
.rev-rail-num{
  position:absolute;bottom:calc(100% + 10px);left:50%;
  transform:translateX(-50%) translateY(4px);
  padding:3px 7px;border-radius:8px;
  background:var(--rev-bg);color:var(--rev-c);
  font:600 10px/1 var(--f-mono);letter-spacing:.06em;white-space:nowrap;
  opacity:0;pointer-events:none;
  transition:opacity .2s var(--ease),transform .2s var(--ease);
}
.rev-rail:hover .rev-rail-num,.rev-rail:focus-visible .rev-rail-num,
.rev[data-rev-scrub] .rev-rail-num{opacity:1;transform:translateX(-50%) translateY(0)}

/* Виды появления кадра. Класс rev-in скрипт вешает на ТОТ кадр, который
   сейчас въезжает: движение дорожки одно на все виды, а «эффект» —
   это появление самого кадра. Дорожка движется всегда, поэтому «сдвиг»
   отдельного правила не требует. */
[data-rev-effect="fade"] .rev-card.rev-in{animation:revFade .6s var(--ease) both}
[data-rev-effect="zoom"] .rev-card.rev-in{animation:revZoom .62s var(--ease) both}
[data-rev-effect="flip"] .rev-card.rev-in{animation:revFlip .72s var(--ease) both}
[data-rev-effect="rise"] .rev-card.rev-in{animation:revRise .6s var(--ease) both}
[data-rev-effect="glide"] .rev-card.rev-in{animation:revGlide .6s var(--ease) both}
@keyframes revFade{from{opacity:0}to{opacity:1}}
@keyframes revZoom{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}
@keyframes revFlip{
  from{opacity:0;transform:perspective(900px) rotateY(-16deg)}
  to{opacity:1;transform:perspective(900px) rotateY(0)}
}
@keyframes revRise{from{opacity:0;transform:translateY(26px)}to{opacity:1;transform:translateY(0)}}
@keyframes revGlide{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:translateX(0)}}

/* ============================================================
   ФОРМА КАРТОЧКИ: ОБЪЁМНАЯ (3D-КАРТА) ИЛИ ПЛОСКАЯ

   Владелец (29.10): «надо сами карточки сделать объёмными и настоящими как
   3д карты, с настройкой в админ панели».

   Настройка — `data-rev-card` на корне раздела (`volume` / `flat`). Это
   ОТДЕЛЬНАЯ настройка от вида карусели: вид говорит, где стоят кадры, форма —
   как выглядит сам кадр.

   Из чего складывается объём:
     • ТОЛЩИНА — плита из теней под карточкой (`--rev-edge`): шесть шагов по
       1 px дают край картонного листа, а не размытое пятно под ним;
     • НАКЛОН — за курсором (`--rev-mx`/`--rev-my`, считает скрипт). Углы
       небольшие нарочно: окно карусели режет по своей кромке, и наклонённая
       карточка иначе срезалась бы углом. Усадку `--rev-s` тоже считает
       скрипт — она тем сильнее, чем круче наклон;
     • ПЕРЕЛИВ — блик фольги, который ходит по карточке вместе с курсором
       (`::before` под текстом: `z-index:-1`, поэтому буквы не выцветают);
     • ГЛУБИНА — кружок с фотографией плавает над листом, сдвигаясь против
       курсора на несколько пикселей.

   Плоская форма — это ОТСУТСТВИЕ всех этих правил: карточка остаётся ровно
   такой, какой была до настройки, пиксель в пиксель. */
.rev[data-rev-card="volume"] .rev-card{
  position:relative;
  /* Край карточки. Тёмное значение — для ночного вида; бумага перекрашивает
     его в цвет картона (theme-paper.css). */
  --rev-edge:#0b1424;
  transform:
    perspective(1400px)
    rotateX(calc(var(--rev-my,0) * 3.4deg))
    rotateY(calc(var(--rev-mx,0) * -4.2deg))
    scale(var(--rev-s,1));
  transform-origin:50% 50%;
  /* Перехода на transform здесь НЕТ нарочно: плавность даёт сглаживание
     чисел --rev-mx/--rev-my (см. выше), а transform принадлежит карточке
     целиком — с переходом на нём переезд кадра в 3D-видах стал бы другим. */
  transition:box-shadow .3s var(--ease);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.06),
    0 1px 0 var(--rev-edge),
    0 2px 0 var(--rev-edge),
    0 3px 0 var(--rev-edge),
    0 4px 0 var(--rev-edge),
    0 5px 0 var(--rev-edge),
    0 6px 0 var(--rev-edge),
    0 12px 20px -8px rgba(0,0,0,.55),
    0 30px 50px -28px rgba(0,0,0,.7);
}
/* Перелив фольги. Лежит ПОД содержимым (z-index:-1): карточка создаёт свой
   контекст наложения из-за transform, поэтому блик оказывается над её
   заливкой, но под буквами — текст не выцветает. */
.rev[data-rev-card="volume"] .rev-card::before{
  content:'';position:absolute;inset:0;z-index:-1;
  border-radius:inherit;pointer-events:none;
  background:
    radial-gradient(56% 42% at calc(50% + var(--rev-mx,0) * 46%) calc(50% + var(--rev-my,0) * 46%),
      rgba(255,255,255,.42),rgba(255,255,255,0) 62%),
    linear-gradient(112deg,
      rgba(255,255,255,0) 24%,rgba(255,150,60,.10) 38%,
      rgba(255,255,255,.26) 48%,rgba(110,205,255,.10) 58%,
      rgba(255,255,255,0) 74%);
  opacity:0;
  transition:opacity .3s var(--ease);
}
.rev[data-rev-card="volume"] .rev-card:hover::before,
.rev[data-rev-card="volume"] .rev-card:focus-within::before,
.rev[data-rev-card="volume"][data-rev-tilting] .rev-card::before{opacity:1}
/* Кружок плавает над листом: сдвиг против курсора читается как глубина,
   потому что карточка при этом наклоняется в другую сторону. Своего
   перехода у сдвига нет — его сглаживают те же числа, что и наклон. */
.rev[data-rev-card="volume"] .rev-face{
  transform:translate3d(calc(var(--rev-mx,0) * -5px),calc(var(--rev-my,0) * -5px),0);
}
@media (prefers-reduced-motion:reduce){
  .rev{transition:none}
  .rev[data-rev-card="volume"] .rev-card{transition:none}
  .rev[data-rev-card="volume"] .rev-card::before{transition:none}
}

/* Один отзыв — не карусель: стрелки и точки к одному кадру были бы
   обманом, а не удобством (то же правило, что у карусели в тексте). */
.rev[data-rev-count="1"] .rev-rail{display:none}

@media (max-width:720px){
  .rev-card{padding:22px 20px;gap:16px;border-radius:18px}
  .rev-face{width:70px;height:70px;font-size:26px}
  .rev-quote{padding-left:20px}
  .rev-quote::before{font-size:34px;top:-6px}
}

@media (prefers-reduced-motion:reduce){
  .rev-track{transition:none}
  .rev-card.rev-in{animation:none!important}
  .rev-rail,.rev-rail-thumb,.rev-rail-num{transition:none}
}

/* ============================================================
   ДОСТАВКА И ОПЛАТА — аккордеон с условиями (раздел #shipping)

   Владелец: «Новая вкладка в панели и на сайте "Доставка и оплата" по
   образцу kadanika.ru (аккордеон "Доставка / Подготовка заказа / Оплата /
   Возврат и обмен" с плюсиком) — взять содержание оттуда, подстроить под
   3DGamBiT, с эффектами и разными цветами, правится в панели».

   Устройство — как у «Вопросов и ответов»: нативные <details>/<summary>,
   раскрывает их сам браузер. Поэтому здесь нет ни одного замерённого числа
   высоты — именно замер и обрезал ответы, стоило тексту переразбиться на
   строки (навык grid-ignores-container-height). Высоту плашки анимирует
   сам браузер: ::details-content + interpolate-size.

   Цвет — из общей палитры (--stk-*): владелец задаёт цвет разделу, а
   каждому пункту может задать свой. Атрибут data-ship-color стоит и на
   разделе, и на пункте; работает это наследованием, а не каскадом, —
   подробности в комментарии к палитре выше.

   Псевдонимы --ship-* объявлены И НА ПУНКТЕ, а не только на разделе, и это
   не украшение. Пользовательское свойство подставляется там, где ОБЪЯВЛЕНО:
   объяви мы `--ship-line:var(--stk-line)` только на .ship — значение
   вычислилось бы по цвету РАЗДЕЛА и унаследовалось пункту готовым, и
   собственный цвет пункта не значил бы ничего. На .ship-item подстановка
   идёт заново, уже по цвету самого пункта: свой цвет перебивает цвет
   раздела, а «как у раздела» наследует его. */
.ship,
.ship-item{
  --ship-c:var(--stk-c,var(--cyan));
  --ship-bg:var(--stk-bg,rgba(120,160,220,.09));
  --ship-line:var(--stk-line,var(--line-strong));
  --ship-glow:var(--stk-glow,rgba(34,230,255,.35));
}
.ship{
  --ease-ship:cubic-bezier(.4,0,.2,1);
}

/* Пункт. Рамка, скругление и свечение при открытии — те же приёмы, что у
   вопросов: два аккордеона на сайте обязаны выглядеть одной семьёй.
   Строка кривой для раскрытия своя: общая --ease отдаёт почти весь путь в
   первой четверти и на ВЫСОТЕ читается как рывок с долгим доездом. */
.ship-item{
  interpolate-size:allow-keywords;
  border:1px solid var(--ship-line);
  border-radius:var(--r-md);
  margin-bottom:12px;
  background:rgba(13,18,31,.45);
  overflow:hidden;
  transition:border-color .4s var(--ease),box-shadow .4s var(--ease),
    background .4s var(--ease);
}
.ship-item:last-child{margin-bottom:0}
.ship-item[open]{
  border-color:var(--ship-c);
  background:var(--ship-bg);
  box-shadow:0 0 34px -6px var(--ship-glow);
}

/* Строка с названием. Значок слева, название посередине, плюсик справа:
   flex с `flex:1` у названия — плюсик всегда у правого края, как бы ни
   была длинна подпись. */
.ship-q{
  display:flex;align-items:center;gap:14px;
  padding:18px 22px;
  color:var(--ink);
  font-size:1.01rem;font-weight:600;letter-spacing:-.01em;
  cursor:pointer;list-style:none;
  transition:padding-bottom .45s var(--ease-ship),color .25s var(--ease);
}
/* Системный треугольник убираем: свой значок — .ship-plus. */
.ship-q::-webkit-details-marker{display:none}
.ship-q::marker{content:''}
.ship-q:hover{color:#fff}
.ship-q:focus-visible{outline:2px solid var(--ship-c);outline-offset:-4px;border-radius:var(--r-md)}

/* Значок в кружке. ПУСТОЙ span прячем целиком: «без значка» не должно
   оставлять дырку слева от названия. Поэтому в разметке и скрипте внутри
   не должно быть ни одного пробельного узла — иначе :empty не сработает. */
.ship-ic{
  width:36px;height:36px;flex:none;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  border:1px solid var(--ship-line);background:var(--ship-bg);
  color:var(--ship-c);
  transition:border-color .35s var(--ease),box-shadow .35s var(--ease);
}
.ship-ic:empty{display:none}
.ship-ic svg{width:19px;height:19px}
.ship-item[open] .ship-ic{border-color:var(--ship-c);box-shadow:0 0 16px -3px var(--ship-glow)}

.ship-t{flex:1 1 auto;min-width:0}

/* Плюсик: закрыт — «+», открыт — «×». Обе черты остаются, поворот на 45°
   превращает одну в другую чисто; поворот на 90° прятал бы вертикальную
   черту и оставлял бессмысленную полоску (та же ошибка была у вопросов). */
.ship-plus{
  width:26px;height:26px;flex:none;position:relative;border-radius:50%;
  border:1px solid var(--ship-line);
  transition:transform .45s var(--ease),box-shadow .45s var(--ease),
    border-color .45s var(--ease);
}
.ship-plus::before,.ship-plus::after{
  content:'';position:absolute;top:50%;left:50%;background:var(--ship-c);
  border-radius:2px;transform:translate(-50%,-50%);box-shadow:0 0 8px var(--ship-c);
}
.ship-plus::before{width:11px;height:1.6px}
.ship-plus::after{width:1.6px;height:11px}
.ship-item[open] .ship-plus{
  transform:rotate(45deg);border-color:var(--ship-c);
  box-shadow:0 0 20px -4px var(--ship-glow);
}

/* Плавное раскрытие ответа — высоту плашки анимирует САМ БРАУЗЕР.
   Ключевое слово auto в block-size разрешено через interpolate-size на
   .ship-item, поэтому высота едет от нуля до «сколько получилось»: замеров
   и запомненных чисел нет, обрезать нечего по построению. Где
   ::details-content ещё не поддерживается — правила игнорируются, пункт
   раскрывается мгновенно, как и раньше. */
.ship-item::details-content{
  block-size:0;overflow:hidden;content-visibility:hidden;
  transition:block-size .45s var(--ease-ship);
}
.ship-item[open]::details-content{block-size:auto;content-visibility:visible}
.ship-a{overflow:hidden}
.ship-item[open] .ship-q{padding-bottom:8px}

/* Текст пункта правится редактором, поэтому показываем его разметкой.
   Отступы задаём здесь: у <p> и <ul> из редактора свои поля, и без этого
   длинный пункт разъезжался бы по высоте. */
.ship-body{padding:0 22px 22px;color:var(--ink-2);font-size:.95rem;font-weight:300}
.ship-body p{margin:0 0 12px}
.ship-body p:last-child{margin-bottom:0}
.ship-body h3,.ship-body h4{margin:18px 0 8px;color:var(--ink);font-weight:600;font-size:1rem}
.ship-body h3:first-child,.ship-body h4:first-child{margin-top:0}
.ship-body ul,.ship-body ol{margin:0 0 12px;padding-left:22px}
.ship-body li{margin-bottom:6px}
.ship-body li::marker{color:var(--ship-c)}
.ship-body strong,.ship-body b{color:var(--ink);font-weight:600}
.ship-body a{color:var(--ship-c);text-decoration:underline;text-underline-offset:2px}
.ship-body a:hover{text-decoration:none}

/* Виды появления ответа. Атрибут data-ship-effect стоит на РАЗДЕЛЕ, а
   движется содержимое КАЖДОГО открытого пункта. Правила с атрибутом
   специфичнее заводского, поэтому перебивают его; «сдвиг» отдельного
   правила не требует — он и есть заводское появление. */
.ship-item[open] .ship-body{animation:shipSlide .45s var(--ease-ship)}
[data-ship-effect="fade"] .ship-item[open] .ship-body{animation:shipFade .5s var(--ease-ship)}
[data-ship-effect="zoom"] .ship-item[open] .ship-body{animation:shipZoom .5s var(--ease-ship)}
[data-ship-effect="rise"] .ship-item[open] .ship-body{animation:shipRise .5s var(--ease-ship)}
[data-ship-effect="glide"] .ship-item[open] .ship-body{animation:shipGlide .5s var(--ease-ship)}
[data-ship-effect="none"] .ship-item[open] .ship-body{animation:none}
@keyframes shipSlide{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:none}}
@keyframes shipFade{from{opacity:0}to{opacity:1}}
@keyframes shipZoom{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:none}}
@keyframes shipRise{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:none}}
@keyframes shipGlide{from{opacity:0;transform:translateX(28px)}to{opacity:1;transform:none}}

@media (max-width:720px){
  .ship-q{padding:16px 16px;gap:12px;font-size:.97rem}
  .ship-ic{width:32px;height:32px}
  .ship-ic svg{width:17px;height:17px}
  .ship-body{padding:0 16px 18px}
}

@media (prefers-reduced-motion:reduce){
  .ship-item{transition:none}
  .ship-item::details-content{transition:none}
  .ship-q{transition:none}
  .ship-plus,.ship-ic{transition:none}
  .ship-item[open] .ship-body{animation:none!important}
}

/* ---------- Кнопки владельца в тексте ----------

   Владелец: «добавить функцию создания моих кнопок с назначением им
   условий как купить или сделать заказ … и ссылки там в этих красивых и
   анимированных кнопках».

   Кнопка — это ССЫЛКА: `<a class="rc-btn rc-btn-buy rc-anim-pulse"
   href="…">Купить</a>`. Почему ссылка, а не `<button>`: у `<a>` уже есть
   `href` в белом списке санитайзера, и она работает сама — на сайте, в
   чужом окне, в письме. `<button>` без нашего обработчика был бы мёртвым
   прямоугольником.

   Имя собрано из трёх частей: `rc-btn` — сама кнопка, `rc-btn-<условие>` —
   вид («Купить», «Сделать заказ»), `rc-anim-<движение>` — движение.
   Условие и движение владелец выбирает в панели, а рисуются они ЗДЕСЬ:
   имена в данных панели и правила тут обязаны совпадать, и это сверяет
   `test-editor.js`.

   ПОЧЕМУ БЛОК ЛЕЖИТ В ДВУХ ФАЙЛАХ. Кнопку видно в трёх местах: поле
   редактора, образцы в панели и текст на сайте. Панель грузит только
   admin.css, сайт — только style.css, общего листа у них нет. Поэтому
   блок продублирован дословно, а набор проверок сверяет обе копии
   побайтово: разъедутся — владелец выберет одно, а посетитель увидит
   другое.

   ПОЧЕМУ `a.rc-btn`, А НЕ ПРОСТО `.rc-btn`. Текст владельца лежит внутри
   `.rich-body`, а там есть правило `.rich-body a{color:…;text-decoration:
   underline}`. По весу `.rich-body a` (класс + тег) СИЛЬНЕЕ, чем
   `.rc-btn` (один класс), — кнопка вышла бы подчёркнутой ссылкой чужого
   цвета. `a.rc-btn` весит столько же и стоит в листе позже, поэтому
   побеждает. Виды записаны как `a.rc-btn.rc-btn-buy` — этот вес сильнее
   любого правила раздела. */
a.rc-btn{
  position:relative;
  display:inline-block;
  vertical-align:middle;
  margin:.18em .3em;
  padding:.62em 1.25em;
  border:1px solid transparent;
  border-radius:100px;
  font-size:.94rem;
  font-weight:700;
  line-height:1.25;
  letter-spacing:.01em;
  text-align:center;
  text-decoration:none;
  cursor:pointer;
  overflow:hidden;
  isolation:isolate;
  transition:transform .35s var(--ease), box-shadow .35s var(--ease),
             background .35s var(--ease), border-color .35s var(--ease);
}
/* Второй слой под буквами: наведение и блик. `z-index:-1` внутри
   `isolation:isolate` кладёт слой ПОВЕРХ собственной подложки кнопки, но
   ПОД её текст — ровно то, что нужно. */
a.rc-btn::before{
  content:'';
  position:absolute;
  inset:0;
  z-index:-1;
  opacity:0;
  transition:opacity .4s var(--ease);
}
a.rc-btn:hover::before{opacity:1}

/* Виды: «условие» кнопки — что она делает. */
a.rc-btn.rc-btn-buy{
  color:#02121a;
  background:linear-gradient(115deg,var(--cyan),#6ee7ff 42%,var(--violet));
  box-shadow:0 8px 30px rgba(34,230,255,.28),0 0 0 1px rgba(255,255,255,.16) inset;
}
a.rc-btn.rc-btn-buy::before{background:linear-gradient(115deg,var(--magenta),var(--violet),var(--cyan))}
a.rc-btn.rc-btn-buy:hover{box-shadow:0 14px 44px rgba(34,230,255,.45),0 0 30px rgba(139,92,255,.5)}

a.rc-btn.rc-btn-order{
  color:#1a0413;
  background:linear-gradient(115deg,var(--magenta),#ff8ae4 42%,var(--violet));
  box-shadow:0 8px 30px rgba(255,79,216,.3),0 0 0 1px rgba(255,255,255,.16) inset;
}
a.rc-btn.rc-btn-order::before{background:linear-gradient(115deg,var(--violet),var(--cyan),var(--magenta))}
a.rc-btn.rc-btn-order:hover{box-shadow:0 14px 44px rgba(255,79,216,.45),0 0 30px rgba(139,92,255,.5)}

a.rc-btn.rc-btn-ask{
  color:var(--ink);
  background:transparent;
  border-width:2px;
  border-color:rgba(34,230,255,.45);
}
a.rc-btn.rc-btn-ask::before{background:linear-gradient(115deg,rgba(34,230,255,.22),rgba(139,92,255,.22))}
a.rc-btn.rc-btn-ask:hover{border-color:var(--cyan);box-shadow:0 0 28px rgba(34,230,255,.34)}

a.rc-btn.rc-btn-more{
  color:var(--cyan);
  background:rgba(34,230,255,.08);
  border-color:rgba(34,230,255,.38);
}
a.rc-btn.rc-btn-more::before{background:rgba(34,230,255,.18)}
a.rc-btn.rc-btn-more:hover{color:#02121a;background:var(--cyan);border-color:var(--cyan)}

a.rc-btn.rc-btn-write{
  color:#04180f;
  background:linear-gradient(115deg,var(--lime),#b6ffd9 45%,var(--cyan));
  box-shadow:0 8px 30px rgba(107,255,184,.26),0 0 0 1px rgba(255,255,255,.16) inset;
}
a.rc-btn.rc-btn-write::before{background:linear-gradient(115deg,var(--cyan),var(--lime),var(--magenta))}
a.rc-btn.rc-btn-write:hover{box-shadow:0 14px 44px rgba(107,255,184,.42),0 0 30px rgba(34,230,255,.4)}

a.rc-btn.rc-btn-own{
  color:var(--ink);
  background:linear-gradient(115deg,rgba(255,255,255,.08),rgba(255,255,255,.03));
  border-color:var(--line);
}
a.rc-btn.rc-btn-own::before{background:linear-gradient(115deg,rgba(139,92,255,.24),rgba(34,230,255,.24))}
a.rc-btn.rc-btn-own:hover{border-color:var(--line-strong);box-shadow:0 10px 30px rgba(0,0,0,.35)}

/* Движения. Тоже классом, а не `style`: `animation` в списке разрешённых
   свойств санитайзера нет намеренно — иначе через поле ввода можно было
   бы протащить любое оформление. У каждого движения СВОЁ имя ключевых
   кадров: два движения с одним именем — ловушка, поправив одно, владелец
   молча изменит и второе. */
a.rc-btn.rc-anim-pulse{animation:rcBtnPulse 2.2s var(--ease) infinite}
a.rc-btn.rc-anim-glow{animation:rcBtnGlow 2.6s var(--ease) infinite}
a.rc-btn.rc-anim-shine{animation:none}
a.rc-btn.rc-anim-lift{animation:rcBtnLift 2.8s var(--ease) infinite}
a.rc-btn.rc-anim-wiggle{animation:rcBtnWiggle 2.4s var(--ease) infinite}
a.rc-btn.rc-anim-bounce{animation:rcBtnBounce 1.9s var(--ease) infinite}
a.rc-btn.rc-anim-none{animation:none}

/* Блик ходит отдельным слоем: у самой кнопки подложка — градиент вида, и
   двигать её нельзя, иначе поедет цвет. */
a.rc-btn.rc-anim-shine::after{
  content:'';
  position:absolute;
  top:-40%;
  bottom:-40%;
  left:-70%;
  width:45%;
  z-index:-1;
  background:linear-gradient(105deg,rgba(255,255,255,0),rgba(255,255,255,.72),rgba(255,255,255,0));
  transform:skewX(-18deg);
  animation:rcBtnShine 3.2s linear infinite;
  pointer-events:none;
}

@keyframes rcBtnPulse{
  0%,100%{transform:scale(1)}
  50%{transform:scale(1.055)}
}
@keyframes rcBtnGlow{
  0%,100%{box-shadow:0 8px 26px rgba(34,230,255,.22)}
  50%{box-shadow:0 10px 42px rgba(34,230,255,.62),0 0 62px rgba(139,92,255,.44)}
}
@keyframes rcBtnShine{
  0%{left:-70%}
  100%{left:130%}
}
@keyframes rcBtnLift{
  0%,100%{transform:translateY(0)}
  50%{transform:translateY(-7px)}
}
@keyframes rcBtnWiggle{
  0%,100%{transform:rotate(0)}
  25%{transform:rotate(-3.2deg)}
  75%{transform:rotate(3.2deg)}
}
@keyframes rcBtnBounce{
  0%,100%{transform:translateY(0)}
  35%{transform:translateY(-9px)}
  60%{transform:translateY(-2px)}
  80%{transform:translateY(-5px)}
}

@media (max-width:720px){
  a.rc-btn{padding:.58em 1.05em;font-size:.9rem}
}

@media (prefers-reduced-motion:reduce){
  a.rc-btn,a.rc-btn::before{transition:none}
  a.rc-btn:hover{transform:none}
  a.rc-btn.rc-anim-pulse,a.rc-btn.rc-anim-glow,a.rc-btn.rc-anim-lift,
  a.rc-btn.rc-anim-wiggle,a.rc-btn.rc-anim-bounce{animation:none}
  a.rc-btn.rc-anim-shine::after{display:none}
}

/* ============================================================
   Отдельная страница сайта (/<адрес>)
   ============================================================
   Владелец: «Добавить в эти страницы возможность конструктора страниц где я
   могу добавлять информацию красиво и блоки и фото и видео».

   Блоки на странице — те же .wp-block, что на публикации, и оформление у них
   общее: на вопрос об оформлении новых блоков владелец ответил «в оформлении
   сайта». Значит, отдельного набора правил для них быть не должно — иначе
   блок выглядел бы на странице иначе, чем на публикации, и это расхождение
   пришлось бы ловить глазами.

   Здесь только своя ШАПКА СТРАНИЦЫ: название, подзаголовок и обложка. Своя
   она потому, что у публикации шапка — это карточка работы (кружок, срок,
   карусель кадров), а у страницы работы нет вовсе. Показать вместо неё чужой
   кружок с каруселью было бы враньём о содержимом страницы.
   ============================================================ */
.pg-hero{display:flex;flex-direction:column;gap:14px}
/* Плашка над названием растягивалась во всю ширину колонки. Причина: .pg-hero —
   flex-колонка, у неё align-items по умолчанию stretch, и ребёнок inline-flex
   растягивается вместе с ней (в блоке .sec-head того же не происходит: там
   родитель не flex, и плашка остаётся по содержимому). Владелец показал это
   на снимке: «Плашка над названием она слишком длинная, так не должно быть».
   По содержимому её ставит ТОЛЬКО align-self: менять align-items всей колонки
   нельзя — вместе с плашкой сожмётся обложка (.pg-cover с картинкой
   width:100%), а max-width:100% не даёт длинной строке вылезти за колонку. */
.pg-hero .eyebrow{align-self:flex-start;max-width:100%}
/* Название и подзаголовок страницы слушают те же переменные, что шапка
   блока и раздела сайта (--blk-title / --blk-head): владелец просил, чтобы
   название и описание страницы правились «согласно стилю сайта». Ставит
   их сервер и только когда владелец их задал, — поэтому по умолчанию
   работает прежний вид, и уже заведённые страницы не меняются. */
.pg-title{
  margin:0;font-size:var(--blk-title,clamp(1.7rem,4.2vw,2.6rem));line-height:1.12;
  letter-spacing:-.01em;color:var(--blk-head,var(--ink));
}
.pg-lead{margin:0;max-width:62ch;font-size:1.02rem;line-height:1.6;
  color:var(--blk-head,var(--ink-2))}
.pg-cover{
  margin:6px 0 0;border-radius:22px;overflow:hidden;
  border:1px solid rgba(120,160,220,.18);
  background:rgba(255,255,255,.02);
}
/* Обложка обрезается по высоте: владелец загружает и вертикальные снимки, и
   они отодвинули бы все блоки страницы на две высоты экрана. */
.pg-cover img{display:block;width:100%;height:auto;max-height:60vh;object-fit:cover}
.pg-body{margin-top:30px}

@media (max-width:720px){
  .pg-cover{border-radius:16px}
  .pg-cover img{max-height:42vh}
}

/* ============================================================
   Блоки конструктора страниц сайта
   ============================================================
   Владелец: «Добавить в эти страницы возможность конструктора страниц где
   я могу добавлять информацию красиво и блоки и фото и видео... Добавить
   больше 10 видов блоков... Обязательно сделай такой блок чтобы я мог его
   самостоятельно редактировать в плане карточки описание фото и видео и
   кнопки и плашки и анимации и эффекты там чтобы были».

   Оформление — САЙТА, а не своё. На вопрос об оформлении новых блоков
   владелец ответил «в оформлении сайта». Поэтому здесь нет ни одного
   своего цвета: всё берётся из переменных --cyan/--violet/--ink/--line, и
   переключатель оформления сайта меняет эти блоки вместе со всем
   остальным. Заведи мы свой набор — блок выпадал бы из палитры при первой
   же смене оформления.

   Где можно — переиспользуем ГОТОВЫЕ классы сайта (.btn, .sec-head, .faq/.q,
   .rev-card, .contacts-list/.c-item, .marquee, .field, .form). Свои правила
   ниже — только там, где готового класса нет: сетка карточек, шаги,
   вкладки, тарифы, таблица, цифры, плашка, калькулятор.
   ============================================================ */

/* Широкий блок больше НЕ выходит за полосу.

   Раньше он вырывался на всю ширину окна отрицательными полями с обеих
   сторон, а внутри заводил .wrap. Смысл был один: сетка из трёх карточек
   в колонке 780px читалась бы столбиком.

   Полоса стала 1200 (см. пояснение у .wp-wrap) — причина отпала. И
   вместе с ней ушёл разнобой: замерено на сайте владельца при окне 1440 —
   текст 780, «широкие» 1440, то есть на одной странице две разные ширины,
   и он показал это снимком.

   Теперь блок занимает ровно полосу, как и все прочие: 1200. Внутренний
   .wrap у широких блоков остаётся в разметке (его ставит сервер) и
   считается от полосы — то есть тоже 1200. Правило не удалено, а
   обнулено: класс живёт в разметке, и сторож на него смотрит.

   Поля оставлены нулём, а не убраны совсем, нарочно: если у блока есть
   своё поле (у .pg-notice, у цитаты), оно объявлено отдельным правилом и
   обязано остаться — обнуление общего .wp-wide его не задевает. */
.wp-wide{margin-inline:0}

/* ---------- обложка страницы (блок) ----------
   Не путать с .pg-hero — это шапка САМОЙ страницы (название и подзаголовок
   из панели). Здесь блок, который владелец ставит внутрь страницы: свой
   заголовок, текст, две кнопки и картинка рядом. */
.pg-hero-block{display:grid;gap:28px;align-items:center;
  padding:clamp(24px,3.4vw,40px);border-radius:var(--r-lg);
  border:1px solid var(--line);background:linear-gradient(150deg,var(--surface),var(--surface-2))}
.pg-hero-block.has-media{grid-template-columns:minmax(0,1fr) minmax(0,.9fr)}
.pg-hero-block.is-center{text-align:center;justify-items:center}
.pg-hero-block.is-center .pg-hero-block-in{align-items:center}
.pg-hero-block-in{display:flex;flex-direction:column;gap:14px;align-items:flex-start;min-width:0}
.pg-h2{margin:0;font-size:clamp(1.5rem,3.2vw,2.15rem);line-height:1.14;letter-spacing:-.01em}
.pg-hero-cta{display:flex;gap:12px;flex-wrap:wrap;margin-top:4px}
.pg-fig{margin:0;border-radius:var(--r-md);overflow:hidden;border:1px solid var(--line)}
.pg-fig img{display:block;width:100%;height:auto;max-height:46vh;object-fit:cover}
@media (max-width:820px){
  .pg-hero-block.has-media{grid-template-columns:1fr}
  .pg-hero-block.is-center{text-align:left;justify-items:stretch}
}

/* ---------- карточки ----------
   Число колонок приходит атрибутом data-cols (его задаёт владелец в
   панели). auto-fit здесь не годится: он подгоняет колонки под ширину, и
   выбранные владельцем три колонки на широком экране превратились бы в
   четыре. Но на узком экране auto-fit НУЖЕН — иначе карточки сжались бы до
   нечитаемых полосок. Поэтому выбор владельца действует до первой тесной
   ширины, а дальше колонки считает браузер. */
.pg-card-grid{display:grid;gap:20px;
  grid-template-columns:repeat(var(--cols,3),minmax(0,1fr))}
.pg-cards[data-cols="1"]{--cols:1}
.pg-cards[data-cols="2"]{--cols:2}
.pg-cards[data-cols="3"]{--cols:3}
.pg-cards[data-cols="4"]{--cols:4}
@media (max-width:980px){.pg-card-grid{grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}}

.pg-card{
  position:relative;display:flex;flex-direction:column;gap:10px;
  padding:24px 22px;border-radius:var(--r-md);
  border:1px solid var(--line);background:var(--surface);
  transition:transform .35s var(--ease),box-shadow .35s var(--ease),border-color .35s var(--ease);
  will-change:transform;
}
.pg-card h3{margin:0;font-size:1.08rem}
.pg-card p{margin:0;color:var(--ink-2);font-size:.92rem;font-weight:300;line-height:1.6}
.pg-card-ico{
  width:46px;height:46px;flex:none;border-radius:14px;display:grid;place-items:center;
  border:1px solid rgba(34,230,255,.3);background:rgba(34,230,255,.07);color:var(--cyan);
}
.pg-card-ico svg{width:22px;height:22px}
.pg-card-ico.has-img{padding:0;overflow:hidden;border-color:var(--line);background:none}
.pg-card-ico.has-img img{width:100%;height:100%;object-fit:cover}
.pg-card-badge{
  align-self:flex-start;padding:5px 11px;border-radius:999px;font-size:.72rem;
  font-weight:700;letter-spacing:.04em;color:var(--violet);
  border:1px solid rgba(139,92,255,.34);background:rgba(139,92,255,.1);
}
.pg-card-go{
  margin-top:auto;padding-top:8px;font-size:.86rem;font-weight:700;
  color:var(--cyan);text-decoration:none;
}
.pg-card-go:hover{text-shadow:0 0 14px rgba(34,230,255,.7)}

/* Движение карточки — выбор владельца (data-fx). Видов четыре, и это
   закрытый список с сервера: панель новее сервера не должна получить
   возможность прислать вид, которого сервер не знает. */
.pg-card[data-fx="lift"]:hover{transform:translateY(-7px);border-color:var(--line-strong);
  box-shadow:0 20px 50px rgba(0,0,0,.42)}
.pg-card[data-fx="glow"]:hover{border-color:rgba(34,230,255,.45);box-shadow:var(--glow-cyan)}
/* Наклон за курсором считает js/work.js: углы зависят от того, где стоит
   курсор, а этого в оформлении нет и быть не может. Скрипт кладёт только
   два угла (--tilt-x/--tilt-y), а поворот и перспектива — здесь: владелец
   может поправить вид наклона, не трогая код.

   Две скорости перехода, и это не мелочь. Пока курсор на карточке, угол
   должен поспевать за ним — иначе карточка «плывёт» с запозданием на
   четверть секунды. А когда курсор ушёл, углы снимаются, и возврат обязан
   быть медленным и мягким; на быстрой скорости это выглядело бы щелчком. */
.pg-card[data-fx="tilt"]{
  transform-style:preserve-3d;
  transform:perspective(820px) rotateX(var(--tilt-x,0deg)) rotateY(var(--tilt-y,0deg));
  transition:transform .5s var(--ease),box-shadow .35s var(--ease),border-color .35s var(--ease);
}
.pg-card[data-fx="tilt"]:hover{
  box-shadow:0 20px 50px rgba(0,0,0,.42);
  transition:transform .16s linear,box-shadow .35s var(--ease),border-color .35s var(--ease);
}

/* ---------- цифры ---------- */
.pg-stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:26px}
.pg-stat{padding-left:20px;position:relative}
.pg-stat::before{content:'';position:absolute;left:0;top:6px;bottom:6px;width:2px;border-radius:2px;
  background:linear-gradient(180deg,var(--cyan),var(--violet))}
.pg-stat b{display:block;font-size:clamp(1.7rem,3vw,2.4rem);font-weight:800;
  letter-spacing:-.04em;line-height:1;
  background:linear-gradient(120deg,var(--cyan),var(--violet));
  -webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
.pg-stat small{display:block;margin-top:9px;color:var(--ink-3);font-size:.78rem;
  letter-spacing:.06em;text-transform:uppercase}

/* ---------- шаги ----------
   Своя линия времени, а не .steps сайта: у того правила завязаны на разметку
   главной (.step .dot, ::before у контейнера), и внутри колонки страницы он
   нарисовал бы линию не там. Своё оформление здесь честнее, чем подгонка
   чужого. */
.pg-step-list{position:relative;display:grid;gap:0;margin-top:8px}
.pg-step-list::before{content:'';position:absolute;left:26px;top:14px;bottom:14px;width:1px;
  background:linear-gradient(180deg,transparent,var(--line-strong) 12%,var(--line-strong) 88%,transparent)}
.pg-step{position:relative;display:grid;grid-template-columns:54px 1fr;gap:22px;
  padding:18px 0;align-items:start}
.pg-step-num{
  width:54px;height:54px;flex:none;border-radius:50%;display:grid;place-items:center;
  font-weight:800;font-size:.92rem;letter-spacing:-.02em;color:var(--cyan);
  border:1px solid rgba(34,230,255,.34);background:var(--bg-2);
  box-shadow:0 0 0 6px var(--bg-2);
}
.pg-step-body h3{margin:10px 0 7px;font-size:1.04rem}
.pg-step-body p{margin:0;color:var(--ink-2);font-size:.93rem;font-weight:300;max-width:640px}
@media (max-width:600px){
  .pg-step{grid-template-columns:42px 1fr;gap:16px}
  .pg-step-num{width:42px;height:42px;font-size:.82rem}
  .pg-step-list::before{left:20px}
}

/* ---------- вкладки ---------- */
.pg-tab-btns{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:22px}
.pg-tab-btn{
  padding:10px 18px;border-radius:999px;cursor:pointer;font:inherit;
  font-size:.88rem;font-weight:700;color:var(--ink-2);
  border:1px solid var(--line);background:var(--surface);
  transition:color .25s var(--ease),border-color .25s var(--ease),background .25s var(--ease);
}
.pg-tab-btn:hover{color:var(--ink);border-color:var(--line-strong)}
.pg-tab-btn.is-on{
  color:#04060c;border-color:transparent;
  background:linear-gradient(120deg,var(--cyan),var(--violet));
}
.pg-tab-btn:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
.pg-tab-panel{padding:22px 24px;border-radius:var(--r-md);
  border:1px solid var(--line);background:var(--surface)}
.pg-tab-panel[hidden]{display:none}
.pg-tab-panel > :first-child{margin-top:0}
.pg-tab-panel > :last-child{margin-bottom:0}

/* ---------- тарифы ---------- */
.pg-price-grid{display:grid;gap:20px;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));
  align-items:stretch}
.pg-price{
  position:relative;display:flex;flex-direction:column;gap:12px;
  padding:26px 24px;border-radius:var(--r-lg);
  border:1px solid var(--line);background:var(--surface);
}
.pg-price.is-featured{border-color:rgba(34,230,255,.42);background:rgba(34,230,255,.05);
  box-shadow:0 0 40px rgba(34,230,255,.12)}
.pg-price-tag{
  position:absolute;top:-11px;left:24px;padding:4px 12px;border-radius:999px;
  font-size:.7rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase;
  color:#04060c;background:linear-gradient(120deg,var(--cyan),var(--violet));
}
.pg-price h3{margin:0;font-size:1.05rem}
.pg-price-val{font-size:clamp(1.6rem,2.6vw,2.1rem);font-weight:800;letter-spacing:-.03em;line-height:1}
.pg-price-val small{display:block;margin-top:6px;font-size:.78rem;font-weight:600;
  letter-spacing:.04em;color:var(--ink-3)}
.pg-price-feats{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:9px;
  color:var(--ink-2);font-size:.9rem;font-weight:300}
.pg-price-feats li{position:relative;padding-left:22px}
.pg-price-feats li::before{content:'';position:absolute;left:0;top:.52em;width:9px;height:5px;
  border-left:1.8px solid var(--cyan);border-bottom:1.8px solid var(--cyan);
  transform:rotate(-45deg)}
.pg-price .btn{margin-top:auto;align-self:flex-start}

/* ---------- таблица ----------
   Обёртка с прокруткой: широкая таблица на телефоне иначе растянула бы
   страницу вбок. Прокручивается САМА таблица, а не страница. */
.pg-table-wrap{overflow-x:auto;border-radius:var(--r-md);border:1px solid var(--line)}
.pg-table-wrap table{min-width:520px}
.pg-table-wrap th,.pg-table-wrap td{padding:13px 18px;text-align:left;
  border-bottom:1px solid var(--line);font-size:.92rem}
.pg-table-wrap th{font-size:.76rem;letter-spacing:.1em;text-transform:uppercase;
  color:var(--ink-3);font-weight:700;background:rgba(255,255,255,.02)}
.pg-table-wrap td{color:var(--ink-2)}
.pg-table-wrap tbody tr:last-child td{border-bottom:0}

/* ---------- отзывы ---------- */
.pg-rev-grid{display:grid;gap:18px;grid-template-columns:repeat(auto-fit,minmax(300px,1fr))}
.pg-rev-grid .rev-card{height:100%}
.pg-rev-grid .rev-face.has-img{padding:0;overflow:hidden}
.pg-rev-grid .rev-face img{width:100%;height:100%;object-fit:cover}

/* ---------- плашка ---------- */
.pg-notice{
  display:flex;gap:14px;align-items:flex-start;
  padding:18px 22px;border-radius:var(--r-md);border:1px solid var(--line);
  background:var(--surface);
}
.pg-notice-ico{width:38px;height:38px;flex:none;border-radius:12px;display:grid;place-items:center}
.pg-notice-ico svg{width:19px;height:19px}
.pg-notice-body{min-width:0}
.pg-notice-body b{display:block;margin-bottom:5px;font-size:.98rem}
.pg-notice-body p{margin:0;color:var(--ink-2);font-size:.9rem;font-weight:300;line-height:1.6}
.pg-notice-body p + p{margin-top:8px}
/* Три тона — закрытый список с сервера. Плашка «внимание» и плашка
   «готово» обязаны отличаться не только словом: цвет читается быстрее. */
.pg-notice[data-tone="info"] .pg-notice-ico{color:var(--cyan);
  border:1px solid rgba(34,230,255,.3);background:rgba(34,230,255,.08)}
.pg-notice[data-tone="info"]{border-color:rgba(34,230,255,.28)}
.pg-notice[data-tone="warn"] .pg-notice-ico{color:var(--magenta);
  border:1px solid rgba(255,79,216,.3);background:rgba(255,79,216,.08)}
.pg-notice[data-tone="warn"]{border-color:rgba(255,79,216,.28)}
.pg-notice[data-tone="ok"] .pg-notice-ico{color:var(--lime);
  border:1px solid rgba(107,255,184,.3);background:rgba(107,255,184,.08)}
.pg-notice[data-tone="ok"]{border-color:rgba(107,255,184,.28)}

/* ---------- калькулятор ----------
   Считает js/work.js по ставкам владельца (они в настройках сайта). Пока
   скрипт не загрузился, шаги, кнопки и кнопка отправки на месте — страница
   не выглядит сломанной, а сумма показывает «—», а не выдуманное число.

   Раскладка — как в образце владельца: слева столбец шагов, справа окно с
   итогом. Шаг = номер в кружке + подпись + поле. Номер в кружке не
   украшение: он говорит, в каком порядке заполнять, и по нему владелец
   объясняет заказчику по телефону «заполните третий шаг». */
.pg-calc-box{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(0,1fr);gap:26px;
  padding:26px;border-radius:var(--r-lg);border:1px solid var(--line);background:var(--surface)}
.pg-calc-fields{display:flex;flex-direction:column;gap:22px}

.calc-step{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:start}
.calc-step-no{width:26px;height:26px;border-radius:50%;display:grid;place-items:center;
  font-size:.76rem;font-weight:800;color:var(--cyan);
  border:1px solid rgba(34,230,255,.35);background:rgba(34,230,255,.08)}
.calc-step-body{display:flex;flex-direction:column;gap:9px;min-width:0}
/* Названия шагов и строка массы — своя роль: владелец задаёт им размер и
   цвет в панели (--calc-step-*, --calc-scale-*). Нет переменной — работает
   прежнее оформление, поэтому блоки, которых владелец не касался, выглядят
   ровно как раньше. */
.calc-step-label{font-size:var(--calc-step-size,.74rem);letter-spacing:.1em;text-transform:uppercase;
  color:var(--calc-step-color,var(--ink-3));font-weight:700}
.calc-step-hint{font-size:var(--calc-scale-size,.82rem);
  color:var(--calc-scale-color,var(--ink-2));font-weight:600}
.calc-step-hint:empty{display:none}

/* Выбор кнопками. Кнопка показывает все варианты сразу — список прячет их
   за нажатием, а у технологий владельца их единицы. */
.calc-chips{display:flex;flex-wrap:wrap;gap:8px}
.calc-chip{padding:9px 15px;border-radius:999px;cursor:pointer;
  border:1px solid var(--line);background:rgba(255,255,255,.03);
  color:var(--ink-2);font-size:.86rem;font-weight:600;line-height:1.2;
  font-family:inherit;transition:border-color .2s var(--ease),color .2s var(--ease),
  background .2s var(--ease),box-shadow .2s var(--ease)}
.calc-chip:hover{border-color:var(--line-strong);color:var(--ink)}
.calc-chip[aria-pressed="true"]{color:#04121a;border-color:transparent;
  background:linear-gradient(120deg,var(--cyan),#7ef2ff);box-shadow:var(--glow-cyan)}

/* Бегунок с числом. Числом удобно вписать точный объём из модели, бегунком
   — прикинуть на глазок; поэтому они рядом и ведут друг друга. */
.calc-range{display:flex;align-items:center;gap:12px}
.calc-range input[type="range"]{flex:1;min-width:0;accent-color:var(--cyan);
  height:22px;cursor:pointer}
.calc-num{width:88px;flex:none;padding:8px 10px;border-radius:var(--r-sm);
  border:1px solid var(--line);background:rgba(6,10,20,.6);color:var(--ink);
  font:inherit;font-size:.92rem;text-align:center}
.calc-num:focus{outline:none;border-color:var(--line-strong);box-shadow:var(--glow-cyan)}
.calc-unit{color:var(--calc-scale-color,var(--ink-3));
  font-size:var(--calc-scale-size,.82rem);flex:none}
.calc-step-btn{width:30px;height:30px;flex:none;border-radius:8px;cursor:pointer;
  border:1px solid var(--line);background:rgba(255,255,255,.03);color:var(--ink-2);
  font:inherit;font-size:1rem;line-height:1;display:grid;place-items:center}
.calc-step-btn:hover{border-color:var(--line-strong);color:var(--ink)}
.calc-scale{display:flex;justify-content:space-between;gap:10px;
  font-size:var(--calc-scale-size,.72rem);color:var(--calc-scale-color,var(--ink-3))}
.calc-step-body select{width:100%;padding:10px 12px;border-radius:var(--r-sm);
  border:1px solid var(--line);background:rgba(6,10,20,.6);color:var(--ink);
  font:inherit;font-size:.9rem}

/* Окно с итогом. Число крупное: это то, за чем посетитель пришёл. */
.pg-calc-out{display:flex;flex-direction:column;gap:10px;justify-content:center;
  padding:22px;border-radius:var(--r-md);border:1px solid var(--line-strong);
  background:linear-gradient(150deg,rgba(34,230,255,.08),rgba(139,92,255,.06))}
.pg-calc-out-label{font-size:var(--calc-out-size,.76rem);letter-spacing:.12em;
  text-transform:uppercase;color:var(--calc-out-color,var(--ink-3))}
/* Крупным числом должна быть ТОЛЬКО сумма. Правило «.pg-calc-out b» метило
   во ВСЕ жирные внутри окна — включая значения строк «срок» и «подготовка»,
   и те вырастали до 40px: строка раздувалась до 58px, подписи уезжали
   вверх и пары «подпись — значение» переставали читаться. Поэтому размер
   задаём ровно сумме, а не всякой жирной букве в окне. */
.pg-calc-out [data-calc-sum]{font-size:clamp(1.8rem,3.4vw,2.5rem);font-weight:800;letter-spacing:-.04em;line-height:1;
  color:var(--ink)}
.pg-calc-note{color:var(--ink-3);font-size:.82rem;line-height:1.5}
/* «Цена за 1 шт» и «Скидка за партию» — подписи итога: у них своя роль
   размера и цвета. Скидка остаётся зелёной, пока владелец не задал свой
   цвет: это акцент, и терять его без причины нельзя. */
.pg-calc-hint{color:var(--calc-out-color,var(--ink-2));
  font-size:var(--calc-out-size,.82rem);font-weight:600;letter-spacing:.01em}
.pg-calc-hint:empty{display:none}
.pg-calc-save{color:var(--calc-out-color,var(--lime))}
/* Строки «срок» и «подготовка». Разделителем служит линия, а не отступ:
   так две пары «подпись — значение» читаются как таблица, а не как абзац. */
.pg-calc-rows{display:flex;flex-direction:column;gap:0;margin:4px 0;
  border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
.pg-calc-rows>span{display:flex;justify-content:space-between;gap:12px;
  padding:9px 0;font-size:var(--calc-out-size,.84rem);
  color:var(--calc-out-color,var(--ink-3))}
.pg-calc-rows>span+span{border-top:1px solid var(--line)}
.pg-calc-rows>span b{color:var(--cyan);font-weight:700;text-align:right}
.pg-calc-out .btn{align-self:stretch;margin-top:6px}
@media (max-width:820px){.pg-calc-box{grid-template-columns:1fr}}
@media (max-width:460px){
  .calc-scale{font-size:.68rem}
  .calc-num{width:72px}
  .pg-calc-box{padding:18px}
}

/* ---------- форма заявки ---------- */
.pg-form-block .form{max-width:640px}
.pg-form-note{margin:14px 0 0;color:var(--ink-3);font-size:.84rem;line-height:1.55}
.pg-form-hint{display:block;margin-top:6px;color:var(--ink-3);font-size:.78rem}

/* ---------- экспресс-оценка ----------
   Окно-карточка вокруг всей формы: владелец просил «ИМЕННО ТАКУЮ», а в
   образце форма стоит внутри заметной рамки со своим заголовком. Рамка не
   украшение — она отделяет приём модели от остального текста страницы, и
   посетитель видит, где заканчивается рассказ и начинается дело. */
.pg-express .exp-card{padding:clamp(20px,3vw,34px);border-radius:var(--r-lg);
  border:1px solid var(--line-strong);
  background:linear-gradient(160deg,rgba(34,230,255,.05),rgba(139,92,255,.04) 60%,transparent)}
/* Шапка блока — НАД рамкой, как у калькулятора и у разделов сайта.

   Владелец: «нужно перенести наверх … соблюдай стилистику как у всех
   блоков». Раньше шапка лежала ВНУТРИ .exp-card, и её приходилось
   подгонять: мельче заголовок, узкая середина, свой отступ. Теперь она
   стоит на самой странице, и подгонять нечего — работает то же правило
   .sec-head, что у каждого раздела. Отсюда и пустое место вместо
   прежних строк: оставь их — вышло бы «как у всех, но чуть-чуть иначе»,
   ровно то, на что владелец и жаловался. */
.exp-eyebrow{display:inline-block;padding:6px 14px;border-radius:999px;margin-bottom:14px;
  border:1px solid rgba(34,230,255,.35);background:rgba(34,230,255,.08);
  color:var(--cyan);font-size:.7rem;font-weight:800;letter-spacing:.14em;text-transform:uppercase}

/* Рамка перетаскивания. Поле выбора файла растянуто на всю рамку и
   прозрачно: так нажатие в любом месте открывает выбор файла, а не только
   по слову «выберите». */
.exp-drop{position:relative;display:flex;flex-direction:column;align-items:center;gap:8px;
  padding:30px 22px;margin-bottom:20px;text-align:center;cursor:pointer;
  border-radius:var(--r-md);border:1.5px dashed var(--line-strong);
  background:rgba(255,255,255,.02);
  transition:border-color .22s var(--ease),background .22s var(--ease)}
.exp-drop:hover,.exp-drop.over{border-color:var(--cyan);background:rgba(34,230,255,.06)}
.exp-drop input[type="file"]{position:absolute;inset:0;width:100%;height:100%;
  opacity:0;cursor:pointer}
.exp-drop-ico{display:grid;place-items:center;width:52px;height:52px;border-radius:14px;
  border:1px solid rgba(34,230,255,.35);background:rgba(34,230,255,.1);color:var(--cyan)}
.exp-drop-ico svg{width:26px;height:26px;fill:none;stroke:currentColor;stroke-width:1.6;
  stroke-linecap:round;stroke-linejoin:round}
/* Размер и цвет подписей экспресс-оценки — четыре роли, задаёт владелец в
   панели (--exp-lab-*, --exp-inp-*, --exp-hint-*, --exp-agree-*). Нет
   переменной — работает прежнее оформление. */
.exp-drop-title{font-size:var(--exp-lab-size,1.02rem);font-weight:700;
  color:var(--exp-lab-color,var(--ink))}
.exp-drop-hint{font-size:var(--exp-hint-size,.8rem);
  color:var(--exp-hint-color,var(--ink-3));max-width:520px}
.exp-files{font-size:.8rem;color:var(--lime);font-weight:600;text-align:left;
  max-width:100%;word-break:break-word}
.exp-files:empty{display:none}

.exp-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.exp-field{display:flex;flex-direction:column;gap:7px}
.exp-field-wide{grid-column:1/-1}
.exp-field>span{font-size:var(--exp-lab-size,.74rem);letter-spacing:.1em;
  text-transform:uppercase;color:var(--exp-lab-color,var(--ink-3));font-weight:700}
.exp-field input,.exp-field textarea{width:100%;padding:12px 14px;border-radius:var(--r-sm);
  border:1px solid var(--line);background:rgba(6,10,20,.6);
  color:var(--exp-inp-color,var(--ink));
  font:inherit;font-size:var(--exp-inp-size,.92rem);resize:vertical}
.exp-field input:focus,.exp-field textarea:focus{outline:none;
  border-color:var(--line-strong);box-shadow:var(--glow-cyan)}
/* Подсказка внутри пустого поля. Идёт за цветом текста, но приглушена —
   ровно настолько, чтобы пустое поле отличалось от заполненного. Яркость
   приглушения приходит с сервера ВМЕСТЕ с цветом (--exp-inp-op): без
   настройки прозрачность не трогаем вовсе, иначе изменился бы вид блока,
   которого владелец не касался. */
.exp-field input::placeholder,.exp-field textarea::placeholder{
  color:var(--exp-inp-color,var(--ink-3));opacity:var(--exp-inp-op,1)}

/* Каналы связи — КНОПКИ ВЫБОРА, а не ссылки.

   Владелец: «надо сделать чтобы они ни куда не вели, а просто нажимались
   эффектно и с анимацией … клиент выберает одну или несколько кнопок вида
   связи и CRM заявке я вижу что с ним надо связаться».

   Раньше канал со ссылкой был <a> и уводил в мессенджер: посетитель писал
   там, а в панели заявки не появлялось. Теперь это <button>, и выбор виден
   и посетителю (подсветка), и владельцу (строка в заявке).

   Состояние читаем по aria-pressed, а не по классу: тот же признак видит и
   скрипт, и экранный диктор, — «выбрано» не приходится держать в двух
   местах. */
.exp-chans-wrap{display:flex;flex-direction:column;gap:8px;margin-top:16px}
.exp-label{font-size:var(--exp-lab-size,.74rem);letter-spacing:.1em;
  text-transform:uppercase;color:var(--exp-lab-color,var(--ink-3));font-weight:700}
.exp-chans-hint{font-size:var(--exp-hint-size,.76rem);
  color:var(--exp-hint-color,var(--ink-3))}
.exp-chans{display:flex;flex-wrap:wrap;gap:10px}
/* Кнопка, а не ссылка: снимаем вид кнопки браузера целиком, иначе на ней
   остались бы системный шрифт, серая подложка и рамка. */
.exp-chan{position:relative;overflow:hidden;display:flex;align-items:center;gap:10px;
  padding:11px 15px;cursor:pointer;font:inherit;text-align:left;
  border-radius:var(--r-sm);border:1px solid var(--line);background:rgba(255,255,255,.02);
  color:var(--ink-2);text-decoration:none;
  transition:border-color .2s var(--ease),color .2s var(--ease),
    background .2s var(--ease),transform .16s var(--ease)}
.exp-chan:hover{border-color:var(--line-strong);color:var(--ink)}
.exp-chan svg{width:20px;height:20px;flex:none;fill:none;stroke:currentColor;
  stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
.exp-chan b{display:block;font-size:var(--exp-lab-size,.88rem);font-weight:700;
  color:var(--exp-lab-color,currentColor)}
.exp-chan i{display:block;font-size:var(--exp-hint-size,.74rem);font-style:normal;
  color:var(--exp-hint-color,var(--ink-3))}
.exp-chan-box{position:relative;display:flex;align-items:center;gap:10px;
  pointer-events:none;z-index:1}
.exp-chan:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
/* Нажатие обязано быть СЛЫШНО руке: кнопка на мгновение уходит вниз. Это
   не украшение — без отдачи владелец жаловался, что «не понял, нажалось
   ли». Свечение при этом не режется: box-shadow на самом элементе
   overflow:hidden не обрезает (обрезаются только потомки). */
.exp-chan:active{transform:scale(.97)}
.exp-chan[aria-pressed="true"]{
  border-color:var(--cyan);background:rgba(34,230,255,.1);
  color:var(--ink);box-shadow:var(--glow-cyan);
  /* Одноразовый «толчок» при выборе. Правило применяется ровно в тот
     момент, когда атрибут становится true, — поэтому анимация и
     запускается, и не крутится постоянно. */
  animation:chanPop .34s var(--ease)}
.exp-chan[aria-pressed="true"] b{color:var(--cyan)}
/* Блик по выбранной кнопке. Живёт на потомке, а не на самой кнопке:
   `animation` у элемента один, и второй правило затёрло бы «толчок». */
.exp-chan[aria-pressed="true"] .exp-chan-box::after{
  content:'';position:absolute;inset:-11px -15px;z-index:-1;
  background:linear-gradient(105deg,transparent 35%,rgba(34,230,255,.22) 50%,transparent 65%);
  background-size:220% 100%;
  animation:chanSheen 2.8s linear infinite}
@keyframes chanPop{
  0%{transform:scale(1)}
  45%{transform:scale(1.05)}
  100%{transform:scale(1)}
}
@keyframes chanSheen{from{background-position:120% 0}to{background-position:-120% 0}}
/* Отключённый выбор: кнопок нет вовсе, но если владелец включит режим
   обратно — блок обязан появиться сразу, без правки оформления. */
.exp-chans-wrap[data-chan-mode="off"]{display:none}

.exp-foot{display:flex;align-items:center;justify-content:space-between;gap:18px;
  margin-top:22px;flex-wrap:wrap}
/* Строка согласия — в ОДИН ряд.

   Владелец: «надо чтобы был в один ряд текст, а не в два ряда». Здесь
   стоял жёсткий потолок в 520 точек, и подпись «Согласен с политикой
   конфиденциальности и обработкой персональных данных» ломалась на две
   строки, хотя места рядом с кнопкой было почти вдвое больше.

   Потолок убран, вместо него — «занять всё, что осталось от кнопки»
   (flex:1 1 auto). Перенос остаётся возможным, но теперь он случается
   только тогда, когда строка и правда не влезает: на телефоне, где
   подвал формы разворачивается в столбик. min-width:0 обязателен —
   без него поле flex не даёт содержимому сжаться и перенос не
   наступает даже на узком экране, а блок вылезает за карточку. */
.exp-agree{display:flex;flex-direction:column;gap:6px;flex:1 1 auto;min-width:0}
.exp-check{display:flex;align-items:center;gap:10px;cursor:pointer;
  font-size:var(--exp-agree-size,.86rem);color:var(--exp-agree-color,var(--ink-2))}
.exp-check input{width:18px;height:18px;flex:none;accent-color:var(--cyan)}
/* Обязательная галочка не отмечена — обработчик формы подсвечивает её этим
   классом. Без подсветки посетитель видит «ничего не произошло»: кнопка
   нажата, заявка не ушла, причина не названа. */
.exp-check.is-bad{color:var(--magenta)}
.exp-check.is-bad input{outline:2px solid rgba(255,79,216,.7);outline-offset:2px}
.exp-pol{font-size:var(--exp-agree-size,.78rem);color:var(--cyan);text-decoration:underline;
  text-underline-offset:3px;align-self:flex-start}
.exp-foot .btn{flex:none;min-width:240px}
.pg-express .form-msg{margin-top:14px}

@media (max-width:760px){
  .exp-grid{grid-template-columns:1fr}
  .exp-foot{flex-direction:column;align-items:stretch}
  .exp-foot .btn{width:100%}
}
@media (prefers-reduced-motion:reduce){
  .exp-chan[aria-pressed="true"]{animation:none}
  .exp-chan[aria-pressed="true"] .exp-chan-box::after{animation:none;display:none}
}

/* ---------- контакты ---------- */
.pg-contacts .contacts-list{grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}

/* ---------- бегущая строка ----------
   Классы .marquee/.marquee-track — те же, что у раздела главной: полоса
   обязана выглядеть одинаково и там, и здесь. Своего здесь только отступ:
   у раздела он был нулевым (правило main > section#marquee), а блоку нужен
   воздух, как любому блоку в потоке. */
.pg-marquee{margin-block:6px}
.pg-marquee .marquee{border-radius:var(--r-md)}

/* ============================================================
   Карточка и окно товара: точки, стрелки и просмотр кадра (08.10)
   ============================================================
   Владелец (08.10) просил четыре вещи сразу:
     • «надо сделать меньше точек прокруток оставь по центру внизу три
       большие и по центру одна цветная а по бокам белые и следом справа и
       слева отних идут по две меньшего размера типо уменьшение и добавь в
       админ панель чтобы мог их включать показа и выключать и менять их
       размер и около 7 видов и цветов»;
     • «добавь в админ панель возможность их включать или выключать … показ
       только на компьютере и в планшетах или мобильных телефонах уберать их
       показ … при наведении курсора мыши появлялись эффектно с разными
       скоростями, а при не наведении на карточку курсора мыши стрелок нет»;
     • «чтобы их можно было свайпать и листать пальцем и курсором
       пролистывать»;
     • «сделай рамку внутри стрелки и вверху справа крестик закрыть и
       обязательно чтобы были у фото и у рамки скругления и чтобы в админ
       панели мог задавать размер этого окна».

   Здесь только «как это выглядит». Имена настроек приходят из закрытых
   списков на сервере (`SHOP_DOT_*`, `SHOP_ARROW_*`, `SHOP_FULL_*`), а
   страница ставит их атрибутами — как у плашки у цены, лупы и остатка.

   Раздел стоит В КОНЦЕ файла НАРОЧНО: он перебивает вид, собранный выше
   (`.car-dot`, `.car-nav`, `.pm-nav`, `.pm-full-stage`), и обязан выигрывать
   у них ПОРЯДКОМ, а не спором числом классов. */

/* ---------- точки под лентой товаров: окно из семи ----------

   На виду семь точек, а не столько, сколько товаров: три крупные в середине
   (средняя — цветная, это текущая) и по две мельче по бокам — «типо
   уменьшение». Сколько точек близко, решает скрипт (`data-d` и `data-far`),
   а размеры и краски — здесь.

   Привязка к `[data-dots-window]`, а не к `.car-dots`: точки есть и у ленты
   работ, и окно там не заводилось — у неё точки обязаны остаться прежними,
   ровными, без ступеней. По той же причине блок общий и для точек КАРТОЧКИ
   товара (08.10): у них с лентой общие вид, размер и цвет, а разное только
   число мест и основа размера. */
[data-dots-window]{
  --dot-k:1;                 /* ступень размера из панели */
  --dot-c:var(--cyan);       /* цвет ТЕКУЩЕЙ точки */
  --dot-base:8px;            /* основа, от которой считаются ступени */
  gap:9px;
}
/* Точки КАРТОЧКИ товара мельче точек ленты: карточка ростом с ладонь, и
   прежние восемь пикселей заняли бы треть её ширины. Основа своя, а ступени
   размера, вид и цвет — общие с лентой, как и просил владелец. */
.car-fdots[data-dots-window]{--dot-base:calc(5px * var(--card-k,1))}
/* Свой цвет задан — берём его из общей палитры магазина (`--stk-c`):
   владелец выбирает цвет из того же списка, что у плашки у цены и у лупы, и
   второй палитры для точек быть не должно. */
[data-dots-window][data-dots-color]{--dot-c:var(--stk-c,var(--cyan))}
[data-dots-window][data-dots-size="s"] {--dot-k:.78}
[data-dots-window][data-dots-size="l"] {--dot-k:1.24}
[data-dots-window][data-dots-size="xl"]{--dot-k:1.52}

[data-dots-window] .car-dot{
  --dk:1;                    /* ступень близости к текущей */
  --rx:1;--ry:1;             /* вытянутость: у полоски и чёрточки своя */
  --dot-bg:rgba(255,255,255,.88);
  --dot-line:rgba(255,255,255,.88);
  --dot-bd:0px;
  width:calc(var(--dot-base) * var(--dot-k) * var(--dk) * var(--rx));
  height:calc(var(--dot-base) * var(--dot-k) * var(--dk) * var(--ry));
  background:var(--dot-bg);
  border:var(--dot-bd) solid var(--dot-line);
  border-radius:50%;
  /* Ступень гасит и прежнее `transform:scale(1.5)` у текущей точки: рост
     теперь выражается РАЗМЕРОМ, и две правки одного вида спорили бы. */
  transform:none;
  transition:width .32s var(--ease),height .32s var(--ease),
    background .32s var(--ease),border-color .32s var(--ease),
    opacity .32s var(--ease);
}
/* Три крупные в середине: текущая и две соседние. Дальше — ступень вниз, по
   две на каждой стороне. */
[data-dots-window] .car-dot[data-d="0"],
[data-dots-window] .car-dot[data-d="1"]{--dk:1.5}
[data-dots-window] .car-dot[data-d="2"]{--dk:1.14}
[data-dots-window] .car-dot[data-d="3"]{--dk:.86}
/* Цветная — ТОЛЬКО текущая; соседние белые, как и просил владелец. */
[data-dots-window] .car-dot[data-d="0"]{--dot-bg:var(--dot-c)}
/* Дальняя точка уезжает с глаз: её места в окне нет. Прячем `display:none`,
   а не прозрачностью: прозрачная продолжала бы занимать своё место вместе с
   зазором, и «уменьшение» читалось бы как ряд дырок. */
[data-dots-window] .car-dot[data-far]{display:none}

/* Виды точек — семь, как в списке на сервере. Форму задаёт сама точка, а
   «крупная/мелкая» остаётся общей: вид и размер это две РАЗНЫЕ настройки
   владельца, и одна не должна отменять другую. */
[data-dots-window][data-dots-kind="bar"] .car-dot{
  --rx:2.8;--ry:.6;border-radius:3px}
[data-dots-window][data-dots-kind="pill"] .car-dot{
  --rx:2.3;border-radius:999px}
[data-dots-window][data-dots-kind="square"] .car-dot{border-radius:2px}
[data-dots-window][data-dots-kind="diamond"] .car-dot{
  border-radius:2px;transform:rotate(45deg)}
[data-dots-window][data-dots-kind="line"] .car-dot{
  --rx:3.6;--ry:.34;border-radius:999px}
/* Кольцо — ободок вместо заливки. Идёт ПОСЛЕ правил цвета нарочно: у кольца
   заливки нет вовсе, и «цветная текущая» здесь красит ободок, а не середину
   (иначе текущее кольцо осталось бы белым). */
[data-dots-window][data-dots-kind="ring"] .car-dot{
  --dot-bd:2px;--dot-bg:transparent}
[data-dots-window][data-dots-kind="ring"] .car-dot[data-d="0"]{
  --dot-line:var(--dot-c)}
/* Выключенные точки прячем ЯВНО: `.car-dots` задаёт `display:flex`, а он
   отменяет атрибут `hidden` — та же ловушка, что у `.pm-nav[hidden]`. */
.car-dots[data-dots-off="1"]{display:none}

/* ---------- точки ЛЕНТЫ РАБОТ: три места, как в карточке товара ----------

   Владелец (08.10, снимок #6): «точки сделать как и в карточке товара
   одинаково чтобы мог менять в админ панели и настраивать».

   Класс берём у карточки (`car-fdots`): он приносит и гаснущее пустое место
   (`[data-empty]` выше), и общие ступени размера, вида и цвета. Но МЕСТО у
   ленты своё. Карточка кладёт точки ПОВЕРХ кадра: `position:absolute`,
   `opacity:0` до наведения, `pointer-events:none`. У ленты работ они стоят
   ПОД лентой, в потоке, и видны всегда — иначе настройку было бы не увидеть,
   а по точкам нельзя было бы нажать. Отменяем ровно мешающее, остальное не
   трогаем: иначе разошлись бы размер, цвет и виды, которые у нас общие. */
#wrDots.car-fdots{
  position:static;left:auto;right:auto;bottom:auto;
  opacity:1;pointer-events:auto;
}
/* Основа размера — как у точек ленты товаров (8px), а не как у карточки
   (5px на её масштаб): карточка ростом с ладонь, и её пять пикселей под
   лентой работ читались бы крошками. Ступени размера при этом общие — их и
   просил владелец словами «одинаково». */
#wrDots.car-fdots[data-dots-window]{--dot-base:8px}

/* ---------- точки ОДНИ на весь сайт: карточка работы и страница работы ----------

   Владелец (09.10): «надо точки сделать как везде в единый стиль и настройку
   привести чтобы везде были одни единые настройки».

   Раньше у каждого места была своя дорога: у карточки товара и ленты — общий
   `.car-dot` с окном, у точек кадра на карточке РАБОТЫ — свой кружок через
   `::before`, у страницы работы — своя `.wp-car-dots` со своим кружком. Теперь
   разметка у всех одна (три места `data-slot`, окно `data-dots-window`), а вид
   задаёт ОДНА настройка (`shop.dots`). Здесь остаётся только погасить прежние
   собственные кружки: два кружка в одной кнопке — это уже поломка. */

/* Точки кадра на КАРТОЧКЕ работы. Плашку (`position:absolute`, тёмная
   подложка) оставляем: она стоит на снимке и отделяет точки от светлых
   фотографий. Гасим только свой кружок — его теперь рисует `.car-dot`. */
[data-dots-window].wr-pages button::before{display:none}
/* Кнопка была 12px с полями ради пальца; общий `.car-dot` задаёт размер сам,
   и поля к нему прибавились бы — кружок вышел бы крупнее настроенного. */
[data-dots-window].wr-pages button{padding:0}
/* Пустое место (за краем ленты) гаснет так же, как в карточке товара. Правило
   карточки написано от `.car-fdots`, а плашка работы этот класс НЕ берёт —
   иначе получила бы `opacity:0` и исчезла. Оттого повторяем здесь. */
[data-dots-window].wr-pages .car-dot[data-empty]{opacity:.28}

/* Точки кадров на СТРАНИЦЕ работы. Плашка-подложка остаётся своей, а кружки —
   общие. */
[data-dots-window].wp-car-dots button::before{display:none}
[data-dots-window].wp-car-dots button{padding:0}
[data-dots-window].wp-car-dots .car-dot[data-empty]{opacity:.28}

/* Точки карточки товара гасились НЕ тем атрибутом, что все остальные: правило
   выключения написано от `.car-dots`, а карточка носит только `car-fdots` —
   и на выключенной настройке её точки оставались на месте. Теперь выключение
   одно на все места: выключил владелец — точек нет нигде. */
.car-fdots[data-dots-off="1"]{display:none}

/* ---------- стрелки: кому показывать ----------

   «добавь еще возможность к ним чтобы мог задавать им показ только на
   компьютере и в планшетах или мобильных телефонах уберать их показ».

   Считает это ОФОРМЛЕНИЕ, а не скрипт: ширину окна в скрипте знать нельзя, а
   слушатель изменения размера пересчитывал бы кнопки на каждое движение мыши
   по краю окна. Границы те же, что у раскладки сайта: телефон — до 620px,
   планшет — до 1024px.

   «Нигде» — единственный случай, который прячем и на широком экране: у всех
   остальных состояний есть ширина, где стрелки видны. */
.car-nav[data-arr-devices="none"],
.pm-nav[data-arr-devices="none"]{display:none}
/* только компьютер — прячем на планшете и телефоне */
@media (max-width:1024px){
  .car-nav[data-arr-devices="desktop"],
  .pm-nav[data-arr-devices="desktop"]{display:none}
}
/* только планшет — прячем ВНЕ середины диапазона */
@media (max-width:620px){
  .car-nav[data-arr-devices="tablet"],
  .pm-nav[data-arr-devices="tablet"]{display:none}
}
@media (min-width:1025px){
  .car-nav[data-arr-devices="tablet"],
  .pm-nav[data-arr-devices="tablet"]{display:none}
}
/* только телефон */
@media (min-width:621px){
  .car-nav[data-arr-devices="mobile"],
  .pm-nav[data-arr-devices="mobile"]{display:none}
}
/* компьютер и планшет — то есть без телефона */
@media (max-width:620px){
  .car-nav[data-arr-devices="desktop-tablet"],
  .pm-nav[data-arr-devices="desktop-tablet"]{display:none}
}
/* компьютер и телефон — то есть без планшета */
@media (min-width:621px) and (max-width:1024px){
  .car-nav[data-arr-devices="desktop-mobile"],
  .pm-nav[data-arr-devices="desktop-mobile"]{display:none}
}
/* планшет и телефон — то есть без компьютера */
@media (min-width:1025px){
  .car-nav[data-arr-devices="tablet-mobile"],
  .pm-nav[data-arr-devices="tablet-mobile"]{display:none}
}

/* ---------- стрелки: появление под курсором ----------

   «чтобы они при наведении курсора мыши появлялись эффектно с разными
   скоростями, а при не наведении на карточку курсора мыши стрелок нет».

   Признак наведения лежит на ОБЩЕМ ПРЕДКЕ кнопок (снимок карточки, снимок
   окна, полоса миниатюр) — «мышь внутри снимка» это одно событие на две
   кнопки, а не два. Скрипт ставит его ТОЛЬКО когда владелец выбрал вид,
   отличный от «Всегда видны»: без признака кнопки остаются на месте, как
   было, и магазин, сохранённый до этой настройки, не меняется ни на пиксель.

   Скорость — отдельная переменная (`--arr-t`), потому что вид и скорость это
   две разные настройки владельца: «появлялись эффектно С РАЗНЫМИ
   СКОРОСТЯМИ». */
[data-arr-speed="slow"]   {--arr-t:.62s}
[data-arr-speed="normal"] {--arr-t:.34s}
[data-arr-speed="fast"]   {--arr-t:.18s}
[data-arr-speed="instant"]{--arr-t:0s}

[data-arr-hover] .car-nav,
[data-arr-hover] .pm-nav{
  opacity:0;pointer-events:none;
  transition:opacity var(--arr-t,.34s) var(--ease),
    transform var(--arr-t,.34s) var(--ease),
    filter var(--arr-t,.34s) var(--ease),
    background .25s var(--ease),border-color .25s var(--ease),
    color .25s var(--ease);
}
/* Правило есть у КАЖДОГО вида из списка: настройка без правила — это
   настройка, которая ничего не делает, и владелец решил бы, что она
   сломана. «Всегда видны» правила не имеет нарочно — это покой, то есть
   ровно прежнее поведение. */
[data-arr-hover="fade"] .car-nav,
[data-arr-hover="fade"] .pm-nav{opacity:0}
[data-arr-hover="slide"] .car-nav,
[data-arr-hover="slide"] .pm-nav{transform:translateX(calc(var(--arr-dir,1) * 16px))}
[data-arr-hover="zoom"] .car-nav,
[data-arr-hover="zoom"] .pm-nav{transform:scale(.5)}
[data-arr-hover="rise"] .car-nav,
[data-arr-hover="rise"] .pm-nav{transform:translateY(14px)}
[data-arr-hover="flip"] .car-nav,
[data-arr-hover="flip"] .pm-nav{
  transform:perspective(620px) rotateY(calc(var(--arr-dir,1) * 62deg))}
[data-arr-hover="blur"] .car-nav,
[data-arr-hover="blur"] .pm-nav{transform:scale(.86);filter:blur(7px)}

/* Показ при наведении на СНИМОК. Кнопки окна и полосы миниатюр — общие
   правила; у карточки своё, ниже: там кнопки видны только у ЦЕНТРАЛЬНОЙ
   карточки, и общее правило показало бы их на боковых, повёрнутых. */
[data-arr-hover]:hover .pm-nav{
  opacity:1;pointer-events:auto;transform:none;filter:none}
/* Увеличение при наведении на саму кнопку отменяется показом — возвращаем
   его отдельным правилом, иначе кнопка перестала бы отзываться на курсор. */
[data-arr-hover]:hover .pm-nav:hover{transform:scale(1.08)}

/* Карточка: видна только у центральной. Недоступная стрелка (в начале или в
   конце ленты) остаётся приглушённой и на наведении — она и на виду не
   должна звать нажать её.

   `pointer-events:none` здесь ОБЯЗАТЕЛЕН, и одного `opacity:0` мало: выше
   стоит `.car-card[data-active="1"] .car-nav[data-can="1"]{opacity:.85;
   pointer-events:auto}`, и он по числу классов сильнее общего правила
   появления. Без этой строки спрятанная стрелка продолжала бы ловить мышь —
   владелец нажал бы на пустое место и перелистнул кадр. Поймано набором
   `test-shop-arrows`: проверка «нажать их нельзя» показала `auto`. */
.car-card[data-active="1"] [data-arr-hover] .car-nav{
  opacity:0;pointer-events:none}
.car-card[data-active="1"] [data-arr-hover]:hover .car-nav{
  opacity:.9;pointer-events:auto;transform:none;filter:none}
.car-card[data-active="1"] [data-arr-hover]:hover .car-nav[data-can="0"]{
  opacity:.35;pointer-events:none}
.car-card[data-active="1"] [data-arr-hover]:hover .car-nav[data-can="1"]:hover{
  opacity:1;background:var(--arr-c);color:var(--arr-on);
  border-color:transparent;transform:scale(1.08)}

/* ---------- просмотр кадра: рамка, скругление и размер окна ----------

   «сделай рамку внутри стрелки и вверху справа крестик закрыть и обязательно
   чтобы были у фото и у рамки скругления и чтобы в админ панели мог задавать
   размер этого окна».

   Рамка — ВНУТРИ просмотра и вокруг САМОГО КАДРА, а не по краю экрана: кадр
   вписан в сцену целиком (`object-fit:contain`), у неквадратного снимка по
   бокам остаются поля, и рамка по краю экрана висела бы в пустоте, ничего не
   обрамляя. Поэтому рамку рисует коробка кадра (`.pm-full-shot`, её заводит
   скрипт), а углы и ободок приезжают переменными с накладки.

   Ободок рисуем НАЛОЖЕНИЕМ (`::after` с `inset:0`), а не `border` у коробки:
   рамка обязана лежать внутри и не отнимать у кадра ни пикселя — коробка
   подогнана по сторонам снимка (`fullFitBox`), и `border` сдвинул бы кадр. */
.pm-full{
  --full-r:14px;                      /* скругление: и фото, и рамки */
  --full-rim-w:0px;
  --full-rim-c:rgba(234,242,255,.5);
  --full-rim-out:none;                /* свечение СНАРУЖИ коробки */
  --full-rim-in:none;                 /* ободки ВНУТРИ, поверх кадра */
}
.pm-full[data-full-radius="none"]{--full-r:0px}
.pm-full[data-full-radius="s"]   {--full-r:8px}
.pm-full[data-full-radius="m"]   {--full-r:14px}
.pm-full[data-full-radius="l"]   {--full-r:24px}
.pm-full[data-full-radius="xl"]  {--full-r:38px}

.pm-full-shot{
  position:relative;box-sizing:border-box;z-index:1;flex:none;
  border-radius:var(--full-r);
  /* Обрезаем по скруглению — так углы получает и ФОТО, а не только рамка. */
  overflow:hidden;
  /* Свечение снаружи: `overflow` режет ДЕТЕЙ, а не собственную тень коробки,
     поэтому светящаяся рамка видна и за её краем. */
  box-shadow:var(--full-rim-out);
}
.pm-full-shot>img,.pm-full-shot>video{
  width:100%;height:100%;object-fit:contain;display:block;
  border-radius:inherit;
}
.pm-full-shot::after{
  content:'';position:absolute;inset:0;pointer-events:none;
  border-radius:inherit;
  border:var(--full-rim-w) solid var(--full-rim-c);
  box-shadow:var(--full-rim-in);
}
/* Семь видов рамки. «Без рамки» правила не имеет нарочно: это покой, и его
   вид — ободок шириной 0px; лишнее правило спорило бы с ним порядком. */
.pm-full[data-full-rim="thin"]  {--full-rim-w:1px}
.pm-full[data-full-rim="soft"]  {--full-rim-w:7px;
  --full-rim-c:rgba(234,242,255,.14)}
/* Двойная — два ободка внутри кадра. Рисуем тенями, а не `border`+`outline`:
   тень садится по СКРУГЛЕНИЮ, а `outline` в старых движках шёл бы по прямым
   углам и на «круглых углах» вылез бы за кадр. */
.pm-full[data-full-rim="double"]{--full-rim-w:0px;
  --full-rim-in:inset 0 0 0 2px rgba(234,242,255,.55),
    inset 0 0 0 9px rgba(234,242,255,.55)}
.pm-full[data-full-rim="glow"]  {--full-rim-w:2px;
  --full-rim-c:rgba(34,230,255,.8);
  --full-rim-out:0 0 26px rgba(34,230,255,.55),0 0 60px rgba(34,230,255,.3)}
.pm-full[data-full-rim="plate"] {--full-rim-w:12px;--full-rim-c:rgba(3,5,11,.86)}
.pm-full[data-full-rim="gold"]  {--full-rim-w:3px;--full-rim-c:#ffca28;
  --full-rim-out:0 0 18px rgba(255,179,0,.5)}

/* Размер окна — пять ступеней, как в списке на сервере. Меняется ОТСТУП
   сцены, а не размер кадра: сам кадр всегда вписан в то, что ему оставили
   (`object-fit:contain`), поэтому «во весь экран» показывает его крупнее, а
   «маленькое окно» — целиком и с воздухом по краям. */
.pm-full[data-full-frame="s"]   {--full-pad-y:150px;--full-pad-x:22vw}
.pm-full[data-full-frame="m"]   {--full-pad-y:104px;--full-pad-x:13vw}
.pm-full[data-full-frame="full"]{--full-pad-y:10px; --full-pad-x:10px}
@media (max-width:620px){
  .pm-full[data-full-frame="s"]   {--full-pad-y:64px;--full-pad-x:12vw}
  .pm-full[data-full-frame="m"]   {--full-pad-y:44px;--full-pad-x:8vw}
  .pm-full[data-full-frame="full"]{--full-pad-y:6px; --full-pad-x:6px}
}

/* ---------- полосу миниатюр ведут курсором ----------

   «чтобы их можно было свайпать и листать пальцем и курсором пролистывать».
   Жест ведёт скрипт, а здесь только то, без чего жест не работает и не
   читается: курсор-«лапа» и `touch-action:pan-y`. Без последнего браузер на
   телефоне считает движение своим, и вместе с полосой вбок уезжает вся
   страница. Стоит и на полосе, и на дорожке: `touch-action` считается по
   элементу под пальцем И его предкам, а палец ложится и на плитку, и в
   зазор между ними. */
.pm-thumbs,.pm-thumbs-track{cursor:grab;touch-action:pan-y}
.pm-thumbs-track:active{cursor:grabbing}
/* Плитка — кнопка, и курсор у неё свой: по ней видно, что кадр выбирают
   нажатием, а по дорожке — что её тянут. */
.pm-thumb{cursor:pointer}

/* ============================================================
   ВИД КАРТОЧЕК ТОВАРА: лента ⇄ сетка, вкладки категорий
   ============================================================

   Владелец (09.10, снимки #3–#6):

     «надо добавить вот такую кнопку … там можно выбрать вид, заместо
      карусели карточки товара меняют вид примерно вот так … по 5 карточек
      вряд, 4 ряда вниз, и внизу если все карточки не поместятся сделать
      страницы со стрелками, можно листать другие карточки в таком виде»

   Что откуда берётся:

   * СТРОКУ ВИДА (вкладки слева, переключатель справа) носит каждая карусель
     ТОВАРОВ. Другие карусели сайта — работы, отзывы, ленты блоков — её не
     получают вовсе: владелец просил «сделать только в каруселях товара,
     остальные карусели не трогать».
   * ЧИСЛА (5 в ряд, 20 на страницу) приходят с сервера
     (`SHOP_GRID_COLS`/`SHOP_GRID_ROWS`) и ставятся переменной на саму сетку:
     раскладку решает сайт, но задаёт её владелец — в панели, у каждой
     карусели. Число в оформлении было бы третьим местом, и однажды
     настройка разошлась бы с раскладкой.
   * ПОКАЗ строки решает скрипт атрибутом `hidden`: вкладки — когда у товаров
     ЭТОЙ карусели заведены категории, переключатель — когда владелец
     разрешил выбор вида именно у неё («кнопку показывать только где
     разрешу»). Пустая полоса над лентой читалась бы как поломка.

   ПОЧЕМУ ПРАВИЛА НА `[hidden]` ЗДЕСЬ ОБЯЗАТЕЛЬНЫ. Атрибут `hidden` — это
   правило браузера `display:none`, и любое НАШЕ `display:flex` его
   перебивает: у кого `display` объявлен, тот и победил. Так уже немели
   стрелки окна товара (`.pm-nav[hidden]`). Поэтому у каждого элемента
   строки вида своё правило на `[hidden]`, и стоит оно ПОСЛЕ своего
   `display`. */

/* ---------- строка вида ---------- */
.shop-view{
  display:flex;align-items:center;justify-content:space-between;
  gap:12px 18px;flex-wrap:wrap;
  margin:0 0 16px;
}
.shop-view[hidden]{display:none}

/* Вкладки категорий. Число товаров стоит кружком рядом с названием: по нему
   видно, есть ли смысл нажимать, — вкладка с нулём была бы обещанием,
   которого карусель не сдержит. */
.shop-tabs{
  display:flex;align-items:center;gap:8px;flex-wrap:wrap;
  min-width:0;
}
.shop-tab{
  display:inline-flex;align-items:center;gap:8px;
  padding:8px 15px;border-radius:100px;cursor:pointer;
  border:1px solid var(--line-strong);
  background:var(--surface-2);
  color:var(--ink);font:inherit;font-size:.9rem;font-weight:600;
  transition:border-color .3s var(--ease),background .3s var(--ease),
    color .3s var(--ease);
}
.shop-tab:hover{
  border-color:var(--row-line,rgba(34,230,255,.5));
  background:var(--row-bg,rgba(34,230,255,.09));
}
.shop-tab:focus-visible{outline:2px solid var(--row-c,var(--cyan));outline-offset:2px}
/* Выбранная вкладка — залита цветом строки. Пока владелец цвета не выбрал,
   `--row-c` не объявлена и работает запасной цвет сайта: второго акцента в
   магазине нет, и «свой» цвет здесь означал бы ещё одну настройку, которой
   владелец не просил. С 11.10 он её как раз и просил — «менять цвета» — и
   цвет приходит палитрой магазина тем же приёмом, что у точек и сетки. */
.shop-tab[data-on]{border-color:transparent;background:var(--row-c,var(--cyan));color:#04121a}
.shop-tab-n{
  font-size:.72rem;font-weight:700;line-height:1;
  padding:3px 7px;border-radius:100px;
  background:rgba(255,255,255,.14);
}
.shop-tab[data-on] .shop-tab-n{background:rgba(4,18,26,.22)}

/* Переключатель вида — две кнопки-значка. Выбранная обведена ЗАЛИВКОЙ, как
   вкладка, но маленькая: рядом с залитой вкладкой крупная залитая кнопка
   спорила бы с ней за внимание. */
.shop-switch{
  display:inline-flex;align-items:center;gap:4px;
  padding:3px;border-radius:100px;
  border:1px solid var(--line);
  background:var(--surface-2);
}
.shop-switch[hidden]{display:none}
.shop-sw-btn{
  display:inline-flex;align-items:center;justify-content:center;
  width:34px;height:30px;padding:0;border:0;border-radius:100px;
  background:transparent;color:var(--ink-2);cursor:pointer;
  transition:background .3s var(--ease),color .3s var(--ease);
}
.shop-sw-btn svg{width:17px;height:17px}
.shop-sw-btn:hover{color:var(--ink)}
.shop-sw-btn:focus-visible{outline:2px solid var(--row-c,var(--cyan));outline-offset:2px}
.shop-sw-btn[data-on]{background:var(--row-c,var(--cyan));color:#04121a}

/* ---------- настройки строки вида: цвет, размер, рамка ----------

   Владелец (11.10, второй снимок): «и вот тут сделай чтобы в админ панели мог
   менять цвета размер и сами рамки с разными эффектами». Снимок — полоса НАД
   каруселью: вкладки категорий слева, переключатель «лента/сетка» справа.
   С 09.10 её вид был зашит: заливка всегда цветом сайта, размер один на все
   экраны, рамка одна и та же.

   Всё это — ОФОРМЛЕНИЕ. Скрипт ставит на полосу три признака
   (`data-row-size`, `data-row-frame`, `data-row-color`), а как это выглядит,
   решают правила ниже. Числа и краски в скрипте означали бы второе место, где
   они записаны, и первая же правка разошлась бы с оформлением.

   Цвет — ОБЩАЯ палитра магазина: правила выше (`[data-row-color="…"]`) кладут
   на полосу те же `--stk-c`/`--stk-line`/`--stk-glow`/`--stk-bg`, что и у
   точек со стрелками, а строка ниже переименовывает их в свои. Свой цвет
   владельца приходит теми же переменными прямо из скрипта — переименование
   подходит и ему. Пусто — «как на сайте»: переменных нет, и работает запасное
   значение каждого правила. */
.shop-view[data-row-color]{
  --row-c:var(--stk-c,var(--cyan));
  --row-line:var(--stk-line,rgba(34,230,255,.45));
  --row-glow:var(--stk-glow,rgba(34,230,255,.35));
  --row-bg:var(--stk-bg,rgba(34,230,255,.09));
}

/* Размер строки — ПЯТЬ ступеней.

   Растут ВМЕСТЕ и подпись вкладки, и кружок с числом, и кнопки переключения:
   крупная подпись с крошечным кружком читалась бы как недоделка. «Обычные»
   правила не имеют — это то, что стоит в правилах выше, то есть прежний вид.
   Лестница идёт строго по возрастанию, и ступени без правила не остаётся. */
.shop-view[data-row-size="xs"] .shop-tab{padding:5px 11px;font-size:.78rem}
.shop-view[data-row-size="xs"] .shop-tab-n{font-size:.64rem;padding:2px 5px}
.shop-view[data-row-size="xs"] .shop-switch{padding:2px}
.shop-view[data-row-size="xs"] .shop-sw-btn{width:28px;height:24px}
.shop-view[data-row-size="xs"] .shop-sw-btn svg{width:14px;height:14px}

.shop-view[data-row-size="s"] .shop-tab{padding:6px 13px;font-size:.84rem}
.shop-view[data-row-size="s"] .shop-tab-n{font-size:.68rem;padding:2px 6px}
.shop-view[data-row-size="s"] .shop-sw-btn{width:31px;height:27px}
.shop-view[data-row-size="s"] .shop-sw-btn svg{width:15px;height:15px}

.shop-view[data-row-size="l"] .shop-tab{padding:10px 18px;font-size:.98rem}
.shop-view[data-row-size="l"] .shop-tab-n{font-size:.78rem;padding:4px 8px}
.shop-view[data-row-size="l"] .shop-switch{padding:4px}
.shop-view[data-row-size="l"] .shop-sw-btn{width:38px;height:34px}
.shop-view[data-row-size="l"] .shop-sw-btn svg{width:19px;height:19px}

.shop-view[data-row-size="xl"] .shop-tab{padding:12px 22px;font-size:1.06rem}
.shop-view[data-row-size="xl"] .shop-tab-n{font-size:.84rem;padding:4px 9px}
.shop-view[data-row-size="xl"] .shop-switch{padding:5px}
.shop-view[data-row-size="xl"] .shop-sw-btn{width:42px;height:38px}
.shop-view[data-row-size="xl"] .shop-sw-btn svg{width:21px;height:21px}

/* Рамка строки: вид И эффект — «сами рамки с разными эффектами».

   Рамка ложится и на вкладки, и на переключатель: это одна полоса, и рамка
   на половине её читалась бы как поломка. Вес селектора выше, чем у
   `.shop-tab[data-on]` с его прозрачной кромкой, поэтому у выбранной вкладки
   рамка не пропадает. «Без рамки» правила не имеет — остаётся своя тонкая
   рамка оформления, то есть прежний вид. */
.shop-view[data-row-frame="line"] .shop-tab,
.shop-view[data-row-frame="line"] .shop-switch{
  border-color:var(--row-line);box-shadow:none;
}
.shop-view[data-row-frame="thick"] .shop-tab,
.shop-view[data-row-frame="thick"] .shop-switch{
  border-width:2px;border-color:var(--row-c);
  box-shadow:0 0 0 1px rgba(6,10,18,.9);
}
.shop-view[data-row-frame="double"] .shop-tab,
.shop-view[data-row-frame="double"] .shop-switch{
  border-color:var(--row-line);
  box-shadow:inset 0 0 0 1px rgba(6,10,18,.92),inset 0 0 0 2px var(--row-line);
}
.shop-view[data-row-frame="neon"] .shop-tab,
.shop-view[data-row-frame="neon"] .shop-switch{
  border-color:var(--row-c);
  box-shadow:0 0 0 1px var(--row-c),0 0 20px -4px var(--row-glow);
}
.shop-view[data-row-frame="glass"] .shop-tab,
.shop-view[data-row-frame="glass"] .shop-switch{
  border-color:rgba(255,255,255,.26);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.22),
             0 12px 24px -18px rgba(0,0,0,.9);
}
.shop-view[data-row-frame="inset"] .shop-tab,
.shop-view[data-row-frame="inset"] .shop-switch{
  border-color:rgba(255,255,255,.05);
  box-shadow:inset 0 2px 9px rgba(0,0,0,.6);
}
.shop-view[data-row-frame="edge3d"] .shop-tab,
.shop-view[data-row-frame="edge3d"] .shop-switch{
  border-color:var(--row-line);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.3),
             0 9px 20px -13px var(--row-glow);
}


/* ---------- сетка карточек ----------

   Сетка показывает ТЕ ЖЕ карточки, что лента: узлы ПЕРЕЕЗЖАЮТ сюда, а не
   собираются заново. Вторая сборка значила бы вторую дорогу для цены,
   значка и отсчёта акции — и однажды они разошлись бы.

   Отсюда две правки, без которых карточка в сетке не стоит:

   1) Вернуть карточку В ПОТОК. В ленте она `position:absolute` и занимает
      место, которое ей считает скрипт (`--tx`, `--sc`, `--ry`). В сетке это
      место задаёт ячейка, и карточка должна просто лечь в неё.

   2) Дать фотографии ТВЁРДУЮ долю. В ленте высота карточки задана числом
      (`--car-h`, 432px), и фотография берёт ОСТАТОК после подписи. В сетке
      высоту задаёт ячейка, и тот же «остаток» мог бы стать нулевым: подпись
      на узкой карточке (в ряд из пяти это ~225px) занимает больше, чем
      осталось. Тогда вместо товаров вышел бы ряд подписей. Поэтому доля
      здесь ОБРАТНАЯ — фотография квадрат, а подпись берёт сколько нужно;
      `flex-shrink:0` не украшение: без него подпись всё равно сжала бы
      фотографию. */
.shop-grid{
  display:grid;
  grid-template-columns:repeat(var(--grid-cols,5),minmax(0,1fr));
  gap:26px 18px;
}
.shop-grid[hidden]{display:none}
[data-view="grid"] .car-card{
  position:static;top:auto;left:auto;margin-left:0;
  width:100%;height:auto;
  /* Рамку и тень активной карточки гасим: в сетке активной нет — карточки
     равны, и одна подсвеченная читалась бы как случайная. Рамку при этом
     не «убираем насовсем»: её вид в сетке задаёт настройка «Рамка плиток»
     (см. ниже), и она вернёт и цвет, и свечение. */
  border-color:transparent;
  box-shadow:none;
}
/* «Движение карточек» в сетке НЕ работает — и это не упущение, а условие.

   Виды движения описаны правилами `[data-move="…"] .car-card`, и половина
   из них опирается на `:not([data-active="1"])` — «все, кроме центральной».
   В ленте центральная одна, а в сетке `data-active` не стоит ни у кого:
   правило про соседей становится правилом про ВСЕ карточки сразу. Тогда
   «С подъёмом» поднимает каждую плитку на 8px, «Наклон вперёд» наклоняет
   каждую, «Покачивание» качает каждую. Владелец (11.10) увидел это как «в
   состоянии сетки они рандомно перемещаются».

   Гасим ПЕРЕМЕННЫЕ, а не сами виды: `--ty`, `--rx` и `--rz` — ровно то,
   чем виды движения друг от друга отличаются (разбор — у SHOP_MOVES в
   server.js). Вес селектора тот же, что у правил видов, и стоит он НИЖЕ их
   по файлу: при равном весе побеждает нижестоящее. */
[data-view="grid"].shop-block[data-move] .car-card{--ty:0px;--rx:0deg;--rz:0deg}
[data-view="grid"] .car-card .car-shot{
  flex:1 0 auto;
  aspect-ratio:1 / 1;
}
/* Объёмная кромка остаётся (она у всех карточек), а затемнение `--dim` —
   нет: оно считалось от «дальности» в ленте, и в сетке у одних карточек
   было бы, у других нет. */
[data-view="grid"] .car-card::after{background:transparent}
/* Лейбл и точки кадров в ленте видны ТОЛЬКО у центральной карточки — иначе
   на боковых они читались бы рябью. В сетке центральной нет, поэтому они у
   всех: это обычные плитки каталога, и покупателю нужно видеть и значок
   «Хит», и сколько у товара снимков. */
[data-view="grid"] .car-card .car-badge{visibility:visible}
[data-view="grid"] .car-card .car-fdots{opacity:1}
/* Стрелки кадров — по наведению: у плитки каталога они не должны висеть
   поверх фотографии постоянно. */
[data-view="grid"] .car-card:hover .car-nav[data-can="1"]{
  opacity:.85;pointer-events:auto;
}
[data-view="grid"] .car-card:hover .car-nav[data-can="1"]:hover{
  opacity:1;background:var(--arr-c);color:var(--arr-on);border-color:transparent;
}

/* ---------- настройки плиток сетки ----------

   Владелец (11.10): «в админ панели чтобы мог настраивать карточки в ряд 2, 3,
   4, 5 и также вниз 1, 2, 3, 4, 5, 6, 7 рядов… и мог менять размер до 5
   вариантов размера… Так же в админ панель добавить эффекты посветками цвета
   наклоны и сделать их уникальными в плане как настоящие 3д карты при
   наведении курсора или пальца на экране телефона или планшета если есть
   сенсорный экран именно в положении сетки. И вот тут… сделай чтобы в админ
   панели мог менять цвета размер и сами рамки с разными эффектами».

   Всё это — ОФОРМЛЕНИЕ, а не скрипт. Скрипт ставит на сетку четыре признака
   (`data-grid-size`, `data-grid-frame`, `data-grid-color`, `data-grid-hover`) и
   две переменные для указателя (`--hx`, `--hy`), а как это выглядит — решают
   правила ниже. Числа и краски в скрипте означали бы второе место, где они
   записаны, и первая же правка разошлась бы с оформлением.

   Цвет рамки — ОБЩАЯ палитра магазина: правила выше (`[data-grid-color="…"]`)
   кладут на сетку те же `--stk-c`/`--stk-line`/`--stk-glow`, что и у точек со
   стрелками, а строка ниже переименовывает их в свои. Свой цвет владельца
   приходит теми же переменными прямо из скрипта — переименование подходит и
   ему. Пусто — «как на сайте»: переменных нет, и работает запасное значение
   каждого правила. */
.shop-grid[data-grid-color]{
  --grid-c:var(--stk-c,var(--cyan));
  --grid-line:var(--stk-line,rgba(34,230,255,.45));
  --grid-glow:var(--stk-glow,rgba(34,230,255,.35));
}

/* Размер плитки — ПЯТЬ ступеней.

   Ширину плитки задаёт РЯД (её считает оформление по числу карточек в ряд),
   поэтому ступень меняет ВЫСОТУ: чем выше плитка, тем крупнее снимок при том
   же ряде. Владелец просил ровно этого — «чтобы фото было больше по размеру».
   Ряд долей идёт по возрастанию: 1.33, 1.125, 1, 0.89, 0.75 — то есть каждая
   следующая ступень строго выше предыдущей, и ступени без правила не
   остаётся. */
.shop-grid[data-grid-size="xs"] .car-card .car-shot{aspect-ratio:4 / 3}
.shop-grid[data-grid-size="s"]  .car-card .car-shot{aspect-ratio:9 / 8}
.shop-grid[data-grid-size="m"]  .car-card .car-shot{aspect-ratio:1 / 1}
.shop-grid[data-grid-size="l"]  .car-card .car-shot{aspect-ratio:8 / 9}
.shop-grid[data-grid-size="xl"] .car-card .car-shot{aspect-ratio:3 / 4}

/* Ступень «Кадр 3:4 — фото 225 × 300» в сетке.

   Владелец (12.10, четвёртый снимок): «И такое же надо сделать вот тут …
   чтобы и там был такой формат и принцип работы как описал уже». Принцип тот
   же, что у ступени `f34` ленты (см. её разбор у `.shop-block` выше):
   снимок — ТВЁРДАЯ доля, подпись берёт сколько нужно.

   Сетке не хватало ровно одного: полоса плитки ТЯНУЛАСЬ (`flex:1 0 auto`), а
   ряд растягивал карточки на свою высоту (`align-items:stretch` — у сетки по
   умолчанию). Оттого у плиток ОДНОГО ряда снимки были разной высоты. Замер
   до работы (`_probe-b13-cardsize.js`): ряд 601px, полосы 386×386 / 386×469 /
   386×412, снимки 290×386 / 340×454 / 313×417, боковые полосы 48 / 17 / 39px.
   Полоса была квадратной (`aspect-ratio:1 / 1`), а снимок вписан 3:4 — отсюда
   и полосы.

   Теперь полоса не тянется и сама 3:4: её высоту задаёт ШИРИНА плитки, а
   подпись забирает остаток. Растяжение ряда при этом не трогаем — оно стало
   безвредным: тянется ПОДПИСЬ, а не снимок, и карточки ряда остаются одной
   высоты, как и раньше.

   Ширину плитки по-прежнему задаёт РЯД — это и есть устройство сетки, и
   ломать его незачем: при пяти карточках в ряд `.wrap` шириной 1200px и
   промежутках 4×18px даёт плитку 225px, то есть ровно 225 × 300. При другом
   ряде размер будет другим, и в названии ступени это сказано. */
.shop-grid[data-grid-size="f34"] .car-card .car-shot{
  flex:0 0 auto;aspect-ratio:3 / 4;
}
.shop-grid[data-grid-size="f34"] .car-card .car-body{flex:1 1 auto}

/* Ступень «Маркетплейс» в сетке.

   Владелец: «эти карточки должны быть в кареселе и также в сетке одинаковые по
   размерам». Устройство то же, что у `f34` в сетке: снимок — твёрдая доля
   4:5, подпись берёт остаток. Ширину плитки задаёт РЯД (при пяти в ряд это
   225px — те же 225, что и в ленте), поэтому «одинаковые по размерам» здесь
   означает одинаковое УСТРОЙСТВО и одинаковую ширину при том же ряде.

   Кнопка приезжает та же — она часть карточки, а не ленты: собирает её скрипт
   один раз, и в сетке карточка остаётся собой. */
.shop-grid[data-grid-size="mp"] .car-card .car-shot{
  flex:0 0 auto;aspect-ratio:3 / 4;
}
.shop-grid[data-grid-size="mp"] .car-card .car-body{flex:1 1 auto}
/* Увеличение при наведении гасим и в сетке: карточка одна, и вести себя в
   ленте и в сетке она обязана одинаково. */
.shop-grid[data-grid-size="mp"] .car-card:hover .car-shot img,
.shop-grid[data-grid-size="mp"] .car-card:hover .car-shot video{--ph-h:1}

/* Рамка плитки: вид И эффект.

   Раньше рамки в сетке не было вовсе — `[data-view="grid"] .car-card` гасит
   и `border-color`, и тень. Поэтому «без рамки» — не пустой вид, а ПРЕЖНИЙ, и
   он же запасной: у магазина, сохранённого до этой настройки, сетка обязана
   выглядеть как выглядела.

   Виды отличаются не толщиной, а приёмом: свечение, двойной обвод, стекло,
   вдавленность, объёмная кромка. Вес селекторов выше, чем у гасящего правила
   выше, поэтому рамка действительно возвращается. */
.shop-grid[data-grid-frame="line"] .car-card{
  border-color:var(--grid-line);box-shadow:none;
}
.shop-grid[data-grid-frame="thick"] .car-card{
  border-width:3px;border-color:var(--grid-c);
  box-shadow:0 0 0 1px rgba(6,10,18,.9);
}
.shop-grid[data-grid-frame="double"] .car-card{
  border-color:var(--grid-line);
  box-shadow:inset 0 0 0 1px rgba(6,10,18,.92),inset 0 0 0 2px var(--grid-line);
}
.shop-grid[data-grid-frame="neon"] .car-card{
  border-color:var(--grid-c);
  box-shadow:0 0 0 1px var(--grid-c),0 0 22px -2px var(--grid-glow),
             inset 0 0 18px -8px var(--grid-glow);
}
.shop-grid[data-grid-frame="glass"] .car-card{
  border-color:rgba(255,255,255,.26);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.22),
             inset 0 0 22px -10px rgba(255,255,255,.18),
             0 14px 28px -20px rgba(0,0,0,.9);
}
.shop-grid[data-grid-frame="inset"] .car-card{
  border-color:rgba(255,255,255,.05);
  box-shadow:inset 0 2px 12px rgba(0,0,0,.65),inset 0 0 0 1px var(--grid-line);
}
.shop-grid[data-grid-frame="edge3d"] .car-card{
  border-color:var(--grid-line);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.32),
             0 10px 22px -14px var(--grid-glow);
}

/* ---------- плитка под курсором и под пальцем ----------

   Скрипт кладёт на плитку долю от её СЕРЕДИНЫ — `--hx` и `--hy`, от −1 до
   1, — и признак `data-hover="1"`, пока указатель на ней. В покое переменных
   нет вовсе, и запасной ноль каждого правила ставит плитку ровно.

   Один вид — одно правило, и все они построены на этих двух переменных:
   «наклон» поворачивает плитку туда, где палец, «подъём» поднимает её и
   добавляет наклон, «приближение» растит снимок, «слои» уводят снимок против
   пальца, «блик» ведёт по снимку светлое пятно, «обод» уводит свечение в
   сторону пальца — как от настоящего источника света. */
.shop-grid[data-grid-hover] .car-card{
  transition:transform .3s var(--ease),box-shadow .3s var(--ease);
}
.shop-grid[data-grid-hover="tilt"] .car-card{
  transform:perspective(900px)
            rotateY(calc(var(--hx,0) * 10deg))
            rotateX(calc(var(--hy,0) * -10deg));
}
/* Пока указатель ведёт плитку, переход короче: с длинным она отставала бы от
   него на четверть секунды и читалась бы как «заело». */
.shop-grid[data-grid-hover="tilt"] .car-card[data-hover="1"],
.shop-grid[data-grid-hover="parallax"] .car-card[data-hover="1"]{
  transition-duration:.08s;
}
.shop-grid[data-grid-hover="lift"] .car-card{
  transform:translate3d(0,0,0) rotateX(calc(var(--hy,0) * -4deg));
}
.shop-grid[data-grid-hover="lift"] .car-card[data-hover="1"]{
  transform:translate3d(0,-10px,0) rotateX(calc(var(--hy,0) * -5deg));
  box-shadow:0 22px 34px -22px rgba(0,0,0,.9),
             0 0 26px -12px var(--grid-glow,rgba(34,230,255,.45));
}
.shop-grid[data-grid-hover="rise"] .car-card{
  transform:translate3d(0,0,0) rotateX(calc(var(--hy,0) * -5deg))
            rotateY(calc(var(--hx,0) * 6deg));
}
.shop-grid[data-grid-hover="rise"] .car-card[data-hover="1"]{
  transform:translate3d(0,-20px,0) rotateX(calc(var(--hy,0) * -7deg))
            rotateY(calc(var(--hx,0) * 9deg)) scale(1.03);
  box-shadow:0 30px 46px -26px rgba(0,0,0,.92),
             0 0 30px -12px var(--grid-glow,rgba(34,230,255,.45));
}
.shop-grid[data-grid-hover="zoom"] .car-card[data-hover="1"]{
  box-shadow:0 18px 30px -22px rgba(0,0,0,.9);
}
.shop-grid[data-grid-hover="zoom"] .car-card[data-hover="1"] .car-shot img,
.shop-grid[data-grid-hover="zoom"] .car-card[data-hover="1"] .car-shot video{
  transform:scale(1.14);
}
.shop-grid[data-grid-hover="parallax"] .car-card .car-shot{
  transform:translate3d(calc(var(--hx,0) * -12px),calc(var(--hy,0) * -12px),0)
            scale(1.08);
  transition:transform .3s var(--ease);
}
.shop-grid[data-grid-hover="parallax"] .car-card[data-hover="1"]{
  box-shadow:0 16px 28px -22px rgba(0,0,0,.9);
}
/* Блик — слой `::before`: у карточки он свободен, а `::after` занят объёмной
   кромкой и затемнением. Слой обрезан скруглением карточки (у неё
   `overflow:hidden`) — блик и должен лежать по снимку, а не вылезать наружу. */
.shop-grid[data-grid-hover="shine"] .car-card::before{
  content:'';position:absolute;inset:0;z-index:5;pointer-events:none;opacity:0;
  background:radial-gradient(46% 38% at calc(50% + var(--hx,0) * 50%)
    calc(50% + var(--hy,0) * 50%),rgba(255,255,255,.32),transparent 70%);
  transition:opacity .3s var(--ease);
}
.shop-grid[data-grid-hover="shine"] .car-card[data-hover="1"]::before{opacity:1}
/* Обод — свечение, уехавшее в сторону указателя: наружу карточка не пускает
   (`overflow:hidden`), поэтому свет идёт тенью, а не отдельным слоем. */
.shop-grid[data-grid-hover="halo"] .car-card[data-hover="1"]{
  box-shadow:calc(var(--hx,0) * 9px) calc(var(--hy,0) * 9px) 28px -8px
               var(--grid-glow,rgba(34,230,255,.5)),
             inset calc(var(--hx,0) * -7px) calc(var(--hy,0) * -7px) 24px -12px
               var(--grid-c,var(--cyan));
}

/* Кому движение противопоказано — плитки стоят ровно, как и раньше: эффект
   наведения здесь не украшение, а именно движение. */
@media (prefers-reduced-motion:reduce){
  .shop-grid[data-grid-hover] .car-card,
  .shop-grid[data-grid-hover] .car-card[data-hover="1"],
  .shop-grid[data-grid-hover] .car-card .car-shot{transform:none}
}

/* ---------- страницы сетки ----------

   «внизу если все карточки не поместятся сделать страницы со стрелками».
   Число рядом со стрелками — не украшение: по нему видно, сколько всего
   страниц, и «дальше» перестаёт быть прыжком в темноту.

   Одна страница — полосы нет ВОВСЕ (её гасит скрипт): мёртвая стрелка
   читается как поломка. Стрелка, которой некуда идти, гаснет, но остаётся
   на месте — по ней видно, что вы в начале или в конце. */
.shop-pager{
  display:flex;align-items:center;justify-content:center;gap:12px;
  margin-top:24px;
}
.shop-pager[hidden]{display:none}
.shop-pg{
  display:inline-flex;align-items:center;justify-content:center;
  width:40px;height:40px;padding:0;border-radius:50%;cursor:pointer;
  border:1px solid var(--line-strong);
  background:var(--surface-2);color:var(--ink);
  transition:border-color .3s var(--ease),color .3s var(--ease),
    box-shadow .3s var(--ease),transform .3s var(--ease);
}
.shop-pg svg{width:20px;height:20px}
.shop-pg:hover{border-color:rgba(34,230,255,.55);color:#fff;
  box-shadow:0 0 26px rgba(34,230,255,.24);transform:scale(1.06)}
.shop-pg:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
.shop-pg[disabled]{opacity:.32;cursor:default;box-shadow:none;transform:none}
.shop-pg-count{
  font-size:.9rem;font-weight:600;color:var(--ink-2);
  min-width:56px;text-align:center;font-variant-numeric:tabular-nums;
}

/* Ряд сужается вместе с окном. Пять в ряд остаются на широком экране — там,
   где владелец их и заказывал; на узком пять карточек превратились бы в
   полоски. Это дело ОФОРМЛЕНИЯ, а не скрипта: ширину окна скрипт не знает,
   и слушатель изменения размера пересчитывал бы сетку на каждое движение
   мыши по краю окна. Границы те же, что у остальных раскладок сайта.

   С 11.10 число в ряд ВЫБИРАЕТ владелец (2, 3, 4 или 5), поэтому правило
   привязано не к «сетке вообще», а к ВЫБРАННОМУ числу: сужаем только то, что
   на узком экране не помещается. Прежнее правило — `repeat(4, …)` без
   условия — перебивало выбор владельца: выбрав «по 2 в ряд», он получил бы
   на ноутбуке четыре, и настройка означала бы разное на разных экранах.

   Число берём из признака `data-grid-cols`, который скрипт ставит рядом с
   переменной `--grid-cols`: в `repeat()` переменную не подставить — счётчик
   колонок обязан быть целым числом, а `var()` даёт «число», и правило
   отбрасывается целиком. */
@media (max-width:1100px){
  .shop-grid[data-grid-cols="5"]{grid-template-columns:repeat(4,minmax(0,1fr))}
}
@media (max-width:880px){
  .shop-grid[data-grid-cols="5"],
  .shop-grid[data-grid-cols="4"]{grid-template-columns:repeat(3,minmax(0,1fr))}
}
@media (max-width:620px){
  .shop-grid[data-grid-cols="5"],
  .shop-grid[data-grid-cols="4"],
  .shop-grid[data-grid-cols="3"]{
    grid-template-columns:repeat(2,minmax(0,1fr));gap:20px 12px;
  }
}

/* ---------- та же ширина — у карточки ленты «Маркетплейс» ----------

   Ширина карточки этой ступени считается по формуле сетки (разбор — у
   `[data-card="mp"]` в начале файла), и формула обязана повторять ЗДЕСЬ то,
   что сетка делает на узком экране. Иначе на телефоне вышло бы (350 − 54) / 4
   = 74 px: владелец увидел бы вместо карточки полоску.

   Три запроса — зеркало трёх запросов сетки выше, с теми же границами и тем
   же числом колонок. Дублирование здесь осознанное: `repeat()` не принимает
   `var()`, поэтому «сколько колонок на самом деле» знает только признак
   `data-grid-cols`, а признак у каждого числа свой. Меняется ряд у сетки —
   меняется и здесь; проверить это можно замером (см. дневник за 30.10). */
@media (max-width:1100px){
  .shop-block[data-card="mp"][data-grid-cols="5"]{
    --card-w:calc((100% - 3 * 18px) / 4);
  }
}
@media (max-width:880px){
  .shop-block[data-card="mp"][data-grid-cols="5"],
  .shop-block[data-card="mp"][data-grid-cols="4"]{
    --card-w:calc((100% - 2 * 18px) / 3);
  }
}
@media (max-width:620px){
  .shop-block[data-card="mp"][data-grid-cols="5"],
  .shop-block[data-card="mp"][data-grid-cols="4"],
  .shop-block[data-card="mp"][data-grid-cols="3"]{
    --card-w:calc((100% - 12px) / 2);
  }
}

/* Полоса точек и кнопка паузы при сетке уезжают целиком: они относятся к
   ленте, и в сетке листать ими нечего. Правило на `[hidden]` обязательно —
   у `.car-foot` объявлен `display:flex`, и он перебил бы атрибут. */
.car-foot[hidden]{display:none}

/* ============================================================
   ПОМОЩНИК ПО САЙТУ — 3D-принтер, печатающий короля, и окно разговора

   Владелец (20.10): «должен вылезать из принтера идет печать настоящей
   шахматной фигуры 3д моушен объемный король на доске живой помощьник по
   сайту». Владелец (30.10): «переделать полностью чтобы он был объемным
   в 3д и моушен эффектах как настоящий 3д принтер и по-настоящему печатает
   на шахматной доске объемную 3д фигуру шахматного короля».

   Значок стоит НАД кнопкой связи (`.hub`), а не поверх неё: две кнопки в
   одном углу перекрывали бы друг друга. По расписанию владельца они
   показываются по очереди — прячет и показывает их js/assistant.js
   атрибутом `data-show`.

   3D здесь настоящее и дешёвое: рама из двух плоскостей на каждую деталь,
   стол-доска, 30 слоёв-дисков короля. Ни картинок, ни WebGL — значок
   должен открываться мгновенно и работать на телефоне.
   ============================================================ */
.as-dock{
  position:fixed; right:20px; bottom:150px; z-index:66;
  display:flex; flex-direction:column; align-items:flex-end; gap:12px;
  /* Высота значка — переменной: по ней считается, на сколько опустить окно,
     когда значок улетает (см. `.as-dock.is-open .as-win`). Одно число на два
     места, чтобы окно не разошлось со значком при смене размера. */
  --orb-h:132px;
  font-family:inherit;
  transform-origin:100% 100%;
  transition:transform .62s cubic-bezier(.32,.86,.28,1), visibility 0s;
}
/* На одной линии с кнопкой «Связь» — по нижней кромке.

   Владелец (30.10): «его надо спустить ниже где связь чтобы они были на
   одном уровне, а то сейчас он слишком высоко». Замер до правки: низ
   значка 750 px от верха окна (то есть 150 px от низа), низ видимой
   кнопки связи — 816 px (84 px от низа). Значок висел на 66 px выше.

   Откуда 84: сама колонка связи стоит на `bottom:20px`, но под круглой
   кнопкой в ней живёт кнопка «наверх» (52 px) с зазором 12 px — её место
   и есть нижняя граница видимой кнопки. Поэтому низ значка = 20 + 52 + 12.
   Слагаемые записаны в calc, а не числом: если «наверх» когда-нибудь
   изменит размер, совпадение посчитается само.

   Правило включается ТОЛЬКО когда идёт чередование (признак
   `data-show-phase` на <html>, его ставит js/assistant.js). Выключит
   владелец чередование — оба окна видны одновременно, и знакóм на одной
   линии они просто наложились бы друг на друга; тогда остаётся прежний
   запас 150 px, значок стоит НАД кнопкой связи. */
html[data-show-phase] .as-dock{bottom:calc(20px + 52px + 12px)}
/* Уход в 3D: значок поворачивается вокруг правого нижнего угла и уходит в
   глубину. Прозрачность здесь НЕ анимируется намеренно: `opacity` меньше
   единицы у предка схлопывает `preserve-3d`, и в середине ухода фигура
   становилась бы плоской. Исчезновение делает сам поворот — на 74° значок
   виден с ребра, — а `visibility` убирает его из нажатий уже после. */
.as-dock[data-show="off"]{
  transform:perspective(760px) rotateY(74deg) rotateX(-10deg) translate3d(24px,10px,-40px) scale(.74);
  visibility:hidden; pointer-events:none;
  transition:transform .62s cubic-bezier(.5,.02,.62,1), visibility 0s .62s;
}
/* Владелец выключил 3D-эффект — просто гаснет. */
.as-dock[data-motion="0"]{transition:opacity .34s var(--ease), visibility 0s;}
.as-dock[data-motion="0"][data-show="off"]{transform:none; opacity:0;}

/* ---------- значок ---------- */
.as-orb{
  position:relative;
  width:var(--orb-h); height:var(--orb-h); padding:0;
  border:1px solid var(--line-strong); border-radius:34px;
  background:radial-gradient(130% 110% at 50% -10%, var(--surface), var(--bg-2) 62%);
  box-shadow:0 18px 40px rgba(0,0,0,.26);
  cursor:pointer; overflow:hidden;
  display:grid; place-items:center;
  /* `visibility` в переходе — для ухода в глубину: поворот заканчивается,
     и только потом значок перестаёт ловить нажатия. */
  transition:transform .4s cubic-bezier(.32,.86,.28,1), box-shadow .25s var(--ease), visibility 0s;
}
.as-orb:hover{transform:translateY(-2px); box-shadow:0 24px 48px rgba(0,0,0,.32)}
.as-orb:focus-visible{outline:2px solid var(--cyan); outline-offset:3px}

/* ---------- значок улетает, когда открыто окно ----------

   Владелец (30.10): «при нажатии на это чтобы оно спарялось в 3д моушен
   эффекте и появлялось вот это диалоговое окно» и «при нажатии накрытия
   этого окна также присутствовал 3д моушен эффект закрытия чтобы все
   выглядило WOW и со спецэффектами».

   Значок не гаснет, а ПОВОРАЧИВАЕТСЯ в глубину вокруг правого края: на 78°
   он виден с ребра и уходит назад, `visibility` убирает его из нажатий уже
   после поворота. Прозрачность здесь не годится — `opacity` меньше единицы
   схлопнула бы объёмную сцену внутри, и король на секунду стал бы плоской
   картинкой (это же правило у ухода по расписанию выше).

   Класс `is-closing` держит значок свёрнутым, пока окно складывается: если
   вернуть его сразу, он выскочил бы ПОВЕРХ ещё не уехавшего окна. */
.as-dock.is-open .as-orb,
.as-dock.is-closing .as-orb{
  transform:perspective(700px) rotateY(78deg) rotateX(12deg) translate3d(10px,14px,-50px) scale(.56);
  visibility:hidden; pointer-events:none;
  transition:transform .44s cubic-bezier(.4,0,.25,1), visibility 0s .44s;
}

.as-stage{
  position:absolute; inset:0;
  display:grid; place-items:center;
  perspective:520px; perspective-origin:50% 40%;
}
/* Сцена поворачивается целиком — как на поворотном столе. Ось Y в CSS
   смотрит вниз, поэтому «вверх» у фигуры — это минус по Y. */
.as-scene{
  position:relative; width:0; height:0;
  transform-style:preserve-3d;
  transform:rotateX(21deg) rotateY(-24deg);
  animation:as-turn 18s ease-in-out infinite;
}
.as-printer{
  position:absolute; left:0; top:0; width:0; height:0;
  transform-style:preserve-3d;
  transform:translateY(36px);
}

/* Деталь рамы: две плоскости на крест. Одна смотрит в лицо, вторая
   повёрнута на 90° — при повороте сцены они подменяют друг друга, и
   стойка не превращается в невидимую линию с ребра. */
.as-bar{position:absolute; left:0; top:0; width:0; height:0; transform-style:preserve-3d;}
.as-face{
  position:absolute; left:0; top:0; border-radius:1px;
  background:linear-gradient(180deg, rgba(160,152,142,.96), rgba(104,97,90,.96));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.22);
}
.as-post{
  position:absolute; left:0; top:0; border-radius:1px;
  background:linear-gradient(90deg, rgba(178,170,160,.96), rgba(94,88,82,.96));
}

/* ---------- стол-шахматная доска ----------
   Клетки — одной заливкой: 58 px на восемь клеток даёт ровно 7,25 px, а
   `background-size` 14,5 px — плитку из четырёх клеток. Толщина стола —
   четыре боковые плоскости: без них доска с ребра была бы листом бумаги. */
.as-bed{position:absolute; left:0; top:0; width:0; height:0; transform-style:preserve-3d;}
.as-bed-top{
  position:absolute; left:0; top:0; border-radius:2px;
  background-image:repeating-conic-gradient(#2a2622 0% 25%, #e9e0cf 0% 50%);
  background-size:14.5px 14.5px;
  box-shadow:inset 0 0 0 1.5px #17140f, 0 12px 26px rgba(0,0,0,.22);
}
.as-bed-side{
  position:absolute; left:0; top:0; border-radius:1px;
  background:linear-gradient(180deg, #3d352d, #1b1713);
}

/* ---------- король ----------
   Слой — плоский диск, повёрнутый в горизонт. Диаметр идёт по профилю
   точеной фигуры, поэтому снизу вверх читаются основание, ножка,
   воротник, голова и крест. */
.as-king{position:absolute; left:0; top:0; width:0; height:0; transform-style:preserve-3d;}
.as-slice{position:absolute; left:0; top:0;}
.as-slice-face{
  position:absolute; inset:0; border-radius:50%;
  /* Мягкий градиент, а не «кольцо»: у каждого слоя он свой, и резкая
     тёмная кромка превращала фигуру в стопку колец. */
  background:radial-gradient(circle at 46% 40%, #ffbe8a 0%, #fb8f47 55%, #f2711f 100%);
}
.as-cross{position:absolute; left:0; top:0; width:0; height:0; transform-style:preserve-3d;}
/* Крест — последний слой печати: он не должен висеть в воздухе, пока
   печатается основание. Появляется к концу печати, вместе с макушкой. */
.as-printer.is-print .as-cross{animation:as-cross .3s ease-out both; animation-delay:1.95s}
@keyframes as-cross{from{opacity:0} to{opacity:1}}
.as-cross-v{
  position:absolute; left:-2px; top:-6.5px; width:4px; height:13px; border-radius:1px;
  background:linear-gradient(180deg, #ffbb8c, #e8560f);
}
.as-cross-h{
  position:absolute; left:-5px; top:-2.5px; width:10px; height:4px; border-radius:1px;
  background:linear-gradient(90deg, #ffbb8c, #e8560f);
}

/* ---------- портал и сопло ---------- */
.as-gantry{
  position:absolute; left:0; top:0; width:0; height:0;
  transform-style:preserve-3d;
  /* Портал стоит чуть впереди фигуры (z=+16), а не в её плоскости: иначе
     луч проходит ровно сквозь короля, и браузер рисует их «вперемешку» —
     полосы то над фигурой, то под ней. */
  transform:translate3d(0,-10px,16px);
}
.as-tool{
  position:absolute; left:0; top:0; width:0; height:0;
  transform-style:preserve-3d;
  transform:translate3d(0,3px,0);
}
.as-tool-face{
  position:absolute; left:0; top:0; border-radius:2px;
  background:linear-gradient(180deg, #4c463f, #211e1a);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.16);
}
.as-nozzle{
  position:absolute; left:-2.5px; top:3.5px; width:5px; height:6px;
  background:linear-gradient(180deg, #9a9184, #3c3731);
  clip-path:polygon(0 0, 100% 0, 62% 100%, 38% 100%);
}
.as-drop{
  position:absolute; left:-2px; top:9.5px; width:4px; height:4px; border-radius:50%;
  background:#ffb066; box-shadow:0 0 6px 2px rgba(255,106,26,.5);
  opacity:0;
}

/* Печать: слои растут снизу вверх, портал поднимается, сопло ездит. */
.as-printer.is-print .as-slice-face{
  animation:as-layer .3s cubic-bezier(.2,.8,.3,1) both;
  animation-delay:calc(var(--i) * 36ms);
}
.as-printer.is-print .as-gantry{animation:as-gantry 2.05s linear both}
/* Сопло ездит быстро и ровно столько, сколько идёт печать: шесть проходов
   по 0,34 с — это те же 2,05 с. После печати оно замирает, как настоящая
   головка в покое. */
.as-printer.is-print .as-tool{animation:as-tool .34s ease-in-out 6 alternate both}
.as-printer.is-print .as-drop{animation:as-drop .7s ease-in-out infinite}

@keyframes as-turn{
  0%,100%{transform:rotateX(21deg) rotateY(-24deg)}
  50%{transform:rotateX(16deg) rotateY(22deg)}
}
@keyframes as-layer{
  from{opacity:0; transform:scale(.15)}
  to{opacity:1; transform:none}
}
@keyframes as-gantry{
  from{transform:translate3d(0,-10px,16px)}
  to{transform:translate3d(0,-68px,16px)}
}
@keyframes as-tool{
  from{transform:translate3d(-11px,3px,0)}
  to{transform:translate3d(11px,3px,0)}
}
@keyframes as-drop{
  0%,100%{opacity:.35; transform:translateY(0)}
  50%{opacity:1; transform:translateY(1.5px)}
}

/* ---------- реплика после печати ----------
   Владелец: «он потом начинает говорить что может помочь и ответить на
   вопросы». Это кнопка: прочитал — нажал — открылось окно. */
.as-say{
  position:relative;
  max-width:232px; padding:10px 13px;
  border:1px solid var(--line-strong); border-radius:14px;
  background:var(--bg-2); color:var(--ink);
  font:inherit; font-size:.8rem; line-height:1.45; text-align:left;
  box-shadow:0 16px 36px rgba(0,0,0,.22);
  cursor:pointer;
  opacity:0; transform:translateY(10px) scale(.94);
  transform-origin:100% 100%;
  pointer-events:none;
  transition:opacity .3s var(--ease), transform .3s var(--ease);
}
.as-say.is-on{opacity:1; transform:none; pointer-events:auto}
.as-say:focus-visible{outline:2px solid var(--cyan); outline-offset:3px}
.as-say::after{
  content:''; position:absolute; right:26px; bottom:-6px;
  width:11px; height:11px;
  background:var(--bg-2);
  border-right:1px solid var(--line-strong);
  border-bottom:1px solid var(--line-strong);
  transform:rotate(45deg);
}

/* ---------- окно ---------- */
.as-win{
  width:min(360px, calc(100vw - 40px));
  max-height:min(520px, 72vh);
  display:flex; flex-direction:column;
  /* padding:0 — ОБЯЗАТЕЛЕН. Окно объявлено <section>, а общее правило
     `section{padding:var(--gap-sec) 0}` (начало файла) добавляло ему 57,6 px
     сверху и столько же снизу. Отсюда и «слишком большие расстояния» на
     снимке владельца: над заголовком «Помощник» и под строкой ввода висели
     две пустые полосы, а само окно вырастало на 115 px. Класс весомее
     селектора тега, поэтому одного этого объявления хватает. */
  padding:0;
  border:1px solid var(--line-strong); border-radius:18px;
  background:var(--bg-2);
  box-shadow:0 30px 70px rgba(0,0,0,.34);
  overflow:hidden;
  /* Ось разворота — правый нижний угол, то есть ровно то место, где стоит
     значок. Окно «выезжает» из него, а не появляется из середины себя. */
  transform-origin:100% 100%;
}
.as-win[hidden]{display:none}

/* Окно занимает МЕСТО УЛЕТЕВШЕГО ЗНАЧКА.

   Без этого окно висело бы над пустотой: значок спрятан, но его полоса в
   колонке осталась, и низ окна стоял на 144 px выше нижней кромки. Отрицательное
   поле ровно на «значок + зазор» опускает окно туда, где значок стоял, а сам
   значок при этом не двигается — он и так невидим. Числа берутся из тех же
   переменных, что и раскладка, поэтому на телефоне (значок 112) считаются
   сами. */
.as-dock.is-open .as-win{margin-bottom:calc(-1 * (var(--orb-h) + 12px))}

/* Появление: окно разворачивается из глубины с лёгким перелётом, и по рамке
   проходит короткая вспышка акцента. Две анимации на одном элементе, потому
   что это два разных свойства: форма (transform) и свечение (box-shadow).
   `both` обязателен: без него окно на кадр мигнуло бы своим обычным видом. */
.as-win:not([hidden]){
  animation:as-win-in .56s both,
            as-win-flash .95s ease-out both;
}
/* Закрытие: то же движение назад и в глубину, но короче и без перелёта —
   уход не должен выглядеть как второй вход. Правило стоит ПОСЛЕ появления и
   с тем же весом, поэтому в момент закрытия побеждает оно. */
.as-win.is-closing{
  animation:as-win-out .36s cubic-bezier(.55,.02,.75,.2) both;
}

/* Разворот ДВУХСТУПЕНЧАТЫЙ, и это не украшательство.

   Первая версия была одним переходом от наклона к нулю с кривой
   `cubic-bezier(.2,.9,.25,1.06)`. Замер кадров показал, что на 50 мс окно уже
   стояло почти прямо: кривая с высокой первой точкой проходит 65% пути за
   первые 10% времени, и весь объём укладывался в две-три картинки — глаз его
   не ловил. Поэтому в ключевых кадрах стоит «задержка»: к 20% времени окно
   ещё повёрнуто на 30° и уменьшено, и только потом разворачивается. Угол
   виден, а не пролетает.

   У второго отрезка своя кривая с перелётом 1.06 — им окно «дожимается» в
   плоскость и чуть пружинит. */
@keyframes as-win-in{
  0%{
    opacity:0;
    transform:perspective(900px) rotateY(-38deg) rotateX(18deg)
              translate3d(18px,30px,-160px) scale(.66);
    animation-timing-function:cubic-bezier(.2,.6,.35,1);
  }
  20%{
    opacity:.62;
    transform:perspective(900px) rotateY(-30deg) rotateX(13deg)
              translate3d(12px,20px,-104px) scale(.78);
    animation-timing-function:cubic-bezier(.2,.9,.25,1.06);
  }
  100%{opacity:1; transform:none}
}
@keyframes as-win-out{
  from{opacity:1; transform:none}
  to{
    opacity:0;
    transform:perspective(900px) rotateY(-28deg) rotateX(13deg)
              translate3d(18px,30px,-140px) scale(.72);
  }
}
@keyframes as-win-flash{
  0%{box-shadow:0 30px 70px rgba(0,0,0,.34), 0 0 0 0 rgba(255,106,26,0)}
  22%{box-shadow:0 30px 70px rgba(0,0,0,.34), 0 0 0 3px rgba(255,106,26,.42),
                 0 0 46px rgba(255,106,26,.38)}
  100%{box-shadow:0 30px 70px rgba(0,0,0,.34), 0 0 0 0 rgba(255,106,26,0)}
}

.as-head{
  display:flex; align-items:center; gap:10px;
  padding:12px 14px;
  border-bottom:1px solid var(--line);
}
.as-title{font-size:.95rem; font-weight:700; color:var(--ink)}
.as-close{
  margin-left:auto; width:30px; height:30px; flex:none;
  border:1px solid var(--line); border-radius:9px;
  background:none; color:var(--ink-2);
  font-size:18px; line-height:1; cursor:pointer;
  transition:color .2s, border-color .2s, background .2s;
}
.as-close:hover{color:var(--ink); border-color:var(--line-strong); background:var(--surface)}

.as-feed{
  display:flex; flex-direction:column; gap:8px;
  flex:1 1 auto; overflow-y:auto;
  padding:14px;
  -webkit-overflow-scrolling:touch;
}

.as-msg{display:flex; max-width:88%}
.as-msg.is-me{align-self:flex-end; justify-content:flex-end}
.as-msg.is-bot, .as-msg.is-owner{align-self:flex-start}
.as-bubble{
  padding:9px 12px; border-radius:13px;
  font-size:.85rem; line-height:1.5;
  white-space:pre-wrap; overflow-wrap:anywhere;
  color:var(--ink);
  background:var(--surface);
  border:1px solid var(--line);
}
.as-msg.is-me .as-bubble{
  background:rgba(255,106,26,.14);
  border-color:rgba(255,106,26,.34);
  border-bottom-right-radius:4px;
}
.as-msg.is-bot .as-bubble{border-bottom-left-radius:4px}
/* Ответ владельца отличается от ответа помощника: посетитель должен
   видеть, что ответил человек. Полоса слева — самый дешёвый признак,
   который не путается с цветом темы. */
.as-msg.is-owner .as-bubble{
  border-left:3px solid var(--cyan);
  border-bottom-left-radius:4px;
}
.as-msg.is-typing .as-bubble{opacity:.55; transition:opacity .24s}

.as-form{
  display:flex; align-items:center; gap:8px;
  padding:10px 12px;
  border-top:1px solid var(--line);
}
.as-input{
  flex:1 1 auto; min-width:0;
  padding:10px 12px;
  border:1px solid var(--line); border-radius:11px;
  background:var(--bg); color:var(--ink);
  font:inherit; font-size:.86rem;
}
.as-input::placeholder{color:var(--ink-3)}
.as-input:focus{
  outline:none; border-color:rgba(255,106,26,.55);
  box-shadow:0 0 0 3px rgba(255,106,26,.12);
}
.as-send{
  flex:none;
  padding:10px 14px;
  border:0; border-radius:11px;
  /* Глубокий филамент, а не яркий: белая надпись на #ff6a1a даёт контраст
     2.5 — ниже нормы. На бумаге --violet это #c24405, на тёмной теме —
     фиолетовый; в обоих случаях белый текст читается. */
  background:var(--violet); color:#fff;
  font:inherit; font-size:.84rem; font-weight:700;
  cursor:pointer;
  transition:filter .2s;
}
.as-send:hover{filter:brightness(1.08)}
.as-send:focus-visible{outline:2px solid var(--ink); outline-offset:2px}

@media (max-width:640px){
  /* Высота значка — переменной: от неё считается и его размер, и то, на
     сколько опускается окно, когда значок улетает. */
  .as-dock{right:14px; bottom:136px; --orb-h:112px}
  /* Та же линия, что у связи на телефоне: колонка стоит на 14 px, под
     кнопкой — «наверх» 46 px и зазор 12 px. Замер: низ кнопки связи
     772 px при окне 844, то есть 72 px от низа. */
  html[data-show-phase] .as-dock{bottom:calc(14px + 46px + 12px)}
  .as-orb{border-radius:28px}
  .as-say{max-width:198px}
  .as-win{width:calc(100vw - 28px); max-height:74vh}
}

@media (prefers-reduced-motion: reduce){
  .as-scene,
  .as-printer.is-print .as-slice-face,
  .as-printer.is-print .as-gantry,
  .as-printer.is-print .as-tool,
  .as-printer.is-print .as-cross,
  .as-printer.is-print .as-drop{
    animation:none;
  }
  /* Окно и значок при уменьшенном движении просто появляются: разворот в
     глубине — это ровно то движение, которого владелец такого режима не
     хочет. */
  .as-win:not([hidden]), .as-win.is-closing{animation:none}
  .as-dock, .as-dock[data-show="off"]{transition:none}
  .as-orb, .as-dock.is-open .as-orb, .as-dock.is-closing .as-orb{transition:none}
}
