/* Motion and status for @thelve/coworkers.
   Optional — the avatars are complete without it.

   Everything here is driven from outside the SVG:

     data-status="working|needs|idle"   on the avatar or any ancestor
     data-av-halos="all|needs"          rings are opt-in; the default is none
     data-av-jump="often|normal|rare|never"   how often needs-you hops
     --av-eye-rate                      eye tempo, 1 = default, 2 = twice as often
     --av-gaze-x     / --av-gaze-y      resting eye direction, -1 … 1
     data-av-off="eyes body"            hard off switches

   Status never touches the SVG, so a roster can change state without any avatar
   being rebuilt, and transitions stay in the compositor. */

/* ---------- the rig ---------- */

.av {
  display: block;
  /* The float, the ring and the cast shadow all reach outside the viewBox. */
  overflow: visible;
}

.av-halo {
  display: none;
  transform-box: view-box;
  transform-origin: 50px 52px;
}

/* Gaze rides on `translate` while the status animations drive `transform`.
   Separate properties, so they compose without a second group. */
.av-look {
  translate: calc(var(--av-gaze-x, 0) * 2.6px) calc(var(--av-gaze-y, 0) * 1.8px);
}

/* ---------- tempo ----------
   Each rule declares --av-cycle on the element that uses it, so the rate is read
   where it lands rather than baked in at :root, and an override on any ancestor
   works. The delay is a fraction of that same cycle: change the rate and the
   spread across a roster rescales with it instead of collapsing. */

.av-body {
  transform-origin: 50px 52px;
  --av-cycle: 5.4s;
  animation: av-float var(--av-cycle) ease-in-out infinite;
  animation-delay: calc(var(--av-phase, 0) * var(--av-cycle) * -1);
}

.av-spec {
  --av-cycle: 5.4s;
  animation: av-spec var(--av-cycle) ease-in-out infinite;
  animation-delay: calc(var(--av-phase, 0) * var(--av-cycle) * -1);
}

.av-eyes {
  --av-cycle: 7.1s;
  animation: av-blink var(--av-cycle) infinite;
  animation-delay: calc(var(--av-phase, 0) * var(--av-cycle) * -1);
}

/* ---------- working ----------
   Scanning something. The body keeps the ambient float from the base rule; at
   list sizes a turn of the head was invisible and cost a second animation on
   every row. */

[data-status="working"] .av-look {
  --av-cycle: calc(8.5s / var(--av-eye-rate, 1));
  animation: av-scan var(--av-cycle) linear infinite;
  animation-delay: calc(var(--av-phase, 0) * var(--av-cycle) * -1);
}

/* Concentration suppresses blinking. */
[data-status="working"] .av-eyes {
  --av-cycle: 10.5s;
  animation: av-blink var(--av-cycle) infinite;
  animation-delay: calc(var(--av-phase, 0) * var(--av-cycle) * -1);
}

