/* AVI - the animated, voice-driven face of A.V.I.S AI. One shared widget (avi-widget.js loads this
   file), included on every signed-in/marketplace page so he can never drift out of sync between
   mounts the way five separate hand-copied implementations previously did.

   Reported directly: he should not "sit in a window" - so there is no dialog/modal chrome here.
   He is a small character fixed to the page (position:fixed, moved by translateX) who paces
   around on his own, turns his head toward the pointer, gestures and flaps his mouth while
   talking, and settles down to "read" when idle. Chat surfaces as a bubble above his head that
   fades out slowly after each line - never a persistent scrollback window covering his face.

   Reported directly (again, after Phase 3 shipped): "he does no action to type... mostly his face
   is covered by this writing window... microphone should be by default ON... this is important by
   car driving... the language selector should disappear... going standby but listening" - the
   voice/language picker is gone entirely (see populateVoices() and speak() in avi-widget.js, which
   now choose a voice silently), and the old always-visible settings+input panel is now a compact,
   normally-hidden surface (.avi-panel) that only opens for a pending confirm or an explicit "type
   instead" tap - conversation itself is mic-in/bubble-out by default, continuously listening (see
   startListening()'s continuous:true + auto-restart), so he never has to be chased or clicked to
   talk to. .avi-mic-badge is the one small, always-visible indicator of that listening state. */
:root {
  --avi-accent: #7f172a;
  --avi-ink: #201e20;
  --avi-line: #e9e4e5;
}

/* Codex review finding (P2): the mail platform's own sign-in state is body.login-active (see
   showLogin()/hideLogin() in platform/app.js) - unlike admin.mjs's data-avi-mount gate, this
   widget otherwise mounts unconditionally, so a signed-out visitor there saw AVI floating over
   the sign-in dialog even though every message would fail (mail.avis.land isn't one of the
   guest-access marketplace hosts in src/avi.mjs, so it always 401s there). This class doesn't
   exist on admin/marketplace/exchange/shop pages, so the rule is a no-op everywhere else. */
body.login-active .avi-character,
body.login-active .avi-panel {
  display: none;
}

.avi-character {
  position: fixed;
  left: 0;
  /* Clears the fixed footer/fly-control bars several avis.land pages have along the bottom edge
     (e.g. the city page's 32px division menu) without needing per-page tuning. --avi-bottom is
     the drag-to-reposition system's own handle on this (see avi-widget.js) - untouched by the
     autonomous walk, which only ever moves --avi-x, so a dragged height sticks even as he keeps
     pacing left/right on his own afterward. */
  bottom: var(--avi-bottom, 46px);
  z-index: 6000;
  width: 96px;
  transform: translateX(var(--avi-x, 20px));
  transition: transform var(--avi-walk-ms, 900ms) ease-in-out, bottom 0.4s ease;
  cursor: grab;
  /* A draggable element needs the browser's own touch panning/zooming out of the way entirely -
     manipulation still let a touch drag start scrolling the page underneath instead. */
  touch-action: none;
  -webkit-tap-highlight-color: transparent;
}
/* Reported directly: "his face is covered by this writing window" - walking him sideways to
   conversationX() (see avi-widget.js) is only ever enough to clear the panel on a wide viewport;
   on a narrow one .avi-panel goes full-width (see its own media query below) and would sit right
   over him regardless of x. Lifted clear of the panel's own height whenever it's actually open -
   a plain, viewport-independent guarantee instead of one more width calculation to keep in sync. */
.avi-character.is-panel-open { bottom: 260px; }
@media (max-width: 640px) {
  .avi-character.is-panel-open { bottom: 300px; }
}
/* Reported directly: "if he is covering some part of the screen... I can move him away" - a real
   press-and-drag, not just the autonomous pacing. No easing while an actual drag is in progress
   (transitions are for HIS OWN movement, not for tracking a pointer 1:1), and the grab cursor
   flips to grabbing so it reads as a real hand-hold rather than a click waiting to happen. */
.avi-character.is-dragging { transition: none; cursor: grabbing; }
.avi-character:focus-visible .avi-sprite { outline: 2px solid var(--avi-accent); outline-offset: 3px; border-radius: 50%; }

