/* BeefyDan portfolio - showcase. Owner: P6 (Showcase + condense).
   One piece of media in the centre of the lattice with a title and a meta row
   under it. No card chrome, no shadows: the media, the words and the controls
   sit directly on the lattice. The dialog is a grid whose tracks are the
   numbers showcase.js measured (--sc-*), so the text stacks wrap-safely and the
   media rect stays on lattice slots. Colours: ink and gray on ground only.

   The composition is ONE column, the media's own width, on lattice rows
   (1440x900, pitch 8, the whole stage - the band stands down while a piece is
   open, showcase.js parks it below the fold - every gap a whole number of rows):
     48px   the header bar's band - painted, INERT, taken to one dim tone and
            then to 40% while a piece is on the field (the last blocks in this
            file): two words reading the STATE and two dim links, stood back far
            enough that the composition reads the whole band as air. No lamp, no
            clock, no rail, no red
     16px   air at the stage's top edge - 64px of air in all
     row 2  the ONE row, and TWO objects in it:
                             02/12        <- PREV  NEXT ->  ........  CLOSE
                             the two READINGS bracket the row at 11px in the one
                             dim tone - where you are, and the way out; the two
                             ACTIONS stand between them at 12px in INK, 16px
                             apart and 48px off the reading, the row's one lit
                             object. CLOSE holds the column's right edge. The
                             pair answers the pointer with a hairline, CLOSE by
                             lifting to ink. Nothing here is red: the work is the
                             only colour on the field
     16px   (the row sits on the picture's top edge, a label on a plate)
     row 4  media            cells canvas under the real picture: 672px tall -
                             79% of the stage's height, 37% of the screen. THE
                             WORK IS THE FRAME (showcase.js, ROWS): every gap
                             around it is the least a composition needs and the
                             difference is the picture's
     16px   (20px to the name's cap: the label HANGS OFF the plate, so the block
             belongs to the picture rather than to the page)
     row 6  name             VOICE 22 / 24, mixed case, INK. The piece's name in
                             the page's own type, on every piece the page speaks
                             for. A PLACEHOLDER sets its own name inside the
                             frame in our lit cells, and only there does this row
                             leave the composition (see below)
     0px    (the boxes meet: the label's own leading is the whole gap, about 9px
             of air, so the pair reads as ONE block with a head)
     row 8  spec             READOUT 11 / 16, uppercase, 0.09em, one flat dim run
                             - kind . year . place|stack. Under a name the label
                             demotes in size, tracking AND tone together
     68px   air before the field's bottom edge. A caption 32px off the screen's
            edge reads as a footer stuck to the window rather than as the
            picture's own label - see the note on padBottom in showcase.js
   Everything left-aligns on the media's left edge; CLOSE right-aligns on its
   right edge. The showcase owns no ground: base.css paints the page's dot
   lattice on the .showcase frame (opaque, tiled from the stage's own origin),
   and this dialog is transparent over it.

   THE FIELD IS THE WORK'S, AND THE PAGE NAMES THE WORK.
   A gallery label does not disappear because the painting is signed. Every
   piece of stand-in media on the site today carries lettering inside the
   picture - the twelve pipeline tiles model their names in 3D across their top
   third - and for three rounds this file read that as the page's cue to say
   nothing: it shrank the name (44px -> 18), then removed it. Round 4's blind
   critic found the result mute - "the piece's name exists only as pixels baked
   inside the artwork" - and that is the defect, because a portfolio that will
   not name its own work in its own type has been silenced by the one voice on
   the screen it exists to present. A picture that DEPICTS lettering is a
   picture; the page labels it.
   The one exception is where the words in the frame are the PAGE's own type: a
   placeholder has no file, its media IS its title in our 5x7 lit cells (MATRIX,
   one of the site's three roles), and setting the same words again 20px lower
   in VOICE is the page speaking twice. There the heading keeps its element, its
   id and the dialog's aria-labelledby and goes visually-hidden - the piece is
   still named to a screen reader - and the spec line captions it alone with a
   rank of its own: the KIND lit, the record dim.
   Either way there is exactly ONE display object on the field, it is the
   picture, and it leads the name that labels it by 30 to 1.
   Two runs carry ink and they are the piece's NAME and the two things you can
   DO to it (PREV / NEXT, 12px); the label, the position and CLOSE rest in the
   site's one dim gray at 11px. That is the whole tone ladder, and it is two
   deep because the site owns exactly two text colours - there is no third grey
   under --sc-dim that is not a fifth colour AND a contrast failure, so a rank
   step inside the row is taken upward or not at all (the long block below
   html { --sc-dim }). There is ONE row of chrome on an open piece, it stands on
   the piece's own column, and it holds two groups: a reading and a pair of
   actions. 64px of air above it, and it sits 16px off the picture's top edge:
   the ratio, not the tone, is what keeps chrome from competing with the work -
   672px of picture against 12px of type is 56 to 1.

   ---- the words sit in CLEARED SLOTS, never on the dots ----
   Type over a dot field erodes: at 12px the dots between the strokes read as
   holes in the letters. So every text run - the pager (the position, PREV and
   NEXT as one slot), CLOSE, the name, the spec line - is a .showcase__chip: a
   slot of solid --ground exactly the run's line box tall and ONE TILE
   (--pitch) wider than its ink on each side, so the ink still stands on the
   column's own edges and the slot's edges fall one tile outboard.
   showcase.js measures each run's ink and writes --sc-chip-w = the next
   whole number of tiles, so a slot never cuts a dot in half: its four edges
   are tile lines (the row boxes are whole rows already: 16 / 16 / 16 at pitch
   8), the dots it covers it covers whole, the dots outside it it leaves
   whole. The media rect keeps its lattice: the condense lights the ground's
   own dots and needs them there. The card's index and flag chips (base.css)
   are the same device at the card's scale.

   ---- and the INK stands on the rail, not the advance box ----
   A glyph starts inside its advance box. showcase.js measures the first and
   last glyph of each run for real (canvas TextMetrics) and writes --sc-bear /
   --sc-rbear; the slot's padding absorbs them, so the ink moves onto the
   column's edge while the SLOT stays exactly where it was, on tile lines.
   Fallback 0px = the advance box, which is where these runs stood before.
   Every run in the dialog is printed type, so one measurement serves them all:
   the position, the actions, the name and the spec line share one rail. */

