/* ==========================================================================
   Cast Beacon — the scroll-tracked H2 stage

   THE PIN IS SCROLL DISTANCE, NOT A SCROLL HANDLER. `.stg__track` is tall and
   `.stg__pin` is sticky inside it, so the stage holds still while the page
   scrolls past it. Nothing here fights the scroller, which is why the section
   still behaves under a trackpad, a keyboard, find-in-page and a screen reader.

   NOTHING IN HERE IS ARMED UNTIL stage.js SAYS SO. The track is display:none
   until `html.stage-on` exists, and that class is added on the last line of
   stage.js — after the graph is built and the first frame is painted. Until
   then (and forever, if JS is off, if the file throws, or if the reader has
   asked for reduced motion) `.stg__static` is the section: the same argument
   in prose, with a real graph beside it.
   ========================================================================== */

.stg{padding:0;overflow:clip}
.stg__track{display:none}
.stage-on .stg__track{display:block}

/* THE WRITTEN VERSION IS NOT DELETED WHEN THE ANIMATED ONE ARMS.

   It was display:none, which takes it out of the accessibility tree and puts
   it in the category search engines discount as hidden content — for a section
   whose entire argument lives in text a script types into empty elements, that
   is the whole section discounted. Clipped instead: still laid out, still read
   by screen readers, still indexed as real content, and saying exactly what the
   animation says, which is the condition that makes this honest rather than
   cloaking.

   The graph inside it does go, because it is a picture and there is a better
   one on screen. */
.stage-on .stg__static{
  position:absolute;
  width:1px;height:1px;
  margin:-1px;padding:0;border:0;
  overflow:hidden;
  clip:rect(0 0 0 0);
  clip-path:inset(50%);
}
.stage-on .stg__static .split__b{display:none}

/* The height is written by stage.js from the beat table, so a beat added or
   retuned in one file cannot silently disagree with the scroll length in the
   other. This is only the floor. */
.stg__track{height:900vh}

.stg__pin{
  position:sticky;
  top:var(--barh);
  height:calc(100svh - var(--barh));
  min-height:520px;
  display:flex;
  align-items:stretch;
}
.stg__inner{
  display:grid;
  grid-template-rows:auto auto minmax(0,1fr) auto;
  gap:clamp(12px,1.8vh,26px);
  padding:clamp(16px,2.4vh,34px) var(--pad) clamp(20px,3vh,40px);
  width:100%;
}

/* ---------- the two typed lines ----------
   Same treatment as the hero headline — heavy, tight, black ink with the one
   orange mark — one step down in size, because this is the H2 of the page and
   the hero has to stay the largest thing on it. */
.stg__line{
  font-size:clamp(23px,3vw,44px);
  line-height:1.05;
  letter-spacing:-.03em;
  font-weight:800;
  color:var(--on-bone);
  /* A DEFINITE width, not a shrink-to-fit one. The line breaks are measured
     against this box before a character is typed, so the box cannot be allowed
     to depend on what is currently in it — that would be circular, and on the
     flush-right price line it would move the text every time a word landed. */
  width:100%;
  max-width:30ch;
  position:relative;
  min-height:1.05em;          /* the line holds its space before it is written */
  transition:none;            /* the sequence owns these values, not the cascade */
}

.stg__body{display:block;position:relative}

/* THE WHEEL. Three machine names on the surface of a cylinder, on their own
   line under the stem — between the second gate and the fourth the sentence
   above never changes and these are the only thing that does, so they are the
   only thing that turns.

   The window is one line tall, and the room around it — written by fit(), in
   pixels, because it is derived from the measured line height — is where a
   face finishes fading out. It is generous on purpose: the reader must never
   catch a name being cut off by the edge of its frame, so the frame is set
   back past the point where there is anything left to cut. The negative
   margin hands that room straight back to the layout, so nothing below the
   sentence moves to make space for a frame that only catches the invisible. */
.stg__wheel{
  display:block;
  position:relative;
  overflow:hidden;
  /* CONTENT-BOX, against the page's global border-box. stage.js sets this
     height to exactly one line, and the padding either side is the fade's
     room — under border-box the padding would come out of that one line
     instead of adding to it, and the window would close around the ink. */
  box-sizing:content-box;
  perspective:1400px;
  perspective-origin:50% 50%;
  transform-style:preserve-3d;
  will-change:transform,opacity;
}
.stg__wface{
  display:block;
  position:absolute;
  left:0;right:0;top:0;
  white-space:pre;
  backface-visibility:hidden;
  transform-origin:50% 50%;
  will-change:transform,opacity;
}

/* ACT ONE, KEPT. The wheel's first gate is the whole of the first act, so each
   line holds a second finished copy of what it said there. It sits over the
   live line and the two are rolled past each other — laid out, never typed,
   and out of the accessibility tree because the clipped prose block already
   says all of this once. */
.stg__gate{
  display:block;
  position:absolute;
  left:0;right:0;top:0;
  pointer-events:none;
  will-change:transform,opacity;
}
.stg__body{will-change:transform,opacity}

/* Same cylinder, in the graph. The cards are already inside a clipping slot,
   so this only has to supply the depth — deeper here, because the wheel these
   cards turn on is over two hundred pixels across. */