/* Reported directly: "make him a backside as well so we can twist him" - a standard CSS 3D
   flip-card. .avi-turn holds BOTH faces and is the only element that ever gets the full 180deg
   turn; .avi-sprite (the front, unchanged) and .avi-face-back (the synthesized back - see
   avi-widget.mjs's own comment on how it was made) are each backface-visibility:hidden, with
   .avi-face-back pre-rotated 180deg so the two exactly swap which one faces the viewer as .avi-
   turn itself rotates - never both, never neither. Nested inside .avi-character's own perspective
   (see below), composing with whatever .avi-sprite's own walk/idle/talk animation is doing, the
   same nested-transform trick already used for the hip/knee and shoulder/elbow joints above. */
.avi-turn {
  position: relative;
  width: 100%;
  transform-style: preserve-3d;
  transition: transform 0.7s ease-in-out;
}
.avi-character.is-turned .avi-turn { transform: rotateY(180deg); }
.avi-face-back {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  backface-visibility: hidden;
  transform: rotateY(180deg);
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}

.avi-sprite {
  position: relative;
  width: 100%;
  transform-origin: 50% 100%;
  backface-visibility: hidden;
  animation: avi-idle-bob 3.4s ease-in-out infinite;
  filter: drop-shadow(0 10px 14px rgba(20, 12, 14, 0.28));
}
/* A CSS depth cue, not a WebGL model: perspective on the character (the direct parent of the
   transformed sprite) makes rotateY in avi-walk-cycle below actually foreshorten instead of just
   squashing flat, so the weight shift from leg to leg during a stride reads as a real, if subtle,
   turn rather than a paper cutout swaying in place. */
.avi-character { perspective: 320px; }
.avi-character.is-walking .avi-sprite { animation: avi-walk-cycle 0.6s ease-in-out infinite; }
.avi-character.is-talking .avi-sprite { animation: avi-talk-gesture 0.42s ease-in-out infinite; }
.avi-character.is-thinking .avi-sprite { animation: avi-idle-bob 1s ease-in-out infinite; }
.avi-character.is-sleepy .avi-sprite { animation: avi-idle-bob 5.5s ease-in-out infinite; }
/* Task #78: the real, user-invoked "go to sleep" state - visibly at rest and dimmed, distinct
   from the automatic is-sleepy fatigue animation above, which still wakes itself back up on a
   timer. Never fully hidden or unclickable - a single tap is still the way out of it. */
.avi-character.is-dormant { opacity: 0.45; transition: opacity 0.6s ease; }
.avi-character.is-dormant .avi-sprite { animation: none; }

@keyframes avi-idle-bob { 0%, 100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-5px) rotate(-1deg); } }
/* Matches the 5-phase hip/knee gait below: the body settles slightly at each foot-strike (0%/50%/
   100% - a leg landing forward) and rises as the other leg swings through underneath (25%/75%),
   with a gentle rotateY turn toward whichever leg just planted - a real double-bounce walk, not a
   single side-to-side rock. */
@keyframes avi-walk-cycle {
  0% { transform: translateY(0) rotate(-2deg) rotateY(-6deg); }
  25% { transform: translateY(-7px) rotate(0deg) rotateY(0deg); }
  50% { transform: translateY(0) rotate(2deg) rotateY(6deg); }
  75% { transform: translateY(-7px) rotate(0deg) rotateY(0deg); }
  100% { transform: translateY(0) rotate(-2deg) rotateY(-6deg); }
}
@keyframes avi-talk-gesture { 0%, 100% { transform: translateY(0) scale(1) rotate(0deg); } 50% { transform: translateY(-4px) scale(1.025) rotate(1.2deg); } }

/* A subtle "look toward the pointer" turn, set by JS as a CSS variable and eased independently of
   the bob/walk/talk keyframes above (each nesting level owns exactly one transform source, so
   they compose instead of fighting over the same property). */
.avi-look {
  position: relative;
  width: 100%;
  transform: rotate(var(--avi-look-rotate, 0deg));
  transition: transform 0.5s ease-out;
}

.avi-frame {
  display: block;
  width: 100%;
  height: auto;
  position: relative;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}
.avi-frame-open, .avi-frame-closed {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 0.09s linear;
}
.avi-character.is-talking .avi-frame-rest { opacity: 0; }
.avi-character.is-talking .avi-frame-open.is-active,
.avi-character.is-talking .avi-frame-closed.is-active { opacity: 1; }