/* ---- THE ONE DIM TONE, declared once, on the root ----
   The meta line, the pager's position, the instrument row at rest and - while
   a piece is on the field - every word in the header bar all read in this one
   gray: --gray as-is on the black page (5.7:1 on --ground), --gray pulled a
   quarter toward --ink on the white page (#6A6A6A, 4.83:1; raw --gray is
   3.08:1 there and fails text). It lives on html rather than on the dialog
   because the header sits outside the dialog and must be able to inherit it:
   one declaration, one gray, no fifth colour anywhere on an open piece. */
html { --sc-dim: var(--dim); }

/* ---- HOW LONG THE PIECE TAKES TO COME OUT OF THE LATTICE ----
   (2026-09-04, Dan: "use the same effect of the tile when the tile below comes
   back after you click close... same in and out animation.")

   The piece in the centre and the card in the strip are made of the same tiles
   at the same grain - --reveal-tile, two lattice cells, declared once in
   reveal.css and read by both. The only thing that has to answer to SIZE is the
   length, and these are P6's two numbers for it. They live on the frame rather
   than on :root because they belong to this piece: showcase.js reads them off
   `.showcase`, and nothing outside it has an opinion about them.

   --dur-materialize   560ms against the card's 500 (--dur-dissolve). The
                       picture is 672 square against a 312x232 card: six times
                       the area, four times the tiles. Rendered as a ladder at
                       500 / 560 / 640 / 720 and chosen off the ladder - at 500
                       the third-of-a-second frame is already three quarters
                       whole, at 720 the last third is dead air.
   --dur-fall          the close, and shorter than the arrival because leaving
                       is: 400ms. The strip is asked for the card one
                       --dur-micro before the last tile lands, so reveal.js's
                       own fade of this frame ends on the same frame the fall
                       does - the card comes out of a lattice the piece is still
                       falling into, and nothing keeps dissolving behind a
                       curtain. (Measured before that rule existed, with the
                       fall set to the arrival's 560: the frame hid with 27% of
                       the piece still standing.) */
.showcase {
  --dur-materialize: 560ms;
  --dur-materialize-mobile: 440ms;
  --dur-fall: 400ms;
  --dur-fall-mobile: 320ms;
}

/* ---- AND THE ROW HAS EXACTLY TWO TONES, BECAUSE THE PALETTE HAS EXACTLY TWO
   (wave 2 round 5, the blind critic against the reference) ----
   "02/12, ←PREV, NEXT→ and CLOSE sit at the same ~10px letterspaced mono and
   the same value, and the header adds three more small-caps items plus two
   vertical rules at that identical weight - seven micro-labels competing at one
   tone. Demote the counter and CLOSE to a lighter grey so only PREV/NEXT carry
   full value."

   The reading is right and the prescription cannot be followed literally, for a
   reason worth stating once because it decides every tone question this file
   will ever have: THERE IS NO GREY BELOW --sc-dim. The site has four colours
   (spec 3) and exactly two of them are text - --ink and --gray. --sc-dim is
   already --gray pulled a quarter toward --ink on the white page, and it is
   pulled there because raw --gray measures 3.08:1 on #F2F2F0 and fails as text;
   at 4.83:1 it is standing ON the 4.5:1 floor. A lighter grey under it is a
   fifth colour that also fails the contrast line - two rules broken to answer
   one note.
   So the step the critic asked for is taken in the only direction the palette
   leaves open, and it is the better one: the two things you can DO rise to
   --ink and everything else in the row stays where it is. The row now reads
     02/12   ← PREV  NEXT →   ...................................   CLOSE
     11 dim    12 INK, the pair                                     11 dim
   - one lit object flanked by two whispers, instead of four marks at one value.
   CLOSE drops 12 -> 11 as well, so it sits with the counter by SIZE where it
   cannot sit under it by tone: leaving is not one of the two things you do
   while you are here, and the row's rank ladder is now size AND tone pulling
   the same way. Contrast goes UP on every run rather than down (ink is 17:1,
   dim holds its 4.83), which is the direction a demotion is not usually
   allowed to travel.
   The picture is 672px square and the pair is 12px: the ratio that keeps chrome
   from competing is unchanged, and there is one fewer value on the field. */

/* ---- FOUR COLOURS MEANS FOUR, INCLUDING THE ONES THE RENDERER INVENTS ----
   Same round, same critic: "the multi-colour meta row (the green/rust tokens in
   PROJECT · 2026 · CLAUDE · NODE)". Nothing in this file lights that line - it
   is one flat run of --sc-dim, and it measures as one flat run in the DOM. The
   colour is Chromium's, not ours: LCD subpixel antialiasing puts an orange edge
   on one side of every stem and a blue one on the other, and at 11px with
   0.09em of tracking on a light ground the fringes are most of the glyph.
   Measured on the round-4 render: 603 of 3290 pixels in the meta row's box are
   saturated past 22/255 - rgb(220,171,124) rust and rgb(106,147,193) blue - and
   922 of 3885 in the name's. A row allowed four colours was rendering in six,
   and a critic read it as a design decision, which is the fairest possible
   reading of what is on the screen.
   P1 hit this on the bar and measured the cure: Chromium turns subpixel text
   off only where the layer under the glyphs is not known to be opaque. The
   bar's cells carry no ground of their own, so promoting them was enough; every
   run in this dialog sits in a CLEARED SLOT that is opaque --ground by
   requirement (type over a dot field erodes), so the same trick cannot work
   here - the layer is opaque exactly where it has to be.
   Asking the font directly was tried FIRST and does not work, and it is written
   down so nobody spends the round again: `-webkit-font-smoothing: antialiased`
   on the dialog changed not one pixel (603/3290 and 922/3885 before and after,
   measured on the render). MDN says the property is macOS-only in Blink and the
   render agrees; the declaration is kept below for the engines that do honour
   it, and it is not the mechanism.
   The mechanism is the LAYER, and the proof arrived in the same round from the
   bar: taken to `opacity: 0.4` for the reason two blocks down, the header's
   fringing went to ZERO saturated pixels - with its opaque background still in
   place. An effect node with opacity < 1 composites its subtree with alpha, and
   Chromium will not use subpixel text there no matter what is painted behind
   the glyphs. So each cleared slot takes the smallest opacity that is not 1.
   It is free, exactly: the slot's ground is #F2F2F0 (or #0B0B0C) over a lattice
   whose dots differ from it by 22 steps of 255, and 0.1% of 22 steps is 0.02 of
   a step - it rounds to the same byte on every pixel of every slot. The tone is
   not paid for; the layer is simply declared. */
