/* ttweb shell styles. ALL colors/type/space come from tokens.css (W0) —
   never hardcode a palette value here; extend the token sheet instead. */
@import url("/tokens.css");
@import url("/controls/controls.css");
@import url("/pulse/features.css");

/* Local values this sheet needs and tokens.css cannot carry. tokens.css is
   copied BYTE-FOR-BYTE to the marketing site (tools/ttsite/lint.py,
   tests/test_site.py: "one sheet, copied, never forked") and a brochure page
   has no tap targets and no fields — so these live here.

   THE SEAM ITSELF IS NOT HERE: a media query cannot read a custom property, so
   720px is written as a literal in the two narrow blocks at the foot of this
   sheet, and their comments are its name. */
:root {
  /* THE TOUCH FLOOR, and it is 48 rather than the 44 a thumb strictly needs:
     this repo had already decided 48 twice on a phone before this sheet had any
     phone rules at all — tests/test_web_controls_page.py at 430px and
     tests/test_web_paddle_learning.py at 430px both fail a visible control
     under 48. One number for a thumb and a laser is also one number to reason
     about. Naming it is not decoration: the gate in
     tools/ttweb/test_panel_mode.py captures `min-height: (\d+)px` after the
     panel-mode class with a brace- and comment-blind regex, and `var(--tap)`
     cannot be captured by it — a literal gives that immunity up. */
  --tap: 48px;
  /* The size of a field you TYPE in. 15px on a wide screen; the narrow block
     lifts it to 17px, because below 16px iOS Safari zooms the whole page when
     an input takes focus — and ADR-0056 makes the phone the surface that
     types, so every typed field in this shell is a phone field. */
  --field-fs: var(--fs-2);
}

* { box-sizing: border-box; }
body {
  margin: 0; background: var(--bg); color: var(--fg);
  font: var(--fs-2)/1.45 var(--font-ui);
  display: flex; flex-direction: column; min-height: 100vh;
}
/* WRAPS AT EVERY WIDTH — nothing wraps when there is room. This is the ENTIRE
   cause of the sideways scroll in the owner's phone screenshot: measured at a
   390px viewport, header.scrollWidth is 522 while .layout, nav, main and footer
   are all exactly 390, so the phone zooms the whole page out to 0.75 to make the
   header fit. #build is `white-space: nowrap` (see its rule below) and cannot
   shrink; without wrapping it pushes #persp half off the edge and #conn entirely
   off. The panel section below already knew this and said so — it just wrote the
   rule behind a class no phone ever gets. */
header {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; row-gap: var(--sp-1);
  padding: 14px 22px; border-bottom: 1px solid var(--line);
}
h1 { font-size: 19px; margin: 0; font-weight: 650; }
.sub { color: var(--sub); font-size: var(--fs-0); font-weight: 400; }
.pill {
  font-size: var(--fs-0); padding: 3px 10px; border-radius: 999px;
  border: 1px solid var(--line); color: var(--sub);
}
.pill.ok { color: var(--ok); border-color: var(--ok); }
.pill.err { color: var(--err); border-color: var(--err); }
/* Perspective switcher (ADR-0064): rearranges the nav for an audience. */
#persp {
  margin-right: 10px; background: none; color: var(--fg); font: inherit;
  font-size: var(--fs-0); border: 1px solid var(--line);
  border-radius: var(--radius-s); padding: 4px 8px; cursor: pointer;
}
.layout { display: flex; flex: 1; min-height: 0; }
nav {
  width: 210px; border-right: 1px solid var(--line); padding: 14px 10px;
  display: flex; flex-direction: column; gap: 4px;
}
nav button {
  text-align: left; background: none; border: 0; color: var(--fg);
  padding: 9px 12px; border-radius: var(--radius-s); cursor: pointer; font: inherit;
  transition: background var(--t-fast) var(--ease);
}
nav button:hover { background: var(--panel); }
nav button.active { background: var(--accent); color: var(--accent-ink); font-weight: 600; }
nav button .d { display: block; font-size: 11px; color: var(--sub); margin-top: 2px; }
nav button.active .d { color: var(--accent-ink); opacity: .75; }
main { flex: 1; padding: 22px 26px; overflow: auto; }
main h2 { margin: 0 0 4px; font-size: var(--fs-3); }
main .desc { color: var(--sub); margin: 0 0 18px; }
.card {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius-m); padding: 16px 18px; margin-bottom: 14px;
}
.card h3 { margin: 0 0 8px; font-size: 14px; color: var(--accent); }
table { border-collapse: collapse; width: 100%; font-size: var(--fs-1); }
/* One unbroken token in one cell sets the table's min-content width; the Raw
   settings doc measured 1472px at EVERY viewport, which is why `main` scrolls
   sideways on the desktop console too. `anywhere`, not `break-word`: only
   `anywhere` lowers a cell's MIN-CONTENT width, which is the measure a table
   sizes its columns from. */
td, th { border-bottom: 1px solid var(--line); padding: 5px 8px; vertical-align: top;
         overflow-wrap: anywhere; }
th { text-align: left; color: var(--sub); font-weight: 500; }
td:first-child { color: var(--sub); }
pre {
  background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius-s);
  padding: 12px; overflow: auto; font-size: var(--fs-0); margin: 0; color: var(--fg);
  font-family: var(--font-mono);
}
.loading { color: var(--sub); }
.gated { color: var(--warn); font-size: var(--fs-0); }
/* An ACTION, whatever element carries it. This was `button.act`, which left
   <a class="act" href="/qr"> — the phone's only route to signing a headset in
   (app.js renderProfileCards, ADR-0132 Amendment 1 A5) — dressed by nothing but
   `a { color: var(--accent) }`. Measured at 390px: display inline, no
   background, no padding, weight 400, underlined, 81x64 across three line
   boxes. That is the "Sign a headset in with a code" line the owner could not
   find in their screenshot: an action that does not look like one. Three more
   anchors carry the class and none of them is panel-gated.
   inline-flex so an <a> can carry a min-height and centre a wrapped label the
   way a <button> does; `gap` because the applied tick below becomes a flex item
   and its leading space would collapse. */
