/* Character widget presentation.
   A frame PNG is the fixed foundation: within one pose it never moves between
   states. Small additions use CSS layers; geometry-changing poses and strong
   expressions switch the whole frame, so no second brow or mismatched face part
   is drawn over the source artwork.
   This file is a generic catalogue: it knows the *shape* of every gesture and
   every decoration, but not which character uses them, how long they run, or
   where on a face they sit. Those belong to the pack (see characters/*/config.js). */

:root { color-scheme: normal; background-color: rgba(0, 0, 0, 0) !important; }
html, body {
  width: 100%; height: 100%; margin: 0; overflow: hidden;
  background-color: rgba(0, 0, 0, 0) !important;
  font-family: "Yu Gothic UI", "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
}

.character {
  position: relative; display: grid; place-items: center; width: 100%; height: 100%;
  animation: wmv-idle-float 3.4s ease-in-out infinite alternate; transform-origin: 50% 70%;
}

/* The stack is exactly the "object-fit: contain" box of the character canvas, so
   every percentage below addresses the artwork itself instead of the iframe. */
.stack {
  position: relative;
  width: min(100%, calc(100vh * var(--canvas-width, 1024) / var(--canvas-height, 1536)));
  height: min(100%, calc(100vw * var(--canvas-height, 1536) / var(--canvas-width, 1024)));
  aspect-ratio: var(--canvas-width, 1024) / var(--canvas-height, 1536);
}

.frame {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain;
  filter: drop-shadow(0 22px 30px rgba(0, 0, 0, .5));
}

/* One-shot pose frames sit over the untouched base and switch on the same clock
   as their gesture. The two frames never blend: a waist bow rendered over an
   upright body reads as a ghosted double character. Sustained poses keep the
   underlay out of the document. This is generic: the runtime derives it from
   `pose + action`, not from a state or character name. */
.pose-underlay { display: none; }
.character[data-pose-transition="true"] .pose-underlay { display: block; }
.character[data-pose-transition="true"].is-gesturing #base {
  animation: wmv-pose-cutover var(--gesture-duration, 1.8s) linear var(--gesture-iterations, 1) both;
}
.character[data-pose-transition="true"].is-gesturing .pose-underlay {
  animation: wmv-pose-underlay-cutover var(--gesture-duration, 1.8s) linear var(--gesture-iterations, 1) both;
}