.showcase__dialog {
  -webkit-font-smoothing: antialiased;  /* honoured on macOS only; the layer below is what works */
  -moz-osx-font-smoothing: grayscale;
  /* set by showcase.js: media rect on lattice slots + the rhythm it used */
  --sc-x: 0px;
  --sc-y: 0px;
  --sc-w: 0px;
  --sc-h: 0px;
  /* fallbacks only - showcase.js writes every one of these from ROWS on each
     layout. They are the desktop values so a frame that somehow paints before
     the first applyLayout() is composed, not collapsed. */
  --sc-toprow: 16px;
  --sc-gaptop: 16px;
  --sc-gaptitle: 16px;
  --sc-titlelh: 24px;
  --sc-gapmeta: 0px;
  --sc-metalh: 16px;
  /* the row's two gaps, both whole tiles: SIX between the reading and the
     actions (48 desktop / 36 phone), TWO between PREV and NEXT (16 / 12). One
     big gap and one small one is what makes two groups out of four words; the
     old row spaced all of them at three tiles and read as one run-on string. */
  --sc-grpgap: calc(var(--pitch) * 6);
  --sc-keygap: calc(var(--pitch) * 2);
  /* the position's own size: one step under the row's 12px actions, one step
     over the spec line's 10px label - the smallest reading in the composition
     (see THE READING below) */
  --sc-countfs: 11px;
  --sc-clear: var(--pitch);            /* a slot's clearance past its ink: one tile, so its edges are tile lines */

  /* reset the UA dialog: a transparent full-stage surface on the frame's ground */
  position: absolute;
  inset: 0;
  width: auto;
  height: auto;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent; /* the .showcase frame's CSS lattice (base.css) shows through */
  color: var(--ink);
  overflow: hidden;
  outline: none;

  display: grid;
  grid-template-columns: var(--sc-x) var(--sc-w) minmax(0, 1fr);
  grid-template-rows:
    calc(var(--sc-y) - var(--sc-toprow) - var(--sc-gaptop))  /* 1 air above */
    var(--sc-toprow)                                         /* 2 instrument row */
    var(--sc-gaptop)                                         /* 3 */
    var(--sc-h)                                              /* 4 media */
    var(--sc-gaptitle)                                       /* 5 */
    auto                                                     /* 6 title */
    var(--sc-gapmeta)                                        /* 7 */
    auto                                                     /* 8 meta */
    minmax(0, 1fr);                                          /* 9 air below */
  align-content: start;

  opacity: 1;
  transition: opacity var(--dur-micro) var(--ease);
}
.showcase__dialog:not([open]) { display: none; }
.showcase__dialog::backdrop { background: transparent; }
.showcase__dialog:focus-visible { outline: none; }

/* first paint fades in over the micro token; the close path fades out */
@starting-style {
  .showcase__dialog[open] { opacity: 0; }
}
.showcase__dialog[data-state="closed"] { opacity: 0; }

/* ---- media: cells canvas under the real picture ---- */
.showcase__media {
  grid-area: 4 / 2 / 5 / 3;
  position: relative;
  width: var(--sc-w);
  height: var(--sc-h);
  overflow: hidden;
}

/* transparent between tiles: the frame's own dot lattice shows through wherever
   the picture has not arrived yet, which is what a tile falls into and grows
   out of. Cleared entirely once the piece has settled and the real <img> holds
   the rect (showcase.js paint). */
.showcase__cells {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.showcase__img,
.showcase__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover; /* the rect has the media's own aspect: cover == contain */
  opacity: 0; /* driven per frame by showcase.js (mediaOpacity(q)) */
}
.showcase__video { cursor: pointer; }

.showcase__play {
  position: absolute;
  left: var(--s-4);
  bottom: var(--s-4);
  padding: 3px var(--s-1);
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-readout);
  font-size: var(--fs-12);
  line-height: 1;
  letter-spacing: var(--track-readout);
  text-transform: uppercase;
  opacity: 0;
  transition: opacity var(--dur-micro) var(--ease);
}
.showcase__dialog[data-settled="true"] .showcase__play { opacity: 1; }

/* ---- the cleared slot: solid ground, one tile past the ink each side ----
   Width: the run's own ink + two clearances, snapped UP to whole tiles by
   showcase.js (--sc-chip-w); until it has measured, the natural width. The
   negative inline margins put the INK on the column's edge and the slot's
   edge one tile outboard of it. Height: the run's line box (whole rows). */
.showcase__chip {
  box-sizing: border-box;
  background: var(--ground);
  /* the transparency layer that keeps the palette at four colours: see the
     block on subpixel fringing at the top of this file. The slot is the right
     place for it - it is the one element that owns both the opaque ground and
     the run standing on it - and 0.1% of a 22-step difference is 0.02 of a
     byte, so no pixel of the ground moves. */
  opacity: 0.999;
  /* the slot sits one tile past the ink each side; the two bearings move the
     INK onto the column's edges inside that fixed box (see the note above) */
  padding-inline: calc(var(--sc-clear) - var(--sc-bear, 0px)) var(--sc-clear);
  margin-inline: calc(-1 * var(--sc-clear));
}

/* ---- words: the ranks under an open piece ----
     HEAD   the name (the <h2>): VOICE, 22px, mixed case, tight, INK. The one
            lit object under the picture, and the only rank on the field the
            page speaks in its own voice. Out of the composition on a
            placeholder alone, where the frame already sets the name in the
            page's own lit cells.
     LABEL  the spec line: READOUT, 11px, uppercase, 0.09em - the widest-tracked
            run on the field, because tracking is what makes this face read as
            an instrument reading. One flat dim run under a name; on a
            placeholder, where it is the whole caption, it takes 12px and lights
            its KIND.
     ROW    two ranks, uppercase, 0.08em: the READINGS (the position and CLOSE)
            at 11px in the dim tone, the ACTIONS (PREV / NEXT) at 12px in ink.
            Chrome sits above the picture, out of the caption.
   Size, case, tracking and tone all carry rank, and no rank depends on tone
   alone. Sizes come from showcase.js (--sc-titlefs / --sc-metafs) so the phone's
   smaller head is the same decision, in the same table, as the desktop's. */
.showcase__controls button {
  margin-block: 0;
  font-family: var(--font-readout);
  font-weight: 400;
  font-size: var(--fs-12);
  line-height: var(--sc-toprow); /* the row's own box: a slot is exactly its row tall */
  letter-spacing: var(--track-readout);
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--sc-dim);
}

