/* -------------- Character -------------- */
/* --------------
The face, everywhere it appears. It carries its own resting size as a custom
property rather than a fixed length so a page can ask for a different one
without reaching into these rules — the hero steps it up at the comfortable
threshold, the login page sets it inline. The fills are the two drawn variants
stated as one set of pairs: each light-dark() holds the value from
character.svg on the left and the one from character-dark-mode.svg on the
right, so the face inverts with the page while the highlight, which was the
same purple in both drawings, does not. They reach for ramp primitives rather
than color roles because these are a drawing's colors, not a surface or a piece
of text the roles were built to describe.

Clipping is off because the sleeping Z's drift past the viewBox they are
measured in. The transition is here rather than on the sleep state so that
nodding off and waking up both travel — a transition declared only inside the
state rule would leave with the class and snap on the way back.
-------------- */
.character {
  --character-size: var(--nn-space-8x12);

  display: block;
  flex-shrink: 0;
  overflow: visible;
  inline-size: var(--character-size);
  block-size: auto;
  transition:
    rotate var(--nn-anima-time-500ms) var(--nn-anima-ease-in-out),
    translate var(--nn-anima-time-500ms) var(--nn-anima-ease-in-out);
}

.character__face {
  fill: light-dark(var(--nn-neutral-10), var(--nn-neutral-80));
}

.character__highlight {
  fill: var(--nn-brand-30);
}

.character__eye {
  fill: light-dark(var(--nn-neutral-110), var(--nn-neutral-10));
  transform-box: view-box;
  transform-origin: var(--nn-anima-transformorigin-var2);
  translate: var(--nn-anima-translate-x-var2, 0) var(--nn-anima-translate-y-var2, 0);
  transition:
    translate var(--nn-anima-time-150ms) var(--nn-anima-ease-out),
    transform var(--nn-anima-time-200ms) var(--nn-anima-ease-out);
}

.character__eye--left {
  --nn-anima-transformorigin-var2: 37px 54.6px;
}

.character__eye--right {
  --nn-anima-transformorigin-var2: 96.84px 54.6px;
}

.character__outline {
  fill: none;
  stroke: light-dark(var(--nn-neutral-110), var(--nn-neutral-10));
  stroke-width: 4px;
  stroke-miterlimit: 10;
}

/* -------------- Character moods -------------- */
/* --------------
Both drawn eyes are present in the markup and cross-fade by opacity rather than
being swapped in and out, which keeps each eye group's geometry fixed — the
squash origins above are stated in the drawing's own coordinates, but a shape
leaving the document would still move the gaze and the blink around it.
-------------- */
.character__eye-shape {
  opacity: 0;
  transition: opacity var(--nn-anima-time-150ms) var(--nn-anima-ease-out);
}

.character__eye-shape--normal {
  opacity: 1;
}

.character--angry .character__eye-shape--normal {
  opacity: 0;
}

.character--angry .character__eye-shape--angry {
  opacity: 1;
}

.character--happy .character__eye-shape--normal,
.character--happy .character__eye-shape--angry {
  opacity: 0;
}

.character--happy .character__eye-shape--happy {
  opacity: 1;
}

/* -------------- Character blink and sleep -------------- */
/* --------------
Both eyes blink together once the script hands them over, and the character
nods off after a spell with no pointer. Each state is a whole animation
declaration rather than an added one, so taking the class off restores the
state beneath it — awake replaces any intro the page put on the eyes, and
asleep replaces the blink with a held shape the eyes transition into and back
out of. Asleep is written after awake because the two match at the same
specificity and the sleep has to be the one that lands.
-------------- */
.character--awake .character__eye {
  animation: character-blink var(--nn-anima-time-5000ms) var(--nn-anima-ease-in-out) infinite;
}

@keyframes character-blink {
  0%,
  94%,
  100% {
    transform: scale(1, 1);
  }

  96%,
  97% {
    transform: scale(1.2, 0.07);
  }
}

/* --------------
Two deliberate blinks, for coming back from having the eyes shut. It opens on
the way in rather than out, because the only thing that asks for it is a pair of
eyes that were already closed and a snap open before the first blink would read
as a flinch.
-------------- */
.character--blinking .character__eye {
  animation: character-double-blink var(--nn-anima-time-700ms) var(--nn-anima-ease-in-out) 1;
}

@keyframes character-double-blink {
  0% {
    transform: scale(1.2, 0.07);
  }

  16%,
  34%,
  58%,
  100% {
    transform: scale(1, 1);
  }

  42%,
  46% {
    transform: scale(1.2, 0.07);
  }

  74%,
  78% {
    transform: scale(1.2, 0.07);
  }
}

/* --------------
Eyes held shut while somebody types a passcode, and the gaze pinned with them —
a closed eye that still slides toward the pointer is a closed eye that is
clearly still looking.
-------------- */
.character--eyes-shut .character__eye {
  animation: none;
  transform: scale(1.2, 0.07);
  translate: 0 0;
}

/* --------------
One eye cracked open when the typing stops or the field is emptied — a peek at
what is holding things up. It is written to need the shut state as well as its
own class so the pair can only ever mean "shut, except this one". On its own the
class would match a character whose eyes are otherwise perfectly normal and pin
that eye part open for good, which is exactly the kind of leak a lingering class
causes.

The inline scale is not 1. A lid opening widens as it lifts, so the eye is drawn
part way along the same path the blink takes rather than simply made short —
0.4 of the way open, and the width read off that point instead of chosen.

The eye eases rather than snaps because the shut state has already stopped the
blink, which leaves the transform free to take the transition it was given.
-------------- */
.character--eyes-shut.character--peeking .character__eye--right {
  transform: scale(1.12, 0.4);
}