.act {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--sp-1); text-align: center; text-decoration: none;
  background: var(--accent); color: var(--accent-ink); border: 0;
  border-radius: var(--radius-s); padding: 7px 14px; font: inherit;
  font-weight: 600; cursor: pointer;
  transition: filter var(--t-fast) var(--ease);
}
.act:hover { filter: brightness(1.1); }
/* :disabled is form-control-only, so an <a class="act"> can never match it. If
   one ever needs the state it wants aria-disabled and a rule for that. */
.act:disabled { background: var(--line); color: var(--sub); cursor: not-allowed; filter: none; }
/* [hidden] MUST WIN. The UA's `[hidden] { display: none }` loses to any author display
   rule — `.act { display: inline-flex }` above beat it, and the phone's "Clear PIN"
   (app.js renderPlayersCard, `clear.hidden = !set`) stayed on screen with no PIN to
   clear (measured 2026-09-08: computed display `flex`). The shell sets the attribute
   on exactly two `.act` buttons — that one and table_grip.js's "Discard unfinished
   match" — and on a few plain divs the UA rule already hid; paddles/learning.css
   carries its own per-class [hidden] rules. One global rule, so `el.hidden = true`
   means hidden whatever the element's display is. Never `style.display` instead. */
[hidden] { display: none !important; }
/* The saved paddle editor is the same surface on a headset and a phone. */
.paddle-actions, .paddle-row-heading, .paddle-sheet-summary {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
}
.paddle-actions { margin: var(--sp-2) 0; }
.paddle-row { border-top: 1px solid var(--line); padding: var(--sp-3) 0; }
.paddle-name { overflow-wrap: anywhere; }
.paddle-sheet-summary { color: var(--sub); font-size: var(--fs-0); margin-top: var(--sp-2); }
.paddle-dot { display: inline-block; width: 16px; height: 16px; border-radius: 50%;
  border: 1px solid var(--sub); margin-right: var(--sp-1); vertical-align: middle; }
.paddle-editor { background: var(--inset); border: 1px solid var(--line);
  border-radius: var(--radius-s); padding: var(--sp-3); margin-top: var(--sp-3); }
.paddle-editor h4 { margin: 0 0 var(--sp-3); overflow-wrap: anywhere; }
.paddle-field { display: flex; flex-direction: column; gap: var(--sp-1); margin: var(--sp-2) 0; }
.paddle-field input[type="text"], .paddle-field select {
  color: var(--fg); background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius-s); padding: var(--sp-2); font: inherit;
  font-size: var(--field-fs); min-height: 44px; max-width: 100%;
}
.paddle-swatch { border: 2px solid var(--line); background: var(--panel); color: var(--fg);
  border-radius: var(--radius-s); padding: var(--sp-2); font: inherit; cursor: pointer; }
.paddle-swatch::before { content: ""; display: inline-block; background: var(--paddle-color);
  width: 20px; height: 20px; border: 1px solid var(--sub); border-radius: 50%;
  margin-right: var(--sp-2); vertical-align: middle; }
.paddle-swatch[aria-pressed="true"] { border-color: var(--accent); }
.paddle-custom-color { display: flex; align-items: center; gap: var(--sp-2); }
.paddle-custom-color input { min-height: 44px; width: 64px; }
.paddle-preview { display: flex; justify-content: center; flex-wrap: wrap; gap: var(--sp-4); }
.paddle-face-preview { margin: var(--sp-2); width: 145px; text-align: center; }
.paddle-preview-shape { width: 100%; text-align: center; }
.paddle-pending { color: var(--warn); }
.paddle-note { margin-top: var(--sp-2); }
.panel-vr .paddle-actions button, .panel-vr .paddle-swatch { min-height: 48px; }
footer { padding: 12px 22px; border-top: 1px solid var(--line); }
.rally-editor { max-width: 1120px; }
.rally-layout { display: grid; grid-template-columns: minmax(190px, 1fr) minmax(300px, 3fr);
  gap: var(--sp-4); margin-top: var(--sp-3); }
.rally-list { border-right: 1px solid var(--line); padding-right: var(--sp-3); }
.rally-list h4, .rally-sheet h4 { margin: var(--sp-2) 0; }
.rally-pick, .rally-phase { display: block; text-align: left; width: 100%; min-height: 44px;
  padding: var(--sp-2) var(--sp-3); margin-bottom: var(--sp-2); border: 1px solid var(--line);
  background: var(--inset); color: var(--fg); border-radius: var(--radius-s); cursor: pointer;
  white-space: normal; font: inherit; }
.rally-pick.selected, .rally-phase.selected { border-color: var(--accent);
  box-shadow: inset 3px 0 var(--accent); }
.rally-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3); }
.rally-field { display: flex; flex-direction: column; gap: var(--sp-1); margin: var(--sp-2) 0;
  color: var(--sub); font-size: var(--fs-1); }
.rally-field input, .rally-field select { min-width: 0; min-height: 44px; padding: var(--sp-2);
  border: 1px solid var(--line); border-radius: var(--radius-s); background: var(--inset);
  color: var(--fg); font: inherit; font-size: var(--field-fs); }
.rally-field input[type="checkbox"] { width: 24px; align-self: flex-start; accent-color: var(--accent); }
.rally-actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: var(--sp-3) 0; }
.rally-live { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3); border: 1px solid var(--line); border-radius: var(--radius-s); }
.rally-live .act { margin-left: auto; }
.rally-notice { min-height: 1.6em; margin: var(--sp-2) 0; }
.rally-pending { border: 1px solid var(--warn); padding: var(--sp-3); }
.rally-pending .act { margin-right: var(--sp-2); }
.rally-sentence { font-size: var(--fs-2); line-height: 1.5; }
.rally-target { display: block; width: min(100%, 300px); height: auto; }
.rally-shots summary { cursor: pointer; padding: var(--sp-3) 0; }
.panel-vr .rally-editor button, .panel-vr .rally-field select, .panel-vr .rally-field input { min-height: 48px; }
@media (max-width: 680px) {
  .rally-layout { grid-template-columns: 1fr; }
  .rally-list { border-right: 0; border-bottom: 1px solid var(--line); padding-bottom: var(--sp-3); }
  .rally-fields { grid-template-columns: 1fr; }
}
/* WHICH build — see app.js showBuild. In the header rather than here because
   .panel-vr hides the footer entirely. */
