/* ══════════════════════════════════════════════════════════════════════════
   STARKOS — THE ONBOARDING CHARACTER
   ONBOARD ROW 1. MASTER-PLAN-2 section 9 ROW 1, Decision Log 002 section 7.
   ══════════════════════════════════════════════════════════════════════════

   The assistant that introduces itself. It is drawn by js/character.js as
   inline SVG and it is THE ONE PLAYFUL THING IN THE PRODUCT, on the onboarding
   conversation and nowhere else. Everything around it stays cold.

   TRANSFORM AND OPACITY ONLY. Every rule below animates one of those two.
   Nothing here touches width, height, top, left or margin, so every state is
   composited and holds its frame rate on an iPhone 12.

   WHY THE DURATIONS ARE AUTHORED HERE AND NOT READ FROM css/tokens.css.
   tokens.css says it itself: "Keyframe choreography, staged/stepped reveals
   and the logo intro keep their authored durations — they have their own
   timing story." The five states are that: MASTER-PLAN-2 names every one of
   their durations (2 s bob, 120 ms blink, 900 ms wave, 400 ms nod) and they
   are a performance, not a reaction to a finger.

   AND ONE OF THEM MUST SURVIVE REDUCED MOTION, which settles it. tokens.css
   collapses --t-fast to 0.01ms under `prefers-reduced-motion`, on purpose. The
   plan says "Reduced motion: no idle, no wave, no tracking; BLINK STAYS". A
   blink driven off --t-fast would still technically fire and would be over in
   a hundredth of a millisecond, which is not a blink, it is a dropped frame.
   So the blink carries its own 120 ms and keeps it in both worlds.

   THE EASINGS ARE TOKENS, because those we do have: --ease for anything that
   reacts (the pupils following a finger), --ease-grow for the two things that
   are meant to look ALIVE rather than mechanical (the breathing bob and the
   nod). That is exactly the distinction --ease-grow was added for. */

.character {
  /* 132 x 224 drawing units. The figure inside it is 116 x 216 units, so at
     the default size below it stands 119.6 px tall on a 390 px screen —
     MASTER-PLAN-2's "about 120 px tall". The margin is the room the wave and
     the bob need so that neither ever clips at the box edge. */
  --char-size: 124px;
  /* HOW FAR A PUPIL MAY TRAVEL, in DRAWING UNITS, and the number is not the
     plan's. The plan says "inside a 4 px radius". 4 px of screen is 7.2
     drawing units at this size; the white of the eye is 7 units across from
     its centre and the pupil is 3, so a 4 px screen move puts the pupil
     outside the eye it lives in. 4 UNITS is 2.21 px on a 390 screen and is
     the largest move that keeps the pupil inside the white.
     Named in QUESTION-ONBOARD.md; the one edit that changes it is this line. */
  --char-track: 4;
  /* WHERE THE PUPILS ARE, in drawing units, written by js/character.js on every
     pointer move and reset to zero when the pointer leaves. Declared here at
     rest rather than left to a var() fallback at the call site: a default that
     only exists inside a calc() is a default nobody can find, and these two are
     the only part of the character's pose that another file writes. */
  --char-px: 0;
  --char-py: 0;

  display: block;
  height: var(--char-size);
  width: auto;
  overflow: visible;
}

/* ── IDLE · a 2 s breathing bob of 2 px ──────────────────────────────────
   2 px of SCREEN, which is 3.6 drawing units at the default size. Authored in
   units because the transform is inside SVG user space. */
.character-bob { animation: char-bob 2s var(--ease-grow) infinite; }

@keyframes char-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(3.6px); }
}

/* ── BLINK · both eyes, 120 ms ───────────────────────────────────────────
   The lids are the screen's own colour, so a blink is two dark caps sliding
   over the eyes and not a shape change on the eye itself. Scaling the eye
   would squash the pupil with it, which reads as a wince. */
.character-lid {
  transform: scaleY(0);
  transform-origin: center center;
  transform-box: fill-box;
}
.character.is-blinking .character-lid { animation: char-blink 120ms var(--ease); }

@keyframes char-blink {
  0%, 100%  { transform: scaleY(0); }
  30%, 70%  { transform: scaleY(1); }
}

/* ── WAVE · the right arm, 900 ms, once on the intro ─────────────────────
   The character's right, which is the LEFT of the screen, the same way a
   person's right hand is on your left when they face you. */
/* THE ORIGIN IS THE SHOULDER IT HANGS FROM, and getting it wrong is not a
   subtle error. This said `93px 98px` on the second pass — the right
   shoulder's coordinates, on the LEFT arm — and the wave swung the whole limb
   across the body and down behind a leg. Every assertion in the shot harness
   stayed green, because "the arm moved" was true. The contact sheet is what
   caught it. These two numbers are GRID.shoulder.lx and GRID.shoulder.cy and
   the suite asserts they still agree with the module. */
.character-arm-r { transform-origin: 41px 101px; }
.character.is-waving .character-arm-r { animation: char-wave 900ms var(--ease); }

/* THE ANGLES ARE COMPUTED, NOT PICKED. The arm is 69.6 units long from the
   shoulder and it hangs down and slightly out, so a small rotation swings the
   hand a long way sideways and a negative one drags the whole limb across the
   body. Rotating the rest vector about the shoulder puts the hand at:

     40deg  (-19.9, 134.7)   far off the left edge, mid-transit
     90deg  (-24.0,  76.0)   the arm straight out, furthest out of the box
    122deg  ( -0.9,  45.4)   up beside the head, just past the edge
    128deg  (  5.2,  41.3)   up beside the head, at eye level  ← the hold
    138deg  ( 16.1,  36.0)   up and inward, the top of each swing

   So the wave lives between 118 and 138 degrees and gets there fast. The first
   pass used -52deg, which by this arithmetic puts the hand at (57, 172): on
   the far side of the body, behind the other leg. It was wrong twice over,
   in sign and in size. */