/* --------------
The resting state, reached two ways. Idling with no pointer is one; waiting out
a lockout is the other. They are separate classes over one set of declarations
because the pointer wakes the first and must not cancel the second — a countdown
that ended the moment the mouse moved would not be a countdown.
-------------- */
.character--asleep,
.character--waiting {
  rotate: 8deg;
  translate: 0 var(--nn-space-4x1);
}

.character--asleep .character__eye,
.character--waiting .character__eye {
  animation: none;
  transform: scale(1.2, 0.07);
  translate: 0 0;
}

/* -------------- Character refusal -------------- */
/* --------------
A shake of the head for no. It is a horizontal wobble with the rotation trailing
it, because a real head shake turns about a vertical axis and a flat drawing has
to suggest that with sideways travel rather than spin. Each swing is smaller
than the last so the movement settles rather than stopping dead.

A page adds the class and takes it off again on animationend, which is what lets
a second refusal play. It travels on translate and rotate, the two properties
the sleep uses, and can borrow them because a character being told no is by
definition one somebody is still typing at.
-------------- */
.character--refused {
  animation: character-refuse var(--nn-anima-time-500ms) var(--nn-anima-ease-in-out) 1;
}

@keyframes character-refuse {
  0%,
  100% {
    translate: 0 0;
    rotate: 0deg;
  }

  15% {
    translate: -6px 0;
    rotate: -4deg;
  }

  40% {
    translate: 5px 0;
    rotate: 3deg;
  }

  65% {
    translate: -3px 0;
    rotate: -2deg;
  }

  85% {
    translate: 2px 0;
    rotate: 1deg;
  }
}

/* -------------- Character celebration -------------- */
/* --------------
Five hops for being let in. Each one is lower than the last, which is most of what
makes the run read as a bounce settling rather than as a shape moving up and down
five times. They are written as one animation instead of five iterations of one
hop for that reason — repeating a single hop gives every one the same height and
loses the decay.

The curve changes inside the animation rather than across it: a thrown thing
leaves fast and slows into the top, then falls the other way about. One easing
over the whole run would give both halves the same shape and lose the weight.
-------------- */
.character--celebrating {
  animation: character-celebrate var(--nn-anima-time-1000ms) 1;
}

@keyframes character-celebrate {
  0% {
    translate: 0 0;
    animation-timing-function: var(--nn-anima-ease-out);
  }

  10% {
    translate: 0 -18px;
    animation-timing-function: var(--nn-anima-ease-in);
  }

  20% {
    translate: 0 0;
    animation-timing-function: var(--nn-anima-ease-out);
  }

  30% {
    translate: 0 -15px;
    animation-timing-function: var(--nn-anima-ease-in);
  }

  40% {
    translate: 0 0;
    animation-timing-function: var(--nn-anima-ease-out);
  }

  50% {
    translate: 0 -12px;
    animation-timing-function: var(--nn-anima-ease-in);
  }

  60% {
    translate: 0 0;
    animation-timing-function: var(--nn-anima-ease-out);
  }

  70% {
    translate: 0 -9px;
    animation-timing-function: var(--nn-anima-ease-in);
  }

  80% {
    translate: 0 0;
    animation-timing-function: var(--nn-anima-ease-out);
  }

  90% {
    translate: 0 -6px;
    animation-timing-function: var(--nn-anima-ease-in);
  }

  100% {
    translate: 0 0;
  }
}

/* -------------- Sleeping Z's -------------- */
/* --------------
Three Z's rising off the head while it dozes. They are the first thing in the
drawing so they paint under the face, and they start behind it — the head is a
filled circle, so what makes them read as coming from behind the character is
that they are genuinely hidden until they clear its edge. Each is the same
drift on a delay a third of the cycle apart, so one is always leaving as
another arrives, and the three sizes keep the column from reading as a machine.
-------------- */
.character__sleep-z {
  opacity: 0;
  font-family: var(--nn-typo-display);
  font-size: 20px;
  font-weight: var(--nn-typo-weight-700);
  fill: light-dark(var(--nn-neutral-70), var(--nn-neutral-30));
  text-anchor: middle;
}

.character--asleep .character__sleep-z,
.character--waiting .character__sleep-z {
  animation: character-sleep-drift var(--nn-anima-time-3000ms) var(--nn-anima-ease-out) infinite;
}

.character__sleep-z:nth-child(2) {
  font-size: 15px;
  animation-delay: calc(var(--nn-anima-time-3000ms) / 3);
}

.character__sleep-z:nth-child(3) {
  font-size: 25px;
  animation-delay: calc(var(--nn-anima-time-3000ms) / 3 * 2);
}

@keyframes character-sleep-drift {
  0% {
    opacity: 0;
    translate: 0 0;
  }

  20% {
    opacity: 1;
  }

  100% {
    opacity: 0;
    translate: 22px -50px;
  }
}

/* -------------- Reduced motion -------------- */
/* --------------
The gaze, the blink and the nodding off are all held back in the script, so
what is left to switch off here is the travel between states. The moods keep
working: a hovered phrase still changes which eye is drawn, since that is a
change of expression rather than a thing moving across the screen.
-------------- */
@media (prefers-reduced-motion: reduce) {
  .character,
  .character__eye {
    transition: none;
  }

  .character--refused,
  .character--celebrating,
  .character--blinking .character__eye,
  .character--waiting .character__sleep-z {
    animation: none;
  }
}