/* #who already carries the margin-left:auto that pushes this group right. */
#build { margin-right: 14px; font-size: var(--fs-1); white-space: nowrap; }
a { color: var(--accent); }
iframe.embed { width: 100%; height: 70vh; border: 1px solid var(--line); border-radius: var(--radius-m); }
.navsection { color: var(--sub); font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
  padding: 12px 12px 4px; }
#who { margin-left: auto; margin-right: 14px; font-size: var(--fs-1); }
#who a { color: var(--accent); }

/* ---- W0 typed-data components (design_language.md §4) -------------------
   READ results render as REAL data, not <pre> JSON: stat tiles for scalar
   maps, tables for lists of records, hero numbers for scalars. The raw JSON
   stays one tap away (truth-first, P1) inside <details class="raw">.      */
.tiles { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.tile {
  background: var(--inset); border: 1px solid var(--line);
  border-radius: var(--radius-s); padding: var(--sp-2) var(--sp-3);
  min-width: 96px;
}
.tile .k { color: var(--sub); font-size: var(--fs-0); }
.tile .v {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: var(--fs-4); line-height: 1.25;
}
.tile .v.small { font-size: var(--fs-2); }
.num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
details.raw { margin-top: var(--sp-2); }
details.raw summary { color: var(--sub); font-size: var(--fs-0); cursor: pointer; }
/* Prints raw capability ids and profile ids — single unbreakable tokens of
   110-175px, which ran off the edge of the owner's phone card. */
.provenance { color: var(--sub); font-size: var(--fs-0); margin-top: var(--sp-2);
              overflow-wrap: anywhere; }

/* Trust grammar badges (P2): the capability kind is ALWAYS visible. The
   confirm-in-VR treatment is deliberately distinct from anything a page
   could fake with an accent button: outlined warn + lock glyph, and the
   control itself stays disabled on the web. */
/* THE "reversible-lo" TRUNCATION in the owner's screenshot. A chip GROUP wraps;
   the chip itself keeps `white-space: nowrap` below, which is right — the word
   is 109px and fits any honest column. Stays INLINE-flex: it lives inside an
   <h3>, and blockifying it would drop the trust chips to their own line in all
   fourteen cards. */
.badges { display: inline-flex; flex-wrap: wrap; gap: var(--sp-1); row-gap: var(--sp-1);
          vertical-align: middle; margin-left: var(--sp-2); }
.badge {
  font-size: var(--fs-0); padding: 1px 8px; border-radius: 999px;
  border: 1px solid var(--line); color: var(--sub); white-space: nowrap;
}
.badge.read { border-color: var(--line); }
.badge.auto { color: var(--accent); border-color: var(--accent); }
.badge.stepup { color: var(--warn); border-color: var(--warn); }
.badge.invr { color: var(--warn); border-color: var(--warn); background: transparent; }
.badge.invr::before { content: "⛨ "; }
.badge.blast { color: var(--sub); }
.badge.signed { color: var(--ok); border-color: var(--ok); }

/* Honesty states (P1): the four canonical looks, reused everywhere. */
.state-loading { color: var(--sub); }
.state-loading::before { content: "… "; }
.state-error { color: var(--err); }
.state-disabled { color: var(--sub); }
.state-disabled .why { color: var(--warn); }

/* Progress verdicts (the flagship honesty component): counts ALWAYS, and
   insufficient-data is a proud refusal, not an apology. */
/* A wrapped verdict is never wrong, and `.verdict .shot` below is a 130px floor
   sitting next to a counts column and a tag. */
.verdict { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-3);
  padding: var(--sp-2) 0; border-bottom: 1px solid var(--line); }
.verdict:last-child { border-bottom: 0; }
.verdict .shot { min-width: 130px; }
.verdict .counts { font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  color: var(--fg); }
.verdict .counts .arrow { color: var(--sub); }
.verdict .tag { font-size: var(--fs-0); padding: 1px 8px; border-radius: 999px;
  border: 1px solid; }
.verdict .tag.improved { color: var(--ok); border-color: var(--ok); }
.verdict .tag.declined { color: var(--err); border-color: var(--err); }
.verdict .tag.flat { color: var(--sub); border-color: var(--line); }
.verdict .tag.insufficient { color: var(--warn); border-color: var(--warn); }
.verdict .tag.baseline { color: var(--data-trace); border-color: var(--data-trace); }

/* W2 Drills & Packs: pack cards -> drill rows with engine-true geometry. */
blockquote.struggle {
  margin: var(--sp-2) 0 var(--sp-3); padding: var(--sp-2) var(--sp-3);
  border-left: 3px solid var(--data-trace); color: var(--sub); font-style: italic;
}
.drill-row { display: flex; gap: var(--sp-4); padding: var(--sp-3) 0;
  border-top: 1px solid var(--line); }
.drill-row .thumb { flex: 0 0 auto; }
.drill-row .drill-body { flex: 1; min-width: 0; }
.drill-name { font-size: var(--fs-3); }
.drill-row canvas { display: block; }
.drill-row p { margin: var(--sp-2) 0 0; }

/* Tranche 4 console additions: preset gallery + named inputs. */
.tile.preset { min-width: 150px; }
.tile.preset.active { border-color: var(--accent); }
.tile.preset .act { margin-top: var(--sp-1); padding: 3px 10px; min-height: 0; }
/* A control this sheet never met. app.js creates BARE <input>s — the invite
   form, the delete-my-data confirm, the venue tints — and bare <select>s in the
   serve console, none of them carrying a class, so they fell through to the
   browser's own 13.33px light-mode widget inside a dark card. Same bug family as
   the unstyled <a class="act"> above: key off the TYPE, and the next bare field
   is dressed the moment it is made.

   `.layout` IS THE FENCE AND IT IS LOAD-BEARING. The seven standalone phone
   pages in tools/ttweb/service.py link THIS sheet and have no .layout, and the
   invitation page dresses a bare `input {}` at 18px. An element+attribute
   selector here would outrank that and shrink a live player's sign-in field
   below the 16px at which iOS stops zooming on focus — damage in a file nobody
   edited. Shell rules stay inside the shell; do not "tidy" this to a bare
   `input {}`. */