@keyframes char-wave {
  0%   { transform: rotate(0deg); }
  15%  { transform: rotate(124deg); }   /* up, fast, past the transit */
  27%  { transform: rotate(138deg); }   /* and two swings */
  38%  { transform: rotate(118deg); }
  53%  { transform: rotate(138deg); }
  68%  { transform: rotate(118deg); }
  85%  { transform: rotate(60deg); }
  100% { transform: rotate(0deg); }     /* back to the side */
}

/* ── NOD · the head, 400 ms, on every "Saved" ────────────────────────────
   Pivoting at the neck, so the head tips forward and back rather than sliding
   down the screen. */
.character-head { transform-origin: 66px 86px; }
.character.is-nodding .character-head { animation: char-nod 400ms var(--ease-grow); }

/* A NOD IS A DIP, NOT A TILT. Drawn first as rotate(7deg), it read as a head
   falling sideways off its neck: seen straight on, a 2D rotation about the
   neck is the "curious dog" tilt and nothing else. A nod is the head going
   DOWN and coming back, so it is mostly translateY with just enough rotation
   to stop the movement reading as the whole head sliding. */
@keyframes char-nod {
  0%, 100% { transform: rotate(0deg) translateY(0); }
  45%      { transform: rotate(2deg) translateY(5px); }
}

/* ── THINK · the eyes drift up and to the right while the dots show ──────
   Held, not looped: thinking is a state the page turns on and off, so it eases
   there and stays there. The pupils' own tracking transform is overridden for
   as long as it lasts, which is correct — he is not being looked at, he is
   being thought about. */
.character-pupils {
  transform: translate(calc(var(--char-px) * 1px), calc(var(--char-py) * 1px));
  transition: transform var(--t-fast) var(--ease);
}
.character.is-thinking .character-pupils { transform: translate(2.6px, -3.2px); }

/* ── REDUCED MOTION ──────────────────────────────────────────────────────
   MASTER-PLAN-2 names three removals and one survivor: "no idle, no wave, no
   tracking; blink stays." IT DOES NOT MENTION THE NOD OR THE THINK, so those
   two are governed by the standing law above the plan — HOUSE-RULES, "Every
   animation off under prefers-reduced-motion" — and they are off here. The
   plan's sentence is read as naming the ONE EXCEPTION to that law, the blink,
   not as a complete list of everything that stops.

   The tracking is switched off in js/character.js as well — the listener is
   never attached — so it has two locks and not one. The blink keeps its full
   120 ms because the reason it exists is to prove the character is awake, and
   that is worth exactly as much to a man who asked his phone to stop moving
   things as to anyone else. */
@media (prefers-reduced-motion: reduce) {
  .character-bob,
  .character.is-waving .character-arm-r { animation: none; }
  .character-pupils { transition: none; transform: none; }
  .character.is-nodding .character-head { animation: none; }
  .character.is-thinking .character-pupils { transform: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE PAINT
   ══════════════════════════════════════════════════════════════════════════
   Every colour the character wears is here, reading the `--char-*` tokens in
   css/tokens.css. js/character.js authors NO colour at all; it hands out class
   names and gradient ids, and this block is what they mean. That split is the
   colour law ("no hex at a call site, tokens only") made structural rather
   than remembered: there is nowhere in the script to put a hex.

   A SURFACE THAT CARRIES THE SILHOUETTE NEVER USES THE BOTTOM OF THE RAMP.
   Measured on #040404, --char-chrome-deep is 1.15:1 and --char-chrome-dark is
   1.37:1, so an outer edge painted in either of them is an outer edge nobody
   can see. The deep tones appear only as the far stop of a gradient and as the
   inside of the screen. */

/* the gradient stops */
.char-stop-hi          { stop-color: var(--char-chrome-hi); }
.char-stop-light       { stop-color: var(--char-chrome-light); }
.char-stop-base        { stop-color: var(--char-chrome-base); }
.char-stop-shade       { stop-color: var(--char-chrome-shade); }
.char-stop-screen-top  { stop-color: var(--char-screen-top); }
.char-stop-screen-deep { stop-color: var(--char-screen-deep); }

/* the limbs: strokes, not outlines, so they stay thin at 120 px */
.char-limb {
  stroke: var(--char-chrome-light);
  stroke-width: 5.2;
  stroke-linecap: round;
  fill: none;
}
.char-finger {
  stroke: var(--char-chrome-mid);
  stroke-width: 2.2;
  stroke-linecap: round;
  fill: none;
}
.char-joint { fill: var(--char-chrome-mid); }

/* the head's bezel keeps a hairline of the brightest tone, which is what reads
   as a machined edge and what separates the head from the matte black behind
   it at small sizes */
.char-bezel {
  stroke: var(--char-chrome-hi);
  stroke-width: 0.8;
  stroke-opacity: 0.55;
}
.char-screen { stroke: var(--char-chrome-deep); stroke-width: 1; }

/* the two body seams: a shade of the ramp at low opacity, never an outline */
.char-seam {
  fill: none;
  stroke: var(--char-chrome-hi);
  stroke-width: 0.9;
  stroke-opacity: 0.2;
  stroke-linecap: round;
}

.char-eye-white { fill: var(--char-eye); }
.char-pupil     { fill: var(--char-pupil); }
.char-glint     { fill: var(--char-eye); opacity: 0.85; }

/* the sweep of light across the glass */
.char-gloss { fill: var(--char-chrome-hi); opacity: 0.1; }