/* Reported directly: "talking and walking and hand gestures, eye & head moves and mouth movements
   conform talking" - six cutout-puppet part layers, cropped from avi-full.webp's own original
   pixels (see avi-widget.mjs and avi-widget.js's own file-header comments for exactly how), laid
   over the SAME base image with those regions feather-erased. Positions/sizes below are the
   source 489x900 illustration's own crop boxes expressed as percentages of the sprite's box (the
   .avi-frame-rest image establishes that box - see .avi-frame above), so they track the sprite at
   any render size (96px, 76px on mobile, or the print-quality asset the resolution-independent %
   figures are actually cropped from). transform-origin on each part is that limb's own real pivot
   (hip for a leg, shoulder for an arm) - the whole reason a rotation reads as a real walk/gesture
   instead of a nonsensical slide. Always rendered (see avi-widget.js) - at rest (no animation
   active, the default for every state below except where named) every part sits at its own exact
   original offset, reconstructing the original illustration exactly.
   */
.avi-part { position: absolute; display: block; width: 100%; height: 100%; pointer-events: none; user-select: none; -webkit-user-drag: none; }
.avi-eye { width: 14.315%; height: 7.778%; transform: translate(var(--avi-eye-dx, 0px), var(--avi-eye-dy, 0px)); transition: transform 0.18s ease-out; }
.avi-eye-left { left: 18.814%; top: 30.889%; }
.avi-eye-right { left: 48.466%; top: 33.111%; }
/* Reported directly: "really walking... not just a little zapping" - each leg is now two nested
   parts instead of one rigid piece rocking from the hip alone: .avi-leg (unchanged box/pivot - the
   hip) holds .avi-thigh directly and .avi-knee (the knee's own pivot box) nested inside it, so a
   knee-bend rotation on .avi-knee composes automatically with whatever the hip is already doing -
   a real two-joint chain, not two rotations a script would have to add together by hand. The
   58%/42% split is the exact pixel cut avi-widget.mjs's own comment describes, expressed as a
   fraction of the leg's own height so it still tracks the sprite at any render size. */
.avi-leg { width: 18.405%; height: 25%; top: 75%; }
.avi-leg-left { left: 20.45%; transform-origin: 50% 0%; }
.avi-leg-right { left: 35.787%; transform-origin: 50% 0%; }
.avi-thigh, .avi-knee { position: absolute; left: 0; width: 100%; }
.avi-thigh { top: 0; height: 57.778%; pointer-events: none; user-select: none; -webkit-user-drag: none; }
.avi-knee { top: 57.778%; height: 42.222%; transform-origin: 50% 0%; }
.avi-shin { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; user-select: none; -webkit-user-drag: none; }
/* Same nested-joint trick as the legs (.avi-thigh/.avi-knee above), applied to the arms: .avi-elbow
   is the forearm's own pivot box, nested INSIDE .avi-arm rather than beside it, so a bend on it
   composes automatically with whatever the shoulder (.avi-arm-left/right) is already doing. The
   split line sits at the elbow crease already drawn into the original avi-arm-left/right.webp
   pixels (see avi-widget.mjs's own comment) - a straight horizontal cut, so the two side's ratios
   differ (each arm was posed slightly differently) unlike the legs' shared 58%/42%. */
.avi-upperarm, .avi-elbow { position: absolute; left: 0; width: 100%; }
.avi-upperarm { top: 0; pointer-events: none; user-select: none; -webkit-user-drag: none; }
.avi-elbow { transform-origin: 50% 0%; }
.avi-forearm { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; user-select: none; -webkit-user-drag: none; }
.avi-arm { width: 19.427%; height: 16.667%; top: 59.444%; }
.avi-arm-left { left: 10.225%; transform-origin: 70% 5%; }
.avi-arm-right { left: 44.99%; transform-origin: 30% 5%; }
.avi-arm-left .avi-upperarm { height: 38.667%; }
.avi-arm-left .avi-elbow { top: 38.667%; height: 61.333%; }
.avi-arm-right .avi-upperarm { height: 45.333%; }
.avi-arm-right .avi-elbow { top: 45.333%; height: 54.667%; }

