/* Little Words — games/shared/lw-feel.css
 * THE feel layer. Every rule here exists to answer one question a three year
 * old asks without words: "did the screen notice me?"
 *
 * Two halves, matching the two scripts:
 *   lw-voice-viz.js  → .lwv-*   the screen reacting to the child's VOICE
 *   lw-juice.js      → .lwj-*   the screen reacting to the child's TOUCH
 *
 * Rules of the house, and they are not negotiable:
 *  - Animate only `transform`, the individual transform properties
 *    (`scale` / `rotate` / `translate`), `opacity` and `filter`. Never width,
 *    height, top, left, margin or box-shadow-in-a-keyframe on a hot path.
 *  - Use the INDIVIDUAL transform properties for anything this layer drives,
 *    because almost every element we touch already carries a `transform` from
 *    turn-loop.css, lw-games.css or an engine stylesheet. `scale`/`rotate`/
 *    `translate` compose with that `transform` instead of clobbering it, so
 *    the feel layer can never break a layout it does not own.
 *  - Every continuous animation is opt-in per element. Nothing is applied to
 *    "all buttons", because a 25-cell bingo grid with 25 infinite keyframes is
 *    how you drop frames on a mid-range phone.
 *  - prefers-reduced-motion removes the OSCILLATION, never the FEEDBACK. A
 *    child on a reduced-motion device must still see that the game heard them.
 */

/* ========================================================================
   0. shared tokens
   ======================================================================== */
:root {
  --lwj-gold: #E8A33D;
  --lwj-sage: #6BAF8D;
  --lwj-sky: #8FCBEA;
  --lwj-navy: #1E2A4A;
  /* live voice level, 0..1, written by lw-voice-viz.js on the .lwt element */
  --lwv-level: 0;
}

/* ========================================================================
   1. particle canvas (lw-juice.js)
   ------------------------------------------------------------------------
   ONE canvas for the whole page, above the card overlay (z-index 60) and the
   celebrate overlay (55), below nothing that matters. Never hit-testable.
   ======================================================================== */
canvas.lwj-fx {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 70;
  pointer-events: none;
  contain: strict;
}

/* ========================================================================
   2. touch feedback (lw-juice.js)
   ------------------------------------------------------------------------
   .lwj-press lands inside the pointerdown handler itself, so the first frame
   after the finger touches the glass is already different. .lwj-pop is the
   release: a small overshoot so the button feels like it has a spring in it.
   ======================================================================== */
