/* ============================================================
   VictoAI · BASE
   Reset, типографика, утилиты, ключевые анимации.
   ============================================================ */
*,*::before,*::after{ box-sizing: border-box; }
html,body{ margin:0; padding:0; }
html{
  scrollbar-color: var(--border) var(--bg);
  -webkit-text-size-adjust: 100%;
}
body{
  font-family: var(--font-sans);
  font-feature-settings: "ss01","cv11";
  background: var(--bg);
  color: var(--text);
  font-size: var(--fs-base);
  line-height: var(--lh-base);
  letter-spacing: -0.005em;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}
img,svg,video{ display:block; max-width:100%; }
a{ color: inherit; text-decoration: none; }
button{ font-family: inherit; }

/* Убираем ручку растягивания у всех textarea (ломает вёрстку при перетаскивании).
   Где текст не влезает — прокрутка остаётся, но скроллбар невидимый. */
textarea{ resize: none !important; scrollbar-width: none; -ms-overflow-style: none; }
textarea::-webkit-scrollbar{ width: 0; height: 0; display: none; }
::selection{ background: var(--accent-soft); color: var(--text); }

:focus-visible{
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

/* ---- Типографические утилиты ---- */
.mono{ font-family: var(--font-mono); font-feature-settings:"ss01"; }
.serif{ font-family: var(--font-serif); font-style: italic; font-weight: 400; letter-spacing: -0.02em; }
.muted{ color: var(--text-muted); }
.faint{ color: var(--text-faint); }
.accent-word{ font-family: var(--font-serif); font-style: italic; font-weight: 400; color: var(--accent); }
.gradient-text{
  background: var(--grad-warm);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
}

h1,h2,h3,h4{ letter-spacing: -0.025em; font-weight: 600; margin: 0; }

/* ---- Контейнер ---- */
.wrap{ width: min(var(--container), 100% - 2 * var(--sp-5)); margin-inline: auto; }
.wrap-narrow{ width: min(var(--container-narrow), 100% - 2 * var(--sp-5)); margin-inline: auto; }

/* ---- Эйконичная подложка-плейсхолдер для изображений ---- */
.imgph{
  position: relative;
  background:
    repeating-linear-gradient(135deg,
      color-mix(in oklab, var(--text) 5%, transparent) 0 1px,
      transparent 1px 9px),
    var(--bg-card-2);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  display:flex; align-items:center; justify-content:center;
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: 11px;
  overflow: hidden;
}
.imgph::after{
  content: attr(data-label);
  white-space: pre; text-align:center;
  padding: 4px 9px;
  background: color-mix(in oklab, var(--bg) 70%, transparent);
  border-radius: var(--r-xs);
  border: 1px solid var(--border);
}

/* ============================================================
   КЛЮЧЕВЫЕ АНИМАЦИИ (только transform / opacity)
   ============================================================ */
@keyframes va-fade-up{
  from{ opacity:0; transform: translateY(14px); }
  to  { opacity:1; transform: none; }
}
@keyframes va-fade{
  from{ opacity:0; } to{ opacity:1; }
}
@keyframes va-scale-in{
  from{ opacity:0; transform: scale(0.96); }
  to  { opacity:1; transform: scale(1); }
}
@keyframes va-msg-in{
  from{ opacity:0; transform: translateY(8px) scale(0.99); }
  to  { opacity:1; transform: none; }
}
@keyframes va-shimmer{
  from{ background-position: 200% 0; }
  to  { background-position: -200% 0; }
}
@keyframes va-spin{ to{ transform: rotate(360deg); } }
@keyframes va-pulse-ring{
  0%  { box-shadow: 0 0 0 0 var(--accent-soft); }
  70% { box-shadow: 0 0 0 8px transparent; }
  100%{ box-shadow: 0 0 0 0 transparent; }
}
@keyframes va-float{
  0%,100%{ transform: translateY(0); }
  50%    { transform: translateY(-8px); }
}
@keyframes va-bar{
  from{ transform: scaleX(0); } to{ transform: scaleX(1); }
}

/* ---- Reveal-on-scroll ----
   ВАЖНО: скрытое состояние применяется ТОЛЬКО когда на <html> есть класс
   .reveal-on (его ставит inline-скрипт в <head>, если IntersectionObserver
   поддержан и нет prefers-reduced-motion). Поэтому при любом сбое (нет JS,
   reduced-motion, окружение без анимаций) контент виден сразу и не зависает.
   Транзишн объявлен только внутри .reveal-on — снятие класса = мгновенная
   видимость без застрявшего перехода. */
.reveal-on .reveal{
  opacity: 0;
  transform: translateY(18px);
  transition:
    opacity var(--dur-4) var(--ease-out),
    transform var(--dur-4) var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}
.reveal-on .reveal.is-in{ opacity: 1; transform: none; }

/* Утилиты задержки для поэтапного reveal */
.d-1{ --reveal-delay: 60ms; }
.d-2{ --reveal-delay: 120ms; }
.d-3{ --reveal-delay: 180ms; }
.d-4{ --reveal-delay: 240ms; }
.d-5{ --reveal-delay: 300ms; }
.d-6{ --reveal-delay: 360ms; }

/* ---- Доступность: уважение к reduced-motion ---- */
@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .reveal{ opacity: 1 !important; transform: none !important; }
}
Важно по reveal-анимациям: скрытое состояние работает только если в <head> страницы стоит инлайн-скрипт, добавляющий класс reveal-on на <html>. Без него .reveal всегда видим (это защита от пустых экранов). Скрипт такой:

<script>
(function(){try{if('IntersectionObserver'in window&&!matchMedia('(prefers-reduced-motion: reduce)').matches){document.documentElement.className+=' reveal-on';}}catch(e){}})();
</script>