[data-status="working"] .av-halo {
  color: var(--av-working, #2E7D6B);
  stroke-dasharray: 5 7;
  opacity: .75;
  --av-cycle: 7s;
  animation: av-halo-spin var(--av-cycle) linear infinite;
  animation-delay: calc(var(--av-phase, 0) * var(--av-cycle) * -1);
}

/* ---------- needs you ----------
   Asks for attention, then stops asking. */

/* A hop pivots on the ground, not on the middle of the body. */
[data-status="needs"] .av-body { transform-origin: 50px 86px; }

[data-status="needs"] .av-body,
[data-av-jump="normal"] [data-status="needs"] .av-body {
  --av-cycle: 12s;
  animation: av-hop-12 var(--av-cycle) cubic-bezier(.3, 0, .35, 1) infinite,
             av-squash-12 var(--av-cycle) ease-out infinite;
  animation-delay: calc(var(--av-phase, 0) * var(--av-cycle) * -1),
                   calc(var(--av-phase, 0) * var(--av-cycle) * -1);
}

[data-av-jump="often"] [data-status="needs"] .av-body {
  --av-cycle: 6s;
  animation: av-hop-6 var(--av-cycle) cubic-bezier(.3, 0, .35, 1) infinite,
             av-squash-6 var(--av-cycle) ease-out infinite;
  animation-delay: calc(var(--av-phase, 0) * var(--av-cycle) * -1),
                   calc(var(--av-phase, 0) * var(--av-cycle) * -1);
}

[data-av-jump="rare"] [data-status="needs"] .av-body {
  --av-cycle: 24s;
  animation: av-hop-24 var(--av-cycle) cubic-bezier(.3, 0, .35, 1) infinite,
             av-squash-24 var(--av-cycle) ease-out infinite;
  animation-delay: calc(var(--av-phase, 0) * var(--av-cycle) * -1),
                   calc(var(--av-phase, 0) * var(--av-cycle) * -1);
}

[data-av-jump="never"] [data-status="needs"] .av-body { animation: none; }

[data-status="needs"] .av-look {
  --av-cycle: calc(8.5s / var(--av-eye-rate, 1));
  animation: av-glance var(--av-cycle) linear infinite;
  animation-delay: calc(var(--av-phase, 0) * var(--av-cycle) * -1);
}

[data-status="needs"] .av-eyes {
  --av-cycle: 4.6s;
  animation: av-blink var(--av-cycle) infinite;
  animation-delay: calc(var(--av-phase, 0) * var(--av-cycle) * -1);
}

[data-status="needs"] .av-halo {
  color: var(--av-needs, #C05621);
  --av-cycle: 2.6s;
  animation: av-halo-ping var(--av-cycle) ease-out infinite;
  animation-delay: calc(var(--av-phase, 0) * var(--av-cycle) * -1);
}

/* ---------- idle ---------- */

[data-status="idle"] { opacity: .55; }
[data-status="idle"] .av-body,
[data-status="idle"] .av-look,
[data-status="idle"] .av-spec,
[data-status="idle"] .av-eyes { animation: none; }
[data-status="idle"] .av-halo { display: none; }

/* ---------- switches ---------- */

/* Rings are opt-in. Most rosters will not want one, so nothing shows a ring
   until a container asks — but see the note on reduced motion below. */
[data-av-halos="all"] [data-status="working"] .av-halo,
[data-av-halos="all"] [data-status="needs"] .av-halo,
[data-av-halos="needs"] [data-status="needs"] .av-halo { display: block; }

[data-av-off~="eyes"] .av-look,
[data-av-off~="body"] .av-body,
[data-av-off~="body"] .av-spec { animation: none; }

/* `still` on any ancestor freezes a subtree — an offline coworker, a print
   stylesheet, a screenshot. The rings stay drawn; only the motion stops. */
.still .av-body,
.still .av-spec,
.still .av-eyes,
.still .av-look,
.still .av-halo { animation: none; }

/* Under reduced motion nothing moves, so any ring that is switched on becomes
   the only visual status channel — which is why the three differ in form,
   dashed against solid against absent, and not merely in colour. With rings off
   (the default) there is no visual channel left at all under reduced motion, so
   give each row a visually-hidden label whichever way you set them. */
@media (prefers-reduced-motion: reduce) {
  .av-body, .av-spec, .av-eyes, .av-look, .av-halo { animation: none !important; }
  [data-status="needs"] .av-halo { opacity: .9; }
}

/* ---------- keyframes ---------- */

@keyframes av-float {
  0%, 100% { translate: 0 0; }
  50%      { translate: 0 -1.7px; }
}

/* The highlight lags the body by a hair, which is what reads as a curved surface
   turning under a fixed light rather than a sticker sliding around. Its throw
   follows --av-depth so a faint highlight doesn't swing as far as a bright one. */
@keyframes av-spec {
  0%, 100% { transform: translate(0, 0); }
  50%      { transform: translate(calc(1.7px * var(--av-depth, 1)), calc(-.9px * var(--av-depth, 1))); }
}

@keyframes av-blink {
  0%, 93.5%, 100% { transform: scaleY(1); }
  95.5%           { transform: scaleY(.06); }
  97%             { transform: scaleY(1); }
}

/* Saccades, not pans. Eyes reading a screen hold for most of a second and then
   jump in about 50 ms; easing that into a smooth sweep reads dreamy, not busy.
   Each step holds, then crosses in 1% of the cycle. */
@keyframes av-scan {
  0%,  20%  { transform: translate(-2px, .9px); }
  21%, 42%  { transform: translate(2.2px, .7px); }
  43%, 55%  { transform: translate(-1.6px, 1.5px); }
  56%, 78%  { transform: translate(1.9px, 1.1px); }
  79%, 100% { transform: translate(-.5px, -.9px); }
}

@keyframes av-glance {
  0%,  10%  { transform: translate(0, -.4px); }
  13%, 22%  { transform: translate(-2.4px, -.6px); }
  23%, 32%  { transform: translate(2.4px, -.6px); }
  33%, 100% { transform: translate(0, -.5px); }
}

/* The hop is always the same 660 ms of movement; only the gap between hops
   changes. That means the keyframe percentages have to be recomputed per
   interval rather than the duration simply being scaled — scaling the duration
   would make the jump itself faster or slower, which is not what a frequency
   control should do. test/motion.test.js checks the 660 ms window holds. */

@keyframes av-hop-6 {
  0%       { translate: 0 0; }
  2.288%   { translate: 0 -6px; }
  5.038%   { translate: 0 0; }
  7.337%   { translate: 0 -3.2px; }
  9.625%   { translate: 0 0; }
  11%      { translate: 0 0; }
  100%     { translate: 0 0; }
}

@keyframes av-squash-6 {
  0%       { scale: 1 1; }
  1.1%     { scale: 1.07 .93; }
  2.288%   { scale: .95 1.06; }
  5.038%   { scale: 1.08 .92; }
  6.237%   { scale: 1 1; }
  7.337%   { scale: .97 1.04; }
  9.625%   { scale: 1.04 .96; }
  10.637%  { scale: 1 1; }
  11%      { scale: 1 1; }
  100%     { scale: 1 1; }
}

@keyframes av-hop-12 {
  0%       { translate: 0 0; }
  1.144%   { translate: 0 -6px; }
  2.519%   { translate: 0 0; }
  3.669%   { translate: 0 -3.2px; }
  4.812%   { translate: 0 0; }
  5.5%     { translate: 0 0; }
  100%     { translate: 0 0; }
}

@keyframes av-squash-12 {
  0%       { scale: 1 1; }
  0.55%    { scale: 1.07 .93; }
  1.144%   { scale: .95 1.06; }
  2.519%   { scale: 1.08 .92; }
  3.119%   { scale: 1 1; }
  3.669%   { scale: .97 1.04; }
  4.812%   { scale: 1.04 .96; }
  5.319%   { scale: 1 1; }
  5.5%     { scale: 1 1; }
  100%     { scale: 1 1; }
}

@keyframes av-hop-24 {
  0%       { translate: 0 0; }
  0.572%   { translate: 0 -6px; }
  1.26%    { translate: 0 0; }
  1.834%   { translate: 0 -3.2px; }
  2.406%   { translate: 0 0; }
  2.75%    { translate: 0 0; }
  100%     { translate: 0 0; }
}

@keyframes av-squash-24 {
  0%       { scale: 1 1; }
  0.275%   { scale: 1.07 .93; }
  0.572%   { scale: .95 1.06; }
  1.26%    { scale: 1.08 .92; }
  1.559%   { scale: 1 1; }
  1.834%   { scale: .97 1.04; }
  2.406%   { scale: 1.04 .96; }
  2.659%   { scale: 1 1; }
  2.75%    { scale: 1 1; }
  100%     { scale: 1 1; }
}

@keyframes av-halo-spin { to { rotate: 360deg; } }

@keyframes av-halo-ping {
  0%        { scale: .93; opacity: .8; }
  70%, 100% { scale: 1.16; opacity: 0; }
}
