/* Живые герои, слой А (docs/engagement-spec.md, 1.7; js/ui/life.js): дыхание, разовые реакции, эмоция реплики,
   говорящий, хвостик облачка, сон, мягкое «не то» на карточке ответа. Все keyframes движения героев — здесь.
   Подключать ПОСЛЕ styles.css и css/tasks.css: правила той же силы, что у них, должны их перекрывать.
   Разметка (stage.js): .hero (поза: говорящий, эмоция mood-*) > .hero-rr (разовые реакции rr-*) > .hero-img
   (дыхание и такт речи) + у Пыха .hero-eyes (закрытые глаза, видны при .hero.is-asleep).
   Двигаем только transform и opacity, filter не анимируем. Опора — стопы: transform-origin 50% 96% картинки.
   Нарисованная под героем земля движется вместе с ним, поэтому подъём — не больше 3 px, главное — приседание.
   Всё движение висит на .is-live (его ставит life.attach, пока движение разрешено): снят — герои стоят. В покое
   (reduced-motion, «Спокойные герои», ?still) анимаций нет вовсе — animation: none, длительность в 0 не ставим
   (в BV от 0,01 мс герой дрожал). */

/* ---------- разметка и опоры ---------- */
.hero { position: relative; transform-origin: 50% calc(100% - 20px); }   /* опора позы — стопы над подписью */
.hero-rr { position: relative; display: block; isolation: isolate; transform-origin: 50% 96%; }
.hero .hero-img { transform-origin: 50% 96%; }
/* закрытые глаза Пыха: картинка того же холста, что pyh-<стадия>.webp (прозрачна везде, кроме глаз) — ложится
   ровно поверх и движется теми же keyframes, что и картинка (см. «сон») */
.hero-eyes { position: absolute; inset: 0; display: none; background: center / 100% 100% no-repeat; transform-origin: 50% 96%; pointer-events: none; }
.hero.is-asleep .hero-eyes { display: block; }

/* говорящий: неподвижная подложка — бумажное свечение за ним и бумажный овал с чернильным контуром под ногами
   (в покое — единственная отметка, кто говорит) */
