/* stage.css - piece P2 (Stage composition). Owner: P2.
   ONE LOCKUP IN A FIELD OF LATTICE: object, statement, sub-line, on one axis,
   held by two measured gaps (48px under the object's ink, 40px under the
   statement's caps - the second rounded to the dot rhythm, 42 at 1440x900)
   with the air left over falling around the whole thing: 32px over the object
   against 43px under the sub-line at 1440x900.
   THE OBJECT CARRIES THE SCREEN (this round's fix): that air was 152/151 around
   a 384 mount, and the Storyteller's portrait was 290px of ink in a 1440 frame -
   "a smudge rather than a form", a blind critic measured, asking for 430-460px
   of head. The mount is now the least of a share of the FIELD, a cap on the
   display's width, and the page's own measure (stage.js: FINE.fillShare,
   widthCap, FINE.airRows) - 576 at 1440x900, which is 436 x 641 of portrait.
   On the ground (base.css's dot lattice; nothing here paints or decorates)
   the stage holds:
     .stage__center   the centrepiece's mount: a tile-snapped square, sized by
                      the field and capped by the display, snapped to an even
                      number of tiles by stage.js - 576 @1440x900, 432
                      @768x1024, 348 @390x844 (where the page's own measure
                      binds) - centred horizontally on a tile column. P13 / P14
                      render INSIDE it. Its top is where stage.js puts the composition's
                      optical centre: the whole LOCKUP (the painted box - the
                      mount plus or minus what the centrepiece actually lights -
                      plus the gap, the statement, the tail and the sub-line)
                      centres in the field, lifted one tile, then steps clear of
                      the band's seam. Storyteller mount 112..688 @1440x900
                      viewport, its portrait's ink 87..728.
     the statement    lit cells on the ground canvas (stage.js): a 3px glyph
                      pixel on the desktop (cap 21px), 2px on the phone (cap
                      14px), solid strokes, --ink, each line centred on the
                      mount's own midline TO THE PIXEL, six tiles below the
                      PAINTED box (48 / 36). Measured from the INK, not the
                      mount and not the canvas: P13's face canvas is the mount
                      grown 12.5% a side while the portrait's ink stops 25px
                      short of it, and P14's wave stops 56px INSIDE its own
                      mount, so one gap number rendered as 65px of air under the
                      beard against 96px under the wave. stage.js reads the alpha
                      channel of the mount's canvases and the shares are SIGNED,
                      so both twins now stand their line the same distance off
                      their object's real floor. A centrepiece may still DECLARE
                      its overhang with --center-bleed (or P13's --face-bleed)
                      and that is what the first frame uses, before anything has
                      painted. Its own measure is two tiles tighter than the
                      frame on each side, so at 390 it wraps rather than run kerb
                      to kerb. On a twin carrying data-statement-mode="rewrite"
                      the canvas stays clear and .stage__line is the slot P14
                      types into - which since 2026-09-04 is BOTH of them: Dan
                      asked for one sentence, BE THE CURE FOR AI SLOP, with the
                      Developer's re-writing behaviour, on the Storyteller too.
     .stage__line     that slot: top = the statement's top, left = the axis,
                      width = the display, height = the cap. --stage-glyph
                      (3px / 2px) and --stage-line-cx say how and where.
     .stage__cue      "scroll up" beside Dan's own Point.svg finger, drawn in
                      his own upward orientation. It is FURNITURE, not a line of
                      lockup: it seats its own box on the frame's floor, in the
                      bottom air, so the composition above it does not move and
                      the object keeps its size. See the block near the foot of
                      this file for why, and for what reduced motion does.
     .stage__caption  the SUB-LINE: the page's own description, broken where
                      stage.js's balanceLines says (balanced, and never ending on
                      a connective, which `text-wrap: balance` cannot promise),
                      READOUT 12px in --stage-cap-ink, ONE line at every desktop
                      width, five tiles under the statement's caps and centred on
                      --stage-cap-cx - the STATEMENT'S axis, not the viewport's.
                      For three rounds it was a corner mark at x = 96 and three
                      blind critics judging three different pieces called the
                      same fault: three alignment axes, no counterweight. Moving
                      it onto the composition's axis fixed that and exposed the
                      next one, which a fourth critic named exactly: seated on
                      the frame's bottom line it sat ~90px under the statement,
                      led with the side's name in ink while the top-right rail
                      read "DEVELOPER ->", and set the rest three tonal steps
                      down. "It reads as leftover metadata, not a statement."
                      So it is no longer a mark that shares an axis - it is the
                      statement's own caption, inside the lockup the field
                      centres: the gap is 40px nominal (42 as the READOUT's own
                      dot rhythm rounds it), the kicker is gone (the
                      descriptor says what the side is far better than its name
                      did), the tone comes up 40% of the way to --ink, and the
                      axis is the lit line's own to the pixel.
   Every position is a CSS custom property written by stage.js from the
   lattice's own paint, so the DOM boxes sit on the same tile columns and rows
   as the lit cells. --stage-x is still the page's LEFT axis (the lattice's
   margin: 96 @1440, 72 @1280/1024, 48 @768, 24 @430, 18 @390 - identical on
   both twins); the bar and the strip read it for their own insets. No colours
   beyond the tokens. */

