/* =========================================================
   The mascot: motion only.

   Two kinds of skull live on the site, and both are animated from here.

   - The mark, skullMascot() in src/components/icons.mjs: the brand glyph in
     the header and the assistant, split into body, gaze and eyes.
   - The illustrations, renderSkull() in src/components/skull.mjs: the full
     character, inlined, with its body, gaze, eyes, props and marks each in
     their own group, plus an alternate face (`sk-face--hover`) for this file
     to swap in.

   Nothing here draws. The identity forbids a mouth and the renderer draws no
   limbs, so the character's whole range is the face it pulls, how its head
   moves, and what its props do (assets/graphics/README.md, "Motion").

   Motion language: quick, bouncy, physical. A reaction is one beat — a hop
   with squash and stretch, a face swap, a prop doing its job — on the
   overshoot curve (--ease-snap), then it holds still. Loops are reserved for
   the few things that are ambient on purpose: blinking, the header mark's
   breathing, the 404 search and the peeking skull above the closing call to
   action. Timing tokens are in theme.css (--dur-micro, --dur-character,
   --dur-ambient).
   ========================================================= */

.mascot {
  display: inline-block;
  flex: 0 0 auto;
  overflow: visible;
  color: inherit;
}

/* fill-box centres a percentage transform-origin on the shape's own bounding
   box rather than on the SVG viewport, which is what lets "center" mean the
   same thing for the body (roughly square) and the eyes (a short, wide
   strip) without hand-tuned origins for each. */
.mascot__body,
.mascot__eyes,
.mascot__look,
.sk-body,
.sk-look,
.sk-eye,
.sk-prop,
.sk-marks {
  transform-box: fill-box;
  transform-origin: center;
}

/* ---------- Faces ----------
   The alternate faces are drawn but hidden. The swap is a cut, not a fade: a
   cartoon changes expression between frames, and a cross-fade shows both
   faces at once for a moment, which reads as a double exposure. */
.sk-face--hover,
.sk-face--poke,
.sk-face--sleep,
.sk-face--jolt,
.sk-marks--hover,
.sk-marks--poke,
.sk-marks--sleep,
.sk-marks--jolt {
  visibility: hidden;
}

/* What counts as "the character has been noticed": the card, stage or tier it
   sits in being hovered or focused, the drawing itself being hovered, or —
   for the two figures that stand beside a button — that button. */
:is(.card, .stage, .tier, .family-strip__item):is(:hover, :focus-within) :is(.sk-face--base, .sk-marks--base),
:is(.section__art, .page-head__art, .split__skull, .site-footer__skull, .cta__art, .system__hub):hover :is(.sk-face--base, .sk-marks--base),
.hero:has(.hero__actions .btn:is(:hover, :focus-visible)) .system__skull :is(.sk-face--base, .sk-marks--base),
.cta:has(.cta__actions :is(:hover, :focus-visible)) .cta__art :is(.sk-face--base, .sk-marks--base) {
  visibility: hidden;
}

:is(.card, .stage, .tier, .family-strip__item):is(:hover, :focus-within) :is(.sk-face--hover, .sk-marks--hover),
:is(.section__art, .page-head__art, .split__skull, .site-footer__skull, .cta__art, .system__hub):hover :is(.sk-face--hover, .sk-marks--hover),
.hero:has(.hero__actions .btn:is(:hover, :focus-visible)) .system__skull :is(.sk-face--hover, .sk-marks--hover),
.cta:has(.cta__actions :is(:hover, :focus-visible)) .cta__art :is(.sk-face--hover, .sk-marks--hover) {
  visibility: visible;
}

/* Poked three times (src/client/mascot.js). Listed after the hover rules so
   it wins while the pointer is still on the figure. */
.system__skull.is-dizzy :is(.sk-face--base, .sk-marks--base, .sk-face--hover, .sk-marks--hover) {
  visibility: hidden;
}

.system__skull.is-dizzy :is(.sk-face--poke, .sk-marks--poke) {
  visibility: visible;
}

