/* ====================================================================
   WCPE INSTRUCTOR STUDIO — APP SHELL / RAIL / TOPBAR / TRANSPORT / OVERLAYS
   ====================================================================
   Cascade layer 3 of 7. Owns regions 3-5, 7 (+7b), 15-17 and 22 — app shell,
   setup rail, workspace/topbar, script-help modal, device-settings gear,
   transport bar, the retired pop-out, toasts + QA sink, support gate.
   Region 16 is RETIRED (pop-out, item 52) and regions 7b and 22 post-date the
   mockup; the shipped banner text is the rule wherever they differ.

   LOAD ORDER IS LOAD-BEARING:
     tokens -> base -> shell -> stage -> panels -> producer -> responsive
   This file is #3. Nothing here may depend on a rule that loads later.
   The numbered region banners below are this file's own. The numbering is one
   flat sequence 1-22 shared across the seven cascade files and no two files
   repeat a number; it was inherited from the concept-6 "Studio Rail" mockup the
   cascade was first extracted from. That mockup is deleted and these banners
   are the source now. Region map: docs/CODEBASE.html#style-css-regions
   ==================================================================== */
/* ====================================================================
   3 · APP SHELL
   Root grid: rail column + workspace column, full viewport height
   (dvh with an svh floor so mobile URL bars cannot shrink the app).
   The background signature — navy radial glow + faint cyan wash over
   the void — is the approved palette's own page background
   (docs/CODEBASE.html#style-palette); do not restyle.
   ==================================================================== */
.app-shell{
  width:100%;
  height:100dvh;
  min-height:100svh;
  display:grid;
  grid-template-columns:var(--rail-w) minmax(0,1fr);
  overflow:hidden; /* item-32 audit: the frozen page-never-scrolls premise; 18b releases it below 240px, and every interior region scrolls or grows */
  background:
    radial-gradient(circle at 48% -10%,rgba(7,76,142,.34),transparent 36%),
    linear-gradient(135deg,rgba(34,211,238,.03),transparent 30%),
    var(--void);
}
/* ====================================================================
   4 · SETUP RAIL — the left column IS the setup process
   Brand lockup, step buttons with status badges, anchored setup
   sheets, presence/help, and (full mode) the Minimize-menu control.
   Step state classes: .is-attention (red — needs the user),
   .is-active (cyan — current step), .is-good (complete), .is-optional
   (dim). Status is icon + shape, never color alone; icon/collapsed
   modes move the status to a corner badge. Setup sheets are fixed
   overlays anchored to the rail in compact modes and render inline
   inside the rail in full mode (see the full-mode block).
   ==================================================================== */
/* Item 32 (the panel-height rule): a region holding CONTROLS either grows or
   scrolls — it never lets content paint over a neighbour. The compact rail
   assumed a column of icons fits any height; at the 200%-browser-zoom halves
   of the tested viewport matrix (docs/CODEBASE.html#arch-viewport-matrix —
   422x195, 640x310) it does not, and with the old minmax(0,1fr) the
   overflowing step buttons rendered UNDER the help/presence cluster that
   follows them in the DOM — measured by the ?qa=1 reachability probe:
   elementFromPoint over "PowerPoint" landed on .presence-button, and the
   producer's four verbs were unreachable outright (the help cluster alone
   outmeasures a 195px rail, so no inner scroller could save them). The
   min-content floor keeps the menu at its honest height and the whole RAIL
   scrolls: brand, every step and the help cluster are all one scroll away.
   auto shows nothing wherever content fits — every matrix viewport at the
   design's text size renders identically — and the anchored setup sheets are
   position:fixed, which a scrolling ancestor does not clip. Full mode
   overrides both declarations with its own arithmetic (19/19a: the rail is
   overflow:hidden there and the LIST is the scroller). */
.setup-rail{
  position:relative;
  z-index:100;
  grid-row:1;
  min-width:0;
  min-height:0;
  padding:7px 6px;
  display:grid;
  grid-template-rows:auto minmax(min-content,1fr) auto;
  gap:6px;
  border-right:1px solid rgba(34,211,238,.52);
  background:linear-gradient(180deg,var(--navy),var(--surface) 48%,var(--raised));
  box-shadow:10px 0 32px -26px var(--cyan);
  overflow-y:auto;
  overscroll-behavior:contain;
}
.brand-lockup{
  min-height:48px;
  padding:4px;
  display:grid;
  place-items:center;
  border:1px solid rgba(34,211,238,.38);
  border-radius:var(--card-radius);
  background:rgba(8,51,97,.48);
}
.brand-fan{
  display:block;
  width:88%;
  height:auto;
  aspect-ratio:357/129;
  object-fit:contain;
  filter:drop-shadow(0 0 7px rgba(34,211,238,.46));
}
.brand-lockup::after{
  content:"WESTERN CPE";
  display:none;
  color:var(--text);
  font-size:15px;
  font-weight:900;
  line-height:1.1;
  letter-spacing:.18em;
  text-align:center;
  white-space:nowrap;
}
.rail-heading{display:none}
.setup-list,.producer-menu{
  min-width:0;
  min-height:0;
  margin:0;
  padding:0;
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:4px;
  list-style:none;
  overflow:visible;
}
.setup-item{position:relative;min-width:0}
/* Accessibility beats fidelity to the frozen mockup, and the deviation is
   commented here rather than applied silently
   (docs/CODEBASE.html#style-accessibility-over-fidelity; S9, Hudson H3).
   These buttons are flex items of a height-constrained scrolling column
   (.setup-list/.producer-menu go overflow-y:auto in the full-mode band),
   and an EXPLICIT min-height replaces flex's automatic content minimum —
   so once 200% text wrapped a populated producer sub-label ("11 sent ·
   37 logged · 11 failed") past the 54px floor, flex-shrink compressed
   the button back to the floor and the overflowing text painted under
   the next button's opaque background: the attention words visually
   gone, which is content lost at 200%
   (docs/CODEBASE.html#style-zoom-and-reflow). flex-shrink:0 restores the
   honest behavior the scroll container was built for — each button keeps its
   content height and the COLUMN scrolls. In the icon-rail modes copy is
   display:none and content height equals the floor, so nothing changes
   at the design's text size. */