.layout input:not([type]), .layout input[type="text"], .layout input[type="email"],
.layout input[type="number"], .layout input[type="search"],
.layout input[type="password"], .layout select, .layout textarea {
  color: var(--fg); background: var(--inset); border: 1px solid var(--line);
  border-radius: var(--radius-s); padding: var(--sp-2);
  font: inherit; font-size: var(--field-fs); min-height: var(--tap); max-width: 100%;
}
input.preset-name, select.preset-select {
  background: var(--inset); border: 1px solid var(--line); color: var(--fg);
  border-radius: var(--radius-s); padding: 6px 10px; font: inherit;
  font-size: var(--field-fs);
}
.panel-vr input.preset-name { min-height: 48px; }  /* typing via system keyboard */
/* Every CLASSED field above already had a laser floor; the bare ones had none
   (measured 21px in the headset). The headset does not type into these
   (ADR-0056), but it can tap one to read what it says, and a 21px target is not
   tappable with a ray. */
.panel-vr input, .panel-vr select, .panel-vr textarea { min-height: 48px; }

/* Paddle & Table: a calibration tile carries a name FIELD plus up to four
   verbs, so it is wider than a preset tile and stacks its buttons rather than
   letting them wrap mid-row. */
.tile.calib { min-width: 230px; }
.tile.calib.active { border-color: var(--accent); }
.tile.calib input.preset-name { width: 100%; margin-top: var(--sp-2); }
.calib-actions { display: flex; flex-wrap: wrap; gap: var(--sp-2);
  margin-top: var(--sp-2); }
.calib-actions .act { padding: 3px 10px; min-height: 0; }
.panel-vr .calib-actions .act { min-height: 48px; }
/* The preset tile's action had no such twin, so a hand-narrowed panel would take
   the 44px thumb floor from the block at the foot of this sheet. 48 is the
   laser's. */
.panel-vr .tile.preset .act { min-height: 48px; }

/* W1 live machine console: slider rows mirroring the in-VR ranges. */
.slider-row { display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) 0; }
.slider-row .slider-label { min-width: 84px; color: var(--sub); }
.slider-row input[type="range"] { flex: 1; accent-color: var(--accent); }
.slider-row .slider-val { min-width: 92px; text-align: right; }
.panel-vr .slider-row input[type="range"] { height: 32px; }

/* ---- VR panel mode (ADR-0062 P1): the same shell served at ?panel=vr, tuned
   for the in-headset WebUI quad. Laser-pointer targets need >=48px; text is
   read at ~1.4m on a low-DPI-per-degree panel, so everything steps up one
   size. Presentation only — never gate behaviour on .panel-vr; capability
   gating stays server-side. */
.panel-vr body { font-size: 19px; }
.panel-vr header { padding: 18px 26px; }
.panel-vr h1 { font-size: 23px; }
.panel-vr .pill, .panel-vr #who { font-size: 15px; }
/* P4b live finding (2026-07-19): the OS spawns the panel NARROW and the
   two-pane desktop layout overflows/clips. In panel mode go SINGLE
   COLUMN: nav becomes a horizontal chip row on top, the content pane
   takes the full width below — fits any panel size the shell gives us. */
/* Single column only while the panel is NARROW. The shell used to spawn it
   at 400x640dp (portrait, phone-shaped) where the two-pane layout clipped;
   with the manifest size hint it now opens at 1280x800dp landscape, which
   has room for the sidebar. Width decides, not the mode — the user can
   resize the panel by hand at any time, and this follows them. */
/* Those four blocks were scoped to the panel class and now live UNSCOPED, at the
   foot of this sheet under "NARROW SURFACES". Measured: at the 1280x800dp the
   manifest now asks for, and at the 960 the streamed panel pins clientWidth to,
   none of them fired — dead in the headset every day, and unreachable on a
   phone, which has no such class. Width decides, not the mode: the note above
   always said so; only the selectors disagreed. */

/* NOT part of that move — this rule was never inside a media block, it merely
   sat between two of them, and cutting "the region" as one range drops it
   silently. It keeps the perspective switcher on the laser floor at every panel
   width.
   AND A LINT HOLE WORTH KNOWING: test_panel_mode.py's 48px assertion inspects
   only the rules that SURVIVE — a panel-mode floor that disappears leaves the
   list shorter and still all >= 48, so the gate stays green. Deletions in this
   region must be audited by hand. */
.panel-vr #persp { min-height: 48px; font-size: 15px; }

/* `.panel-vr main { padding: 12px }` used to stand here and was dead code: the
   rule four lines below restates the same selector at the same specificity and
   wins, so a narrow panel never got its tight padding. It does now, from the
   mode block at the foot of this sheet. */
.panel-vr nav button { padding: 15px 16px; min-height: 56px; border-radius: 12px; }
.panel-vr nav button .d { font-size: 13px; margin-top: 4px; }
.panel-vr .navsection { font-size: 13px; }
.panel-vr main { padding: 26px 30px; }
.panel-vr main h2 { font-size: 21px; }
.panel-vr .card { padding: 22px 24px; margin-bottom: 18px; border-radius: 14px; }
.panel-vr .card h3 { font-size: 17px; }
.panel-vr pre { font-size: 15px; padding: 14px; }
.panel-vr .sub, .panel-vr .gated { font-size: 14px; }
.panel-vr .act { padding: 14px 26px; min-height: 52px; border-radius: 12px; }
.panel-vr .tile .v { font-size: var(--fs-5); }
.panel-vr .badge, .panel-vr .verdict .tag { font-size: var(--fs-1); }
.panel-vr footer { display: none; }  /* prose footer is phone/desktop chrome */
/* ...which is why #build sits in the HEADER (app.js showBuild): in the headset
   there is no title bar, no URL and no footer, so this is the only place a
   version can be read off the screen. Small and dim — it is for bug reports,
   not for the player. */
