/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   Ally's motion. Pairs with templates/students/_ally_character.html and static/js/ally_character.js.

   Everything is scoped under .ally-char, and every selector inside targets a CLASS — several Ally's
   share a page (widget header, launcher, lesson hero, recap) and they all animate together.

   SQUASH AND STRETCH IS THE IDLE LOOP, NOT A REACTION. The old .ac-bob translated 4px and scaled Y
   by 1.8% — a rigid box sliding up and down. A soft body never rests at 1:1: it stretches as it
   rises, squashes as it lands, and overshoots on the settle. That is the whole difference between
   a sticker that moves and something that feels alive, and it is why .ac-head exists to counter it.

   FOUR NESTED TRANSFORM LAYERS — because a running animation beats a normal declaration for the
   SAME property, so two things that both want `transform` cannot share one element:
     .ac-sway   state pose      — transition-driven (lean).       JS sets data-state, CSS poses.
     .ac-bob    the squish loop — @keyframes, always running.
     .ac-pop    one-shot beats  — @keyframes on a class JS adds then strips (hop / stretch / nod / ack).
     .ac-head   counter-squish  — @keyframes, cancels the body's deformation across the face.
   Collapse any two and the loser stops working silently. The antenna repeats the split for the
   same reason (.ac-antenna poses, .ac-ant-swing loops) — see the note at the antenna below.

   ONE TEMPO KNOB. --beat drives every loop, so a state changes her energy by changing one number
   instead of six animation-durations that can drift out of phase with each other.

   transform-box:fill-box is set ONLY on the elements actually transformed. Applying it to `*` makes
   transform-origin resolve against each child's own bbox, which silently misplaces the pupils.

   No audio is played by any of this. While she speaks, ally_character.js speak() sizes the open
   mouth from the reply's loudness, frame by frame; nothing here animates the mouth.
   ═══════════════════════════════════════════════════════════════════════════════════════════════ */

.ally-char { display: inline-block; line-height: 0; --beat: 3.2s; }
.ally-char svg { display: block; width: 100%; height: 100%; overflow: visible; }

.ally-char .ac-sway,
.ally-char .ac-bob,
.ally-char .ac-pop,
.ally-char .ac-head,
.ally-char .ac-shadow,
.ally-char .ac-antenna,
.ally-char .ac-ant-swing,
.ally-char .ac-eye,
.ally-char .ac-brow,
.ally-char .ac-confetti > * { transform-box: fill-box; }

/* Energy per state. Everything below reads --beat, so these nine lines are the tempo of the whole
   character. `gentle` is slower than idle on purpose: a wrong answer should feel like she is
   settling down next to the student, not bouncing at them. */
.ally-char[data-state="listening"] { --beat: 2.9s; }
.ally-char[data-state="thinking"]  { --beat: 3.8s; }
.ally-char[data-state="talking"]   { --beat: 1.3s; }
.ally-char[data-state="happy"]     { --beat: .9s;  }
.ally-char[data-state="gentle"]    { --beat: 4.4s; }
.ally-char[data-state="curious"]   { --beat: 3s;   }
.ally-char[data-state="surprised"] { --beat: 2.4s; }
.ally-char[data-state="celebrate"] { --beat: .85s; }

/* ── layer 1: state pose ──────────────────────────────────────────────────────────────────────── */
.ally-char .ac-sway {
  transform-origin: 50% 90%;
  transition: transform .55s cubic-bezier(.34, 1.3, .5, 1);
}

/* ── layer 2: the squish. She is never at 1:1 ─────────────────────────────────────────────────── */
.ally-char .ac-bob {
  transform-origin: 50% 99%;                 /* she deforms against the FLOOR, not her own middle */
  animation: ac-squish var(--beat) cubic-bezier(.45, .05, .35, 1) infinite;
}
@keyframes ac-squish {
  0%, 100% { transform: translateY(0)    scale(1, 1); }
  26%      { transform: translateY(-7px) scale(.95, 1.06); }   /* rise + stretch */
  52%      { transform: translateY(0)    scale(1.07, .93); }   /* land + squash  */
  72%      { transform: translateY(-2px) scale(.98, 1.02); }   /* overshoot      */
  88%      { transform: translateY(0)    scale(1.02, .985); }  /* settle         */
}
/* Happy and celebrate squash harder. Kept modest on purpose: react() also fires the .ac-pop hop,
   and a -26px hop stacked on a -27px squish throws her clean out of the widget header. */
