/* header.css - piece P1: the header bar as ONE instrument line.
   Owner: P1. Loads after base.css and wins on cascade order.

   The row at 1440 (wave 2 round 5 - the chrome is one 48px row again):

     0                                                                     1440
     | beefydan     | :: STANDING BY          [13 channel glyphs]  13:14 | DEVELOPER -> |
     0             256                       680             1088  1119 1184        1440
       ^ axis 96     ^ rule 1                  ^ the run              ^ rule 2  ^ ink 1344
     |<--- 256 --->|<--------------- 928: THE SCREEN ------------->|<--- 256 --->|

   ---- WHAT CHANGED THIS ROUND, AND WHAT IT COST ----
   Dan, on the built page: "On the very top, leave the 'standing by' message,
   but remove those lines |----|----| and just move the icons there instead."

   So the PLAYHEAD TRACK is gone - the dotted rail, its stations, the red head -
   and the thirteen channel glyphs come up out of their own sticky plate and
   into the bar, in the span the track used to hold. The plate went with them:
   --bar-h is 48px again, the single row it meant before the track ever asked
   for a second one, and base.css / app.js / lattice.js all spend that token as
   "the chrome above the stage", so the stage grew back by 40px for free.

   Be clear-eyed about the cost. The track was the row's only PICTURE of which
   piece is playing - a scale with a lit head on it. Nothing replaces it here,
   because Dan asked for glyphs in that span and not for a smaller scale. What
   is left carrying that state is the READOUT'S WORDS, which is why the words
   are given the whole left half of the screen (354px at 1440 - every title in
   items.json fits) and the run is anchored on the RIGHT, one gap inside the
   clock: the run may never be the reason a reading is cut, and it must not
   move when the reading under it changes length. When a title does not fit,
   the words ellipsise and readout.js says so, loudly, in the console.

   ---- THE RUN'S GAP IS ONE LATTICE TILE ----
   Thirteen 24px glyphs at a one-tile gap is a 408px run and a 32px step - both
   whole tiles, so every glyph's left edge stands on a lattice column, the same
   column the ground's dots are drawn on. A two-tile gap was tried and rejected:
   it costs 96px, which is exactly the width of the longest title in the
   library, and the airier spacing made thirteen glyphs read as thirteen items
   instead of as one instrument. The glyph is 24px and never smaller - the art
   is on a 2px grid inside a 24 box, so 24px is one art pixel = 2 CSS px and
   crisp, where 16px would be 1.33px per art pixel. A blurred icon is worse
   than a scrolled one.

   ---- THE FIT, AND THE ORDER THINGS GIVE WAY ----
   That 408px run shares a 48px row with a wordmark, a lamp, the state words, a
   clock and a twin switch. At 1440 it fits with 170px to spare. It does not fit
   at 390. The ladder, top to bottom - every step is MEASURED by readout.js and
   reported on the elements and in the console, never silent:

     >= 1152   everything: cell inset 3 tiles, the clock, 13 of 13 glyphs.
     < 1152    the CLOCK STANDS DOWN FIRST. It is the only reading in the row
               that says nothing about this site - the visitor already has a
               clock - so it goes before any channel does, and the cell inset
               tightens with it (1 tile), which pulls the shared module in by
               32px a side. Still 13 of 13, down to about 950.
     < ~950    the run no longer fits beside the words, so it SCROLLS in place:
               overflow-x on the run's own window, cut at the last WHOLE glyph
               (never through one), a hairline end cap on the side that
               continues, keyboard focus scrolls a glyph into view, and
               readout.js names every off-screen channel in the console.
               Nothing is dropped and nothing is unreachable.
               Measured: 13 at 1024, 11 at 900, 8 at 768.
     <= 767    THE ROW CANNOT HOLD THEM AND STOPS PRETENDING. See below.

   ---- 2026-09-04, DAN: THE CHANNELS MUST SURVIVE THE PHONE ----
   For one round the phone kept the single row and paid for it: measured, 390
   printed ONE glyph of thirteen and 320 printed none - twelve links alive in
   the tab order and invisible on the device where the thumb is the only
   pointer. Dan's call: keep the single 48px bar on the desktop, and give the
   phone a second row.

   So below 768 the run comes OUT of the instrument line and back onto its own
   plate directly under it - the rail this file used to carry, restored whole:
   the same sticky plate, the same 24px glyphs on the same lattice, the same
   7 + 6 block with its short row centred under its long one. Thirteen of
   thirteen at 390 AND at 320, no scroll, nothing behind a swipe.

   WHAT IS NEW IS THAT IT IS CONDITIONAL, and that is the whole risk of this
   round. base.css, app.js and lattice.js all spend --bar-h as "the chrome above
   the stage" - the page's height, the stage's sticky top, the stage's own
   height, the band's seam row. So --bar-h has to MEAN the chrome at every
   width, and now the chrome is two different things:

     >= 768   48        the instrument row alone           6 tiles at pitch 8
     <= 767   48 + 72 = 120   the row plus the rail plate  20 tiles at pitch 6

   The bar's own height is pinned to --bar-row-h (48 always) so that growing the
   chrome never grows the instrument line. LITERAL PIXELS, never calc(): an
   unregistered custom property's computed value is an unevaluated token stream,
   so `calc(48px + var(--rail-h))` reaches app.js's parseFloat as the string
   "calc(" and silently becomes the 48px fallback - the page would keep the
   desktop's arithmetic while the chrome grew. Both numbers are written out and
   the arithmetic that makes them is on the line above.

     <= 359    the row is two pixels short of even the state: 212 of 320 goes to
               the two end cells. The arrow stands down (it is aria-hidden - the
               link's own word is the reading) and READOUT drops to 11px, the
               bottom of its stated range, so STANDING BY still reads in full.
               The rail plate is unaffected: its 240px run clears 320 with 78px
               of lead on either side, both whole tiles.

   ---- AND SINCE 2026-09-04, EVERY GLYPH SAYS WHERE IT GOES ----
   Dan: "on mouse hover over the icons, I need a label just like the callout
   from the tile that says where the icon link is going to. Not the URL but the
   platform itself." That callout is the last block in this file; it hangs off
   the bar's own bottom hairline and adds exactly one stroke to the page.

   ---- WHAT DID NOT CHANGE ----
   The lamp is still a 2x2 block of lit lattice cells in --red, 10px on a
   16px slot, bottoming out on the row's baseline (y=29) with the mono's caps
   and the wordmark. The clock and the state words are still READOUT mono at
   ONE size and weight. The name cell and the switch cell still share ONE
   module width. The bar's content still stands on the stage's own left axis.
   The bar still paints no ground of its own beyond the page's lattice. */