.hero-rr::before, .hero-rr::after { content: ''; position: absolute; z-index: -1; border-radius: 50%; opacity: 0; pointer-events: none; }
.hero-rr::before { left: -14%; right: -14%; top: 6%; bottom: 2%;
  background: radial-gradient(closest-side, #fffaf0, rgba(255, 250, 240, .92) 52%, rgba(255, 250, 240, 0)); }
.hero-rr::after { left: 4%; right: 4%; bottom: -2%; height: 15%; background: #fffaf0; border: 2px solid rgba(34, 48, 74, .55); }
.hero.is-talking .hero-rr::before, .hero.is-talking .hero-rr::after { opacity: 1; }
.hero.is-live .hero-rr::before, .hero.is-live .hero-rr::after { transition: opacity .25s ease-out; }
/* Пых нарисован у левого края холста, справа от морды холст прозрачный: круглая подложка просвечивала там белым
   пятном между Пыхом и соседом. У Пыха вместо неё — ореол по силуэту (drop-shadow ставится сменой класса и не
   анимируется; тень земли из styles.css сохранена первой), бумажная земля ::after остаётся */
.hero[data-who="pyh"] .hero-rr::before { display: none; }
/* Ореол тонкий (3 + 1 px): широкий 7 px обводил и нарисованное облачко дыма у носа — справа от морды выходило белое
   пятно (аудит 1108, остаток волны вовлечения). Бумажная земля — по нарисованной земле стадии, а не по всему холсту:
   у малыша и подростка справа на холсте облачко, у яйца и ребёнка — пусто, и овал торчал вправо белым языком
   (края земли сняты с картинок art/heroes/pyh-*.webp: правая граница 0,85–0,98 ширины) */
.hero[data-who="pyh"].is-talking .hero-img { filter: drop-shadow(0 3px 0 rgba(34, 48, 74, .18)) drop-shadow(0 0 3px #fffaf0) drop-shadow(0 0 1px #fffaf0); }
.hero[data-who="pyh"] .hero-rr::after { left: 3%; right: 12%; }
.hero[data-who="pyh"][data-stage="kid"] .hero-rr::after { right: 18%; }
.hero[data-who="pyh"][data-stage="teen"] .hero-rr::after { right: 6%; }

/* ---------- покой: ничего не движется ---------- */
/* не живой герой: ни дыхания, ни такта речи (старое покачивание hop из styles.css — тоже) */
.hero:not(.is-live), .hero:not(.is-live) .hero-rr, .hero:not(.is-live) .hero-img, .hero:not(.is-live) .hero-eyes { animation: none; }
html[data-motion="off"] .hero, html[data-motion="off"] .hero *, html[data-motion="off"] .hero *::before, html[data-motion="off"] .hero *::after,
html[data-still] .hero, html[data-still] .hero *, html[data-still] .hero *::before, html[data-still] .hero *::after { animation: none !important; transition: none !important; }
@media (prefers-reduced-motion: reduce) {
  .hero, .hero *, .hero *::before, .hero *::after { animation: none !important; transition: none !important; }
}
/* вне экрана и на скрытой вкладке — пауза (класс ставит life.js по IntersectionObserver и visibilitychange) */
.hero.is-paused, .hero.is-paused * { animation-play-state: paused !important; }

/* ---------- поза (.hero): говорящий и эмоция реплики ---------- */
/* переменные позы: --life-y сдвиг, --life-r наклон, --life-s «чуть крупнее», --life-sy «осел». Разовые эмоции
   (подскок, вздрогнул) — keyframes без 0 % и 100 %: начинаются и кончаются в текущей позе, поэтому складываются
   с говорящим без рывка */
.hero.is-live { transform: translateY(var(--life-y, 0px)) rotate(var(--life-r, 0deg)) scale(var(--life-s, 1)) scale(1, var(--life-sy, 1));
  transition: transform .32s cubic-bezier(.22, .8, .3, 1); }
.hero.is-live.is-talking { --life-s: 1.04; }
.hero.is-live.mood-sad { --life-y: 1px; --life-sy: .965; }       /* оседает */
.hero.is-live.mood-worried { --life-r: 1.5deg; --life-sy: .975; } /* оседает, чуть отклонился */
.hero.is-live.mood-curious { --life-r: -3deg; }                   /* наклонился посмотреть */
.hero.is-live.mood-happy { animation: life-happy .55s; }          /* маленький подскок */
.hero.is-live.mood-surprised { animation: life-startle .45s; }    /* вздрогнул */
@keyframes life-happy {
  30% { transform: translateY(var(--life-y, 0px)) rotate(var(--life-r, 0deg)) scale(var(--life-s, 1)) scale(1.03, .95); animation-timing-function: cubic-bezier(.22, .8, .3, 1); }
  62% { transform: translateY(calc(var(--life-y, 0px) - 3px)) rotate(var(--life-r, 0deg)) scale(var(--life-s, 1)) scale(.985, 1.025); animation-timing-function: cubic-bezier(.55, 0, .75, .3); }
}
@keyframes life-startle {
  18% { transform: translateY(calc(var(--life-y, 0px) - 2px)) rotate(var(--life-r, 0deg)) scale(var(--life-s, 1)) scale(.97, 1.04); animation-timing-function: cubic-bezier(.22, .8, .3, 1); }
  45% { transform: translateY(var(--life-y, 0px)) rotate(var(--life-r, 0deg)) scale(var(--life-s, 1)) scale(1.02, .975); animation-timing-function: cubic-bezier(.45, 0, .25, 1); }
}

/* ---------- картинка (.hero-img): дыхание, яйцо, такт речи, сон ---------- */
/* период — свой у каждого (--life-period из life.BREATH), задержка — случайная отрицательная (--life-delay) */
.hero.is-live .hero-img { animation: life-breath var(--life-period, 3400ms) cubic-bezier(.45, 0, .55, 1) var(--life-delay, 0ms) infinite; }
/* яйцо не дышит, а покачивается на ±2° от низа */
.hero.is-live .hero-img[src*="pyh-egg."] { animation: life-rock calc(var(--life-period, 3400ms) / 2) cubic-bezier(.45, 0, .55, 1) var(--life-delay, 0ms) infinite alternate; }
/* говорящий: вместо прыжков — медленное покачивание ±1° (от края до края — 0,6 с) */
.hero.is-live.is-talking .hero-img, .hero.is-live.is-talking.is-asleep .hero-eyes { animation: life-talk 1.2s cubic-bezier(.45, 0, .55, 1) infinite; }
/* сон: дышит глубже и реже; глаза-вставка и картинка начинают одни keyframes в одну отрисовку — идут в такт */
.hero.is-live.is-asleep:not(.is-talking) :is(.hero-img, .hero-eyes) { animation: life-sleep calc(var(--life-period, 3400ms) * 1.6) cubic-bezier(.45, 0, .55, 1) infinite; }
/* трещинки яйца на сцене (stage.js pyhOverlay, grow.cracksSvg) качаются вместе с яйцом и в такт речи — иначе картинка
   поворачивалась бы под неподвижными штрихами */
.hero .egg-cracks { transform-origin: 50% 96%; }
.hero.is-live .hero-img[src*="pyh-egg."] ~ .egg-cracks { animation: life-rock calc(var(--life-period, 3400ms) / 2) cubic-bezier(.45, 0, .55, 1) var(--life-delay, 0ms) infinite alternate; }
.hero.is-live.is-talking .hero-img[src*="pyh-egg."] ~ .egg-cracks { animation: life-talk 1.2s cubic-bezier(.45, 0, .55, 1) infinite; }
.hero:not(.is-live) .egg-cracks { animation: none; }
@keyframes life-breath { 0%, 100% { transform: scale(1, 1); } 50% { transform: scale(1.006, 1.02); } }
@keyframes life-rock { from { transform: rotate(-2deg); } to { transform: rotate(2deg); } }
@keyframes life-talk { 0%, 100% { transform: rotate(0deg); } 25% { transform: rotate(1deg); } 75% { transform: rotate(-1deg); } }
@keyframes life-sleep { 0%, 100% { transform: scale(1, 1); } 50% { transform: scale(1.01, 1.026); } }

/* ---------- разовые реакции (.hero-rr): верно, кивок, «хм», праздник ---------- */
/* длительности — как life.REACT_MS (по ним life.js снимает класс, облачко и конфетти) */
.hero.is-live .hero-rr.rr-correct { animation: life-correct .7s both; }
.hero.is-live .hero-rr.rr-nod { animation: life-nod .6s both; }
.hero.is-live .hero-rr.rr-hmm { animation: life-hmm .7s both; }
.hero.is-live .hero-rr.rr-celebrate { animation: life-celebrate 1.25s both; }
/* верно: присел — подпрыгнул (не выше 3 px) — приземлился */
@keyframes life-correct {
  0% { transform: translateY(0px) rotate(0deg) scale(1, 1); animation-timing-function: cubic-bezier(.22, .8, .3, 1); }
  22% { transform: translateY(0px) rotate(0deg) scale(1.04, .93); animation-timing-function: cubic-bezier(.22, .8, .3, 1); }
  50% { transform: translateY(-3px) rotate(0deg) scale(.98, 1.03); animation-timing-function: cubic-bezier(.55, 0, .75, .3); }
  74% { transform: translateY(0px) rotate(0deg) scale(1.03, .96); animation-timing-function: cubic-bezier(.45, 0, .25, 1); }
  100% { transform: translateY(0px) rotate(0deg) scale(1, 1); }
}
/* спрашивавший кивает дважды: голова «опускается» — сжатие от стоп с лёгким наклоном вперёд */
@keyframes life-nod {
  0%, 50%, 100% { transform: translateY(0px) rotate(0deg) scale(1, 1); animation-timing-function: cubic-bezier(.45, 0, .25, 1); }
  25% { transform: translateY(0px) rotate(1.5deg) scale(1, .965); animation-timing-function: cubic-bezier(.45, 0, .25, 1); }
  74% { transform: translateY(0px) rotate(1deg) scale(1, .978); animation-timing-function: cubic-bezier(.45, 0, .25, 1); }
}
/* «хм, задумался»: наклон −4…−3° вокруг стоп за 0,7 с — без красного, без тряски и без грустного лица */
@keyframes life-hmm {
  0% { transform: translateY(0px) rotate(0deg) scale(1, 1); animation-timing-function: cubic-bezier(.22, .8, .3, 1); }
  30% { transform: translateY(0px) rotate(-4deg) scale(1, 1); animation-timing-function: cubic-bezier(.45, 0, .25, 1); }
  62% { transform: translateY(0px) rotate(-3deg) scale(1, 1); animation-timing-function: cubic-bezier(.45, 0, .25, 1); }
  100% { transform: translateY(0px) rotate(0deg) scale(1, 1); }
}
/* праздник (наклейка, рост): двойной прыжок — приседания и вытягивания, подъём не выше 3 px; конфетти — ниже */
@keyframes life-celebrate {
  0% { transform: translateY(0px) rotate(0deg) scale(1, 1); animation-timing-function: cubic-bezier(.22, .8, .3, 1); }
  10% { transform: translateY(0px) rotate(0deg) scale(1.05, .92); animation-timing-function: cubic-bezier(.22, .8, .3, 1); }
  24% { transform: translateY(-3px) rotate(-2deg) scale(.97, 1.05); animation-timing-function: cubic-bezier(.55, 0, .75, .3); }
  38% { transform: translateY(0px) rotate(0deg) scale(1.05, .93); animation-timing-function: cubic-bezier(.22, .8, .3, 1); }
  52% { transform: translateY(-3px) rotate(2deg) scale(.97, 1.05); animation-timing-function: cubic-bezier(.55, 0, .75, .3); }
  66% { transform: translateY(0px) rotate(0deg) scale(1.03, .96); animation-timing-function: cubic-bezier(.45, 0, .25, 1); }
  80%, 100% { transform: translateY(0px) rotate(0deg) scale(1, 1); }
}

/* облачко «пых» из носа (на верный ответ): левый нижний угол — у кончика носа (left/top ставит life.js) */
.life-puff { position: absolute; width: 38%; max-width: 84px; aspect-ratio: 64 / 44; transform-origin: 0 100%; opacity: 0; pointer-events: none;
  animation: life-puff .74s cubic-bezier(.22, .8, .3, 1) .12s both; }
.life-puff > svg, .life-puff > img { display: block; width: 100%; height: 100%; }
.life-puff > img { object-fit: contain; object-position: 0 100%; }   /* puff.webp другой пропорции не сплющится */
@keyframes life-puff {
  0% { transform: translate(0%, -100%) scale(.25); opacity: 0; }
  28% { opacity: 1; }
  100% { transform: translate(34%, -150%) scale(1); opacity: 0; }
}

/* бумажное конфетти праздника: обрывки 4 красок листа Б, веер вверх и вниз; один раз, к 1,25 с исчезает. Узлы
   убирает life.js по таймеру реакции и при detach */
.life-confetti { position: absolute; left: 50%; top: 30%; width: 0; height: 0; z-index: 2; pointer-events: none; }
.life-confetti i { position: absolute; left: -5px; top: -7px; width: 10px; height: 14px; background: var(--c, #e0762c); opacity: 0;
  clip-path: polygon(8% 0, 100% 10%, 88% 100%, 0 86%); animation: life-confetti 1.15s cubic-bezier(.2, .65, .35, 1) var(--d, 0ms) both; }
.life-confetti i:nth-child(3n) { width: 8px; height: 11px; clip-path: polygon(0 8%, 92% 0, 100% 90%, 12% 100%); }
.life-confetti i:nth-child(3n+1) { width: 12px; height: 9px; }
@keyframes life-confetti {
  0% { transform: translate(0px, 0px) rotate(0deg) scale(.5); opacity: 0; }
  8% { opacity: 1; }
  45% { transform: translate(var(--x, 0px), var(--y, 0px)) rotate(calc(var(--r, 0deg) * .45)) scale(1); opacity: 1; }
  78% { opacity: 1; }
  100% { transform: translate(calc(var(--x, 0px) * 1.25), calc(var(--y, 0px) + 80px)) rotate(var(--r, 0deg)) scale(.9); opacity: 0; }
}

/* ---------- хвостик облачка — к говорящему ---------- */
/* data-tail, --life-tail-x (где на нижнем крае) и --life-tail-dx (куда смотрит кончик) ставит life.js по stage.showLine.
   Чернильный треугольник и бумажный поверх: бумажный закрывает рамку облачка на ширине хвостика */
.bubble[data-tail]::before, .bubble[data-tail]::after { content: ''; position: absolute; top: 100%; pointer-events: none;
  clip-path: polygon(0 0, 100% 0, calc(50% + var(--life-tail-dx, 0px)) 100%); }
.bubble[data-tail]::before { left: calc(var(--life-tail-x, 50%) - 12px); width: 24px; height: 14px; background: var(--c-ink); }
.bubble[data-tail]::after { left: calc(var(--life-tail-x, 50%) - 8px); width: 16px; height: 9px; background: var(--c-card); }

/* ---------- карточка ответа: мягкое «не то» вместо тряски ---------- */
/* Тряска «нет-нет» (@keyframes nope в styles.css и её копии в заданиях) превращает ошибку в наказание (CONTRACT,
   педагогика 1). Вместо неё карточка плавно темнеет до 0,85 и наклоняется на 2°, потом так же плавно
   возвращается. Задания снимают .is-wrong через 600 мс (kit.wiggle, tap-choice, read-aloud), поэтому анимация
   короче (0,56 с) и кончается там же, где началась: в keyframes нет 0 % и 100 % — старт и финиш берутся из
   обычного вида карточки, снятие класса ничего не дёргает. Красная рамка (border-color: var(--c-oops)) — не
   движение, её здесь не трогаем: решение методиста.
   «Спокойные герои» карточек не касаются (меняются только герои). При «Уменьшить движение» styles.css гасит все
   анимации (animation: none !important) — карточка просто темнее, пока висит .is-wrong: без движения, но ошибка
   видна и без красной рамки. На снимках ?still — ничего. */
.choice.is-wrong, .np-bump.is-wrong, .dm-card.is-wrong, .sq-card.is-wrong, .ra.is-wrong .ra-card, .gn-card.is-wrong, .gn-btn.is-wrong {
  animation: life-wrong .56s cubic-bezier(.45, 0, .25, 1); }
/* у карточки свой transform (диктант: шаг «сейчас» приподнят; танграм: деталь на поле) — только затемнение */
.gd-card.is-wrong, .tg-piece.is-wrong { animation: life-wrong-dim .56s cubic-bezier(.45, 0, .25, 1); }
/* предметы счёта повёрнуты каждый на свой угол (--rot): наклон — от него, а не качание ±14° */
.ct-obj.is-wrong > img, .ct-obj.is-wrong > .ct-berry { animation: life-wrong-rot .56s cubic-bezier(.45, 0, .25, 1); }
@keyframes life-wrong { 30% { transform: rotate(2deg); opacity: .85; } 62% { transform: rotate(1deg); opacity: .85; } }
@keyframes life-wrong-dim { 30%, 62% { opacity: .85; } }
@keyframes life-wrong-rot { 30% { transform: rotate(calc(var(--rot, 0deg) + 2deg)); opacity: .85; } 62% { transform: rotate(calc(var(--rot, 0deg) + 1deg)); opacity: .85; } }
@media (prefers-reduced-motion: reduce) {
  .choice.is-wrong, .np-bump.is-wrong, .dm-card.is-wrong, .sq-card.is-wrong, .ra.is-wrong .ra-card, .gn-card.is-wrong, .gn-btn.is-wrong,
  .gd-card.is-wrong, .tg-piece.is-wrong, .ct-obj.is-wrong > img, .ct-obj.is-wrong > .ct-berry { opacity: .85; }
}
html[data-still] :is(.choice, .np-bump, .dm-card, .sq-card, .gn-card, .gn-btn, .gd-card, .tg-piece).is-wrong,
html[data-still] .ra.is-wrong .ra-card, html[data-still] .ct-obj.is-wrong > :is(img, .ct-berry) { animation: none; }