.setup-button,.producer-nav-button,.presence-button{
  position:relative;
  flex-shrink:0;
  width:48px;
  min-width:48px;
  min-height:48px;
  padding:10px;
  display:grid;
  place-items:center;
  border:1px solid var(--line);
  border-radius:var(--control-radius);
  background:var(--raised);
  color:var(--text);
}
.setup-button:hover,.producer-nav-button:hover,.presence-button:hover{
  border-color:var(--cyan);
  box-shadow:var(--glow);
}
.setup-button.is-attention{
  border:2px solid var(--red);
  box-shadow:0 0 20px -8px var(--red),inset 3px 0 0 var(--red);
}
.setup-button.is-active:not(.is-attention){
  border:2px solid var(--cyan);
  background:linear-gradient(90deg,var(--blue),var(--raised));
  box-shadow:var(--glow);
}
.setup-button.is-good .status-icon,.producer-nav-button.is-good .status-icon{color:var(--good)}
.setup-button.is-attention .status-icon{color:var(--red)}
.setup-button.is-optional .status-icon{color:var(--dim)}
.step-copy,.producer-nav-copy{display:none}
.step-icon,.producer-nav-icon,.presence-icon{width:26px;height:26px;fill:var(--cyan)}
.status-icon{
  position:absolute;
  right:-3px;
  bottom:-3px;
  width:20px;
  height:20px;
  display:grid;
  place-items:center;
  border:2px solid var(--surface);
  border-radius:50%;
  background:var(--surface);
}
.status-icon svg{width:16px;height:16px;fill:currentColor}
.setup-sheet{
  position:fixed;
  z-index:120;
  left:calc(var(--rail-w) + 8px);
  top:calc(var(--top-h) + 8px);
  width:min(360px,calc(100vw - var(--rail-w) - 16px));
  max-height:calc(100dvh - var(--top-h) - var(--transport-h) - 16px);
  padding:16px;
  display:none;
  align-content:start;
  gap:12px;
  overflow-y:auto;
  border:2px solid var(--cyan);
  border-radius:var(--major-radius);
  background:rgba(11,19,34,.99);
  box-shadow:var(--shadow),0 0 34px -11px var(--cyan);
}
.setup-item.is-open>.setup-sheet{display:grid}
.setup-sheet-header{display:flex;align-items:center;gap:10px}
.setup-sheet-header h2{min-width:0;margin:0;font-size:21px;line-height:1.1}
.setup-close{
  margin-left:auto;
  min-width:88px;
  min-height:44px;
  padding:8px 12px;
  display:flex;
  align-items:center;
  justify-content:center;
  gap:7px;
  border-radius:var(--control-radius);
  font-size:15px;
  font-weight:900;
  white-space:nowrap;
}
.setup-close svg{width:20px;height:20px;fill:currentColor}
.setup-sheet p{margin:0;color:var(--dim);font-size:16px}
.setup-control{display:grid;gap:7px}
.setup-control>span{font-size:15px;font-weight:900}
.setup-select,.setup-action{
  width:100%;
  min-height:48px;
  padding:9px 12px;
  border-radius:var(--control-radius);
  font-size:15px;
  font-weight:800;
}
.setup-action{
  display:flex;
  align-items:center;
  justify-content:center;
  gap:8px;
  border-color:var(--steel);
}
.setup-action.primary{border:2px solid var(--azure);background:var(--blue);box-shadow:var(--azure-glow)}
/* base.css:29 gives a colour to button/select/textarea/input and NOT to <a>, so
   an anchor wearing .setup-action keeps the user agent's link colour and
   underline. Measured on the dark theme that is rgb(158,158,255) underlined on
   the primary button's rgb(7,76,142) — 3.6:1, under the 4.5:1 AA floor at this
   15px/800 weight, and reading as an already-visited link rather than a button.
   It is the first thing a producer ever sees (src/surfaces/producer.php:76 is
   the only such anchor today). Match the buttons rather than pick a colour. */
a.setup-action{color:var(--text);text-decoration:none}
.setup-action svg{width:22px;height:22px;fill:var(--cyan)}
.presence-button{
  align-self:end;
  border:2px solid var(--cyan);
  background:rgba(7,76,142,.65);
  box-shadow:var(--glow);
}
.producer-help{align-self:end;min-width:0}
.producer-help-copy{display:none}
/* Chat attention (Tor, 2026-08-17) — the corner .presence-dot bullet is
   retired. A message not yet seen turns the chat glyph green and shakes the
   door once: the shake is the MOMENT, the green is the STANDING unread fact
   (presence.js clears it when the chat panel goes on stage), and the worded
   twin rides the accessible name — never color alone. The producer watches a
   recording session, so their shake is the subtle variant. A greyed door
   (no producer yet) leans on base.css's button:disabled dim; the glyph goes
   grey with it so the state reads at a glance from across a desk. */
.presence-button:disabled .presence-icon{fill:var(--dim)}
.presence-button.has-unread .presence-icon{
  fill:var(--good);
  filter:drop-shadow(0 0 6px var(--good));
}
.presence-button.is-nudged .presence-icon{animation:chat-nudge .6s ease-in-out}
[data-presence-chat="producer"].is-nudged .presence-icon{animation-name:chat-nudge-subtle}
@keyframes chat-nudge{
  0%,100%{transform:translateX(0)}
  15%,45%,75%{transform:translateX(-3px)}
  30%,60%,90%{transform:translateX(3px)}
}
@keyframes chat-nudge-subtle{
  0%,100%{transform:translateX(0)}
  20%,60%{transform:translateX(-1px)}
  40%,80%{transform:translateX(1px)}
}
@media (prefers-reduced-motion:reduce){
  .presence-button.is-nudged .presence-icon{animation:none}
}
.producer-nav-button.is-active{
  border:2px solid var(--cyan);
  background:linear-gradient(90deg,var(--blue),var(--raised));
  box-shadow:var(--glow);
}
/* ====================================================================
   5 · WORKSPACE + TOPBAR
   Session title, the global ON AIR pill, and (styled here for compact
   widths) the producer dashboard shortcut. .global-air-status.is-live
   is the recording state — red border, red glow, pulse. Reduced-motion
   removes the pulse but never the state.
   ==================================================================== */
/* Item 32 — accessibility beats fidelity to the frozen mockup
   (docs/CODEBASE.html#style-accessibility-over-fidelity). The mockup pins the
   topbar row to a constant, and the topbar is a row of text (title + clock)
   that grows with text zoom while a constant cannot: at 200% text a
   realistic session title left the bar's box and painted over the stage
   heading — content loss the fixed row hid rather than paid for. minmax
   keeps the mockup's exact height wherever
   the content fits it (every tested viewport,
   docs/CODEBASE.html#arch-viewport-matrix, at the design's text size renders
   identically) and grows the bar only when its own content is genuinely
   taller; the stage row shrinks instead — a stage that shrinks is correct, a
   control nobody can reach is not
   (docs/CODEBASE.html#style-zoom-and-reflow). The portrait and
   short-landscape blocks in responsive.css keep their own tighter
   declarations of the same shape. overflow:hidden survives here as the
   frozen shell's layout clip: with every row minmax(...,auto) or scrolling
   inside itself, nothing real can reach this edge. */
