/* ==========================================================================
   kiaribot.com — hero.css
   The landing hero, the step smear, and the scroll rail. Loaded after
   site.css and deliberately UNLAYERED, which is site.css header rule 2a: an
   unlayered declaration outranks every layer without !important. This file
   owns .hero*, .steps/.d*, .rec and .rail, and nothing else on the site uses
   those names.

   NO COLOUR LITERAL AND NO FONT SIZE OUTSIDE THE SEVEN --t-* STEPS. Colours
   come from the resolved ground names (--bot-mark, --text-dim, ...), so the
   hero would be correct on a paper section too even though it is on ink.

   THE ONE GEOMETRY FACT: the chart is a single viewBox, 1200x480, at every
   width. There is no second render and no JS branch. At 390px it draws about
   350x140, which is a legitimate trend at sparkline scale, and the argument
   is carried by the number above it.

   THE TWO STATES ON THE SECTION
     data-hero-state="replay|settled"   the seven-second replay, hero.js
     data-hero-live="live|down"         is the trainer reporting. Server sets
                                        it, hero.js keeps it. Under "down" the
                                        cached chart stays (it is still true)
                                        and everything that would read as a
                                        CURRENT reading stops: the head stops
                                        beating, the step columns go sharp and
                                        still, and .hero__stale says which
                                        measurement you are looking at.
   ========================================================================== */

.hero {
  /* assigns the variables, never the property — site.css header rule 4 */
  --pad-top: var(--space-5);
  --pad-bottom: var(--space-5);
  display: flex;
  align-items: center;
  /* one viewport, but never taller than 56rem on a very tall screen. min()
     rather than min-height + max-height, because a max-height that content
     overruns would spill the hero into the section under it. */
  min-height: min(calc(100svh - var(--nav-h)), 56rem);
}
.hero__wrap {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  width: 100%;
}

.hero__eyebrow {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: var(--tr-caps);
  text-transform: uppercase;
  color: var(--text-dim);
}

/* --- the number, and the interval it is never printed without ------------ */

.hero__stage { position: relative; }

/* AT REST it sits in the wedge the climb opens up: bottom right of the plot,
   under the trace's last third and above the 50% line. That corner is the one
   large area a rising line leaves empty, so the number needs no box, no scrim
   and no apology for covering the data.
   DURING THE REPLAY hero.js rides it on the head, up and to the right, and
   phase 3 settles it back into this slot. */
.hero__readout {
  position: absolute;
  right: 0;
  bottom: 14%;
  z-index: var(--z-raise);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-2);
  margin: 0;
  /* the trace passes under it; nothing here is a target */
  pointer-events: none;
}
.hero__share {
  font-family: var(--font-mono);
  font-size: var(--t-display);
  font-weight: var(--wt-medium);
  line-height: var(--lh-flat);
  letter-spacing: var(--tr-tight);
  color: var(--bot-mark);
}
/* WHILE IT RIDES, the number crosses the line that is drawing it, and two
   blue things the same colour on top of each other is unreadable. A soft
   radial scrim of the ground colour, with no edge anywhere, plus a halo on
   the glyphs themselves. Both are gone at rest, where the number sits in
   clear space and a permanent scrim would print a visible dark patch. */
[data-hero-state="replay"] .hero__readout {
  background: radial-gradient(ellipse 62% 74% at 55% 45%,
              var(--ground) 45%, transparent 78%);
}
[data-hero-state="replay"] .hero__share {
  text-shadow: 0 0 2rem var(--ground), 0 0 1rem var(--ground),
               0 0 0.5rem var(--ground), 0 0 0.25rem var(--ground);
}
[data-hero-state="replay"] .hero__band {
  text-shadow: 0 0 1rem var(--ground), 0 0 0.5rem var(--ground),
               0 0 0.25rem var(--ground);
}
.hero__band {
  font-family: var(--font-mono);
  font-size: var(--t-lead);
  line-height: var(--lh-flat);
  color: var(--text-dim);
}

/* The chart is cached and outlives the trainer, so when the trainer stops
   reporting the trace and the number are still true — they are just no longer
   current, and this is the line that says which. Amber, because it is neither
   the bot's blue nor the human's orange: it is a state. */
.hero__stale {
  display: none;
  max-width: 34ch;
  font-size: var(--t-small);
  line-height: var(--lh-tight);
  text-align: right;
  text-wrap: pretty;
  color: var(--warn-text);
}
[data-hero-live="down"] .hero__stale { display: block; }
/* the readout sits in the wedge above the 50% line, and the stale line is a
   third row it does not have room for: lift the block by its own height */