:root {
  /* ---- THE CHROME CONTRACT ----------------------------------------------
     --bar-h is THE CHROME ABOVE THE STAGE, at every width, because that is
     what base.css, app.js and lattice.js all spend it as. --bar-row-h is the
     instrument line and never changes. On the desktop they are the same 48;
     on the phone the rail plate is the difference. Written out in literal
     pixels - see the note above for why a calc() here is a silent bug. */
  --bar-row-h: 48px;                /* the instrument line: 6 tiles at pitch 8 */
  --rail-h: 0px;                    /* desktop: the channels are IN that line */
  --bar-h: 48px;                    /* THE CHROME = 48 + 0 */

  --rail-icon: 24px;                /* three tiles: one art pixel = 2 CSS px */
  --rail-gap: var(--pitch);
  --rail-top: 12px;                 /* a 3-tile glyph centred in a 6-tile row */
  /* the run's window, as insets from the bar's two edges. readout.js measures
     and overwrites both every layout; these two are the 1440 answer, so a boot
     that never reaches the script still puts the glyphs in the right span
     instead of over the wordmark. */
  --rail-start: 41%;
  --rail-end: 18%;

  /* the block the plate is sized for; readout.js counts the laid-out rows and
     REPORTS a disagreement, because a wrapped row the chrome is not tall enough
     for is a clipped plate and a stage that starts in the wrong place */
  --rail-rows: 1;
}

/* THE PHONE'S CHROME IS TWO ROWS. 72 = one tile of air, a 24px glyph, two tiles
   between the rows, a 24px glyph, one tile of air closed by the floor rule -
   twelve whole tiles at pitch 6, and 48 + 72 = 120 is twenty. */
@media (max-width: 767px) {
  :root {
    --rail-h: 72px;
    --bar-h: 120px;                 /* THE CHROME = 48 + 72 */
    --rail-rows: 2;
  }
}

/* the instrument line keeps its own 48 while --bar-h grows around it. This one
   declaration is the entire cost of the second row to the row above it. */
.bar { height: var(--bar-row-h); }