/* Walking: a real gait, not a symmetric rock - each leg's hip (.avi-leg-*) and knee (.avi-knee-*,
   nested inside it - see the comment above) run their own keyframes at the same 0.6s beat
   avi-walk-cycle (above) bobs the whole sprite to, phase-shifted a half-cycle apart so they
   alternate; the knee bends through the swing phase (foot clearing the ground as the hip drives
   the leg forward) and straightens again for the planted/push-off phase, the way a real stride's
   weight actually transfers. Arms counter-swing opposite the same-side leg, as they would walking
   for real. */
.avi-character.is-walking .avi-leg-left { animation: avi-hip-left-step 0.6s ease-in-out infinite; }
.avi-character.is-walking .avi-leg-right { animation: avi-hip-right-step 0.6s ease-in-out infinite; }
.avi-character.is-walking .avi-knee-left { animation: avi-knee-left-step 0.6s ease-in-out infinite; }
.avi-character.is-walking .avi-knee-right { animation: avi-knee-right-step 0.6s ease-in-out infinite; }
.avi-character.is-walking .avi-arm-left { animation: avi-arm-left-swing 0.6s ease-in-out infinite; }
.avi-character.is-walking .avi-arm-right { animation: avi-arm-right-swing 0.6s ease-in-out infinite; }
.avi-character.is-walking .avi-elbow-left { animation: avi-elbow-left-swing 0.6s ease-in-out infinite; }
.avi-character.is-walking .avi-elbow-right { animation: avi-elbow-right-swing 0.6s ease-in-out infinite; }
@keyframes avi-hip-left-step { 0% { transform: rotate(14deg); } 25% { transform: rotate(-4deg); } 50% { transform: rotate(-18deg); } 75% { transform: rotate(-4deg); } 100% { transform: rotate(14deg); } }
@keyframes avi-hip-right-step { 0% { transform: rotate(-18deg); } 25% { transform: rotate(-4deg); } 50% { transform: rotate(14deg); } 75% { transform: rotate(-4deg); } 100% { transform: rotate(-18deg); } }
@keyframes avi-knee-left-step { 0% { transform: rotate(2deg); } 25% { transform: rotate(34deg); } 50% { transform: rotate(8deg); } 75% { transform: rotate(0deg); } 100% { transform: rotate(2deg); } }
@keyframes avi-knee-right-step { 0% { transform: rotate(8deg); } 25% { transform: rotate(0deg); } 50% { transform: rotate(2deg); } 75% { transform: rotate(34deg); } 100% { transform: rotate(8deg); } }
@keyframes avi-arm-left-swing { 0%, 100% { transform: rotate(-5deg); } 50% { transform: rotate(5deg); } }
@keyframes avi-arm-right-swing { 0%, 100% { transform: rotate(5deg); } 50% { transform: rotate(-5deg); } }
/* A real elbow bends more as the arm swings, less at its extremes - the same follow-through a real
   arm swing has - rather than the shoulder and elbow rotating as one rigid unit. */
@keyframes avi-elbow-left-swing { 0%, 100% { transform: rotate(10deg); } 50% { transform: rotate(-6deg); } }
@keyframes avi-elbow-right-swing { 0%, 100% { transform: rotate(-10deg); } 50% { transform: rotate(6deg); } }

/* Talking: a subtle, continuous hand gesture at the same 0.42s beat avi-talk-gesture already
   pulses the whole sprite to - legs stay still (he isn't walking while he talks). */
.avi-character.is-talking .avi-arm-left { animation: avi-arm-left-gesture 0.42s ease-in-out infinite; }
.avi-character.is-talking .avi-arm-right { animation: avi-arm-right-gesture 0.42s ease-in-out infinite; }
.avi-character.is-talking .avi-elbow-left { animation: avi-elbow-left-gesture 0.42s ease-in-out infinite; }
.avi-character.is-talking .avi-elbow-right { animation: avi-elbow-right-gesture 0.42s ease-in-out infinite; }
@keyframes avi-arm-left-gesture { 0%, 100% { transform: rotate(-2deg); } 50% { transform: rotate(2.5deg); } }
@keyframes avi-arm-right-gesture { 0%, 100% { transform: rotate(2deg); } 50% { transform: rotate(-2.5deg); } }
@keyframes avi-elbow-left-gesture { 0%, 100% { transform: rotate(4deg); } 50% { transform: rotate(-3deg); } }
@keyframes avi-elbow-right-gesture { 0%, 100% { transform: rotate(-4deg); } 50% { transform: rotate(3deg); } }