.workspace{
  min-width:0;
  min-height:0;
  display:grid;
  grid-template-rows:minmax(var(--top-h),auto) minmax(0,1fr);
  overflow:hidden;
}
.topbar{
  min-width:0;
  min-height:0;
  padding:7px 9px;
  display:grid;
  /* Item 60 S2 (Tor's R8): THREE columns — title, air chip, Settings. The
     fourth was item 56 S3's Switch-session slot, and R8 moved that button to
     the left menu; the instructor bar never filled a fourth column at all, so
     retiring it costs that surface nothing. The air chip keeps its 118px
     floor from .global-air-status below either way. */
  grid-template-columns:minmax(76px,1fr) auto auto;
  align-items:center;
  gap:7px;
  border-bottom:1px solid var(--line);
  background:rgba(7,14,26,.94);
}
/* Boko F5: the topbar is a size container so its own rules can answer WIDTH
   PRESSURE — the ratio of the bar's width to its own text size — which is the
   only honest trigger for a 200% text zoom that width media queries cannot
   see.
   Item 60 S2 fix cycle (Stinker F4) moved this from .producer-topbar to the
   shared .topbar. It was producer-scoped because in item 56 the only
   word-bearing furniture in the bar was the producer's own Switch button, so
   the instructor bar had nothing to relieve. S2 changed that: the merged
   Settings hub carries a visible label and it is shared by BOTH roles. With
   the relief still producer-only, the instructor's bar kept the label at
   every width above 520px and paid for it — measured against pristine
   d8f77ea at 200% text, the instructor's session clock ("Ends 12:30 PM ·
   Finish is safe") left the topbar AND the 900px viewport at 600x900 and
   640x900, where the base tree was green. The asymmetry was the bug, so the
   container is now shared and the two bars get the same honest trigger.
   What stays producer-scoped is the AIR CHIP's half of the relief
   (responsive.css §19d): that rule was measured for the producer bar and the
   instructor chip keeps its own behaviour byte-identical. */
.topbar{container-type:inline-size;container-name:topbar}
.session-title{min-width:0;display:grid;align-content:center}
/* The "open on another device" chip (multi-device, 2026-08-12): quiet and
   informational — dim words under the session identity, never an alarm. The
   glyph rides currentColor and the words wrap rather than truncate (the
   title column is already the topbar's one flexible track); 15px IS the 65+
   floor, deliberately at it and not above — this is ambient status, not an
   action. Hidden by default in markup; features/presence.js flips it on ≥2
   live seats. */
.seat-note{
  margin-top:3px;
  display:inline-flex;
  align-items:center;
  gap:6px;
  color:var(--dim);
}
.seat-note svg{width:17px;height:17px;fill:currentColor;flex:0 0 auto}
.seat-note span{font-size:15px;line-height:1.3;overflow-wrap:anywhere}
/* The pre-join wizard is a first landing with one job; its compact masthead
   keeps only the brand, session name and clock (slice 3). The seat fact
   returns with the studio chrome the moment the instructor enters. */
.app-shell.prejoin .seat-note{display:none}
/* Long-token policy — server data breaks rather than truncates or overflows
   (docs/CODEBASE.html#style-long-token-policy): the session title is server
   data and can arrive as one unbroken string. anywhere wraps it instead of
   overflowing the topbar; ordinary titles still break only at spaces. */
.session-title h1{
  margin:0;
  color:var(--text);
  font-size:18px;
  line-height:1.05;
  overflow-wrap:anywhere;
  word-break:normal;
}
/* Boko F1 (item 56 fix cycle; a deviation from the frozen mockup, documented
   at the site — docs/CODEBASE.html#style-accessibility-over-fidelity): the h1
   now carries UNBOUNDED server data (the session title) and the taller tiers
   deliberately let the topbar row grow (item 32's minmax) with no cap — an
   ordinary 80-character course title at 200% text measured a 541px bar at
   1440x900 and at the GATED 1180x820 pushed the entire workspace off a page
   that does not scroll (24 audit violations, every transport and rack verb
   unreachable). So the producer title block carries the compact tiers' own
   answer at every width: four FULL-tier h1 lines plus the state line
   (4x25 + 21 = 121, capped 124 with slack clear of font rounding) and past
   it the block scrolls in place — nothing truncated, nothing clipped dead,
   the workspace keeps the screen. Invisible in every ordinary case (an
   80-char title at 1180 wraps to two 25px lines at 100%). The portrait
   tier's tighter caps (responsive.css, ≤520w) still override; the
   instructor topbar carries neither class and is untouched.

   Boko F4 — WHERE the cap lives is load-bearing. It was on
   .producer-session-title, the .session-title grid itself, which is
   align-content:center above: a centered scrollport puts its overflow
   ABOVE the box, where scrollTop cannot reach (forced to -9999 the head
   never returns), so a long title rendered its own first line 37px out of
   view on a phone and 1486px out at 1180x820 zoomed — the producer read
   the TAIL of the name with the state line pushed clean out, which is the
   very question the session bar exists to answer. So each CHILD carries
   its own cap instead: a block's content starts at its own top edge and
   overflows downward into reach, which is the instructor topbar's existing
   idiom (it caps its child h1) rather than a new one. Capping them
   separately also keeps the state line OUT of the title's scrollport —
   under one shared cap a long name pushed instructor/LIVE/times out of the
   visible box, mutually exclusive with the title's head. Same total budget
   the collapse fix was measured against: 80 (h1, the instructor's own
   constant — 3 FULL-tier lines) + 44 (state, 2 lines) + the 2px gap. */
.producer-session-title h1{max-height:80px;overflow-y:auto}
/* Item 56 S3: the producer topbar's state line under the session h1 —
   instructor · state word · times · presence, the session-clock's 15px
   rhythm. LIVE takes the red token with its ● glyph: the word carries the
   state, never colour alone (docs/CODEBASE.html#style-glyph-needs-word).
   Server data wraps (docs/CODEBASE.html#style-long-token-policy). */