.mouth {
  opacity: 0; filter: none;
  -webkit-mask-image: radial-gradient(ellipse var(--mouth-width, 15%) var(--mouth-height, 7.5%) at var(--mouth-x, 51.5%) var(--mouth-y, 45.8%), #000 42%, transparent 76%);
  mask-image: radial-gradient(ellipse var(--mouth-width, 15%) var(--mouth-height, 7.5%) at var(--mouth-x, 51.5%) var(--mouth-y, 45.8%), #000 42%, transparent 76%);
}

/* A pose frame is a separate render of the same character. Its mouth and eyes do
   not necessarily land on the geometry measured for the base frame, so when the
   pack marks a pose `featureOverlays: false` the two layers that depend on that
   geometry are taken out of the document entirely - belt and braces next to the
   contract check that already forbids such a state from asking for them. */
.character[data-feature-overlays="false"] .mouth,
.character[data-feature-overlays="false"] .eye,
.character[data-feature-overlays="false"] .cry-tear { display: none; }

/* ── 瞬き ──────────────────────────────────────────────────────────────────
   The blink is made out of the character's own pixels. A copy of the base frame,
   lined up with it exactly and clipped to one eye, is squeezed onto the line
   where that eye's lids meet; the skin it uncovers is an opaque lid the pack
   measured on this face, and the closed lash is a short tapered stroke sitting
   on the same line. Nothing is retouched while the eyes are open: the whole
   overlay is hidden outside a blink, so neutral is the untouched artwork.
   Close, hold and open are keyframe animations rather than transitions, so the
   eye is never left parked halfway. */

.eye { position: absolute; pointer-events: none; visibility: hidden; }
.character.is-blinking .eye { visibility: visible; }

/* The lid and the aperture are the same ellipse, so the skin and the squeezed
   eye have one silhouette and no seam can open up between them. Its size and
   offset come from the pack, because how much clear skin surrounds an eye - and
   how close the hair and the brow come to it - is a property of the face.
   --lid-core is where the ellipse stops being opaque; the runtime derives it
   from the pack's feather so the two can never drift apart. */
.eye-lid, .eye-aperture {
  position: absolute;
  left: var(--lid-left); top: var(--lid-top);
  width: var(--lid-width); height: var(--lid-height);
  -webkit-mask-image: radial-gradient(ellipse closest-side at 50% 50%, #000 0 var(--lid-core, 88%), transparent 100%);
  mask-image: radial-gradient(ellipse closest-side at 50% 50%, #000 0 var(--lid-core, 88%), transparent 100%);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

.eye-lid {
  opacity: 0;
  background: linear-gradient(180deg, var(--lid-top-color, #f7d0b9) 0%, var(--lid-mid-color, #fbd7c1) 46%, var(--lid-bottom-color, #fcd9c2) 100%);
}

.eye-aperture { overflow: hidden; }

/* The eye itself, straight out of the base frame at the same size and position,
   so at rest this layer is the artwork and nothing else.
   The order of these two elements matters. .eye-squash crops the frame down to
   this one eye FIRST and is squeezed afterwards, so only the eye is ever drawn
   into the aperture. Squeezing the frame first and cropping after would pull the
   whole head and body into the eye socket. Its transform-origin is the line the
   lids meet on, so the aperture closes onto that line from above and below at the
   same time: the skin is uncovered on both sides at once and never appears as a
   band floating over the face. */
.eye-squash {
  position: absolute; inset: 0; overflow: hidden;
  transform-origin: 50% var(--squash-origin, 50%);
}

.eye-art {
  position: absolute;
  left: var(--art-left); top: var(--art-top);
  width: var(--art-width); height: var(--art-height);
}

/* The closed lash: an ellipse minus the same ellipse pushed down, which leaves a
   stroke that is thickest in the middle and tapers away into both corners,
   arched the way this character's upper lash is drawn. It pivots on its own top
   edge so the pack's lean tilts the stroke instead of moving it. */
.eye-lash {
  position: absolute; opacity: 0;
  left: var(--lash-left); top: var(--lash-top);
  width: var(--lash-width); height: var(--lash-height);
  rotate: var(--lash-rotate, 0deg);
  transform-origin: 50% 0;
  border-radius: 50%;
  background: var(--lash-color, #2b1a12);
  -webkit-mask-image: radial-gradient(ellipse 50% 50% at 50% calc(50% + var(--lash-weight, 30%)), transparent 0 97%, #000 100%);
  mask-image: radial-gradient(ellipse 50% 50% at 50% calc(50% + var(--lash-weight, 30%)), transparent 0 97%, #000 100%);
}

/* One quick human blink at the pack's duration: about 80ms closing, a 29ms hold,
   then 72ms opening. The lash only fades in once the aperture is nearly shut, so
   it is always inside the remaining eye and never reads as a line drawn across
   an open one. */
.character.is-blinking .eye-squash { animation: wm-blink-aperture var(--blink-duration, 180ms) both; }
.character.is-blinking .eye-lid { animation: wm-blink-lid var(--blink-duration, 180ms) both; }
.character.is-blinking .eye-lash { animation: wm-blink-lash var(--blink-duration, 180ms) both; }

@keyframes wm-blink-aperture {
  0% { transform: scaleY(1); animation-timing-function: cubic-bezier(.55, .05, .85, .5); }
  44%, 60% { transform: scaleY(0); animation-timing-function: cubic-bezier(.2, .6, .4, 1); }
  100% { transform: scaleY(1); }
}
@keyframes wm-blink-lid {
  0%, 94% { opacity: 1; }
  100% { opacity: 0; }
}
@keyframes wm-blink-lash {
  0%, 32% { opacity: 0; }
  44%, 60% { opacity: 1; }
  74%, 100% { opacity: 0; }
}

/* ── 泣く ──────────────────────────────────────────────────────────────────
   The runtime places these streams from the measured eye boxes. Their shape is
   reusable CSS, so crying needs neither a new full-frame render nor two new
   hand-tuned coordinates for every character. */
.cry-tear {
  position: absolute; opacity: 0; pointer-events: none; z-index: 3;
  transform-origin: 50% 0;
  filter: drop-shadow(0 2px 3px rgba(25, 95, 145, .34));
}
.cry-tear::before {
  content: ""; position: absolute; left: 36%; top: 0; width: 28%; height: 72%;
  border-radius: 999px;
  background: linear-gradient(90deg, rgba(228, 251, 255, .42), rgba(126, 211, 246, .95) 46%, rgba(239, 253, 255, .72));
  box-shadow: inset 1px 0 0 rgba(255, 255, 255, .72);
}
.cry-tear::after {
  content: ""; position: absolute; left: 18%; bottom: 2%; width: 64%; aspect-ratio: 1;
  border-radius: 62% 38% 66% 34%; rotate: 45deg;
  background: linear-gradient(135deg, #e9fbff 4%, #8bd8f6 58%, #5fb9e7 100%);
}
.character[data-expression="crying"] .cry-tear { opacity: 1; }
.character[data-expression="crying"] .cry-tear-left { animation: wmv-tear-flow 1.1s ease-in infinite; }
.character[data-expression="crying"] .cry-tear-right { animation: wmv-tear-flow 1.1s .32s ease-in infinite; }

@keyframes wmv-tear-flow {
  0% { opacity: .78; transform: translateY(-5%) scaleY(.72); }
  32% { opacity: 1; transform: translateY(0) scaleY(1); }
  78% { opacity: .92; transform: translateY(8%) scaleY(1.08); }
  100% { opacity: .74; transform: translateY(14%) scaleY(.82); }
}

/* ── 笑顔 / 喜ぶ: cheek blush ── */
.blush {
  position: absolute; opacity: 0; pointer-events: none; border-radius: 50%;
  background: radial-gradient(ellipse 50% 50% at 50% 50%, var(--blush-color, rgba(255, 122, 122, .4)) 0%, transparent 72%);
  transition: opacity .2s ease-out;
}
.character[data-expression="happy"] .blush,
.character[data-expression="joyful"] .blush { opacity: 1; }

/* ── Reaction marks ────────────────────────────────────────────────────────
   Two kinds. Free marks sit in the corners of the canvas, where no face ever is.
   Anchored marks (.mark-accent) are placed by the runtime from the pack's
   `accents` map, because "beside the raised hand" is a different place on every
   character and in every pose; without a declared anchor they stay hidden. */
.reaction { position: absolute; inset: 0; pointer-events: none; }
.mark {
  position: absolute; opacity: 0; font-weight: 900; line-height: 1;
  transform: translateY(6%) scale(.7); transition: opacity .18s ease-out, transform .22s cubic-bezier(.16, 1, .3, 1);
}
.mark-accent { aspect-ratio: 1; translate: -50% -50%; }
.mark-accent i { position: absolute; display: block; }

.mark-surprise {
  top: 7%; font-size: min(13vmin, 74px); color: #ffd23f;
  text-shadow: 0 0 10px rgba(0, 0, 0, .55), 0 3px 0 rgba(140, 80, 0, .55);
}
.mark-surprise-left { left: 5%; rotate: -14deg; }
.mark-surprise-right { right: 5%; rotate: 13deg; }
.mark-impressed {
  top: 8%; right: 1%; padding: .35em .6em; border-radius: 999px 999px 999px 4px;
  color: #12202c; background: #fff; font-size: min(8.4vmin, 44px);
  box-shadow: 0 8px 22px rgba(0, 0, 0, .34);
}
.mark-sparkle { top: 11%; font-size: min(9vmin, 50px); color: #ffe98a; text-shadow: 0 0 12px rgba(255, 216, 120, .8); }
.mark-sparkle-left { left: 6%; }
.mark-sparkle-right { right: 6%; top: 17%; }

/* 首をかしげる */
.mark-question {
  top: 6%; right: 4%; font-size: min(13vmin, 72px); color: #ffd23f; rotate: 9deg;
  text-shadow: 0 0 10px rgba(0, 0, 0, .5), 0 3px 0 rgba(140, 80, 0, .5);
}

/* 喜ぶ: several light marks drifting at different speeds. Deliberately unlike
   ガッツポーズ, which gets one hard burst at each fist instead. */
.mark-cheer { font-size: min(9.5vmin, 52px); text-shadow: 0 0 14px rgba(255, 226, 150, .85); }
.mark-cheer-1 { left: 5%; top: 9%; color: #ffe98a; }
.mark-cheer-2 { right: 5%; top: 6%; color: #a8f0ff; }
.mark-cheer-3 { right: 11%; top: 26%; color: #ffc7e6; font-size: min(7vmin, 38px); }

/* いくぞー！: speed streaks rising off the raised fist. */
.mark-streak i {
  height: 9%; border-radius: 999px; rotate: -32deg;
  background: linear-gradient(90deg, rgba(255, 204, 77, 0) 0%, #ffcc4d 55%, #fff0bf 100%);
}
.mark-streak i:nth-child(1) { left: 0; top: 62%; width: 72%; }
.mark-streak i:nth-child(2) { left: 17%; top: 38%; width: 58%; }
.mark-streak i:nth-child(3) { left: 34%; top: 14%; width: 44%; }

/* 手を振る: two crescents beside the waving hand. A single static frame cannot
   move an arm, so the motion is in these marks and in the body's rock. */
.mark-arc i {
  inset: 0; border-radius: 50%;
  background: radial-gradient(closest-side, transparent 0 66%, #7fdcff 71% 86%, transparent 92%);
  clip-path: inset(0 62% 0 0);
}
.mark-arc i:nth-child(2) { inset: -26%; opacity: .55; }

/* 指差し: a ring around what the finger is aimed at. */
.mark-target {
  border-radius: 50%;
  background: radial-gradient(closest-side, transparent 0 60%, #42f5b0 65% 80%, transparent 88%);
}
.mark-target i {
  inset: 39%; border-radius: 50%; background: #42f5b0;
  box-shadow: 0 0 14px rgba(66, 245, 176, .85);
}

/* 考える: three thought dots climbing away from the head. */
.mark-think i { border-radius: 50%; background: #eef4ff; box-shadow: 0 2px 10px rgba(0, 0, 0, .35); }
.mark-think i:nth-child(1) { left: 2%; top: 66%; width: 16%; height: 16%; }
.mark-think i:nth-child(2) { left: 27%; top: 34%; width: 24%; height: 24%; }
.mark-think i:nth-child(3) { left: 57%; top: 2%; width: 34%; height: 34%; }

/* ガッツポーズ: a short hard burst at each fist. */
.mark-burst i {
  left: 50%; top: 0; width: 9%; height: 50%; margin-left: -4.5%;
  border-radius: 999px; transform-origin: 50% 100%;
  background: linear-gradient(to top, rgba(255, 210, 63, 0) 0%, #ffd23f 48%, #fff4c8 100%);
}
.mark-burst i:nth-child(1) { rotate: -66deg; height: 40%; }
.mark-burst i:nth-child(2) { rotate: -24deg; }
.mark-burst i:nth-child(3) { rotate: 20deg; }
.mark-burst i:nth-child(4) { rotate: 58deg; height: 42%; }

/* 拍手: applause arcs on the outside of each hand. */
.mark-clap i {
  inset: 0; border-radius: 50%;
  background: radial-gradient(closest-side, transparent 0 64%, #cdf6ff 70% 86%, transparent 92%);
  filter: drop-shadow(0 0 6px rgba(0, 40, 70, .5));
}
.mark-clap i:nth-child(2) { inset: -28%; opacity: .5; }
.mark-clap-left i { clip-path: inset(0 58% 0 0); }
.mark-clap-right i { clip-path: inset(0 0 0 58%); }

/* 謝る: one cool sweat drop at the temple. The square's sharp corner is rotated
   to the top, which is what turns it into a drop. */
.mark-sweat {
  border-radius: 0 50% 50% 50%; rotate: -45deg;
  background: linear-gradient(150deg, #f2fbff 0%, #9ed6f5 55%, #6ab6e4 100%);
  box-shadow: 0 2px 10px rgba(0, 20, 40, .35);
}

.character[data-expression="surprised"] .mark-surprise,
.character[data-expression="impressed"] .mark-impressed,
.character[data-expression="happy"] .mark-sparkle,
.character[data-expression="inquisitive"] .mark-question,
.character[data-expression="joyful"] .mark-cheer,
.character[data-expression="eager"] .mark-streak[data-placed="true"],
.character[data-expression="friendly"] .mark-arc[data-placed="true"],
.character[data-expression="focused"] .mark-target[data-placed="true"],
.character[data-expression="thoughtful"] .mark-think[data-placed="true"],
.character[data-expression="triumphant"] .mark-burst[data-placed="true"],
.character[data-expression="appreciative"] .mark-clap[data-placed="true"],
.character[data-expression="apologetic"] .mark-sweat[data-placed="true"] {
  opacity: 1; transform: none;
}

/* Marks that keep moving while their state is held. Visibility never depends on
   these: every mark is fully drawn with the animation switched off, which is what
   makes the reduced-motion view a complete still of the state. */
.character[data-expression="eager"] .mark-streak i { animation: wmv-accent-rise 1s ease-out infinite; }
.character[data-expression="eager"] .mark-streak i:nth-child(2) { animation-delay: .12s; }
.character[data-expression="eager"] .mark-streak i:nth-child(3) { animation-delay: .24s; }
.character[data-expression="friendly"] .mark-arc i { animation: wmv-accent-pulse .9s ease-in-out infinite; }
.character[data-expression="friendly"] .mark-arc i:nth-child(2) { animation-delay: .18s; }
.character[data-expression="focused"] .mark-target { animation: wmv-accent-ping 1.4s ease-out infinite; }
.character[data-expression="thoughtful"] .mark-think i { animation: wmv-accent-breathe 2.4s ease-in-out infinite; }
.character[data-expression="thoughtful"] .mark-think i:nth-child(2) { animation-delay: .3s; }
.character[data-expression="thoughtful"] .mark-think i:nth-child(3) { animation-delay: .6s; }
.character[data-expression="triumphant"] .mark-burst i { animation: wmv-accent-flare 2.6s ease-out infinite; }
.character[data-expression="appreciative"] .mark-clap i { animation: wmv-accent-pulse .56s ease-out infinite; }
.character[data-expression="joyful"] .mark-cheer { animation: wmv-accent-drift 2.2s ease-in-out infinite alternate; }
.character[data-expression="joyful"] .mark-cheer-2 { animation-duration: 1.7s; animation-delay: .2s; }
.character[data-expression="joyful"] .mark-cheer-3 { animation-duration: 2.8s; animation-delay: .45s; }
@keyframes wmv-accent-rise {
  0% { opacity: 0; transform: translate(-14%, 26%); }
  30%, 62% { opacity: 1; transform: none; }
  100% { opacity: 0; transform: translate(14%, -26%); }
}
@keyframes wmv-accent-pulse {
  0%, 100% { opacity: .35; transform: scale(.82); }
  45% { opacity: 1; transform: scale(1.06); }
}
@keyframes wmv-accent-ping {
  0% { transform: scale(.84); opacity: .75; }
  55% { transform: scale(1.06); opacity: 1; }
  100% { transform: scale(.84); opacity: .75; }
}
@keyframes wmv-accent-breathe {
  0%, 100% { opacity: .45; transform: translateY(8%); }
  45% { opacity: 1; transform: none; }
}
@keyframes wmv-accent-flare {
  0% { opacity: 1; transform: scaleY(.55); }
  9% { opacity: 1; transform: scaleY(1.12); }
  26% { opacity: .5; transform: scaleY(.8); }
  40%, 100% { opacity: .28; transform: scaleY(.62); }
}
@keyframes wmv-accent-drift {
  from { transform: translateY(4%) rotate(-6deg); }
  to { transform: translateY(-8%) rotate(7deg); }
}

/* ── Gestures move the whole stack, never a body part ──────────────────────
   One driver, one catalogue. The driver takes the keyframes the state's gesture
   selects and the duration / repeat count the pack declares for it, so adding a
   character with different timings needs no change here. The `is-gesturing`
   class is removed and re-added on every state application, which is what makes
   re-picking the same action restart its motion from the first frame. */
.character.is-gesturing .stack {
  animation-name: var(--gesture-keyframes);
  animation-duration: var(--gesture-duration, 1.8s);
  animation-timing-function: var(--gesture-easing, ease-in-out);
  animation-iteration-count: var(--gesture-iterations, infinite);
  animation-fill-mode: both;
}

.character[data-gesture="jolt"] { --gesture-keyframes: wmv-gesture-jolt; --gesture-easing: ease-out; }
.character[data-gesture="nod"] { --gesture-keyframes: wmv-gesture-nod; }
.character[data-gesture="bounce"] { --gesture-keyframes: wmv-gesture-bounce; }
.character[data-gesture="charge"] { --gesture-keyframes: wmv-gesture-charge; --gesture-easing: ease-out; }
.character[data-gesture="nod-once"] { --gesture-keyframes: wmv-gesture-nod-once; }
.character[data-gesture="tilt"] { --gesture-keyframes: wmv-gesture-tilt; }
.character[data-gesture="wave"] { --gesture-keyframes: wmv-gesture-wave; }
.character[data-gesture="point"] { --gesture-keyframes: wmv-gesture-point; --gesture-easing: ease-out; }
.character[data-gesture="sway"] { --gesture-keyframes: wmv-gesture-sway; }
.character[data-gesture="victory-pop"] { --gesture-keyframes: wmv-gesture-victory-pop; --gesture-easing: ease-out; }
.character[data-gesture="bow"] { --gesture-keyframes: wmv-gesture-bow; }
.character[data-gesture="bow-deep"] { --gesture-keyframes: wmv-gesture-bow-deep; }
.character[data-gesture="celebrate"] { --gesture-keyframes: wmv-gesture-celebrate; }
.character[data-gesture="clap"] { --gesture-keyframes: wmv-gesture-clap; --gesture-easing: ease-out; }
.character[data-gesture="sob"] { --gesture-keyframes: wmv-gesture-sob; }
.character[data-gesture="fume"] { --gesture-keyframes: wmv-gesture-fume; --gesture-easing: ease-out; }

/* Motions that pivot rather than translate need the hinge low in the frame. */
.character[data-gesture="tilt"] .stack,
.character[data-gesture="wave"] .stack,
.character[data-gesture="sway"] .stack,
.character[data-gesture="celebrate"] .stack { transform-origin: 50% 96%; }
.character[data-gesture="bow"] .stack,
.character[data-gesture="bow-deep"] .stack { transform-origin: 50% 100%; }

@keyframes wmv-idle-float { to { transform: translateY(-7px) rotate(.5deg); } }
@keyframes wmv-pose-cutover {
  0%, 11.9% { opacity: 0; }
  12%, 87.9% { opacity: 1; }
  88%, 100% { opacity: 0; }
}
@keyframes wmv-pose-underlay-cutover {
  0%, 11.9% { opacity: 1; }
  12%, 87.9% { opacity: 0; }
  88%, 100% { opacity: 1; }
}
@keyframes wmv-gesture-jolt {
  0% { transform: none; }
  6% { transform: translateY(-3.2%) scale(1.022); }
  14% { transform: translateY(.6%) scale(.996); }
  22% { transform: none; }
  100% { transform: none; }
}
@keyframes wmv-gesture-nod {
  0% { transform: none; }
  18% { transform: translateY(2.2%) rotate(2.1deg); }
  40% { transform: translateY(0) rotate(0deg); }
  100% { transform: none; }
}
@keyframes wmv-gesture-bounce {
  0%, 100% { transform: none; }
  50% { transform: translateY(-1.9%) scale(1.008); }
}
/* いくぞー！ - one hard forward beat, a smaller echo, then a breath of rest. */
@keyframes wmv-gesture-charge {
  0% { transform: none; animation-timing-function: cubic-bezier(.2, .9, .3, 1); }
  9% { transform: translateY(-4.4%) scale(1.035) rotate(-1.5deg); }
  20% { transform: translateY(.9%) scale(.992) rotate(.5deg); }
  30% { transform: none; animation-timing-function: ease-out; }
  40% { transform: translateY(-1.8%) scale(1.013); }
  54%, 100% { transform: none; }
}
/* うなずく - a firm nod and a small second dip, then still. */
@keyframes wmv-gesture-nod-once {
  0% { transform: none; animation-timing-function: cubic-bezier(.4, 0, .35, 1); }
  30% { transform: translateY(3.6%) rotate(2.8deg); }
  54% { transform: translateY(0) rotate(0deg); }
  74% { transform: translateY(1.7%) rotate(1.3deg); }
  100% { transform: none; }
}
/* 首をかしげる - lean, hold the question, come back. */
@keyframes wmv-gesture-tilt {
  0% { transform: none; animation-timing-function: cubic-bezier(.3, .7, .3, 1); }
  28%, 66% { transform: rotate(-7.5deg) translateY(.5%); }
  100% { transform: none; }
}
@keyframes wmv-gesture-wave {
  0%, 100% { transform: rotate(-1.8deg) translateY(-.3%); }
  50% { transform: rotate(2.1deg) translateY(-1.3%); }
}
@keyframes wmv-gesture-point {
  0%, 100% { transform: none; }
  18% { transform: translateX(-2.4%) scale(1.012); }
  42% { transform: translateX(.5%); }
}
@keyframes wmv-gesture-sway {
  0%, 100% { transform: rotate(-.8deg) translateY(.2%); }
  50% { transform: rotate(.8deg) translateY(-.6%); }
}
/* ガッツポーズ - one strong pop, one echo, then a long hold. */
@keyframes wmv-gesture-victory-pop {
  0% { transform: none; animation-timing-function: cubic-bezier(.16, 1, .3, 1); }
  7% { transform: translateY(-5.6%) scale(1.045); }
  18% { transform: translateY(1.1%) scale(.99); }
  27% { transform: none; animation-timing-function: ease-out; }
  35% { transform: translateY(-2.1%) scale(1.016); }
  46%, 100% { transform: none; }
}
@keyframes wmv-gesture-bow {
  0% { transform: translateY(-2.4%) scale(.988); animation-timing-function: cubic-bezier(.22, 1, .36, 1); }
  30%, 62% { transform: translateY(1.2%) scale(1.002); }
  100% { transform: translateY(-1%) scale(.994); }
}
/* 謝る - the frame itself is a deeper posture; motion settles farther and holds longer. */
@keyframes wmv-gesture-bow-deep {
  0% { transform: translateY(-3.6%) scale(.982); animation-timing-function: cubic-bezier(.22, 1, .36, 1); }
  28%, 76% { transform: translateY(2.2%) scale(1.006); }
  100% { transform: translateY(-1.4%) scale(.99); }
}
/* 喜ぶ - a continuous springy bounce, unlike the single pop of ガッツポーズ. */
@keyframes wmv-gesture-celebrate {
  0%, 100% { transform: translateY(0) rotate(-1.2deg); }
  50% { transform: translateY(-3.4%) rotate(1.2deg) scale(1.014); }
}
@keyframes wmv-gesture-clap {
  0% { transform: none; animation-timing-function: cubic-bezier(.3, .8, .4, 1); }
  18% { transform: translateY(-1.6%) scale(1.016); }
  42% { transform: translateY(.5%) scale(.995); }
  62%, 100% { transform: none; }
}
/* 泣く - shoulders sink while a small uneven tremble repeats. */
@keyframes wmv-gesture-sob {
  0%, 100% { transform: translateY(.7%) rotate(0deg) scale(.995); }
  14% { transform: translate(-.45%, 1.25%) rotate(-.35deg) scale(.991); }
  28% { transform: translate(.38%, .85%) rotate(.28deg) scale(.994); }
  46% { transform: translate(-.28%, 1.55%) rotate(-.22deg) scale(.989); }
  64% { transform: translate(.22%, 1.05%) rotate(.16deg) scale(.992); }
  82% { transform: translateY(1.45%) scale(.99); }
}

/* 怒る - a tight, impatient shake with a brief upward emphasis. */
@keyframes wmv-gesture-fume {
  0%, 100% { transform: none; }
  14% { transform: translate(-.7%, -.7%) rotate(-.35deg) scale(1.006); }
  28% { transform: translate(.7%, -1%) rotate(.35deg) scale(1.009); }
  42% { transform: translate(-.55%, -.55%) rotate(-.25deg) scale(1.004); }
  58% { transform: translate(.45%, -.75%) rotate(.2deg) scale(1.006); }
  76% { transform: translateY(-.25%) scale(1.002); }
}

@media (prefers-reduced-motion: reduce) {
  /* Nothing decorative moves, and the state is still complete: the pose frame,
     the marks and the cheeks are all drawn, only their motion is gone. */
  .character, .character *, .character *::before, .character *::after { animation: none !important; }
  .blush, .mark { transition: none !important; }
  /* The dedicated pose is itself the complete reduced-motion result. */
  .character[data-pose-transition="true"] .pose-underlay { display: none; }
  /* With the animation switched off the blink becomes a cut: fully open or fully
     closed, never an in-between frame. */
  .character.is-blinking .eye-squash { transform: scaleY(0); }
  .character.is-blinking .eye-lid, .character.is-blinking .eye-lash { opacity: 1; }
}