.lwj-press {
  scale: 0.93 0.95;
  filter: brightness(1.06);
  transition: scale 70ms cubic-bezier(0.34, 1.4, 0.64, 1), filter 70ms linear;
}
.lwj-pop {
  animation: lwj-pop 260ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes lwj-pop {
  0%   { scale: 0.93 0.95; }
  45%  { scale: 1.07 0.94; }
  70%  { scale: 0.98 1.03; }
  100% { scale: 1 1; }
}

/* the oversized one: a success is supposed to feel like too much */
.lwj-celebrate {
  animation: lwj-celebrate 640ms cubic-bezier(0.22, 1.4, 0.36, 1);
  z-index: 3;
}
@keyframes lwj-celebrate {
  0%   { scale: 1;    rotate: 0deg; }
  18%  { scale: 1.28; rotate: -5deg; }
  38%  { scale: 0.94; rotate: 4deg; }
  58%  { scale: 1.14; rotate: -2.5deg; }
  78%  { scale: 0.99; rotate: 1deg; }
  100% { scale: 1;    rotate: 0deg; }
}

/* a soft ring that blooms out of whatever just succeeded */
.lwj-bloom {
  position: fixed;
  z-index: 69;
  pointer-events: none;
  border-radius: 50%;
  border: 5px solid var(--lwj-gold);
  opacity: 0.85;
  translate: -50% -50%;
  animation: lwj-bloom 620ms cubic-bezier(0.2, 0.8, 0.3, 1) forwards;
}
@keyframes lwj-bloom {
  0%   { scale: 0.2; opacity: 0.9; }
  100% { scale: 2.6; opacity: 0; }
}

/* ========================================================================
   3. affordance + idle life (lw-juice.js)
   ------------------------------------------------------------------------
   .lwj-tap      marked by JS on anything genuinely interactive
   .lwj-lift     only added where the element had NO shadow of its own, so we
                 never fight an engine's existing button styling
   .lwj-primary  the one thing we most want tapped right now — it breathes
   ======================================================================== */
.lwj-tap { cursor: pointer; -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
.lwj-lift {
  box-shadow: 0 3px 0 rgba(30, 42, 74, 0.14), 0 6px 14px rgba(30, 42, 74, 0.12);
}

/* nothing on screen is perfectly still: Buddy and the primary target breathe.
   The bob animates ONLY `translate` so it composes with the press `scale` and
   the nudge `rotate` instead of racing them for the same property. */
body.lwj-alive .lwt-buddy img,
body.lwj-alive .lwt-bar-buddy img {
  animation: lwj-breathe 3.6s ease-in-out infinite;
  transform-origin: 50% 85%;
}
body.lwj-alive .lwj-primary {
  animation: lwj-bob 2.9s ease-in-out infinite;
}
@keyframes lwj-breathe {
  0%, 100% { scale: 1 1; }
  50%      { scale: 1.035 1.055; }
}
@keyframes lwj-bob {
  0%, 100% { translate: 0 0; }
  50%      { translate: 0 -4px; }
}

/* --- cascade guards ------------------------------------------------------
   A CSS animation beats an inline style, so any rule that animates `scale`
   would silently win over the per-frame `style.scale` that lw-voice-viz.js
   writes, and Buddy would stop reacting to the child's voice. Two overrides
   keep the priority honest:
     .lwv-live   set on <body> while a listening window is open — the voice
                 owns Buddy for that whole window
     .lwj-celebrate  a win outranks idle breathing                          */
body.lwv-live .lwt-buddy img,
body.lwv-live .lwt-bar-buddy img { animation: none; }

body.lwj-alive .lwt-buddy img.lwj-celebrate,
body.lwj-alive .lwt-bar-buddy img.lwj-celebrate,
body.lwv-live .lwt-buddy img.lwj-celebrate,
body.lwj-alive .lwj-primary.lwj-celebrate {
  animation: lwj-celebrate 640ms cubic-bezier(0.22, 1.4, 0.36, 1);
}
body.lwj-alive .lwj-primary.lwj-nudge {
  animation: lwj-nudge 780ms cubic-bezier(0.34, 1.56, 0.64, 1) 3;
}

/* "come tap me" — fires after the idle delay, three beats, then stops */
.lwj-nudge {
  animation: lwj-nudge 780ms cubic-bezier(0.34, 1.56, 0.64, 1) 3;
  outline: 4px solid var(--lwj-gold);
  outline-offset: 4px;
  border-radius: 14px;
}
@keyframes lwj-nudge {
  0%, 100% { scale: 1;    rotate: 0deg; }
  30%      { scale: 1.09; rotate: -2deg; }
  60%      { scale: 1.03; rotate: 2deg; }
}
/* a finger that points at it, so the nudge says WHERE and not just "hey" */
.lwj-hand {
  position: fixed;
  z-index: 69;
  pointer-events: none;
  font-size: 30px;
  line-height: 1;
  translate: -50% -50%;
  animation: lwj-hand 780ms ease-in-out 3;
  filter: drop-shadow(0 3px 5px rgba(30, 42, 74, 0.35));
}
@keyframes lwj-hand {
  0%, 100% { translate: -50% -50%; opacity: 0.95; }
  50%      { translate: -50% -18%; opacity: 1; }
}

/* ========================================================================
   4. VOICE-REACTIVE VISUALS (lw-voice-viz.js)
   ------------------------------------------------------------------------
   This is the part that proves "the game hears me". It runs off the analyser
   in lw-speech.js, at animation-frame rate, BEFORE and INDEPENDENT of any
   word recognition — so it is alive even on a browser with no SpeechRecognition
   at all.
   ======================================================================== */

/* -- 4a. sound rings radiating out of the mic orb ------------------------ */
.lwv-rings {
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: 50%;
}
.lwv-ring {
  position: absolute;
  inset: -2px;
  border-radius: 50%;
  border: 4px solid var(--lwj-sage);
  opacity: 0;
  will-change: scale, opacity;
}
.lwv-ring.go {
  animation: lwv-ring 900ms cubic-bezier(0.15, 0.75, 0.35, 1) forwards;
}
@keyframes lwv-ring {
  0%   { scale: 0.86; opacity: 0.7; border-width: 5px; }
  100% { scale: 2.5;  opacity: 0;   border-width: 1px; }
}

/* -- 4b. the balloon: the voice meter a toddler reads without being told --
   It INFLATES while they talk and sighs back down when they stop. No bar, no
   number, no chart. Decorative only — never hit-testable, so it can never
   steal a tap from the orb. */
.lwv-balloon {
  position: relative;
  flex: 0 0 auto;
  width: 46px;
  height: 58px;
  pointer-events: none;
  transform-origin: 50% 100%;
  scale: 0.55;
  opacity: 0.75;
  will-change: scale;
}
.lwv-balloon svg { width: 100%; height: 100%; display: block; overflow: visible; }
.lwv-balloon .lwv-b-body { fill: var(--lwj-sage); transition: fill 260ms linear; }
.lwv-balloon.full .lwv-b-body { fill: var(--lwj-gold); }
/* the "you filled it!" wiggle must NOT animate `scale` — that is written every
   frame from JS and an animation would override the inflation it is showing */
.lwv-balloon.full { animation: lwv-balloon-full 620ms ease-in-out 2; }
@keyframes lwv-balloon-full {
  0%, 100% { rotate: 0deg; }
  30%      { rotate: -7deg; }
  65%      { rotate: 6deg; }
}

/* the mic row has to hold the orb AND the balloon without shoving either off
   a 320px screen */
.lwt-mic { position: relative; gap: 10px; align-items: center; }

/* -- 4c. the scene notices too: a warm glow that swells with the voice ----
   position:fixed + opacity only, so this costs one composited layer and zero
   layout. */
.lwv-glow {
  position: fixed;
  inset: 0;
  z-index: 59;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(120% 90% at 50% 78%, rgba(232, 163, 61, 0.42), rgba(232, 163, 61, 0) 62%);
  will-change: opacity;
}

/* -- 4d. Buddy and the card picture are driven per-frame from JS via the
   individual transform properties; nothing to declare here except the origin
   and the promise to the compositor. */
.lwt-buddy img { transform-origin: 50% 85%; }
/* `transform`, not the individual property names: the hint has to name the
   property GROUP for the compositor to promote the element, otherwise every
   frame re-rasters the card picture and the frame budget goes with it. */
.lwv-react { will-change: transform; }

/* ========================================================================
   5. reduced motion
   ------------------------------------------------------------------------
   The reaction stays. The oscillation goes. A child who cannot have motion
   still has to be able to see that their voice did something, so the voice
   layer switches to discrete steps (lw-voice-viz.js quantises the level and
   sets data-lwv-step) instead of a continuous wiggle.
   ======================================================================== */
@media (prefers-reduced-motion: reduce) {
  body.lwj-alive .lwt-buddy img,
  body.lwj-alive .lwt-bar-buddy img,
  body.lwj-alive .lwj-primary,
  .lwj-pop,
  .lwj-celebrate,
  .lwj-nudge,
  .lwj-hand,
  .lwv-ring.go,
  .lwv-balloon.full,
  body.lwj-alive .lwt-buddy img.lwj-celebrate,
  body.lwv-live .lwt-buddy img.lwj-celebrate,
  body.lwj-alive .lwj-primary.lwj-nudge,
  body.lwj-alive .lwj-primary.lwj-celebrate {
    animation: none !important;
  }

  /* touch still answers, it just answers in one step instead of a spring */
  .lwj-press { scale: 0.95; transition: scale 60ms linear, filter 60ms linear; }
  .lwj-celebrate { scale: 1.12; transition: scale 200ms linear; }
  .lwj-nudge { outline: 4px solid var(--lwj-gold); outline-offset: 4px; }
  .lwj-hand { opacity: 1; }
  .lwj-bloom { animation: none; opacity: 0.5; scale: 1.8; transition: opacity 400ms linear; }

  /* voice still answers: four visible steps, each a flat state change */
  .lwv-ring { display: none; }
  .lwv-balloon { transition: scale 160ms linear, opacity 160ms linear; }
  .lwv-glow { transition: opacity 180ms linear; }

  .lwt-orb[data-lwv-step='0'] { border-color: #6BAF8D; }
  .lwt-orb[data-lwv-step='1'] { border-color: #4E9E77; background: #DFF2E8; }
  .lwt-orb[data-lwv-step='2'] { border-color: #3E8F68; background: #CFEBDC; }
  .lwt-orb[data-lwv-step='3'] { border-color: var(--lwj-gold); background: #FBEBCF; }
}

/* ========================================================================
   6. small screens
   ------------------------------------------------------------------------
   320px is the floor we support. The balloon is the only thing this layer
   adds to a laid-out row, so it is the only thing that needs to shrink.
   ======================================================================== */
@media (max-width: 400px) {
  .lwv-balloon { width: 40px; height: 50px; }
  .lwt-mic { gap: 8px; }
}
@media (max-width: 340px) {
  .lwv-balloon { width: 34px; height: 43px; }
  .lwt-mic { gap: 6px; }
  .lwj-hand { font-size: 26px; }
}

/* a page must never scroll sideways because of anything in this file */
.lwv-glow, canvas.lwj-fx, .lwj-bloom, .lwj-hand { max-width: 100vw; }