/* Confused, beside an assistant reply that found nothing: each eye glances a
   different way, the one place they move independently. A pose, not motion,
   so it stays with reduced motion too. */
.mascot--confused .mascot__eye--l {
  transform: translate(-4.5px, 2.5px);
}

.mascot--confused .mascot__eye--r {
  transform: translate(4.5px, -2.5px);
}

/* The figure's labels lie over the whole drawing. They carry no interaction of
   their own, so they let the pointer through to the skull underneath. */
.system__nodes {
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  /* ---------- Idle: the mark ----------
     Two blinks and two held glances per loop, at uneven intervals so it never
     reads as a metronome. The seed classes offset the loop so the header mark
     and the assistant's are never mid-blink together. */
  @keyframes mascot-ambient {
    0%,
    4%,
    10%,
    30%,
    42%,
    58%,
    70%,
    88%,
    94%,
    100% {
      transform: none;
    }

    6%,
    91% {
      transform: scaleY(0.08);
    }

    32%,
    40% {
      transform: translateX(-7px);
    }

    60%,
    68% {
      transform: translateX(7px);
    }
  }

  .mascot--idle .mascot__eyes {
    animation: mascot-ambient var(--dur-ambient) var(--ease) infinite;
  }

  /* The body's half of idle: a slow rise and fall on its own period, so the
     two loops drift in and out of phase instead of ticking together. */
  @keyframes mascot-breathe {
    50% {
      transform: translateY(-1.75px);
    }
  }

  .mascot--idle .mascot__body {
    animation: mascot-breathe 3.4s ease-in-out infinite;
  }

  .mascot--seed-1 :is(.mascot__eyes, .mascot__body) {
    animation-delay: -1.5s;
  }

  .mascot--seed-2 :is(.mascot__eyes, .mascot__body) {
    animation-delay: -2.9s;
  }

  .mascot--seed-3 :is(.mascot__eyes, .mascot__body) {
    animation-delay: -3.7s;
  }

  /* The gaze wrapper is moved by src/client/mascot.js; this only smooths it. */
  .mascot__look,
  .sk-look {
    transition: transform 0.18s var(--ease);
  }

  /* ---------- Entrance ----------
   A quarter-second rise on the snap curve, then straight into idle. */
  @keyframes mascot-enter {
    from {
      opacity: 0;
      transform: translateY(18px) scale(0.7);
    }
  }

  .mascot--enter {
    animation: mascot-enter var(--dur-character) var(--ease-snap) both;
  }

  /* ---------- Hop ----------
     The one reaction shared by every skull: anticipation squash, stretch on
     the way up, squash on landing, settle. Origin at the feet, so it squashes
     into the ground rather than about its middle. Played on the mark by the
     script (a click on a call to action, an answer in the assistant) and on
     illustrations when they are noticed. */
  @keyframes skull-hop {
    0% {
      transform: none;
    }

    14% {
      transform: scale(1.08, 0.9);
    }

    42% {
      transform: translateY(-14%) scale(0.95, 1.06);
    }

    72% {
      transform: scale(1.05, 0.94);
    }

    100% {
      transform: none;
    }
  }

  .mascot--hop {
    transform-origin: 50% 100%;
    animation: skull-hop 0.52s var(--ease) both;
  }

  /* ---------- Looking around (404) ---------- */
  @keyframes mascot-search {
    0%,
    45%,
    86%,
    100% {
      transform: none;
    }

    22% {
      transform: rotate(-15deg);
    }

    68% {
      transform: rotate(15deg);
    }
  }

  .mascot--search .mascot__body {
    animation: mascot-search 4.5s var(--ease) infinite;
  }

  .mascot--search .mascot__eyes {
    animation: mascot-ambient 4.5s var(--ease) infinite;
    animation-delay: -1.1s;
  }

  /* ---------- Confused ----------
     Beside an assistant reply that found nothing. The eyes glance different
     ways — the one place they move independently — and a single wobble plays
     on arrival rather than looping, so three misses in a row do not produce
     an increasingly frantic skull. */
  @keyframes mascot-wobble-in {
    30% {
      transform: rotate(-16deg);
    }

    60% {
      transform: rotate(11deg);
    }
  }

  .mascot--confused .mascot__body {
    animation: mascot-wobble-in var(--dur-character) var(--ease-snap);
  }

  /* ---------- The assistant's launcher ----------
     Hovered, the mark turns to look up at the visitor. */
  .chat-launcher .mascot {
    transition: transform var(--dur-character) var(--ease-snap);
  }

  .chat-launcher:hover .mascot {
    transform: rotate(-12deg) translateY(-1px);
  }

  .chat-launcher:hover .mascot__look {
    transform: translate(-5px, -6px);
  }

  /* ---------- Illustrations: noticed ----------
     The hop, the prop doing its job, and the new face's marks popping in.
     Each is a single play: move the pointer away and back to see it again.

     The hero figure is not in this list: its body already carries the wake
     sequence, so its hop is handed to that rule's reaction slot instead — see
     "The hero: waking up" below. */
  :is(.card, .stage, .tier, .family-strip__item):is(:hover, :focus-within) .sk-body,
  :is(.section__art, .page-head__art, .split__skull, .site-footer__skull, .cta__art):hover .sk-body,
  .cta:has(.cta__actions :is(:hover, :focus-visible)) .cta__art .sk-body {
    transform-origin: 50% 100%;
    animation: skull-hop 0.52s var(--ease) both;
  }

  @keyframes sk-marks-pop {
    from {
      transform: scale(0.2);
      opacity: 0;
    }

    60% {
      transform: scale(1.15);
      opacity: 1;
    }
  }

  :is(.card, .stage, .tier, .family-strip__item):is(:hover, :focus-within) .sk-marks--hover,
  :is(.section__art, .page-head__art, .split__skull, .site-footer__skull, .cta__art, .system__hub):hover .sk-marks--hover,
  .hero:has(.hero__actions .btn:is(:hover, :focus-visible)) .system__skull .sk-marks--hover,
  .cta:has(.cta__actions :is(:hover, :focus-visible)) .cta__art .sk-marks--hover {
    animation: sk-marks-pop var(--dur-character) var(--ease-snap) 0.08s both;
  }

  /* Props. The default is a small lift and tilt, a beat after the hop, so the
     skull moves first and the thing it is holding follows. The props below
     that have a job do the job instead. */
  @keyframes sk-prop-lift {
    45% {
      transform: translateY(-10%) rotate(-7deg);
    }
  }

  @keyframes sk-prop-type {
    20%,
    60% {
      transform: translateY(4%);
    }

    40%,
    80% {
      transform: translateY(-3%);
    }
  }

  @keyframes sk-prop-scan {
    30% {
      transform: translate(-22%, -12%) rotate(-10deg);
    }

    65% {
      transform: translate(12%, 6%) rotate(6deg);
    }
  }

  @keyframes sk-prop-rattle {
    20%,
    60% {
      transform: rotate(-12deg);
    }

    40%,
    80% {
      transform: rotate(10deg);
    }
  }

  @keyframes sk-prop-scuttle {
    25% {
      transform: translateX(-14%) rotate(-8deg);
    }

    50% {
      transform: translateX(10%) rotate(6deg);
    }

    75% {
      transform: translateX(-6%);
    }
  }

  @keyframes sk-prop-spin {
    to {
      transform: rotate(360deg);
    }
  }

  @keyframes sk-prop-launch {
    40% {
      transform: translateY(-28%) rotate(4deg);
    }

    70% {
      transform: translateY(-20%);
    }
  }

  @keyframes sk-prop-pop {
    40% {
      transform: scale(1.35) rotate(-8deg);
    }
  }

  :is(.card, .stage, .tier):is(:hover, :focus-within) .sk-prop,
  :is(.section__art, .page-head__art, .split__skull, .cta__art):hover .sk-prop,
  .cta:has(.cta__actions :is(:hover, :focus-visible)) .cta__art .sk-prop {
    animation: sk-prop-lift 0.5s var(--ease-snap) 0.1s both;
  }

  /* Screens and keyboards: typing. */
  :is(.card, .stage, .tier):is(:hover, :focus-within) :is(.sk-prop--laptop, .sk-prop--terminal, .sk-prop--terminalError, .sk-prop--request, .sk-prop--packets, .sk-prop--keyboard),
  :is(.section__art, .page-head__art, .split__skull):hover :is(.sk-prop--laptop, .sk-prop--terminal, .sk-prop--request, .sk-prop--packets) {
    animation-name: sk-prop-type;
    animation-duration: 0.45s;
    animation-timing-function: linear;
  }

  /* The magnifier sweeps across what it is looking at. */
  :is(.card, .stage, .tier):is(:hover, :focus-within) .sk-prop--magnifier,
  :is(.section__art, .page-head__art, .split__skull):hover .sk-prop--magnifier {
    animation-name: sk-prop-scan;
    animation-duration: 0.9s;
    animation-timing-function: var(--ease);
  }

  /* Locks get tried. */
  :is(.card, .stage, .tier):is(:hover, :focus-within) :is(.sk-prop--lock, .sk-prop--lockOpen, .sk-prop--warning),
  :is(.section__art, .page-head__art, .split__skull, .cta__art):hover :is(.sk-prop--lock, .sk-prop--lockOpen, .sk-prop--warning) {
    animation-name: sk-prop-rattle;
    animation-duration: 0.45s;
    animation-timing-function: linear;
  }

  /* The bug tries to get away. */
  :is(.card, .stage, .tier):is(:hover, :focus-within) .sk-prop--bug,
  :is(.section__art, .split__skull, .cta__art):hover .sk-prop--bug,
  .cta:has(.cta__actions :is(:hover, :focus-visible)) .cta__art .sk-prop--bug {
    animation-name: sk-prop-scuttle;
    animation-duration: 0.6s;
  }

  /* Gears turn once. The scanner's spinner keeps spinning for as long as it
     is watched, which is the joke on the "Automated" tier. */
  :is(.card, .stage, .tier):is(:hover, :focus-within) .sk-prop--gear,
  :is(.section__art, .split__skull):hover .sk-prop--gear {
    animation: sk-prop-spin 0.8s var(--ease-snap) 0.05s both;
  }

  :is(.card, .stage, .tier):is(:hover, :focus-within) .sk-prop--spinner,
  :is(.section__art, .split__skull):hover .sk-prop--spinner {
    animation: sk-prop-spin 0.7s linear infinite;
  }

  :is(.card, .stage, .tier):is(:hover, :focus-within) .sk-prop--rocket {
    animation-name: sk-prop-launch;
    animation-duration: 0.7s;
  }

  :is(.card, .stage, .tier):is(:hover, :focus-within) .sk-prop--check {
    animation-name: sk-prop-pop;
    animation-duration: 0.45s;
  }

  /* ---------- The hero: waking up ----------
     Plays once as the page loads, about three seconds. The skull arrives
     asleep at its desk (sleepy face, zzz, head drooping and nodding), lifts
     the coffee for a sip, jolts awake (surprised face, a hop and a shake) and
     settles into the concentrating face it works in. After that it rests: its
     eyes follow the pointer (src/client/mascot.js), a primary action under the
     pointer gets the mischievous face, three pokes make it dizzy, and every so
     often it takes another sip.

     The face keyframes switch `visibility` and fill backwards only, so once
     the sequence ends the faces go back to the ordinary rules above and the
     hover and poke swaps keep working. */
  @keyframes hero-wake-body {
    0% {
      opacity: 0;
      transform: translateY(8%) rotate(8deg) scale(0.9);
    }

    8% {
      opacity: 1;
      transform: translateY(4%) rotate(7deg);
    }

    20% {
      transform: translateY(2%) rotate(3deg);
    }

    30% {
      transform: translateY(5%) rotate(9deg);
    }

    44% {
      transform: translateY(3%) rotate(4deg);
    }

    52% {
      transform: translateY(-12%) scale(0.95, 1.07);
    }

    58% {
      transform: rotate(-5deg) scale(1.05, 0.94);
    }

    64% {
      transform: rotate(4deg);
    }

    70% {
      transform: rotate(-2deg);
    }

    78%,
    100% {
      transform: none;
    }
  }

  @keyframes hero-wake-sleep {
    0%,
    48% {
      visibility: visible;
    }

    49%,
    100% {
      visibility: hidden;
    }
  }

  @keyframes hero-wake-jolt {
    0%,
    48% {
      visibility: hidden;
    }

    49%,
    70% {
      visibility: visible;
    }

    71%,
    100% {
      visibility: hidden;
    }
  }

  @keyframes hero-wake-base {
    0%,
    70% {
      visibility: hidden;
    }

    71%,
    100% {
      visibility: visible;
    }
  }

  /* The cup comes up to the face, tips, and goes back down to the desk. */
  @keyframes hero-sip {
    0%,
    100% {
      transform: none;
    }

    35%,
    70% {
      transform: translate(-24px, -4px) rotate(-32deg);
    }
  }

  @keyframes hero-wake-coffee {
    0%,
    16% {
      transform: none;
    }

    30%,
    46% {
      transform: translate(-24px, -4px) rotate(-32deg);
    }

    52% {
      transform: translate(-18px, -12px) rotate(-8deg);
    }

    66%,
    100% {
      transform: none;
    }
  }

  /* The occasional sip: a second at the end of a twenty-second loop. */
  @keyframes hero-sip-later {
    0%,
    92%,
    100% {
      transform: none;
    }

    94%,
    97% {
      transform: translate(-24px, -4px) rotate(-32deg);
    }
  }

  /* The wake sequence is an entrance: it opens on `opacity: 0` and is meant to
     play once, as the page loads. So this declaration has to be the only one
     that sets the body's animation — replacing it with a reaction and letting
     it come back would start the entrance again from its first frame, which
     blanks the skull for a quarter of a second and then replays all three
     seconds of it. A reaction goes in the second slot instead: hover and the
     poke classes set --skull-reaction on the figure, which leaves the wake
     running underneath rather than restarting it. */
  .system__skull .sk-body {
    transform-origin: 50% 100%;
    animation:
      hero-wake-body 3.2s var(--ease) 0.2s both,
      var(--skull-reaction, none);
  }

  .system__skull :is(.sk-face--sleep, .sk-marks--sleep) {
    animation: hero-wake-sleep 3.2s linear 0.2s backwards;
  }

  .system__skull :is(.sk-face--jolt, .sk-marks--jolt) {
    animation: hero-wake-jolt 3.2s linear 0.2s backwards;
  }

  .system__skull :is(.sk-face--base, .sk-marks--base) {
    animation: hero-wake-base 3.2s linear 0.2s backwards;
  }

  .system__skull .sk-marks--jolt {
    animation:
      hero-wake-jolt 3.2s linear 0.2s backwards,
      sk-marks-pop var(--dur-character) var(--ease-snap) 1.77s backwards;
  }

  .system__skull .sk-prop--coffee {
    animation:
      hero-wake-coffee 3.2s var(--ease-snap) 0.2s backwards,
      hero-sip-later 20s var(--ease) 8s infinite;
  }

  /* Steam off the cup, always: the one ambient detail that says the coffee
     is hot and somebody is at the desk. */
  @keyframes hero-steam {
    0%,
    100% {
      opacity: 0.25;
      transform: translateY(0);
    }

    50% {
      opacity: 0.8;
      transform: translateY(-3px);
    }
  }

  .system__skull .sk-steam {
    transform-box: fill-box;
    animation: hero-steam 2.6s ease-in-out infinite;
  }

  .system__skull .sk-eye {
    animation: skull-blink 5.5s 3.6s infinite;
  }

  .system__skull .sk-eye + .sk-eye {
    animation-delay: 3.64s;
  }

  .system__hub:hover .system__skull,
  .system__skull.is-poked {
    --skull-reaction: skull-hop 0.52s var(--ease) both;
  }

  @keyframes skull-dizzy {
    20% {
      transform: rotate(-14deg);
    }

    45% {
      transform: rotate(10deg);
    }

    70% {
      transform: rotate(-6deg);
    }
  }

  .system__skull.is-dizzy {
    --skull-reaction: skull-dizzy 0.9s var(--ease) 2 both;
  }

  /* The labels are the things the skull is attacking. Hovering one lifts it,
     and the skull's eyes, already following the pointer, look at it. */
  .system__node {
    transition: translate var(--dur-micro) var(--ease-snap);
  }

  /* ---------- One blink per page ----------
     The page's own head illustration is the only other drawing that moves at
     rest. Every card on the page blinking would be a room full of eyes. */
  .page-head__art .sk-eye {
    animation: skull-blink 6.5s 2s infinite;
  }

  /* ---------- 404: looking for the page ---------- */
  @keyframes skull-search-look {
    0%,
    100% {
      transform: none;
    }

    20%,
    35% {
      transform: translateX(-6px);
    }

    55%,
    75% {
      transform: translate(6px, -2px);
    }
  }

  .skull--not-found .sk-face--base .sk-look {
    animation: skull-search-look 3.8s var(--ease) infinite;
  }

  /* ---------- Peeking over the jaw ----------
     A small skull lives behind the teeth at the top of the closing band. Every
     so often it comes up for a look and ducks back down; hover or focus the
     call to action and it comes up and stays. The loop is fixed rather than
     random, and nine-tenths of it is spent out of sight. */
  @keyframes cta-peek {
    0%,
    78%,
    96%,
    100% {
      transform: translateY(180%);
    }

    81%,
    92% {
      transform: translateY(44%);
    }

    84% {
      transform: translateY(36%) rotate(-8deg);
    }
  }

  .cta__peek .mascot {
    animation: cta-peek 16s var(--ease-snap) 4s infinite;
  }

  .cta:is(:hover, :focus-within) .cta__peek .mascot {
    animation: none;
    transform: translateY(40%) rotate(-6deg);
    transition: transform var(--dur-character) var(--ease-snap);
  }
}