/* the caption block: the head and its label, on the media's left rail. The
   two boxes MEET (showcase.js budgets no gap between them): the label's own
   leading is the whole gap, so the pair reads as one block. */
.showcase__title,
.showcase__meta {
  margin-block: 0;
  font-variant-numeric: tabular-nums;
  justify-self: start;
  width: var(--sc-chip-w, fit-content);
  /* the caption keeps the media's column (+ the slot's two clearances); under
     a narrow (portrait) column a line may run past its right edge rather than
     wrap into a tower */
  max-width: calc(max(var(--sc-w), 320px) + 2 * var(--sc-clear));
  white-space: normal; /* a long name or stack wraps to a second line; nothing is cut */
  text-wrap: balance;
}

.showcase__title {
  grid-area: 6 / 2 / 7 / 3;
  /* VOICE, which is what the piece's NAME is. The site has three type roles and
     the spec assigns this string to the same one as `beefydan`, `Developer` and
     `Storyteller`; it had drifted into READOUT by sharing a font shorthand with
     the meta line directly above, so the one word that names the work was set in
     the label face while the site's own wordmark was not. Now the caption block
     reads as what it is - a name in VOICE over its label in READOUT - the way
     every other head-and-label pair on the page does. */
  font-family: var(--font-voice);
  font-weight: 500;
  /* 22px desktop / 18px phone (showcase.js, ROWS). A PLATE LABEL'S HEAD, not a
     headline: it leads its own label by 2x and is beaten by the picture above
     it by a distance nothing in the caption can argue with. 22 sits between the
     site's 18 (body) and 28 (display) steps deliberately - the six-step scale
     in spec 3 is the PAGE's display ladder, and a label under a plate is a
     caption rank, the one place the page speaks about someone else's picture
     rather than in its own voice. */
  font-size: var(--sc-titlefs, 22px);
  line-height: var(--sc-titlelh);
  /* a name is set as it is written: The Script Factory. Uppercase here would
     echo the artwork's own baked lettering in a second all-caps run, and it is
     the case difference that tells a name from a label at a glance. */
  text-transform: none;
  letter-spacing: var(--track-voice);
  color: var(--ink); /* the one lit word under the picture */
}

.showcase__meta {
  grid-area: 8 / 2 / 9 / 3;
  /* READOUT, declared here rather than shared with the title: the two boxes
     share a geometry, not a face. */
  font-family: var(--font-readout);
  font-weight: 400;
  font-size: var(--sc-metafs, 11px);
  line-height: var(--sc-metalh);
  text-transform: uppercase;
  letter-spacing: 0.09em; /* the label's own width: a hair past the site's 0.08em */
  color: var(--sc-dim);
}

/* the spec line's own head: WHAT THE PIECE IS - and it is lit ONLY where this
   line is the whole caption.
   Under a name (every piece the page speaks for) the label is one flat run in
   the dim tone, because the block already has a head and a second ink run under
   a 22px ink name is two heads in a four-line composition. That is the whole of
   "demote the slug": size, tracking AND tone all step down together, and the
   caption has exactly one lit object - the piece's name.
   On a PLACEHOLDER the name is inside the frame and this line stands alone, so
   it carries the block's rank inside itself: the KIND lit, the record dim - one
   lit token among unlit ones, the lattice device's own grammar, at no cost in
   size, face or colour. */
.showcase__dialog[data-named="media"] .showcase__kind { color: var(--ink); }

/* ---- when the PAGE'S OWN TYPE is already in the frame, the caption does not
   repeat it ----
   One case, and only one: a placeholder, whose picture IS its title condensed
   into our 5x7 lit cells at a 56px cap. Those words are the page's, in the
   page's own MATRIX face, so setting them again 20px lower in VOICE is the page
   saying one thing twice - which is the real defect three earlier rounds kept
   circling and mis-locating. There the name line leaves the composition and the
   spec line captions the piece alone. The heading stays in the DOM and in the
   accessibility tree - it is the dialog's aria-labelledby - so the piece is
   still named to a screen reader; showcase.js (nameIsSetInTheFrame) budgets one
   caption line instead of two and zeroes the block's inner gap, so the field
   keeps the rows it gives up.
   A picture that DEPICTS lettering does NOT qualify: the words in it are the
   artwork's, and the page labels the artwork. See the long note at the top. */
.showcase__dialog[data-named="media"] .showcase__title {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  background: none;
}

.showcase__title,
.showcase__meta,
.showcase__controls {
  position: relative;
}

/* ---- the instrument row: TWO objects on the media's width ----
   The reading (11px, dim) and the actions (PREV + NEXT, real buttons) share
   ONE cleared slot on the left, six tiles apart; CLOSE stands alone on the
   right in its own slot, its last glyph on the picture's right edge. The
   buttons' boxes are their line boxes (so a slot is exactly its row tall);
   the hit area grows by a pseudo-element instead of padding. */
.showcase__controls {
  grid-area: 2 / 2 / 3 / 3;
  display: flex;
  align-items: center;
  height: var(--sc-toprow);
}

/* the reading and the actions, six tiles apart, in one cleared slot */
.showcase__pager {
  display: inline-flex;
  align-items: center;
  gap: var(--sc-grpgap);
  height: var(--sc-toprow);
  width: var(--sc-chip-w, auto);
  flex: none;
}

/* the actions: one pair, two tiles apart, the arrows bracketing them */
.showcase__keys {
  display: inline-flex;
  align-items: center;
  gap: var(--sc-keygap);
  height: var(--sc-toprow);
  flex: none;
}

/* ---- THE READING: 02/12, THE SMALLEST RANK ON THE FIELD ----
   The blind critic, against the reference: "the 02/12 counter is set in ~15px
   letterspaced mono in near-black, making it as heavy as the ~17px title that
   sits 730px below it." It was heavier still. The counter had been promoted to
   MATRIX - lit cells at a 2px glyph pixel, 14px of solid ink strokes - to cure
   an older defect, a numeral split across two greys. That cure worked and then
   overshot: lit cells are this site's HEADLINE texture (spec §3 gives MATRIX to
   the statement and the strip indices), so the piece's POSITION was being set
   in the loudest face the page owns, immediately above the work, while the
   work's own name sat 730px below it at 18px.

   Rank, not face, was the defect both times. A position is chrome: it is worth
   less than the picture, less than the picture's name, and less than the two
   actions beside it, which are things you can DO. So it takes the smallest rank
   in the composition - 11px READOUT in the dim tone the spec line wears - and
   the row steps 11 dim (where you are) / 12 ink (what you can do) / 11 dim (the
   way out) under a 22px name and a picture above everything. The two greys that
   started all this cannot return: there is one tone in the token and no second
   run inside it to mismatch.
   Grouping, not size, is what keeps the row from reading as five loose tokens:
   the reading stands six tiles off the actions, the actions one pair apart.
   Never red - the strip marks a card red because "current" needs marking among
   twelve; here there is one piece on the field and the work is the colour. Not
   focusable, so Tab runs prev -> next -> close. */