/* ---- the bar ---- */
.bar {
  /* THE AXIS. readout.js overwrites this with the stage's own measured margin
     every layout; 96px is the value at 1440 and the fallback before the stage
     reports. The wordmark's ink and the switch's ink stand on it, so the bar's
     content and the stage's content share one left/right edge. */
  --bar-axis: 96px;
  --bar-pad: calc(var(--pitch) * 3);                     /* cell inset */
  --bar-module: 160px;                                   /* readout.js measures */
  --bar-end: calc(var(--bar-axis) + var(--bar-module));
  --bar-end-start: var(--bar-end);
  --bar-end-end: var(--bar-end);
  --bar-arrow-gap: 8px;
  --bar-name-ink: 78px;                                  /* readout.js measures */
  --bar-switch-ink: 90px;                                /* readout.js measures */
  --bar-slot-shift: 0px;                                 /* lamp -> lattice column */

  /* THE LAMP: two cells across, two down, one seam between, on the lattice. */
  --bar-lamp-cell: var(--cell-size, 4px);
  --bar-lamp-seam: 2px;
  --bar-lamp: calc(var(--bar-lamp-cell) * 2 + var(--bar-lamp-seam));    /* 10px */
  --bar-lamp-inset: calc((var(--pitch) * 2 - var(--bar-lamp)) / 2);
  --bar-lamp-gap: calc(var(--pitch) + 1px - var(--bar-lamp-inset));
  --bar-lamp-row: 2;
  --bar-lamp-top: calc(var(--pitch) * var(--bar-lamp-row));
  --bar-mid: calc(var(--bar-lamp-top) + var(--pitch));

  /* THE BASELINE. Every reading in the row stands on y=29: the mono's caps,
     the wordmark's, and the bottom of the lamp's lit block. */
  --bar-base: calc(var(--bar-mid) + 5px);
  --bar-word-top: calc(var(--bar-base) - 10px);
  --bar-name-top: calc(var(--bar-base) - 15px);
  --bar-run-gap: calc(var(--pitch) * 3);                 /* words | run | clock */
  --bar-phantom: var(--track-readout);                   /* mono's trailing track */
  --bar-dim: var(--dim);

  display: grid;
  grid-template-columns:
    var(--bar-end-start)
    minmax(0, 1fr)
    var(--bar-end-end);
  align-items: stretch;
  column-gap: 0;
  padding: 0;
  background-color: var(--ground);
  background-image: radial-gradient(var(--lat-dot) 1.1px, transparent 1.2px);
  background-size: var(--pitch) var(--pitch);
  border-bottom: 1px solid var(--hairline);
}

.bar__name,
.bar__readout,
.bar__switch { will-change: opacity; }

/* base.css mounts a canvas for the bar's ground; the bar paints its own tile
   from the same token, so the canvas has nothing left to draw. */
.bar__lattice { background: transparent; }

.bar__name,
.bar__readout,
.bar__switch {
  display: flex;
  align-items: flex-start;
  justify-self: stretch;
  min-width: 0;
  height: 100%;
  margin: 0;
  padding: 0 var(--bar-pad);
  white-space: nowrap;
}

/* ---- cell 1: the name ---- */
.bar__name {
  grid-column: 1;
  justify-content: flex-start;
  padding-top: var(--bar-name-top);
  padding-inline-start: var(--bar-axis);
  border-inline-end: 1px solid var(--hairline);
  font-family: var(--font-voice);
  font-weight: 500;
  font-size: var(--fs-18);
  line-height: 1;
  letter-spacing: var(--track-voice);
  color: var(--ink);
  transition: color var(--dur-micro) var(--ease);
}
.bar__name:hover,
.bar__name:focus-visible { color: var(--ink); }

/* ---- READOUT: one size, one weight, for the state words, the clock and the
   switch. With the wordmark's VOICE that is the row's two type settings. ---- */
.bar__readout,
.bar__switch {
  font-family: var(--font-readout);
  font-weight: 400;
  font-size: var(--fs-12);
  line-height: 1;
  letter-spacing: var(--track-readout);
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}

/* ---- cell 2: the screen - lamp, state words, and (fixed over it) the run ---- */
.bar__readout {
  grid-column: 2;
  justify-content: flex-start;
  padding-inline-start: calc(var(--bar-pad) + var(--bar-slot-shift));
  padding-inline-end: calc(var(--bar-pad) - var(--bar-phantom));
  gap: 0;
  overflow: hidden;
  color: var(--ink);
}

/* THE LAMP. The markup's <svg> carries four rects for anyone without CSS; the
   four gradients below are the same four cells drawn on the lattice's own
   geometry, so the block lands on tile columns at both pitches. */
