/* ==========================================================================
   Слой приёмов (02.09.2026). Снимается вместе с fx.js двумя строками в конце
   index.html — страница остаётся полностью рабочей и читаемой.

   Порядок здесь такой же, как в fx.js. Всё, что ПРЯЧЕТ содержимое, работает
   только из-под .js-fx (класс ставит сам fx.js): не поднялся скрипт — ничего
   не спрятано. Правило куплено кровью 31.08 на другом слое.
   ========================================================================== */

/* ---- прогресс чтения ----------------------------------------------------
   Родной путь — scroll-timeline, без единой строки JS. Где его нет, полосу
   двигает запасной обработчик из fx.js (класс .prog--js).                  */
.prog{
  position:fixed; top:0; left:0; right:0; height:3px; z-index:60;
  background:linear-gradient(90deg, var(--ac-deep), var(--ac), var(--ac-hot));
  box-shadow:0 0 10px color-mix(in oklab, var(--ac) 45%, transparent);
  transform:scaleX(0); transform-origin:0 50%; pointer-events:none;
}
@supports (animation-timeline: scroll()){
  @media (prefers-reduced-motion:no-preference){
    .prog{ animation:prog linear both; animation-timeline:scroll(root block) }
    @keyframes prog{ to{ transform:scaleX(1) } }
  }
}
.prog--js{ will-change:transform }

@media (prefers-reduced-motion:no-preference){

  /* ---- заголовок секции проявляется маской, а не прозрачностью ----------
     clip-path режет по горизонтали — текст «выезжает» из-под края.
     Только из-под .js-fx: без скрипта заголовки просто видны.            */
  @supports (animation-timeline: view()){
    .js-fx .act .h{
      animation:hmask linear both;
      animation-timeline:view();
      animation-range:entry 40% entry 85%;
    }
    @keyframes hmask{
      from{ clip-path:inset(0 100% 0 0) }
      to{ clip-path:inset(0 -3% 0 0) }
    }
  }

  /* ---- свет за курсором на карточке ------------------------------------
     Координаты пишет fx.js в --mx/--my, рисует CSS. Слой не ловит клики. */
  .w::after,.do::after{
    content:''; position:absolute; inset:0; pointer-events:none;
    border-radius:inherit; opacity:0;
    transition:opacity .3s var(--ease);
    background:radial-gradient(240px circle at var(--mx,50%) var(--my,50%),
      color-mix(in oklab, var(--ac) 13%, transparent), transparent 62%);
  }
  .w.is-lit::after,.do.is-lit::after{ opacity:1 }

  /* ---- магнитные кнопки -------------------------------------------------
     Возврат длиннее притяжения и с перелётом — так рука чувствует пружину. */
  .hero__cta .btn,.end .btn,.end__mail{
    transition:transform .38s cubic-bezier(.22,1.2,.36,1),
               border-color .2s var(--ease), color .2s var(--ease),
               background-color .2s var(--ease);
  }
  .hero__cta .btn.is-mag,.end .btn.is-mag,.end__mail.is-mag{
    transition-duration:.13s; position:relative; z-index:2;
  }

  /* ---- параллакс скриншота в карточке ----------------------------------- */
  .w.is-lit .w__shot img,.w.is-lit .w__v{ transition-duration:.2s }

  /* ---- волна по тегам стека ---------------------------------------------
     Конечное состояние совпадает с обычным видом: не сработает — не заметно. */
  .tags .tag{ display:inline-block }
  .tags__d{ font-style:normal; opacity:.5; margin:0 .35em }
  .js-fx .tags.is-wave .tag{
    animation:tagwave .7s var(--ease) both;
    animation-delay:calc(var(--i,0) * 70ms);
  }
  @keyframes tagwave{
    0%{   color:var(--fg-3); transform:translateY(3px) }
    45%{  color:var(--ac-hot); transform:translateY(-1px) }
    100%{ color:var(--ac-dim); transform:none }
  }
}