.stage {
  --stage-x: 96px;
  --stage-glyph: 3px;
  /* THE SUB-LINE IS ONE BLOCK, ON ONE LEADING - its own line box (18), so a
     wrapped descriptor (430 and below) leads as one paragraph and not at a
     wider pitch. The BLOCK's seat is on the dot grid - stage.js puts its first
     line box's caps between two dot rows through nearestGutterLine - so what
     lands on a tile row is the block, not each line's leading, which belongs to
     the type. voxel.css sets the same 18 for the same reason; it is a harmless
     duplicate of this line and can go whenever P14 next touches that file. */
  --stage-corner-pitch: 18px;
  --stage-corner-box: 18px;     /* the READOUT line box: 12px caps centred */
  /* THE SUB-LINE'S TONE, one step under the lit statement instead of three.
     The site's --dim is chrome's tone - the bar's clock, the showcase's meta -
     and at 12px in a hairline mono on the black page it RENDERS at a median of
     82/255 (measured on the capture), which a blind critic read as "#6f6f6f
     ... three tonal steps below the headline". This line is not chrome; it is
     the statement's caption, so it takes --gray pulled 40% of the way to --ink:
     #B4B4B4 on the black page (9.5:1, rendering at a median near #909090 -
     the tone the critic asked for) and #575757 on the white one (6.4:1, and
     comfortably past the 4.5:1 the rubric wants of text there). Still the gray
     role, still between two tokens; no fifth colour enters the page. */
  --stage-cap-ink: color-mix(in srgb, var(--gray) 60%, var(--ink));
}

/* first-frame defaults before stage:layout arrives (stage.js overwrites) */
@media (max-width: 767px) {
  .stage {
    --stage-x: 18px;
    --stage-glyph: 2px;
  }
}