.bar__live {
  flex: none;
  box-sizing: border-box;
  width: calc(var(--pitch) * 2);
  height: calc(var(--pitch) * 2);
  margin-top: var(--bar-lamp-top);
  margin-inline-end: var(--bar-lamp-gap);
  padding: 0;
  color: transparent;
  background-color: transparent;
  background-image:
    linear-gradient(var(--red), var(--red)),
    linear-gradient(var(--red), var(--red)),
    linear-gradient(var(--red), var(--red)),
    linear-gradient(var(--red), var(--red));
  background-size: var(--bar-lamp-cell) var(--bar-lamp-cell);
  background-repeat: no-repeat;
  background-position:
    var(--bar-lamp-inset) var(--bar-lamp-inset),
    calc(var(--bar-lamp-inset) + var(--bar-lamp-cell) + var(--bar-lamp-seam)) var(--bar-lamp-inset),
    var(--bar-lamp-inset) calc(var(--bar-lamp-inset) + var(--bar-lamp-cell) + var(--bar-lamp-seam)),
    calc(var(--bar-lamp-inset) + var(--bar-lamp-cell) + var(--bar-lamp-seam)) calc(var(--bar-lamp-inset) + var(--bar-lamp-cell) + var(--bar-lamp-seam));
}

/* THE READING. --bar-words-max is written by readout.js: it is the distance
   from the words' own left edge to whatever stands next in the row (the run,
   or the clock when the run is down). A title longer than that ellipsises and
   is REPORTED - never quietly shortened. */
.bar__readout-text {
  display: block;
  min-width: 0;
  max-width: var(--bar-words-max, none);
  margin-top: var(--bar-word-top);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: opacity var(--dur-micro) var(--ease);
}
.bar__readout-text[data-fading] { opacity: 0; }
.bar__readout-label { color: var(--bar-dim); }
.bar__readout-value { color: var(--ink); }
/* the em dash is the VALUE's, not the label's: it is drawn here so that a
   caller which stands the piece's name down (P6 does, while its dialog is
   naming the piece a second time) takes the separator with it, instead of
   leaving the row reading "PLAYING NOW —" on a dangling dash. Dim, because it
   is punctuation between two readings and not one of them. */
.bar__readout-dash { color: var(--bar-dim); }

.bar__clock {
  flex: none;
  margin-inline-start: auto;
  margin-top: var(--bar-word-top);
  color: var(--bar-dim);
}

/* ---- cell 3: the twin switch ---- */
.bar__switch {
  grid-column: 3;
  justify-content: flex-end;
  gap: calc(var(--bar-arrow-gap) - var(--bar-phantom));
  padding-top: var(--bar-word-top);
  padding-inline-end: var(--bar-axis);
  border-inline-start: 1px solid var(--hairline);
  color: var(--ink);
}
.bar__switch-arrow {
  font: inherit;
  letter-spacing: 0;
  color: var(--bar-dim);
  transition: color var(--dur-micro) var(--ease);
}
.bar__switch:hover .bar__switch-arrow,
.bar__switch:focus-visible .bar__switch-arrow { color: var(--ink); }

/* ---- focus: a hairline box around the CELL's ink, never a browser ring that
   would sit off the lattice ---- */
.bar__name,
.bar__switch { position: relative; }
.bar__name:focus-visible,
.bar__switch:focus-visible { outline: none; }
.bar__name:focus-visible::after,
.bar__switch:focus-visible::after {
  content: "";
  position: absolute;
  top: var(--pitch);
  bottom: calc(var(--pitch) - 1px);
  border: 1px solid var(--ink);
  pointer-events: none;
}
.bar__name:focus-visible::after {
  left: calc(var(--bar-axis) - var(--pitch));
  right: calc(100% - var(--bar-axis) - var(--bar-name-ink) - var(--pitch));
}
.bar__switch:focus-visible::after {
  left: calc(100% - var(--bar-axis) - var(--bar-switch-ink) - var(--pitch));
  right: calc(var(--bar-axis) - var(--pitch));
}

@media (forced-colors: active) {
  .bar__name:focus-visible,
  .bar__switch:focus-visible {
    outline: 1px solid CanvasText;
    outline-offset: calc(-1 * var(--pitch));
  }
  .bar__name:focus-visible::after,
  .bar__switch:focus-visible::after { display: none; }
  .bar__live { color: CanvasText; background-image: none; }
}

/* THE CLOCK STANDS DOWN FIRST. Below 1152 the run needs its 65px more than the
   visitor needs a time they already have in their status bar. */
@media (max-width: 1151px) {
  .bar__clock { display: none; }
  /* and the cells tighten with it - one tile of inset instead of three, which
     also pulls the shared module in by 32px a side and is worth two glyphs */
  .bar { --bar-pad: var(--pitch); }
}

