/* CharacterStage layer mechanics — the companion stylesheet to sdk/stage.js,
   shared by the chat app and the stream demo. The HOST page sizes and skins the
   stage box (background, border-radius, aspect); this file owns how the layers
   inside it stack and swap. Apply class `char-stage` to the host element.

   The stage is layers toggled by opacity - full-frame <img>s, or under a patch set
   one base frame and small <svg> patches - mouths underneath, emotion overlays above
   them, blink on top, all inside one box (.layers) that sdk/stage.js places on the
   art itself. */

.char-stage { position: relative; overflow: hidden; }

.char-stage img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  /* Discrete swaps ARE the animation — easing here would smear the mouth shapes. */
  transition: none;
}

.char-stage img.on { opacity: 1; }

/* The blink overlay is the ONE layer that eases. A real blink is motion-blurred,
   so a hard cut reads as a glitch — but that same easing on a viseme swap would
   smear the mouth shapes, which is why the rule above stays transition:none.
   This selector's specificity (0,2,1) has to beat that one's (0,1,1), or the
   crossfade silently never happens and the blink pops. */
.char-stage img.blink { transition: opacity 45ms linear; }

/* Emotion overlays ease longer than the blink: an expression change is a face
   moving, not a lip position — 140ms reads as intent where a hard cut reads as
   a glitch. */
.char-stage img.emotion { transition: opacity 140ms ease; }

/* ── Breath ───────────────────────────────────────────────────────────────────
   The character's chest moves. sdk/breath.js animates the chest layer's transform
   (a Web Animation on that one element, built once - nothing runs per frame) and
   writes the mask and the pivot as custom properties when the box resizes.

   ONE transformed element, and it is masked. Everything else on the stage is
   pixel-static, which is the whole point: these frames are a character AND a room,
   and a transform on the stack — or a mask that is a full-width band — moves the
   doorframe with her. This stylesheet itself transforms nothing: the chest rule
   below fixes the pivot and declares the intent, and the only transform under
   .char-stage is the one sdk/breath.js animates (harness breath's `one mover`
   gate). Defaults are a fully transparent mask, so a character with no geometry,
   an unbaked build or prefers-reduced-motion renders exactly as it did before this
   file grew. */

.char-stage .layers {
  position: absolute;
  inset: 0;
}

/* The torso copy: one more full-frame layer, always visible (never toggled by
   `on`), masked to a shoulder-sized blob and scaled about a pivot BELOW the frame —
   far enough away that ~0.5% of scale reads as a lift rather than a stretch. */
.char-stage img.chest {
  opacity: 1;
  transform-origin: 50% var(--breath-pivot, 100%);
  /* The transform is the breath's Web Animation - the only thing that moves, every frame. */
  will-change: transform;
  /* Two gradients, intersected: a vertical band crossed with a horizontal one is a
     blob. Either alone spans the frame on the other axis and takes the scenery with
     it. No mask at all means no torso was derived, and the layer must then be
     invisible rather than a second opaque copy of the character on top of the
     first — hence the transparent default. */
  -webkit-mask-image: var(--breath-mask, linear-gradient(#0000, #0000)), var(--breath-mask-x, linear-gradient(#0000, #0000));
  mask-image: var(--breath-mask, linear-gradient(#0000, #0000)), var(--breath-mask-x, linear-gradient(#0000, #0000));
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

/* A still character is the correct rendering of "no motion", not a degraded one.
   breath.js never animates under this preference and cancels when it flips on
   mid-session; `!important` is what lets an author rule outrank a Web Animation,
   so this is the belt to that braces. */
@media (prefers-reduced-motion: reduce) {
  .char-stage img.chest { transform: none !important; }
}

/* The picker's card crops the art to fill a fixed box instead of fitting it. The
   layer mechanics are identical; only the fit differs, and breath.js is told which
   one so the mask lands on the body either way. */
.char-stage.fit-cover img {
  object-fit: cover;
  object-position: top;
}

/* ── Patches ──────────────────────────────────────────────────────────────────
   Under a patch set (sdk/stage.js) the rest frame is ONE full layer, img.base, and
   every other layer is a small crop that must land exactly where that layer's pixels
   sit in the full frame. Each is an <svg class="patch"> filling the art box, its
   viewBox the frame, holding one <image> at the crop's rect, so SVG draws the crop at
   its true sub-pixel position over the base (an <img> placed at that rect is snapped
   to whole device pixels and drifts off the base by up to half a pixel). The same
   on/off and the same two eased fades as the full-frame layers. Placement only -
   nothing here transforms. */

.char-stage svg.patch {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
  transition: none;
}

.char-stage svg.patch.on { opacity: 1; }
.char-stage svg.patch.blink { transition: opacity 45ms linear; }
.char-stage svg.patch.emotion { transition: opacity 140ms ease; }

/* Once sdk/stage.js has placed the layer box on the art (snapped to whole device
   pixels, inline left/top/width/height), every full layer fills it edge to edge:
   the box IS the frame, so there is nothing left to fit or letterbox, and a fit that
   letterboxed by a fraction of a pixel would put back the snapping the box removed.
   Higher specificity than the fit-cover rule above, on purpose. */
.char-stage .layers.art img { object-fit: fill; }

/* The base is the picture at rest: always lit, whatever mouth is on. */
.char-stage img.base { opacity: 1; }