/* ---- the centrepiece's mount ---- */
.stage__center {
  /* THE CONTRACT a centrepiece owns: where its paint stands relative to this
     box, PER SIDE and SIGNED, as a length or a % of the box. Positive spills
     past the mount, negative stops short of it. It is the FIRST FRAME's answer -
     before a centrepiece has drawn there is nothing to measure and this is what
     the composition stands on; from the moment there IS paint, stage.js reads
     the alpha channel of the mount's canvases and the measurement wins in both
     directions.
     DECLARING IT ACCURATELY IS NOT A NICETY (Dan, 2026-09-04). The first frame
     is a frame the visitor sees: P13 declared a symmetric +10% while its head's
     ink stops about a tenth of the mount INSIDE the box, and the statement
     painted 48px low for three frames before the measurement moved it - "when
     you first enter the page it is positioned lower on the screen and then it
     jumps back to under the bear." A centrepiece that declares these two
     honestly composes the same column at 0ms and at 3s; stage.js compares the
     declaration against the settled measurement and warns when they part, so a
     stale number is reported and never merely endured.
     Set these, never a margin: the mount's own box is P2's. */
  --center-ink-top: 0px;
  --center-ink-bottom: 0px;
  /* The symmetric, positive legacy of the pair above: how far past the box the
     paint spills, the same on every side. Read only when neither side above is
     declared. */
  --center-bleed: 0px;
  /* THE FIRST FRAME'S SEED, before stage.js has written --stage-center-size:
     the same three terms it uses, in CSS. The field is the stage (the viewport
     under the 48px bar), the ramp is widthCap()'s two anchors as a clamp, and
     the page's own measure is the guard. It used to be the spec's
     clamp(280px, 42vh, 520px) = 378 at 900 tall, and the centrepieces' first
     ResizeObserver notification measured their field against THAT - 200px away
     from what the composition settles on, and one wasted settle for each of
     them. Keeping the two in step is the whole point of writing it twice. */
     THE GUARD IS IN vw, NOT %, AND THAT IS THE WHOLE POINT (2026-09-04). One
     custom property feeds BOTH `width` and `height` below, and `100%` does not
     mean one thing in those two places: it is the containing block's width in
     one and its height in the other. At 390x844 that resolved to a 354 x 432
     mount - the seed was not square - and P13 allocated a 425 x 518 canvas for
     it, which the mount's real 348px square then squashed to 418 x 418 the
     moment stage.js composed. A vertically squashed head measures as a head
     with less ink in it, and the settled box P2 reads off that measurement came
     back 0.16 of the mount instead of 0.084 - the phone composed a different
     column from the desktop for no reason anyone could see. A width-based unit
     means the same length in both properties, so the seed is square wherever it
     lands. */
  --stage-center-seed: min(
    calc(0.68 * (100svh - 48px)),
    clamp(432px, calc(21.43vw + 267px), 576px),
    calc(100vw - 2 * var(--stage-x))
  );
  position: absolute;
  top: var(--stage-center-top, 20%);
  left: var(--stage-center-left, 50%);
  /* width AND height, explicitly: stage.js snaps the seed to an even number
     of tiles and writes it; a sibling's zero-specificity fallback (voxel.css
     :where()) must never be the one that sets either side */
  width: var(--stage-center-size, var(--stage-center-seed));
  height: var(--stage-center-size, var(--stage-center-seed));
  z-index: 1;
  margin: 0;
  padding: 0;
  overflow: visible;
  /* The mount is a decorative frame, not a surface: it is aria-hidden, nothing
     inside it listens (both centrepieces take their canvas out of the hit test
     and P13 reads the pointer off the window), and the painted box it stands
     for may now overlap the statement's row - P14's wave stops 56px inside its
     own mount, so the mount's box outlives its paint at the bottom. A
     transparent box that eats a hover is a bug waiting for a reason. */
  pointer-events: none;
}
/* until the lattice has measured, the box is centred by transform and unseen;
   the first visible frame is the composed one */
.stage:not([data-stage-ready]) .stage__center {
  transform: translateX(-50%);
  visibility: hidden;
}

/* ---- the re-writing statement's slot (BOTH twins now; P14 draws inside) ---- */
.stage__line {
  position: absolute;
  top: var(--stage-line-top, 70%);
  left: var(--stage-line-x, var(--stage-x));
  width: var(--stage-line-w, calc(100% - 2 * var(--stage-x)));
  height: var(--stage-line-h, calc(7 * var(--stage-glyph)));
  z-index: 1;
  margin: 0;
  padding: 0;
  pointer-events: none;
}
.stage:not([data-stage-ready]) .stage__line { visibility: hidden; }