@media (max-width: 767px) {
  .bar {
    --bar-axis: 18px;
    --bar-pad: var(--pitch);
    --bar-end: max-content;
    --bar-lamp-row: 3;
    --bar-run-gap: calc(var(--pitch) * 2);
  }
  .bar__name {
    font-size: 16px;
    padding-top: calc(var(--bar-base) - 13px);
  }
}

/* 320 is two pixels short of the row: the two end cells take 212 of it and the
   lamp plus STANDING BY need 110 of the 108 that are left, so the state - the
   one reading this row exists for - was ellipsising. The arrow is the only
   thing in the row that carries no meaning (it is aria-hidden; the link's own
   word is the reading), so it is what goes. */
@media (max-width: 359px) {
  /* READOUT at the bottom of its own range (the piece brief: 11-12px, never
     below 11) rather than a truncated state. The wordmark keeps VOICE, so the
     row still has its two settings. */
  .bar__readout,
  .bar__switch { font-size: 11px; }
  .bar__switch-arrow { display: none; }
  .bar__switch { gap: 0; }
}

/* the run has no window at all: it keeps its links (a scroll container with no
   width still hands them to the keyboard and to AT, where display:none would
   not) but it prints nothing - not even the sliver of a first glyph. Only the
   ROW can reach this state; the plate below 768 always prints thirteen. */