/* ITEM 62 — the cap is SEVEN of this line's OWN line boxes (7 x 1.3), not a
   pixel constant. Two faults were paid off here.
   (1) A px cap is only ever right at ONE text size. 44px is two lines at the
       design's size and barely one at 200%, and the FULL tier patched that
       with a second constant (80px, behind a container query) calibrated
       against whatever string one fixture happened to produce that day.
   (2) That 80px sat ON the wrap boundary. The formatter omits the first
       meridiem when a session's start and end share one ("3:08 – 5:08 PM")
       and prints BOTH when they straddle noon ("11:46 AM – 1:46 PM"), and
       the longer branch wrapped to a third line: 117px of content in an 80px
       box. Two strings of near-identical length measured 78 and 117 — the
       whole difference between two rows and three — so trivial glyph widths,
       or a width change anywhere else in the bar, flipped a fact off screen.
   SEVEN is derived from the string the writer can really emit, and the first
   cycle got that number wrong in a way worth writing down, because it is the
   same mistake one layer up (Stinker F1). It sized the cap at FIVE against a
   fixture rendering `Approved` (8) and `CDT` (3) — the shortest members of
   two vocabularies — and called it the longest realistic string. The real
   ceiling is:
     52  instructor name with credentials (tests/producer-shell.mjs CEIL_NAME)
     14  `Ended — review`, longest of the state vocabulary
     11  `Fri, Jul 17`, day at its two-digit width
     28  `10:30 AM – 12:30 PM GMT-2:30` — two meridiems, two-digit hours, and
         Newfoundland's EIGHT-character short name. America/St_Johns is in
         tz_choices(); Chromium renders it GMT-2:30 / GMT-3:30 in both DST
         phases, where every other served zone is 3 and Alaska is 4.
     22  `instructor in the room`
     12  four ` · ` separators — three from render(), one from timesLine
   = 139 characters, and the suite RENDERS it rather than arguing it. Measured:
   THREE line boxes at 100% and SIX at 200% at 820x1180, the narrowest tier
   this base cap serves. Seven is that worst case plus a whole spare line box,
   and because the cap is stated in em it stays a whole spare box at every
   text size instead of only at the one it was measured at.
   Raising it costs nothing until a string is genuinely that long: max-height
   adds no height, so the bar grows only for facts that really need the room.
   ONE tier overrides this: the portrait phone (responsive.css's
   max-width:520px + min-height:521px band), which has no height to spend and
   carries a measured pair of pixel caps and its own scroll-in-place
   deviation from the mockup, documented there
   (docs/CODEBASE.html#style-accessibility-over-fidelity). Every other tier —
   including the short-landscape band, which pins the bar's row and scrolls
   the BAR instead — inherits this cap. */
.session-bar-state{
  min-width:0;
  max-height:9.1em;
  overflow-y:auto;
  margin:2px 0 0;
  color:var(--dim);
  font:800 15px/1.3 "Studio Sans",Tahoma,Arial,Helvetica,sans-serif;
  overflow-wrap:anywhere;
  word-break:normal;
}
.session-bar-state .session-bar-live{color:var(--red);font-weight:900}
.session-clock{
  min-width:0;
  display:grid;
  margin-top:2px;
  color:var(--dim);
  font:800 15px/1.2 "Studio Sans",Tahoma,Arial,Helvetica,sans-serif;
}
.session-clock-copy,.session-clock-reserve{
  min-width:0;
  grid-area:1/1;
  display:block;
  overflow-wrap:anywhere;
}
/* The invisible longest-state pair reserves the clock's exact state-change
   footprint at every text size. It prevents a running take from reflowing when
   the wording changes while remaining absent from the accessibility tree. */
.session-clock-reserve{visibility:hidden}
.session-clock-separator{padding-inline:5px;color:currentColor}
.session-clock.is-running-long{color:var(--cyan);font-weight:900}
.session-clock.is-closed{color:var(--red);font-weight:900}
/* The mockup's .producer-sessions-shortcut rules were extracted here but the
   class never matched anything rendered — at the time the shortcut button was
   .producer-nav-button.producer-dashboard-button (rail-producer.php), the same
   markup/CSS mismatch the frozen mockup itself carries. Deleted in the S8
   producer slice alongside the producer-action-bar zoom fix below, after
   verifying the class matches no markup anywhere, frozen mockup included;
   zero pixels change. (That button has since been retired too — item 60 S4 —
   so the producer's door back to the picker is now the rail's own head,
   [data-producer-section="sessions"] at rail-producer.php:97.) */
/* position:relative anchors the ruling-125 reason clause, which is an .sr-only
   span — position:absolute with a 1px box — and an absolutely positioned
   element with NO positioned ancestor resolves its static position against the
   VIEWPORT. With the first-draft word it was measured at x=406 on a 390px
   phone, overflowing the page by itself. NOT RE-MEASURED against Tor's shorter
   `Standby`, unlike the two width reliefs beside it, which were and were
   removed: this one is a correctness anchor for a span that exists whatever the
   word says, so it is kept and the gap in its evidence is stated rather than
   quietly assumed away. */
.global-air-status{
  position:relative;
  min-width:118px;
  min-height:48px;
  padding:8px 10px;
  display:flex;
  align-items:center;
  justify-content:center;
  gap:7px;
  border:2px solid var(--steel);
  border-radius:var(--control-radius);
  background:var(--surface);
  color:var(--text);
  font:900 15px/1.1 ui-monospace,SFMono-Regular,Menlo,monospace;
  text-align:center;
  white-space:nowrap;
}
/* The pill's longest-state ghost (studio.php / producer.php carry the
   per-role string) — the .session-clock-reserve idiom one component over.
   Everywhere the pill is a nowrap flex row its own min-width already fixes
   the footprint, so the ghost stays out of the row entirely; only the
   portrait phone bar (responsive.css 18c), where the pill is a full-width
   wrapping grid, stacks it under the live span so Off air -> ON AIR cannot
   change the bar's height mid-take. visibility:hidden is what keeps it
   invisible THERE; display:none is what keeps it out of every other mode. */
.global-air-reserve{display:none;visibility:hidden}
.global-air-status.is-live{
  border-color:var(--red);
  color:var(--text);
  background:rgba(255,59,107,.18);
  box-shadow:0 0 24px -8px var(--red);
  animation:onAirPulse 1.4s ease-in-out infinite;
}
.air-dot{color:var(--red)}
/* Pinned inside the chip (see position:relative above): the default sr-only
   offsets are -1px margins from wherever the inline flow left it. */
.global-air-status .sr-only{left:0;top:0;margin:0}
@keyframes onAirPulse{50%{box-shadow:0 0 36px -4px var(--red)}}
.rail-collapse{display:none}
/* ====================================================================
   7 · SCRIPT HELP MODAL — the teaching-dialog pattern
   Center-screen card over a dimmed, blurred backdrop; focus moves to
   the close button and the shell behind goes inert; Esc, click-outside,
   or "Got it" closes. Reuse this exact pattern for any modal that
   teaches a convention — contextual help beats documentation nobody
   finds.
   ==================================================================== */