/* THE TYPED LINE'S CANVAS, DECLARED WHERE BOTH TWINS READ IT (2026-09-04).
   rewrite.js appends this canvas to the slot above, and until today the only
   rules for it lived in voxel.css - a sheet the Developer loads and the
   Storyteller does not. The moment Dan asked for the same re-writing statement
   on the Storyteller, that made the two twins render one object under two sets
   of rules, which is the exact divergence a caption leading set from voxel.css
   already cost this project a round for. The slot is P2's box, so the rule that
   fills it belongs beside the slot. Identical values to voxel.css's copy, which
   loads after this file on the Developer and changes nothing there; that copy
   can go whenever P14 next touches its own sheet. */
.rewrite__canvas {
  position: absolute;
  top: 0;
  left: 0;
  display: block;
  max-width: none;          /* base.css caps canvases at 100%; the slot's canvas is sized in px */
  pointer-events: none;
}

/* ---- the sub-line: the statement's own caption, on the statement's axis ---- */
.stage__caption {
  position: absolute;
  /* stage.js places the top (the line box five tiles under the lit caps, its
     own caps then centred between two dot rows) and the AXIS. The box spans
     0 .. 2 * cx so its midline IS that axis without a transform: the statement
     and its sub-line are centred on one number, and the ~6px disagreement a
     critic caught - the lit line's tile-snapped ink centre against a block
     centred on the viewport - cannot come back by construction. .stage is
     overflow: hidden, so a box wider than the display (390, where the mount's
     midline sits left of centre) is clipped and never scrolls the page. */
  top: var(--stage-cap-top, 76%);
  left: 0;
  width: calc(2 * var(--stage-cap-cx, 50%));
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  row-gap: calc(var(--stage-corner-pitch) - var(--stage-corner-box));
  margin: 0;
  text-align: center;
  pointer-events: none;
  user-select: none;
  color: var(--stage-cap-ink);
}

.stage__meta {
  margin: 0;
  font-family: var(--font-readout);
  font-size: var(--fs-12);
  line-height: var(--stage-corner-box);
  letter-spacing: var(--track-readout);
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  color: inherit;
  /* ONE line wherever the display holds one - the Storyteller's descriptor is
     59 READOUT characters (494px at 1440) under a 503px statement and the
     Developer's is 63 (528px) under a 468px one, so the pair reads as one measure.
     stage.js writes the px value it actually set the text to; the `ch` fallback
     is Fragment Mono's own (7.42px at 12px), which is why the measure lives on
     THIS element and not on the block, whose font is the body's. It wraps at
     430 and below, where the frame itself is narrower than the sentence. */
  max-width: min(var(--stage-meta-measure, 64ch), 100%);
  white-space: normal;
}

.stage__meta-line {
  /* One chosen line per box. The break is picked in stage.js (balanceLines:
     fewest lines, then balanced, then never ending on a connective) because
     `text-wrap: balance` balances LENGTHS and is indifferent to ENDINGS - it
     hung "BREADSTICK, ARES, AGENTVILLE AND /" off the Developer for three
     rounds. Having chosen the line, nothing may re-wrap it. */
  display: block;
  white-space: nowrap;
}