.showcase__index {
  display: inline-flex;
  align-items: center;
  height: var(--sc-toprow);
  flex: none;
}

/* ONE token - no floating slash - in tabular figures, so 01/12 and 11/12 are
   the same width and the actions beside it never step as you page through. Its
   own size, declared here and nowhere else, because it is the one reading on
   the page that is quieter than the row it sits in. */
.showcase__count {
  font-family: var(--font-readout);
  font-weight: 400;
  font-size: var(--sc-countfs);
  line-height: var(--sc-toprow);
  letter-spacing: var(--track-readout);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--sc-dim);
}

.showcase__controls button {
  position: relative;
  display: inline-flex;
  align-items: center;
  /* an arrow belongs to ITS word: 4px inside the button against 16px between
     the two buttons, so the pair steps 1 : 4 and reads as two labelled keys
     rather than as four evenly spaced marks. At 8px inside (where this stood)
     the internal and external gaps were 1 : 2 and the group blurred. */
  gap: var(--s-1);
  height: var(--sc-toprow);
  line-height: var(--sc-toprow);
  transition: color var(--dur-micro) var(--ease);
}
/* THE PAIR IS THE ROW'S ONE LIT OBJECT (see the block at the top of this file
   on why the step goes up and not down). PREV and NEXT are the two things you
   can do to the piece in front of you, so they carry the row's full value in
   --ink; the counter that says where you are and the exit that leaves both rest
   in the one dim tone. Two tones, because the site owns two text colours. */
.showcase__keys > button {
  padding: 0;
  margin: 0;
  color: var(--ink);
}
/* the hit box: a bar and a half tall, one gutter wide past the word - drawn
   by nothing, so the slot stays exactly the row's box */
.showcase__controls button::after {
  content: "";
  position: absolute;
  inset: -6px calc(-1 * var(--s-2));
}
/* CLOSE reads at rest and lights under the pointer / a focus ring: it is a
   reading until it is used, so the work keeps the field. The pair is already
   lit, so it answers the pointer with a HAIRLINE under the word instead - the
   card separators' own device, at the type's scale - rather than with a colour
   it has nowhere left to go to. A focus ring draws its own box either way. */
.showcase__close:hover,
.showcase__close:focus-visible { color: var(--ink); }
.showcase__keys > button:hover,
.showcase__keys > button:focus-visible {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
}
.showcase__controls button:disabled {
  color: color-mix(in srgb, var(--sc-dim) 45%, var(--ground));
  cursor: default;
  text-decoration: none;
}
.showcase__keys > button:disabled { color: color-mix(in srgb, var(--ink) 38%, var(--ground)); }
.showcase__controls button:disabled:hover { color: color-mix(in srgb, var(--sc-dim) 45%, var(--ground)); text-decoration: none; }
.showcase__keys > button:disabled:hover { color: color-mix(in srgb, var(--ink) 38%, var(--ground)); }
/* the ring on the button's own box (a whole-row slot): its lines fall in the
   seams between dot rows and columns, over no dot */
.showcase__controls button:focus-visible { outline-offset: 0; }

/* the pager arrows: the bar's twin-switch arrow, same face, same size */
.showcase__arrow { font-family: var(--font-readout); }

/* CLOSE: a slot of its own at the row's far end (the auto start margin), the
   slot's edge one tile past the column's right edge; the word's LAST glyph's
   ink ends on that edge - showcase.js measures the trailing bearing (the mono
   advance plus 0.08em of tracking) and --sc-rbear pulls it back.

   The slot is a WRAPPER, not the button. While the button wore the slot it also
   wore the slot's box, and a focus ring draws on the box: CLOSE's ring stood a
   tile proud of the word on both sides and ended 8px past the picture's right
   edge, while PREV's ring - a bare button - hugged its own word. Two rings on
   one row, two fits. Now the slot clears the dots and the button inside it is
   just the word, so every ring in the row draws on the same box. */
/* and it takes the counter's size, not the pair's: 11px, the quietest rank in
   the composition. Where tone had no step left to give (the block at the top of
   this file), size does - so the row's two readings stand together at 11 and
   the two actions between them stand alone at 12 in ink. */
.showcase__controls .showcase__close { font-size: var(--sc-countfs); }

.showcase__closer {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  height: var(--sc-toprow);
  width: var(--sc-chip-w, auto);
  flex: none;
  margin-block: 0;
  margin-inline: auto calc(-1 * var(--sc-clear));
  padding-block: 0;
  padding-inline: var(--sc-clear) calc(var(--sc-clear) - var(--sc-rbear, 0px));
}

/* ---- THE WORDS ABOUT THE PIECE (showcase.js, BODY) ----
   Where the column stands is the layout's decision: showcase.js writes its rect
   as --sc-px/py/pw/ph, on whole tiles, beside the picture or under the spec
   line. This block says only what the words ARE.

   A CLEARED SLOT, like every run of words on this field: the box stands one
   tile proud of its text on every side and paints the ground, so no dot of the
   lattice sits inside a letter. The text's own left edge is therefore the rect's
   x exactly - the picture's rail when the words stack under it, the column's
   rail when they stand beside it.

   VOICE, because this is a person describing his own work in sentences; READOUT
   is for labels. 15px on a 24px line box - three tiles on the desktop lattice,
   four on the phone's - which showcase.js writes as --sc-plh, because the
   column's height is snapped to whole lines of it. Full --ink: long text in the dim
   tone would fail the rubric's 4.5:1 for body copy on the white page.

   A long description scrolls INSIDE the column. The page behind is locked while
   a piece is open, so a column that let its words run past the field would make
   the end of them unreachable. overscroll-behavior keeps a flick that reaches
   the last line from pulling the page; the gutter is reserved so the scrollbar
   never lands on a word. */