.script-help-modal{
  position:fixed;
  z-index:220;
  inset:0;
  display:grid;
  place-items:center;
  padding:16px;
  background:rgba(5,8,15,.72);
  backdrop-filter:blur(6px);
}
.script-help-modal[hidden]{display:none}
.script-help-card{
  width:min(560px,100%);
  max-height:calc(100dvh - 32px);
  padding:20px;
  display:grid;
  gap:14px;
  overflow-y:auto;
  border:2px solid var(--cyan);
  border-radius:var(--card-radius);
  background:var(--surface);
  box-shadow:var(--shadow),var(--glow);
}
.script-help-card h2{margin:0;font-size:23px}
.script-help-list{margin:0;padding-left:22px;display:grid;gap:10px}
.script-help-list li{font-size:16px;line-height:1.45;overflow-wrap:normal;word-break:normal}
.script-help-list code{padding:1px 7px;border:1px solid var(--line);border-radius:var(--control-radius);background:var(--raised);color:var(--cyan);font:900 15px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace}
.script-help-example{display:grid;gap:6px}
.script-help-example span{color:var(--cyan);font:900 15px/1 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.08em;text-transform:uppercase}
.script-help-example pre{
  margin:0;
  padding:12px 14px;
  overflow-x:auto;
  border:1px solid var(--line);
  border-left:4px solid var(--cyan);
  border-radius:var(--control-radius);
  background:var(--raised);
  color:var(--text);
  font:600 15px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;
}
.script-help-close{
  justify-self:end;
  min-height:48px;
  padding:9px 18px;
  display:flex;
  align-items:center;
  gap:8px;
  border:2px solid var(--azure);
  border-radius:var(--control-radius);
  background:var(--blue);
  box-shadow:var(--azure-glow);
  font-size:16px;
  font-weight:900;
}
.script-help-close svg{width:20px;height:20px;fill:currentColor}
/* ====================================================================
   7b · DEVICE SETTINGS — topbar gear + modal (walk item 19 RULING)
   A DEVIATION FROM THE FROZEN MOCKUP, documented at the site as the
   rule requires (docs/CODEBASE.html#style-accessibility-over-fidelity):
   the mockup has no settings modal and no topbar gear. Tor ruled them
   in (2026-07-29) because both earlier picker homes failed — the rack
   could not afford their 108px (the marks editor needs it;
   partials/rack.php) and the stage home was invisible unless the
   Camera panel was focused. The topbar is the one region every
   responsive mode keeps. The gear reuses
   stage.css's .window-action quiet icon-button idiom (§6) sized to
   the topbar's 48px rhythm; the modal reuses the §7 script-help
   teaching-dialog pattern above (same backdrop, same card;
   focus/inert lifecycle in ui/settings-modal.js). z-index 230: above
   the sheets and the script-help dialog, below the site-wide device
   banner (240) and the blocking gate.
   ==================================================================== */
/* Item 60 S2: the merged hub is the ONE control for everything about the
   person at this machine (camera, microphone, output, time zone, sign out),
   so it stops being an unlabeled glyph. S1 merged the ACCESSIBLE name to
   "Settings" and deferred the sighted label here; a 65+/low-vision producer
   should not have to know what a cog means to find their sign-out. The width
   goes auto so the label has somewhere to be — 48px was the icon-only box —
   and the 48px floor stays as a min. Under real width pressure the label
   sheds and the control returns to the square glyph (responsive.css §19d),
   which is why the name lives in aria-label and never in this span.
   Selector note: .window-action (stage.css) pins width:44px, display:grid and
   padding:8px, and stage.css loads AFTER shell.css — so a bare
   .topbar-settings loses on source order and the label lands stacked UNDER
   the glyph, 8px wider than its own button (measured: label 1372-1432 across
   a 1380-1424 button). The .topbar ancestor is what wins it, and every later
   .topbar-settings rule in responsive.css carries the same ancestor so the
   shedding rules can still override this one on source order. */
.topbar .topbar-settings{width:auto;min-width:48px;min-height:48px;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 12px}
.topbar .topbar-settings .topbar-settings-label{font-size:15px;font-weight:900;line-height:1.1;white-space:nowrap}
.settings-modal{
  position:fixed;
  z-index:230;
  inset:0;
  display:grid;
  place-items:center;
  padding:16px;
  background:rgba(5,8,15,.72);
  backdrop-filter:blur(6px);
}
.settings-modal[hidden]{display:none}
.settings-modal-card{
  width:min(460px,100%);
  max-height:calc(100dvh - 32px);
  padding:20px;
  display:grid;
  gap:14px;
  overflow-y:auto;
  border:2px solid var(--cyan);
  border-radius:var(--card-radius);
  background:var(--surface);
  box-shadow:var(--shadow),var(--glow);
}
.settings-modal-card h2{margin:0;font-size:23px}
/* FULL mode hides .setup-control>span (there the sheet is inline under the
   setup rail's step button, and that row already names it; responsive.css
   §19) — a bare dropdown in a dialog needs its label, so it is re-shown
   here at the 15px floor, the 17px/48px control floor with it (the
   panels.css S3b precedent). */
.settings-modal-card .setup-control{gap:4px}
.settings-modal-card .setup-control>span{display:block;font-size:15px}
.settings-modal-card .setup-select{font-size:19px;min-height:48px}
/* The producer variant's scope sentence (item 37 follow-up): what the
   time-zone choice governs, in plain words at the 15px+ floor. */
.settings-modal-note{margin:0;font-size:16px;line-height:1.4;color:var(--text)}
/* The during-take lock sentence: red = attention (ON AIR family), and the
   alert icon plus the sentence's own words carry the status — never colour
   alone (docs/CODEBASE.html#style-glyph-needs-word). */