.panel-vr #build { font-size: 13px; opacity: 0.75; }
#vrnote {
  position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%);
  background: var(--panel); border: 1px solid var(--accent); color: var(--fg);
  padding: 12px 22px; border-radius: 12px; font-size: 17px;
  opacity: 0; pointer-events: none; transition: opacity var(--t-view) var(--ease);
}
#vrnote.show { opacity: 1; }

/* Remote-action progress (actWithProgress in app.js).
   These controls drive a HEADSET over a com lane, so a press and its effect
   are seconds apart. The state lives on the button itself so the feedback is
   where the eye already is, rather than only in a note underneath. */
/* box-shadow rings, not border-color: .act sets `border: 0`, so a border-colour
   rule would never render. */
.act[data-state="sending"],
.act[data-state="sent"] { cursor: progress; }
.act[data-state="sent"]    { box-shadow: 0 0 0 2px var(--accent); }
.act[data-state="applied"] { box-shadow: 0 0 0 2px var(--ok); }
.act[data-state="applied"]::after { content: "✓"; }  /* the space is the rule's `gap` now */
.act[data-state="error"]   { box-shadow: 0 0 0 2px var(--err); }

/* ---------------------------------------------------------------------------
   Streamed-panel chrome (ADR-0082)

   The panel bar carries the only in-page way out ("Back to game"), so it must
   survive scrolling. It used to be an ordinary first child of <body> and went
   off the top with everything else, which left Y as the sole exit.

   Sticky, not fixed: sticky participates in flow, so it cannot overlap the
   first screenful of content the way a fixed bar does — and on a surface with
   no keyboard and no browser chrome, overlapped content is unrecoverable.

   Worth stating plainly because it is the whole answer to "how do fixed
   elements work in a streamed panel": they work EXACTLY as they do in any
   browser. We stream the rendered page, so the page's own layout does the
   work. Nothing about VR needs special handling here — the bar simply was
   not sticky.
   --------------------------------------------------------------------------- */
.panel-vr #vrpanelbar {
  position: sticky;
  top: 0;
  z-index: 50;
}

/* ADR-0104 D1: the Collection card's gap map — status colours only, words
   in the cells (never a bucket key). never = err, thin = warn, ok = ok. */
.gapgrid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px; margin: 6px 0; }
.gapcell { font-size: var(--fs-0); padding: 3px 6px; border-radius: var(--radius-s);
  border: 1px solid var(--sub); color: var(--sub); text-align: center; }
.gapcell.never { border-color: var(--err); color: var(--err); }
.gapcell.thin { border-color: var(--warn); color: var(--warn); }
.gapcell.ok { border-color: var(--ok); color: var(--ok); }
.gaprow { display: flex; align-items: center; gap: 6px; margin: 3px 0; }
.gapdot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; background: var(--sub); }
.gapdot.never { background: var(--err); }
.gapdot.thin { background: var(--warn); }
.gapdot.ok { background: var(--ok); }

/* Preferences (2026-09-05): one card per category, and a chip strip on top
   that jumps to each — the page is long in the headset. The chips SCROLL
   rather than link: the hash is the router's (app.js renderFromHash). */