.ally-char[data-state="happy"]     .ac-bob,
.ally-char[data-state="celebrate"] .ac-bob { animation-name: ac-squish-big; }
@keyframes ac-squish-big {
  0%, 100% { transform: translateY(0)     scale(1, 1); }
  24%      { transform: translateY(-11px) scale(.92, 1.09); }
  50%      { transform: translateY(0)     scale(1.10, .90); }
  72%      { transform: translateY(-4px)  scale(.97, 1.03); }
}

/* The floor shadow is part of the performance. It tightens and fades as she leaves the ground —
   without it the squish reads as the character stretching rather than as her jumping. */
.ally-char .ac-shadow {
  transform-origin: 50% 50%;
  animation: ac-shadow var(--beat) cubic-bezier(.45, .05, .35, 1) infinite;
}
@keyframes ac-shadow {
  0%, 100% { transform: scale(1);       opacity: 1; }
  26%      { transform: scale(.85);     opacity: .58; }
  52%      { transform: scale(1.07);    opacity: 1; }
  72%      { transform: scale(.96);     opacity: .88; }
}

/* The head fights the body so the face does not smear. .ac-head is INSIDE .ac-bob, so the scales
   multiply: at the 52% squash the body is 1.07/.93 and the head 0.95/1.05, leaving the face at
   1.0165/0.9765 — visibly intact while the body is visibly deforming. */
.ally-char .ac-head {
  transform-origin: 50% 100%;
  animation: ac-counter var(--beat) cubic-bezier(.45, .05, .35, 1) infinite;
}
@keyframes ac-counter {
  0%, 100% { transform: scale(1, 1); }
  26%      { transform: scale(1.035, .97); }
  52%      { transform: scale(.95, 1.05); }
  72%      { transform: scale(1.012, .99); }
}

/* ── layer 3: one-shot beats. Squash and stretch is what makes a hop read as joy ──────────────── */
.ally-char .ac-pop { transform-origin: 50% 90%; }
.ally-char .ac-pop.ac-hop     { animation: ac-hop .78s cubic-bezier(.3, .8, .4, 1); }
.ally-char .ac-pop.ac-stretch { animation: ac-stretch .5s ease-out; }
.ally-char .ac-pop.ac-nod     { animation: ac-nod .7s ease-in-out; }
@keyframes ac-hop {
  0%   { transform: translateY(0) scale(1, 1); }
  18%  { transform: translateY(2px) scale(1.07, .93); }   /* anticipation: dip before the jump */
  45%  { transform: translateY(-26px) scale(.94, 1.08); }
  70%  { transform: translateY(0) scale(1.06, .94); }
  85%  { transform: translateY(-5px) scale(.99, 1.01); }
  100% { transform: translateY(0) scale(1, 1); }
}
@keyframes ac-stretch {
  0%   { transform: scale(1, 1); }
  30%  { transform: scale(.9, 1.12) translateY(-6px); }
  60%  { transform: scale(1.05, .96); }
  100% { transform: scale(1, 1); }
}
@keyframes ac-nod {
  0%, 100% { transform: rotate(0); }
  30%      { transform: rotate(5deg); }
  65%      { transform: rotate(-2deg); }
}
/* The typed-reply acknowledgement — AllyChar.nod(). A vertical "mm-hm" dip, NOT ac-nod above: that
   one is a side rock and is already the `incorrect` beat, so reusing it would make every typed
   answer look like a wrong one. Units are SVG user units (224 tall): 10 here is ~4px at 96px. */
.ally-char .ac-pop.ac-ack     { animation: ac-ack .62s cubic-bezier(.3, .7, .4, 1); }
@keyframes ac-ack {
  0%, 100% { transform: translateY(0)    scale(1, 1); }
  30%      { transform: translateY(10px) scale(1.04, .95); }
  58%      { transform: translateY(-3px) scale(.99, 1.015); }
  80%      { transform: translateY(3px)  scale(1.01, .99); }
}

/* ── antenna: deliberately lags the body. Cheap personality, sells every state ─────────────────── */
/* TWO ELEMENTS, NOT ONE. This used to be a single .ac-antenna carrying both the sway keyframes and
   the per-state `transform: rotate(...)`. A running animation outranks a normal declaration for the
   same property, so the animation won and EVERY antenna pose below was dead code — she swayed the
   identical +/-4deg whether she was thinking, celebrating or consoling. Outer poses, inner loops. */