.settings-modal-lock{
  margin:0;
  padding:10px 12px;
  display:flex;
  align-items:flex-start;
  gap:8px;
  border:1px solid var(--red);
  border-left:4px solid var(--red);
  border-radius:var(--control-radius);
  background:rgba(255,59,107,.14);
  color:var(--text);
  font-size:16px;
  line-height:1.4;
}
.settings-modal-lock svg{width:20px;height:20px;flex:0 0 auto;fill:var(--red)}
.settings-modal-close{
  justify-self:end;
  min-height:48px;
  padding:9px 18px;
  display:flex;
  align-items:center;
  gap:8px;
  border:2px solid var(--azure);
  border-radius:var(--control-radius);
  background:var(--blue);
  box-shadow:var(--azure-glow);
  font-size:16px;
  font-weight:900;
}
.settings-modal-close svg{width:20px;height:20px;fill:currentColor}
/* ====================================================================
   15 · TRANSPORT / ACTION BAR
   Exactly the recording actions, centered under the stage column in
   full mode (the rack runs full height beside it). The bar itself is
   chromeless — no border around the button group. Instructor verbs:
   Previous · Record · Mark · Next (Tor 2026-08-14: one line, never
   wrapped — the labels were shortened from "Previous slide"/"Drop
   marker" because the long forms broke the bar to two rows at
   1440x900; see the full-mode .action-bar comment in responsive.css);
   Record is the only red control on screen and pulses while
   .is-recording. Producer verbs: Approve take · Request retake · Mark.
   Hotkeys render as quiet kbd.hotkey-hint key-caps (desktop + fine
   pointer only — see section 19). Compact modes reduce to
   Record + Mark at 52px+, thumb-reachable.
   ==================================================================== */
.transport{
  min-width:0;
  min-height:0;
  padding:3px 8px;
  display:grid;
  align-content:center;
  overflow:hidden; /* item-32 audit: layout clip — the bar's row is minmax(var(--transport-h),auto), so grown labels enlarge the row instead of reaching this edge */
  background:linear-gradient(180deg,rgba(5,8,15,.36),var(--void) 42%);
}
.action-bar{
  width:100%;
  min-width:0;
  min-height:62px;
  margin:0 auto;
  padding:0;
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:6px;
  border:0;
  border-radius:0;
  background:transparent;
  box-shadow:none;
}
.action-bar button{
  min-width:0;
  min-height:52px;
  padding:7px 10px;
  display:flex;
  align-items:center;
  justify-content:center;
  gap:7px;
  border-radius:var(--control-radius);
  font-size:16px;
  font-weight:900;
  line-height:1.1;
  white-space:normal;
  overflow-wrap:normal;
  word-break:normal;
}
.action-bar svg{width:21px;height:21px;fill:currentColor;flex:0 0 auto}
.hotkey-hint{display:none;flex:0 0 auto;min-width:26px;min-height:26px;margin-left:3px;padding:0 7px;align-items:center;justify-content:center;border:1px solid rgba(231,238,247,.34);border-bottom-width:2px;border-radius:var(--control-radius);background:rgba(5,8,15,.42);color:inherit;opacity:.85;font:900 15px/1 ui-monospace,SFMono-Regular,Menlo,monospace}
.action-bar [data-action="previous"],.action-bar [data-action="next"]{display:none}
/* Item 21: the close guard's visible "still uploading — N left" line, painted
   by ui/transport.js from the recorder's pending-chunk count and the pending
   deck/script upload calls; hidden whenever closing would lose nothing.
   65+ floor: plain words at 19px, never just a spinner. */
.upload-status{
  margin:4px 0 0;
  padding:2px 6px;
  text-align:center;
  font-size:19px;
  line-height:1.4;
  color:var(--text);
  overflow-wrap:anywhere;
}
.upload-status[hidden]{display:none}
/* No concurrent recording (2026-08-12): the sentence beside the disabled
   Record — same shape and floor as .upload-status above, in the same
   transport bar as the control it explains. ui/transport.js toggles hidden
   from store remoteRecording; the words themselves are markup
   (partials/transport.php), one home. */
.record-blocked{
  margin:4px 0 0;
  padding:2px 6px;
  text-align:center;
  font-size:19px;
  line-height:1.4;
  color:var(--text);
  overflow-wrap:anywhere;
}
.record-blocked[hidden]{display:none}
.record-action{
  border:2px solid var(--red)!important;
  background:var(--red)!important;
  color:var(--text);
  box-shadow:0 0 20px -7px var(--red);
}
.record-action.is-recording{
  border-color:var(--red)!important;
  background:color-mix(in srgb,var(--red) 18%,var(--void))!important;
  animation:recordButton 1.2s ease-in-out infinite;
}
/* No concurrent recording (2026-08-12): the disabled state keeps the red
   identity but drops the glow — base.css's button:disabled dims it (.62) and
   sets not-allowed; the WORDS live in .record-blocked below, never in the
   dimming alone. */
.record-action:disabled{box-shadow:none;animation:none}
@keyframes recordButton{50%{box-shadow:0 0 30px -3px var(--red)}}
.marker-action{border:2px solid var(--cyan)!important;background:var(--blue)!important}
/* The 200% text-zoom gate (docs/CODEBASE.html#style-zoom-and-reflow) against
   mockup fidelity — and accessibility wins
   (docs/CODEBASE.html#style-accessibility-over-fidelity). A pre-existing
   defect that failed 4 of 22 producer QA cells and hid through two migration
   slices, because every earlier producer matrix had audited the
   unauthenticated sign-in card, which has no transport and audits trivially
   green. The mockup's repeat(3,minmax(0,1fr)) is a count, and a count cannot
   know how wide "Approve" is: at 200% text zoom the three verbs each need
   ~115-120px of a ~97-110px column at producer@390x844, 410x590 and
   422x195, so the words render through the neighbouring button's label.
   The fourth failing cell is producer@640x310, fixed in responsive.css's
   short-landscape band. Same answer the instructor bar already gives
   (labels wrap, the bar grows, stage.css's minmax(var(--transport-h),auto)
   row absorbs it): the em floor holds the mockup's exact 3-up bar at the
   design's text size — 4.6em is 87px at the inherited 19px (was 78px at the
   old 17px default), still under a third
   of every compact bar in the tested viewport matrix
   (docs/CODEBASE.html#arch-viewport-matrix) — and steps the bar down to
   2-up/stacked only once the text has grown past what a third of the bar
   can carry, measured in the text it is carrying (em, which text zoom
   scales; a px count is blind to it). min(...,100%) guards containers
   narrower than one floor. */
.producer-action-bar{grid-template-columns:repeat(auto-fit,minmax(min(4.6em,100%),1fr))}
.producer-action-bar .retake-action{border-color:var(--red);background:rgba(255,59,107,.14)}
/* ====================================================================
   16 · (retired) POP-OUT WINDOW
   The floating clone window's styles lived here until item 52 retired
   the feature and the item-54 slice removed the inert remnants. The
   section number is kept so the numbered map of this file stays
   stable. The teleprompter's open-in-a-new-tab (54a) is a real browser
   tab styled by prompter.css, not a layer of this shell.
   ==================================================================== */