.prefnav { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: 0 0 var(--sp-4); }
.prefnav button {
  background: var(--inset); color: var(--fg); border: 1px solid var(--line);
  border-radius: 999px; padding: 6px 14px; font: inherit; font-size: var(--fs-1);
  cursor: pointer;
  transition: border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.prefnav button:hover { border-color: var(--accent); color: var(--accent); }
.panel-vr .prefnav button { min-height: 48px; padding: 8px 18px; font-size: var(--fs-2); }
/* Preference rows: many chips and real labels — wrap instead of clipping (the
   machine console's .slider-row stays single-line: one slider each). In the
   headset the chips are laser targets, so they get the 48px floor too. */
.card.pref .slider-row { flex-wrap: wrap; align-items: baseline; row-gap: var(--sp-1); }
.card.pref .slider-row .slider-label { flex: 0 0 auto; min-width: 200px; }
.card.pref .slider-row .badges { flex: 1 1 320px; flex-wrap: wrap; row-gap: var(--sp-1); margin-left: 0; }
.card.pref .slider-row input[type="range"] { flex: 1 1 200px; }
.panel-vr .card.pref .slider-row .badge { min-height: 48px; padding: 8px 16px;
  font-size: var(--fs-1); display: inline-flex; align-items: center; }

/* The Profile section (ADR-0132 D8): who you are, your seats, your accounts. */
.you { display: flex; align-items: center; gap: var(--sp-4); margin: var(--sp-2) 0; }
.avatar { width: 56px; height: 56px; border-radius: 50%; object-fit: cover; flex: 0 0 auto;
  background: var(--inset); border: 1px solid var(--line); color: var(--sub);
  display: inline-flex; align-items: center; justify-content: center; font-size: var(--fs-4); }
.you .name { font-size: var(--fs-3); font-weight: 600; margin-bottom: var(--sp-1); }
/* "Spin Ma…", "Meta Hor…", "prof-3f9a21…" were all clipped at the column edge in
   the owner's screenshot. The 56px avatar plus a 16px gap take 72 of a 90px card
   box; without min-width:0 the text column refuses to give any back. The
   offending box is a flex ITEM that is an UNCLASSED <div>, reachable only as a
   child. */
.you > div { min-width: 0; }
/* `white-space: normal` is a targeted escape from .badge's nowrap above: the
   source line here is a SENTENCE ("signed in with e-mail link, Google"), not a
   chip. */
.you .name, .you .badge, .you .provenance { overflow-wrap: anywhere; white-space: normal; }
.linkcode { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 44px;
  letter-spacing: .25em; padding: var(--sp-3) 0 var(--sp-1); color: var(--fg); }
.panel-vr .linkcode { font-size: 60px; }
.seats { display: flex; flex-direction: column; gap: var(--sp-2); margin-top: var(--sp-2); }
.seat { display: flex; align-items: center; gap: var(--sp-3); }
.seat .k { color: var(--sub); min-width: 72px; }
/* The seat's label span is UNCLASSED and pinned positionally by the browser
   gate, so it can only be reached as a child. `.seat > span` does NOT outrank
   `.seat .k`, so the desktop's 72px key column survives. */
.seat > span { min-width: 0; overflow-wrap: anywhere; }
/* Your data (ADR-0132 P4): a wider key column so four store names line up, and the
   last-synced badge held to the right edge so the eye reads one column of dates. */
.seat.data .k { min-width: 160px; }
.seat.data .badge { margin-left: auto; }
/* Players on this headset (ADR-0132 D4 + D8, runbook S6): one chip per remembered
   profile, the active one lit through aria-pressed (the attribute the table-grip group
   lights on), and the PIN pad. THE PAD IS ONE COMPONENT (app.js pinPad) for the
   headset's switch and the phone's Set / reset PIN; its keys are var(--tap) — 48 px,
   the number D4 names — on EVERY surface, not only under the panel class, so a 44 px
   phone floor never shrinks a PIN key. The lit chip is the accent ring the settings
   chips use; every colour is a token. */
.players { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: var(--sp-2) 0; }
.player-chip {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  min-height: var(--tap); padding: var(--sp-1) var(--sp-3) var(--sp-1) var(--sp-1);
  background: var(--inset); color: var(--fg); border: 1px solid var(--line);
  border-radius: 999px; font: inherit; cursor: pointer; max-width: 100%;
  transition: border-color var(--t-fast) var(--ease);
}
.player-chip:hover { border-color: var(--accent); }
.player-chip[aria-pressed="true"] { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.player-chip[aria-pressed="true"] .player-name { color: var(--accent); }
.player-chip:disabled { cursor: progress; opacity: .7; }
.player-chip .avatar { width: 32px; height: 32px; font-size: var(--fs-2); }
.player-who { display: inline-flex; flex-direction: column; align-items: flex-start; gap: 2px; min-width: 0; }
.player-name { font-weight: 600; text-align: left; overflow-wrap: anywhere; white-space: normal; }
.pinpad { margin: var(--sp-3) 0; max-width: 320px; }
.pinpad-shown { font-family: var(--font-mono); font-size: var(--fs-5); letter-spacing: .3em;
  min-height: 1.4em; padding: var(--sp-2) 0; color: var(--fg); }
.pinpad-keys { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-2); }
.pinkey {
  min-height: var(--tap); min-width: var(--tap); font: inherit; font-size: var(--fs-4);
  font-variant-numeric: tabular-nums; background: var(--inset); color: var(--fg);
  border: 1px solid var(--line); border-radius: var(--radius-s); cursor: pointer;
}
.pinkey:hover { border-color: var(--accent); }
.pinkey.ok { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 600; }
.pinkey:disabled { opacity: .5; cursor: not-allowed; }
.pinpad-cancel { margin-top: var(--sp-2); }
.panel-vr .pinkey { min-height: 56px; font-size: var(--fs-5); }

/* ===========================================================================
   NARROW SURFACES — a phone browser, and a VR panel a hand has made small
   ===========================================================================
   THE SEAM IS 720px and it is the only one in this sheet. At 721px and up
   nothing below applies and the two-pane operator console is exactly what it
   was. It must stay strictly under 960: tools/ttpanel/tiers.py pins the
   streamed panel's documentElement.clientWidth at BASE_WIDTH = 960 "so no media
   query reflows", and cdp.launch defaults to 960 — the width three browser
   suites lay out at. The headset spawns its panel at 1280x800dp
   (android_ball_machine/AndroidManifest.xml), so none of this fires there.
   LAST IN THE FILE ON PURPOSE: a media query adds no specificity, so
   `nav { width: auto }` beats `nav { width: 210px }` above only by source order.

   WIDTH OWNS LAYOUT; PANEL MODE OWNS SCALE. Column count, nav orientation,
   gutters, wrapping rows and the THUMB floor are decided here, for whoever is
   narrow. The type step-up, the 48px laser floor and the chrome the quad has no
   room for stay on the panel-mode rules above, which are more specific and win
   wherever the two disagree. That is what the note above the panel section
   always said — "Width decides, not the mode" — and the selectors finally agree.

   WHY THIS BLOCK EXISTS: a phone browser carries no panel class, so it was the
   one audience with no responsive rule of any kind. Measured at 390px: the 210px
   sidebar took 54% of the screen, `main` got 180px and a card's text box was
   90px. Headings broke mid-phrase ("Phone &" / "computer"), the trust badge
   clipped to "reversible-lo", the seat rows read "browser | phone browser" with
   their buttons off-screen, and every blue action sat past the right edge. The
   header alone made the document 522px wide, which is why the phone zoomed the
   whole page out and everything looked oversized.

   WRITING RULE FOR THIS BLOCK: never spell the dot-prefixed panel-mode class in
   a COMMENT here. tools/ttweb/test_panel_mode.py finds that literal and then
   walks brace-blind to the next `min-height: Npx` demanding N >= 48, so prose
   above a 44px floor fails the gate on a selector that does not exist. The block
   is structurally safe because every thumb floor is var(--tap), which the
   `(\d+)px` capture cannot match; that immunity belongs to the token.
   =========================================================================== */
@media (max-width: 720px) {
  :root { --field-fs: var(--fs-3); }   /* 17px — see the note on the token */

  /* ---- frame: six declarations ---------------------------------------- */
  .layout { flex-direction: column; }
  /* main STOPS being its own scroll container. `overflow: auto` did three bad
     things at once: it zeroed main's automatic minimum size, which is how a
     210px nav won a 390px screen outright; it swallowed the overflow, so the
     clipped blue buttons were not scrollable but simply gone (measured
     main.scrollWidth 1517 against a 180px client box while the DOCUMENT reported
     no overflow at all — which is why the one responsive test this repo had
     could never have caught it); and on iOS an inner scroller pins content to
     100vh rather than the visual viewport, defeating the browser's own
     chrome-hiding. Visible turns an invisible clip into a failure the eye and
     the new gate can both see. */
  main { overflow: visible; min-width: 0; padding: var(--sp-4) var(--sp-3); }
  header { padding: var(--sp-3) var(--sp-4); }
  /* A header item sized by its own text cannot push the row wider than the
     screen. From the paddle track's narrow pass (8048112e), which found the
     same header the same day. */
  header > * { max-width: 100%; overflow-wrap: anywhere; }
  footer { padding: var(--sp-3) var(--sp-4); }
  /* #build is diagnostics, not player content, and it is the widest unshrinkable
     item in the header at 179px. Fine print here, so the wrapped header costs
     one short row instead of two. */
  #build { font-size: var(--fs-0); margin-right: 0; }
  #who { margin-right: var(--sp-2); }

  /* ---- the nav is a RAIL ---------------------------------------------- */
  /* 14 entries in four labelled groups. Three shapes, all measured at 390px:
       sidebar   210px wide = 54% of the screen, card interior 90px  (before)
       wrapped  ~321px tall = 38% of the viewport, on EVERY screen, re-presented
                              after every navigation
       rail      ~61px tall =  7%, content starts immediately below
     A thumb flicks a rail; it is the gesture a phone is best at. The cost is
     discovery — the strip is ~1887px long, so about 3.5 of 14 chips are visible
     at rest — and the two answers are the edge fade below and markNav()'s
     scrollIntoView in app.js. IF THE OWNER WOULD RATHER SEE EVERY DESTINATION
     the retreat is one declaration: `flex-wrap: wrap; overflow-x: visible` and
     drop the mask, at a measured cost of ~260px of chrome above every screen.
     Written down so nobody re-litigates it from scratch. */
  nav {
    width: auto; flex-direction: row; flex-wrap: nowrap;
    gap: var(--sp-2); padding: var(--sp-2) var(--sp-3);
    border-right: 0; border-bottom: 1px solid var(--line);
    overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    /* Stays reachable at the bottom of a long Preferences page, now that the
       page itself scrolls rather than a box inside it. */
    position: sticky; top: 0; z-index: 20; background: var(--bg);
    /* The strip visibly RUNS ON past the edge. A mask, not an overlay child: the
       rail scrolls, and an overlay would scroll away with it. `black` and
       `transparent` are keywords — the palette lint scans this file for
       #rrggbb, and a fade is not a colour decision. */
    -webkit-mask-image: linear-gradient(to right, black 0 calc(100% - 20px), transparent);
            mask-image: linear-gradient(to right, black 0 calc(100% - 20px), transparent);
  }
  nav button {
    flex: 0 0 auto; scroll-snap-align: start; white-space: nowrap;
    min-height: var(--tap); padding: var(--sp-2) var(--sp-4);
    background: var(--inset); border: 1px solid var(--line); border-radius: 999px;
  }
  nav button.active { border-color: var(--accent); }
  /* A chip is TITLE ONLY: the summary line doubles every chip's height and
     halves how many fit. This used to be a panel-mode rule; width decides it
     now, for everyone. */
  nav button span.d { display: none; }
  nav .navsection { display: inline-flex; align-items: center; flex: 0 0 auto;
                    white-space: nowrap; padding: 0; margin: 0 2px 0 var(--sp-2); }

  /* ---- a hand-held type ramp ------------------------------------------ */
  /* The reported "body text far too large" was the COLUMN, not the prose: at
     90px of measure anything looks huge. Prose is untouched. The card title was
     14px — SMALLER than body, which reads as a label on a wide page and as
     nothing at all in a column you hold. */
  main h2 { font-size: var(--fs-4); }
  .card { padding: var(--sp-3) var(--sp-4); }
  .card h3 { font-size: var(--fs-3); }

  /* ---- the TOUCH floor --------------------------------------------------- */
  /* var(--tap). The panel-mode rules above are more specific and keep their own
     numbers where they are larger (56px nav buttons, 52px actions).
     button.badge ONLY: app.js makes the distinction itself — a settings choice
     chip, a console toggle and "Feed this" are <button class="badge">, while a
     trust chip is a <span> nobody presses. 126 of the former measured 18px tall
     on the screenshotted page. */
  .act, button.badge, .prefnav button, #persp { min-height: var(--tap); }
  #persp { max-width: 100%; }
  .tile.preset .act, .calib-actions .act { min-height: var(--tap); padding: 3px 10px; }
  /* The saved-paddle editor is a phone surface in its own right; from the
     paddle track's narrow pass (8048112e), whose own gate measures these. */
  .paddle-actions button, .paddle-swatch, .paddle-field input[type="text"],
  .paddle-field select, .paddle-custom-color input { min-height: var(--tap); }
  /* Its learning page too — the same gate walks these. */
  .paddle-learning button, .paddle-learning select { min-height: var(--tap); }
  /* A range is a DRAG target and had no height at all outside panel mode. */
  .slider-row input[type="range"] { height: var(--tap); }

  /* ---- rows written for a wide card ------------------------------------ */
  /* Every one stacks by WRAPPING. The browser gates read this DOM positionally
     (`.seat span:nth-child(2)`, `> .sub:last-child`, `.linkcode + .sub`), so a
     new wrapper element is not available — CSS is the only tool here. */
  .seat, .verdict, .slider-row, .drill-row { flex-wrap: wrap; row-gap: var(--sp-2); }
  /* The wide key column and the right-aligned date are a wide-page intent; a
     340px column cannot honour it. */
  .seat .k, .seat.data .k { min-width: 0; }
  .seat.data .badge { margin-left: 0; }
  .seat .act { flex: 1 1 auto; }
  /* Caption ABOVE the field — what the standalone phone pages in service.py
     already do. The villain was `flex: 0 0 auto`, not the 200px: a box that
     cannot shrink and is sized by its own text put a label at x=580 in a 390px
     viewport. */
  .slider-row .slider-label,
  .card.pref .slider-row .slider-label { flex: 1 1 100%; min-width: 0; }
  .slider-row .slider-val { min-width: 0; text-align: left; }
  .card.pref .slider-row .badges { flex-basis: 100%; }
  .card.pref .slider-row input[type="range"] { flex: 1 1 100%; }
  .drill-row .drill-body { flex-basis: 100%; }
  .verdict .shot { flex: 1 0 100%; min-width: 0; }
  .tile, .tile.preset, .tile.calib { min-width: 0; }
  /* The six-digit pairing code is a CORRECTNESS surface — split across two lines
     it is misread. 44px at .25em tracking is ~240px; 28px fits. */
  .linkcode { font-size: var(--fs-5); }

  /* ---- things genuinely wider than a phone ----------------------------- */
  /* `table-layout: fixed` makes the table obey the column instead of the column
     obeying the table; the cells' `overflow-wrap: anywhere` above does the rest.
     NOT `display: block; overflow-x: auto` — that contains it too, but it strips
     table semantics from some screen readers and the raw settings doc nests
     tables inside cells. */
  table { table-layout: fixed; }
  /* Fixed-attribute canvases (the sweep chart at 420px, the drill previews and
     thumbs, the ball-library thumbs) have no CSS width, and with main visible
     they would drag the whole page sideways rather than clip invisibly.
     `.avatar` and `#qr svg` both outrank this, so the 56px avatar and the 260px
     sign-in code keep their sizes. A canvas drawn at its attribute resolution
     and shown smaller loses crispness, never geometry. */
  canvas, img, svg { max-width: 100%; height: auto; }
}

/* ---- WHAT THE MODE STILL DECIDES AT A NARROW WIDTH ------------------------
   Small on purpose. Everything above is width. These four are about the INPUT
   DEVICE and the panel's own chrome, not about pixels. */
@media (max-width: 720px) {
  /* 1. A LASER IS NOT A THUMB. Under the seam every other surface gets the chip
        RAIL. A pointer has to drag a ray precisely along a moving strip, which
        is the worst thing you can ask of one, so a hand-narrowed panel keeps the
        WRAPPED block it has today — a height a panel can afford (measured 216px
        of an 800px-tall panel at 600px wide) and never a drag.
     2. THE PANEL ALREADY HAS A STICKY BAR at top:0 (#vrpanelbar, ADR-0082) and
        it carries the only in-page way out. Two stickies at one offset overlap,
        the bar wins on z-index, and the nav hides behind the exit. */
  .panel-vr nav {
    flex-wrap: wrap; overflow-x: visible; position: static;
    -webkit-mask-image: none; mask-image: none;
  }
  /* 3. The tight padding the panel's own narrow layout always meant to have. It
        was written once and then restated four lines later at equal specificity,
        so it never applied — see the deletion note above. */
  .panel-vr main { padding: var(--sp-4) var(--sp-3); }
  /* 4. 60px of pairing code does not fit a narrow panel either. */
  .panel-vr .linkcode { font-size: var(--fs-5); }
  /* 5. A card the panel sizes for a 1280px quad is too padded for a 340px one.
        Kept from the paddle track's own narrow pass (8048112e), which reached
        the same conclusion independently. */
  .panel-vr .card { padding: var(--sp-3) var(--sp-4); }
}
/* Body preferences and guided movement recording share the menu's cards. */
.avatar-page { max-width: 72rem; }
.avatar-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem 1.5rem; margin: 1rem 0; }
.avatar-field label { display: flex; flex-direction: column; gap: .4rem; }
.avatar-field input, .avatar-field select { width: 100%; min-height: var(--tap); box-sizing: border-box; }
.avatar-field .sub { margin: .35rem 0; }
.avatar-actions { display: flex; flex-wrap: wrap; gap: .6rem; }
.avatar-instruction { font-size: 1.25rem; line-height: 1.5; }
.avatar-hold { font-weight: 600; }
.avatar-confirm { display: block; margin: 1rem 0; }
.avatar-confirm input { width: auto; }
.avatar-timing > summary { cursor: pointer; padding: 1rem; }
@media (max-width: 720px) { .avatar-fields { grid-template-columns: 1fr; } }
/* The room focus LAB (C30, room_focus.js): a dev grid of layers x occasion columns. The column the
   headset is in NOW is outlined from the heartbeat; a lab-edited cell carries a warn edge; a value a
   layer cannot honour is a disabled chip. Wide tables scroll inside their card, never the page. */