.ally-char .ac-antenna {
  transform-origin: 50% 100%;                /* it STANDS on the body — pivot at the base, not the tip */
  transition: transform .5s cubic-bezier(.34, 1.5, .5, 1);
}
.ally-char .ac-ant-swing {
  transform-origin: 50% 100%;
  animation: ac-sway var(--beat) ease-in-out infinite;
  animation-delay: .14s;                     /* the lag IS the effect — do not remove */
}
@keyframes ac-sway {
  0%, 100% { transform: rotate(-4deg); }
  50%      { transform: rotate(4deg); }
}
.ally-char .ac-ball { transition: fill .35s; }

/* ── screen ───────────────────────────────────────────────────────────────────────────────────── */
.ally-char .ac-screen { transition: fill .45s; }
.ally-char .ac-glow   { opacity: 0; filter: blur(9px); transition: opacity .45s; }

/* ── eyes ─────────────────────────────────────────────────────────────────────────────────────── */
.ally-char .ac-eye    { transform-origin: 50% 50%; transition: transform .2s ease; }
.ally-char .ac-pupil  { transition: transform .34s cubic-bezier(.34, 1.4, .5, 1); }
.ally-char .ac-happy  { opacity: 0; transition: opacity .18s; }
.ally-char .ac-norm   { transition: opacity .18s; }
.ally-char[data-state="happy"]     .ac-happy,
.ally-char[data-state="celebrate"] .ac-happy { opacity: 1; }
.ally-char[data-state="happy"]     .ac-norm,
.ally-char[data-state="celebrate"] .ac-norm  { opacity: 0; }

/* ── brows. The single biggest expressive gain over the old PNG face ──────────────────────────── */
.ally-char .ac-brow {
  transform-origin: 50% 50%;
  transition: transform .34s cubic-bezier(.34, 1.4, .5, 1);
  opacity: .95;
}

/* ── mouth: exactly one shape visible; JS swaps which ─────────────────────────────────────────── */
.ally-char .ac-m { transition: opacity .16s; }
.ally-char .ac-m.ac-off { opacity: 0; }

/* ═══ per-state poses ═══════════════════════════════════════════════════════════════════════════
   Each state changes lean, antenna, brows and (sometimes) the screen. Brows do most of the work:
   `thinking` and `gentle` differ mainly by brow angle, and that is what makes them read as
   different feelings rather than as different mouth shapes. */

.ally-char[data-state="listening"] .ac-sway    { transform: rotate(-2deg) translateY(-3px); }
.ally-char[data-state="listening"] .ac-antenna { transform: rotate(-12deg); }
.ally-char[data-state="listening"] .ac-brow    { transform: translateY(-4px); }

.ally-char[data-state="thinking"]  .ac-sway    { transform: rotate(7deg); }
.ally-char[data-state="thinking"]  .ac-antenna { transform: rotate(16deg); }
.ally-char[data-state="thinking"]  .ac-ball    { animation: ac-pulse 1s ease-in-out infinite; }
.ally-char[data-state="thinking"]  .ac-brow.ac-l { transform: translateY(-6px) rotate(-11deg); }
.ally-char[data-state="thinking"]  .ac-brow.ac-r { transform: translateY(1px) rotate(4deg); }
@keyframes ac-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

.ally-char[data-state="talking"]   .ac-brow    { transform: translateY(-2px); }

.ally-char[data-state="happy"]     .ac-antenna { transform: rotate(-8deg); }
.ally-char[data-state="happy"]     .ac-brow.ac-l { transform: translateY(-7px) rotate(-6deg); }
.ally-char[data-state="happy"]     .ac-brow.ac-r { transform: translateY(-7px) rotate(6deg); }

/* A wrong answer is warmth, never disappointment — brows tilt in with concern, screen goes warm,
   and the mouth stays a soft smile. She must never look like she is scolding a child. */
.ally-char[data-state="gentle"]    .ac-sway    { transform: rotate(-4deg); }
.ally-char[data-state="gentle"]    .ac-antenna { transform: rotate(-19deg); }
/* SIGNS MATTER HERE. SVG y runs DOWNWARD, so a positive rotate swings the LEFT brow's inner (right)
   end DOWN. The old values were +13/-13, which drops BOTH inner ends — the textbook angry scowl, on
   the one state whose entire job is not to look like scolding. Inner ends UP is concern. */