/* ==========================================================================
   Приёмы 6–10 (02.09.2026, вторая партия).
   ========================================================================== */

/* ---- 6. Рельс-оглавление ------------------------------------------------ */
.rail{
  position:fixed; right:14px; top:50%; transform:translateY(-50%); z-index:55;
  display:grid; gap:2px; padding:10px 6px;
}
.rail a{
  display:grid; grid-template-columns:auto auto; align-items:center; gap:8px;
  justify-content:end; text-decoration:none; color:var(--fg-3); font-size:11.5px;
  letter-spacing:.06em; text-transform:uppercase;
  min-height:40px; padding:0 6px;   /* цель нажатия ≥40 px, точка остаётся мелкой */
}
.rail i{
  grid-column:2; width:7px; height:7px; border-radius:50%;
  background:transparent; border:1px solid var(--fg-3);
  transition:background-color .25s var(--ease), border-color .25s var(--ease),
             box-shadow .25s var(--ease), transform .25s var(--ease);
}
.rail b{ display:none }
.rail span{
  grid-column:1; grid-row:1; opacity:0; transform:translateX(6px);
  transition:opacity .25s var(--ease), transform .25s var(--ease);
  white-space:nowrap;
}
.rail a.is-on i{
  background:var(--ac); border-color:var(--ac); transform:scale(1.25);
  box-shadow:0 0 0 4px color-mix(in oklab, var(--ac) 14%, transparent);
}
.rail a.is-on span,.rail:hover a span{ opacity:1; transform:none }
.rail a.is-on span{ color:var(--fg-2) }
@media (max-width:1199px){ .rail{ display:none } }
@media (prefers-reduced-motion:reduce){ .rail i,.rail span{ transition:none } }

/* ---- 9. Кнопка «наверх» -------------------------------------------------
   Появляется не по таймеру и не по обработчику, а по позиции прокрутки. */
.up{
  position:fixed; right:20px; bottom:22px; z-index:56;
  width:44px; height:44px; border-radius:50%;
  border:1px solid var(--line); background:color-mix(in oklab, var(--card) 88%, transparent);
  color:var(--ac); font-size:17px; line-height:1; cursor:pointer;
  backdrop-filter:blur(6px);
  /* без scale: он ужимал цель нажатия до 38 px и ронял проверку ≥40 */
  opacity:0; transform:translateY(10px); pointer-events:none;
  transition:opacity .3s var(--ease), transform .3s var(--ease),
             border-color .2s var(--ease), color .2s var(--ease);
}
.up:hover{ border-color:var(--ac); color:var(--ac-hot) }
@supports (animation-timeline: scroll()){
  @media (prefers-reduced-motion:no-preference){
    .up{ animation:upshow linear both; animation-timeline:scroll(root block);
         animation-range:22% 30% }
    /* оба кадра ЯВНО: без from обратная заливка (both) не знает, откуда
       начинать, и кнопка висит видимой ещё до входа в диапазон */
    @keyframes upshow{
      from{ opacity:0; transform:translateY(10px) }
      to{   opacity:1; transform:none }
    }
  }
}
/* запасной путь там, где scroll-timeline нет: класс ставит fx.js */
.up.is-up{ pointer-events:auto }
/* запасной путь там, где scroll-timeline нет: класс ставит fx.js */
.prog--js ~ .up.is-up{ opacity:1; transform:none }