/* ====================================================================
   17 · TOASTS + QA SINK
   One shared toast (role="status") bottom-left of the workspace: cyan
   hairline, auto-dismiss, reduced-motion safe — the standard
   confirmation ("Saved", "Marker dropped…"). The qa-output element is
   the invisible sink for the built-in ?qa=1 layout audit; .qa-mode
   freezes all animation so headless captures are deterministic.
   ==================================================================== */
.toast{
  position:fixed;
  z-index:200;
  left:calc(var(--rail-w) + 12px);
  bottom:calc(var(--transport-h) + 12px);
  max-width:min(460px,calc(100vw - var(--rail-w) - 24px));
  min-height:48px;
  padding:10px 14px;
  display:flex;
  align-items:center;
  border:2px solid var(--cyan);
  border-radius:var(--card-radius);
  background:var(--surface);
  box-shadow:var(--shadow),var(--glow);
  font-size:16px;
  font-weight:900;
  overflow-wrap:normal;
  word-break:normal;
}
[data-qa-output]{
  position:fixed;
  z-index:-1;
  left:0;
  top:0;
  width:1px;
  height:1px;
  overflow:hidden;
  opacity:0;
  font-size:15px;
}
.qa-mode .first-run-callout{display:none!important}
.qa-mode *, .qa-mode *::before, .qa-mode *::after{animation:none!important;transition:none!important}
/* ====================================================================
   22 · SUPPORT GATE — the browser capability gate: degrade per missing
   capability, never hard-block (docs/CODEBASE.html#arch-browser-support-gate)
   Additive S1 section, not a frozen-mockup region; the number continues
   the global sequence (18-21 live in responsive.css). Three surfaces
   from ui/support.js: the blocking .support-gate dialog, the
   dismissible .support-notice, and the on-control .support-reason note
   beside the `disabled` + aria-disabled control it explains.

   CRITICAL: the blocking overlay renders precisely when container
   queries are missing, so this section uses NO container queries and
   NO dvh/svh units — plain vh/% only. Nothing is lost or trapped at
   200% zoom (docs/CODEBASE.html#style-zoom-and-reflow): the overlay
   scrolls its card at small heights / 200% zoom. Each gap is named in
   words beside an icon shape, never colour alone
   (docs/CODEBASE.html#style-glyph-needs-word).
   ==================================================================== */
.support-gate{
  position:fixed;
  z-index:300; /* above every layer: toast 200, help modal 220, device banner 240 */
  inset:0;
  display:grid;
  padding:16px;
  overflow-y:auto;
  background:rgba(5,8,15,.86);
}
/* margin:auto, not place-items:center on the parent: auto margins center
   the card while it fits and collapse when it overflows, so the top of a
   tall card stays scroll-reachable at 200% zoom instead of being clipped
   past the scrollport's start edge. */
.support-gate-card{
  width:min(92vw,560px);
  margin:auto;
  padding:24px;
  display:grid;
  gap:16px;
  border:2px solid var(--red);
  border-radius:var(--major-radius);
  background:var(--surface);
  box-shadow:var(--shadow),0 0 34px -11px var(--red);
  color:var(--text);
  font-size:19px;
  line-height:1.45;
}
.support-gate-card h2{margin:0;font-size:24px;line-height:1.15}
.support-gate-card p{margin:0;font-size:19px}
.support-gap{
  display:flex;
  align-items:flex-start;
  gap:12px;
  padding:12px 14px;
  border:1px solid var(--line);
  border-left:4px solid var(--red);
  border-radius:var(--card-radius);
  background:var(--raised);
}
.support-gap-icon{width:24px;height:24px;flex:0 0 auto;margin-top:2px;fill:var(--red)}
/* Long-token insurance (docs/CODEBASE.html#style-long-token-policy): this
   copy is server-independent today, but anywhere costs nothing and keeps
   a future dynamic value from forcing a sideways scroll. Prose still
   breaks only at spaces. */
.support-gap-copy{
  min-width:0;
  display:grid;
  gap:4px;
  overflow-wrap:anywhere;
  word-break:normal;
}
.support-gap-copy strong{font-size:19px}
.support-gap-copy p{color:var(--dim)}
.support-notice{
  position:fixed;
  z-index:240; /* above topbar + sheets, below only the blocking gate */
  top:12px;
  left:50%;
  transform:translateX(-50%);
  width:min(560px,calc(100vw - 24px));
  max-height:calc(100vh - var(--transport-h) - 24px); /* never covers the transport */
  display:flex;
  align-items:flex-start;
  gap:12px;
  padding:14px 16px;
  overflow-y:auto;
  border:2px solid var(--azure);
  border-radius:var(--card-radius);
  background:var(--surface);
  box-shadow:var(--shadow),var(--azure-glow);
  color:var(--text);
  font-size:19px;
  line-height:1.45;
}
.support-notice-icon{width:24px;height:24px;flex:0 0 auto;margin-top:2px;fill:var(--azure)}
.support-notice-copy{
  min-width:0;
  display:grid;
  gap:8px;
  overflow-wrap:anywhere;
  word-break:normal;
}
.support-notice-copy h2{margin:0;font-size:19px;line-height:1.2}
.support-notice-copy p{margin:0;color:var(--dim);font-size:19px}
.support-notice-dismiss{
  flex:0 0 auto;
  min-width:88px;
  min-height:44px;
  padding:8px 14px;
  display:flex;
  align-items:center;
  justify-content:center;
  border:1px solid var(--steel);
  border-radius:var(--control-radius);
  background:var(--raised);
  color:var(--text);
  font-size:19px;
  font-weight:900;
}
.support-notice-dismiss:hover{border-color:var(--cyan);box-shadow:var(--glow)}
.support-reason{
  margin:8px 0 0;
  display:inline-flex;
  align-items:flex-start;
  gap:8px;
  padding:8px 10px;
  border:1px solid var(--line);
  border-left:4px solid var(--azure);
  border-radius:var(--control-radius);
  background:var(--raised);
  color:var(--dim);
  font-size:19px;
  line-height:1.4;
  overflow-wrap:anywhere;
  word-break:normal;
}
/* The reason renders inside feature panels, whose own `.x-panel p` rules
   (panels.css, a later stylesheet) outweigh a single class — this heavier
   selector keeps the 19px page default (Ruling 7) from being overridden
   there. */