@media (min-width: 768px) {
  .rail[data-rail-empty] { padding-inline: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .bar__name,
  .bar__switch-arrow,
  .bar__readout-text { transition: none; }
}

/* ================= THE CHANNEL RUN =================================
   The markup keeps this as a real <nav aria-label="Elsewhere"> so every glyph
   is a named link and the group is a landmark. readout.js moves that nav INTO
   the header on boot, immediately before the switch, so the tab order runs the
   way the eye does (name -> state -> channels -> switch); the two rules below
   put it in exactly the same place either way, so the move can never flash.

   It is taken out of flow on purpose. Left in the grid it would either fight
   the readout for column 2 or force a fourth column that the wordmark and the
   switch would then have to share a module with - and the run is not a CELL of
   this row, it is an object ON it, the way the lamp is. readout.js measures the
   window between the state words and the clock and writes --rail-start /
   --rail-end so the run's left edge lands on a lattice column at both pitches
   (glyph 24 + gap 8 or 16 = a step of 32 or 40, both whole tiles). */
.rail {
  position: fixed;            /* before the move: pinned to the same 48px row */
  inset-block-start: 0;
  inset-inline: var(--rail-start) var(--rail-end);
  height: var(--bar-row-h);   /* the INSTRUMENT LINE, not the chrome: on the
                                 phone the chrome is 120 and this box is 72 of
                                 it, sized by the plate block at the end */
  z-index: var(--z-bar);
  display: flex;
  flex-wrap: nowrap;
  align-items: flex-start;
  justify-content: flex-start;
  gap: var(--rail-gap);
  /* one tile of padding all round the run's own window: it is what the focus
     ring and the 40px hit area hang in, and it keeps both off the clip edge */
  padding: var(--rail-top) var(--pitch) 0;
  /* WHEN IT DOES NOT FIT IT SCROLLS - it never drops a channel. The box is
     exactly the window, so it takes the pointer (a swipe has to land
     somewhere) without covering the wordmark, the clock or the switch. */
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  background-color: transparent;
  background-repeat: no-repeat;
  background-size: 1px var(--rail-icon);
}
.rail::-webkit-scrollbar { display: none; }

/* after readout.js has moved it: the bar is sticky, so it is the containing
   block, and the run measures against the bar's own width rather than the
   viewport's (which includes the scrollbar gutter and would sit 15px off). */
.bar > .rail { position: absolute; }

/* THE END CAP. A run that continues past its window says so with one hairline
   on the side that continues, the height of a glyph. It is a background, not a
   border, so it holds still while the content scrolls under it and costs the
   run no width. Nothing is drawn when nothing is hidden.

   SCOPED TO THE ROW, deliberately. The phone's plate paints the page's dot tile
   in this same background, and an end cap is a reading about a window that has
   been cut - the plate has no window and cuts nothing. Left unscoped these
   three rules outrank the plate's own background on specificity and would
   replace its dot field with a stray hairline. */
@media (min-width: 768px) {
  .rail[data-clip~="start"],
  .rail[data-clip~="end"] {
    background-image: linear-gradient(var(--hairline), var(--hairline));
  }
  /* on the CONTENT edge, one tile inside the box: the window's own edge, which
     is where the run is actually cut - and never on top of the switch's rule,
     which sits one tile further out. */
  .rail[data-clip~="start"] { background-position: left var(--pitch) top var(--rail-top); }
  .rail[data-clip~="end"] { background-position: right var(--pitch) top var(--rail-top); }
  .rail[data-clip~="start"][data-clip~="end"] {
    background-image:
      linear-gradient(var(--hairline), var(--hairline)),
      linear-gradient(var(--hairline), var(--hairline));
    background-position: left var(--pitch) top var(--rail-top), right var(--pitch) top var(--rail-top);
  }
}

.rail__link {
  position: relative;
  flex: none;
  display: block;
  width: var(--rail-icon);
  height: var(--rail-icon);
  color: var(--dim);                       /* dim at rest */
  transition: color var(--dur-micro) var(--ease);
}
.rail__link > svg {
  display: block;
  width: 100%;
  height: 100%;
}
/* the hit area is the glyph plus a tile all round - 40px, thumb-sized, without
   the box itself growing and breaking the run's step */
.rail__link::before {
  content: "";
  position: absolute;
  inset: calc(-1 * var(--pitch));
}
.rail__link:hover,
.rail__link:focus-visible { color: var(--ink); }   /* full ink on hover/focus */
.rail__link:focus-visible { outline: none; }
.rail__link:focus-visible::after {
  content: "";
  position: absolute;
  inset: calc(-1 * var(--pitch));
  border: 1px solid var(--ink);
  pointer-events: none;
}

@media (forced-colors: active) {
  .rail__link { color: LinkText; }
  .rail__link:focus-visible {
    outline: 1px solid CanvasText;
    outline-offset: calc(var(--pitch) - 1px);
  }
  .rail__link:focus-visible::after { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .rail { scroll-behavior: auto; }
  .rail__link { transition: none; }
}

/* ================= THE PHONE'S PLATE ===============================
   Below 768 the run stops being an object ON the instrument line and becomes
   the chrome's own SECOND ROW, directly under it, sharing its ground, its dot
   field and the grammar of its floor rule. This is the rail that used to be
   here, restored rather than reinvented - the same plate, the same block, the
   same arithmetic - with one thing new: it exists only at this end of the
   ladder, and --bar-h says so at both ends.

   IT IS IN FLOW, AND THAT IS THE WHOLE OF THE --bar-h CONTRACT. The desktop's
   run is an OBJECT ON the row - taken out of flow on purpose, because it is not
   a cell of it. A plate cannot be: --bar-h is spent as the chrome above the
   stage, and an out-of-flow plate would grow the token without growing the
   document, so the stage would start 72px too high, the band would already be
   72px revealed at scrollY 0, and every downstream piece would be composed
   against a frame that is not there. So below 768 the nav goes back to where
   the markup authors it - a real sibling immediately after the header - and
   takes its 72px of flow. 48 + 72 = 120 = --bar-h, measured and reported.

   AND THE TAB ORDER FOLLOWS THE EYE, both ways. readout.js moves the nav INTO
   the header on the desktop, because there the run sits between the state and
   the switch and must be tabbed there. On the phone the plate sits under the
   whole instrument line, so the eye reads name -> state -> switch -> channels -
   which is exactly the authored order. One move each way, at the breakpoint,
   and each position is the reading order of its own layout.

   THE BLOCK IS SEVEN AND SIX, MEASURED. At pitch 6 with a one-tile gap, twelve
   glyphs and eleven gaps came to 354 - which is 390 minus the two 18px margins,
   so the twelfth was already standing on the last inch and the thirteenth
   needed 30px that do not exist. Distributed, the gaps fall to 3.5px: off the
   lattice in both directions, and thirteen busy little drawings become one dark
   smear. So the run wraps at seven and the block is SQUARE - 12px across AND
   down, two tiles at pitch 6 - which makes every cell 36x36 once the hit area
   below extends one tile on all four sides, so the cells tile the block edge to
   edge, touching and never overlapping, and a thumb can never be handed the
   wrong channel.

   Seven glyphs and six gaps = 240px of run. It clears 390 with 75px of lead a
   side and clears 320 with 40 - and at both widths readout.js snaps that lead
   to the nearest lattice column, so every glyph in both rows stands on one.

   THE SHORT ROW IS CENTRED UNDER THE LONG ONE, and the two-tile gap is what
   buys it: the module is 36, half a module is 18, and 18 is three whole tiles
   at pitch 6 - so six glyphs centred under seven are still on lattice columns.
   With a one-tile gap the module would be 30, the offset 15, and the stagger
   three pixels off the grid. Thirteen keys on a seven-wide pad leave one cell
   unlit, which is the reference device's own idiom.

   NO NEW COLOUR, NO NEW TYPE, NO NEW STROKE: one tone at rest (--dim, the
   clock's and the arrow's), one on hover or focus (--ink), the bar's own 150ms,
   and a floor rule in the same hairline as the bar's. Two rows, two rules, one
   plate. And no vertical rules: the bar has exactly two and each divides two
   things that exist; twelve more under them would make a grille. */
@media (max-width: 767px) {
  /* THE BAR CARRIES THE CALLOUT LAYER, so the bar has to paint over the plate.
     Both boxes are --z-bar and the plate comes later in the document, so by DOM
     order the plate's opaque ground painted over the bar's whole subtree - and
     the callout's leader, which is the 42px stroke from a first-row glyph down
     to the chrome's rule, was drawn UNDER it and simply never appeared. The two
     boxes do not overlap (0-48 and 48-120), so ordering the bar above the plate
     changes nothing that is visible except the one stroke that should be. */
  .bar { z-index: calc(var(--z-bar) + 1); }

  .rail,
  .bar > .rail {
    --rail-per-row: 7;
    --rail-gap: calc(var(--pitch) * 2);   /* 12: the same measure across and down */
    /* IN FLOW AND STUCK UNDER THE ROW. inset-inline is stated back to auto
       because the desktop's rule spends it as the run's window insets, and on a
       sticky box those are horizontal sticky constraints, not a position. */
    position: sticky;
    inset-block-start: var(--bar-row-h);
    inset-inline: auto;
    width: auto;
    height: var(--rail-h);
    flex-wrap: wrap;
    row-gap: var(--rail-gap);
    /* a wrapped flex container's default align-content is STRETCH, which hands
       each line an equal share of the leftover cross space: measured, that
       pushed the two rows 39px apart instead of the authored 36 and put the
       second row 3px off its lattice row. The rows are a cadence, not a
       distribution. */
    align-content: flex-start;
    justify-content: center;              /* the short row, centred under the long one */
    /* nothing scrolls here - all thirteen are on the plate - and the focus ring
       and the 40px hit area both hang one tile outside their glyph, so the box
       must not clip them */
    overflow: visible;
    /* one tile of air above the first row; the floor rule closes the one below
       the second. readout.js writes the lead and the trail as whole pixels off
       one measurement of the plate. */
    padding: var(--pitch) var(--rail-trail, 0px) 0 var(--rail-lead, 0px);
    /* the plate paints the page's own ground and dot tile, at the same pitch
       and in the same phase: its box starts at y=48, which is eight tiles at
       pitch 6, so the field runs straight through the chrome and arrives at the
       stage without a seam. background-repeat and -position are stated because
       the row above sets both for its end cap. */
    background-color: var(--ground);
    background-image: radial-gradient(var(--lat-dot) 1.1px, transparent 1.2px);
    background-size: var(--pitch) var(--pitch);
    background-repeat: repeat;
    background-position: 0 0;
    border-bottom: 1px solid var(--hairline);
  }
}

/* ================= THE CHANNEL CALLOUT =============================
   Dan, 2026-09-04: "on mouse hover over the icons, I need a label just like
   the callout from the tile that says where the icon link is going to. Not
   the URL but the platform itself."

   So each of the thirteen glyphs names its PLATFORM on hover and on
   :focus-visible - X, Facebook, Instagram, ... Contact - and never its URL.
   The words come from icons.json's `label`, which is already the one source
   of truth for both halves of a link (its accessible name and where it
   goes); readout.js reads them off the link's own aria-label, so there is no
   second list here to drift from that one.

   IT IS THE CUBE'S CALLOUT, SPOKEN IN THE BAR. voxel.css draws the live
   cube's link as READOUT mono on the page's own ground, hung off ONE
   hairline, with no fill, no radius, no shadow and no blur - a leader rising
   from the object to a rule, and the name sitting against that rule. This is
   the same device with the geometry turned over, because the instrument is
   ABOVE its annotation here instead of below it:

        [ glyph ]          <- the control, in the 48px row
            |              <- the leader: ONE hairline, glyph bottom to rule
     ------- -------       <- the rule IS the bar's own bottom hairline
            INSTAGRAM      <- the name, hung under the rule at the leader

   The bar's bottom border is the callout's rule, so the callout adds exactly
   one stroke to the page (the 12px leader) and never a box. Four colours:
   --ink for the name, --hairline for the leader, the ground showing through,
   and the red that stays on the lamp where it belongs.

   WHY IT HANGS BELOW. The row is 48px and a 12px line with its air does not
   fit inside it beside a 24px glyph. Hanging it below costs nothing: the
   layer is an absolutely-positioned zero-height box in the sticky bar, so it
   is outside flow, it cannot reflow the row, and it cannot grow the document
   (it is 20px tall at the top of a 1173px page). Nothing moves when it
   appears - see the scroll proof in readout.js.

   IT IS aria-hidden. Every glyph already carries the platform's name in its
   own aria-label, so a screen reader reads it from the link. This is the
   same word a second time, for eyes only; announcing it twice would be the
   defect, not the feature.

   THE LEADER STANDS ON A DOT COLUMN. A glyph's left edge is snapped to a
   lattice column and the glyph is 24 wide, so its centre is column + 12 -
   which is exactly where the ground's dots are drawn (the radial-gradient
   centres each dot at half a tile). The leader descends the dot column it
   was already standing on. */

.bar__note {
  position: absolute;
  /* the bar's own height, NOT top:100%: percentages resolve against the
     padding box, which the 1px bottom border makes 47 - and the whole point
     of this layer is that its top edge IS that border's row. */
  top: var(--bar-h);
  left: 0;
  right: 0;
  height: 0;
  z-index: 1;
  pointer-events: none;          /* it is a reading, never a target */
  font-family: var(--font-readout);
  font-weight: 400;
  font-size: var(--fs-12);
  line-height: 1;
  letter-spacing: var(--track-readout);
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}

.bar__note-lead,
.bar__note-name {
  position: absolute;
  white-space: nowrap;
  opacity: 0;
  transition: opacity var(--dur-micro) var(--ease);
}

/* THE LEADER. readout.js measures it: the glyph's bottom edge to the bar's
   bottom rule, so it is right at either pitch and cannot be a guess. */
.bar__note-lead {
  top: calc(-1 * var(--note-lead-h, 12px));
  left: var(--note-x, 0px);
  width: 1px;
  height: var(--note-lead-h, 12px);
  background: var(--hairline);
}

/* THE NAME. One tile of air under the rule (48 -> 56, a lattice row) and the
   cube's own 10px inset from its leader. */
.bar__note-name {
  top: 0;
  left: var(--note-x, 0px);
  padding: var(--pitch) 0 0 10px;
  color: var(--ink);
}

/* the cube's callout ends on a dim arrow: the name says what the control IS,
   the arrow says it takes you somewhere. Same 10px step, same --dim, and no
   tracking on a single glyph. */
.bar__note-arrow {
  margin-inline-start: 10px;
  letter-spacing: 0;
  color: var(--dim);
}

/* THE RULE RUNS BACK OVER THE RUN when the page's margin will not take the
   name - voxel.css does exactly this on the phone, and readout.js measures
   and chooses. The last glyph in the run is 96px from the right edge at
   1440 and "Skyframe Innovations" is 170px wide; a name that clipped at the
   viewport would be worse than no name, so it turns round instead. */
.bar__note[data-note-side="left"] .bar__note-name {
  left: auto;
  right: var(--note-r, 0px);
  padding: var(--pitch) 10px 0 0;
  text-align: right;
}

/* ...and if neither side has the room (a narrow row, a long platform name)
   the name stands on the page's own axis and says so, rather than running
   off the edge. The leader still points at the glyph it belongs to. */
.bar__note[data-note-side="axis"] .bar__note-name {
  left: var(--note-axis, 0px);
  right: auto;
  padding: var(--pitch) 0 0 0;
  text-align: left;
}

.bar__note[data-note-open="true"] .bar__note-lead,
.bar__note[data-note-open="true"] .bar__note-name { opacity: 1; }

/* A LABEL IS INFORMATION, NOT DECORATION: under reduced motion it still
   appears, it simply does not fade in. */
@media (prefers-reduced-motion: reduce) {
  .bar__note-lead,
  .bar__note-name { transition: none; }
}

@media (forced-colors: active) {
  .bar__note-name { color: CanvasText; }
  .bar__note-lead { background: CanvasText; }
}

/* The callout is the row's READOUT setting spoken one line lower, so it steps
   down with the rest of it below 360 or the row would carry a third size.
   It has to be stated HERE, after .bar__note's own font-size and not up in the
   359 block with its siblings: same specificity, and the later rule wins. */
@media (max-width: 359px) {
  .bar__note { font-size: 11px; }
}