@media (prefers-reduced-motion:no-preference){

  /* ---- 7. Наклон карточки услуги ---------------------------------------- */
  .do{ transition:transform .35s var(--ease), border-color .25s var(--ease) }
  .do.is-lit{ transition-duration:.12s }

  /* ---- 8. Проявка скриншота из размытия ---------------------------------
     ВАЖНО: здесь НЕЛЬЗЯ animation-timeline:view(). У карточки .w и у рамки
     .w__shot стоит overflow:hidden, а это по спецификации создаёт собственный
     контейнер прокрутки — view() привязывается к нему, а он не скроллится,
     поэтому прогресс всегда 1 и картинка «проявлена» ещё до появления.
     Поэтому наблюдатель + переход: класс ставит fx.js.
     Только шесть картинок портфолио — фильтр дорогой, шире не пускаем. */
  .js-fx .w__shot img{
    filter:blur(12px) saturate(.55);
    transition:filter .75s var(--ease), transform .5s var(--ease);
  }
  .js-fx .w__shot img.is-shown{ filter:none }

  /* ---- 10. Портрет отзывается на подведённый курсор ---------------------- */
  .ava__v{ transition:transform .45s cubic-bezier(.22,1,.36,1) }
}

/* ==========================================================================
   Оформление (02.09.2026, третья партия).
   ========================================================================== */

/* ---- 11. Зерно -----------------------------------------------------------
   Плёночный шум поверх всей страницы: тёмная тема перестаёт выглядеть
   «пластмассовой», градиенты получают материал. Слой один, кликов не ловит.
   Без mix-blend-mode осознанно: смешивание на весь экран стоит кадров,
   а нужный эффект даёт обычная низкая прозрачность.                        */
.grain{
  position:fixed; inset:0; z-index:1; pointer-events:none;
  opacity:.045;
  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='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size:160px 160px;
}

/* ---- 12. Номер акта на фоне ----------------------------------------------
   Цифра главы в пол-экрана, еле проступающая за содержимым. Даёт странице
   журнальную структуру: видно, что это главы, а не бесконечная лента.      */
.ghost{
  position:absolute; z-index:0; pointer-events:none; user-select:none;
  /* Цифра стоит В ПОЛОСЕ ЗАГОЛОВКА: у h2 max-width 18ch, справа всегда воздух.
     Раньше висела на 8 % высоты секции — а там уже идут карточки, и номер
     уходил под них (замер: до 78 % площади под содержимым). */
  right:2vw; top:calc(var(--s8) - 18px);
  --par:60px;                       /* размах параллакса, см. ghostpar */
  font-family:'Unbounded',sans-serif; font-weight:900; font-style:normal;
  font-size:clamp(84px,11vw,190px); line-height:.78;
  color:color-mix(in oklab, var(--fg) 5%, transparent);
  letter-spacing:-.04em;
}
/* Содержимое выше фоновой цифры. ВНИМАНИЕ: .zero здесь БЕЗ position —
   у него position:sticky, и перебивать его на relative нельзя: липкая сцена
   «42 934 → 0» сразу срывается (поймано пробником, top уехал в −696). */
.act > .wrap,.act > .rows{ position:relative; z-index:1 }
.act > .zero{ z-index:1 }
/* right строго 0: отрицательный вынос давал 4 px переполнения и горизонтальную
   прокрутку на телефоне. Прятать overflow-x нельзя — он ломает sticky-сцену. */
/* Телефон: справа от заголовка места нет вовсе, поэтому цифра уходит ВВЕРХ,
   в собственный отступ секции (padding-block 72px) — над надзаголовком.
   Размах параллакса срезан: с 60 px цифра въезжала обратно в текст. */
@media (max-width:760px){
  /* top держим БОЛЬШЕ размаха параллакса: при top 10 и размахе 18 цифра
     уезжала на 6 px выше секции и обрезалась верхом экрана (замер). */
  .ghost{ right:0; top:22px; font-size:13vw; opacity:.85; --par:12px }
}

@supports (animation-timeline: view()){
  @media (prefers-reduced-motion:no-preference){
    /* плывёт медленнее содержимого — глубина без единой строки JS */
    .ghost{
      animation:ghostpar linear both;
      animation-timeline:view();
      animation-range:cover 0% cover 100%;
    }
    @keyframes ghostpar{
      from{ transform:translateY(var(--par)) }
      to{   transform:translateY(calc(var(--par) * -1)) }
    }
  }
}