.ally-char[data-state="gentle"]    .ac-brow.ac-l { transform: translateY(-5px) rotate(-13deg); }
.ally-char[data-state="gentle"]    .ac-brow.ac-r { transform: translateY(-5px) rotate(13deg); }
/* Warmth comes from the GLOW, not from repainting the screen. `fill` on .ac-screen replaces the
   whole url(#ally-screen) gradient with one flat colour, so the lit glass went dead flat — and a
   warm brown flat fill sitting inside a persimmon body is mud. She looked unwell, not kind.

   .12 AND NO HIGHER. The screen already carries a permanent #ffb545 wash at .13 in the markup, so
   this stacks on top of it: at .28 the composite hit ~.37 and the whole panel turned khaki — the
   same mud, arrived at the other way. The emotion is carried by the brows, the lean and the soft
   mouth; the screen only needs to tip warm, not change colour. */
.ally-char[data-state="gentle"]    .ac-glow    { opacity: .12; }

.ally-char[data-state="celebrate"] .ac-antenna { transform: rotate(-20deg); }
.ally-char[data-state="celebrate"] .ac-brow.ac-l { transform: translateY(-9px) rotate(-8deg); }
.ally-char[data-state="celebrate"] .ac-brow.ac-r { transform: translateY(-9px) rotate(8deg); }

.ally-char[data-state="curious"]   .ac-sway    { transform: rotate(-9deg); }
.ally-char[data-state="curious"]   .ac-antenna { transform: rotate(-15deg); }
.ally-char[data-state="curious"]   .ac-brow.ac-l { transform: translateY(-8px); }
.ally-char[data-state="curious"]   .ac-brow.ac-r { transform: translateY(0) rotate(-3deg); }

.ally-char[data-state="surprised"] .ac-antenna { transform: rotate(2deg) scaleY(1.2); }
.ally-char[data-state="surprised"] .ac-brow    { transform: translateY(-10px); }
.ally-char[data-state="surprised"] .ac-eye     { transform: scale(1.18); }

/* LAST, and it has to stay last. blink() adds .ac-blink to .ac-eye, and the `surprised` pose above
   also sets .ac-eye's transform at equal specificity — so whichever is written later wins. When
   this rule sat above the poses she could not blink while surprised. */
.ally-char .ac-eye.ac-blink { transform: scaleY(.09); }     /* the eye squashes into a line */

/* ── confetti, celebrate only ─────────────────────────────────────────────────────────────────── */
.ally-char .ac-confetti > * { opacity: 0; }
.ally-char .ac-confetti.ac-go > * { animation: ac-burst .95s ease-out forwards; }
@keyframes ac-burst {
  0%   { opacity: 1; transform: translate(0, 0) scale(.4); }
  100% { opacity: 0; transform: translate(var(--dx), var(--dy)) scale(1.1) rotate(200deg); }
}

/* ═══ reduced motion ════════════════════════════════════════════════════════════════════════════
   Same contract the rest of the app uses (base.html:694, lesson_plan.html:75): motion off, meaning
   intact. The state poses are removed too, not just the loops — a silent instant snap between poses
   is its own kind of motion. She holds one calm face and the conversation still works.
   The squish loops go with them: a resting frame of ac-squish is scale(1,1), so she simply stands
   still, correctly proportioned, fully painted.
   ally_character.js checks the same query and skips the JS-driven beats, so nothing queues up. */
@media (prefers-reduced-motion: reduce) {
  .ally-char .ac-bob,
  .ally-char .ac-head,
  .ally-char .ac-shadow,
  .ally-char .ac-ant-swing,
  .ally-char .ac-ball,
  .ally-char .ac-pop.ac-hop,
  .ally-char .ac-pop.ac-stretch,
  .ally-char .ac-pop.ac-nod,
  .ally-char .ac-pop.ac-ack,
  .ally-char .ac-confetti.ac-go > * { animation: none !important; }
  .ally-char .ac-sway,
  .ally-char .ac-antenna,
  .ally-char .ac-pupil,
  .ally-char .ac-brow,
  .ally-char .ac-eye { transition: none !important; }
}