/* Reported directly: "the head should be moved arms and hands" - standing idle (none of the modes
   below - is-walking's own gait and is-talking's own gesture already move the arms plenty) used to
   leave both arms perfectly still, reading as frozen rather than alive. A slow (~5s), very small
   sway, timed independently on each arm (4.8s/5.4s, not a mirrored pair on the same beat) so the
   two sides drift in and out of phase with each other instead of looking like one rigid unit. */
.avi-character:not(.is-walking):not(.is-talking):not(.is-thinking):not(.is-sleepy):not(.is-dormant) .avi-arm-left { animation: avi-arm-idle-sway-left 4.8s ease-in-out infinite; }
.avi-character:not(.is-walking):not(.is-talking):not(.is-thinking):not(.is-sleepy):not(.is-dormant) .avi-arm-right { animation: avi-arm-idle-sway-right 5.4s ease-in-out infinite; }
.avi-character:not(.is-walking):not(.is-talking):not(.is-thinking):not(.is-sleepy):not(.is-dormant) .avi-elbow-left { animation: avi-elbow-idle-sway-left 4.8s ease-in-out infinite; }
.avi-character:not(.is-walking):not(.is-talking):not(.is-thinking):not(.is-sleepy):not(.is-dormant) .avi-elbow-right { animation: avi-elbow-idle-sway-right 5.4s ease-in-out infinite; }
@keyframes avi-arm-idle-sway-left { 0%, 100% { transform: rotate(0deg); } 50% { transform: rotate(-1.6deg); } }
@keyframes avi-arm-idle-sway-right { 0%, 100% { transform: rotate(0deg); } 50% { transform: rotate(1.6deg); } }
@keyframes avi-elbow-idle-sway-left { 0%, 100% { transform: rotate(0deg); } 50% { transform: rotate(1.2deg); } }
@keyframes avi-elbow-idle-sway-right { 0%, 100% { transform: rotate(0deg); } 50% { transform: rotate(-1.2deg); } }

/* Reported directly: "when he has nothing to do he is sitting on the floor reading a book,
   imitating." There is no seated-pose artwork to swap to (that needs a real illustrator or video
   vendor, deliberately out of scope for now, see the PR description) - so deep idle is signalled
   by a small CSS-drawn book prop with a page-flip loop near his hands instead, layered on the same
   standing render. */