/* ---- 13. Барабан цифр ----------------------------------------------------
   Число не досчитывается плавно, а прокручивается колонками, как на
   механическом счётчике: тот же факт читается как показание прибора.       */
.rl{
  display:inline-block; height:1em; overflow:hidden; line-height:1;
  vertical-align:baseline;
}
.rl > b{
  display:block; font-weight:inherit; will-change:transform;
  transform:translateY(0);
  transition:transform 1.5s cubic-bezier(.16,.84,.24,1);
}
.rl > b > i{ display:block; height:1em; font-style:normal }
.rl--sp{ display:inline-block; width:.28em }
/* Ячейка равна самой широкой цифре ленты (это и делает счётчик неподвижным),
   но узкие «1» из-за этого стоят разреженно — подтягиваем трекинг. */
.reels{ letter-spacing:-.05em }
/* Лента 0–9 не должна попадать ни в копирование, ни в поиск по странице:
   рядом лежит настоящее число, скрытое визуально, но живое для читалки. */
.rl{ user-select:none }
.reels .vh{
  position:absolute; width:1px; height:1px; overflow:hidden;
  clip-path:inset(50%); white-space:nowrap;
}
@media (prefers-reduced-motion:reduce){
  .rl > b{ transition:none }
}

/* ==========================================================================
   Приёмы 14–18 (02.09.2026, четвёртая партия).
   ========================================================================== */

/* ---- 14. Обводка, бегущая по периметру карточки -------------------------
   Угол градиента — анимируемое свойство (@property), поэтому обводка едет
   по кругу без единого кадра JS. Где @property не поддержан, остаётся
   обычная рамка: правило внутри @supports.                                */
@supports (background: conic-gradient(from 0deg, red, blue)) {
  @property --spin{ syntax:'<angle>'; initial-value:0deg; inherits:false }

  @media (hover:hover) and (pointer:fine) and (prefers-reduced-motion:no-preference){
    .js-fx .w{ --spin:0deg }
    .js-fx .w::before{
      content:''; position:absolute; inset:-1px; z-index:0; pointer-events:none;
      border-radius:inherit; padding:1px; opacity:0;
      background:conic-gradient(from var(--spin),
        transparent 0deg, transparent 260deg,
        color-mix(in oklab, var(--ac) 85%, transparent) 320deg,
        var(--ac-hot) 350deg, transparent 360deg);
      -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
      -webkit-mask-composite:xor; mask-composite:exclude;
      transition:opacity .25s var(--ease);
    }
    .js-fx .w.is-lit::before{ opacity:1; animation:spin 2.6s linear infinite }
    /* Карточка оживает САМА, когда доходишь до неё: обводка проходит круг
       один раз и гаснет. Раньше эффект был только под курсором — владелец
       справедливо заметил, что «просто так не работает». */
    .js-fx .w.is-seen::before{ opacity:1; animation:spin 2.2s linear 1 }
    @keyframes spin{ to{ --spin:360deg } }
  }
}