.feature-panel p.support-reason{font-size:19px}
.support-reason-icon{width:20px;height:20px;flex:0 0 auto;margin-top:1px;fill:var(--azure)}
.support-page-link{
  justify-self:start;
  min-height:44px;
  display:inline-flex;
  align-items:center;
  color:var(--cyan);
  font-size:19px;
  font-weight:800;
  text-decoration:underline;
  text-underline-offset:3px;
}
/* S1 ruling (Jeeves blocker, Carson observation): a standalone navigation
   link is a control, so the 44px target floor binds. This covers the two
   links S1 added AND the pre-existing producer-dashboard link, which shipped
   through S0 at 19px under the prose-link reading now rejected. */
.booking-producer-link a{
  display:inline-flex;
  align-items:center;
  min-height:44px;
}
/* The requirements.php live checklist (roles/requirements.js fills the
   status spans). Verdicts read as icon + words; the classes carry
   spacing only, per the page's own contract. */
.support-check-list{
  margin:0;
  padding:0;
  list-style:none;
  display:grid;
  gap:10px;
}
.support-check{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:12px;
  flex-wrap:wrap;
  padding:10px 12px;
  border:1px solid var(--line);
  border-radius:var(--control-radius);
  background:var(--raised);
  font-size:19px;
  line-height:1.4;
}
.support-check [data-support-check-status]{
  display:inline-flex;
  align-items:center;
  gap:8px;
  font-weight:800;
  color:var(--dim);
}
.support-check [data-support-check-status] svg{
  width:20px;
  height:20px;
  flex:0 0 auto;
  fill:currentColor;
}

/* ==================================================================
   DEVICE-PERMISSION BANNER — the site-wide camera/mic error
   Fixed above the toast (200) and help modal (220): a refused device is not a
   per-panel detail. 17px text and a 48px-tall retry button per the
   accessibility floor; wraps rather than truncating on phones.
   ================================================================== */
.device-banner{
  position:fixed;top:0;left:0;right:0;z-index:240;
  display:flex;gap:16px;align-items:center;justify-content:center;flex-wrap:wrap;
  padding:14px 20px;background:#2b0d16;border-bottom:2px solid var(--red);
  color:var(--text);font-size:19px;overflow-wrap:anywhere;
}
.device-banner-retry{
  min-height:48px;padding:0 22px;border-radius:10px;border:1px solid var(--red);
  background:var(--red);color:#fff;font-size:19px;font-weight:600;cursor:pointer;
}
.device-banner-retry:hover,.device-banner-retry:focus-visible{filter:brightness(1.15)}

/* ====================================================================
   THE UPLOAD WAIT CARD — ruling 124(d)
   ====================================================================
   "I need that warning to be a modal in the middle of the screen with a
   upload progress displayed." Same backdrop and card idiom as the §7
   teaching dialog and the settings hub, so a person meets one modal
   convention rather than three. z-index 250 puts it above the settings
   hub (230) and the site-wide device banner (240) — it can appear while
   either is up and it is always the innermost thing.

   THAT IT COVERS THE TOAST (200) IS WHY THE CARD CARRIES ITS OWN ALERT
   NODE (item 65's standing rule): a refusal rendered behind this card
   would be invisible exactly when a person is watching this card. The
   sentence therefore renders IN the card, and its legibility is proven
   by elementFromPoint at its own centre, never by presence in the DOM.

   NOTHING HERE ANIMATES ON ITS OWN. The fill's width is written by
   ui/uploadmodal.js from measured bytes; there is no keyframe, no
   indeterminate shimmer, and no transition long enough to outrun the
   number in the sentence beneath it. A bar that moves without bytes
   behind it is the defect this ruling exists to prevent.
   ==================================================================== */
/* CENTRED, NOT BLOCKING (ruling 135, reversing 134(a)). The full-bleed layer
   exists only to CENTRE the card; it is not a barrier. It takes no pointer
   events and paints nothing, so every control behind it stays clickable — the
   card opens during the post-Stop drain on the instructor's own page, and an
   instructor who wants to correct a mark must not be locked out of the marks
   editor for up to two minutes. Measured cost of the barrier before it was
   removed: tests/room-flow.mjs 84/0 -> 58/1. Only the CARD itself takes
   clicks. */
.upload-modal{
  position:fixed;
  z-index:250;
  inset:0;
  display:grid;
  place-items:center;
  padding:16px;
  pointer-events:none;
}
.upload-modal[hidden]{display:none}
/* CLICK-THROUGH (ruling 136). The card takes no pointer events either: a click
   anywhere on it lands on whatever live control is underneath, so the centred
   card can never block work. Only the close button takes a click, because a
   control a person cannot press is not a control. NOTE for anyone measuring
   this later: pointer-events:none removes the card from hit testing, so
   document.elementFromPoint over the card returns what is BEHIND it. That is
   the intended behaviour, not a legibility failure — a test that wants to
   check paint order has to re-enable hit testing for the one measurement and
   put it back afterwards. */
.upload-modal-card{
  pointer-events:none;
  width:min(520px,100%);
  max-height:calc(100vh - 32px);
  overflow-y:auto;
  display:grid;
  gap:14px;
  padding:24px;
  border:2px solid var(--steel);
  border-radius:var(--card-radius);
  background:var(--raised);
  box-shadow:0 28px 70px -20px rgba(0,0,0,.75);
}
.upload-modal-card h2{margin:0;font-size:22px;font-weight:900;line-height:1.2;color:var(--text)}
/* The 19px page default, above the 15px floor: this is the sentence the whole card exists
   to deliver and the person reading it is 65+ and waiting. */
.upload-modal-line{margin:0;font-size:19px;line-height:1.5;color:var(--text)}
.upload-modal-stall{margin:0;font-size:16px;line-height:1.45;color:var(--amber,#ffb648)}
.upload-modal-alert{margin:0;font-size:16px;line-height:1.45;font-weight:700;color:var(--text)}
.upload-modal-alert[hidden],.upload-modal-stall[hidden]{display:none}
.upload-modal-truth{margin:0;font-size:15px;line-height:1.45;color:var(--dim)}
.upload-progress{
  height:16px;
  border:2px solid var(--steel);
  border-radius:8px;
  background:var(--surface);
  overflow:hidden;
}
.upload-progress[hidden]{display:none}
.upload-progress-fill{display:block;height:100%;width:0;background:var(--cyan)}
.upload-modal-close{
  pointer-events:auto; /* the one part of a click-through card that takes a click */
  justify-self:start;
  min-height:44px;
  padding:0 18px;
  border:2px solid var(--steel);
  border-radius:var(--control-radius);
  background:var(--surface);
  color:var(--text);
  font-size:16px;
  font-weight:900;
  cursor:pointer;
}