.stg__slot{perspective:1600px;perspective-origin:50% 50%}

/* One block per line of the finished sentence, so a block cannot rewrap: the
   words that belong to line two were decided when the sentence was set, not
   when the typing reached them. `pre` because these are exact strings now,
   including the spaces inside them. */
.stg__ln{display:block;white-space:pre}

/* The rest of the sentence: in the layout, out of sight. Not display:none and
   not opacity — it has to take up its space to hold the line's shape. */
.stg__ghost{visibility:hidden}

/* The provisional full stop. Same zero-width trick as the caret, and for the
   same reason: it appears, is read, and is taken back, and none of that is
   allowed to move the sentence it sits at the end of. */
.stg__stop{display:inline-block;width:0;overflow:visible;color:var(--beam)}

/* Where the line breaks are measured. Same box, same type, normal wrapping —
   out of the flow so that measuring costs the visible line nothing. */
.stg__probe{
  position:absolute;left:0;right:0;top:0;
  visibility:hidden;pointer-events:none;white-space:normal;
}
.stg__line--bot{
  justify-self:end;
  text-align:right;
  max-width:26ch;
}
/* The accent this section types words in — the machine being named, and every
   full stop. Same colour as the mark the hero resolves into and the panel the
   page ends on, at a value bone can actually carry. */
.stg__o{color:var(--beam)}

/* The caret. Solid while the sequence is moving — which is to say, while the
   reader is scrolling — and blinking the moment it stops, the way a cursor in
   any editor behaves when you take your hands off the keys. */
.stg__caret{
  color:var(--beam);
  font-weight:800;
  /* ZERO WIDTH. A cursor must not be part of the line's measurements: when it
     moved from the end of one line to the start of the next, the line it left
     got narrower by exactly one underscore, and on the flush-right price line
     that shunted the whole sentence sideways. It is drawn but never counted. */
  display:inline-block;
  width:0;
  overflow:visible;
}
.stg__caret.is-blink{animation:cb-caret-blink 1s steps(1,end) infinite}

/* ---------- the canvas ---------- */
.stg__canvas{
  display:flex;
  align-items:center;
  justify-content:center;
  min-height:0;
  overflow:hidden;
}
.stg__scale{position:relative;transform-origin:50% 50%}
.stg__nodes{position:absolute;inset:0}
.stg__wires{position:absolute;inset:0;overflow:visible}

/* Boxes are moved and faded thirty times a second from scroll position. A CSS
   transition on top of that would be a second opinion about where a node is,
   arriving late. */
.stg__node{transition:none;cursor:default}
.stg__node:focus{outline:none}

/* The one place this stage departs from the static graphs: its nodes are
   FILLED, in the plate's own colour, so they are identical to look at and
   different to overlap. The rolodex slides one card out from behind another,
   and two unfilled cards mid-swap read as a single illegible box with two
   titles in it. Filling in the ground colour costs nothing visually here
   because no wire on this canvas is routed across a node body. */
.stg__node{background:var(--bone)}

/* The rolodex frame. Exactly one card is visible in here at a time; the rest
   are outside the clip, above or below. Its size and position come from
   stage.js, which derives them from the node heights rather than repeating
   them. */
.stg__slot{position:absolute;overflow:hidden}
.stg__slot .stg__node{left:0}
.stg__node.is-front{z-index:3}
.stg__node.is-back{z-index:1}

/* Their box. Dashed, and greyed to the muted ink — it is in the picture to be
   compared with, not to be read as part of your pipeline. */
.stg__node--ext{
  border-style:dashed;
  border-color:var(--on-bone-3);
  color:var(--on-bone-2);
}
.stg__node--ext .stu__ttl{color:var(--on-bone-2)}

.stg__wire{transition:none}

/* ---------- the static section ----------
   Not a fallback in the apologetic sense: this is the whole argument, written
   out, and it is what the section is until stage.js proves it can do better. */
.stg__static{padding:clamp(56px,7vw,110px) var(--pad) clamp(64px,8vw,124px)}
.stg__static .stg__say{
  font-size:clamp(23px,2.6vw,38px);
  line-height:1.15;
  letter-spacing:-.03em;
  font-weight:800;
  max-width:34ch;
  margin-bottom:clamp(20px,2.4vw,32px);
}
.stg__static .stg__say em{font-style:normal;color:var(--beam)}

/* ---------- narrow ----------
   The stage keeps its shape rather than restacking: the argument is a picture
   of a fan-out, and a fan-out drawn as a column is a chain. The graph scales
   to fit instead — see fit() in stage.js, which clamps on height as well as
   width, because on a phone in portrait the height is what runs out first. */
@media (max-width:760px){
  .stg__pin{min-height:460px}
  .stg__line{font-size:clamp(20px,5.2vw,30px);max-width:24ch}
  .stg__line--bot{max-width:22ch}
  .stg__inner{gap:clamp(10px,1.4vh,18px)}
}

/* A short landscape phone cannot hold a headline, a graph and a price line at
   readable sizes at once. Give the type the room and let the graph take what
   is left — it is the sentence that carries the meaning here. */
@media (max-height:560px){
  .stg__line{font-size:clamp(18px,2.4vw,26px)}
}