[data-hero-live="down"] .hero__readout { bottom: calc(14% + 3rem); }

/* --- the chart ----------------------------------------------------------- */

.hero__plot { position: relative; }
.hero__chart { display: block; width: 100%; height: auto; overflow: visible; }

.hero__ribbon { fill: var(--bot-mark); fill-opacity: 0.14; }
.hero__even {
  stroke: var(--text-faint);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}
.hero__even-label {
  position: absolute;
  left: 3.3%;               /* x = 40 of 1200. The left end: the right end of
                               this line is where the readout lives. */
  top: 89.6%;               /* y = 430 of 480 */
  transform: translateY(-125%);
  font-size: var(--t-micro);
  color: var(--text-dim);
  pointer-events: none;
}
/* THE RAW READINGS. Every one of them, at full amplitude, nothing smoothed
   away — but a stroke weight back, because 609 readings that swing a point
   between neighbours are the texture the claim is made of, not the claim. */
.hero__trace {
  fill: none;
  stroke: var(--bot-mark);
  stroke-width: 1.5;
  stroke-opacity: 0.45;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* THE ROLLING MEDIAN, through the raw and not instead of it. This is the line
   that carries "it is still climbing", so it is the one at full weight. It is
   labelled in .hero__meta: a derived line has to say what it is. */
.hero__median {
  fill: none;
  stroke: var(--bot-mark);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* A reading that beat every reading before it. Resting: an outline. Lit: a
   filled dot, one 300ms flash, then it stays lit for good. */
.rec {
  fill: var(--ground);
  stroke: var(--bot-mark);
  stroke-width: 1.5;
  transition: r var(--dur-mid) var(--ease-out),
              fill var(--dur-mid) var(--ease-out),
              opacity var(--dur-mid) var(--ease-out);
}
.rec--lit { fill: var(--bot-mark); r: 3.5; }
/* while it replays, a dot the head has not reached yet is not there at all:
   thirty-seven outlines ahead of the line would draw the ending first */
[data-hero-state="replay"] .rec:not(.rec--lit) { opacity: 0; }

/* The two annotations stand aside for the seven seconds the number is riding
   through the space they sit in, and arrive with the settled composition. */
.hero__even-label,
.hero__meta { transition: opacity var(--dur-slow) var(--ease-out); }
[data-hero-state="replay"] .hero__even-label,
[data-hero-state="replay"] .hero__meta { opacity: 0; }
.rec--flash { animation: rec-flash 300ms var(--ease-out); }
@keyframes rec-flash {
  0%   { r: 3.5; opacity: 1; }
  40%  { r: 8; opacity: 0.55; }
  100% { r: 3.5; opacity: 1; }
}

.hero__head-dot { fill: var(--bot-mark); }
.hero__head-ring {
  fill: var(--bot-mark);
  opacity: 0.25;
  /* the same heartbeat the live badges use, from site.css */
  animation: pulse 2.4s var(--ease-inout) infinite;
}
/* A pulse is a claim that something is happening right now. Nothing is. */
[data-hero-live="down"] .hero__head-ring { animation: none; opacity: 0.1; }
[data-hero-live="down"] .hero__head-dot { fill: var(--text-dim); }

.hero__meta {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
  margin: 0;
  font-size: var(--t-micro);
  color: var(--text-dim);
}
.hero__meta-next { font-family: var(--font-mono); }

/* --- the words ----------------------------------------------------------- */

.hero__heading {
  margin: 0;
  font-size: var(--t-head);
  max-width: 22ch;
}
.hero__lead {
  margin: 0;
  max-width: 40rem;
  color: var(--text-dim);
}

/* --- the step smear ------------------------------------------------------
   Each column turns over at its own true rate: column k from the right at
   sps / 10^k per second. At or above 5/s a digit is no longer readable and
   the column is blurred; under that it is a reel you can watch turn; under
   1/s it is a digit you can read, changing when it actually changes. Every
   boundary moves with sps, and the same three-way test is in hero.html.

   THE FOOTPRINT LAW, and the reason this is a reel and not a blurred glyph.
   A column is exactly 1ch wide and exactly 1em tall and it clips, in every
   state. What is blurred is a STRIP of digits, so the blur has digit texture
   above and below the window to smear into and the window fills top to
   bottom; blurring one glyph instead leaves a lozenge with a gap over and
   under it, the filter region spills past the glyph box, and twelve digits
   stop reading as one twelve-digit number and start reading as a truncated
   number with light bars next to it. Clip the reel, blur the reel, never blur
   past the box.

   ONE VARIABLE, --smear, drives the whole down-state: it goes to 0 and every
   blurred layer cross-fades to the sharp glyph underneath while the reels
   stop. A number becoming legible is the exact visual opposite of alive. */

.hero__steps {
  --smear: 1;
  --dur-smear: 600ms;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
}
.steps {
  display: flex;
  align-items: flex-start;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums lining-nums;
  font-weight: var(--wt-medium);
  line-height: 1;
  color: var(--bot-text);
  /* 15 glyphs at the mono advance ratio, or --t-title, whichever is smaller.
     One expression: no breakpoint to maintain and no horizontal body scroll
     at any width. Spline Sans Mono's advance is exactly 0.6em, measured. */
  font-size: min(var(--t-title),
                 calc((100vw - 2 * var(--space-gutter)) / var(--glyphs, 15) / 0.6));
  transition: color var(--dur-smear) linear;
}
/* one column of the number: one glyph advance wide, one line tall, clipping */
.d {
  position: relative;
  display: block;
  flex: 0 0 auto;
  width: 1ch;
  height: 1em;
  overflow: hidden;
}
/* the group separators carry no rate and no reel, and a comma has a tail that
   drops below the line box, so this is the one column that does not clip */
.d--sep { overflow: visible; color: var(--text-faint); }
/* Every column carries both layers and the state picks one, so a change in
   sps only rewrites an attribute and the swap cross-fades. The sharp glyph is
   the default: if anything here fails to load, the number is readable. */
.d__sharp {
  display: block;
  height: 1em;
  line-height: 1;
  transition: opacity var(--dur-smear) linear;
}
/* 13 rows: '9', then 0-9, then '0','1'. Row d+1 is digit d, so the resting
   transform below puts the true digit in the window, pixel-aligned with the
   sharp glyph underneath it. The 10-row cycle plus the 1-row window leaves a
   row of margin at each end, which is what the blur reaches into — it must
   never reach the end of the strip or the column flickers once a cycle.

   THE MASK is the footprint law again, vertically. A row of the strip is 1em
   but the ink in it runs 11% to 88% of that, so an unmasked reel paints a
   band a third taller than the digits standing next to it — which is exactly
   what "the blurred columns extend past where digits would be" describes.
   The mask cuts the reel back to the digits' own ink band, with four percent
   of feather at each edge so the window has no hard lid on it. */
.d__reel {
  position: absolute;
  top: 0;
  left: 0;
  white-space: pre;
  line-height: 1;
  mask-image: linear-gradient(to bottom, transparent 0 9%, #000 13% 86%,
                              transparent 90% 100%);
  opacity: 0;
  /* visibility, not just opacity: the reel is thirteen glyphs tall and an
     invisible one still lands in innerText, in a text selection, and in any
     count of what is on the page. It is hidden after its own fade, so the
     cross-fade still plays. */
  visibility: hidden;
  transform: translateY(calc((var(--start, 0) + 1) * -1em));
  transition: opacity var(--dur-smear) linear,
              visibility 0s linear var(--dur-smear);
}
.d[data-state="band"] .d__reel,
.d[data-state="reel"] .d__reel {
  opacity: var(--smear);
  visibility: visible;
  transition: opacity var(--dur-smear) linear, visibility 0s;
}
.d[data-state="band"] .d__sharp,
.d[data-state="reel"] .d__sharp { opacity: calc(1 - var(--smear)); }

/* EVERY COLUMN THAT IS NOT STILL TURNS, and the only difference between the
   two moving states is the blur. --start is folded into a negative
   animation-delay so the strip needs 13 rows and not 22.

   THE 6/s CAP is a property of the display, not of the trainer. A column
   turning over 100 times a second moves nearly two whole rows between two
   frames of a 60Hz screen, and since the strip repeats every row, drawing it
   at its true rate produces a pattern that jumps a fraction of a row per
   frame: noise, not motion. Past 6 rows a second nobody can tell one speed
   from another anyway, so the turn saturates there and what carries the rate
   from that point on is the blur — which is also the honest thing to look at,
   because the column really has stopped being resolvable. The true rate is
   never inferred from this picture: it is printed underneath in figures.
   Never let this stand in for the number. */
.d[data-state="band"] .d__reel,
.d[data-state="reel"] .d__reel {
  animation: hero-reel calc(10s / min(var(--rate, 1), 6)) linear infinite;
  animation-delay: calc(var(--start, 0) * -1s / min(var(--rate, 1), 6));
  will-change: transform;
}
/* --blur is the column's own filter, from the template: vertical only, and
   sized so the rows are gone but their ghost is not. */
.d[data-state="band"] .d__reel { filter: var(--blur); }
@keyframes hero-reel {
  from { transform: translateY(-1em); }
  to   { transform: translateY(-11em); }
}
/* Reduced motion: site.css collapses every animation to one 1ms iteration, so
   every reel reverts to its base transform, which is its true digit. A column
   that has stopped turning should show that digit rather than a blur of a
   thing that is not moving, so the reel state hands back to the sharp glyph.
   The band columns keep their smear and lose only the turn: a still
   photograph of a column going a hundred times a second is exactly that
   smear, and it is the one honest thing to draw when motion is unwelcome. */
@media (prefers-reduced-motion: reduce) {
  .d[data-state="reel"] .d__reel { opacity: 0; visibility: hidden; }
  .d[data-state="reel"] .d__sharp { opacity: 1; }
}
/* the whole number as plain text: the state where the server had no reading
   to classify into columns. Smaller than the smear on purpose — it is what
   this page can still say, not a stand-in dressed as the real thing. */
.steps__plain { font-size: var(--t-head); line-height: 1; }
.hero__steps-label {
  font-family: var(--font-mono);
  font-size: var(--t-small);
  color: var(--text-dim);
}
.hero__steps-label:empty { display: none; }

/* Two consecutive failed polls. Never a zero, never a spinner: the number
   goes completely sharp and completely still, and the label says when it was
   last read. */
.hero__steps[data-down] { --smear: 0; }
.hero__steps[data-down] .steps { color: var(--text-faint); }
.hero__steps[data-down] .d__sharp { opacity: 1; }
.hero__steps[data-down] .d__reel { animation-play-state: paused; }

/* --- actions ------------------------------------------------------------- */

.hero__actions { --cluster-gap: var(--space-4); }
.hero__replay {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--t-small);
  color: var(--text-dim);
  text-decoration: underline;
  text-underline-offset: 0.3em;
  cursor: pointer;
}
.hero__replay:hover { color: var(--text); }

/* --- the scroll rail -----------------------------------------------------
   All that survives of the signature line: scroll position still maps to
   training time. hero.js builds it, so with no JS there is no dead furniture.
   It lives inside the sticky nav, which is what keeps it ground-correct as
   the page alternates ink and paper. */

.rail {
  position: absolute;
  left: 0;
  bottom: -1px;
  width: 100%;
  height: 2px;
  background: var(--bot-mark);
  transform-origin: 0 50%;
  transform: scaleX(var(--p, 0));
  pointer-events: none;
}
.rail__label {
  position: absolute;
  right: var(--space-gutter);
  bottom: -1.5rem;
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--text-faint);
  pointer-events: none;
}
@media (max-width: 48rem) {
  /* the nav is two rows here and the label would land on the copy */
  .rail__label { display: none; }
}