@media (prefers-reduced-motion:no-preference){

  /* ---- 15. Подчёркивание растёт от точки курсора -------------------------
     Линия не просто появляется, а расходится из-под пальца: позицию пишет
     обработчик в --ux, ширину гонит background-size.                      */
  .js-fx .w__l a,.js-fx .more span a,.js-fx .do__link{
    background-image:linear-gradient(var(--ac-hot), var(--ac-hot));
    background-repeat:no-repeat;
    background-position:var(--ux,50%) 100%;
    background-size:0% 1px;
    transition:background-size .32s var(--ease), color .2s var(--ease);
    text-decoration:none;
  }
  .js-fx .w__l a:hover,.js-fx .more span a:hover,.js-fx .do__link:hover{
    background-size:100% 1px;
  }

  /* ---- 16. Шов между актами с бегущим бликом -----------------------------
     Тонкая линия на стыке глав; по ней один раз проходит блик, когда стык
     входит в кадр. Не цикл — цикличное мигание на семи стыках это шум.    */
  /* Блик — ВТОРОЙ СЛОЙ ФОНА, а не потомок с overflow:hidden.
     Первая версия была через ::after внутри overflow:hidden — и попалась в тот
     же капкан, что проявка скриншотов: overflow создаёт контейнер прокрутки,
     view() цепляется к нему, прогресс намертво 1, блик стоит в конце.
     Фон обрезается границами элемента сам по себе, ничего прятать не нужно. */
  .seam{
    height:1px; margin:0; border:0;
    background-image:
      linear-gradient(90deg, transparent,
        color-mix(in oklab, var(--ac) 70%, transparent), transparent),
      linear-gradient(90deg, transparent, var(--line) 18%, var(--line) 82%, transparent);
    background-size:22% 100%, 100% 100%;
    background-repeat:no-repeat, no-repeat;
    background-position:-25% 0, 0 0;
  }
  @supports (animation-timeline: view()){
    .seam{
      animation:sweep linear both;
      animation-timeline:view();
      animation-range:entry 30% cover 45%;
    }
    @keyframes sweep{
      from{ background-position:-25% 0, 0 0 }
      to{   background-position:125% 0, 0 0 }
    }
  }

  /* ---- 17. Кнопка со сдвигом текста -------------------------------------- */
  .btn--go{ position:relative; overflow:hidden }
  .btn--go > span{ display:inline-block; transition:transform .28s var(--ease) }
  .btn--go::after{
    content:'\2192'; position:absolute; right:18px; top:50%;
    transform:translate(10px,-50%); opacity:0;
    transition:transform .28s var(--ease), opacity .28s var(--ease);
  }
  @media (hover:hover) and (pointer:fine){
    .btn--go:hover > span{ transform:translateX(-9px) }
    .btn--go:hover::after{ transform:translate(0,-50%); opacity:1 }
  }

  /* 18. Ореол за курсором убран по слову владельца 02.09 («это жесть»).
     Не возвращать: своя графика поверх системного курсора его раздражает. */
}

/* ---- 21. Клик по портрету: импульс по дождю ------------------------------
   От портрета уходят два кольца, по фоновой сцене шапки идёт ударная волна
   (scenes.js §rain). Кольца лежат в fixed, кликов не ловят; портрет не
   смещается, поэтому раскладка шапки неподвижна.
   Полноэкранной развёртки тут намеренно нет — видео во весь экран читается
   как кадр из фильма, а не как страница резюме.                            */
.ava{ cursor:pointer }
.ava:focus-visible{ outline:2px solid var(--ac); outline-offset:8px; border-radius:50% }
/* Подсветки портрета через filter тут НЕТ намеренно: filter на <video>
   заставляет перерисовывать видеослой каждый кадр, и вместе с кольцами
   это давало 13 тяжёлых кадров из 70. Отклик даёт только масштаб. */

/* два кольца разной плотности: первое тонкое и яркое — это сам фронт,
   второе мягче и шире — послесвечение */
.pulse-ring{
  position:fixed; z-index:70; width:min(52vw,470px); aspect-ratio:1;
  border-radius:50%; pointer-events:none; will-change:transform,opacity;
  /* тень у растущего кольца дорога: слой переrasterизуется на каждом кадре.
     Свечение даёт второе, мягкое кольцо — оно же и дешевле. */
  border:2px solid color-mix(in oklab, var(--ac) 92%, transparent);
}
.pulse-ring--soft{
  border-width:10px;
  border-color:color-mix(in oklab, var(--ac) 26%, transparent);
  filter:blur(3px);
}
@media (prefers-reduced-motion:reduce){
  .pulse-ring{ display:none }
}