.room-focus { max-width: 76rem; }
.room-focus-rowline { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.room-focus-rowline .badges { margin-left: 0; }
.room-focus button.badge { background: none; font: inherit; font-size: var(--fs-0); cursor: pointer; }
.room-focus button.badge:disabled { cursor: not-allowed; opacity: .55; }
.room-focus button.badge.room-focus-refused { text-decoration: line-through; opacity: .45; }
.room-focus-grid-wrap { overflow-x: auto; }
.room-focus-grid { min-width: 46rem; }             /* a phone scrolls the grid, never squeezes it */
.room-focus-grid th.room-focus-layer { min-width: 14rem; font-weight: 500; color: var(--fg); }
.room-focus-grid .room-focus-cost { color: var(--data-mark); }
.room-focus-grid td.room-focus-cell .badges { margin-left: 0; }
.room-focus-grid .room-focus-now { display: none; margin-left: var(--sp-1); color: var(--accent);
  font-size: var(--fs-0); }
.room-focus-grid th.is-now .room-focus-now { display: inline; }
.room-focus-grid th.is-now, .room-focus-grid td.is-now { background: var(--inset); }
.room-focus-grid td.is-edited { box-shadow: inset 3px 0 0 var(--warn); }
.room-focus-facts { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); margin-bottom: var(--sp-2); }
.room-focus-fact { display: inline-flex; gap: var(--sp-1); align-items: baseline; }
.room-focus-value { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.room-focus-level { display: grid; grid-template-columns: 9rem minmax(6rem, 16rem) 3rem;
  gap: var(--sp-2); align-items: center; }
.room-focus-track { display: block; height: 8px; border-radius: 999px; overflow: hidden;
  background: var(--inset); border: 1px solid var(--line); }
.room-focus-fill { display: block; height: 100%; background: var(--accent);
  transition: width var(--t-view) var(--ease); }
.room-focus-bucket-table { min-width: 52rem; }
.room-focus-bucket-table td { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.room-focus-copy-fields { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: var(--sp-2) 0; }
.room-focus-copy-fields input { color: var(--fg); background: var(--inset); border: 1px solid var(--line);
  border-radius: var(--radius-s); padding: var(--sp-1) var(--sp-2); font: inherit; min-width: 12rem; }
.room-focus-cpp { width: 100%; box-sizing: border-box; background: var(--inset); color: var(--fg);
  border: 1px solid var(--line); border-radius: var(--radius-s); padding: var(--sp-2);
  font-family: var(--font-mono); font-size: var(--fs-0); }