/* --- phone ---------------------------------------------------------------
   The same composition, not a stripped one. The number stops travelling and
   sits above the chart; the head still runs along the trace. One rule, no JS
   branch. */

@media (max-width: 40rem) {
  .hero {
    min-height: 0;          /* content-driven: nothing is locked to 100svh */
    --pad-top: var(--space-5);
  }
  .hero__stage { display: flex; flex-direction: column; gap: var(--space-3); }
  /* the strip under the 50% line is 14px tall here, so the caption goes back
     into the flow rather than sitting on the trace */
  .hero__meta { position: static; }
  /* and the axis label goes under its line: at sparkline scale the trace's
     left end is only a few pixels above 50% and the label lands on it */
  .hero__even-label { transform: translateY(20%); }
  .hero__readout {
    position: static;
    align-items: flex-start;
    /* the replay must not move it here */
    transform: none !important;
  }
  .hero__stale { text-align: left; }
  .hero__even-label { font-size: var(--t-micro); }
  .rec { r: 2.5; }
  .rec--lit { r: 3; }
  .hero__trace { stroke-width: 1.25; }
  .hero__median { stroke-width: 2.25; }
  .hero__actions .btn { flex: 1 0 100%; }
  @keyframes rec-flash {
    0%   { r: 3; opacity: 1; }
    40%  { r: 7; opacity: 0.55; }
    100% { r: 3; opacity: 1; }
  }
}