.showcase__body {
  position: absolute;
  left: calc(var(--sc-px, 0px) - var(--sc-clear));
  top: calc(var(--sc-py, 0px) - var(--sc-clear));
  width: calc(var(--sc-pw, 0px) + 2 * var(--sc-clear));
  /* a CEILING, not a height: the slot is as tall as its words and never taller than
     the column the layout snapped to whole lines - a short description does not
     stand in a full-height blank slab with the lattice missing under it */
  height: auto;
  max-height: calc(var(--sc-ph, 0px) + 2 * var(--sc-clear));
  box-sizing: border-box;
  margin: 0;
  padding: var(--sc-clear);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-gutter: stable;
  scrollbar-width: thin;
  scrollbar-color: var(--sc-dim) transparent;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-voice);
  font-weight: 400;
  font-size: 15px;
  line-height: var(--sc-plh, calc(var(--pitch) * 3));
  letter-spacing: var(--track-voice);
  text-align: left;
  opacity: 1;
  transition: opacity var(--dur-micro) var(--ease);
}
.showcase__dialog[data-panel="none"] .showcase__body { display: none; }
.showcase__body > .showcase__para { margin: 0; }
/* one reading of the spec line, never broken inside itself (showcase.js renderWords) */
.showcase__token { white-space: nowrap; }
/* a paragraph break is a whole line box, so every line of every paragraph stays
   on the column's one leading grid and the snap above holds at any scroll */
.showcase__body > .showcase__para + .showcase__para { margin-top: var(--sc-plh, calc(var(--pitch) * 3)); }
.showcase__body:focus-visible { outline-offset: 0; }

/* the words step back first when a close is requested */
.showcase__dialog[data-state="closing"] :is(.showcase__title, .showcase__meta, .showcase__body, .showcase__controls, .showcase__play) {
  opacity: 0;
  transition: opacity var(--dur-micro) var(--ease);
  pointer-events: none;
}

/* ---- THE HEADER STANDS OUT OF THE WAY WHILE A PIECE IS ON THE FIELD ----
   The blind critic, comparing the rendered piece against the reference:
   "two stacked chrome rows sit between the viewer and the work, and they obey
   different grids - the site header centres its counter on the 1440px viewport
   at y 25 while the PREV / NEXT / CLOSE row aligns to the 576px image column
   at y 80."

   Last round the bar was QUIETENED for an open piece: the plane dropped, the
   rules and the clock and the lamp gone, one gray, every run in its own
   cleared slot. It was a good line. It was still a LINE - a second run of type
   on a second grid, stating the piece's title a second time (PLAYING NOW - THE
   SCRIPT FACTORY, 44px above the title that says it properly), plus two site
   links a modal has no use for. Quieting could not fix that, because the
   defect was the row's existence, not its volume.

   So the bar goes. One rule, and every property of the old block is a property
   this one does not need to state:
     the plane, the three rules, the clock, the lamp, the wordmark, the status
     and the twin switch are all invisible together, in one declaration.
   Its BOX stays - `visibility`, never `display` - so nothing reflows: the 48px
   band holds its place, the stage keeps its offset, the page's own dot field
   carries that band (body and .bar tile from the same origin at the same
   pitch, so the rows run through unbroken), and the composition reads it as
   AIR: 48 + 16 = 64px above the instrument row against 36px under the meta
   line. There is ONE row of chrome on an open piece and it stands on the
   piece's own column.

   THE BAR STAYS, AND SO DOES ITS PLAYHEAD. It used to go entirely - opacity 0,
   then visibility hidden - and that was defensible while the bar was a line of
   text. It is not now: the middle cell is a device playhead, a dotted rail with
   one station per piece and the current one lit --red, and its whole reason to
   exist is to say WHICH piece is playing. Hiding it the instant a piece opens
   meant the one readout the instrument is for was visible only during the
   reveal - the 700ms nobody is reading - and then the page carried a 48px
   band of empty chrome where the reading had been. The dialog's own column was
   already composed for a bar that stays (48 + 16 = 64px above the instrument
   row); nothing moves to bring it back.

   What the modal is owed instead is INERTNESS, not absence. showcase.js sets
   `inert` on the header for exactly as long as the frame is up: the bar is
   painted but unfocusable, unclickable and out of the accessibility tree, so
   the dialog's Tab loop is still the only thing on the page and the live region
   still stops announcing a title the dialog already names. Visible, silent,
   untouchable - which is what chrome behind a modal should be.

   The clock alone keeps `display: none`, and not for the pixel: readout.js
   stops its minute timer only when the cell's COMPUTED DISPLAY is none (it
   checks before scheduling the next tick), so an open piece must never leave a
   timer ticking behind a still picture. Losing it also costs the reading
   nothing - the time is the one value on the bar that has no bearing on the
   piece in front of you, and dropping it hands its run to the playhead.

   Scope: html[data-showcase-frame="on"], which showcase.js writes when the
   frame actually becomes VISIBLE (not when the card is clicked), so the swap
   lands with the frame rather than in the middle of the card's dissolve, and
   dropping the attribute restores the clock in one frame - the same frame the
   showcase leaves the screen. A page that never opens a piece is never
   touched. */
html[data-showcase-frame="on"] .bar__clock { display: none; }

