* { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, "Segoe UI", Roboto, sans-serif; }
body { background: #0f0f14; color: #f5f5f7; min-height: 100vh; }

/* Панель входа над hero. Занимает только свою строку и не влияет на ширину
   остальной страницы: hero и upload-screen остаются как были. */
.topbar { display: flex; justify-content: flex-end; padding: 14px 20px 0; }
.btn-ghost { background: transparent; border: 1px solid #3a3a45; color: #f5f5f7; padding: 8px 16px; border-radius: 10px; font-size: 14px; cursor: pointer; }

.hero { text-align: center; padding: 60px 20px 30px; }
.hero h1 { font-size: 32px; margin-bottom: 12px; line-height: 1.3; }
.subtitle { color: #a0a0ab; font-size: 18px; }

.screen { max-width: 900px; margin: 0 auto; padding: 20px; text-align: center; }
.screen.hidden { display: none; }

.upload-box { border: 2px dashed #3a3a45; border-radius: 16px; padding: 60px 20px; margin-top: 20px; }
.btn-primary { background: linear-gradient(135deg,#7c3aed,#a855f7); color: #fff; border: none; padding: 16px 32px; border-radius: 12px; font-size: 16px; font-weight: 600; cursor: pointer; }
.btn-large { font-size: 18px; padding: 18px 40px; margin-top: 24px; }
.hint { color: #7a7a85; font-size: 14px; margin-top: 12px; }

.progress-bar { width: 100%; height: 10px; background: #232330; border-radius: 5px; overflow: hidden; margin: 20px 0; }
.progress-fill { height: 100%; width: 0%; background: linear-gradient(90deg,#7c3aed,#a855f7); transition: width 0.4s ease; }

/* Индикатор обработки на progress-screen: четыре светящихся шарика, качающихся
   по пологой дуге. Ни одно существующее правило его не читает и он ничего не
   перекрывает — блок просто встаёт в поток над полосой прогресса.

   Единственная ручка цвета. В файле до сих пор не было ни одной custom property
   (все цвета — литералы), поэтому переменная заводится здесь, но значение взято
   не с потолка: 168,85,247 — это #a855f7, тот же акцент, что уже стоит в
   .progress-fill, .btn-primary и .btn-link. Хранится тройкой RGB, а не hex,
   потому что свечение состоит из полупрозрачных слоёв: rgba(var(...), a) работает
   везде, тогда как альфа поверх hex-переменной требовала бы color-mix(). */
:root { --pulse-accent-rgb: 168, 85, 247; }

.processing-pulse {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 18px;
  height: 44px;
  margin: 26px 0 6px;
}

/* Общее фоновое зарево под рядом. Лежит ниже шариков (z-index) и не ловит
   события (pointer-events), чтобы не мешать ничему на экране. */
.processing-pulse::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 190px;
  height: 60px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(ellipse at center,
              rgba(var(--pulse-accent-rgb), 0.13) 0%,
              rgba(var(--pulse-accent-rgb), 0.05) 40%,
              rgba(var(--pulse-accent-rgb), 0) 100%);
  filter: blur(10px);
  pointer-events: none;
  z-index: 0;
  animation: pulseHalo 6s ease-in-out infinite;
}

.processing-pulse span {
  position: relative;
  z-index: 1;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  /* Блик смещён от центра: на круге вращение иначе было бы не видно вовсе. */
  background: radial-gradient(circle at 34% 30%,
              rgb(var(--pulse-accent-rgb)) 0%,
              rgba(var(--pulse-accent-rgb), 0.86) 55%,
              rgba(var(--pulse-accent-rgb), 0.55) 100%);
  /* Четыре слоя разного радиуса и прозрачности: ядро, ближний ореол и два
     дальних — так свечение спадает плавно, а не обрывается кольцом. */
  box-shadow:
    0 0 4px rgba(var(--pulse-accent-rgb), 0.95),
    0 0 11px rgba(var(--pulse-accent-rgb), 0.55),
    0 0 24px rgba(var(--pulse-accent-rgb), 0.28),
    0 0 44px rgba(var(--pulse-accent-rgb), 0.12);
  animation: pulseArc 2.6s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}

/* Сдвиг фазы делает из четырёх одинаковых качелей бегущую волну. Шаг мелкий
   (0.16 с при цикле 2.6 с — около 22°), поэтому соседние шарики расходятся
   максимум на ~3 px при просвете 8 px и никогда не наезжают друг на друга. */
.processing-pulse span:nth-child(2) { animation-delay: 0.16s; }
.processing-pulse span:nth-child(3) { animation-delay: 0.32s; }

/* Последний — намеренно не такой, как остальные: выше апекс, чуть крупнее и
   период на 0.3 с длиннее. Из-за разного периода он медленно уходит от общей
   волны и возвращается к ней, так что за 30-90 с картинка ни разу не повторяется
   буквально. Плюс медленный доворот, заметный по смещённому блику. */
.processing-pulse span:nth-child(4) {
  width: 11px;
  height: 11px;
  animation: pulseArcTail 2.9s cubic-bezier(0.45, 0, 0.55, 1) infinite;
  animation-delay: 0.48s;
}

/* Цикл замкнут: кадр 0% и кадр 100% совпадают до пикселя, поэтому на стыке
   повторов нет скачка. Шарик уходит вправо через верх и возвращается тем же
   путём; на краях он тускнеет и сжимается, в апексе — ярче и крупнее всего. */
@keyframes pulseArc {
  0%, 100% { transform: translate3d(-7px, 2px, 0) scale(0.78); opacity: 0.34; }
  25%      { transform: translate3d(0, -6px, 0)   scale(1.06); opacity: 1; }
  50%      { transform: translate3d(7px, 2px, 0)  scale(0.78); opacity: 0.34; }
  75%      { transform: translate3d(0, -6px, 0)   scale(1.06); opacity: 1; }
}

@keyframes pulseArcTail {
  0%, 100% { transform: translate3d(-6px, 2px, 0) scale(0.76) rotate(0deg);   opacity: 0.38; }
  25%      { transform: translate3d(0, -9px, 0)   scale(1.14) rotate(90deg);  opacity: 1; }
  50%      { transform: translate3d(6px, 2px, 0)  scale(0.76) rotate(180deg); opacity: 0.38; }
  75%      { transform: translate3d(0, -9px, 0)   scale(1.14) rotate(270deg); opacity: 1; }
}

@keyframes pulseHalo {
  0%, 100% { opacity: 0.6; transform: translate(-50%, -50%) scale(0.94); }
  50%      { opacity: 1;   transform: translate(-50%, -50%) scale(1.05); }
}

/* Движение выключается целиком, а не смягчается: в файле уже есть такой прецедент
   (.gen-reveal), и для вестибулярных нарушений остановка надёжнее ослабленной
   анимации. Индикатор при этом не выглядит сломанным — остаётся ровное свечение,
   а «идёт работа» и без него сообщают #progressFill и #progressText, которые
   продолжают обновляться. */
@media (prefers-reduced-motion: reduce) {
  .processing-pulse span,
  .processing-pulse span:nth-child(4),
  .processing-pulse::before {
    animation: none;
    transform: none;
    opacity: 0.9;
  }
  .processing-pulse::before { transform: translate(-50%, -50%); opacity: 0.7; }
}

.warning-text { color: #f59e0b; margin-bottom: 20px; }
.results-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px,1fr)); gap: 14px; margin: 20px 0; }
.results-grid img { width: 100%; border-radius: 10px; aspect-ratio: 3/4; object-fit: cover; background: #1a1a24; }

/* Прогрессивный показ: кадр проявляется в момент добавления в сетку. Анимация
   разовая (both) и висит на самом <img>, поэтому уже показанные карточки при
   появлении соседей не перерисовываются. */
.results-grid img.gen-reveal { animation: genReveal 260ms ease-out both; }
@keyframes genReveal { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .results-grid img.gen-reveal { animation: none; } }
/* «Готово N из 10» на экране результата — состояние, а не предупреждение. */
.result-note-progress { color: #7a7a85; }

/* ============================================================================
   Плитки-плейсхолдеры демо-сетки.

   Десять плиток встают в .results-grid сразу после ответа POST /api/demo/generate,
   по одной на генерацию, и до прихода своего кадра заняты узором: пять
   светящихся узлов, ходящих по замкнутым путям, и пять связей между ними,
   которые тянутся за узлами. Ни canvas, ни requestAnimationFrame, ни JS-цикла:
   после вставки разметки анимация живёт сама.

   ПОЧЕМУ УЗЕЛ И СВЯЗЬ — ЭТО СЛОЙ ПЛЮС ::before, А НЕ ОДИН ЭЛЕМЕНТ.
   Проценты в translate() считаются от собственного размера элемента. Точка —
   6x6 px, значит translate(10%, 10%) сдвинул бы её на 0.6 px, а не на 10%
   ширины плитки; у линии высота 1 px, и translateY(%) бессмыслен вовсе.
   Поэтому .net-node и .net-link растянуты на всю плитку (inset: 0) и несут
   только движение, а видимые точка и линия рисуются их ::before. Разметку
   это не меняет.

   ГЕОМЕТРИЯ СЧИТАЕТСЯ ЗАРАНЕЕ. Плитка имеет постоянное соотношение сторон 3/4
   (то же, что у .results-grid img, — чтобы макет не дёргался при подмене узора
   фотографией). При постоянном соотношении проценты масштабируются равномерно,
   и угол между двумя процентными точками не зависит от ширины плитки. Для узлов
   (x1,y1) и (x2,y2), заданных в процентах:

       dx = x2 - x1                 (в % ширины)
       dy = (y2 - y1) * 4/3         (перевод % высоты в % ширины)
       width  = sqrt(dx^2 + dy^2)   (в % ширины)
       rotate = atan2(dy, dx)

   Путь узла — фигура Лиссажу, смещение в тех же % ширины:

       u(t) = Ax * [cos(2*pi*fx*t + f) - cos(f)]
       v(t) = Ay * [sin(2*pi*fy*t + f) - sin(f)]

   Вычитание значения при t=0 обязательно: оно даёт translate(0,0) на кадре 0%,
   то есть кривая проходит через базовую позицию узла, и нулевой кадр совпадает
   со статичным узором. Именно его показывает prefers-reduced-motion.
   В CSS вертикаль умножается на 3/4 — обратный перевод в % высоты; без этого
   круг превратился бы в эллипс.

   ДЛИНА И УГОЛ СВЯЗИ — ЧЕРЕЗ scaleX/rotate, А НЕ width/rotate. width участвует
   в раскладке, и его анимация заставляла бы браузер пересчитывать layout каждый
   кадр для каждой из полутора сотен линий. scaleX композиторный и вдобавок не
   трогает высоту, поэтому линия остаётся ровно 1 px при любом растяжении.
   Слой связи повторяет путь узла A и переиспользует ЕГО keyframes.

   Стопов десять. Между ними браузер интерполирует rotate и scaleX линейно, а
   путь узла — кривая, поэтому конец линии слегка расходится с самим узлом.
   Максимум расхождения: 0.59% ширины у первого паттерна, 1.00% у второго,
   1.21% у третьего. Порог — радиус точки (3 px, то есть ~1.75% при типичной
   ширине плитки 170 px): выше него линия визуально отрывается от узла. На восьми
   стопах второй паттерн давал 1.59% — впритык, на двенадцати выигрыш уже не виден.

   Порядок детей внутри .gen-net — контракт разметки: сначала пять .net-node,
   затем пять .net-link. На нём держатся все :nth-child ниже.
   ========================================================================= */

.gen-tile {
  position: relative;
  width: 100%;
  aspect-ratio: 3/4;
  border-radius: 10px;
  background: #1a1a24;
  overflow: hidden;
}

/* Фото приходит ВНУТРЬ плитки, оставаясь потомком .results-grid, поэтому правило
   .results-grid img.gen-reveal продолжает совпадать и отдельная анимация
   появления не нужна. aspect-ratio снимается: размер уже задан плиткой. */
.gen-tile img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  aspect-ratio: auto;
  object-fit: cover;
  border-radius: 10px;
}

.gen-net { position: absolute; inset: 0; pointer-events: none; }

/* Слой узла: несёт только путь. Видимая точка — в ::before. */
.net-node { position: absolute; inset: 0; }

.net-node::before {
  content: '';
  position: absolute;
  width: 6px;
  height: 6px;
  /* Центрируется margin'ами, а не translate(-50%,-50%): проценты в translate
     считались бы от 6 px, а не от плитки. */
  margin: -3px 0 0 -3px;
  border-radius: 50%;
  background: rgb(var(--pulse-accent-rgb));
  /* Два слоя вместо четырёх у .processing-pulse: там светились 4 точки, здесь 50,
     и четырёхслойные тени на 44px залили бы плитку сплошным фиолетовым. */
  box-shadow:
    0 0 4px rgba(var(--pulse-accent-rgb), 0.9),
    0 0 10px rgba(var(--pulse-accent-rgb), 0.4);
}

/* Слой связи: повторяет путь своего первого узла и мерцает. */
.net-link { position: absolute; inset: 0; }

.net-link::before {
  content: '';
  position: absolute;
  height: 1px;
  margin-top: -0.5px;
  background: rgb(var(--pulse-accent-rgb));
  transform-origin: 0 50%;
}

/* Паттерн 1 — восходящая дуга, узлы по кругу. */
.net-pattern-1 .net-node:nth-child(1)::before { left: 22%; top: 20%; }
.net-pattern-1 .net-node:nth-child(2)::before { left: 74%; top: 30%; }
.net-pattern-1 .net-node:nth-child(3)::before { left: 50%; top: 55%; }
.net-pattern-1 .net-node:nth-child(4)::before { left: 28%; top: 78%; }
.net-pattern-1 .net-node:nth-child(5)::before { left: 78%; top: 72%; }
.net-pattern-1 .net-link:nth-child(6)::before { left: 22%; top: 20%; width: 53.68%; transform: rotate(14.38deg); }
.net-pattern-1 .net-link:nth-child(7)::before { left: 74%; top: 30%; width: 41.07%; transform: rotate(125.75deg); }
.net-pattern-1 .net-link:nth-child(8)::before { left: 50%; top: 55%; width: 37.74%; transform: rotate(125.66deg); }
.net-pattern-1 .net-link:nth-child(9)::before { left: 50%; top: 55%; width: 36.02%; transform: rotate(38.99deg); }
.net-pattern-1 .net-link:nth-child(10)::before { left: 22%; top: 20%; width: 54.42%; transform: rotate(59.04deg); }

/* Паттерн 2 — вертикальная цепь, узлы восьмёркой 1:2. */
.net-pattern-2 .net-node:nth-child(1)::before { left: 30%; top: 14%; }
.net-pattern-2 .net-node:nth-child(2)::before { left: 68%; top: 34%; }
.net-pattern-2 .net-node:nth-child(3)::before { left: 24%; top: 52%; }
.net-pattern-2 .net-node:nth-child(4)::before { left: 70%; top: 68%; }
.net-pattern-2 .net-node:nth-child(5)::before { left: 44%; top: 86%; }
.net-pattern-2 .net-link:nth-child(6)::before { left: 30%; top: 14%; width: 46.42%; transform: rotate(35.06deg); }
.net-pattern-2 .net-link:nth-child(7)::before { left: 68%; top: 34%; width: 50.12%; transform: rotate(151.39deg); }
.net-pattern-2 .net-link:nth-child(8)::before { left: 24%; top: 52%; width: 50.71%; transform: rotate(24.88deg); }
.net-pattern-2 .net-link:nth-child(9)::before { left: 70%; top: 68%; width: 35.38%; transform: rotate(137.29deg); }
.net-pattern-2 .net-link:nth-child(10)::before { left: 68%; top: 34%; width: 45.38%; transform: rotate(87.47deg); }

/* Паттерн 3 — звезда, узлы восьмёркой 2:1. */
.net-pattern-3 .net-node:nth-child(1)::before { left: 50%; top: 26%; }
.net-pattern-3 .net-node:nth-child(2)::before { left: 20%; top: 46%; }
.net-pattern-3 .net-node:nth-child(3)::before { left: 80%; top: 44%; }
.net-pattern-3 .net-node:nth-child(4)::before { left: 34%; top: 76%; }
.net-pattern-3 .net-node:nth-child(5)::before { left: 66%; top: 80%; }
.net-pattern-3 .net-link:nth-child(6)::before { left: 50%; top: 26%; width: 40.14%; transform: rotate(138.37deg); }
.net-pattern-3 .net-link:nth-child(7)::before { left: 50%; top: 26%; width: 38.42%; transform: rotate(38.66deg); }
.net-pattern-3 .net-link:nth-child(8)::before { left: 20%; top: 46%; width: 42.38%; transform: rotate(70.71deg); }
.net-pattern-3 .net-link:nth-child(9)::before { left: 80%; top: 44%; width: 50.00%; transform: rotate(106.26deg); }
.net-pattern-3 .net-link:nth-child(10)::before { left: 34%; top: 76%; width: 32.44%; transform: rotate(9.46deg); }

/* Привязка путей. Все пять узлов плитки и все её связи обязаны иметь ОДНУ
   длительность: иначе связь и её концы разъедутся по фазе. Поэтому цикл задан
   паттерном, а не узлом. linear, а не ease-in-out: смягчение на каждом из десяти
   отрезков превратило бы равномерное движение в пульсирующее, а форма пути и так
   задана кривой. */
.net-pattern-1 .net-node:nth-child(1) { animation: netPath1a 8.9s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-1 .net-node:nth-child(2) { animation: netPath1b 8.9s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-1 .net-node:nth-child(3) { animation: netPath1c 8.9s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-1 .net-node:nth-child(4) { animation: netPath1d 8.9s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-1 .net-node:nth-child(5) { animation: netPath1e 8.9s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-1 .net-link:nth-child(6) { animation: netLink 4.2s ease-in-out infinite, netPath1a 8.9s linear infinite; animation-delay: calc(var(--tile-phase, 0s) + var(--link-phase, 0s)), var(--tile-phase, 0s); }
.net-pattern-1 .net-link:nth-child(6)::before { animation: netTrack11 8.9s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-1 .net-link:nth-child(7) { animation: netLink 4.2s ease-in-out infinite, netPath1b 8.9s linear infinite; animation-delay: calc(var(--tile-phase, 0s) + var(--link-phase, 0s)), var(--tile-phase, 0s); }
.net-pattern-1 .net-link:nth-child(7)::before { animation: netTrack12 8.9s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-1 .net-link:nth-child(8) { animation: netLink 4.2s ease-in-out infinite, netPath1c 8.9s linear infinite; animation-delay: calc(var(--tile-phase, 0s) + var(--link-phase, 0s)), var(--tile-phase, 0s); }
.net-pattern-1 .net-link:nth-child(8)::before { animation: netTrack13 8.9s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-1 .net-link:nth-child(9) { animation: netLink 4.2s ease-in-out infinite, netPath1c 8.9s linear infinite; animation-delay: calc(var(--tile-phase, 0s) + var(--link-phase, 0s)), var(--tile-phase, 0s); }
.net-pattern-1 .net-link:nth-child(9)::before { animation: netTrack14 8.9s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-1 .net-link:nth-child(10) { animation: netLink 4.2s ease-in-out infinite, netPath1a 8.9s linear infinite; animation-delay: calc(var(--tile-phase, 0s) + var(--link-phase, 0s)), var(--tile-phase, 0s); }
.net-pattern-1 .net-link:nth-child(10)::before { animation: netTrack15 8.9s linear infinite; animation-delay: var(--tile-phase, 0s); }

.net-pattern-2 .net-node:nth-child(1) { animation: netPath2a 10.3s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-2 .net-node:nth-child(2) { animation: netPath2b 10.3s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-2 .net-node:nth-child(3) { animation: netPath2c 10.3s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-2 .net-node:nth-child(4) { animation: netPath2d 10.3s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-2 .net-node:nth-child(5) { animation: netPath2e 10.3s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-2 .net-link:nth-child(6) { animation: netLink 4.2s ease-in-out infinite, netPath2a 10.3s linear infinite; animation-delay: calc(var(--tile-phase, 0s) + var(--link-phase, 0s)), var(--tile-phase, 0s); }
.net-pattern-2 .net-link:nth-child(6)::before { animation: netTrack21 10.3s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-2 .net-link:nth-child(7) { animation: netLink 4.2s ease-in-out infinite, netPath2b 10.3s linear infinite; animation-delay: calc(var(--tile-phase, 0s) + var(--link-phase, 0s)), var(--tile-phase, 0s); }
.net-pattern-2 .net-link:nth-child(7)::before { animation: netTrack22 10.3s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-2 .net-link:nth-child(8) { animation: netLink 4.2s ease-in-out infinite, netPath2c 10.3s linear infinite; animation-delay: calc(var(--tile-phase, 0s) + var(--link-phase, 0s)), var(--tile-phase, 0s); }
.net-pattern-2 .net-link:nth-child(8)::before { animation: netTrack23 10.3s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-2 .net-link:nth-child(9) { animation: netLink 4.2s ease-in-out infinite, netPath2d 10.3s linear infinite; animation-delay: calc(var(--tile-phase, 0s) + var(--link-phase, 0s)), var(--tile-phase, 0s); }
.net-pattern-2 .net-link:nth-child(9)::before { animation: netTrack24 10.3s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-2 .net-link:nth-child(10) { animation: netLink 4.2s ease-in-out infinite, netPath2b 10.3s linear infinite; animation-delay: calc(var(--tile-phase, 0s) + var(--link-phase, 0s)), var(--tile-phase, 0s); }
.net-pattern-2 .net-link:nth-child(10)::before { animation: netTrack25 10.3s linear infinite; animation-delay: var(--tile-phase, 0s); }

.net-pattern-3 .net-node:nth-child(1) { animation: netPath3a 11.3s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-3 .net-node:nth-child(2) { animation: netPath3b 11.3s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-3 .net-node:nth-child(3) { animation: netPath3c 11.3s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-3 .net-node:nth-child(4) { animation: netPath3d 11.3s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-3 .net-node:nth-child(5) { animation: netPath3e 11.3s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-3 .net-link:nth-child(6) { animation: netLink 4.2s ease-in-out infinite, netPath3a 11.3s linear infinite; animation-delay: calc(var(--tile-phase, 0s) + var(--link-phase, 0s)), var(--tile-phase, 0s); }
.net-pattern-3 .net-link:nth-child(6)::before { animation: netTrack31 11.3s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-3 .net-link:nth-child(7) { animation: netLink 4.2s ease-in-out infinite, netPath3a 11.3s linear infinite; animation-delay: calc(var(--tile-phase, 0s) + var(--link-phase, 0s)), var(--tile-phase, 0s); }
.net-pattern-3 .net-link:nth-child(7)::before { animation: netTrack32 11.3s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-3 .net-link:nth-child(8) { animation: netLink 4.2s ease-in-out infinite, netPath3b 11.3s linear infinite; animation-delay: calc(var(--tile-phase, 0s) + var(--link-phase, 0s)), var(--tile-phase, 0s); }
.net-pattern-3 .net-link:nth-child(8)::before { animation: netTrack33 11.3s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-3 .net-link:nth-child(9) { animation: netLink 4.2s ease-in-out infinite, netPath3c 11.3s linear infinite; animation-delay: calc(var(--tile-phase, 0s) + var(--link-phase, 0s)), var(--tile-phase, 0s); }
.net-pattern-3 .net-link:nth-child(9)::before { animation: netTrack34 11.3s linear infinite; animation-delay: var(--tile-phase, 0s); }
.net-pattern-3 .net-link:nth-child(10) { animation: netLink 4.2s ease-in-out infinite, netPath3d 11.3s linear infinite; animation-delay: calc(var(--tile-phase, 0s) + var(--link-phase, 0s)), var(--tile-phase, 0s); }
.net-pattern-3 .net-link:nth-child(10)::before { animation: netTrack35 11.3s linear infinite; animation-delay: var(--tile-phase, 0s); }

/* Мерцание связи: базовая непрозрачность лежит на слое, её же и анимируем. */
.net-link { opacity: 0.08; }

/* Сдвиг фазы мерцания внутри плитки: пять связей вспыхивают не разом, а
   перекатываются по узору. --link-phase попадает ТОЛЬКО в задержку мерцания
   (первая в списке выше); если он попадёт и на путь, геометрия связи разъедется
   со своими узлами. Задержки отрицательные — анимация стартует с середины цикла,
   и первая волна не заставляет ждать несколько секунд пустой плитки. */
.net-link:nth-child(6)  { --link-phase: 0s; }
.net-link:nth-child(7)  { --link-phase: -0.9s; }
.net-link:nth-child(8)  { --link-phase: -1.8s; }
.net-link:nth-child(9)  { --link-phase: -2.7s; }
.net-link:nth-child(10) { --link-phase: -3.6s; }

/* Сдвиг фазы между плитками. Без него десять ячеек двигались бы синхронно и
   сетка читалась бы как один пульсирующий блок. */
.gen-tile:nth-child(1)  { --tile-phase: 0s; }
.gen-tile:nth-child(2)  { --tile-phase: -0.37s; }
.gen-tile:nth-child(3)  { --tile-phase: -0.74s; }
.gen-tile:nth-child(4)  { --tile-phase: -1.11s; }
.gen-tile:nth-child(5)  { --tile-phase: -1.48s; }
.gen-tile:nth-child(6)  { --tile-phase: -1.85s; }
.gen-tile:nth-child(7)  { --tile-phase: -2.22s; }
.gen-tile:nth-child(8)  { --tile-phase: -2.59s; }
.gen-tile:nth-child(9)  { --tile-phase: -2.96s; }
.gen-tile:nth-child(10) { --tile-phase: -3.33s; }

/* Пути узлов. */
@keyframes netPath1a {
    0% { transform: translate(0%, 0%); }
   10% { transform: translate(-1.15%, 2.65%); }
   20% { transform: translate(-4.15%, 4.28%); }
   30% { transform: translate(-7.85%, 4.28%); }
   40% { transform: translate(-10.85%, 2.65%); }
   50% { transform: translate(-12.00%, 0%); }
   60% { transform: translate(-10.85%, -2.65%); }
   70% { transform: translate(-7.85%, -4.28%); }
   80% { transform: translate(-4.15%, -4.28%); }
   90% { transform: translate(-1.15%, -2.65%); }
  100% { transform: translate(0%, 0%); }
}
@keyframes netPath1b {
    0% { transform: translate(0%, 0%); }
   10% { transform: translate(-3.09%, 0%); }
   20% { transform: translate(-5.59%, -1.36%); }
   30% { transform: translate(-6.55%, -3.57%); }
   40% { transform: translate(-5.59%, -5.77%); }
   50% { transform: translate(-3.09%, -7.13%); }
   60% { transform: translate(0%, -7.13%); }
   70% { transform: translate(2.50%, -5.77%); }
   80% { transform: translate(3.45%, -3.57%); }
   90% { transform: translate(2.50%, -1.36%); }
  100% { transform: translate(0%, 0%); }
}
@keyframes netPath1c {
    0% { transform: translate(0%, 0%); }
   10% { transform: translate(-1.34%, -3.09%); }
   20% { transform: translate(0%, -6.17%); }
   30% { transform: translate(3.50%, -8.08%); }
   40% { transform: translate(7.83%, -8.08%); }
   50% { transform: translate(11.33%, -6.17%); }
   60% { transform: translate(12.66%, -3.09%); }
   70% { transform: translate(11.33%, 0%); }
   80% { transform: translate(7.83%, 1.91%); }
   90% { transform: translate(3.50%, 1.91%); }
  100% { transform: translate(0%, 0%); }
}
@keyframes netPath1d {
    0% { transform: translate(0%, 0%); }
   10% { transform: translate(2.75%, -1.50%); }
   20% { transform: translate(6.15%, -1.50%); }
   30% { transform: translate(8.90%, 0%); }
   40% { transform: translate(9.95%, 2.42%); }
   50% { transform: translate(8.90%, 4.85%); }
   60% { transform: translate(6.15%, 6.35%); }
   70% { transform: translate(2.75%, 6.35%); }
   80% { transform: translate(0%, 4.85%); }
   90% { transform: translate(-1.05%, 2.42%); }
  100% { transform: translate(0%, 0%); }
}
@keyframes netPath1e {
    0% { transform: translate(0%, 0%); }
   10% { transform: translate(3.00%, 1.63%); }
   20% { transform: translate(4.15%, 4.28%); }
   30% { transform: translate(3.00%, 6.92%); }
   40% { transform: translate(0%, 8.56%); }
   50% { transform: translate(-3.71%, 8.56%); }
   60% { transform: translate(-6.71%, 6.92%); }
   70% { transform: translate(-7.85%, 4.28%); }
   80% { transform: translate(-6.71%, 1.63%); }
   90% { transform: translate(-3.71%, 0%); }
  100% { transform: translate(0%, 0%); }
}
@keyframes netPath2a {
    0% { transform: translate(0%, 0%); }
   10% { transform: translate(-1.15%, 2.85%); }
   20% { transform: translate(-4.15%, 1.76%); }
   30% { transform: translate(-7.85%, -1.76%); }
   40% { transform: translate(-10.85%, -2.85%); }
   50% { transform: translate(-12.00%, 0%); }
   60% { transform: translate(-10.85%, 2.85%); }
   70% { transform: translate(-7.85%, 1.76%); }
   80% { transform: translate(-4.15%, -1.76%); }
   90% { transform: translate(-1.15%, -2.85%); }
  100% { transform: translate(0%, 0%); }
}
@keyframes netPath2b {
    0% { transform: translate(0%, 0%); }
   10% { transform: translate(-1.24%, -3.97%); }
   20% { transform: translate(0%, -5.19%); }
   30% { transform: translate(3.25%, -1.98%); }
   40% { transform: translate(7.27%, 1.23%); }
   50% { transform: translate(10.52%, 0%); }
   60% { transform: translate(11.76%, -3.97%); }
   70% { transform: translate(10.52%, -5.19%); }
   80% { transform: translate(7.27%, -1.98%); }
   90% { transform: translate(3.25%, 1.23%); }
  100% { transform: translate(0%, 0%); }
}
@keyframes netPath2c {
    0% { transform: translate(0%, 0%); }
   10% { transform: translate(2.75%, 2.85%); }
   20% { transform: translate(3.80%, 5.71%); }
   30% { transform: translate(2.75%, 4.62%); }
   40% { transform: translate(0%, 1.09%); }
   50% { transform: translate(-3.40%, 0%); }
   60% { transform: translate(-6.15%, 2.85%); }
   70% { transform: translate(-7.20%, 5.71%); }
   80% { transform: translate(-6.15%, 4.62%); }
   90% { transform: translate(-3.40%, 1.09%); }
  100% { transform: translate(0%, 0%); }
}
@keyframes netPath2d {
    0% { transform: translate(0%, 0%); }
   10% { transform: translate(-3.71%, -0.95%); }
   20% { transform: translate(-6.71%, -4.04%); }
   30% { transform: translate(-7.85%, -4.99%); }
   40% { transform: translate(-6.71%, -2.50%); }
   50% { transform: translate(-3.71%, 0%); }
   60% { transform: translate(0%, -0.95%); }
   70% { transform: translate(3.00%, -4.04%); }
   80% { transform: translate(4.15%, -4.99%); }
   90% { transform: translate(3.00%, -2.50%); }
  100% { transform: translate(0%, 0%); }
}
@keyframes netPath2e {
    0% { transform: translate(0%, 0%); }
   10% { transform: translate(2.50%, -1.23%); }
   20% { transform: translate(5.59%, 1.98%); }
   30% { transform: translate(8.09%, 5.19%); }
   40% { transform: translate(9.05%, 3.97%); }
   50% { transform: translate(8.09%, 0%); }
   60% { transform: translate(5.59%, -1.23%); }
   70% { transform: translate(2.50%, 1.98%); }
   80% { transform: translate(0%, 5.19%); }
   90% { transform: translate(-0.95%, 3.97%); }
  100% { transform: translate(0%, 0%); }
}
@keyframes netPath3a {
    0% { transform: translate(0%, 0%); }
   10% { transform: translate(-3.80%, 1.98%); }
   20% { transform: translate(-9.95%, 3.21%); }
   30% { transform: translate(-9.95%, 3.21%); }
   40% { transform: translate(-3.80%, 1.98%); }
   50% { transform: translate(0%, 0%); }
   60% { transform: translate(-3.80%, -1.98%); }
   70% { transform: translate(-9.95%, -3.21%); }
   80% { transform: translate(-9.95%, -3.21%); }
   90% { transform: translate(-3.80%, -1.98%); }
  100% { transform: translate(0%, 0%); }
}
@keyframes netPath3b {
    0% { transform: translate(0%, 0%); }
   10% { transform: translate(-4.15%, -1.09%); }
   20% { transform: translate(0%, -2.85%); }
   30% { transform: translate(6.71%, -4.62%); }
   40% { transform: translate(6.71%, -5.71%); }
   50% { transform: translate(0%, -5.71%); }
   60% { transform: translate(-4.15%, -4.62%); }
   70% { transform: translate(0%, -2.85%); }
   80% { transform: translate(6.71%, -1.09%); }
   90% { transform: translate(6.71%, 0%); }
  100% { transform: translate(0%, 0%); }
}
@keyframes netPath3c {
    0% { transform: translate(0%, 0%); }
   10% { transform: translate(6.15%, -1.36%); }
   20% { transform: translate(9.95%, -1.36%); }
   30% { transform: translate(6.15%, 0%); }
   40% { transform: translate(0%, 2.20%); }
   50% { transform: translate(0%, 4.41%); }
   60% { transform: translate(6.15%, 5.77%); }
   70% { transform: translate(9.95%, 5.77%); }
   80% { transform: translate(6.15%, 4.41%); }
   90% { transform: translate(0%, 2.20%); }
  100% { transform: translate(0%, 0%); }
}
@keyframes netPath3d {
    0% { transform: translate(0%, 0%); }
   10% { transform: translate(0%, 1.76%); }
   20% { transform: translate(-6.71%, 3.53%); }
   30% { transform: translate(-10.85%, 4.62%); }
   40% { transform: translate(-6.71%, 4.62%); }
   50% { transform: translate(0%, 3.53%); }
   60% { transform: translate(0%, 1.76%); }
   70% { transform: translate(-6.71%, 0%); }
   80% { transform: translate(-10.85%, -1.09%); }
   90% { transform: translate(-6.71%, -1.09%); }
  100% { transform: translate(0%, 0%); }
}
@keyframes netPath3e {
    0% { transform: translate(0%, 0%); }
   10% { transform: translate(-5.59%, 0%); }
   20% { transform: translate(-5.59%, -1.23%); }
   30% { transform: translate(0%, -3.21%); }
   40% { transform: translate(3.45%, -5.19%); }
   50% { transform: translate(0%, -6.42%); }
   60% { transform: translate(-5.59%, -6.42%); }
   70% { transform: translate(-5.59%, -5.19%); }
   80% { transform: translate(0%, -3.21%); }
   90% { transform: translate(3.45%, -1.23%); }
  100% { transform: translate(0%, 0%); }
}

/* Дорожки связей: длина и угол до второго узла на тех же стопах. */
@keyframes netTrack11 {
    0% { transform: rotate(14.38deg) scaleX(1.0000); }
   10% { transform: rotate(11.08deg) scaleX(0.9502); }
   20% { transform: rotate(6.56deg) scaleX(0.9480); }
   30% { transform: rotate(3.08deg) scaleX(0.9945); }
   40% { transform: rotate(2.11deg) scaleX(1.0674); }
   50% { transform: rotate(3.59deg) scaleX(1.1369); }
   60% { transform: rotate(6.67deg) scaleX(1.1788); }
   70% { transform: rotate(10.31deg) scaleX(1.1806); }
   80% { transform: rotate(13.48deg) scaleX(1.1417); }
   90% { transform: rotate(15.13deg) scaleX(1.0738); }
  100% { transform: rotate(14.38deg) scaleX(1.0000); }
}
@keyframes netTrack12 {
    0% { transform: rotate(125.75deg) scaleX(1.0000); }
   10% { transform: rotate(127.29deg) scaleX(0.8941); }
   20% { transform: rotate(124.37deg) scaleX(0.7940); }
   30% { transform: rotate(117.06deg) scaleX(0.7468); }
   40% { transform: rotate(109.28deg) scaleX(0.7804); }
   50% { transform: rotate(105.48deg) scaleX(0.8744); }
   60% { transform: rotate(106.32deg) scaleX(0.9825); }
   70% { transform: rotate(110.30deg) scaleX(1.0650); }
   80% { transform: rotate(115.78deg) scaleX(1.0986); }
   90% { transform: rotate(121.39deg) scaleX(1.0750); }
  100% { transform: rotate(125.75deg) scaleX(1.0000); }
}
@keyframes netTrack13 {
    0% { transform: rotate(125.66deg) scaleX(1.0000); }
   10% { transform: rotate(118.65deg) scaleX(0.9898); }
   20% { transform: rotate(113.25deg) scaleX(1.0640); }
   30% { transform: rotate(111.83deg) scaleX(1.1828); }
   40% { transform: rotate(113.99deg) scaleX(1.2955); }
   50% { transform: rotate(118.30deg) scaleX(1.3651); }
   60% { transform: rotate(123.40deg) scaleX(1.3725); }
   70% { transform: rotate(128.00deg) scaleX(1.3158); }
   80% { transform: rotate(130.77deg) scaleX(1.2101); }
   90% { transform: rotate(130.26deg) scaleX(1.0886); }
  100% { transform: rotate(125.66deg) scaleX(1.0000); }
}
@keyframes netTrack14 {
    0% { transform: rotate(38.99deg) scaleX(1.0000); }
   10% { transform: rotate(41.85deg) scaleX(1.2050); }
   20% { transform: rotate(48.71deg) scaleX(1.3522); }
   30% { transform: rotate(57.20deg) scaleX(1.4092); }
   40% { transform: rotate(65.78deg) scaleX(1.3652); }
   50% { transform: rotate(72.96deg) scaleX(1.2283); }
   60% { transform: rotate(76.53deg) scaleX(1.0280); }
   70% { transform: rotate(72.73deg) scaleX(0.8248); }
   80% { transform: rotate(58.88deg) scaleX(0.7232); }
   90% { transform: rotate(44.06deg) scaleX(0.8032); }
  100% { transform: rotate(38.99deg) scaleX(1.0000); }
}
@keyframes netTrack15 {
    0% { transform: rotate(59.04deg) scaleX(1.0000); }
   10% { transform: rotate(54.53deg) scaleX(0.8805); }
   20% { transform: rotate(45.52deg) scaleX(0.8430); }
   30% { transform: rotate(37.49deg) scaleX(0.9114); }
   40% { transform: rotate(34.74deg) scaleX(1.0438); }
   50% { transform: rotate(36.83deg) scaleX(1.1783); }
   60% { transform: rotate(41.81deg) scaleX(1.2700); }
   70% { transform: rotate(47.99deg) scaleX(1.2953); }
   80% { transform: rotate(53.95deg) scaleX(1.2481); }
   90% { transform: rotate(58.24deg) scaleX(1.1397); }
  100% { transform: rotate(59.04deg) scaleX(1.0000); }
}
@keyframes netTrack21 {
    0% { transform: rotate(35.06deg) scaleX(1.0000); }
   10% { transform: rotate(24.87deg) scaleX(0.9000); }
   20% { transform: rotate(22.42deg) scaleX(0.9821); }
   30% { transform: rotate(28.24deg) scaleX(1.2007); }
   40% { transform: rotate(29.77deg) scaleX(1.3927); }
   50% { transform: rotate(23.78deg) scaleX(1.4245); }
   60% { transform: rotate(16.17deg) scaleX(1.3594); }
   70% { transform: rotate(17.15deg) scaleX(1.2708); }
   80% { transform: rotate(28.09deg) scaleX(1.2065); }
   90% { transform: rotate(37.14deg) scaleX(1.1456); }
  100% { transform: rotate(35.06deg) scaleX(1.0000); }
}
@keyframes netTrack22 {
    0% { transform: rotate(151.39deg) scaleX(1.0000); }
   10% { transform: rotate(140.40deg) scaleX(1.0360); }
   20% { transform: rotate(136.21deg) scaleX(1.1110); }
   30% { transform: rotate(143.61deg) scaleX(1.1030); }
   40% { transform: rotate(155.08deg) scaleX(1.1279); }
   50% { transform: rotate(157.49deg) scaleX(1.2508); }
   60% { transform: rotate(151.87deg) scaleX(1.4006); }
   70% { transform: rotate(148.02deg) scaleX(1.4517); }
   80% { transform: rotate(150.26deg) scaleX(1.3193); }
   90% { transform: rotate(154.81deg) scaleX(1.1167); }
  100% { transform: rotate(151.39deg) scaleX(1.0000); }
}
@keyframes netTrack23 {
    0% { transform: rotate(24.88deg) scaleX(1.0000); }
   10% { transform: rotate(22.35deg) scaleX(0.8432); }
   20% { transform: rotate(13.22deg) scaleX(0.7190); }
   30% { transform: rotate(13.53deg) scaleX(0.7180); }
   40% { transform: rotate(22.84deg) scaleX(0.8408); }
   50% { transform: rotate(25.03deg) scaleX(0.9945); }
   60% { transform: rotate(17.32deg) scaleX(1.0773); }
   70% { transform: rotate(8.44deg) scaleX(1.1205); }
   80% { transform: rotate(8.61deg) scaleX(1.1229); }
   90% { transform: rotate(17.53deg) scaleX(1.0837); }
  100% { transform: rotate(24.88deg) scaleX(1.0000); }
}
@keyframes netTrack24 {
    0% { transform: rotate(137.29deg) scaleX(1.0000); }
   10% { transform: rotate(129.94deg) scaleX(0.8713); }
   20% { transform: rotate(113.16deg) scaleX(0.9846); }
   30% { transform: rotate(104.98deg) scaleX(1.0995); }
   40% { transform: rotate(107.44deg) scaleX(0.9663); }
   50% { transform: rotate(120.61deg) scaleX(0.7881); }
   60% { transform: rotate(130.81deg) scaleX(0.8826); }
   70% { transform: rotate(129.60deg) scaleX(1.1749); }
   80% { transform: rotate(128.73deg) scaleX(1.3616); }
   90% { transform: rotate(132.56deg) scaleX(1.2516); }
  100% { transform: rotate(137.29deg) scaleX(1.0000); }
}
@keyframes netTrack25 {
    0% { transform: rotate(87.47deg) scaleX(1.0000); }
   10% { transform: rotate(90.54deg) scaleX(1.0876); }
   20% { transform: rotate(95.74deg) scaleX(1.0381); }
   30% { transform: rotate(102.43deg) scaleX(0.9324); }
   40% { transform: rotate(106.52deg) scaleX(0.9280); }
   50% { transform: rotate(105.09deg) scaleX(1.0347); }
   60% { transform: rotate(101.19deg) scaleX(1.1086); }
   70% { transform: rotate(96.71deg) scaleX(1.0401); }
   80% { transform: rotate(91.55deg) scaleX(0.9109); }
   90% { transform: rotate(87.52deg) scaleX(0.8905); }
  100% { transform: rotate(87.47deg) scaleX(1.0000); }
}
@keyframes netTrack31 {
    0% { transform: rotate(138.37deg) scaleX(1.0000); }
   10% { transform: rotate(143.36deg) scaleX(0.9422); }
   20% { transform: rotate(137.18deg) scaleX(0.6811); }
   30% { transform: rotate(129.42deg) scaleX(0.5235); }
   40% { transform: rotate(139.90deg) scaleX(0.6348); }
   50% { transform: rotate(147.57deg) scaleX(0.8855); }
   60% { transform: rotate(142.65deg) scaleX(0.9510); }
   70% { transform: rotate(126.45deg) scaleX(0.8407); }
   80% { transform: rotate(114.34deg) scaleX(0.8065); }
   90% { transform: rotate(123.62deg) scaleX(0.8770); }
  100% { transform: rotate(138.37deg) scaleX(1.0000); }
}
@keyframes netTrack32 {
    0% { transform: rotate(38.66deg) scaleX(1.0000); }
   10% { transform: rotate(26.06deg) scaleX(1.1576); }
   20% { transform: rotate(19.74deg) scaleX(1.3799); }
   30% { transform: rotate(23.16deg) scaleX(1.3051); }
   40% { transform: rotate(35.71deg) scaleX(1.0835); }
   50% { transform: rotate(44.88deg) scaleX(1.1021); }
   60% { transform: rotate(40.68deg) scaleX(1.3712); }
   70% { transform: rotate(35.79deg) scaleX(1.6012); }
   80% { transform: rotate(36.54deg) scaleX(1.4934); }
   90% { transform: rotate(41.19deg) scaleX(1.1692); }
  100% { transform: rotate(38.66deg) scaleX(1.0000); }
}
@keyframes netTrack33 {
    0% { transform: rotate(70.71deg) scaleX(1.0000); }
   10% { transform: rotate(67.50deg) scaleX(1.1188); }
   20% { transform: rotate(81.45deg) scaleX(1.1574); }
   30% { transform: rotate(93.90deg) scaleX(1.2372); }
   40% { transform: rotate(89.38deg) scaleX(1.2687); }
   50% { transform: rotate(75.02deg) scaleX(1.2778); }
   60% { transform: rotate(69.49deg) scaleX(1.2220); }
   70% { transform: rotate(80.55deg) scaleX(1.0478); }
   80% { transform: rotate(95.09deg) scaleX(0.9476); }
   90% { transform: rotate(89.13deg) scaleX(0.9097); }
  100% { transform: rotate(70.71deg) scaleX(1.0000); }
}
@keyframes netTrack34 {
    0% { transform: rotate(106.26deg) scaleX(1.0000); }
   10% { transform: rotate(117.32deg) scaleX(1.1215); }
   20% { transform: rotate(121.51deg) scaleX(1.1303); }
   30% { transform: rotate(114.74deg) scaleX(0.9628); }
   40% { transform: rotate(105.46deg) scaleX(0.7913); }
   50% { transform: rotate(112.64deg) scaleX(0.7273); }
   60% { transform: rotate(129.03deg) scaleX(0.8174); }
   70% { transform: rotate(131.51deg) scaleX(0.8915); }
   80% { transform: rotate(118.03deg) scaleX(0.8574); }
   90% { transform: rotate(103.65deg) scaleX(0.8938); }
  100% { transform: rotate(106.26deg) scaleX(1.0000); }
}
@keyframes netTrack35 {
    0% { transform: rotate(9.46deg) scaleX(1.0000); }
   10% { transform: rotate(6.44deg) scaleX(0.8193); }
   20% { transform: rotate(-1.74deg) scaleX(1.0213); }
   30% { transform: rotate(-6.79deg) scaleX(1.3303); }
   40% { transform: rotate(-10.41deg) scaleX(1.3214); }
   50% { transform: rotate(-13.92deg) scaleX(1.0162); }
   60% { transform: rotate(-11.92deg) scaleX(0.8320); }
   70% { transform: rotate(-2.75deg) scaleX(1.0220); }
   80% { transform: rotate(3.35deg) scaleX(1.3232); }
   90% { transform: rotate(6.97deg) scaleX(1.3093); }
  100% { transform: rotate(9.46deg) scaleX(1.0000); }
}

@keyframes netLink {
  0%, 100% { opacity: 0.08; }
  20%      { opacity: 0.5; }
  45%      { opacity: 0.5; }
  65%      { opacity: 0.08; }
}

@keyframes netFadeOut {
  from { opacity: 1; }
  to   { opacity: 0; }
}

/* Во всех блоках ниже animation/transform снимаются через !important, и это
   не перестраховка. Базовые правила адресуют узел как
   .net-pattern-1 .net-node:nth-child(1) — специфичность (0,3,0), — а
   отменяющее правило по одному классу даёт (0,1,0). Медиазапрос специфичности
   не добавляет, поэтому без !important движение продолжалось бы и при
   prefers-reduced-motion, и на узком экране. Поймано форсированным рендером:
   узлы стояли в смещённой позиции при якобы выключенной анимации. */

/* Кадр пришёл: узор уходит за те же 260 мс, что и проявляется .gen-reveal, —
   получается кроссфейд, а не подмена рывком. Сами узлы и связи при этом
   останавливаются: держать полторы сотни бесконечных анимаций под непрозрачной
   фотографией незачем. */
.gen-tile--filled .gen-net { animation: netFadeOut 260ms ease-out both; }
.gen-tile--filled .net-node,
.gen-tile--filled .net-link,
.gen-tile--filled .net-link::before { animation: none !important; }

/* Кадр не получился. Узор замирает и глушится вместо того, чтобы обещать
   результат бесконечно; словами об этом говорит #resultNote над сеткой
   («Готово N из 10: остальные кадры не получились»), а на самой плитке текста
   нет — десять подписей в мелких ячейках были бы шумом. Для программы чтения
   с экрана в плитку кладётся скрытая строка. */
.gen-tile--failed .gen-net { opacity: 0.3; }
.gen-tile--failed .net-node,
.gen-tile--failed .net-link,
.gen-tile--failed .net-link::before { animation: none !important; }

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Узкий экран: 15 анимируемых элементов на плитку — это 150 на сетку, и для
   слабого телефона многовато. Узор прореживается до трёх связей, а движение по
   траектории снимается целиком: остаётся статичная фигура с мерцающими связями.
   Дрожание на месте сюда не возвращается намеренно — связи за ним не следуют, и
   их концы отходили бы от точек. Анимированных элементов остаётся три на плитку. */
@media (max-width: 480px) {
  .net-node:nth-child(5),
  .net-link:nth-child(9),
  .net-link:nth-child(10) { display: none; }
  .net-node,
  .net-link::before { animation: none !important; }
  .net-node { transform: none !important; }
  /* transform у .net-link::before НЕ сбрасывается: там базовый поворот линии. */
  .net-link { animation: netLink 4.2s ease-in-out infinite !important; animation-delay: calc(var(--tile-phase, 0s) + var(--link-phase, 0s)) !important; transform: none !important; }
}

@media (prefers-reduced-motion: reduce) {
  .gen-net,
  .net-node,
  .net-link,
  .net-link::before { animation: none !important; }
  /* transform обнуляется у слоёв, но НЕ у .net-link::before: там лежит базовый
     поворот линии, и сброс превратил бы весь узор в горизонтальные чёрточки. */
  .net-node { transform: none !important; }
  .net-link { transform: none !important; opacity: 0.28; }
  /* Кроссфейд выключен — значит узор надо убрать совсем, иначе он остался бы
     поверх фотографии. */
  .gen-tile--filled .gen-net { display: none; }
}

/* Восстановление demo-сессии после перезагрузки (app.js: restoreDemoSession). */
.demo-restore-panel { margin: 20px 0; }
.demo-restore-title { font-size: 18px; font-weight: 600; margin-bottom: 8px; }
.demo-restore-text { color: #7a7a85; font-size: 14px; margin-bottom: 16px; }

.modal { position: fixed; inset: 0; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; }
.modal.hidden { display: none; }
.modal-content { background: #17171f; padding: 32px; border-radius: 16px; max-width: 400px; width: 90%; text-align: center; position: relative; }
/* Без кнопки закрытия модалка, открытая из шапки, запирала бы страницу:
   .modal перекрывает весь экран, а закрыть его было нечем. */
.modal-close { position: absolute; top: 10px; right: 14px; background: none; border: none; color: #a0a0ab; font-size: 26px; line-height: 1; cursor: pointer; }
.modal-content h3 { margin-bottom: 12px; }
.modal-content p { color: #a0a0ab; margin-bottom: 20px; font-size: 14px; }
.input-field { width: 100%; padding: 14px; border-radius: 10px; border: 1px solid #3a3a45; background: #0f0f14; color: #fff; margin-bottom: 12px; font-size: 16px; }

/* Шаг подтверждения: появляется только после того, как файл выбран локально.
   Атрибут hidden обязан побеждать display у самого блока, иначе флекс его покажет. */
.confirm-step { display: flex; flex-direction: column; align-items: center; gap: 12px; margin-top: 20px; }
.confirm-step[hidden] { display: none; }

.portrait-preview { max-width: 200px; max-height: 200px; border-radius: 12px; object-fit: cover; border: 1px solid #2c2c38; }
.btn-link { background: none; border: none; color: #a855f7; font-size: 14px; cursor: pointer; padding: 0; text-decoration: underline; }

/* Согласие. Текст справа от флажка не растягивается на всю ширину упаковки:
   читать длинную строку через весь экран неудобно. */
.consent-check { display: flex; gap: 10px; text-align: left; max-width: 460px; margin: 0 auto; color: #a0a0ab; font-size: 14px; line-height: 1.5; cursor: pointer; }
.consent-check input { margin-top: 3px; flex-shrink: 0; width: 18px; height: 18px; accent-color: #a855f7; cursor: pointer; }
.consent-check a { color: #a855f7; }
.consent-check--required { color: #a0a0ab; }
.btn-primary:disabled { opacity: 0.45; cursor: not-allowed; }

.site-footer { text-align: center; padding: 40px 20px; color: #7a7a85; font-size: 13px; border-top: 1px solid #232330; margin-top: 60px; }
.site-footer a { color: #7a7a85; }

/* Признак тестового режима владельца. Обычный посетитель этой строки не видит:
   элемент остаётся hidden, если в sessionStorage вкладки нет пропуска. */
.beta-banner { max-width: 460px; margin: 0 auto 16px; padding: 8px 14px; border-radius: 10px;
               background: #1a1726; border: 1px solid #3a2f55; color: #a855f7; font-size: 13px; }