/* ---- the scroll cue: the frame's furniture, not the lockup's third line ----
   Dan asked for this twice, and the second ask REPLACES the first. On the
   morning of 2026-09-04: "let's put 'scroll down' and use the Point.svg icon
   but upside down so that way the finger is instructing the user to scroll
   down." Then, the same day: "I miscalculated, the little finger that says
   'scroll down' at the bottom of both pages should be 'scroll up' with the
   finger pointing up." So the words are "scroll up" and the mark is Dan's
   artwork in its OWN orientation - the 180deg turn that used to live in
   .stage__cue-mark is gone rather than re-aimed, which is the smaller rule.

   WHERE IT SITS IS THE WHOLE DESIGN DECISION. The lockup - object, statement,
   sub-line - is one column the field centres, and a fourth member would have
   cost the object the size a blind critic spent a round asking for (every 16px
   the column grows takes 16px off the mount, and the mount's 560 is 435px of
   portrait). So the cue is NOT in the lockup. It seats its own box on the
   frame's floor - the same --stage-x-free bottom air the sub-line keeps off the
   stage's edge (FINE.airRows, 32px) - which is where a scroll affordance
   belongs anyway: at the bottom of the screen, in the margin, pointing off it.
   At 1440x900 that leaves 55px between the sub-line's baseline box and the
   cue's top against the lockup's own 48/42 - so the largest gap on the stage is
   the one BETWEEN the composition and its furniture, and the cue reads as a
   separate object rather than a third line of copy. stage.js still reserves the
   block (FINE.cueGapRows + cueBox) in the column's fit test, so on a field too
   short for both the OBJECT gives way and the cue is never dropped silently.

   It is a cue, not a control: no href, no button, no hover state, no focus
   stop, pointer-events off, aria-hidden (the "Skip to the work" link is the
   real affordance in the accessibility tree, and a screen reader has no use for
   "scroll up"). The words are real text in READOUT; only the mark is a glyph.

   IT DOES NOT MOVE. Everything else on this site is still unless it is the
   centrepiece or the typing line, and a bobbing arrow would be the one
   un-house-trained object on the page. Under reduced motion it is not drawn at
   all - the band is pre-revealed, the page is 100svh and there is no scroll to
   cue, so an instruction to scroll would be a lie about the page's own state.

   THE MARK IS DAN'S OWN Point.svg, VERBATIM. The artwork points UP in the file
   (it is licensed original art and icons.json flags it rail:false so the header
   rail correctly ignores it); the turn is done here, in CSS, and never in his
   path data. stage.js inlines the same path so the glyph takes currentColor -
   it has to invert with the polarity, and an <img> cannot. */
.stage__cue {
  --stage-cue-icon: 24px;      /* three tiles, the rail's own icon size: one art pixel = 2px, crisp at DPR 1 */
  position: absolute;
  /* the same construction the sub-line uses: a box from 0 to twice the axis, so
     its midline IS the statement's axis with no transform in the way */
  top: var(--stage-cue-top, 92%);
  left: 0;
  width: calc(2 * var(--stage-cue-cx, 50%));
  height: var(--stage-cue-icon);
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  column-gap: var(--pitch);
  margin: 0;
  pointer-events: none;
  user-select: none;
  /* the quietest tone on the stage, and an existing one: --dim is the site's
     chrome grey (the bar's clock, the showcase's meta). Statement in --ink,
     sub-line one step down, cue one step further - three ranks, no fifth colour. */
  color: var(--dim);
}

.stage__cue-mark {
  flex: none;
  width: var(--stage-cue-icon);
  height: var(--stage-cue-icon);
  /* NO TRANSFORM. The finger is drawn pointing UP in Dan's file and the cue
     wants it pointing up, so the mark is his artwork untouched. If it ever has
     to aim the other way again, turn it HERE - never in his path data. */
}
.stage__cue-mark > svg { width: 100%; height: 100%; }

.stage__cue-text {
  font-family: var(--font-readout);
  font-size: var(--fs-12);
  /* the words' line box IS the mark's box, so the caps centre on the glyph */
  line-height: var(--stage-cue-icon);
  letter-spacing: var(--track-readout);
  text-transform: uppercase;
  white-space: nowrap;
}

/* nothing shows until the lattice has measured where it goes */
.stage:not([data-stage-ready]) .stage__caption,
.stage:not([data-stage-ready]) .stage__cue,
.stage__meta:empty {
  visibility: hidden;
}

/* no scroll tax under reduced motion (base.css pulls the band up and the page
   is 100svh), so there is nothing to scroll to and nothing to cue */
@media (prefers-reduced-motion: reduce) {
  .stage__cue { display: none; }
}

/* The noscript fallback keeps F0's centred setting; nothing else changes. */