/* ---- AND WHILE IT IS PAINTED, IT IS THE QUIETEST ROW ON THE SCREEN ----
   The blind critic, wave 2 round 2, against the reference: "the header restates
   the piece verbatim - PLAYING NOW — THE SCRIPT FACTORY - directly above a
   caption that already reads The Script Factory, and it carries a ~545px run of
   dots with a red marker. The busiest texture on the page sits farthest from
   the work."

   Inertness answered what the bar could DO behind a modal. It said nothing
   about what the bar SAYS, and what it was saying was the piece's name in near
   black, over a leader that ran two thirds of the screen, at the top edge -
   the furthest point from the picture and the first thing the eye lands on.
   Three subtractions, all of them scoped to the open frame and none of them
   touching the bar at rest:

     THE NAME. showcase.js asks readout.js, through its own api.set(), to read
     PLAYING NOW with no title, and (wave 2 round 4) it asks in the one way that
     still works: set('playing', '', '') with NO SLUG, because readout.js now
     resolves a missing title from the slug it is handed, plus position() for
     the station. See quietReadout.
     The rule below is what is left of the old belt, and it is deliberately NOT
     escalated to beat header.css's un-hide at >= 1024. It holds where P1 does
     not un-hide (below 1024, where readout.js drops the title anyway) and loses
     where P1 does - which is the right way round: if the API ever regresses,
     the failure should be the bar printing a name twice, not the bar printing
     "PLAYING NOW —" with nothing after the dash. A dangling field name is a
     defect this page can see; a duplicated one is a defect a critic can. The
     mechanism is the API and the API alone.

     THE SCALE. It is flex: 1 1 0 in header.css, so at rest it takes every pixel
     the words and the clock leave: 545px of lit cells with one red mark
     somewhere along it - the finest texture on the page, at the point furthest
     from the picture. It stands down while a piece is open, and the reading it
     carries is not lost, because the reading is already on the field TWICE and
     better both times: the position leads the piece's own instrument row
     (02/12, on the picture's left edge), and the piece is named under it. A
     scale exists to place one piece among twelve at a glance from across the
     room, which is what a page at REST needs and exactly what a page with the
     piece already open does not.
     Shortening it was measured first and rejected: capped at fifteen tiles it
     still drew, but P1's scale is one countable station per piece at a
     three-tile step, so twelve pieces need 34 columns - a 120px cap prints a
     scale that is missing a third of the library and readout.js says so in the
     console, correctly. A wrong reading quietly drawn is worse than no reading.
     `display: none` is the state P1 already handles by name (layoutTrack reports
     `off('hidden')` and draws nothing), so this costs one declaration and no
     warnings, and the scale comes back whole on close.

     THE TONE. Every word in the bar drops to --sc-dim, the one gray the
     caption's spec line wears - the wordmark, the state words AND the twin
     switch, which was the last full-ink run left up there (measured on the
     render: #0B0B0C at the top right, the same ink as the piece's name 780px
     below it, and the first thing the eye meets on a page whose subject is a
     picture). The two links keep their face and their size - they are the site's
     name and the way across - but they stop being ink, so the only ink above the
     picture is the picture's own name under it. One tone, one rank, no fifth
     colour.

   What is left while a piece plays is a device at its quietest: a red lamp
   saying live, two words saying the state, and two dim links. ONE red on the
   whole screen and it means live; one ink object on the whole screen and it is
   the work.

   Each of the three is independent of the others on purpose - the bar is P1's
   surface and it is being built in the same wave. If a class here is renamed
   the rule stops matching and the bar simply keeps its resting behaviour, which
   is a page that is a little loud, never a page that is broken; and the title
   is subtracted twice over (through readout.js's own API and by the first rule
   below), so it takes both mechanisms failing for the name to be printed
   twice. */
/* 2026-09-04: this hid the VALUE only, which was right while the playhead track
   existed — the track said which piece was playing, so the words did not need
   to. Dan had the track removed and the icons moved into that span, and hiding
   the value alone then left the bar reading a bare "PLAYING NOW" naming
   nothing: an orphan label is worse than either saying the name or saying
   nothing. So the whole readout stands down instead. The lamp is a sibling
   (.bar__live) and is untouched, so the bar still says LIVE — it just stops
   half-saying WHICH, and the showcase names the piece once, in VOICE, which is
   the "one name per open piece" rule Dan asked for in the first place. */
html[data-showcase-frame="on"] .bar__readout-text { display: none; }

/* ---- THE RAIL STANDS DOWN WHILE A PIECE IS UP. A DECLARED COLLISION. ----
   Read this before changing it: two owners disagree here in one wave, and the
   disagreement is written down rather than hidden in a selector.

   P1 put the scale BACK from header.css this round (a `.bar` third simple
   selector, 0,3,1, deliberately over this file's 0,2,1) with a real argument:
   the rail is the one reading only that row can give - which of twelve you are
   on, as a form - and an open piece is the first moment it has anything to say.

   Against it stand three consecutive BLIND passes on the composite, none of
   which saw either file:
     r1  "two stacked chrome rows sit between the viewer and the work, and they
          obey different grids"
     r2  "it carries a ~545px run of dots with a red marker. The busiest texture
          on the page sits farthest from the work."
     r4  "the red-square PLAYING NOW indicator is a music-player idiom sitting
          on a still image, and it is the only saturated pixel above the fold."
   Three readers, three rounds, one finding: at the moment a piece is open, the
   top of the screen is the loudest thing on it and the work is not.

   The middle way was BUILT AND MEASURED FIRST, because it would have cost P1
   nothing: keep the rail, take only the red, let the head read as the one LIT
   tick among dim ones (the lattice's own grammar, and what the spec line's KIND
   does). It does not survive the render. readout.js paints every station BEHIND
   the head in --ink, so an ink head at station 02 is the same mark as the ink
   terminal 66px to its left - two identical black ticks and no way to tell the
   position from the trace. P1's own note names why ("the difference between the
   position and the trace is height AND colour"); spending the colour leaves the
   height, and the height alone does not carry it. A reading drawn ambiguously
   is worse than a reading not drawn, so the rail goes down whole and comes back
   whole the frame the piece leaves.

   The scale is NOT the casualty it looks like. Which piece is playing is on the
   field twice while the frame is up, and better both times: the position leads
   the piece's own instrument row (02/12, on the picture's left edge) and the
   piece is named under it in the page's own voice. What the rail gives at REST
   - one piece among twelve, at a glance, from across the room - is exactly the
   reading a screen with the piece already open does not need.

   Equal specificity to P1's (0,3,1) and a later sheet, so it wins on ORDER, not
   by escalating: whoever settles this seam flips ONE line in ONE file, and both
   arguments are on the page above it. The colour rule under it is the belt -
   restore P1's rail and the red still does not come back. */
html[data-showcase-frame="on"] .bar .bar__track { display: none; }
html[data-showcase-frame="on"] .bar .bar__track-head { background-color: var(--ink); }

/* ---- AND THE LAMP GOES OUT WHILE A PICTURE IS UP ----
   The blind critic, wave 2 round 4, on the settled piece: "the red-square
   PLAYING NOW indicator is a music-player idiom sitting on a still image, and
   it is the only saturated pixel above the fold."

   Both halves are right, and they are one fault. The lamp is the device's LIVE
   light: on the reference board it sits on a bar that is genuinely transporting
   something, and at rest on this page it means the instrument is running, which
   is true and is P1's to say. Over an open piece it means neither. Nothing is
   playing - a still picture is being LOOKED at - so the one saturated mark on
   the screen is asserting a state the screen contradicts, and it is doing it at
   the point furthest from the work, where the eye lands first. Red is the
   site's only accent and spec 3 spends it on exactly one job at a time (live /
   current / active); an open piece has no such reading left to give, because
   the strip's own current-index red is behind the frame and the row over the
   picture is deliberately colourless. So while a piece is on the field there is
   NO red on the screen at all, and the only saturated thing in view is the
   artwork.

   Scoped like every other rule in this block: the resting bar keeps its lamp,
   this only takes it for as long as the frame is up, and the lamp's slot is
   `display: none` rather than transparent so the two words close up on the
   bar's own gap instead of hanging off a 16px void. The state is still stated -
   PLAYING NOW, in words, in the one dim tone - and words are what a bar behind a
   modal is for. */