@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .system__node:hover {
    translate: 0 -3px;
  }
}

/* Shared with styles.css's hero rules. */
@keyframes skull-blink {
  0%,
  93%,
  100% {
    transform: scaleY(1);
  }

  95% {
    transform: scaleY(0.08);
  }
}

/* ---------- Peeking skull: placement ----------
   Clipped to a box that ends at the band's top edge, so below that edge the
   skull is simply gone; the teeth are painted after it and hide it between
   them. At rest (and with reduced motion) it stays out of sight. */
.cta__peek {
  position: absolute;
  right: clamp(1.5rem, 14vw, 12rem);
  bottom: 100%;
  width: 2.75rem;
  height: 5rem;
  overflow: hidden;
  pointer-events: none;
}

.cta__peek .mascot {
  display: block;
  width: 100%;
  height: auto;
  transform: translateY(180%);
}

/* ---------- Reduced motion ----------
   Same policy as motion.css: switched off by name rather than left to the
   duration clamp in theme.css. The meaning survives without the movement: the
   hover faces still swap, and the confused mark beside a failed reply still
   shows. */
@media (prefers-reduced-motion: reduce) {
  .mascot,
  .mascot__eyes,
  .mascot__body,
  .mascot__look,
  .sk-body,
  .sk-look,
  .sk-eye,
  .sk-prop,
  .sk-marks,
  .sk-face,
  .sk-steam {
    animation: none !important;
    transition: none !important;
  }

  .cta__peek {
    display: none;
  }
}