.avi-book { position: absolute; left: 58%; top: 62%; width: 22px; height: 16px; opacity: 0; transition: opacity 0.4s ease; pointer-events: none; }
.avi-character.is-sleepy .avi-book { opacity: 1; }
.avi-book::before, .avi-book::after { content: ""; position: absolute; top: 0; width: 11px; height: 16px; background: #fbf6ea; border: 1px solid #c9bda0; }
.avi-book::before { left: 0; border-radius: 2px 0 0 2px; transform-origin: right center; animation: avi-book-page 2.6s ease-in-out infinite; }
.avi-book::after { right: 0; border-radius: 0 2px 2px 0; }
@keyframes avi-book-page { 0%, 40% { transform: rotateY(0deg); } 50% { transform: rotateY(-140deg); } 60%, 100% { transform: rotateY(0deg); } }

/* Reported directly: "that with the cloud bubble is also not working looks ugly and text goes out
   of the cloud... Better we bring it back to a light square that expands itself similar as before
   until we will find a good solution." Back to a plain, light, self-expanding rounded-rectangle
   bubble - its box grows with the actual text (width:max-content up to a max-width; height is
   entirely content-driven, no fixed box a long reply could spill past) instead of a fixed
   decorative shape. max-height/overflow-y:auto stays on as a last-resort backstop for a reply so
   long even the expanded box would run off-screen. */
.avi-bubble {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 10px);
  transform: translateX(-50%) translateY(6px);
  min-width: 120px;
  max-width: 240px;
  width: max-content;
  max-height: 260px;
  overflow-y: auto;
  padding: 9px 12px;
  border-radius: 14px;
  background: #fff;
  color: var(--avi-ink);
  font: 12.5px/1.4 Aptos, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  box-shadow: 0 12px 30px rgba(20, 12, 14, 0.22);
  opacity: 0;
  pointer-events: none;
  /* Reported directly: "a real bubble above his head that fades out very slowly" - a long,
     gentle opacity fade (the position settles quickly; only the fade-out itself is slow) reads as
     him letting the last thing he said linger, rather than a UI toast snapping away. */
  transition: opacity 1.4s ease, transform 0.25s ease;
  cursor: default;
  z-index: 1;
}
.avi-bubble::after { content: ""; position: absolute; left: 50%; top: 100%; width: 12px; height: 12px; margin-left: -6px; margin-top: -6px; background: #fff; transform: rotate(45deg); box-shadow: 3px 3px 6px rgba(20, 12, 14, 0.06); }
.avi-bubble.is-visible { opacity: 1; transform: translateX(-50%) translateY(0); }
.avi-bubble.from-user { background: var(--avi-accent); color: #fff; }
.avi-bubble.from-user::after { background: var(--avi-accent); }

/* Reported directly: "microphone should be by default ON... by car driving then by default he
   should listen to speech and responding... going standby but listening" - the one small,
   always-visible indicator of continuous listening, small enough to never cover his face the way
   the old settings panel did. Sits at his feet, tappable to mute/unmute (also mirrored by clicking
   the character itself - see avi-widget.js). Three states: listening (the default "standby but
   listening" state - a soft pulse), muted (tapped off), and speaking (paused while HE talks, so
   the mic never hears and reacts to his own voice). */
.avi-mic-badge {
  position: absolute;
  right: -2px;
  bottom: -2px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--avi-accent);
  color: #fff;
  border: 2px solid #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  box-shadow: 0 3px 8px rgba(20, 12, 14, 0.25);
  cursor: pointer;
  z-index: 2;
}
.avi-mic-badge.is-listening { animation: avi-mic-pulse 1.8s ease-in-out infinite; }
.avi-mic-badge.is-muted { background: #9a9296; }
.avi-mic-badge.is-speaking { background: #4a7a4f; animation: none; }
.avi-mic-badge[hidden] { display: none; }
@keyframes avi-mic-pulse {
  0%, 100% { box-shadow: 0 3px 8px rgba(20, 12, 14, 0.25), 0 0 0 0 rgba(127, 23, 42, 0.35); }
  50% { box-shadow: 0 3px 8px rgba(20, 12, 14, 0.25), 0 0 0 6px rgba(127, 23, 42, 0); }
}

/* The "type instead" fallback - a small keyboard tab near the mic badge, only for a browser with
   no speech recognition or a moment where typing is just easier. Opens the same compact .avi-panel
   the pending-confirm banner already uses, instead of a second UI. */
.avi-type-toggle {
  position: absolute;
  right: 22px;
  bottom: -2px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #fff;
  color: var(--avi-accent);
  border: 1px solid var(--avi-line);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  cursor: pointer;
  z-index: 2;
}

/* Reported directly: "that window must be changed for a real bubble above his head... [no more]
   language selector" - .avi-panel is no longer the default way to talk to him at all. It now only
   ever opens for the "type instead" fallback above or a pending confirm banner (see showConfirm()/
   openTypePanel() in avi-widget.js) - compact, and closed the rest of the time so it never sits
   over his face the way the old always-open settings+chat window did. */
.avi-panel {
  position: fixed;
  right: 16px;
  bottom: 16px;
  z-index: 6001;
  width: min(280px, calc(100vw - 32px));
  border-radius: 16px;
  background: #fff;
  box-shadow: 0 24px 60px rgba(20, 12, 14, 0.3);
  overflow: hidden;
  font: 13px/1.4 Aptos, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  color: var(--avi-ink);
}
.avi-panel[hidden] { display: none; }
.avi-panel-head { display: flex; align-items: center; gap: 8px; padding: 10px 10px 10px 14px; background: var(--avi-accent); color: #fff; }
.avi-panel-head strong { flex: 1; min-width: 0; font-size: 13px; letter-spacing: 0.02em; }
.avi-panel-close { flex: 0 0 auto; width: 26px; height: 26px; border: 0; border-radius: 50%; background: rgba(255, 255, 255, 0.15); color: #fff; font-size: 13px; cursor: pointer; }
.avi-panel-close:hover { background: rgba(255, 255, 255, 0.28); }
.avi-status { padding: 8px 12px 0; font-size: 10.5px; color: #8a8386; }
.avi-confirm { margin: 8px 12px; padding: 9px 10px; border: 1px solid #d8a13a; border-radius: 10px; background: #fff8e8; }
.avi-confirm[hidden] { display: none; }
.avi-confirm p { margin: 0 0 8px; font-size: 12px; color: var(--avi-ink); }
.avi-confirm-actions { display: flex; gap: 8px; }
.avi-confirm-actions button { flex: 1; padding: 7px 8px; border: 0; border-radius: 8px; font: inherit; font-weight: 600; cursor: pointer; background: var(--avi-accent); color: #fff; }
.avi-confirm-actions button.secondary { background: #eee; color: var(--avi-ink); }
.avi-input-row { display: flex; align-items: center; gap: 7px; padding: 10px 12px; }
.avi-input-row input { flex: 1; min-width: 0; padding: 8px 10px; border: 1px solid var(--avi-line); border-radius: 9px; font: inherit; }
.avi-send { flex: 0 0 auto; width: 32px; height: 32px; border-radius: 50%; border: 1px solid var(--avi-line); background: var(--avi-accent); color: #fff; font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
/* Task #77: a real PDF/Word/image can be handed to AVI directly - the attach button matches
   .avi-send's own circular shape/size, just a neutral background since it opens a picker rather
   than submitting anything. */
.avi-attach { flex: 0 0 auto; width: 32px; height: 32px; border-radius: 50%; border: 1px solid var(--avi-line); background: #eee; color: var(--avi-ink); font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
.avi-pending-attachment { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 0 12px 8px; padding: 6px 10px; border: 1px solid var(--avi-line); border-radius: 9px; background: #f4f2f0; font-size: 11.5px; color: var(--avi-ink); }
.avi-pending-attachment span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.avi-pending-attachment[hidden] { display: none; }
.avi-pending-attachment button { flex: 0 0 auto; border: 0; background: none; color: #8a8386; font-size: 13px; cursor: pointer; padding: 0 2px; }

@media (max-width: 640px) {
  .avi-character { width: 76px; }
  .avi-panel { left: 12px; right: 12px; bottom: 12px; width: auto; }
}
@media (prefers-reduced-motion: reduce) {
  .avi-sprite, .avi-character.is-walking .avi-sprite, .avi-character.is-talking .avi-sprite, .avi-character.is-thinking .avi-sprite, .avi-character.is-sleepy .avi-sprite, .avi-character.is-dormant .avi-sprite { animation: none; }
  .avi-character { transition: none; }
  .avi-book::before { animation: none; }
  .avi-eye { transition: none; }
  .avi-look { transition: none; }
  .avi-character.is-walking .avi-leg-left, .avi-character.is-walking .avi-leg-right,
  .avi-character.is-walking .avi-knee-left, .avi-character.is-walking .avi-knee-right,
  .avi-character.is-walking .avi-arm-left, .avi-character.is-walking .avi-arm-right,
  .avi-character.is-walking .avi-elbow-left, .avi-character.is-walking .avi-elbow-right,
  .avi-character.is-talking .avi-arm-left, .avi-character.is-talking .avi-arm-right,
  .avi-character.is-talking .avi-elbow-left, .avi-character.is-talking .avi-elbow-right,
  .avi-character:not(.is-walking):not(.is-talking):not(.is-thinking):not(.is-sleepy):not(.is-dormant) .avi-arm-left,
  .avi-character:not(.is-walking):not(.is-talking):not(.is-thinking):not(.is-sleepy):not(.is-dormant) .avi-arm-right,
  .avi-character:not(.is-walking):not(.is-talking):not(.is-thinking):not(.is-sleepy):not(.is-dormant) .avi-elbow-left,
  .avi-character:not(.is-walking):not(.is-talking):not(.is-thinking):not(.is-sleepy):not(.is-dormant) .avi-elbow-right { animation: none; }
  .avi-mic-badge.is-listening { animation: none; }
  .avi-turn { transition: none; }
}