html[data-showcase-frame="on"] .bar__live { display: none; }

html[data-showcase-frame="on"] :is(.bar__name, .bar__readout, .bar__switch) { color: var(--sc-dim); }

/* ---- AND THEN THE WHOLE BAND STANDS BACK TO 40% ----
   Wave 2 round 5, the blind critic counting marks rather than reading rows:
   "seven micro-labels competing at one tone" - the row's four, plus beefydan,
   PLAYING NOW and STORYTELLER →, plus the two vertical rules between the bar's
   cells. Every subtraction above took something OUT of the bar (the clock, the
   lamp, the rail, the piece's name, the ink) and every one of them was right,
   and the thing they could not fix is that what remains is still THREE runs of
   type and two rules at the top of a screen whose subject is one picture.
   Volume was the last variable left, so it is spent: while a piece is on the
   field the whole band - ground, rules, dots, wordmark, state and switch - is
   painted at 40%. Seven marks become four, and the four that survive are the
   piece's own row on the piece's own column.

   Opacity, not `visibility: hidden`, and the difference is the point. The band
   keeps its box either way (the composition budgets 48 + 16 = 64px of air over
   the instrument row and reads the bar as air), but a bar taken to nothing
   leaves a bare hairline floating across the top of the page with a void above
   it, and the visitor loses every trace of where they are. At 40% the
   instrument is still legibly there, still says the state, still offers the way
   across - it has simply stepped behind the work, which is what a modal's
   chrome is for.

   The contrast floor does not apply here and this is the one place on the site
   where that is true: showcase.js sets `inert` on the header for exactly as
   long as the frame is up, so the bar is unfocusable, unclickable and out of
   the accessibility tree - an inactive user interface component, which WCAG
   1.4.3 exempts by name. Nothing a screen reader can reach is dimmed, and the
   frame the piece leaves, `inert` and this rule are dropped together.
   A side effect worth naming: an element at opacity < 1 paints into a
   transparency layer, so Chromium's LCD subpixel text is off inside the bar
   for free - the same fringing the dialog buys with -webkit-font-smoothing. */
html[data-showcase-frame="on"] .bar { opacity: 0.4; }
/* the two links re-state --ink on hover/focus; while the bar is inert neither
   can fire, but the declaration would still win on specificity */
html[data-showcase-frame="on"] :is(.bar__name, .bar__switch):is(:hover, :focus-visible) { color: var(--sc-dim); }
html[data-showcase-frame="on"] :is(.bar__name, .bar__switch):is(:hover, :focus-visible) .bar__switch-arrow { color: var(--sc-dim); }

/* ---- AND THE LINK RAIL STANDS DOWN. A DECLARED COLLISION (2026-09-04). ----
   A `nav.rail` of social links landed above the stage this round, from another
   piece, and it sits OUTSIDE both `.bar` and `[data-stage]` - so every
   subtraction above walks past it and an open piece was composing under TWO
   rows of chrome, twelve icons of it, on a third grid. That is the exact defect
   the whole block above exists to answer ("two stacked chrome rows sit between
   the viewer and the work"), and a modal owes more than tidiness here: nothing
   behind a dialog should compete for the eye or be reachable.

   So while a piece is on the field the rail is `visibility: hidden` - never
   `display`, so its box holds and NOTHING on the page moves between the click
   and the settle - and showcase.js adds `inert` to it beside the bar's, so it
   is out of the tab order and out of the accessibility tree for exactly as long.
   Both are dropped the frame the piece leaves.

   This is written down rather than hidden in a selector because it is a rule
   about somebody else's element: the rail's owner keeps every decision about
   the rail at rest, and this file only says what happens to it while a dialog
   covers the page. If the rail should instead stay lit while a piece is open,
   delete these two lines and the `.rail` entry in setChromeInert() - they are
   the only two places that touch it. */
html[data-showcase-frame="on"] .rail { visibility: hidden; }

/* ---- AN OPEN PIECE IS ABOVE THE WORK STRIP. The frame is `position: absolute;
   inset: 0` inside the stage, and the stage is a sticky element with a z-index
   of its own - which makes it a stacking context, which traps --z-showcase (20)
   INSIDE it. The band is the stage's SIBLING at --z-band, so 2 beat 1 and the
   strip painted over the open dialog wherever the two overlap. At 1440 they
   never do (the open parks the page at the top and the band is off-screen), so
   it was invisible on the desktop and total on the phone: at 390 the piece
   opens with the band 253px up the screen, and `elementFromPoint` at the bottom
   of an open piece returned a strip card - the modal was being painted over AND
   out-hit-tested by the page behind it.

   The stage takes the layer, not the frame: --z-showcase can only order the
   stage's own children, and only the context's own z can beat a sibling. Above
   the band, below the bar - so the piece covers the strip and the instrument row
   (which is now inert, and reading which piece is playing) stays over both. The
   flag is dropped in the same frame the showcase leaves the screen, so the
   stage is back on its own layer exactly when there is nothing left to lift. */
html[data-showcase-frame="on"] .stage { z-index: var(--z-stage-open); }

/* ---- dev scrub (?scrub) ---- */
.showcase__scrub {
  position: absolute;
  left: var(--gutter);
  right: var(--gutter);
  bottom: var(--s-3);
  width: auto;
  margin: 0;
  accent-color: var(--red);
}

/* ---- mobile: same column, taller hit boxes ---- */
@media (max-width: 767px) {
  .showcase__controls button::after { inset: -10px calc(-1 * var(--s-2)); }
}

/* reduced motion: every state change here is a swap, never a fade - the bar
   included (visibility flips in the same frame as the dialog appears) */
@media (prefers-reduced-motion: reduce) {
  .showcase__dialog,
  .showcase__controls button,
  .showcase__play { transition: none; }
}
